🧩
Broadcasting Reshaping Compatibility Transformation

Topic 5.6: Broadcasting and Reshaping

Handling arrays of different shapes and transforming data structures

📡 The Broadcasting Problem

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?

Python
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)
▶ Output
Discounted prices:
[[ 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 Rules

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:

Python
# 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)
▶ Output
Shape (2,3) + scalar:
[[11 12 13]
 [14 15 16]]
Python
# 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
▶ Output
Shape (2,3) + (3,):
[[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.

Python
# 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)
▶ Output
Shape (1,3) + (2,1):
[[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.

Broadcasting Compatibility
✅ Compatible Shapes
  • (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)
❌ Incompatible Shapes
  • (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
🔄 Reshaping Arrays

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.

Python
# 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)
▶ Output
Original shape: (12,)
[ 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:

Python
# 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
▶ Output
Shape: (5, 4)
[[ 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:

Python
# 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}")
▶ Output
Original:
[[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.

🔀 Transposing: Swapping Rows and Columns

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.

Python
# 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}")
▶ Output
Original (students × tests):
[[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.

💡 Practical Example: Normalizing Test Scores

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.

Python
# 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)}")
▶ Output
Original scores:
[[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.
📚External Resources