Broadcasting: the number, the row and the column
In short: Broadcasting is the rule by which NumPy computes with arrays of different shapes: a number, a row or a column is stretched to the shape of the table when the sizes fit.
Problem
“How much did each product make each week?”
The manager gives you the units sold over the last 4 weeks for three products: white bread, fresh milk and apples. A table with 4 rows (the weeks) and 3 columns (the products). The prices are €1.20, €1.60 and €2.10.
They want the revenue in a table of the same kind: week by product. With Python lists that is two nested for loops, row by row and cell by cell.
In NumPy calculations work on the whole array. But here there are two arrays with different shapes: a 4 × 3 table and a row of 3 prices. How do you multiply one by the other?
The idea
The number and the array
Start with the simplest case. The supplier raises every price by 20%. products['price'].to_numpy() gives the price column as a NumPy array, and the multiplication is by a single number:
25 prices times one number gives 25 new prices: €1.20 becomes €1.44, and the €39 knife set becomes €46.80. The number is “stretched” to the shape of the array: as if a 1.2 stood next to every price.
This stretching is called broadcasting. The number is only the first case. The same happens with a row and a table.
The idea
The row and the table
The units are a (4, 3) table, the prices are a (3,) row. Multiply them directly:
The row of prices is repeated for each of the 4 weeks, and every cell is multiplied by the price of its column. The first week: 30 loaves at €1.20 are €36, 22 milks at €1.60 are €35.20, 14 kg of apples at €2.10 are €29.40. Not a single for.
When does NumPy stretch and when does it stop? There is one rule:
- Shapes are compared from right to left. (4, 3) and (3,): first 3 and 3, then 4 and the empty place.
- The sizes must be equal, or one of them must be 1. An empty place counts as 1.
- The 1 stretches to the other size. Here the (3,) row becomes (1, 3) and stretches to (4, 3).
More precisely, if you are curious
NumPy does not copy the row 4 times in memory. It reads the same 3 prices again for every row. That is why broadcasting is both fast and light on memory, even when the table has millions of rows.
The number from the previous card is an array of shape (), with no sizes. Ones are added in front of it until it is (1,) or (1, 1), and then it stretches. The rule is the same.
See it
How the row fits the table
Here is the same calculation slowly: the 4 × 3 table, the row of 3 prices, the stretch downwards and the multiplication cell by cell.
import numpy as np
# units over 4 weeks: white bread, fresh milk, applesunits = np.array([[30, 22, 14], [26, 25, 18], [34, 20, 12], [28, 27, 20]])prices = np.array([1.2, 1.6, 2.1])
revenue = units * pricesprint(units.shape, prices.shape, revenue.shape)revenueStep by step, in words
- units is a (4, 3) table: the units for 4 weeks, one column per product.
- prices is a (3,) row: €1.20, €1.60, €2.10. One price per product.
- Shapes are compared from the right: 3 and 3 are equal. To the left of (3,) there is nothing, which counts as 1, so the row becomes (1, 3).
- The 1 stretches to 4: the row of prices repeats for every week and becomes (4, 3).
- Now cell by cell. The first week: 30 × 1.2 = 36, 22 × 1.6 = 35.2, 14 × 2.1 = 29.4.
- revenue is (4, 3), like units: 12 revenues from one multiplication, without a single for.
Try it
Do the shapes fit?
The idea
When shapes do not fit
Now the manager asks something else: which product sold above the week’s average in which week. You need to subtract each row’s mean from that same row.
units.mean(axis=1) gives the mean units for each week (axis is from the previous lesson: the size that disappears). That is 4 numbers. Subtract them from the table:
The means are 22, 23, 22 and 25, with shape (4,). NumPy compares from the right: 3 from the table against 4 from the means. They are not equal and neither is 1, so it stops: operands could not be broadcast together with shapes (4,3) (4,).
The error is useful. A row of 4 numbers really cannot lie over 3 columns.
The idea
The column and the table
The 4 means need to be a column: one number per row, stretched to the right across the products. A column has shape (4, 1). There are two ways to get it:
week_mean[:, None]:Nonein second place adds a size of 1;week_mean.reshape(4, 1): the same, with the shape written out.
On the right there are now 3 and 1 (the 1 stretches to 3), then 4 and 4. The result is a (4, 3) table: in the third week bread is 12 units above the average, and apples are 10 below it.
In the second week everything was 10% off, and in the fourth week 20% off. discount holds the discount for each week. Store in after the revenue after the discount, a 4 × 3 table.
Solution
import numpy as np
units = np.array([[30, 22, 14],
[26, 25, 18],
[34, 20, 12],
[28, 27, 20]])
prices = np.array([1.2, 1.6, 2.1])
revenue = units * prices
discount = np.array([0, 0.1, 0, 0.2])
after = revenue * (1 - discount[:, None])
afterafter = revenue * (1 - discount[:, None]). The discount is per week, so it has to be a (4, 1) column. Without [:, None] NumPy compares 3 with 4 and raises an error. The second week becomes €28.08, €36 and €34.02, and the fourth €26.88, €34.56 and €33.60. Weeks without a discount stay the same. With revenue - discount[:, None] you subtract 10 cents from every cell, not 10%.
The idea
The silent trap: 3 × 3 instead of 3
An error from NumPy shows up at once. It is more dangerous when the shapes fit and the result is something else.
A colleague sends you the new prices, copied from a column in Excel. The array is a (3, 1) column, not a (3,) row. You subtract the old prices to see how much they went up:
You expect 3 differences and get 9. The (3, 1) column stretched to the right, the (3,) row stretched down, and every new price was subtracted from every old one. Only the diagonal (0.1, 0 and 0.2) is the difference you want. The other numbers mean nothing, and NumPy says nothing: the shapes fit the rule.
So after every calculation with different shapes, print .shape. If you expect (3,) and see (3, 3), something stretched without you asking.
Try it
How much did prices go up
new_prices is a (3, 1) column with the new prices. Store in change how much the price of each product went up: 3 numbers, shape (3,).
Solution
import numpy as np
prices = np.array([1.2, 1.6, 2.1])
new_prices = np.array([[1.3], [1.6], [2.3]])
change = new_prices.reshape(3) - prices
changechange = new_prices.reshape(3) - prices. reshape(3) turns the column into a (3,) row, and a row minus a row gives 3 differences: bread goes up by €0.10, milk stays at €1.60, and apples go up by €0.20. new_prices - prices runs without an error and gives a 3 × 3 table: that is the trap.
Exercise
How much more with the new prices
Back to the manager. They want to know how much more each product would have made each week if the new prices had applied already.
units are the units (4, 3), prices the old prices (3,), new_prices the new prices as a (3, 1) column. Store in extra how much higher the revenue would have been: a 4 × 3 table, week by product.
Solution
import numpy as np
units = np.array([[30, 22, 14],
[26, 25, 18],
[34, 20, 12],
[28, 27, 20]])
prices = np.array([1.2, 1.6, 2.1])
new_prices = np.array([[1.3], [1.6], [2.3]])
extra = units * (new_prices.reshape(3) - prices)
extraextra = units * (new_prices.reshape(3) - prices). The price difference is a (3,) row and stretches across the 4 weeks like the prices in the scene. The first week gives €3 from bread, 0 from milk and €2.80 from apples. Over the four weeks together it is €24.60.
With units * (new_prices - prices) the difference becomes a 3 × 3 table (the silent trap), and then (4, 3) times (3, 3) no longer fits and NumPy raises an error. This time the trap catches itself, but that is not always so.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: The first table · The NumPy array · Vectorization · Summaries and missing values
- Continues in: Honest charts · Sample and population
- Tasks on this topic: NumPy