Arrays of different shapes, working together
So far we combined arrays of the same shape. Broadcasting is the set of rules that lets NumPy combine arrays of different shapes without copying data. The simplest case is a scalar: a + 10 adds 10 to every element, as if 10 had been stretched to the shape of a. The same stretching works between arrays: a vector of three numbers can be added to every row of a matrix with three columns, one value per column. It is how you subtract each column's mean, scale each asset by its volatility or build a table of all pairwise differences without a single loop.
import numpy as np
a = np.array([[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0]]) # shape (2, 3)
print(a + 10) # a scalar is stretched to every element
# [[11. 12. 13.]
# [14. 15. 16.]]
per_column = np.array([1, 10, 100]) # shape (3,): one value per column
print(a * per_column)
# [[ 1. 20. 300.]
# [ 4. 50. 600.]]
print(a - a.mean(axis=0)) # subtract each column's mean
# [[-1.5 -1.5 -1.5]
# [ 1.5 1.5 1.5]]The rule: compare shapes from the right
NumPy lines the two shapes up from the right. In each position the two sizes must be equal, or one of them must be 1 (that one is stretched), or one array simply has no dimension there (treated as 1). Shapes (2, 3) and (3,) work: 3 meets 3, and the missing dimension is stretched over the 2 rows. Shapes (2, 3) and (2,) fail, because 3 meets 2: NumPy raises ValueError: operands could not be broadcast together. The result shape takes the larger size in each position.
import numpy as np
a = np.ones((2, 3))
print((a + np.ones(3)).shape) # (2, 3) 3 meets 3
try:
a + np.ones(2) # 3 meets 2: no match
except ValueError as error:
print(error) # operands could not be broadcast together with shapes (2,3) (2,)
# Size 1 stretches: (3, 1) with (1, 4) gives (3, 4)
print((np.ones((3, 1)) + np.ones((1, 4))).shape) # (3, 4)Column vectors and tables of pairs
A 1-D array of shape (n,) always lines up with the last axis, so it acts like a row. To apply one value per row you need a column of shape (n, 1): write x[:, np.newaxis] or x.reshape(-1, 1). Combining a column with a row builds a whole table: x[:, None] - x[None, :] is the n×n matrix of all differences x[i] - x[j], and corr * vols[:, None] * vols[None, :] turns a correlation matrix into a covariance matrix. Be careful with the opposite mistake: adding shapes (3,) and (3, 1) silently gives a 3×3 table, not a vector of three numbers.
import numpy as np
x = np.array([1.0, 2.0, 4.0])
print(x[:, np.newaxis].shape, x.reshape(-1, 1).shape, x[None, :].shape) # (3, 1) (3, 1) (1, 3)
# All pairwise differences x[i] - x[j]
print(x[:, None] - x[None, :])
# [[ 0. -1. -3.]
# [ 1. 0. -2.]
# [ 3. 2. 0.]]
# One factor per row needs a column
a = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
print(a * np.array([1.0, 2.0])[:, None])
# [[ 1. 2. 3.]
# [ 8. 10. 12.]]
# Correlation matrix to covariance matrix
corr = np.array([[1.0, 0.5], [0.5, 1.0]])
vols = np.array([0.2, 0.3])
print(corr * vols[:, None] * vols[None, :])
# [[0.04 0.03]
# [0.03 0.09]]
print((np.ones(3) + np.ones((3, 1))).shape) # (3, 3) a silent table, not a vectorimport numpy as np
# Standardise every column at once: (x - mean) / std, column by column
a = np.array([[1.0, 10.0],
[2.0, 20.0],
[3.0, 30.0]])
z = (a - a.mean(axis=0)) / a.std(axis=0) # (3, 2) - (2,) -> (3, 2), then divided by (2,)
print(z.round(3).tolist()) # [[-1.225, -1.225], [0.0, 0.0], [1.225, 1.225]]
print(z.mean(axis=0).round(9).tolist()) # [0.0, 0.0]
# keepdims keeps the reduced axis, so the result can be broadcast back by row too
row_share = a / a.sum(axis=1, keepdims=True)
print(row_share.sum(axis=1).tolist()) # [1.0, 1.0, 1.0]result.shape. A (n, n) result where you expected (n,), or a table of billions of entries from two long vectors, is the classic sign of a missing [:, None] or an extra one. The result is built in memory, so two vectors of 100,000 values make a 10-billion-entry table.- Shapes are lined up from the right: sizes must be equal or 1 (or missing); the result takes the larger size.
(n,)acts like a row; make a column withx[:, None]orx.reshape(-1, 1).- Column with row = a table:
x[:, None] - x[None, :]; checkresult.shape, mismatches fail withValueError.
Next up: pandas Series, one-dimensional arrays that carry a label with every value.
Lab · what's graded
- Centre every column with
demean_columns25 pts - Give every row its own factor with
scale_rows25 pts - Build the table of all differences with
pairwise_differences25 pts - Turn correlations into covariances with
covariance_from_correlation25 pts