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:

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/<ipython console> in <module>()

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:

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:

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:

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:

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:

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():

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():

In [31]: enteros = np.arange(6)

In [32]: print(enteros)
[0 1 2 3 4 5]

In [33]: type(enteros)
Out[33]: <type 'numpy.ndarray'>

In [34]: type(enteros[0])
Out[34]: <type 'numpy.int32'>

In [35]: decimales = enteros.astype('float')

In [36]: type(decimales)
Out[36]: <type 'numpy.ndarray'>

In [37]: type(decimales[0])
Out[37]: <type 'numpy.float64'>

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:

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:

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:

# 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).

# 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:

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:

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:

# 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:

# 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:

# 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:

# 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:

# 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():

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:

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:

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:

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:

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.

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.

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:

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<n>

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):

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='<U16')

In [353]: # Primer elemento del array, con todos los campos (columnas)
In [354]: galaxies[0]
Out[354]: ('M 81', 0, 'SA(s)b', 6.93)

In [355]: # Nombres de galaxias más brillantes que magnitud 9
In [356]: galaxies[galaxies['magnitude'] < 9]['name']
Out[356]: array(['M 81', 'NGC 253', 'M 51'], dtype='<U16')

Adicionalmente, numpy tiene una subclase de array estructurado llamado record array que básicamente es idéntico a los arrays estructurados pero permite el acceso a los campos como métodos además de como índice. Se declaran igual que los estructurados usando np.rec.array(), pero podemos convertir el anterior de la siguiente forma:

In [360]: # Array estructurado a record array
In [361]: galaxies_ra = galaxies.view(np.recarray)

In [362]: # Acceso al campo name como un método, en lugar de galaxies_ra['name']
In [363]: galaxies_ra.name
Out[364]: array(['M 81', 'NGC 253', 'M 51', 'NGC 4676', 'M 106'], dtype='<U16')

Lectura y escritura de datos con numpy

numpy posee algunos métodos de lectura de ficheros de texto que nos pueden facilitar la vida si son relativamente sencillos. En más básico es np.loadtxt(); si todos las columnas del fichero son numéricas, basta con indicar el delimitador de columnas si es distinto de espacios:

In [400]: # Leo un fichero de datos con campos delimitados por ";"
In [401]: data = np.loadtxt("medidas_radio.txt", delimiter=";")

In [402]: data.shape
Out[402]: (480, 2)

Si hay más de una columna como en este ejemplo, np.loadtxt() devuelve un array bidimiensional en el que primera dimensión o eje son las filas y el segundo las columnas, de maneras que el fichero que acabamos de leer tiene 480 filas y dos columnas. Quizás sea más práctico poner la columnas por separado, para lo que podemos hacer:

In [405]: tiempo = data[:, 0]  # tiempo, la primera columna
In [406]: masa = data[:, 1]    # masa, la segunda columna

pero si al leer añadimos el parámetro unpack=True, np.loadtxt() desempaqueta por columnas en lugar de por filas (como si invirtiera el array), perminiéndonos desempaquetar en variables las columnas, que ahora están en el eje 0:

In [410]: tiempo, masa = np.loadtxt("medidas_radio.txt", delimiter=";", unpack=True)

Si el fichero a leer tiene distintos tipos datos (string y float), hay que indicar con el parámetro dtype la lista de tipos de dato que tienen las columnas que queremos leer. En este caso es más práctico usar el método np.genfromtxt(), que similar a loadtxt() pero más flexible para leer columnas de distinto tipo. Si usamos np.genfromtxt() con el parámetro dtype, que puede ser una lista con tuplas nombre-tipo, podemos indicar el nombre de la columna y el tipo dato que contiene, creando un array estructurado como vimos antes:

In [420]: dtypes = [('tiempo', 'float'), ('masa', 'float')]
In [421]: data = np.genfromtxt('medidas_radio.txt', dtype=dtypes)

En este caso ya no hay que usar el desempaquetado, porque tenemos un array estructurado con columnas con nombre data['tiempo'] y data['masa'].

De manera similar, podemos usar np.savetxt() para guardar un fichero de datos por columnas:

# Guardamos un array datos en un fichero de texto, con los campos
# delimitados por tabulador (\t) en formato float con dos decimales
# y le damos una cabecera
In [426]: np.savetxt('datos.tsv', data, delimiter='\t', fmt='%.2f', header="tiempo\t masa")

En este ejemplo delimitamos las columnas por tabuladores (\t), escribimos los números como floats con dos decimales (%.2f, por defecto es notación científica) y añadimos un string que hace de cabecera.

Cálculo matricial con numpy

numpy incluye algunas funciones para álgebra y cálculo matricial, en el que es clave el tipo de dato matrix. Es similar al array, pero opera como una matrix y tiene método propios de las matrices.

In [425]: A = array([[1,2,3], [4,5,6], [7,8,9]])

In [426]: Am = mat(A)  # Convertimos array a matriz

In [428]: A*A  # Aquí hace producto matricial, no elemento a elemento como con arrays
Out[428]:
array([[ 1,  4,  9],
       [16, 25, 36],
       [49, 64, 81]])

In [429]: Am*Am
Out[429]:
matrix([[ 30,  36,  42],
        [ 66,  81,  96],
        [102, 126, 150]])

In [433]: Am.T                 # Matriz traspuesta

In [437]: Am.diagonal()
Out[437]: matrix([[1, 5, 9]])

In [438]: linalg.det(Am)       # Determinante
Out[438]: 6.6613381477509402e-16

In [441]: linalg.eigvals(Am)   # Autovalores