Skip to main content
NumPy — Arrays, Vectors, and Why Loops Are Slow

Data Foundations

NumPy — Arrays, Vectors, and Why Loops Are Slow

Reading15 min read

NumPy — Arrays, Vectors, and Why Loops Are Slow

NumPy is the foundation of the entire Python data and AI stack. pandas DataFrames are built on NumPy arrays; scikit-learn takes them as input; embeddings (Module 4) are NumPy vectors. You don't need to be a NumPy expert, but you need its core idea: operate on whole arrays at once, not element by element.

Arrays

import numpy as np

durations = np.array([412, 2310, 188, 95, 1980])   # ms
durations.dtype     # dtype('int64')
durations.shape     # (5,)
durations.mean()    # 997.0
durations.max(), durations.argmax()   # (2310, 1) — value and its index

Unlike a Python list, an array holds one type packed tightly in memory. That's what makes it fast.

Vectorisation: No Loops

Arithmetic applies to every element at once:

seconds = durations / 1000
over_budget = durations > 1000          # array([False, True, False, False, True])
durations[over_budget]                  # array([2310, 1980]) — boolean indexing
over_budget.sum()                       # 2 — True counts as 1
np.percentile(durations, 95)            # p95 latency

The equivalent Python loop does the same work one element at a time, in the interpreter. NumPy does it in optimised C. On a million latency samples, the difference is typically 50–100×:

samples = np.random.default_rng(0).normal(300, 50, 1_000_000)

# Python loop: ~150 ms
total = 0.0
for s in samples:
    total += s

# NumPy: ~1 ms
total = samples.sum()

That's why idiomatic AI code almost never loops over individual numbers.

Shapes and 2-D Arrays

Data usually comes as a matrix: rows are examples, columns are features.

# 4 test runs × 3 metrics (duration_ms, retries, assertions)
runs = np.array([
    [412, 0, 12],
    [2310, 2, 30],
    [188, 0, 4],
    [1980, 1, 28],
])
runs.shape            # (4, 3)
runs[:, 0]            # first column — all durations
runs[1]               # second row
runs.mean(axis=0)     # mean of each column
runs.mean(axis=1)     # mean of each row

axis=0 collapses rows (giving one value per column); axis=1 collapses columns. Getting the axis wrong is one of the most common NumPy bugs — check .shape whenever results look odd.

Broadcasting

NumPy can combine arrays of different shapes when they're compatible. The common case: apply a per-column operation to every row.

col_means = runs.mean(axis=0)          # shape (3,)
col_stds = runs.std(axis=0)            # shape (3,)
standardised = (runs - col_means) / col_stds   # (4,3) op (3,) → each row

That one line standardises every feature — a typical step before training many ML models.

Vectors and Similarity — Preview of Embeddings

In Module 4, text is turned into vectors of a few hundred numbers. "How similar are these two texts?" becomes "how close are these two vectors?" — measured with cosine similarity:

def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
    return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))

a = np.array([0.2, 0.9, 0.1])
b = np.array([0.25, 0.85, 0.05])
cosine_similarity(a, b)   # ≈ 0.99 — very similar direction

@ is matrix/dot product. And it vectorises too — comparing one query vector against 10,000 document vectors is a single matrix multiplication:

scores = docs @ query / (np.linalg.norm(docs, axis=1) * np.linalg.norm(query))
top5 = np.argsort(scores)[::-1][:5]

Random Numbers, Reproducibly

ML is full of randomness — shuffling, splitting, initialising. Always use a seeded generator so results are reproducible:

rng = np.random.default_rng(seed=42)
rng.integers(0, 100, size=5)

A test or experiment that can't be reproduced can't be debugged.

Testing Numeric Code

Never compare floats with ==. Use tolerances:

np.testing.assert_allclose(result, expected, rtol=1e-6)

np.testing gives clear failure messages showing which elements differ — far better than assert (a == b).all().

💬 Discussion

Where in your test reporting do you compute statistics with Python loops? Which metric (p95 latency, failure rates) would be easiest to vectorise?

Q
Knowledge Check
1 / 3

Why is `samples.sum()` far faster than a Python for-loop summing the same array?

Next Lesson

pandas — Loading, Filtering, and Grouping Data

The Python AI Stack — What Each Library Is For