The rule is read from the right, and it is short. The trouble is that two different intentions can both satisfy it.
import numpy as np grid = np.array([[1, 2], [3, 4], [5, 6]]) # shape (3, 2) # Compare shapes from the right. Each pair must be equal, or one must be 1. # (3, 2) and (2,) -> (3, 2) and (1, 2) -> fits: one row per row grid - np.array([1, 2]) # subtracts a value per COLUMN # (3, 2) and (3,) -> (3, 2) and (1, 3) -> 2 against 3: no grid - np.array([1, 2, 3]) # ValueError: operands could not be broadcast # To subtract a value per ROW, give it a shape that lines up: (3, 1) grid - np.array([[1], [3], [5]]) grid - grid.mean(axis=1, keepdims=True) # same thing, computed
- Shapes are compared from the right, and 1 means 'stretch me'
Line the two shapes up at their right-hand ends. Each pair of dimensions must either match or have a 1 in it, and a missing dimension counts as 1. So (3, 2) and (2,) fit — the (2,) becomes one row repeated three times. That is the whole rule, and every broadcasting error is it being applied to shapes you did not check.
- A 1-D array broadcasts across rows, not down columns
np.array([1, 2]) against a (3, 2) grid subtracts 1 from every value in the first column and 2 from every value in the second — a value per column. To subtract a value per row you need shape (3, 1), which is what keepdims=True produces. Getting this backwards is the silent version of the bug: on a square array both shapes fit and the arithmetic is simply wrong.
- keepdims is how a reduction stays broadcastable
grid.mean(axis=0) gives shape (2,) — the dimension it reduced is gone. With keepdims=True it gives (1, 2), which still lines up against the original. For axis=0 the collapsed version happens to broadcast correctly anyway; for axis=1 it does not, and that asymmetry is why using keepdims by habit saves you from having to reason about it.
One expression: the array minus its column means. The only question is which axis the mean is taken along, and whether the result of that mean still lines up against the grid.
axis=0 collapses the rows, leaving one value per column — which is what 'each column’s mean' means. Reading axis as 'the axis that disappears' is the way to keep it straight.
grid - grid.mean(axis=0) works here because (2,) broadcasts against (3, 2) from the right. Adding keepdims=True gives shape (1, 2), which broadcasts the same way and would keep working if you ever switched to axis=1 — so it is worth writing by habit.
import numpy as np def centre_columns(grid): means = grid.mean(axis=____, keepdims=____) return grid ____ means
import numpy as np
def centre_columns(grid):
# axis=0 collapses the rows, so this is one mean per column: shape
# (1, n) with keepdims, which broadcasts back across every row.
means = grid.mean(axis=0, keepdims=True)
# Minus, not -=: the caller's array is not ours to change, and an
# in-place subtraction of floats into an int array raises anyway.
return grid - means
axis=0 removes the row dimension, so what is left is one value per column. Everyone learns this as 'axis=0 means columns' and then trips over it on the first three-dimensional array. 'The axis being collapsed' survives into any number of dimensions and is the same sentence for mean, sum, max and argmax.
Here the collapsed shape (n,) broadcasts correctly anyway, so keepdims changes nothing — which is exactly why it is worth adding: with axis=1 the collapsed shape does *not* broadcast correctly, and the version without keepdims either raises or, on a square array, quietly centres the wrong way. Writing it always means never having to work out which case you are in.
grid - means builds a result and leaves the input alone; grid -= means writes into the caller’s memory. On a large array the in-place version is genuinely cheaper, which is why it exists — but it is a different function, and it belongs behind a name that says so. It also fails outright on an int array, because float results cannot be cast back into int storage.
Centring is half of standardising, and in a real pipeline sklearn’s StandardScaler does both — crucially remembering the training means so the same shift is applied to the test set, which is the leakage lesson from the data track. Write the arithmetic yourself when you genuinely need one axis of one array shifted and nothing remembered. And when a broadcast is doing something subtle, print the shapes rather than reasoning about them: `grid.shape` and `means.shape` on two lines has resolved more numpy confusion than any amount of staring at the expression.
- Write centre_rows using axis=1 and find out what happens without keepdims — this is the case where it matters.
- Divide by the column deviations as well, to standardise rather than merely centre, and notice which column would make that fail.
- Try grid - np.array([1, 2, 3]) on a (3, 2) array and read the error message carefully: it tells you both shapes and which dimension refused.