Topic 5.6: Broadcasting and Reshaping
Handling arrays of different shapes and transforming data structures
Imagine you have a table of product prices and you want to apply a 15% discount to everything. The prices are in a 2D array (products × stores), and the discount is a single number. How do you multiply them together?
import numpy as np # Prices at different stores (3 products × 4 stores) prices = np.array([ [100, 105, 98, 102], # Product A [50, 52, 48, 51], # Product B [200, 210, 195, 205] # Product C ]) # Apply 15% discount (multiply by 0.85) discount_multiplier = 0.85 # This works! NumPy broadcasts the single value discounted = prices * discount_multiplier print("Discounted prices:") print(discounted)
[[ 85. 89.25 83.3 86.7 ]
[ 42.5 44.2 40.8 43.35]
[170. 178.5 165.75 174.25]]
When you multiply a (3,4) array by a single number, NumPy automatically "stretches" that number to match the array's shape. It's as if you had a (3,4) array filled with 0.85 in every position. This automatic stretching is called broadcasting.
Broadcasting doesn't actually create the larger array in memory—that would waste space. Instead, NumPy pretends it exists and does the math efficiently. This is why broadcasting is both fast and memory-efficient.
Broadcasting isn't magic—it follows specific rules. NumPy compares the shapes of the two arrays dimension by dimension, starting from the rightmost (trailing) dimensions. Two dimensions are compatible if:
1. They are equal, OR
2. One of them is 1
If these conditions aren't met, NumPy raises a "shape mismatch" error. Let's see examples:
# Example 1: Scalar (single number) a = np.array([[1, 2, 3], [4, 5, 6]]) # Shape: (2, 3) b = 10 # Shape: () result = a + b print("Shape (2,3) + scalar:") print(result) # The scalar is broadcast to (2,3)
[[11 12 13]
[14 15 16]]
# Example 2: Compatible shapes a = np.array([[1, 2, 3], [4, 5, 6]]) # Shape: (2, 3) b = np.array([10, 20, 30]) # Shape: (3,) result = a + b print("\nShape (2,3) + (3,):") print(result) # b is broadcast to (2,3) by copying the row
[[11 22 33]
[14 25 36]]
In this case, the shapes are (2,3) and (3,). NumPy compares from the right: 3 equals 3 ✓, then sees 2 versus nothing. The (3,) array gets stretched into (2,3) by repeating its row.
# Example 3: Row and column vectors row = np.array([[1, 2, 3]]) # Shape: (1, 3) column = np.array([[10], [20]]) # Shape: (2, 1) result = row + column print("\nShape (1,3) + (2,1):") print(result) # Both stretch to (2,3)
[[11 12 13]
[21 22 23]]
Here, (1,3) and (2,1) are compatible: the trailing 3 vs 1 works (one is 1), and the leading 1 vs 2 works (one is 1). Both get stretched to (2,3) and combined.
- (3, 4) and (4,) → Second stretches to (3, 4)
- (5, 1) and (5, 8) → First stretches to (5, 8)
- (1, 6) and (7, 6) → First stretches to (7, 6)
- (2, 3, 4) and (3, 4) → Second stretches to (2, 3, 4)
- (3, 4) and (3,) → 4 ≠ 3 and neither is 1
- (2, 5) and (3, 5) → 2 ≠ 3 and neither is 1
- (4, 3) and (4,) → Last dimensions don't match
Sometimes your data arrives in the wrong shape for what you need to do. A common example: you have a flat list of 12 numbers, but you need a 3×4 table. The .reshape() method lets you reorganize the data without changing the values.
# Start with a flat array of 12 numbers flat = np.arange(12) print(f"Original shape: {flat.shape}") print(flat) # Reshape into a 3×4 table table = flat.reshape(3, 4) print(f"\nReshaped to (3,4):") print(table) # Reshape into a 2×6 table other_table = flat.reshape(2, 6) print(f"\nReshaped to (2,6):") print(other_table)
[ 0 1 2 3 4 5 6 7 8 9 10 11]
Reshaped to (3,4):
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
Reshaped to (2,6):
[[ 0 1 2 3 4 5]
[ 6 7 8 9 10 11]]
The rule is simple: the total number of elements must stay the same. You can reshape 12 elements into (3,4), (2,6), (4,3), (1,12), or (12,1), but you can't reshape them into (3,5) because 3×5 = 15, not 12.
Using -1 for Automatic Dimension
If you know one dimension but want NumPy to figure out the other, use -1:
# I know I want 4 columns, but how many rows? data = np.arange(20) auto_reshape = data.reshape(-1, 4) print(f"Shape: {auto_reshape.shape}") print(auto_reshape) # NumPy calculates: 20 elements ÷ 4 columns = 5 rows
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]
[16 17 18 19]]
The -1 tells NumPy: "figure out this dimension automatically based on the total size and the other dimensions I specified." This is safer than calculating it yourself—if you make a mistake in your math, NumPy will catch it.
Flattening: Collapsing to 1D
The opposite of reshaping into multiple dimensions is flattening back to one:
# A 2D array grid = np.array([[1, 2, 3], [4, 5, 6]]) print("Original:") print(grid) # Flatten to 1D flat = grid.flatten() print(f"\nFlattened: {flat}") # Alternative: reshape to 1D also_flat = grid.reshape(-1) print(f"Also flattened: {also_flat}")
[[1 2 3]
[4 5 6]]
Flattened: [1 2 3 4 5 6]
Also flattened: [1 2 3 4 5 6]
Both .flatten() and .reshape(-1) give you a 1D array, but .flatten() always creates a copy, while .reshape(-1) tries to create a view. For most purposes, they're interchangeable.
Sometimes you need to flip your data so that rows become columns and columns become rows. This operation is called transposing, and it's essential for many mathematical operations, especially in linear algebra.
# Original: 3 students × 4 tests scores = np.array([ [85, 90, 88, 92], [78, 82, 80, 85], [92, 95, 93, 96] ]) print("Original (students × tests):") print(scores) print(f"Shape: {scores.shape}") # Transpose: 4 tests × 3 students scores_t = scores.T print("\nTransposed (tests × students):") print(scores_t) print(f"Shape: {scores_t.shape}")
[[85 90 88 92]
[78 82 80 85]
[92 95 93 96]]
Shape: (3, 4)
Transposed (tests × students):
[[85 78 92]
[90 82 95]
[88 80 93]
[92 85 96]]
Shape: (4, 3)
After transposing, what was in row 0, column 2 is now in row 2, column 0. The .T attribute (short for transpose) swaps all the coordinates. This is particularly useful when you need data organized differently for calculations or when interfacing with libraries that expect a specific layout.
Let's combine broadcasting and reshaping to solve a real problem: normalizing test scores so each test has a mean of 0 and standard deviation of 1.
# Test scores: 5 students × 3 tests scores = np.array([ [85, 72, 90], [92, 85, 88], [78, 68, 82], [88, 78, 95], [95, 90, 92] ]) print("Original scores:") print(scores) # Calculate mean and std for each test (column) test_means = scores.mean(axis=0) test_stds = scores.std(axis=0) print(f"\nTest means: {test_means}") print(f"Test stds: {test_stds}") # Normalize: subtract mean, divide by std # Broadcasting handles the shape mismatch automatically normalized = (scores - test_means) / test_stds print("\nNormalized scores:") print(normalized) # Verify: means should be ~0, stds should be ~1 print(f"\nNew means: {normalized.mean(axis=0)}") print(f"New stds: {normalized.std(axis=0)}")
[[85 72 90]
[92 85 88]
[78 68 82]
[88 78 95]
[95 90 92]]
Test means: [87.6 78.6 89.4]
Test stds: [ 6.06 8.22 4.58]
Normalized scores:
[[-0.43 -0.8 0.13]
[ 0.73 0.78 -0.31]
[-1.58 -1.29 -1.62]
[ 0.07 -0.07 1.22]
[ 1.22 1.39 0.57]]
New means: [ 0.00e+00 -8.88e-17 4.44e-17]
New stds: [1. 1. 1.]
Here's what happened: test_means has shape (3,), representing one mean per test. When we subtract it from scores (shape 5,3), broadcasting automatically stretches the (3,) array to (5,3) by repeating it for each student. Same with division by test_stds. The result: each test now has mean 0 and standard deviation 1, making them directly comparable.
The tiny values like -8.88e-17 are essentially zero—just floating-point rounding errors. This normalization technique is fundamental in data science and machine learning.
- Broadcasting automatically stretches smaller arrays to match larger ones when doing arithmetic, avoiding the need for manual loops or copies.
- Two dimensions are compatible for broadcasting if they're equal OR one is 1—checked from right to left (trailing dimensions first).
- reshape() reorganizes array elements into a new shape without changing values—total element count must remain constant.
- Use reshape(-1, n) to automatically calculate one dimension based on total size and other dimensions specified.
- flatten() or reshape(-1) converts multi-dimensional arrays to 1D—useful for preparing data for certain algorithms.
- The .T attribute transposes arrays, swapping rows and columns—essential for linear algebra and reorganizing data layout.
- Broadcasting + aggregation enables powerful data transformations like normalization with minimal code and maximum efficiency.
- ↗ NumPy Broadcasting Documentation
https://numpy.org/doc/stable/user/basics.broadcasting.html - ↗ Array Reshaping Guide
https://numpy.org/doc/stable/reference/generated/numpy.reshape.html - ↗ Visual Guide to Broadcasting
https://scipy-lectures.org/intro/numpy/operations.html#broadcasting