Vectorised maths and broadcasting

Element-wise operations, the ufunc model, aggregation, and the broadcasting rules that make array code concise.

Element-wise operations

a = np.array([1, 2, 3])
b = np.array([10, 20, 30])

a + b          # array([11, 22, 33])
a * 2          # array([2, 4, 6])
a ** 2         # array([1, 4, 9])

np.sqrt(a)
np.exp(a)
np.log(a)

a.sum(), a.prod(), a.mean(), a.std(), a.cumsum()

These are ufuncs: functions that operate element by element at C speed and support an out= parameter so you can write into preallocated memory instead of allocating a new array each call.

out = np.empty_like(a, dtype=float)
np.multiply(a, 2.5, out=out)     # no new allocation

Broadcasting rules

When shapes differ, NumPy compares them from the right: dimensions are compatible if they are equal or one of them is 1. The size-1 dimension is stretched without copying data.

m = np.ones((3, 4))
v = np.array([1, 2, 3, 4])

m + v            # (3,4) + (4,)  -> v broadcast across rows ✅
# m + np.array([1, 2, 3])   # (3,4) + (3,) -> ValueError ❌

col = np.array([[10], [20], [30]])   # shape (3, 1)
m + col          # one value per row

# normalise each column to 0..1
norm = (m - m.min(axis=0)) / (m.max(axis=0) - m.min(axis=0))
💡
Reshape with None (or np.newaxis) to add a length-1 axis deliberately: v[:, None] turns a shape-(4,) vector into (4, 1). That one character is often the fix for a broadcasting error.

Aggregation and where

x = np.array([3, np.nan, 7])

x.sum()                      # nan - NaN poisons everything
np.nansum(x)                 # 10 - ignores NaN
np.isnan(x)                  # array([False,  True, False])
x[~np.isnan(x)]              # drop NaN

np.percentile(np.arange(101), [25, 50, 75])
np.clip(x, 0, 5)             # cap values into a range
⚠️
A single NaN silently destroys sum, mean and std. In real datasets, decide up front whether missing values should be dropped or filled — and use the nan* variants consciously rather than by accident.

FAQ

Is a Python loop ever acceptable?
For genuinely sequential logic, yes — but measure first. If the loop body is pure maths on array elements, rewrite it as vectorised expressions or use np.apply_along_axis as a middle ground.
Why is my result a float when I expected an int?
Division, mean, and mixing dtypes promote to float. Cast explicitly with .astype() when you need integers, and check for truncation.

NumPy arrays Indexing, slicing and masks

Last refreshed 2026-09-18.