The idea
You've seen prices * 1.11: one number applied to every element. And prices * units: two arrays of the same shape, element by element. Broadcasting is the rule that connects these: NumPy can combine arrays of different shapes by stretching the smaller one to fit, as long as the shapes are compatible.
It sounds abstract, but you'll use it constantly: apply a price per month to a stores × months grid, subtract each store's target from its row, compare every product against every discount level. All without a single loop.
The data
The examples in this module use the units-sold grid again, now with four stores (rows) and six months (columns):
import numpy as np
stores = ["Jakarta", "Bandung", "Surabaya", "Medan"]
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
units = np.array([
[120, 135, 150, 160, 155, 170],
[ 80, 85, 90, 88, 95, 100],
[ 95, 92, 105, 110, 108, 115],
[ 40, 45, 43, 50, 52, 55],
])
print(units.shape)
(4, 6)
A scalar: the simplest broadcast
You've already used broadcasting without knowing its name. Multiplying the whole grid by one number, say to plan for doubling every store's stock, works because NumPy applies that number to every cell:
print(units * 2)
[[240 270 300 320 310 340]
[160 170 180 176 190 200]
[190 184 210 220 216 230]
[ 80 90 86 100 104 110]]
The single number is "stretched" to the shape of the grid. NumPy doesn't actually make copies; it just reuses the value.
A row across every row
The selling price changed each month (thousand Rupiah). Revenue per store per month is units × that month's price:
monthly_price = np.array([95, 95, 100, 100, 105, 105]) # shape (6,)
revenue = units * monthly_price
print(revenue)
[[11400 12825 15000 16000 16275 17850]
[ 7600 8075 9000 8800 9975 10500]
[ 9025 8740 10500 11000 11340 12075]
[ 3800 4275 4300 5000 5460 5775]]
units is (4, 6) and monthly_price is (6,). NumPy lines the shapes up from the right: the 6 matches the 6, and the price row is reused for each of the 4 stores.
A column across every column
Each store has a monthly target. To subtract it from its own row, the targets must be a column, shape (4, 1):
targets = np.array([150, 90, 100, 50]).reshape(4, 1)
print(targets)
print(units - targets)
[[150]
[ 90]
[100]
[ 50]]
[[-30 -15 0 10 5 20]
[-10 -5 0 -2 5 10]
[ -5 -8 5 10 8 15]
[-10 -5 -7 0 2 5]]
The (4, 1) column is stretched across the 6 months. Positive numbers beat the target, negative ones missed it.
What if you forget to make it a column?
flat_targets = np.array([150, 90, 100, 50]) # shape (4,)
print(units - flat_targets)
ValueError: operands could not be broadcast together with shapes (4,6) (4,)
Lining up from the right, 6 (months) meets 4 (targets): they don't match, and neither is 1, so NumPy refuses. That error is a gift: it means your shapes don't mean what you think.
The rules
Compare the shapes from the right, dimension by dimension. Two dimensions are compatible when they're equal, or one of them is 1 (a missing dimension counts as 1).
| Shape A | Shape B | Result | Why |
|---|---|---|---|
(4, 6) | () scalar | (4, 6) | scalar fits everything |
(4, 6) | (6,) | (4, 6) | 6 = 6; missing → 1 → stretched to 4 |
(4, 6) | (4, 1) | (4, 6) | 1 stretched to 6; 4 = 4 |
(4, 1) | (1, 6) | (4, 6) | both stretched |
(4, 6) | (4,) | error | 6 ≠ 4 and neither is 1 |
For "one value per column", use shape (columns,). For "one value per row", use shape (rows, 1). When in doubt, print .shape on both sides.
Every combination: a column meets a row
When a column (n, 1) meets a row (m,), broadcasting builds an n × m table of every combination. A price table for 3 discount levels × 5 products:
prices = np.array([185, 320, 95, 65, 110])
discounts = np.array([0.0, 0.1, 0.2]).reshape(3, 1)
table = prices * (1 - discounts)
print(table.shape)
print(table)
(3, 5)
[[185. 320. 95. 65. 110. ]
[166.5 288. 85.5 58.5 99. ]
[148. 256. 76. 52. 88. ]]
Row 0 is full price, row 1 is 10% off, row 2 is 20% off. In a spreadsheet you'd build this with mixed $A1 references; here it's one line.
Universal functions (ufuncs)
Arithmetic isn't the only thing that works on whole arrays. NumPy's maths functions, like square roots, logarithms, rounding and absolute values, also apply to every element at once. They're called universal functions, or ufuncs:
x = np.array([1.0, 4.0, 9.0, 16.0])
print(np.sqrt(x))
print(np.log10(np.array([10, 100, 1000])))
print(np.round(np.array([1.234, 5.678]), 1))
print(np.abs(np.array([-3, 2, -1])))
[1. 2. 3. 4.]
[1. 2. 3.]
[1.2 5.7]
[3 2 1]
np.maximum and np.minimum compare two arrays (or an array and a number) element by element, which is handy for floors and caps:
gap = units - targets
shortfall = np.maximum(-gap, 0) # how far below target, 0 if met
print(shortfall)
[[30 15 0 0 0 0]
[10 5 0 2 0 0]
[ 5 8 0 0 0 0]
[10 5 7 0 0 0]]
Don't confuse np.maximum(a, b) (element-wise between two arrays) with np.max(a) (the single largest value), which the next module covers.
Division by zero
Dividing by zero doesn't crash: NumPy returns inf (infinity) or nan (not a number) and prints a warning:
growth = np.array([10, 0, 5]) / np.array([2, 0, 0])
print(growth)
RuntimeWarning: divide by zero encountered in divide
RuntimeWarning: invalid value encountered in divide
[ 5. nan inf]
0/0 is nan, 5/0 is inf. Both spread through later calculations, so check for them (np.isnan, np.isinf) or guard the denominator with np.where.
Why this matters for clean code
Broadcasting replaces nested loops with one expression that matches the maths: units - targets instead of two for loops with index bookkeeping. The price is that shapes become part of your code's meaning, so make them obvious: name your columns (targets reshaped to (4, 1)), and print shapes when you're unsure.
Recap
- Arithmetic between arrays works element by element; broadcasting stretches size-1 (or missing) dimensions.
- Compare shapes from the right; each pair must be equal or contain a 1.
- One value per column: shape
(cols,). One per row: shape(rows, 1). - A column
(n, 1)with a row(m,)gives every combination(n, m). - Ufuncs (
np.sqrt,np.log10,np.abs,np.round,np.maximum...) work element-wise. - Division by zero gives
inf/nan, not an error.