Skip to content

Common Broadcasting Patterns

Once the broadcasting rules are understood, a small set of recurring patterns covers most practical use cases. These patterns eliminate explicit loops and temporary arrays, making numerical code both faster and more readable. This page collects the patterns that appear most frequently in data analysis and scientific computing.

Mental Model

Most broadcasting use cases fall into a handful of recipes: subtract a row mean, divide by a column norm, or add a bias vector. Learn these few patterns and you will recognize them everywhere -- centering, normalizing, and outer-product-style operations are the building blocks of nearly all vectorized NumPy code.


Centering Data

Subtract the column mean from every row to produce zero-mean columns.

1. Column-wise Centering

```python import numpy as np

def main(): X = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]) # (3, 3)

col_means = X.mean(axis=0)       # (3,)
X_centered = X - col_means       # (3, 3) - (3,) broadcasts
print("Column means:", col_means)
print("Centered:\n", X_centered)
print("New column means:", X_centered.mean(axis=0))

if name == "main": main() ```

Output:

Column means: [4. 5. 6.] Centered: [[-3. -3. -3.] [ 0. 0. 0.] [ 3. 3. 3.]] New column means: [0. 0. 0.]

2. Row-wise Centering

```python import numpy as np

def main(): X = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) # (2, 3)

row_means = X.mean(axis=1, keepdims=True)  # (2, 1)
X_centered = X - row_means                  # (2, 3) - (2, 1) broadcasts
print("Row means:\n", row_means)
print("Centered:\n", X_centered)

if name == "main": main() ```

Output:

Row means: [[2.] [5.]] Centered: [[-1. 0. 1.] [-1. 0. 1.]]

3. keepdims is Essential

Without keepdims=True, X.mean(axis=1) returns shape (2,) instead of (2, 1), and the subtraction broadcasts incorrectly along the wrong axis.

Standardization

Centering and scaling to unit variance in a single broadcasting expression.

1. Z-score Normalization

```python import numpy as np

def main(): X = np.random.randn(100, 5) # (100, 5) mu = X.mean(axis=0) # (5,) sigma = X.std(axis=0) # (5,) Z = (X - mu) / sigma # each column: mean 0, std 1 print("Column means after:", np.round(Z.mean(axis=0), 10)) print("Column stds after: ", np.round(Z.std(axis=0), 10))

if name == "main": main() ```

Output:

Column means after: [-0. -0. 0. -0. 0.] Column stds after: [1. 1. 1. 1. 1.]

2. Min-Max Scaling

```python import numpy as np

def main(): X = np.random.randn(100, 5) X_min = X.min(axis=0) # (5,) X_max = X.max(axis=0) # (5,) X_scaled = (X - X_min) / (X_max - X_min) # all values in [0, 1] print("Min per column:", X_scaled.min(axis=0)) print("Max per column:", X_scaled.max(axis=0))

if name == "main": main() ```

3. Two Broadcasts in One Line

The expression (X - mu) / sigma performs two broadcasts: subtraction of mu with shape (5,) from X with shape (100, 5), then division by sigma with the same shapes.

Outer Products

Combine a column vector and a row vector to produce a 2D result.

1. Addition Table

```python import numpy as np

def main(): a = np.array([1, 2, 3])[:, np.newaxis] # (3, 1) b = np.array([10, 20, 30])[np.newaxis, :] # (1, 3) table = a + b # (3, 3) print(table)

if name == "main": main() ```

Output:

[[11 21 31] [12 22 32] [13 23 33]]

2. Multiplication Table

```python import numpy as np

def main(): a = np.arange(1, 10)[:, np.newaxis] # (9, 1) b = np.arange(1, 10)[np.newaxis, :] # (1, 9) table = a * b # (9, 9) print(table)

if name == "main": main() ```

3. np.newaxis vs reshape

np.newaxis (alias for None) inserts a size-1 dimension. These are equivalent:

python a[:, np.newaxis] # (n,) → (n, 1) a[:, None] # same a.reshape(-1, 1) # same

