Every Pandas example elsewhere on this site — df["total"] = df["price"] * df["quantity"], a groupby().agg(), a boolean mask — is, underneath,
NumPy doing the actual arithmetic. Pandas provides the
labeled table; NumPy provides the fast array operations that make working
with a million rows in Python practical at all. This article covers the two
NumPy ideas that explain why: vectorization and broadcasting.
A loop, and why it is slow#
A plain Python loop over an array recomputes the same three steps for every single element: fetch the value, call into the arithmetic operator, store the result back:
def scale_loop(values: list[float], factor: float) -> list[float]: result = [] for v in values: result.append(v * factor) return resultFor a small list this cost is invisible. For a million values, the per-element overhead of the Python interpreter — not the multiplication itself, which is trivial — dominates the total runtime.
The vectorized equivalent#
Array programming — applying one operation to an entire array at once — sidesteps the interpreter overhead entirely:
import numpy as np
def scale_vectorized(values: np.ndarray, factor: float) -> np.ndarray: return values * factorvalues * factor is not Python looping over elements at all. NumPy hands
the whole operation to a compiled C loop that runs once, over contiguous
memory, for the entire array. The Python-level function call happens once;
the multiplication happens a million times, but never through the Python
interpreter.
Broadcasting: combining arrays of different shapes#
Vectorized operations extend naturally to combining two arrays of different shapes, as long as they follow NumPy's broadcasting rules — compare shapes from the trailing dimension inward, and any dimension of size 1 stretches to match the other array's size in that dimension:
prices = np.array([[10.0, 20.0, 30.0], [15.0, 25.0, 35.0]])tax_rate = np.array([0.1, 0.2, 0.1])
with_tax = prices * (1 + tax_rate)prices has shape (2, 3); tax_rate has shape (3,). NumPy treats
tax_rate as if it were repeated across both rows without actually copying
it in memory — the per-column tax rate applies to every row in one
vectorized call, with no explicit loop over rows.
Broadcasting can mask a real bug
Broadcasting succeeding is not the same as broadcasting doing what you
meant. A (3,) array and a (1, 3) array combine without error and
produce a (1, 3) result — silently different from what a (3,) result
would have been in code that assumed the simpler shape. When a broadcast
result's shape is a surprise, check .shape on every operand before
assuming the arithmetic itself is wrong.
Views, copies, and why it matters for performance#
Basic slicing (arr[1:5]) returns a view — a new array object pointing
at the same underlying memory, created without copying any data. Fancy
indexing (arr[[1, 3, 5]]) and boolean masking (arr[arr > 0]) return a
copy, because the selected elements are not contiguous in memory and
have to be assembled into a new block. Both are correct; only one avoids an
extra allocation. Memory-Efficient Data Representations
covers the memory side of this distinction in more depth, including the
same view/copy question as it shows up in Pandas.
Common mistakes#
Appending to a NumPy array inside a loop#
result = np.array([])for v in values: result = np.append(result, v * factor)np.append allocates an entirely new array on every call, because NumPy
arrays have a fixed size once created. A loop like this is not just
unvectorized — its time complexity
is quadratic, copying the whole growing array on every iteration. Build a
Python list and convert once with np.array(result) at the end, or
preallocate the output array's final size up front.
Assuming an operation is vectorized because it involves NumPy#
np.vectorize
and apply_along_axis are convenience wrappers around a Python-level loop,
not genuine vectorization — they exist for cases with no vectorized
equivalent, and carry the same per-element interpreter overhead this
article opened with. A truly vectorized operation is one built from NumPy's
own array operators and functions operating on whole arrays.
Not checking shapes before trusting a broadcast result#
A shape mismatch that happens to satisfy broadcasting's rules produces a
result with no error at all — just a shape, and values, that were probably
not intended. Print .shape while developing any broadcast expression more
complex than a single scalar multiply.
Further reading#
- NumPy: Broadcasting — the full rules, with worked examples of each compatible and incompatible shape pair.
- Array programming — the general paradigm NumPy implements.
numpy.ndarray— the core array type referenced throughout this article.