NumPy essentials — vectorise everything
Array shapes, broadcasting, views vs copies, and the dtypes that decide your memory footprint.
Why this matters
NumPy is the foundation of every Python data / ML / scientific library — pandas, scikit-learn, PyTorch, JAX, OpenCV all build on ndarray. Vectorised NumPy is 10–1000× faster than pure Python loops because the work happens in tight C code, releasing the GIL. This lesson teaches you to think in arrays.
Learning objectives
- Create and reshape
ndarrays. - Slice, index, and boolean-mask arrays.
- Apply broadcasting correctly.
- Vectorise instead of looping.
- Use
numpy.random, linalg, and reductions.
1. The ndarray
import numpy as np
a = np.array([1, 2, 3, 4])
a.shape # (4,)
a.dtype # dtype('int64')
a.ndim # 1
a.size # 4
a.itemsize # 8 (bytes per element)
a.nbytes # 32NumPy arrays are:
- Homogeneous (one dtype).
- Contiguous in memory.
- Fixed-shape at creation (resizing is rare).
Common constructors
np.array([[1,2],[3,4]])
np.zeros((3, 4))
np.ones((2, 3), dtype=np.float32)
np.full((3,), 7.0)
np.empty((2, 2)) # uninitialised; faster, may contain garbage
np.eye(3) # identity
np.arange(0, 10, 2) # [0, 2, 4, 6, 8]
np.linspace(0, 1, 5) # 5 evenly-spaced points incl. endpoints
np.logspace(0, 3, 4) # [1, 10, 100, 1000]
# From random
rng = np.random.default_rng(seed=42)
rng.standard_normal((3, 3))
rng.integers(0, 100, size=10)
rng.uniform(0, 1, size=5)
rng.choice([1,2,3], size=10, p=[0.5, 0.3, 0.2])Always seed the Generator (default_rng), not the legacy np.random.* functions.
dtypes
| dtype | use |
|---|---|
int8, int16, int32, int64 | signed integers |
uint8, ..., uint64 | unsigned (uint8 for images) |
float16, float32, float64 | floats (float32 for ML) |
bool_ | True/False |
complex64, complex128 | complex |
str_ (fixed-width) / object | strings (slow; avoid) |
a.astype(np.float32)2. Indexing and slicing
a = np.arange(12).reshape(3, 4)
# [[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]]
a[0, 1] # 1
a[1] # [4,5,6,7]
a[:, 0] # [0,4,8] (first column)
a[1:, 2:] # subarray
a[::-1] # reversed rows
a[::2, ::2] # every other row, every other columnSlices are views (no copy). Mutations propagate to the original.
b = a[1]
b[0] = 99
a[1] # [99, 5, 6, 7] — changed!
b = a[1].copy() # explicit copy if neededFancy indexing (always copies)
a[[0, 2], [1, 3]] # diagonal of corners: a[0,1] and a[2,3]
a[[True, False, True]] # boolean mask; rows where True
a[a > 5] # 1D array of values > 5
a[(a > 3) & (a < 9)] # combine masks with &, |, ~ (parentheses!)np.where:
np.where(a > 5, a, 0) # element-wise: a if >5 else 0
np.where(a > 5) # indices where condition true3. Shape manipulation
a = np.arange(12)
a.reshape(3, 4)
a.reshape(3, -1) # -1 means "infer"
a.reshape(-1, 1) # column vector
np.concatenate([a, a]) # along axis 0
np.stack([a, a]) # add new axis
np.vstack([a, a]); np.hstack([a, a])
np.split(a, 4) # 4 equal parts
np.expand_dims(a, axis=0) # (12,) -> (1, 12)
a[..., np.newaxis] # add trailing axis
a.T # transpose
np.transpose(a, axes=(1, 0))
np.swapaxes(arr, 0, 1)Contiguity & memory layout
a.flags["C_CONTIGUOUS"] # True if row-major
a.flags["F_CONTIGUOUS"] # True if column-major
np.ascontiguousarray(a)For interop with C libraries (PyTorch, OpenCV) and best cache behaviour, use C-contiguous arrays.
4. Broadcasting — the killer feature
NumPy automatically aligns shapes for element-wise ops:
a = np.array([[1,2,3], [4,5,6]]) # shape (2, 3)
b = np.array([10, 20, 30]) # shape (3,)
a + b # broadcasts b across rows: [[11,22,33],[14,25,36]]
c = np.array([[100], [200]]) # shape (2, 1)
a + c # broadcasts across columnsRules
- Align shapes from the trailing dimension.
- Two dims are compatible if equal or one is 1.
- Otherwise → ValueError.
a: (2, 3)
b: ( 3)
→ align as (1, 3) and (2, 3) → result (2, 3)
a: (4, 3)
b: (4, 1)
→ result (4, 3)Broadcasting avoids explicit loops AND avoids creating intermediate copies. Reading shapes mentally is a core skill.
5. Vectorisation
Pure Python:
result = [x*x + 1 for x in data]NumPy:
result = data ** 2 + 1100× faster for size ≥ 1000.
Common reductions
a.sum(); a.mean(); a.std(); a.min(); a.max()
a.sum(axis=0) # column sums
a.sum(axis=1) # row sums
a.sum(axis=0, keepdims=True) # keeps shape (1, n)
np.cumsum(a); np.cumprod(a)
np.percentile(a, 95)
np.median(a)
np.var(a, ddof=1) # sample variance
np.argmin(a); np.argmax(a)
a.any(); a.all()Element-wise functions (ufuncs)
np.exp(a), np.log(a), np.sqrt(a), np.sin(a)
np.maximum(a, b) # element-wise max
np.minimum(a, b)
np.clip(a, 0, 10)
np.abs(a)
np.where(cond, x, y)
np.isnan(a); np.isfinite(a)All operate element-wise, broadcast naturally, and run in C.
6. Linear algebra
np.dot(a, b) # matrix multiplication (use @)
a @ b # preferred
np.linalg.inv(a)
np.linalg.det(a)
np.linalg.eig(a) # eigenvalues, eigenvectors
np.linalg.svd(a) # SVD
np.linalg.solve(A, b) # solve Ax = b
np.linalg.norm(a) # Frobenius / L2
np.linalg.matrix_rank(a)
np.linalg.pinv(a) # pseudoinverseFor ML, prefer np.linalg.solve(A, b) over inv(A) @ b — numerically more stable.
7. Boolean and integer masking idioms
data = rng.standard_normal(1000)
positive = data[data > 0]
data[data < 0] = 0
data = np.clip(data, 0, None)
# Count true
(data > 0).sum()
np.count_nonzero(data > 0)
# Pick top k indices
top_k = np.argpartition(data, -10)[-10:] # not sorted
top_k_sorted = top_k[np.argsort(-data[top_k])]
# argsort tie-broken indices
np.argsort(data)8. Saving and loading
np.save("arr.npy", a)
b = np.load("arr.npy")
np.savez("arrs.npz", a=a, b=b) # multiple arrays
data = np.load("arrs.npz"); data["a"]
# Compressed
np.savez_compressed("arrs.npz", a=a)For very large arrays use memory-mapped files:
m = np.memmap("big.dat", dtype="float32", mode="w+", shape=(10_000_000,))
m[:] = ... # writes to disk
del m # flushesFor interop with pandas / parquet / etc., go through pyarrow.
9. Random numbers (the modern API)
rng = np.random.default_rng(42)
rng.normal(loc=0, scale=1, size=10)
rng.uniform(0, 1, size=10)
rng.integers(0, 100, size=10)
rng.choice([1, 2, 3], size=5, replace=True, p=[0.6, 0.3, 0.1])
rng.shuffle(a) # in-place
rng.permutation(a) # copyPass a Generator to functions that need randomness — keeps tests deterministic.
10. Performance tips
- Avoid Python loops over arrays. If you find yourself writing
for i in range(len(a)), find a vectorised expression. - Use views (
a[:, 0]) over copies when possible. - Watch dtypes:
float64 + float32upcasts tofloat64;uint8 - 1wraps to 255. Explicit.astypewhen intent matters. - Pre-allocate: build outputs with
np.emptyand fill in, rather thannp.appendin a loop. np.einsumfor elegant tensor contractions:pythonnp.einsum("ij,jk->ik", A, B) # matrix multiply np.einsum("ii->i", A) # diagonal np.einsum("bij,bjk->bik", A, B) # batched matmulnumexpr(pip install numexpr) can sometimes outperform NumPy on big expressions by avoiding intermediates.
11. Worked example: image manipulation
import numpy as np
from PIL import Image
img = np.asarray(Image.open("photo.jpg")) # shape (H, W, 3), dtype uint8
# Convert to grayscale (Rec. 709 luminance)
gray = (img @ np.array([0.2126, 0.7152, 0.0722])).astype(np.uint8)
# Invert
inverted = 255 - img
# Crop centre 200x200
h, w = img.shape[:2]
crop = img[h//2 - 100 : h//2 + 100, w//2 - 100 : w//2 + 100]
# Brightness boost (clip to avoid overflow)
brighter = np.clip(img.astype(np.int32) + 30, 0, 255).astype(np.uint8)
Image.fromarray(gray).save("gray.png")No loops. Each operation runs at C speed.
Hands-on lab (2 hours)
- Generate 1M normally distributed numbers; compute mean, std, 95th percentile.
- Reshape
np.arange(24)into(2, 3, 4); sum along each axis; print shapes. - Build a 5×5 multiplication table with broadcasting (
np.arange(1,6)[:, None] * np.arange(1,6)). - Implement min-max scaling on a column matrix without loops.
- Solve
Ax = bwithnp.linalg.solvefor a 1000×1000 system. - Convert a colour image to greyscale (above example); time vs Python-loop version.
- Bonus: implement k-means initialisation (k-means++) using
np.argmin+ cumulative probability sampling.
Common pitfalls
- Confusing view vs copy; mutating a view changes the original.
- Forgetting
axis=—a.sum()returns scalar,a.sum(axis=0)returns array. ==between arrays returns an array; use.all()/.any()for booleans.- Mixing
np.array(multi-dim) withnp.matrix(deprecated; never use). - Using lists where arrays would work — silently slow.
np.append/np.concatenatein a loop — O(n²). Pre-allocate.- Reshaping non-contiguous arrays with implicit copy can spike memory.
Self-check
- Why is NumPy fast?
- State the broadcasting rules.
- Difference between a view and a copy.
- Why use
default_rngovernp.random.*? - When use
solvevsinv?
References
- Python for Data Analysis, Wes McKinney — Chapter 4.
- From Python to NumPy, Nicolas Rougier (free book).
- NumPy user guide: https://numpy.org/doc/stable/user/.
- Travis Oliphant, Guide to NumPy.
- "100 NumPy exercises" — github.com/rougier/numpy-100.
Sign in to save your progress and earn badges.