Broadcasting

Data Science · Lesson 6 of 32 6 min read Updated

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.

PythonRun it yourself
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))
Output
(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:

  1. If they are equal, fine.
  2. If one of them is 1, that array is stretched to match the other.
  3. If one array has fewer dimensions, it is treated as having size 1 in the missing ones on the left.
  4. Otherwise NumPy raises an error.
Shape AShape BResultWhy
(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,)errorLast 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.

PythonRun it yourself
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
Output
(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.

PythonRun it yourself
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
Output
(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.

PythonRun it yourself
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))
Output
[[-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. Use keepdims=True or [:, 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, not n values. 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=True or [:, 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
Python
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))
Output
[[ 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?

2. What shape does a.mean(axis=1, keepdims=True) have if a is (6, 4)?

3. What does combining shapes (3, 1) and (1, 4) produce?

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