Dataslope logoDataslope

Vectorized Thinking

How to express a calculation as array operations — and why that is the whole point of NumPy

There is one habit that separates a programmer using NumPy from a scientist using NumPy: vectorization. A vectorized expression describes what should happen to every element of an array in one algebraic statement, rather than walking through the elements with a Python loop.

Vectorization matters for three reasons:

  1. Speed. A vectorized operation runs in compiled C/FORTRAN at memory-bandwidth speed; a Python loop runs in the interpreter at roughly 1000×1000\times slower.
  2. Clarity. A formula reads like the mathematics it implements.
  3. Composability. Vectorized pieces can be combined, sliced, reduced, and broadcast without rewriting.

This page is the bridge between the foundations you have just learned and the rest of the course. After this page, every example will assume you think in arrays.

The single most important benchmark

Code Block
Python 3.13.2

The speedup is usually 100–1000×\times. The loop version is doing exactly the same arithmetic, but each iteration also pays for a Python bytecode dispatch, an attribute lookup on np, a function call, and the wrapping of a float into a numpy.float64 and back.

How NumPy actually runs

Every NumPy universal function (ufunc) — np.sin, np.add, np.exp, np.minimum, ... — is a compiled C loop that processes contiguous blocks of memory using the CPU's vector instructions (SSE, AVX, NEON). You get the speed of FORTRAN because the inner loop is the FORTRAN-equivalent inner loop.

The full hierarchy of "things that go fast":

LayerWhat it doesCost per element
Python for over a listInterpreted bytecode~10610^{-6} s
Python for over an ndarraySame, plus boxing/unboxing~10610^{-6} s
np.vectorizeCosmetic wrapper around a Python function — not fast~10610^{-6} s
NumPy ufunc / arithmeticCompiled SIMD loop~10910^{-9} s
BLAS routine (matmul, etc.)Heavily tuned blocked algorithm~101010^{-10} s
GPUMassively parallel~101110^{-11} s

Almost every speedup in scientific Python comes from moving one layer down this table.

Broadcasting: the killer feature

Broadcasting lets arrays of different shapes interact as if they were the same shape, by virtually stretching the smaller one along its size-1 dimensions. It is the rule that turns NumPy from "FORTRAN with batteries" into "tensor algebra with notation."

The rules: line up shapes from the right; each pair of dimensions must either match or one must be 11.

The classic example: an outer product written as broadcasting.

Code Block
Python 3.13.2

You will see x[:, None] and y[None, :] everywhere in scientific Python. It is the idiom for "promote this 1-D vector to a 2-D column" or "row." Once you internalize it, multi-dimensional operations become as easy to write as one-dimensional ones.

A non-trivial vectorization

Suppose you have a cloud of NN points in 2-D and you want the pairwise distance matrix: Dij=pipjD_{ij} = \|p_i - p_j\|. The explicit Python is two nested loops. The vectorized version is two lines.

Code Block
Python 3.13.2

The same trick generalizes to DD-dimensional point clouds, signed differences, weighted distances, etc. No loops are ever written.

Reductions and axes

Most NumPy aggregation functions accept an axis= argument. Picking the right axis is sometimes the entire numerical content of an operation.

Code Block
Python 3.13.2

Forgetting which axis is which is the source of about half of all scientific Python bugs. A useful mnemonic: axis=k means "collapse the kk-th axis". Whatever axis you name disappears from the shape of the result.

When not to vectorize

Vectorization is wonderful but not magical. A few situations call for something else.

  • The algorithm is fundamentally sequential. Iterating a recurrence one element at a time (x_{n+1} = f(x_n)) cannot be vectorized without changing the math.
  • Memory is the bottleneck. A vectorized expression that allocates an enormous temporary array can be slower than a loop that touches one element at a time. Use numexpr or write a Numba/Cython loop.
  • The inner step is complicated. When the per-element work is a full sub-algorithm, JIT compilation (Numba) or explicit parallelism (Dask, multiprocessing) wins.

A multi-file example: a Numba-style numerical kernel separated from its driver. We will mimic Numba with a plain function since Numba is not preinstalled, but the structure is identical to a production setup.

Code Block
Python 3.13.2

But the parameter sweep over many r values is embarrassingly parallel, and that part vectorizes beautifully — every rr trajectory runs independently:

Code Block
Python 3.13.2

We ran 1000 independent trajectories simultaneously, with no explicit parallelism — broadcasting did it for us.

Memory layout matters

NumPy arrays are contiguous by default and laid out in C order (row-major): the last axis varies fastest in memory. FORTRAN-order arrays (column-major) exist for compatibility with old libraries.

The order matters because accessing memory in the order it is laid out is roughly 10×10\times faster than jumping around — CPU caches work best on contiguous reads.

Code Block
Python 3.13.2

The difference is usually 2–5×\times. For deeply nested reductions, restructuring an array to make the hot axis the last one can be a free speedup.

Vectorization checklist

When you sit down to translate a formula into NumPy, ask:

  1. What is the natural shape of the inputs? Make them arrays of that shape.
  2. What dimensions need to broadcast? Insert None or call np.newaxis/reshape to align them.
  3. Which axis am I reducing? Pass it explicitly as axis=.
  4. Is there an existing ufunc? Reach for np.where, np.clip, np.minimum, np.einsum, before you start writing a loop.
  5. Did I accidentally fall back to Python? Look for for loops, map, list comprehensions, or np.vectorize — if you see them, ask whether they can be lifted into a ufunc.

Check your understanding

QuestionSelect one

What does x[:, None] produce, given a 1-D array x of shape (n,)?

A scalar

An array of shape (1, n)

An array of shape (n, 1) — a "column vector" view of x

A copy of x with no change in shape

QuestionSelect one

A colleague's NumPy code uses np.vectorize to call a complicated Python function over an array of shape (10**6,). They report it is "as fast as native NumPy." What is most likely going on?

They are correct — np.vectorize is a compiled inner loop

They are mistaken — np.vectorize is a cosmetic wrapper around a Python loop and runs at Python speed. It looks like NumPy but is not vectorized in the compiled sense

They are benchmarking the wrong array

np.vectorize JIT-compiles to C on first call

On this page