Pairwise Distances

Compute distances between all pairs of points without loops.

1. Euclidean Distance Matrix

```python import numpy as np

def main(): # 4 points in 3D space X = np.random.randn(4, 3) # (4, 3)

diff = X[:, np.newaxis, :] - X[np.newaxis, :, :]  # (4, 1, 3) - (1, 4, 3) → (4, 4, 3)
dist = np.sqrt((diff ** 2).sum(axis=2))            # (4, 4)
print("Distance matrix:\n", np.round(dist, 2))

if name == "main": main() ```

2. Shape Breakdown

Expression Shape Explanation
X[:, np.newaxis, :] (4, 1, 3) Each point as a row-block
X[np.newaxis, :, :] (1, 4, 3) Each point as a column-block
diff (4, 4, 3) All pairwise coordinate differences
dist (4, 4) Euclidean distances after sum and sqrt

3. Symmetry Check

```python import numpy as np

def main(): X = np.random.randn(5, 3) diff = X[:, np.newaxis, :] - X[np.newaxis, :, :] dist = np.sqrt((diff ** 2).sum(axis=2)) print("Symmetric:", np.allclose(dist, dist.T)) print("Zero diagonal:", np.allclose(np.diag(dist), 0))

if name == "main": main() ```

Row or Column Scaling

Multiply each row or column by a different weight.

1. Scale Columns

```python import numpy as np

def main(): X = np.ones((3, 4)) # (3, 4) weights = np.array([1, 2, 3, 4]) # (4,) result = X * weights # each column scaled print(result)

if name == "main": main() ```

Output:

[[1. 2. 3. 4.] [1. 2. 3. 4.] [1. 2. 3. 4.]]

2. Scale Rows

```python import numpy as np

def main(): X = np.ones((3, 4)) # (3, 4) weights = np.array([10, 20, 30])[:, np.newaxis] # (3, 1) result = X * weights # each row scaled print(result)

if name == "main": main() ```

Output:

[[10. 10. 10. 10.] [20. 20. 20. 20.] [30. 30. 30. 30.]]

3. Key Difference

Column scaling uses a 1D vector with shape (n_cols,) that aligns with the last axis. Row scaling requires a column vector with shape (n_rows, 1) via np.newaxis or reshape.

Boolean Masking with Broadcasting

Combine boolean conditions across different dimensions.

1. Threshold per Column

```python import numpy as np

def main(): X = np.array([[1, 5, 3], [4, 2, 6], [7, 8, 1]]) # (3, 3) thresholds = np.array([3, 4, 5]) # (3,) mask = X > thresholds # (3, 3) > (3,) broadcasts print("Mask:\n", mask) print("Values above thresholds:", X[mask])

if name == "main": main() ```

Output:

Mask: [[False True False] [ True False True] [ True True False]] Values above thresholds: [5 4 6 7 8]

2. Range Check

```python import numpy as np

def main(): X = np.random.randn(5, 3) lower = np.array([-1, -0.5, 0]) # (3,) upper = np.array([1, 0.5, 2]) # (3,) in_range = (X >= lower) & (X <= upper) print("In range:\n", in_range)

if name == "main": main() ```

3. Combining Row and Column Conditions

```python import numpy as np

def main(): row_mask = np.array([True, False, True])[:, np.newaxis] # (3, 1) col_mask = np.array([True, True, False])[np.newaxis, :] # (1, 3) combined = row_mask & col_mask # (3, 3) print(combined)

if name == "main": main() ```

Summary

The most common broadcasting patterns share the same underlying mechanism: aligning a smaller array against a larger one along a specific axis.

