numpy
numpy
?¶En la documentación del módulo o paquete numpy
dice:
Provides
1. An array object of arbitrary homogeneous items
2. Fast mathematical operations over arrays
3. Linear Algebra, Fourier Transforms, Random Number Generation
numpy
está escrito parcialmente en Python, pero en la mayoría de sus partes que requieren un computo lo más eficiente posible está escrita en C o C++.
Para más detalles acerca del contenido del paquete numpy
consultar:
numpy
y scipy
son dos paquetes de Python complementarios para computación científica y análisis de datos. scipy
construye sobre numpy
y proporciona una amplia gama de algoritmos y herramientas para la resolución de problemas en áreas como la optimización, la interpolación, la transformada de Fourier, el álgebra lineal y la estadística.
Si ya conoce a profundidad todas las características del paquete numpy
, puede consultar los detalles del paquete scipy
en:
Un arreglo (array) es una colección finita de datos del mismo tipo, que se almacenan en posiciones consecutivas de memoria y reciben un nombre común.
En el caso de un arreglo unidimensional estamos hablando de una estructura de datos que tendría cierta equivalencia o similaridad con el objeto matemático: vector $\left( \vec{v} = \left(v_1, v_2, \dots, v_n\right) = \left(v_i\right)_{1 \leq i \leq n} \right)$, ya que ambos corresponden a una secuencia de valores ordenados, que son indexados por un único índice asociado a una única dimensión.
Por ejemplo, el arreglo unidimensional representado aquí:
índice: | 0 | 1 | 2 | 3 | 4 |
valor: | 1.2 | 3.4 | 5.6 | 7.8 | 9 |
contiene la misma información o los mismos datos que el vector:
$$\vec{v} = \left(v_1, v_2, \dots, v_n\right) = \left(1.2, 3.4, 5.6, 7.8, 9\right)$$Los arreglos multidimensionales son aquellos en donde cada elemento del arreglo está indexado por más de un índice (por más de un subíndice cuando los elementos del arreglo se denotan de la siguiente manera: $a_{i,j,k,\dots}$
Por ejemplo, el arreglo multidimensional (bidimensional) representado aquí:
2do índice: | 0 | 1 | 2 | |
1er índice: | 0 | 1.2 | 3.4 | 5.6 |
1 | 7.8 | 9 | 0 |
contiene la misma información o los mismos datos que la matriz:
$$A = \left(a_{ij}\right)_{1 \leq i \leq n, 1 \leq j \leq m} = \begin{bmatrix} 1.2 & 3.4 & 5.6 \\ 7.8 & 9 & 0 \end{bmatrix}$$Las listas y tuplas nos podrían servir para el manejo de arrays pero las operaciones con listas o tuplas son demasiado lentas (lo que se gana en versatilidad se pierde en velocidad). Se ha dicho que las operaciones con arreglos de numpy
es 50 veces más veloz que con listas.
La mayoría, sino todas, las librerías principales de Ciencia de Datos, Inteligencia Artíficial y Aprendizaje Profundo suelen trabajar con arrays de numpy u objetos similares.
Primero importamos (cargamos) el módulo numpy
y le asociamos el alias np
:
# Si no está instalado, lo instalamos primero:
#!conda install numpy
#!pip install numpy
# Importamos:
import numpy as np
Vamos a crear arrays de distintas dimensiones. Para esto podemos usar números, listas o tuplas.
a = np.array(13) #array de dimensión 0
print("type(a):", type(a), "\n")
print("a:", a, sep = "\n")
a
b = np.array([1, 2, 3, 4]) #array de dimensión 1
print("b:", b, sep = "\n")
b
c = np.array([[1, 2, 3, 4], [5, 6, 7, 8]]) #array de dimensión 2
print("c:", c, sep = "\n")
c
d = np.array([[[1, 2, 3, 4], [5, 6, 7, 8]], [[9, 10, 11, 12], [13, 14, 15, 16]]]) #array de dimensión 3
print("d:", d, sep = "\n")
d
Los escalares son arreglos/tensores de dimensión 0, los vectores son arreglos/tensores de dimensión 1, las matrices son arreglos/tensores de dimensión 2, etc.
El atributo ndim
almacena la dimension del arreglo/tensor (no confundir con el tamaño de un vector o una matriz).
print(a.ndim)
print(b.ndim)
print(c.ndim)
print(d.ndim)
El proceso de indexación en numpy es similar al de las listas en python. Cada número implica buscar la posición en cada dimensión del array
print("a =", a)
print("b[2] = ", b[2])
print("c[1] =", c[1])
print("d[1] =", d[1])
print("a =", a)
print("b[2] =", b[2])
print("c[1,2] =", c[1,2])
print("d[1,0,2] =", d[1,0,2])
Similarmente podemos segmentar arrays como en las listas de python,
print(b[1:4]) #b = [1 2 3 4]
print(c[1,1:3]) #c = [[1 2 3 4][5 6 7 8]]
print(d[1:3,1:3,0:2])
Los arrays no están limitados a números enteros, pueden también tener cadenas, booleanos, números de punto flotante, etc.
frutas = np.array(['Manzana', 'Naranja', 'Uva']) #cadenas
print(frutas.dtype)
Podemos manipular el tipo de los datos del array dentro de la función de creación del array np.array
, siempre y cuando el cambio sea posible. Por ejemplo podemos pasar enteros a cadenas (integers a strings).
number_to_string = np.array([6, 1, 2, 4, 623, 8], dtype=str) #b denota un string
print(number_to_string.dtype)
print(number_to_string)
try:
error_array = np.array(['a', '2', '3'], dtype=int)
except:
print("hay un error en su lógica")
Podemos también cambiar el tipo en arrays ya existentes
floating_array = np.array([1.1, 2.1, 3.1]) #punto flotante
int_array = floating_array.astype(int)
print(int_array)
vector = np.array([127, -127, 32767, -32767], dtype=np.int8) # entero de 1 byte = 8 bits (1 bit se necesita para almacenar el signo)
print('`vector`:\t', vector)
print('`vector.dtype`: ', vector.dtype)
vector = np.array([127, -127, 32767, -32767], dtype=np.int16) # entero de 2 bytes = 16 bits (1 bit se necesita para almacenar el signo)
print('`vector`:\t', vector)
print('`vector.dtype`: ', vector.dtype)
El tamaño de un arreglos es diferente a su dimensión. El tamaño del arreglo es el número de elementos posibles por cada dimensión.
print(a.shape) # a = 13
print(b.shape) # b = [1 2 3 4]
print(c.shape) # c = [[1 2 3 4][5 6 7 8]]
print(d.shape) # d = [[[ 1 2 3 4][ 5 6 7 8]] [[ 9 10 11 12][13 14 15 16]]]
Podemos cambiar la forma de los arreglos. Esto significa aumentar el número de dimensiones o el número de elementos por dimensión.
print("c =", c, "\n") # c = [[1 2 3 4][5 6 7 8]]
c_1D = c.reshape(1, 8)
print("c_1D =", c_1D, "\n")
c_4D = c.reshape(4, 2)
print("c_4D =", c_4D, "\n")
¿Podemos hacer cualquier cambio en la forma? Si, mientras la cantidad de elementos sea coincidente.
print(d)
d
es un array con 16 elementos, así que podemos hacer reshape con tamaños por ejemplo 1x16, 4x4, 8x2, 2x4x2.
try:
d.reshape(3, 5)
except:
print('Hay un error al intentar cambiar la forma del arreglo')
d.reshape(2, 2, 2, 2)
Para no especificar explícitamente el número de elementos de una (solamente una) de las dimensiones se le puede dar el valor -1
(solamente una vez) al método reshape()
.
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
arr.reshape(3, -1)
Podemos usar dicho valor -1
para "aplanar" el array a una dimensión.
d.reshape(-1)
Para transponer el array utilizamos .T
print("c =", c, "\n")
print("c.T =", c.T)
Podemos usar los métodos tradicionales para recorrer los valores de un arreglo.
for x in b:
print(x, "\n")
for x in c:
print(x, "\n")
for x in d:
print(x, "\n")
Si queremos recorrer cada elemento para un arreglo de una o más dimensiones, necesitariamos un for
por cada dimensión
for x in d:
for y in x:
for z in y:
print(z)
Esto por supuesto no es óptimo. Así que numpy tiene la función nditer()
para estos casos.
for x in np.nditer(d):
print(x)
Se puede usar la función ndenumerate
, en caso de que además de los valores dentro de un arreglo, también se requiera la indexación respectiva.
for i, x in np.ndenumerate(d):
print('i:', i, '\t\tx:', x)
numpy
permite crear arreglos específicos de forma rápida y fácil:
print(f'`np.zeros((2,3,5))`:\n{np.zeros((2,3,5))}\n')
print(f'`np.ones((2,3,5))`:\n{np.ones((2,3,5))}\n')
print(f'`np.full((3,4), 1.23)`:\n{np.full((2,3,5), 1.23)}\n')
print(f'`np.identity(3)`:\n{np.identity(3)}\n')
print(f'`np.eye(3)`:\n{np.eye(3)}\n')
print(f'`np.empty((2,3,5))`:\n{np.empty((2,3,5))}\n')
print(f'`np.arange(0, 1, 0.1)`:\n{np.arange(0, 1, 0.1)}\n')
print(f'`np.linspace(0, 1, 5)`:\n{np.linspace(0, 1, 5)}\n')
La función where
devuelve los índices de los valores para los cuales se cumple una condición dada. Estos índices podrían ser utilizados para "filtrar". Sin embargo, si el único objetivo es "filtrar", se puede lograr usando una colección de valores booleanos.
vector = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
indx = np.where(vector%3 == 0)
print(f'`type(indx)`:\t{type(indx)}')
print(f'`indx`:\t{indx}')
filtrado = vector[indx]
print(f'`filtrado`:\t{filtrado}')
vector_bool = vector%3 == 0
print(f'`type(vector_bool)`:\t{type(vector_bool)}')
print(f'`vector_bool`:\t\t{vector_bool}')
filtrado = vector[vector_bool]
print(f'`filtrado`:\t\t{filtrado}')
Además de proveer la clase ndarray
, numpy
contiene un conjunto de funciones (que suelen llamar ufuncs, universal functions) que se ejecutan elemento a elemento sin necesidad de utilizar un for
o un while
(cuando una función se ejecuta de esa manera se dice que está vectorizada o que es una función vectorizada). Es más eficiente usar una función vectorizada (si está muy bien hecha, probada, etc.) que iterar mediante un while
o un for
.
add()
, subtract()
, multiply()
, divide()
, power()
, mod()
, remainder()
, divmod()
, absolute()
.trunc()
, fix()
, around()
, floor()
, ceil()
.lcm()
, lcm.reduce()
, gcd()
, gcd.reduce()
log2()
, log10()
, log()
, exp()
.sin()
, cos()
, tan()
, arcsin()
, arccos()
, arctan()
, sinh()
, cosh()
, tanh()
, arcsinh()
, arccosh()
, arctanh()
, deg2rad()
, rad2deg()
.unique()
, union1d()
, intersect1d()
, setdiff1d()
, setxor1d()
.Todas las ufuncs tienen como parámetros adicionales:
dtype
para dar el tipo de elementos para la salida.out
para dar un arreglo en donde la salida será copiada.where
para dar un arreglo booleano o una condición para seleccionar los elementos sobre los que se ejecutará la función.import math
# en vez de algo como:
opt1 = [math.sin(i) for i in [1, 2, 3]]
print(opt1)
import math
# en vez de algo como:
opt1 = [math.sin(i) for i in [1, 2, 3]]
print(f'`opt1`: \t{opt1}')
print(f'`opt1[0]`: \t{opt1[0]:.30f}')
# es mucho más eficiente hacer algo como:
opt2 = np.sin(np.array([1, 2, 3], dtype=np.uint8), dtype=np.float64)
print(f'`opt2`: \t{opt2}')
print(f'`opt2[0]`:\t{opt2[0]:.30f}')
x = np.array([1, 2, 3])
y = np.array([0.0, 0.0, 0.0])
np.sin(x, where = x%2==1, out = y)
print(f'`y`: \t{y}')
x = np.array([1, 2, 3, 4])
y = np.array([4, 5, 6, 7])
z = np.add(x, y)
print(f'`z`:\t{z}')
z = x + y
print(f'`z`:\t{z}')
z = np.add(x, y, dtype=float)
print(f'`z`:\t{z}', )
try:
print(np.array([1, 2, 3, 4]) + np.array([5, 6]))
except:
print("No se pudo hacer la operación")
try:
print(np.array([1, 2, 3, 4]) + 0.9)
except:
print("No se pudo hacer la operación")
Adicionalmente tenemos las funciones: sum()
, cumsum()
, prod()
, cumprod()
, diff()
.
matriz = np.array([[3, 2, 4], [5, 2, 1], [8, 7, 6]])
print(f'`matriz`:\n{matriz}\n')
print(f'`np.prod(matriz)`:\n{np.prod(matriz)}\n')
print(f'`np.prod(matriz, axis=0)`:\n{np.prod(matriz, axis=0)}\n')
print(f'`np.prod(matriz, axis=1)`:\n{np.prod(matriz, axis=1)}\n')
print(f'`matriz`:\n{matriz}\n')
print(f'`np.cumprod(matriz)`:\n{np.cumprod(matriz)}\n')
print(f'`np.cumprod(matriz, axis=0)`:\n{np.cumprod(matriz, axis=0)}\n')
print(f'`np.cumprod(matriz, axis=1)`:\n{np.cumprod(matriz, axis=1)}\n')
Además de las funciones para operaciones matemáticas, numpy
también tiene funciones para realizar algunas operaciones que se suelen requerir desde la estadística (https://numpy.org/doc/stable/reference/routines.statistics.html)
print(f'media: {np.mean(matriz)}')
print(f'desviación estándar muestral: {np.std(matriz, ddof=1)}')
Para la multiplicación de matrices usamos @
, matmul()
, o incluso dot()
. Naturalmente, es necesario tener una especial atención a los tamaños de los arreglos a operar.
arr1 = np.array([[1,2,3],
[4,5,6]]) # tamaño 2,3
arr2 = np.array([[6,5,4],
[3,2,1]]) # tamaño 2,3
print(arr1 @ arr2.T) # 2,3 y 3,2. Resultado: 2,2
print(np.matmul(arr1, arr2.T))
print(np.dot(arr1, arr2.T))
Con matmul
no se puede hacer la multiplicación $cA$, en donde $c$ es un escalar y $A$ una matriz.
try:
print(0.9 @ arr1) # float y arreglo de tamaño 2,3
except:
print("No se pudo hacer la operación")
try:
print(np.array([0.9]) @ arr1) # tamaño 1 y tamaño 2,3
except:
print("No se pudo hacer la operación")
try:
print(np.array([200, 100]) @ arr1) # tamaño 2 y tamaño 2,3
except:
print("No se pudo hacer la operación")
try:
print(np.matmul(arr1, np.array([10, 20, 30]))) # 2,3 y 3
except:
print("No se pudo hacer la operación")
Aunque el resultado de matmul()
y dot()
coincide para arreglos de dimensión dos (matrices), dot()
en general no hace lo mismo que matmul()
. Para más información acerca de la función dot()
, consultar https://numpy.org/doc/stable/reference/generated/numpy.dot.html#numpy.dot
El módulo random
del módulo numpy
permite generar valores pseudoaleatorios (¿cuál será la diferencia entre pseudoaleatorio y aleatorio?).
from numpy.random import default_rng
semilla = 20230421
rng = default_rng(semilla)
enteros = rng.integers(low=1, high=10, size=(2,3,5))
print(f'`type(enteros)`: {type(enteros)}')
print(f'`enteros`:\n{enteros}')
flotantes = rng.random((3,4))
print(f'`type(flotantes)`: {type(flotantes)}')
print(f'`flotantes`:\n{flotantes}')
print(rng.choice.__doc__)
print(flotantes, "\n")
print(rng.choice(flotantes, 5, True, axis=1))
Para conocer las distribuciones incluidas consultar: https://numpy.org/doc/stable/reference/random/generator.html#distributions
Utilicemos la distribución normal (gaussiana):
x = rng.normal(loc=0, scale=1, size=(3,3))
print(x)
Por ejemplo, supongamos que estamos interesados en la variable estatura de una población de personas y por alguna razón sabemos que dichas estaturas se distribuyen normal con media poblacional 170 cm y desviación estándar poblacional 8 cm. Generemos una muestra de 1000 estaturas pseudoaleatorias asociadas a dicha población:
muestra_estaturas = rng.normal(loc=170, scale=8, size=1000)
Calculemos la media y la desviación (muestrales) de los datos generados:
print(f'media: {np.mean(muestra_estaturas)}')
print(f'desviación estándar muestral: {np.std(muestra_estaturas, ddof = 1)}')
¿y si graficamos los datos generados?
import matplotlib.pyplot as plt
import seaborn as sns
plot = sns.histplot(data = muestra_estaturas, kde = True)
La función concatenate()
nos sirve para "pegar" uno o más arreglos.
matriz1 = np.array([[1, 2], [3, 4]])
matriz2 = np.array([[5, 6], [7, 8]])
res1 = np.concatenate((matriz1, matriz2), axis=0)
print(f'`res1`:\n{res1}')
res2 = np.concatenate((matriz1, matriz2), axis=1)
print(f'`res2`:\n{res2}')
Las funciones stack()
, hstack()
, vstack()
y dstack()
también sirven para "pegar" uno o más arreglos (para más detalles consultar la documentación, por ejemplo: np.stack.__doc__
).
print(f'{np.stack((matriz1, matriz2), axis=0)}')
print(f'{np.stack((matriz1, matriz2), axis=1)}')
print(f'{np.stack((matriz1, matriz2), axis=2)}')
La función array_split()
sirve para "dividir"/"partir" un arreglo. También existen las funciones vsplit()
y dsplit()
matriz = np.array([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]])
print(f'`matriz`:\n{matriz}')
res = np.array_split(matriz, 3, axis=0)
print(f'`type(res)`:\t{type(res)}')
for e in res:
print(f'`e`:\n{e}')
matriz = np.array([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]])
print(f'`matriz`:\n{matriz}')
res = np.array_split(matriz, 2, axis=1)
print(f'`type(res)`:\t{type(res)}')
for e in res:
print(f'`e`:\n{e}')
Es posible convertir funciones propias en "ufuncs" mediante el uso de la función frompyfunc()
.
frompyfunc()
es una función de orden superior que recibe tres parámetros: la función a convertir, el número de arreglos de entrada y el número de arreglos de salida que tendrá la función.
x = np.array([[1, 2], [3, 4]])
y = np.array([[4, 5], [6, 7]])
def mi_funcion_ejemplo(a, b):
return a**b / b**a
ufunc_mi_funcion_ejemplo = np.frompyfunc(mi_funcion_ejemplo, 2, 1)
print(f'`type(ufunc_mi_funcion_ejemplo)`:\t{type(ufunc_mi_funcion_ejemplo)}', )
print(f'`ufunc_mi_funcion_ejemplo(x, y)`:\n{ufunc_mi_funcion_ejemplo(x, y)}', )