Topic 14.3
Vectorisation and Broadcasting
In one line
Vectorisation means writing one array expression instead of a Python loop. Broadcasting is the rule that lets arrays of different shapes combine, by stretching size-1 dimensions to match.
Think of it like this
A teacher adds 5 bonus marks to every student. She could walk to each desk and write the new mark (a loop). Or she could announce "everyone gets +5" once (vectorisation). The announcement is one instruction applied to the whole class. Broadcasting is the extra rule: she has a short slip of paper with one bonus per subject, "maths +2, science +3, English +1". She does not copy the slip for every student. She just reads the same slip again at each desk. A small array is reused across a big one in the same way.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Vectorisation
- Replacing a Python loop with an operation on whole arrays.
- ufunc
- A universal function: it applies an operation to every element, such as np.sqrt or np.exp.
- Scalar
- A single number, not an array.
- Broadcasting
- The rule that stretches size-1 dimensions so differently shaped arrays can be combined.
- newaxis
- np.newaxis (or None in an index) adds a dimension of size 1.
- Compatible shapes
- Shapes where, for each dimension from the right, the sizes are equal or one of them is 1.
Step by step
01Loop versus whole-array
The loop below adds up v * 2 + 1 for 200,000 numbers one at a time. The vector version computes the same thing in one line. Both give 40000000000.0, but the second runs in compiled code. (We use floats here so the total does not overflow a 32-bit integer in the browser.)
data = np.arange(200_000, dtype=np.float64)
total = 0.0
for v in data.tolist():
total += v * 2 + 1
vec = (data * 2 + 1).sum()
print(total == vec)02ufuncs
np.sqrt(x) takes the square root of each element. np.maximum(x, 2.0) compares each element with 2.0 and keeps the larger. np.clip(x, 1.0, 5.0) forces every element into the range 1 to 5. Note that np.maximum takes two arrays and gives an array; the plain np.max reduces one array to a single number.
A scalar combined with an array is the simplest broadcast: the scalar behaves like an array of the same shape filled with that number.
03The rule, step by step
Write the two shapes one above the other, aligned on the right. Pad the shorter with 1s on the left. Check each column: equal or one is 1. The result takes the larger size in each column.
Example: (2, 3) and (3,) becomes (2, 3) over (1, 3): compatible, result (2, 3). (3, 1) and (2,) becomes (3, 1) over (1, 2): compatible, result (3, 2). (2, 3) and (2,) becomes (2, 3) over (1, 2): the last column has 3 versus 2, so it fails.
04Scale columns, then scale rows
m * scale with scale of shape (3,) multiplies column 0 by 10, column 1 by 100 and column 2 by 1000. To add a different amount to each row, make a column vector with shape (2, 1).
No copies of the small array are made. NumPy just reuses its values while it walks over the big one.
m = np.array([[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0]])
print(m * np.array([10.0, 100.0, 1000.0]))
print(m + np.array([[1.0], [2.0]]))05Every pair at once
A (3, 1) array plus a (2,) array produces a (3, 2) table with every combination, here 1 to 3 plus 10 and 20. The same trick builds distance tables and multiplication tables without loops.
06Read a shape error
np.ones((2, 3)) + np.ones((2,)) raises ValueError: operands could not be broadcast together with shapes (2,3) (2,). The message lists both shapes, so you can apply the right-to-left rule yourself. The fix is to reshape the small array to (2, 1).
Try it yourself
- 1
Predict before you run
Without running, decide whether these shape pairs are compatible and what the result is:
(4, 1)with(1, 5);(5, 4)with(4,);(5, 4)with(5,). Check withnp.onesand+. - 2
Centre the columns
Make
m = np.arange(6.0).reshape(2, 3)and printm - m.mean(axis=0). Each column now has mean 0. Why did the shapes line up? - 3
Rows, not columns
Now centre each row (subtract each row's own mean from it):
m - m.mean(axis=1)fails. Fix it withkeepdims=True(Topic 14.4) or.reshape(-1, 1). Print the shape ofm.mean(axis=1)first to see why.
Code & diagrams
Both versions give the same answer. Timing it is a good Topic 13.7 exercise on your own computer. A realistic version: a million sale amounts where you need amount * 1.18 for each, the loop against one array line.
Expected output
loop result: 40000000000.0
vector result: 40000000000.0
same: True
sqrt: [0. 1. 2. 3.]
exp(0): [1.]
maximum: [2. 2. 4. 9.]
clip: [1. 1. 4. 5.]Change the second array's shape to (3,) and read the error message in the try block.
Expected output
m shape: (2, 3)
scale shape: (3,)
[[ 10. 200. 3000.]
[ 40. 500. 6000.]]
col shape: (2, 1)
[[2. 3. 4.]
[6. 7. 8.]]
[[11, 21], [12, 22], [13, 23]]
ValueError: operands could not be broadcast together with shapes (2,3) (2,)
fixed shape: (2, 3)Age and monthly spend sit on very different scales. Subtracting each column's mean and dividing by its standard deviation puts both on the same footing (a z-score). Both mean and std have shape (2,), so they broadcast over all rows. In a real project compute them on the training rows only.
Expected output
mean per column: [ 38.75 4425. ]
std per column : [ 10.6 2958.4]
[[-1.29 -1.09]
[-0.63 0.33]
[ 0.78 -0.72]
[ 1.15 1.48]]
column means ~0: True
column stds =1: True
shapes: (4, 2) (2,) (4, 2)Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Mismatched shapes
Add a vector with one value per row to a table.
import numpy as np
table = np.ones((2, 3))
per_row = np.array([10.0, 20.0])
print(table + per_row)Myth vs fact
Myth
Broadcasting copies the small array many times.
Fact
NumPy reuses the values without making the big copy, so it costs no extra memory for the stretch.
Myth
NumPy matches shapes from the left.
Fact
It compares from the right (the last dimension) and pads missing dimensions on the left with 1.
Myth
Vectorised code never uses a loop.
Fact
There is still a loop, in C. Python-level loops are what you avoid.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
np.vectorizeis a convenience wrapper that still calls your Python function once per element, so it gives no speed-up. Real vectorisation means expressing the work with ufuncs, masks andnp.where. - ▸
Temporary arrays cost memory.
a * b + ccreates a temporary fora * b. For huge arrays, in-place operators (out=arguments,+=) or tools such asnumexpr, Numba or a framework compiler avoid it. - ▸
Broadcasting
(n, 1)with(1, m)builds an(n, m)result. A pairwise-distance table for 100,000 points is 10 billion numbers: broadcasting is elegant but can ask for far more memory than you have. - ▸
Broadcasting is free because the stretched array is given a stride of 0 along the stretched axis: NumPy keeps reading the same bytes instead of moving on.
np.broadcast_to(v, (1000, 3))shows this (it returns a read-only view). The output of the operation is still a full new array. Broadcasting also applies to in-place maths, but only in one direction:a += bworks ifbcan stretch toa's shape, and fails if the result would need to be bigger thana. - ▸
Broadcasting never matches by position when shapes are merely compatible by accident. Two square matrices of the same size combine happily with
*even when you meant@, and a(n,)vector against an(n, 1)column silently becomes an(n, n)table. Both are classic silent bugs that give plausible numbers. Cheap defence:assert x.shape == (n, d)at the entry of important functions, andkeepdims=Trueon reductions so the shapes you expect stay put.
Remember this
- 1
Vectorised code replaces
forloops over elements with whole-array operations. The loop still happens, but inside NumPy's compiled code, where it is 10 to 100 times faster than a Python loop on big arrays. The Python style is: think in whole arrays, not in single numbers. - 2
A universal function (ufunc) is a NumPy function that works element by element on arrays:
np.sqrt,np.exp,np.log,np.maximum,np.clip, and the operators+ - * / **themselves. They accept scalars (single plain numbers), arrays, or a mix. ufuncs are the building blocks of vectorised code. - 3
Broadcasting decides what happens when shapes differ. (Recall from Topic 14.1: the shape is the tuple of sizes, one per dimension.) NumPy compares the two shapes starting from the last dimension and moving left. Two dimensions are compatible if they are equal, or if one of them is 1. Missing leading dimensions count as 1. A size-1 dimension is stretched (virtually, with no copy) to match the other. If any pair is neither equal nor 1, the result is a
ValueError. - 4
A worked example: a table of shape
(2, 3)plus a vector of shape(3,). Compare from the right:3and3match; the vector has no first dimension, so it counts as 1 and is stretched to 2. The vector is added to every row. That is how you scale each column by its own factor with a singlem * scale. - 5
To act on rows instead of columns, give the vector a shape
(2, 1)so its size-1 dimension lines up with the columns.v.reshape(-1, 1)orv[:, np.newaxis]does this. Adding a(3, 1)column and a(2,)row yields a(3, 2)table, every combination: the basis of pairwise distance tables. - 6
Common shape error:
(2, 3)plus(2,)fails, because the trailing3and2are neither equal nor 1. The fix is not to make the data fit by trial and error, but to decide what you mean: one value per row means(2, 1). Printing shapes before the operation is the fastest debugging habit. - 7
In AI code, broadcasting is everywhere. A batch is a group of examples processed together, one per row. A bias is a small set of numbers a model adds to its output. So "adding a bias vector
(features,)to every example in a batch(batch, features)" is the teacher's slip of paper again. Other uses: subtracting the mean from each column to centre data, and dividing each embedding by its own length to normalise it (scale it to length 1; Topic 14.5).
Explain it without notes
What is vectorisation and why is it faster than a Python loop?
State the broadcasting rule. Walk through (3, 1) plus (4,).
Why does np.ones((2, 3)) + np.ones((2,)) fail, and how do you fix it?
Where does broadcasting appear in a neural network layer?
Practice
Given prices = np.array([[10.0, 20.0], [30.0, 40.0]]) and tax = np.array([1.05, 1.10]), apply a different tax to each column.
Make a 3-by-3 multiplication table from 1 to 3 using np.arange(1, 4) and broadcasting.
Normalise each row of m = np.array([[1.0, 3.0], [2.0, 2.0]]) so the row sums to 1, using keepdims=True.
Trade-offs
- ↔
Brevity versus clarity. Broadcasting turns loops into one line, but a reader must work out the shapes. Comments with shapes help.
- ↔
Speed versus memory. Vector code is fast but builds temporary arrays; on huge data, work in chunks.
- ↔
Implicit versus explicit. Broadcasting can hide a bug where a shape mismatch happens to be compatible. Assert shapes in important code.
Done when you can
Done when you can replace a numeric
forloop with an array expression.Done when you can name five ufuncs and say what they do.
Done when you can apply the broadcasting rule on paper to any two shapes.
Done when you can turn a vector into a row or a column with
reshapeornewaxis.Done when you can read a broadcasting
ValueErrorand fix it.