junio 8, 2020

~ 10 MIN

Introducción a NumPy

< Blog RSS

Open In Colab

NumPy

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 de Python pero optimizada para el cálculo numérico. Nos referiremos a este objeto como array de NumPy, o simplemente array.
  • Implementa funciones matemáticas que pueden trabajar directamente sobre arrays sin 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.

⚠️ NumPy es un módulo externo a Python. Para poder usarlo primero hay que instalarlo. Puedes hacerlo abriendo un terminal y escribiendo conda install numpy si instalaste Python usando conda. Alternativamente, puedes hacerlo con pip install numpy. Si necesitas ayuda en este paso te recomiendo leer el post en el que instalamos Python y 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 Python el primer valor de un array tiene 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.

< Blog RSS