Operaciones Básicas#
import numpy as np
Trasposición de arrays y producto matricial#
El método T
obtiene el array traspuesto de uno dado:
D = np.arange(15).reshape((3, 5))
print(D)
[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]]
print(D.T)
[[ 0 5 10]
[ 1 6 11]
[ 2 7 12]
[ 3 8 13]
[ 4 9 14]]
Exercise 53
Utiliza la función np.transpose
para convertir el array
arr = np.arange(3*7*4).reshape(3, 7, 4)
en un array de dimensiones (4, 7, 3)
. ¿Se obtiene el mismo resultado si aplicamos arr.reshape(4, 7, 3)
?
En el cálculo matricial será de mucha utilidad el método np.dot
de numpy, que sirve tanto para calcular el producto escalar como el producto matricial. Veamos varios usos:
rng = np.random.default_rng()
E = rng.normal(0, 1, (6, 3))
E
array([[ 0.22772783, -0.00367246, 0.43747047],
[-1.18789042, -0.71980469, 0.13867212],
[ 0.00589372, 0.246798 , 1.1907824 ],
[-0.1718745 , 0.17768684, -0.5166978 ],
[ 3.01260593, -0.17011007, -0.81107211],
[-1.32660359, 0.15628444, 0.42132367]])
Ejemplos de producto escalar:
np.dot(E[:, 0], E[:, 1]) # producto escalar de dos columnas
0.10532537035482703
np.dot(E[2],E[4]) # producto escalar de dos filas
-0.9900377472722307
E.shape
(6, 3)
np.dot(E, E[0]) # producto de una matriz por un vector
array([ 0.24325386, -0.2072073 , 0.52136794, -0.26583318, 0.33185884,
-0.11836184])
np.dot(E.T, E) # producto de dos matrices
array([[12.32819078, 0.10532537, -2.97164791],
[ 0.10532537, 0.66397641, 0.30446676],
[-2.97164791, 0.30446676, 2.73090131]])
Existe otro operador matmul
(o su versión con el operador @
) que también multiplica matrices. Se diferencian cuando los arrays son de más de dos dimensiones.
A = np.arange(3*4*5).reshape(3, 4, 5)
B = np.arange(3*5*6).reshape(3, 5, 6)
np.dot(A, B).shape
(3, 4, 3, 6)
np.dot(A, B)[x1, x2, y1, y2] = A[x1, x2, :].dot(B[y1, y2, :])
np.matmul(A, B).shape # similar a A @ B
(3, 4, 6)
La diferencia radica en que dot
es el producto escalar del último eje de A con el penúltimo de B para cada combinación de dimensiones y matmul
considera los arrays como arrays de matrices, donde las dos últimas dimensiones son la parte matricial.
Funciones universales sobre arrays (componente a componente)#
En este contexto, una función universal (o ufunc) es una función que actúa sobre cada componente de un array o arrays de numpy. Estas funciones son muy eficientes y se denominan vectorizadas. Por ejemplo:
M = np.arange(10)
M
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
np.sqrt(M) # raiz cuadrada de cada componente
array([0. , 1. , 1.41421356, 1.73205081, 2. ,
2.23606798, 2.44948974, 2.64575131, 2.82842712, 3. ])
np.exp(M.reshape(2,5)) # exponencial de cad componente
array([[1.00000000e+00, 2.71828183e+00, 7.38905610e+00, 2.00855369e+01,
5.45981500e+01],
[1.48413159e+02, 4.03428793e+02, 1.09663316e+03, 2.98095799e+03,
8.10308393e+03]])
Existen funciones universales que actúan sobre dos arrays, ya que realizan operaciones binarias:
x = rng.normal(0, 1, 8)
y = rng.normal(0, 1, 8)
x, y
(array([ 0.28340168, 0.03175942, 0.23305291, -0.07663605, -1.95569072,
-0.3278374 , 0.33548056, 1.60195619]),
array([ 1.18139914, -1.48250311, 1.31778755, -0.56692502, -0.04467628,
0.71133588, 0.17245226, -0.81730019]))
np.maximum(x, y)
array([ 1.18139914, 0.03175942, 1.31778755, -0.07663605, -0.04467628,
0.71133588, 0.33548056, 1.60195619])
x.max()
1.60195618625911
Expresiones condicionales vectorizadas con where#
Veamos cómo podemos usar un versión vectorizada de la función if
.
Veámoslo con un ejemplo. Supongamos que tenemos dos arrays (unidimensionales) numéricos y otro array booleano del mismo tamaño:
xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
mask = np.array([True, False, True, True, False])
Si quisiéramos obtener el array que en cada componente tiene el valor de xarr
si el correspondiente en mask
es True
, o el valor de yarr
si el correspondiente en cond
es False
, podemos hacer lo siguiente:
result = [(x if c else y) for x, y, c in zip(xarr, yarr, mask)]
result
[1.1, 2.2, 1.3, 1.4, 2.5]
Sin embargo, esto tiene dos problemas: no es lo suficientemente eficiente, y además no se traslada bien a arrays multidimensionales. Afortunadamente, tenemos np.where
para hacer esto de manera conveniente:
result = np.where(mask, xarr, yarr)
result
array([1.1, 2.2, 1.3, 1.4, 2.5])
No necesariamente el segundo y el tercer argumento tiene que ser arrays. Por ejemplo:
F = rng.normal(0, 1, (4, 4))
F, np.where(F > 0, 2, -2)
(array([[-0.06551489, -0.38220957, 0.79851062, 1.35344099],
[-0.5732897 , 0.12855593, 0.61549471, 0.18336176],
[ 1.32751715, -0.80980333, 0.24063118, 1.59242558],
[ 1.67597108, 1.12482976, -0.37612883, -0.52736448]]),
array([[-2, -2, 2, 2],
[-2, 2, 2, 2],
[ 2, -2, 2, 2],
[ 2, 2, -2, -2]]))
O una combinación de ambos. Por ejemplos, para modificar sólo las componentes positivas:
np.where(F > 0, 2, F)
array([[-0.06551489, -0.38220957, 2. , 2. ],
[-0.5732897 , 2. , 2. , 2. ],
[ 2. , -0.80980333, 2. , 2. ],
[ 2. , 2. , -0.37612883, -0.52736448]])
También existe la función np.select
para concatenar varias máscaras consecutivas.
np.select(
[np.abs(F) > 2, np.abs(F) > 1],
["Poco probable", "Algo probable"],
"Frecuente"
)
array([['Frecuente', 'Frecuente', 'Frecuente', 'Algo probable'],
['Frecuente', 'Frecuente', 'Frecuente', 'Frecuente'],
['Algo probable', 'Frecuente', 'Frecuente', 'Algo probable'],
['Algo probable', 'Algo probable', 'Frecuente', 'Frecuente']],
dtype='<U13')
Exercise 54
Crea una función que transforme un array para aplicar elemento a elemento la siguiente función
Funciones estadísticas#
Algunos métodos para calcular indicadores estadísticos sobre los elementos de un array.
np.sum
: suma de los componentesnp.mean
: media aritméticanp.std
ynp.var
: desviación estándar y varianza, respectivamente.np.max
ynp.min
: máximo y mínimo, resp.np.argmin
ynp.argmax
: índices de los mínimos o máximos elementos, respectivamente.np.cumsum
: sumas acumuladas de cada componente
Estos métodos también se pueden usar como atributos de los arrays. Es decir, por ejemplo A.sum()
o A.mean()
.
Veamos algunos ejemplos, generando en primer lugar un array con elementos generados aleatoriamente (siguiendo una distribución normal):
G = rng.normal(0, 1, (5, 4))
G
array([[ 0.26134337, 0.36482391, 0.41239245, 0.89468577],
[-1.06956386, 1.21322243, 1.26956794, 0.35043845],
[ 0.44061432, 0.21673706, 0.19928195, 0.94046782],
[-0.58565154, -0.17649961, -0.6664634 , -0.23321517],
[-2.38489933, -0.71541006, 0.03047883, 2.2654509 ]])
G.sum()
3.0278022058539333
G.mean()
0.15139011029269667
G.cumsum() # por defecto, se aplana el array y se hace la suma acumulada
array([0.26134337, 0.62616728, 1.03855973, 1.9332455 , 0.86368163,
2.07690406, 3.34647199, 3.69691044, 4.13752477, 4.35426183,
4.55354378, 5.49401159, 4.90836005, 4.73186044, 4.06539703,
3.83218187, 1.44728254, 0.73187248, 0.7623513 , 3.02780221])
Todas estas funciones se pueden aplicar a lo largo de un eje, usando el parámetro axis
. Por ejemplos, para calcular las medias de cada fila (es decir, recorriendo en el sentido de las columnas), aplicamos mean
por axis=1
:
print(G)
[[ 0.26134337 0.36482391 0.41239245 0.89468577]
[-1.06956386 1.21322243 1.26956794 0.35043845]
[ 0.44061432 0.21673706 0.19928195 0.94046782]
[-0.58565154 -0.17649961 -0.6664634 -0.23321517]
[-2.38489933 -0.71541006 0.03047883 2.2654509 ]]
G.mean(axis=1)
array([ 0.48331137, 0.44091624, 0.44927529, -0.41545743, -0.20109492])
Y la suma de cada columna (es decir, recorriendo las filas), con sum
por axis=0
:
G.sum(axis=0)
array([-3.33815704, 0.90287372, 1.24525776, 4.21782777])
Suma acumulada de cada columna:
G.cumsum(axis=0)
array([[ 0.26134337, 0.36482391, 0.41239245, 0.89468577],
[-0.8082205 , 1.57804634, 1.68196038, 1.24512422],
[-0.36760617, 1.7947834 , 1.88124233, 2.18559203],
[-0.95325772, 1.61828378, 1.21477893, 1.95237687],
[-3.33815704, 0.90287372, 1.24525776, 4.21782777]])
Dentro de cada columna, el número de fila donde se alcanza el mínimo se puede hacer asi:
G, G.argmin(axis=0)
(array([[ 0.26134337, 0.36482391, 0.41239245, 0.89468577],
[-1.06956386, 1.21322243, 1.26956794, 0.35043845],
[ 0.44061432, 0.21673706, 0.19928195, 0.94046782],
[-0.58565154, -0.17649961, -0.6664634 , -0.23321517],
[-2.38489933, -0.71541006, 0.03047883, 2.2654509 ]]),
array([4, 4, 3, 3]))
Métodos para arrays booleanos#
H = rng.normal(0, 1, 50)
H
array([ 0.22918441, 1.96962191, 0.96607744, -0.28192589, 0.06229471,
1.34325204, -0.29634434, -0.70406405, 0.53015669, -0.61150176,
0.96244101, -0.36287924, 1.13924256, 1.23207867, -0.02356111,
0.91563052, -0.12796518, -1.11677978, -0.0274941 , -1.92041368,
-0.91610575, 1.95510099, 1.27037163, 1.63111066, 1.40136138,
1.52639233, -0.09556764, -0.87587349, -0.45726439, 0.90125016,
1.18264519, -1.07424022, 0.33128043, 0.44047932, -0.2008745 ,
0.99140371, 0.62753346, -0.32816418, 1.48483719, 1.88926055,
0.16499433, 1.71586951, -1.08086733, 0.38611372, 1.21696416,
0.123246 , 0.28690981, 0.85864404, -0.37883556, -0.69367888])
Es bastante frecuente usar sum
para ontar el número de veces que se cumple una condición en un array, aprovechando que True
se identifica con 1 y False
con 0:
(H > 0).sum() # Number of positive values
30
Las funciones python any
y all
tienen también su correspondiente versión vectorizada. any
se puede ver como un or generalizado, y all
como un and generalizado:
bools = np.array([False, False, True, False])
bools.any(), bools.all()
(True, False)
Podemos comprobar si se cumple alguna vez una condición entre los componentes de un array, o bien si se cumple siempre una condición:
np.any(H > 0)
True
np.all(H < 10)
True
np.any(H > 15)
False
np.all(H > 0)
False
Entrada y salida de arrays en ficheros#
Existen una serie de utilidades para guardar el contenido de un array en un fichero y recuperarlo más tarde.
Las funciones save
y load
hacen esto. Los arrays se almacenan en archivos con extensión npy.
J = np.arange(10)
np.save('un_array', J)
np.load('un_array.npy')
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Con savez
, podemos guardar una serie de arrays en un archivo de extensión npz, asociados a una serie de claves. Por ejemplo:
np.savez('array_archivo.npz', a=J, b=J**2)
Cuando hacemos load
sobre un archivo npz, cargamos un objeto de tipo diccionario, con el que podemos acceder (de manera perezosa) a los distintos arrays que se han almacenado:
arch = np.load('array_archivo.npz')
arch['b']
array([ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81])
arch['a']
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
list(arch)
['a', 'b']
En caso de que fuera necesario, podríamos incluso guardar incluso los datos en formato comprimido con savez_compressed
:
np.savez_compressed('arrays_comprimidos.npz', a=J, b=J**2)
!ls -lah
total 28K
drwxr-xr-x 1 root root 4.0K Nov 20 19:48 .
drwxr-xr-x 1 root root 4.0K Nov 20 19:28 ..
-rw-r--r-- 1 root root 650 Nov 20 19:48 array_archivo.npz
-rw-r--r-- 1 root root 424 Nov 20 19:48 arrays_comprimidos.npz
drwxr-xr-x 4 root root 4.0K Nov 17 14:33 .config
drwxr-xr-x 1 root root 4.0K Nov 17 14:37 sample_data
-rw-r--r-- 1 root root 208 Nov 20 19:48 un_array.npy
!rm un_array.npy
!rm array_archivo.npz
!rm arrays_comprimidos.npz