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

\[\begin{split} f(x) = \begin{cases} \exp(x/2) & \text{si } x < 0 \\ 1-x & \text{si } 0 \leq x \leq 1 \\ 0 & \text{si } x > 1 \end{cases} \end{split}\]

Funciones estadísticas#

Algunos métodos para calcular indicadores estadísticos sobre los elementos de un array.

  • np.sum: suma de los componentes

  • np.mean: media aritmética

  • np.std y np.var: desviación estándar y varianza, respectivamente.

  • np.max y np.min: máximo y mínimo, resp.

  • np.argmin y np.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 allcomo 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