Broadcasting
In the last lesson, every operation used either two arrays of the same shape or one array and a single number. Real problems often need something in between: divide every column of a marks table by that subject's maximum, or subtract each student's own average from their marks.
NumPy solves this with broadcasting: a set of rules that lets arrays of different shapes work together by stretching the smaller one, without actually copying any data. Once you understand the rules, you can replace many loops with one line.
You already know the simplest case
When you write prices * 1.18, NumPy treats the single number as if it were an array of the same shape as prices, filled with 1.18. That is broadcasting. The same idea works with a 1-D array and a 2-D array.
import numpy as np
# rows = 4 students, columns = maths (out of 100), science (out of 80), hindi (out of 50)
marks = np.array([
[72, 61, 40],
[95, 70, 45],
[48, 39, 31],
[83, 66, 47],
])
max_marks = np.array([100, 80, 50])
percent = marks / max_marks * 100
print(marks.shape, max_marks.shape)
print(percent.round(1))(4, 3) (3,) [[72. 76.2 80. ] [95. 87.5 90. ] [48. 48.8 62. ] [83. 82.5 94. ]]
marks has shape (4, 3) and max_marks has shape (3,). NumPy lines them up from the right, sees that both end in 3, and uses the same row of maximums for every student. Each mark is divided by the maximum for its own column.
The broadcasting rules
NumPy compares the two shapes starting from the last dimension and moving left. For each pair of sizes:
- If they are equal, fine.
- If one of them is 1, that array is stretched to match the other.
- If one array has fewer dimensions, it is treated as having size 1 in the missing ones on the left.
- Otherwise NumPy raises an error.
| Shape A | Shape B | Result | Why |
|---|---|---|---|
(4, 3) | (3,) | (4, 3) | B becomes (1, 3), then is stretched down |
(4, 3) | (4, 1) | (4, 3) | B's single column is stretched across |
(4, 1) | (1, 3) | (4, 3) | Both are stretched: an "outer" table |
(4, 3) | (4,) | error | Last sizes 3 and 4 do not match |
The last row is the classic mistake. Let us see it, and the fix.
Working per row: keepdims and newaxis
Suppose you want to know how each student did in each subject compared with their own average. Each student's average is one number per row, a shape of (4,). Subtracting it directly from a (4, 3) array fails, because NumPy lines up the 4 with the 3.
import numpy as np
percent = np.array([
[72.0, 76.2, 80.0],
[95.0, 87.5, 90.0],
[48.0, 48.8, 62.0],
[83.0, 82.5, 94.0],
])
row_avg = percent.mean(axis=1)
print(row_avg.shape)
try:
percent - row_avg
except ValueError as err:
print("Error:", err)
row_avg_col = percent.mean(axis=1, keepdims=True) # shape (4, 1)
print(row_avg_col.shape)
print((percent - row_avg_col).round(1))
print((percent - row_avg[:, np.newaxis]).round(1)[0]) # same thing, other spelling(4,) Error: operands could not be broadcast together with shapes (4,3) (4,) (4, 1) [[-4.1 0.1 3.9] [ 4.2 -3.3 -0.8] [-4.9 -4.1 9.1] [-3.5 -4. 7.5]] [-4.1 0.1 3.9]
keepdims=True keeps the averaged axis with size 1, so the result is a column of shape (4, 1). Now the rules stretch that column across the three subjects. Positive numbers show subjects where a student did better than their own average. row_avg[:, np.newaxis] is another way to turn a 1-D array into a column; np.newaxis is simply None, so you will also see row_avg[:, None] in other people's code.
Building tables from two lists
When one array is a column (n, 1) and the other a row (1, m), broadcasting builds a full (n, m) table. This is perfect for "every combination" problems, such as a price list showing each product at several tax rates.
import numpy as np
products = np.array(["Earbuds", "Pressure cooker", "Notebook"])
base_price = np.array([1499, 1799, 60])
gst_rates = np.array([0.05, 0.18])
final = base_price[:, np.newaxis] * (1 + gst_rates) # (3, 1) with (2,) gives (3, 2)
print(final.shape)
for name, row in zip(products, final.round(2)):
print(f"{name:<16} 5%: ₹{row[0]:>8.2f} 18%: ₹{row[1]:>8.2f}")
qty = np.arange(1, 6)
print(np.outer(base_price[:2], qty)) # np.outer does the same for products of two 1-D arrays(3, 2) Earbuds 5%: ₹ 1573.95 18%: ₹ 1768.82 Pressure cooker 5%: ₹ 1888.95 18%: ₹ 2122.82 Notebook 5%: ₹ 63.00 18%: ₹ 70.80 [[1499 2998 4497 5996 7495] [1799 3598 5397 7196 8995]]
The loop here only formats the printout; the maths itself was a single broadcast multiplication. np.outer() is a shortcut for the common case of multiplying every value of one 1-D array by every value of another.
Advanced: standardising columns
Many statistics and machine learning methods expect each column to be on the same scale. Standardising (computing z-scores) means subtracting each column's mean and dividing by its standard deviation, so every column ends up with mean 0 and standard deviation 1. With broadcasting it is one line.
import numpy as np
# columns: study hours per day, attendance %, maths marks
data = np.array([
[1.5, 78, 52],
[3.0, 92, 81],
[2.0, 85, 64],
[4.5, 96, 90],
[0.5, 70, 41],
])
z = (data - data.mean(axis=0)) / data.std(axis=0)
print(z.round(2))
print("Column means:", z.mean(axis=0).round(10))
print("Column std: ", z.std(axis=0).round(10))[[-0.59 -0.66 -0.75] [ 0.51 0.83 0.85] [-0.22 0.09 -0.09] [ 1.61 1.26 1.35] [-1.32 -1.51 -1.36]] Column means: [ 0. -0. 0.] Column std: [1. 1. 1.]
data.mean(axis=0) has shape (3,), one mean per column, which broadcasts down all five rows. After standardising, a value of 1.5 means "1.5 standard deviations above the average for that column", so you can compare hours, attendance and marks fairly. The check lines round away tiny floating-point leftovers; -0. is simply zero with a minus sign left over from rounding a very small negative number.
Common mistakes
- Subtracting row statistics without
keepdims. A(4,)array lines up with the columns, not the rows. Usekeepdims=Trueor[:, np.newaxis]. - Reading the error message backwards.
operands could not be broadcast together with shapes (4,3) (4,)lists both shapes; compare them from the right to see which sizes clash. - Surprise outer tables. Adding a
(n, 1)column to a(n,)row gives an(n, n)table, notnvalues. Print shapes when in doubt.
Key points
- Broadcasting lets NumPy combine arrays of different shapes without loops or copies.
- Shapes are compared from the right; sizes must be equal or one of them must be 1.
keepdims=Trueor[:, np.newaxis]turns per-row results into a column that broadcasts across.- A column
(n, 1)combined with a row(m,)produces an(n, m)table of every combination. - Standardising columns,
(X - X.mean(axis=0)) / X.std(axis=0), is a common broadcasting pattern.
Exercise
Three shops (rows) sold these units of four products (columns): [[10, 4, 7, 0], [3, 9, 2, 5], [6, 6, 6, 6]]. The prices are ₹40, ₹120, ₹65 and ₹250. Use broadcasting to compute the revenue table, then the revenue per shop. Finally, show each product's share of its shop's revenue as a percentage rounded to one decimal.
Show solution
import numpy as np
units = np.array([[10, 4, 7, 0], [3, 9, 2, 5], [6, 6, 6, 6]])
price = np.array([40, 120, 65, 250])
revenue = units * price
shop_total = revenue.sum(axis=1, keepdims=True)
print(revenue)
print(shop_total.ravel())
print((revenue / shop_total * 100).round(1))[[ 400 480 455 0] [ 120 1080 130 1250] [ 240 720 390 1500]] [1335 2580 2850] [[30. 36. 34.1 0. ] [ 4.7 41.9 5. 48.4] [ 8.4 25.3 13.7 52.6]]
units * price broadcasts the (4,) price row down the three shops. Keeping the totals as a (3, 1) column lets the final division work row by row. ravel() flattens the column back to 1-D just for printing.
Quick Quiz
1. Can arrays of shape (5, 2) and (2,) be added?
Compared from the right, both end in 2, so the 1-D array is used for every row.
2. What shape does a.mean(axis=1, keepdims=True) have if a is (6, 4)?
keepdims=True keeps the averaged axis with size 1.
3. What does combining shapes (3, 1) and (1, 4) produce?
Each size-1 dimension is stretched, so you get every combination of the 3 rows and 4 columns.
Learn Data Science with a mentor
Free lessons get you started. Our Data Science & Machine Learning training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
