Broadcasting#
Hemos visto como la vectorización es la estrategia que permite eliminar bucles lentos. Se denomina broadcasting en este contexto a las reglas que permiten aplicar la vectorización a arrays de diferente tamaño.
import numpy as np
a = np.array([0, 1, 2])
b = np.array([4, 4, 4])
a + b
array([4, 5, 6])
Supongamos ahora que queremos aplicar la suma con un escalar
a + 4
array([4, 5, 6])
El resultado es equivalente a extender o difundir (broadcast) el escalar al tamaño completo del array. Es importante recalcar que esta extensión no se realiza en memoria (no se crea un array nuevo) pero es un modelo para entender lo que sucede.
Veamos que sucede si extendemos la operación a un caso bidimensional.
c = np.ones((3, 3))
print('c=', c)
a + c
c= [[1. 1. 1.]
[1. 1. 1.]
[1. 1. 1.]]
array([[1., 2., 3.],
[1., 2., 3.],
[1., 2., 3.]])
En este caso, el array a
se ha extendido por filas.
Reglas de broadcasting#
Numpy tiene unas reglas que determinan cómo se propagan los valores cuando hay interacción entre arrays. Se comparan las dimensiones de los dos arrays de derecha a izquierda. Los valores son compatibles si:
Son iguales
Uno de ellos es uno
Uno de ellos está vacío
En caso contrario se lanza la excepción ValueError: operands could not be broadcast together
.
Veamos algunos ejemplos.
Ejemplo 1#
c = np.ones((3, 3))
a + c
array([[1., 2., 3.],
[1., 2., 3.],
[1., 2., 3.]])
c (2d) 3 x 3 a (1d) 3 resultado (2d) 3 x 3
Empezando por la derecha, la dimensión 3 es compatible. La siguiente
es 3 para c
y no existe para a
. Así que en esa dimensión a
adquiere una
dimensión 1 (1x3) y luego se replica (por filas).
Ejemplo 2#
¿Cómo lograríamos sumar por columnas en lugar de filas?. Dado que ejes se emparejan por la derecha, tendríamos que tener una distribución así:
c (2d) 3 x 3 b (2d) 3 x 1 resultado (2d) 3 x 3
Sin embargo, el array b
no puede ser 1-dimensional, dado que en numpy
los arrays 1d son siempre vectores fila.
Podemos convertir el array a
en otro array b
2d, pero con una dimensión igual a 1 (vector columna o fila) usando np.newaxis
o reshape
b = a[:, np.newaxis]
print(b.shape)
b = a.reshape((3, 1))
print(b.shape)
(3, 1)
(3, 1)
La forma con np.newaxis
es más práctica ya permite añadir una dimensión
sin tener que saber el tamaño del array
a[:, np.newaxis] + c
array([[1., 1., 1.],
[2., 2., 2.],
[3., 3., 3.]])
Ahora ya tenemos:
c (2d) 3 x 3 b (2d) 3 x 1 resultado (2d) 3 x 3
Y como se ve, el array se replica por columnas.
Ejemplo 3#
Supongamos que queremos multiplicar dos vectores de diferente dimensión
a = np.arange(3)
b = np.arange(4)
a * b
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[7], line 3
1 a = np.arange(3)
2 b = np.arange(4)
----> 3 a * b
ValueError: operands could not be broadcast together with shapes (3,) (4,)
Se produce un error ya que se incumplen las reglas de broadcast.
a (1d) 3 b (1d) 4 resultado error
Para que sea posible el producto, tenemos que añadir una dimensión extra o bien
a a
o bien a b
. El tamaño final depende de a qué array se añada:
a (2d) 3x1 b (1d) 4 resultado (2d) 3x4
y también es posible:
a (1d) 3 b (2d) 4x1 resultado (2d) 4x3
Para el primer caso:
a[:, np.newaxis] * b
array([[0, 0, 0, 0],
[0, 1, 2, 3],
[0, 2, 4, 6]])
y para el segundo:
a * b[:, np.newaxis]
array([[0, 0, 0],
[0, 1, 2],
[0, 2, 4],
[0, 3, 6]])
Ejemplo 4#
Un ejemplo útil es la generación de funciones sobre grid. Supongamos que queremos evaluar \(f(x, y) = \cos (x + 2y) \sin(x^2 - y)\)
Podemos hacer:
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
fxy = np.cos(x + 2 * y) * np.sin(x**2 - y)
print(fxy.shape)
(100,)
Sin embargo, el resultado es unidimensional, no 2d. Tanto x
como y
tienen de tamaño (100,)
, por lo que se multiplican como 1d.
Para conseguir que fxy
sea bidimensional, tenemos que hacer x
o y
bidimensional añadiendo un eje con np.newaxis
.
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)[:, np.newaxis]
fxy = np.cos(x + 2 * y) * np.sin(x**2 - y)
# Podemos pintar la función con matplotlib
import matplotlib.pyplot as plt
plt.imshow(fxy, extent=[-5, 5, -5, 5]);
print('Shape:', fxy.shape)
Shape: (100, 100)

Ejemplo 5#
Como último ejemplo, vamos a centrar un array tabular restando la media por columnas. Empezamos construyendo un array que contendrá la tabla, con 4 columnas (las características) y 100 filas (los objetos). Rellenamos cada columna con datos aleatorios de una distribución normal diferente media y varianza.
arr = np.random.normal(loc=[10,0.1,2.3,-1.0],
scale=[2.1,0.03,0.5,0.2],
size=(100, 4)
)
# la media se calcula colapsando las filas
mean_arr = np.mean(arr, axis=0)
# dadas las dimensiones (100, 4) y (4,), podemos restar
# directamente
res = arr - mean_arr
# y la media es:
print(np.mean(res, axis=0))
# que son valores compatibles con 0
[-2.38031816e-15 -1.82492910e-17 -8.43769499e-17 -2.29816166e-16]