junio 8, 2020
~ 10 MIN
Introducción a NumPy
< Blog RSSNumPy
NumPy (Numerical Python) es uno de los módulos más importantes y, probablemente, el más utilizado en el campo del cálculo numérico en el ecosistema de Python. En posts anteriores hemos visto como utilizar diferentes estructuras de datos en Python y otros módulos que nos ofrecen funciones matemáticas, cómo el módulo math. NumPy extiende esta funcionalidad en el campo del cálculo numérico de la siguiente manera:
- Ofrece el objeto
ndarray, similar a una lista dePythonpero optimizada para el cálculo numérico. Nos referiremos a este objeto comoarraydeNumPy, o simplementearray. - Implementa funciones matemáticas que pueden trabajar directamente sobre
arrayssin tener que implementar bucles. - Proporciona funciones para leer/escribir datos a archivos de manera optimizada.
- Permite aplicaciones de álgebra lineal, generación de números aleatorios y transformadas de Fourier.
El núcleo de Numpy está implementado en C, ofreciendo bindings en Python para interactuar con él. Esto se traduce en que NumPy es más rápido que el equivalente en puro Python. Muchas otras librerías para el análisis de datos están construidas sobre NumPy, utilizando los ndarrays como la estructura de datos básica debido a su eficiencia. Para ilustrar esta propiedad vamos a calcular el tiempo necesario para llevar a cabo la misma operación en puro Python en comparación con arrays de NumPy.
l = [i for i in range(10000000)]
%time l2 = [2*i for i in l]
Wall time: 655 ms
import numpy as np
a = np.array(l)
%time a2 = 2*a
Wall time: 13.1 ms
Podemos ver que Numpy es 50 veces más rápido que Python llevando a cabo la misma operación. No te preocupes por el resto de detalles, en las siguientes secciones aprenderás lo necesario para crear arrays, hacer operaciones, etc. Lo más importante es destacar que en Python necesitamos iterar por cada valor en la lista aplicando la operación en concreto (lo cual es lento) mientras que en NumPy podemos simplemente aplicar la operación al array entero confiando en la implementación para llevar a cabo la operación de la manera más eficiente posible.
⚠️
NumPyes unmóduloexterno aPython. Para poder usarlo primero hay que instalarlo. Puedes hacerlo abriendo un terminal y escribiendoconda install numpysi instalastePythonusandoconda. Alternativamente, puedes hacerlo conpip install numpy. Si necesitas ayuda en este paso te recomiendo leer el post en el que instalamosPythony vimos como instalar librerías.
El objeto ndarray
Como hemos comentado en la sección anterior, NumPy está basado en el objeto ndarray. Puedes ver este objeto como una lista de Python con súperpoderes. El objeto ndarray es multidimensional, lo que implica que nos permite representar tanto valores escalares como vectores, matrices y matrices multidimensionales (lo que llamamos tensores).
Para poder trabajar con NumPy, primero tenemos que importarlo. Es común importarlo con el nombre np.
import numpy as np
Tenemos varias maneras de crear un array. Una de ellas es utilizar funciones implementadas en NumPy para la creación de arrays, indicando el número de elementos en cada dimensión.
# crear un vector de ceros
np.zeros(5)
array([0., 0., 0., 0., 0.])
# crear una matriz de ceros
np.zeros((3, 4))
array([[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]])
De la misma manera podemos crear arrays de 1s, con un valor determinado o sin inicializar con las funciones np.one(), np.full() o np.empty() respectivamente. Estas son algunas de las propiedades de un array.
# tensor de unos
a = np.ones((3, 4, 2))
a
array([[[1., 1.],
[1., 1.],
[1., 1.],
[1., 1.]],
[[1., 1.],
[1., 1.],
[1., 1.],
[1., 1.]],
[[1., 1.],
[1., 1.],
[1., 1.],
[1., 1.]]])
# numero elementos en cada dimension
a.shape
(3, 4, 2)
# longitud del array
a.ndim
3
# elementos totales en el array
a.size
24
Otra manera muy común de inicializar arrays de Numpy es mediante listas de Python. Para ello usamos la función np.array().
np.array([[1, 2, 3],[4, 5, 6]])
array([[1, 2, 3],
[4, 5, 6]])
Por último, también podemos crear arrays mediante funciones secuenciales o con valores aleatorios de la siguiente manera.
# vector de `int` en rango
np.arange(1, 5)
array([1, 2, 3, 4])
# vector de `float` en rango
np.linspace(0, 1, 10)
array([0. , 0.11111111, 0.22222222, 0.33333333, 0.44444444,
0.55555556, 0.66666667, 0.77777778, 0.88888889, 1. ])
# matriz de números aleatorios
np.random.rand(3,4)
array([[0.91866741, 0.285973 , 0.18087869, 0.32169549],
[0.54680139, 0.91715266, 0.37301333, 0.22500604],
[0.34965872, 0.48719109, 0.74743635, 0.03647023]])
Tipos de datos
Un motivo por el que los arrays de NumPy son tan eficientes es que todos los elementos en el array deben tener el mismo tipo.
a = np.arange(1, 5)
a
array([1, 2, 3, 4])
# acceder al tipo de datos
a.dtype
dtype('int32')
Podemos indicarle a NumPy el tipo de dato con el que queremos trabajar al crear nuestro array.
a = np.arange(1, 5, dtype=np.uint8)
a
array([1, 2, 3, 4], dtype=uint8)
Los tipos disponibles son int8, int16, int32, int64, uint8|16|32|64, float16|32|64 y complex64|128. Puedes encontrar una lista completa en la documentación.
Cambiando la forma
Es muy común cambiar la forma de un array para acomodarlo a ciertas operaciones.
# vector
a = np.arange(10)
a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
# convertir vector en matriz
a2 = a.reshape(2,5)
a2
array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])
Obviamente, para poder cambiar la forma del array el número de elementos tiene que encajar en el número de nuevas dimensiones.
a.reshape(2,4)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-63-5fcd0a049a15> in <module>
----> 1 a.reshape(2,4)
ValueError: cannot reshape array of size 10 into shape (2,4)
# convertir en vector
a2.ravel()
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Operaciones aritméticas
Una de las aplicaciones en las que los arrays de NumPy brillan es en la facilidad de usar operaciones artiméticas de manera optimizada y sin tener que implementar bucles como hacemos en Python. Esta propiedad se conoce como vectorización, algo de lo que hablaremos en más detalle en un futuro post. Podemos usar los operadores que ya conocemos de Python directamente con nuestros arrays.
a = np.array([14, 23, 32, 41])
b = np.array([5, 4, 3, 2])
a + b
array([19, 27, 35, 43])
a - b
array([ 9, 19, 29, 39])
a*b
array([70, 92, 96, 82])
a / b
array([ 2.8 , 5.75 , 10.66666667, 20.5 ])
⚠️ Estas operaciones son elementwise, se aplican elemento a elemento. Para llevar a cabo otras operaciones como por ejemplo el producto escalar de dos vectores usaremos las funciones apropiadas que veremos en un futuro post.
Podremos aplicar estas operaciones siempre que las dimensiones de los arrays coincidan. De no ser así, es posible que NumPy siga dándonos resultados. Esto es debido a una propiedad conocida como broadcasting, algo que veremos en más detalle en un próximo post.
Indexado y Troceado
NumPy adopta la misma lógica de indexado y troceado que Python, algo que ya conocemos y que puedes refrescar en este post.
a = np.array([1, 5, 3, 19, 13, 7, 3])
a
array([ 1, 5, 3, 19, 13, 7, 3])
# acceder a un valor por su índice
a[3]
19
⚠️ Igual que en
Pythonel primer valor de unarraytiene el índice 0.
# troceado
a[2:5]
array([ 3, 19, 13])
# usamos índices negativos para indexar desde el final
a[2:-1]
array([ 3, 19, 13, 7])
Podemos indexar arrys multidimensionales con diferentes índices para cada dimensión, separados por comas.
b = np.arange(48).reshape(4, 12)
b
array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
[12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23],
[24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35],
[36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]])
# valor en segunda fila, tercera columna
b[1, 2]
14
# segunda fila
b[1, :]
array([12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23])
# última columna
b[:, -1]
array([11, 23, 35, 47])
Indexado fancy
El indexado fancy nos permite indexar un array mediante una lista con los índices de interés.
# primera y tercera fila, desde la tercera columna a la cuarta
b[(0,2),2:4]
array([[ 2, 3],
[26, 27]])
Indexado booleano
El indexado booleano es muy útil para trabajar con máscaras.
a = np.array([1, 2, 3, 4])
mask = np.array([True, False, True, False])
a[mask]
array([1, 3])
Iterado
Podemos iterar sobre un array de NumPy de la misma manera que iteramos cualquier otra estructura de datos en Python.
a = np.arange(5)
a
array([0, 1, 2, 3, 4])
for i in a:
print(i)
0
1
2
3
4
Al trabajar con arrays multidimensionales, necesitaremos un loop para cada dimensión.
a = np.arange(9).reshape((3,3))
a
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
for fila in a:
for i in fila:
print(i)
0
1
2
3
4
5
6
7
8
Guardar y Cargar
Podemos guardar nuestros arrays en archivos que más tarde podemos cargar de nuevo.
a = np.random.rand(2,3)
a
array([[0.14663265, 0.98325048, 0.36281673],
[0.33008445, 0.31005347, 0.634345 ]])
# guardar array en archivo
np.save("mi_array", a)
Por defecto un array se guarda con la extensión .npy. Para cargar de nuevo el array
b = np.load("mi_array.npy")
b
array([[0.14663265, 0.98325048, 0.36281673],
[0.33008445, 0.31005347, 0.634345 ]])
Las funciones que hemos visto guardan los arrays en formato binario para maximizar la velocidad de lectura. Sin embargo, podemos guardar nuestros arrays en formato texto para utilizarlos en otras aplicaciones.
# guardar array en formato csv
np.savetxt("mi_array.csv", a, delimiter=",")
También podemos guardar varios arrays en un solo archivo comprimido en formato .npz.
b = np.arange(24, dtype=np.uint8).reshape(2, 3, 4)
b
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]]], dtype=uint8)
a
array([[0.14663265, 0.98325048, 0.36281673],
[0.33008445, 0.31005347, 0.634345 ]])
# guardar arrays
np.savez("mis_arrays", a=a, b=b)
# cargar arrays
mis_arrays = np.load("mis_arrays.npz")
mis_arrays
Podemos extraer cada uno de los arrays mediante su nombre, al estilo dict.
mis_arrays["a"]
array([[0.14663265, 0.98325048, 0.36281673],
[0.33008445, 0.31005347, 0.634345 ]])
mis_arrays["b"]
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]]], dtype=uint8)
Resumen
En este post hemos introducido NumPy la librería de Python por defecto para cálculo numérico. Hemos hablado de sus propiedades principales y del objeto básico con el que trabajamos: el ndarray. Esta estructura de datos es similar a la lista de Python, pero implementada de manera eficiente para su aplicación en cálculo numérico. Con el ndarray, o simplemente array, podemos definir y operar con valores escalares, vectores, matrices y tensores de muchos tipos (numéricos). En el proceso del análisis de datos utilizaremos el array como estructura de datos básica tanto para representar nuestros datos (texto, imágenes, vídeos, datos tabulares, etc) como los distintos modelos y algoritmos de Machine Learning y Deep Learning que hagamos. En próximos posts hablaremos en más detalle sobre algunas características importantes que hay que tener en cuenta a la hora de trabajar con NumPy para sacarle el máximo provecho y empezaremos a utilizarlo para asentar las bases fundamentales que nos encaminarán hacia el desarrollo e implementación de algoritmos de Inteligencia Artificial.