Cálculo numérico con Numpy ========================== Aunque Python tiene varios tipos de datos estructurados, en la práctica no son nada adecuados para cálculo numérico. Veamos un ejemplo de un cálculo numérico básico empleando listas: .. code:: ipython3 In [1]: lista = list(range(5)) # Lista de numeros de 0 a 4 In [2]: print(lista*2) [0, 1, 2, 3, 4, 0, 1, 2, 3, 4] In [3]: print(lista*2.5) --------------------------------------------------------------------------- TypeError Traceback (most recent call last) /home/japp/ in () TypeError: can't multiply sequence by non-int of type 'float' En el ejemplo anterior vemos cómo al multiplicar una lista por un número entero, el resultado es concatenar la lista original tantas veces como indica el número, en lugar de multiplicar cada uno de sus elementos por este número, que es lo a veces cabría esperar. Es más, al multiplicarlo por un número no entero da un error, al no poder crear una fracción de una lista. Si quisiéramos hacer esto, se podría resolver iterando cada uno de los elementos de la lista con un bucle ``for``, por ejemplo: .. code:: ipython3 In [4]: lista_nueva = [i*2.5 for i in lista] In [5]: print(lista_nueva) [0.0, 2.5, 5.0, 7.5, 10.0] aunque esta técnica es ineficiente y lenta, sobre todo cuando queremos evaluar funciones, polinomios o cualquier otra operación matemática que aparece en cualquier problema científico. Cuando realmente queremos hacer cálculos con listas de números, debemos usar los arrays. El módulo `numpy `__ nos da acceso a los arrays y a una gran cantidad de métodos y funciones aplicables a los mismos. Naturalmente, ``numpy`` incluye funciones matemáticas básicas similares al módulo ``math``, las completa con otras más elaboradas y además incluye algunas utilidades de números aleatorios, ajuste lineal de funciones y muchas otras. Para trabajar con ``numpy`` y los arrays, importamos el módulos de alguna manera: .. code:: ipython3 In [6]: import numpy # Cargar el modulo numpy, o bien In [7]: import numpy as np # cargar el modulo numpy, llamándolo np, o bien In [8]: from numpy import * # cargar todas funciones de numpy Si cargamos el módulo solamente, accederemos a las funciones como ``numpy.array()`` o ``np.array()``, según cómo importemos el módulo; si en lugar de eso importamos todas las funciones, accederemos a ellas directamente (e.g. ``array()``). Por comodidad usaremos por ahora esta última opción, aunque muy a menudo veremos que usa la notación ``np.array()``, especialmente cuando trabajamos con varios módulos distintos. Un array se puede crear explícitamente o a partir de una lista de la forma siguiente: .. code:: ipython3 In [9]: x = array([2.0, 4.6, 9.3, 1.2]) # Creacion de un array directamente In [10]: notas = [ 9.8, 7.8, 9.9, 8.4, 6.7] # Crear un lista In [11]: notas = array(notas) # y convertir la lista a array Existen métodos para crear arrays automáticamente: .. code:: ipython3 In [12]: numeros = arange(10.) # Array de numeros (floats) de 0 a 9 In [13]: print(numeros) [ 0. 1. 2. 3. 4. 5. 6. 7. 8. 9.] In [14]: lista_ceros = zeros(10) # Array de 10 ceros (floats) In [15]: print(lista_ceros) [ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] In [16]: lista_unos = ones(10) # Array de 10 unos (floats) In [17]: print(lista_unos) [ 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.] In [18]: otra_lista = linspace(0, 30, 8) # Array de 8 números, de 0 a 30 ambos incluidos In [19]: print(otra_lista) [ 0. 4.28571429 8.57142857 12.85714286 17.14285714 21.42857143 25.71428571 30. ] Los arrays se indexan prácticamente igual que las listas y las cadenas de texto; aquí hay algunos ejemplos: .. code:: ipython3 In [18]: print(numeros[3:8]) # Elementos desde el tercero al septimo [ 3. 4. 5. 6. 7.] In [19]: print(numeros[:4]) # Elementos desde el primero al cuarto [ 0. 1. 2. 3.] In [20]: print(numeros[5:]) # Elementos desde el quinto al final [ 5. 6. 7. 8. 9.] In [21]: print(numeros[-3]) # El antepenúltimo elemento (devuelve un elemento, no un array) 7. In [24]: print(numeros[:]) # Todo el array, equivalente a print(numeros) [ 0. 1. 2. 3. 4. 5. 6. 7. 8. 9.] In [25]: print(numeros[2:8:2]) # Elementos del segundo al septimo, pero saltando de dos en dos [ 2. 4. 6.] Al igual que las listas, podemos ver el tamaño de un array unidimensional con ``len()``, aunque la manera correcta de conocer la forma de un array es usando el método ``shape()``: .. code:: ipython3 In [28]: print(len(numeros)) 10 In [29]: print(numeros.shape) (10,) Nótese que el resultado del método ``shape()`` es una tupla, en este caso con un solo elemento ya que el array números es unidimensional. Si creamos un array con ``np.arange()`` usando un número entero, el array que se creará será de enteros. Es posible cambiar todo el array a otro tipo de dato (como a float) usando el método ``astype()``: .. code:: ipython3 In [31]: enteros = np.arange(6) In [32]: print(enteros) [0 1 2 3 4 5] In [33]: type(enteros) Out[33]: In [34]: type(enteros[0]) Out[34]: In [35]: decimales = enteros.astype('float') In [36]: type(decimales) Out[36]: In [37]: type(decimales[0]) Out[37]: In [38]: print(decimales) [ 0. 1. 2. 3. 4. 5.] In [38]: print(decimales.shape) # Forma o tamaño del array (6,) Operaciones con arrays ---------------------- Los arrays permiten hacer operaciones aritméticas básicas entre ellos en la forma que uno esperaría que se hicieran, es decir, haciéndolo elemento a elemento; para ello ambos arrays deben tener siempre la misma longitud, por ejemplo: .. code:: ipython3 In [39]: x = array([5.6, 7.3, 7.7, 2.3, 4.2, 9.2]) In [40]: print(x+decimales) [ 5.6 8.3 9.7 5.3 8.2 14.2] In [41]: print(x*decimales) [ 0. 7.3 15.4 6.9 16.8 46. ] In [42]: print(x/decimales) [ Inf 7.3 3.85 0.76666667 1.05 1.84] Como podemos ver las operaciones se hacen elemento a elemento, por lo que ambas deben tener la misma forma (``shape()``). Fíjense que en la división el resultado del primer elemento es indefinido/infinito (Inf) debido a la división por cero. Varios arrays se pueden unir con el método ``np.concatenate()``, que también se puede usar para añadir elementos nuevos: .. code:: ipython3 In [44]: z = np.concatenate((x, decimales)) In [45]: print(z) [ 5.6 7.3 7.7 2.3 4.2 9.2 0. 1. 2. 3. 4. 5. ] In [46]: z = np.concatenate((x, [7])) In [47]: print(z) [ 5.6 7.3 7.7 2.3 4.2 9.2 7. ] Es importante fijarse que los arrays o listas a unir deben darse como un iterable (tupla, lista, array) como por ejemplo ``(x, [7])``, ``(x, [2,4,7])`` o ``(x, array([2,4,7]))``. Para añadir elementos, numpy tiene las funciones ``insert()`` y ``append()``, que funcionan de manera similar a sus equivalentes en listas, pero en este caso son funciones y no métodos que se aplican a un array, si no que el array en cuestión hay que darlo como parámetro: .. code:: ipython3 # Añadimos el elemento 100 al array z, al final In [55]: z = np.append(z, 100) In [56]: print(z) [ 5.6 7.3 7.7 2.3 4.2 9.2 7. 100. ] # Añadimos el elemento 200 al array z, en el tercer puesto (índice 2) In [57]: z = np.insert(z, 2, 200) In [58]: print(z) [ 5.6 7.3 200. 7.7 2.3 4.2 9.2 7. 100. ] Como se ve, a diferencia de las listas, el primer parámetro es el array y luego el elemento que se quiere añadir, en el caso de ``append()`` y el array, la posición y luego elemento a añadir en el caso de ``append()``. Esto se debe a que estas funciones **devuelven una copia del array** sin modificar el original como hacen los métodos de listas correspondientes. Si en lugar de un elemento a insertar se da una lista y otro array, añade todos los elementos de la lista (a ``append()`` habría que dar también una lista de posiciones, como segundo parámetro). .. code:: ipython3 # Creamos un nuevo array anhadiendo al array z, # los elementos -10, -20, -30 en las posiciones con indices 2, 4, -1, respectivamente In [61]: y = np.insert(z, [2, 4, -1], [-10, -20, -30]) In [61]: print(y) [ 5.6 7.3 -10. 200. 7.7 -20. 2.3 4.2 9.2 7. -30. 100. ] Además de las operaciones aritméticas básicas, los arrays de numpy tienen métodos o funciones específicas para ellas más avanzadas. Algunas de ellas son las siguientes: .. code:: ipython3 In [5]: z.max() # Valor máximo de los elementos del array Out[5]: 200.0 In [6]: z.min() # Valor mínimo de los elementos del array Out[6]: 2.3 In [7]: z.mean() # Valor medio de los elementos del array Out[7]: 38.14444444444444 In [8]: z.std() # Desviación típica de los elementos del array Out[8]: 64.29577679428289 In [9]: z.sum() # Suma de todos los elementos del array Out[9]: 343.29999999999995 In [16]: np.median(z) # Mediana de los elementos del array Out[16]: 7.3 Los métodos, que se operan de la forma ``z.sum()`` también pueden usarse como funciones de tipo ``sum(z)``, etc. Consultar el manual de numpy para conocer otras propiedades y métodos de los arrays o simplemente ver la “ayuda” de las funciones que quieran utilizar. Una gran utilidad de los arrays es la posibilidad de usarlos con datos booleanos (``True`` o ``False``) y operar entre ellos o incluso usarlos con arrays con números. Veamos algunos ejemplos: .. code:: ipython3 In [19]: A = array([True, False, True]) In [20]: B = array([False, False, True]) In [22]: A*B Out[22]: array([False, False, True], dtype=bool) In [29]: C = array([1, 2, 3]) In [30]: A*C Out[30]: array([1, 0, 3]) In [31]: B*C Out[31]: array([0, 0, 3]) En este ejemplo vemos cómo al multiplicar dos arrays booleanos es resultado es otro array booleano con el resultado que corresponda, pero al multiplicar los arrays booleanos con arrays numéricos, el resultado es un array numérico con los mismos elementos, pero con los elementos que fueron multiplicados por False iguales a cero. Tambíén es posible usar los arrays como índices de otro array y como índices se pueden usar arrays numéricos o booleanos. El resultado será en este caso un array con los elementos que se indique en el array de índices numérico o los elementos correspondientes a ``True`` en caso de usar un array de índices booleano. Veámoslo con un ejemplo: .. code:: ipython3 # Array con enteros de 0 a 9 In [37]: mi_array = np.arange(0, 100, 10) # Array de índices numericos con numeros de 0-9 de 2 en 2 In [38]: indices1 = np.arange(0, 10, 2) # Array de índices booleanos In [39]: indices2 = np.array([False, True, True, False, False, True, False, False, True, True]) In [40]: print(mi_array) [ 0 10 20 30 40 50 60 70 80 90] In [43]: print(mi_array[indices1]) [ 0 20 40 60 80] In [44]: print(mi_array[indices2]) [10 20 50 80 90] También es muy sencillo y más práctico crear arrays booleanos usando operadores lógicos y luego usalos como índices, por ejemplo: .. code:: ipython3 # Creamos un array usando un operador booleano In [50]: mayores50 = mi_array > 50 In [51]: print(mayores50) [False False False False False False True True True True] # Lo utilizamos como índices para seleccionar los que cumplen esa condición In [52]: print(mi_array[mayores50]) [60 70 80 90] Arrays multidimensionales ------------------------- Hasta ahora sólo hemos trabajado con arrays con una sola dimensión, pero ``numpy`` permite trabajar con arrays de más dimensiones. Un array de dos dimensiones podría ser por ejemplo un array que tuviera como elementos un sistema de ecuaciones o una imagen. Para crearlos podemos hacerlo declarándolos directamente o mediante funciones como ``np.zeros()`` o ``np.ones()`` dando como parámetro una tupla con la forma del array final que queramos; o también usando ``np.arange()`` y crear un array unidimensional y luego cambiar su forma. Veamos algunos ejemplos: .. code:: ipython3 # Array de 3 filas y tres columnas, creado implícitamente In [56]: arr0 = np.array([[10,20,30],[9, 99, 999],[0, 2, 3]]) In [57]: print(arr0) [[ 10 20 30] [ 9 99 999] [ 0 2 3]] # Array de ceros con 2 filas y 3 columnas In [57]: arr1 = np.zeros((2,3)) In [59]: print(arr1) [[ 0. 0. 0.] [ 0. 0. 0.]] # Array de unos con 4 filas y una columna In [62]: arr2 = ones((4,1)) In [63]: print(arr2) [[ 1.] [ 1.] [ 1.] [ 1.]] # Array unidimensional de 9 elementos y cambio su forma a 3x3 In [64]: arr3 = np.arange(9).reshape((3, 3)) In [65]: print(arr3) [[0 1 2] [3 4 5] [6 7 8]] In [69]: arr2.shape Out[69]: (4, 1) Como vemos en la última línea, la forma o ``shape()`` de los arrays se sigue dando como una tupla, con la dimensión de cada eje separado por comas; en ese caso la primera dimensión son las cuatro filas y la segunda dimensión o eje es una columna. Es por eso que al usar las funciones ``zeros()``, ``ones()``, ``reshape()``, etc. hay que asegurarse que el parámetro de entrada **es una tupla** con la longitud de cada eje. Cuando usamos la función ``len()`` en un array bidimensional, el resultado es la longitud del primer eje o dimensión, es decir, ``len(arr2)`` es 4. El acceso a los elementos es el habitual, pero ahora hay que tener en cuenta el eje al que nos referimos; además podemos utilizar ”:” como comodín para referirnos a todo el eje. Por ejemplo: .. code:: ipython3 # Primer elemento de la primera fila y primera columna (0,0) In [86]: arr0[0,0] Out[86]: 10 # Primera columna In [87]: arr0[:,0] Out[87]: array([10, 9, 0]) # Primera fila In [88]: arr0[0,:] Out[88]: array([10, 20, 30]) # Elementos 0 y 1 de la primera fila In [89]: arr0[0,:2] Out[89]: array([10, 20]) Igualmente podemos modificar un array bidimensional usando sus índices: .. code:: ipython3 # Asignamos el primer elemento a 88 In [91]: arr0[0, 0] = 88 # Asignamos elementos 0 y 1 de la segunda fila In [92]: arr0[1, :2] = [50,60] # Multiplicamos por 10 la última fila In [93]: arr0[-1, :] = arr0[-1, :]*10 In [94]: print(arr0) array([[ 88, 20, 30], [ 50, 60, 999], [ 0, 20, 30]]) Cambiando el tamaño de arrays ----------------------------- Hemos visto que es fácil quitar y poner elementos nuevos en un array unidimensional. Pero con dos o más dimensiones es algo más complicado porque estamos limitados a la estructura y número de elementos del array. Podemos cambiar la forma (``shape``) de un array a otra que tenga el mismo número de elementos fácilmente usado ``reshape()``: .. code:: ipython3 In [91]: numeros = arange(10000) # Array unidimensional de 10000 numeros In [92]: numeros_2D = numeros.reshape((100, 100)) In [93]: numeros_3D = numeros.reshape((100, 10, 10)) In [94]: numeros.shape Out[94]: (10000,) In [95]: numeros_2D.shape Out[95]: (100, 100) In [95]: numeros_3D.shape Out[95]: (100, 10, 10) Para añadir más filas o columnas a un array, la forma más efectiva en crear un array nuevo con la forma deseada y luego añadir las filas o columnas, por ejemplo: .. code:: ipython3 In [100]: A = np.arange(0, 10) In [101]: B = np.arange(100, 1100, 100) In [102]: C = np.zeros((len(A), 2)) # 10 filas, dos columnas In [103]: C[:,0] = A In [104]: C[:,1] = B Existen otros métodos de manipulación de la forma de los arrays como ``hstack()``, ``vstack()`` o ``tile()`` entre otras. Filtros y máscaras de arrays. Arrays enmascarados ------------------------------------------------- Una de las mejores utilidades de numpy es trabajar con índices y máscaras de datos para limitar o seleccionar parte de los datos. Supongamos que tenemos un array de datos, pero que solo nos interesa los positivos, que queremos manipular después. Hay varias formas de seleccionarlos definiendo un array máscara con la condición que nos interesa: .. code:: ipython3 In [264]: datos = array([3, 7, -2, 6, 7, -8, 11, -1, -2, 8]) In [265]: datos Out[265]: array([ 3, 7, -2, 6, 7, -8, 11, -1, -2, 8]) In [266]: mask = datos >= 0 In [267]: mask Out[267]: array([ True, True, False, True, True, False, True, False, False, True], dtype=bool) In [268]: datos*mask Out[268]: array([ 3, 7, 0, 6, 7, 0, 11, 0, 0, 8]) In [269]: datos[mask] Out[269]: array([ 3, 7, 6, 7, 11, 8]) Usando un array ``mask`` de booleanos, podemos operar con el el array de datos, cuando un valor se multiplica por ``True`` es equivalente a **multiplicarse por 1** y si es con ``False``, a **multiplicarse por 0**. Por eso el resultado es un array del mismo tamaño, pero los elementos que no cumplen la condición se hacen 0. Si por el contrario usarmos usamos ``mask`` como un **array de índices**, el resultado es un array con los elementos cuyo índice corresponda con ``True``, ignorando los de índice ``False``. Usaremos uno u otro según lo que queramos hacer, el truco consiste es crear de manera correcta la máscara de datos. Veamos el caso de un array 2D con dos columnas, pero queremos limitar todos los datos en criterios en las dos columnas. Primero creamos una máscara como producto de las dos condiciones, y luego la usamos como array de índices en el array original: .. code:: ipython3 In [270]: from numpy import random In [278]: datos2 = random.randint(-10, 20, (10,2)) In [279]: datos2 Out[279]: array([[ 0, 10], [ 5, 18], [ 19, 4], [ 4, 19], [ -2, -2], [ 11, -10], [ -4, 4], [ 5, 6], [ 13, 13], [ 7, 13]]) # Solo queremos los datos que el la columna 0 sean mayores # que 0 pero menores que 10 en la columna 1 In [284]: condicion1 = datos2[:,0] > 0 In [285]: condicion1 = datos2[:,1] < 10 In [286]: mask_col0 = condicion1*condicion1 In [287]: mask_col0 Out[287]: array([False, False, True, False, False, True, False, True, False, False], dtype=bool) In [288]: datos2[mask_col0] Out[288]: array([[ 19, 4], [ 11, -10], [ 5, 6]]) Como se ve, el resultado en un array de dos columna, donde en la primera columna son todos positivos y en la segunda menores que +10. ¿Y si queremos que al menos se cumpla una condición? Simplemente tenermos que sumar las dos máscaras (las de cada columna) en lugar de multiplicarla, básicamente es como multiplicar o sumar unos o ceros (``True`` o ``False``). Como este tipo de operaciones tienen mucho potencial y pueden llegar a se complejas. ``numpy`` tiene un módulo que puede ayudar en estos casos, que es el de `arrays enmascarados `__ (``numpy.ma``). Se trata de un tipo de datos que permite ignorar algunos elementos de un array según ciertas condiciones. Vemos un ejemplo: .. code:: ipython3 In [300]: import numpy.ma as ma In [301]: x = array([1, 2, 3, -1, 5]) In [302]: # Enmascaramos en cuarto elemento In [303]: mx = ma.masked_array(x, mask=[0, 0, 0, 1, 0]) In [304]: print(mx.mean()) 2.75 In [305]: print(x.mean()) 2.0 Como se ve, el array enmascarado ha ignorado el valor que se enmascara con ``True`` dando un resultado distinto en la media, pero no lo ha eliminado del array. .. code:: ipython3 In [313]: x.view(ma.MaskedArray) Out[313]: masked_array(data = [ 1 2 3 -1 5], mask = False, fill_value = 999999) In [314]: mx.view(ma.MaskedArray) Out[314]: masked_array(data = [1 2 3 -- 5], mask = [False False False True False], fill_value = 999999) Podemos usar algunas funciones de ``ma`` para crear las máscaras, como ``masked_greater()``, ``masked_inside()``, ``masked_where()``, etc. .. code:: ipython3 In [320]: a = np.arange(4) In [321]: print(a) array([0, 1, 2, 3]) In [322]: ma.masked_where(a <= 2, a) masked_array(data = [-- -- -- 3], mask = [ True True True False], fill_value=999999) Arrays estructurados -------------------- Aunque los arrays pueden contener cualquier tipo de dato, los arrays normales sólo pueden ser de un único tipo. Para esto extiste una variante de arrays para contenidos complejos o `estructurados `__, llamado *structured arrays*, que permiten tratar arrays por estructuras o por campos de estruturas. Además de poder contener distintos tipos de datos, facilitan el acceso por columnas. Vemos un ejemplo con un array con distintos tipos de datos: .. code:: ipython3 In [327]: # Array estucturado de 5 elementos, vacio In [328]: galaxies = np.zeros(5, dtype = {'names': ('name', 'order', 'type', 'magnitude'), 'formats': ('U16', 'i4', 'U10', 'f8')}) In [329]: # Listas de datos para contruir el array estructurado In [330]: names = ["M 81", "NGC 253", "M 51", "NGC 4676", "M 106"] In [331]: types = ["SA(s)b", "SAB(s)c", "Sc", "Irr", "SAB(s)bc"] In [332]: magnitudes = [6.93, 7.1, 8.4, 14.7, 9.1] In [333]: order = list(range(5)) In [334]: # Anhadimos valores a los campos (columnas) In [335]: galaxies['name'] = names In [336]: galaxies['type'] = types In [337]: galaxies['magnitude'] = magnitudes In [338]: galaxies['order'] = order In [339]: print(galaxies) [('M 81', 0, 'SA(s)b', 6.93) ('NGC 253', 1, 'SAB(s)c', 7.1 ) ('M 51', 2, 'Sc', 8.4 ) ('NGC 4676', 3, 'Irr', 14.7 ) ('M 106', 4, 'SAB(s)bc', 9.1 )] Se trata de un array con cinco entradas (o *records*) y cada una de ellas posee cuatro campos de distinto tipo, indicados con la propiedad ``dtype``. En este caso son un **string unicode de longitud máxima 16 (U16)**, **un entero 4 bytes (i.e. 32 bit) (i4)**, **string unicode de longitud máxima 16 (U16)** y un **float de 4 bytes (i.e. 64 bit)**. El ``dtype`` de `numpy `__ describe cómo interpretar cada elemento en bytes de bloques de memoria fijos. No sólo se trata de si son *float*, *int*, etc., el ``dtype`` describe lo siguiente: * Tipo de dato (int, float, objeto Python, etc.) * Tamaño del dato (cuantos bytes puede ocupar) * Orden de bytes de datos (little-endian o big-endian) * Si son datos estructurado (por ejemplo mezcla de tipos de dato), también: * Nombre de los campos * Tipo de dato de cada campo * Qué parte del bloque de memoria ocupa cada campo * Si en dato es un sub-array, su forma y tipo de dato De manera resumida, para definir el tipo de cada elemento podemos usar una de las siguientes cadenas: :: b1, i1, i2, i4, i8, u1, u2, u4, u8, f2, f4, f8, c8, c16, a que representan, respectivamente, **bytes**, **ints**, **unsigned ints**, **floats**, **complex** y **strings de longitud fija**. También se pueden usar los tipos de datos estándar de Python equivalentes (``int``, ``float``, etc.) Teniendo un array estructurado como el anterior, podemos ver cada elemento haciendo el indexado habitual y también por campos (columnas): .. code:: ipython3 In [350]: print(galaxies) [('M 81', 0, 'SA(s)b', 6.93) ('NGC 253', 1, 'SAB(s)c', 7.1 ) ('M 51', 2, 'Sc', 8.4 ) ('NGC 4676', 3, 'Irr', 14.7 ) ('M 106', 4, 'SAB(s)bc', 9.1 )] In [351]: # Columna 'name' del array In [352]: galaxies['name'] Out[352]: array(['M 81', 'NGC 253', 'M 51', 'NGC 4676', 'M 106'], dtype='