2.3 Vectorization and Broadcasting
The previous section selected meaningful subsets. We now transform those values. NumPy encourages expressions that describe an operation on a whole array instead of directing Python through one element at a time.
This is vectorization: expressing repeated numerical work as array operations. It improves more than speed. minutes * 60 states the intended transformation directly, while a loop mixes that intention with list construction, iteration, and mutation.
Element-wise operations
Arithmetic between an array and a scalar applies to every element:
minutes = np.array([31.0, 52.0, 28.0, 44.0])
seconds = minutes * 60
with_buffer = minutes + 5
within_target = minutes <= 40Arrays of the same shape operate position by position:
total_minutes = np.array([31, 52, 28, 44])
parking_wait = np.array([3, 7, 2, 5])
travel_minutes = total_minutes - parking_waitNumPy also supplies universal functions, usually called ufuncs, that apply a defined operation element-wise:
roots = np.sqrt(np.array([1, 4, 9, 16]))
capped = np.minimum(minutes, 40)The loop has not vanished from the universe. NumPy’s compiled implementation still processes elements. The advantage is that Python dispatches one array operation instead of interpreting a Python loop body once per element. The exact speedup depends on dtype, memory layout, operation, array size, and temporary allocations; measure important workloads instead of repeating “vectorized is always faster” as a slogan.
Vectorized code must preserve business rules
A shorter expression can still be wrong. If parking wait exceeds total recorded time because of a data defect, direct subtraction produces a negative travel time. You may reject the record, investigate it, or temporarily bound the output depending on the declared rule:
travel_minutes = np.maximum(total_minutes - parking_wait, 0)Bounding is not a substitute for validation. It changes invalid values into zeros and could hide upstream defects. A robust workflow counts how often the boundary was used and preserves enough information to investigate.
Vectorized expressions can also allocate temporary arrays. In an expression such as (values - values.mean()) / values.std(), the subtraction produces an intermediate array before division. This is usually acceptable, but large arrays require attention to memory. Chapter 3 will examine performance and memory behavior more closely.
Broadcasting combines compatible shapes
Broadcasting is NumPy’s rule for applying element-wise operations to arrays whose shapes differ but are compatible. Suppose rows are days and columns are zones:
times = np.array([
[31, 38, 44, 36],
[29, 41, 47, 35],
[33, 39, 42, 37],
])
zone_baseline = np.array([30, 40, 45, 35])
deviation = times - zone_baselineThe shapes are (3, 4) and (4,). NumPy aligns dimensions from the right. The trailing dimensions are both 4, so the baseline is logically reused for each of the three rows. The result has shape (3, 4).
Broadcasting does not normally create a full repeated baseline array in memory. It behaves as if the values were repeated while using stride-aware access internally.
The compatibility rule
Compare shapes from their final dimensions toward the left. Two dimensions are compatible when:
- They are equal, or
- One of them is 1, or
- One shape has no dimension at that position, as with a scalar or shorter shape.
Examples against a matrix of shape (3, 4):
| Other shape | Compatible? | Meaning |
|---|---|---|
() | Yes | One scalar for every cell |
(4,) | Yes | One value per column |
(1, 4) | Yes | Explicit one-row form, reused down rows |
(3, 1) | Yes | One value per row, reused across columns |
(2,) | No | Trailing dimensions 4 and 2 conflict |
(3,) | No | Trailing dimensions 4 and 3 conflict |
To apply one value per row, reshape the vector so its intended axis is explicit:
daily_adjustment = np.array([1, 2, 3]).reshape(3, 1)
adjusted = times - daily_adjustmentnp.newaxis is another spelling for inserting a length-1 dimension:
daily_adjustment = np.array([1, 2, 3])[:, np.newaxis]Guard against accidental outer operations
Broadcasting can produce a valid shape that is conceptually wrong. Combining shape (3, 1) with shape (4,) produces (3, 4): every one of three row values interacts with every one of four column values. That may be the intended outer comparison—or an unnoticed mistake.
Before a broadcasted operation, write three things:
- The meaning of each axis.
- The operand shapes before the operation.
- The expected result shape and meaning.
Then assert important expectations:
assert times.shape == (3, 4)
assert zone_baseline.shape == (4,)
deviation = times - zone_baseline
assert deviation.shape == times.shapeThe next section deliberately collapses axes through aggregation. You will learn to predict which labels survive, how missing values affect the denominator, and why floating-point representation can change apparently simple calculations.