Pattern Typical Shapes Why It Works (Rule) Key Technique
Column centering (m, n) - (n,) (n,) pads to (1, n), axis 0: 1→m mean(axis=0)
Row centering (m, n) - (m, 1) axis 1: 1→n mean(axis=1, keepdims=True)
Standardization (m, n) - (n,) then / (n,) Same rule, applied twice Two broadcasts in sequence
Outer product (m, 1) * (1, n) axis 0: 1→m, axis 1: 1→n np.newaxis
Pairwise distance (m, 1, d) - (1, n, d) axes 0-1: 1→m and 1→n, axis 2: d==d 3D broadcasting
Row/column scaling (m, n) * (n,) or (m, 1) Same as centering Weight vector alignment
Boolean masking (m, n) > (n,) (n,) pads to (1, n), axis 0: 1→m Threshold per column

Exercises

Exercise 1. Given a matrix X of shape (50, 4), write a single expression that performs min-max scaling on each column so that every column's values lie in [0, 1]. Use keepdims where appropriate.

Solution to Exercise 1
import numpy as np

X = np.random.randn(50, 4)
X_min = X.min(axis=0, keepdims=True)   # (1, 4)
X_max = X.max(axis=0, keepdims=True)   # (1, 4)
X_scaled = (X - X_min) / (X_max - X_min)
print(X_scaled.min(axis=0))  # [0. 0. 0. 0.]
print(X_scaled.max(axis=0))  # [1. 1. 1. 1.]

Exercise 2. Using only broadcasting (no np.outer), compute the outer product of two 1D arrays u = np.array([1, 2, 3, 4]) and v = np.array([10, 20, 30]) to produce a (4, 3) result.

Solution to Exercise 2
import numpy as np

u = np.array([1, 2, 3, 4])
v = np.array([10, 20, 30])
outer = u[:, np.newaxis] * v[np.newaxis, :]
print(outer)
# [[10 20 30]
#  [20 40 60]
#  [30 60 90]
#  [40 80 120]]
print(outer.shape)  # (4, 3)

Exercise 3. Given a set of 6 points in 2D stored as points = np.random.randn(6, 2), compute the full (6, 6) pairwise Euclidean distance matrix using broadcasting. Verify that the diagonal is all zeros and the matrix is symmetric.

Solution to Exercise 3
import numpy as np

points = np.random.randn(6, 2)
diff = points[:, np.newaxis, :] - points[np.newaxis, :, :]  # (6, 6, 2)
dist = np.sqrt((diff ** 2).sum(axis=2))                     # (6, 6)
print("Diagonal all zero:", np.allclose(np.diag(dist), 0))
print("Symmetric:", np.allclose(dist, dist.T))

Exercise 4. Given a matrix X of shape (1000, 10), compute the L2 norm of each row and then normalize each row to unit length using broadcasting. Verify by checking that each row's norm is approximately 1.0 after normalization.

Solution to Exercise 4
import numpy as np

X = np.random.randn(1000, 10)
norms = np.sqrt((X ** 2).sum(axis=1, keepdims=True))  # (1000, 1)
X_normalized = X / norms  # (1000, 10) / (1000, 1)

# Verify: each row should have norm ~1.0
row_norms = np.sqrt((X_normalized ** 2).sum(axis=1))
print(np.allclose(row_norms, 1.0))  # True

Exercise 5. Create a boolean mask of shape (5, 5) that is True for elements below the diagonal, using only broadcasting. Hint: compare a column vector np.arange(5)[:, None] with a row vector np.arange(5)[None, :]. Explain which broadcasting rule makes this work.

Solution to Exercise 5
import numpy as np

rows = np.arange(5)[:, np.newaxis]  # (5, 1)
cols = np.arange(5)[np.newaxis, :]  # (1, 5)
below_diagonal = rows > cols         # (5, 5)
print(below_diagonal)
# [[False False False False False]
#  [ True False False False False]
#  [ True  True False False False]
#  [ True  True  True False False]
#  [ True  True  True  True False]]

# This works because:
# rows: (5, 1) — axis 1 is 1, expands to 5
# cols: (1, 5) — axis 0 is 1, expands to 5
# Both axes have size 1 vs 5 → expand → (5, 5)
# This is the outer-product broadcasting pattern applied
# to a comparison operator instead of arithmetic.