Frontend Labs
⋯

Part VII — Python for data science and AI/ML

This is the Part the rest of the guide has been preparing for. It teaches the Python side of data and AI work: arrays, tables, plots, classical models, a neural-network engine you build yourself, PyTorch, Hugging Face, the data pipelines that feed training, and LLM applications. The AI guide teaches the ideas (what a model is, why attention works, how LLMs are trained); this Part teaches the tools, so that when the AI guide says "train it", you know how. The Maths guide is the companion for the linear algebra and calculus.

Install the stack with uv add numpy pandas polars matplotlib scikit-learn (and torch transformers datasets for Ch 58–59).


53. NumPy: arrays, broadcasting, and why vectorized code is fast

53.1 The ndarray

import numpy as np

a = np.array([1.0, 2.0, 3.0])
M = np.arange(12, dtype=np.float32).reshape(3, 4)
print(a.dtype, a.shape, M.shape, M.ndim, M.size, M.nbytes)   # float64 (3,) (3, 4) 2 12 48
print(M)
print(M[1], M[:, 2], M[1:, ::2])          # row, column, sub-block (views, not copies)
print(np.zeros((2, 3)), np.ones(3), np.eye(2), np.linspace(0, 1, 5))
rng = np.random.default_rng(seed=0)       # the modern RNG API: always seed it
print(rng.normal(size=(2, 2)))

An ndarray is a block of raw numbers of one dtype, stored contiguously, plus a header: shape, dtype, and strides (how many bytes to jump to reach the next element along each axis).

 M = np.arange(12, dtype=np.float32).reshape(3, 4)

 memory:  [ 0  1  2  3  4  5  6  7  8  9 10 11 ]   48 contiguous bytes
 shape:   (3, 4)
 strides: (16, 4)     next row = +16 bytes, next column = +4 bytes

 M.T  -> same memory, shape (4, 3), strides (4, 16)   transpose is free: no copy

Compare with a Python list of floats: an array of pointers to separate 24-byte float objects scattered around the heap (Ch 26.3). NumPy's layout is what C, BLAS, and GPUs need, so whole-array operations run as one C (or SIMD, or GPU) loop.

53.2 Vectorization: no Python loops over numbers

import numpy as np

prices = np.array([120.0, 80.5, 99.9, 15.0])
qty = np.array([2, 1, 3, 10])

revenue = prices * qty                 # element-wise, in C
print(revenue, revenue.sum(), revenue.mean())
print(prices[prices > 90])             # boolean mask: filter without a loop
print(np.where(qty > 2, "bulk", "single"))
print(np.sqrt(prices).round(2), np.log1p(qty))

The rule from Ch 25.4: every loop over individual numbers in Python is a bug waiting for a big input. Express the computation as array operations and let NumPy run the loop.

53.3 Broadcasting

When shapes differ, NumPy stretches size-1 dimensions to match, without copying:

import numpy as np

X = np.array([[1.0, 200.0], [2.0, 300.0], [3.0, 400.0]])   # 3 samples, 2 features
mu = X.mean(axis=0)                     # shape (2,): per-feature mean
sigma = X.std(axis=0)
Z = (X - mu) / sigma                    # (3,2) - (2,) broadcasts across rows: standardization
print(Z.round(3))

# Pairwise distances between 4 points in 2-D, no loops:
P = np.random.default_rng(1).random((4, 2))
D = np.sqrt(((P[:, None, :] - P[None, :, :]) ** 2).sum(axis=-1))   # (4,1,2) - (1,4,2) -> (4,4,2)
print(D.shape, np.allclose(D, D.T))

Broadcasting rules: compare shapes from the right; two dimensions are compatible if equal or if one of them is 1. None (or np.newaxis) adds a size-1 axis. Most "shape mismatch" errors in ML code are broadcasting errors; print .shape everywhere until the shapes are second nature.

53.4 Linear algebra: the language of ML

import numpy as np

rng = np.random.default_rng(0)
X = rng.normal(size=(100, 3))                 # 100 samples, 3 features
true_w = np.array([2.0, -1.0, 0.5])
y = X @ true_w + 0.1 * rng.normal(size=100)   # @ is matrix multiplication

# Least squares: the closed-form linear regression from the Maths guide.
w, *_ = np.linalg.lstsq(X, y, rcond=None)
print(w.round(2))                             # close to [2, -1, 0.5]

# The same thing by gradient descent: the loop every neural network runs.
w = np.zeros(3)
for step in range(500):
    grad = 2 / len(X) * X.T @ (X @ w - y)     # gradient of mean squared error
    w -= 0.1 * grad
print(w.round(2))

That second loop is AI Ch 10 in five lines: predict, measure the error, compute the gradient, step downhill. Chapter 57 computes the gradient automatically instead of by hand.

53.5 Views, copies, and dtype traps

import numpy as np

a = np.arange(6)
v = a[1:4]          # a VIEW: shares memory
v[0] = 99
print(a)            # [ 0 99  2  3  4  5]  <- a changed

c = a[[1, 2, 3]]    # fancy indexing makes a COPY
c[0] = -1
print(a[1])         # still 99

x = np.array([200, 100], dtype=np.uint8)
print(x + x)        # [144 200]: silent overflow wraps around in fixed-size dtypes
print(np.array([1, 2]) / 2, np.array([1, 2]) // 2)   # float division vs integer division

Chapter 2's "names versus objects" applies again, one level down: slices are views. Fixed-size dtypes (uint8, int32, float16) overflow and lose precision silently, unlike Python's int. Image pipelines that add uint8 pixels and mixed-precision training (float16/bfloat16) both hit this.


Generated from real-life-python-guide.md · 8 Oct 2026 16:36