# Python NumPy For Your Grandma - 3.1 Broadcasting

Contents

Of all the challenging concepts in NumPy, the one that’s most crucial to wrap your head around is probably broadcasting. In this video, we’ll see how broadcasting works and why it’s useful.

So, what is broadcasting? Well, we’ve already seen it in action although you might not have picked up on it. When we add a scalar to a 1d array like this, the scalar gets added to each element of the array.

``````import numpy as np
np.array([1,2,3]) + 0.5
## array([1.5, 2.5, 3.5])
``````

In essence, NumPy is expanding the scalar into 3-element array and then doing element-wise addition between the arrays. Of course under the hood, NumPy doesn’t actually do this because it’d be horribly inefficient, but in essence that’s what’s happening, and that’s an example of broadcasting.

In two dimensions, you could imagine we have a matrix of values, `foo`, like this

``````foo = np.array([
[1,1,1],
[2,2,2]
])
``````

to which we’ll add a 1d array, `boo`, with the same number of elements as our matrix has columns.

``````boo = np.array([1,0,-1])
foo + boo
## array([[2, 1, 0],
##        [3, 2, 1]])
``````

In this case, NumPy carries out the addition as if it first copies `boo` along a new vertical axis to match the shape of `foo`, and then does normal element-wise addition.

Now, what if `boo` had two elements.. perhaps NumPy would add `boo` to each column of `foo`

``````foo + boo[:2]  # error
``````

Nope. In this case we get an error. So how does broadcasting work and when can we use it?

Formally speaking, suppose we want to add two arrays, `A` and `B`.

1. Moving backwards from the last dimension of each array, we check if their dimensions are compatible. Dimensions are compatible if they are equal or either of them is 1.
2. If all of `A`’s dimensions are compatible with `B`’s dimensions, or vice versa, they are compatible arrays.

Informally speaking, I think it really helps to visualize the arrays you’re working with and how they’d expand for element-wise operations. Let’s see some examples.

``````np.random.seed(1234)

A = np.random.randint(low = 1, high = 10, size = (3, 4))
print(A)
## [[4 7 6 5]
##  [9 2 8 7]
##  [9 1 6 1]]
B = np.random.randint(low = 1, high = 10, size = (3, 1))
print(B)
## [[7]
##  [3]
##  [1]]
``````

What do you think `A + B` will return?

``````A + B
## array([[11, 14, 13, 12],
##        [12,  5, 11, 10],
##        [10,  2,  7,  2]])
``````

Here, `A` is a 3x4 array and `B` is a 3x1 array. We start by comparing the last dimension of each array. Since the last dimension of `A` is 4 and the last dimension of `B` is 1, NumPy can expand `B` by making 4 copies of it along its second axis. So, these dimensions are compatible. Now we have to compare the 1st dimension of `A` and `B`. Since they’re both 3, they’re compatible. The only thing left for NumPy is to carry out whatever procedure we wanted on two equivalently sized 3x4 arrays. (Remember, NumPy doesn’t actually expand `B` like this because it’d be horribly inefficient.)

Let’s see another example.

``````np.random.seed(4321)

A = np.random.randint(low = 1, high = 10, size = (4, 4))
print(A)
## [[3 9 3 2]
##  [8 6 3 5]
##  [7 1 9 7]
##  [6 4 2 2]]
B = np.random.randint(low = 1, high = 10, size = (2, 1))
print(B)
## [[7]
##  [2]]
``````

Again, what do you think `A + B` will return?

``````A + B  # ValueError: operands could not be broadcast together with shapes (4,4) (2,1)
``````

Here, `A` is a 4x4 array and `B` is a 2x1 array. The last dimension of `A` is 4 and the last dimension of `B` is 1, so these dimensions are compatible, and just like the last example we can temporarily transform `B` by making 4 copies of it along its 2nd axis. Now we compare the 1st dimension of each array. In this case, there isn’t an obvious way to expand `B` into a 4x4 array to match `A` or vice versa, so these arrays are not compatible.

Let’s see another example where `A` has 3 dimensions and `B` has 2 dimensions.

``````np.random.seed(1111)

A = np.random.randint(low = 1, high = 10, size = (3, 1, 4))
print(A)
## [[[8 6 2 3]]
##
##  [[5 9 7 5]]
##
##  [[9 7 3 7]]]
B = np.random.randint(low = 1, high = 10, size = (2, 1))
print(B)
## [[9]
##  [4]]
``````

Once again, what do you think `A + B` will return?

``````A + B
## array([[[17, 15, 11, 12],
##         [12, 10,  6,  7]],
##
##        [[14, 18, 16, 14],
##         [ 9, 13, 11,  9]],
##
##        [[18, 16, 12, 16],
##         [13, 11,  7, 11]]])
``````

As before, we start by comparing the last dimension of each array. In this case, `A` is 4 and `B` is 1, so we can expand `B` into a 2x4 array, making these dimensions compatible. Next, we compare the 2nd to last dimension of each array. In this case, `A` is 1 and `B` is 2. This time, we expand `A`, copying it twice along its second axis to match `B`. At this point, we’re out of `B` dimensions, so we know `A` and `B` are compatible. To complete our mental model of how math between these arrays would work, we can imagine copying `B` three times along a newly added 1st dimension. We’re left with two transformed arrays, each with shape 3x2x4, which we can easily add or subtract, or combine in some other way.

Before I close out this lecture, I just want to say that when I do this sort of broadcasting expansion process in my head, I don’t actually go through this formal process, thinking of the shape tuples. I kind of just visualize the shape of these arrays and I visualize how they’d expand into identical sizes.