Day 64 Β· Power tools for numbers

NumPy in Anger

You will be able to
  • Explain what an ndarray is (buffer + dtype + shape) and why that makes it fast
  • Replace Python loops with vectorized operations and measure the speedup
  • Apply the broadcasting rules to predict result shapes before running the code
  • Use boolean masks, fancy indexing, and axis-aware aggregations fluently
  • State the view-vs-copy rules and avoid the mutation bugs they cause
Today's ~120 minutes
Spaced-rep warm-up: Week 9 due cards + Day 63 misses10 min
ELI5 + tech read: buffers, broadcasting, views20 min
Guided: timing lab + broadcasting/axis drills38 min
Practice: the image is just an array18 min
Project: numpy_drills.py24 min
Quiz + flashcards10 min

Builds on: Day 50 β€” Vectors & NumPy basics Β· Day 51 β€” Matrices & matmul Β· Day 57 β€” Random generators & boolean masks

The analogy

You have been cutting boards one at a time with a hand saw: a Python loop picks up each number, works on it, puts it down. NumPy is the table saw: you stack a thousand boards, push the whole stack through, done. The speed is real β€” often 100Γ— β€” because the saw blade is compiled C running over a solid block of memory, instead of Python picking up and unwrapping each number individually.

But power tools have two safety rules. First, the guide fence β€” broadcasting: when two stacks of different sizes meet, NumPy lines them up from the right edge and stretches any side of size one to match. Learn the two-line rule and shapes stop being guesswork. Second, and this one bites everyone exactly once: when you slice an array, you usually get a window onto the SAME wood, not a fresh copy. Sand the piece in your hand and you discover you have sanded the original. Knowing when you hold a view and when you hold a copy is the difference between a power user and a person with mysterious data corruption. Today you use the machine in anger β€” timed, on real-sized data β€” because Week 10 is data craft and every tool this week stands on this one.

Why this matters on the job

pandas columns ARE NumPy arrays; PyTorch tensors (Day 87) copy NumPy's API almost verbatim, broadcasting rules included. A data pipeline written with Python loops is the classic "worked in the demo, died on the customer's 10-million-row file" story β€” Day 22's complexity lesson, but the constant factor is 100Γ—. And shape fluency is the daily language of ML debugging: half of all deep-learning bugs are shape bugs, and the engineers who read (1000, 3) βˆ’ (3,) at a glance fix them in seconds.

Guided practice

guided 1

Time the loop out of your code

18 min
  1. Paste the starter. It computes the same two results β€” sum of squares, then standardization (subtract mean, divide by sd) β€” on a million floats, once with Python loops and once vectorized.
  2. Run it and record the two speedups. Expect roughly 50–200Γ— depending on the machine. Say out loud WHY: the loop version unboxes a Python object per element; the vectorized version is one C loop over a packed buffer.
  3. Add a third comparison of your own: count how many values exceed 2.0 (loop with a counter vs (x > 2).sum()).
  4. Now the dtype experiment: rebuild the data as float32 and re-time. Then make an int8 array of 200 and add 100 to it β€” observe the overflow wraparound. Write the rule: dtype is a contract, and arithmetic near the edges of the contract lies silently.
  5. Check memory: x.nbytes for the float64 vs float32 versions. Halving memory matters when Day 66 loads real datasets.
🐍 python β€” editable, runs in your browser
Ctrl/⌘+Enter runs · Tab indents · numpy/pandas/sklearn auto-load on import (torch and network calls need a local run)
guided 2

Broadcasting and axes β€” predict, then run

20 min
  1. The starter creates data with shape (1000, 3): a thousand rows of [duration_ms, tokens, cost]. For each expression below, WRITE the result shape on paper first, then run and check: a. data - data.mean(axis=0) (center each column) b. data / data.max(axis=0) (scale each column to ≀ 1) c. data.mean(axis=1) (row means β€” shape (1000,)) d. col = data[:, 0]; col[:, None] * np.ones(3) (what shape? why?)
  2. The trap, on purpose: v = np.arange(4); w = v[:, None]; print((v + w).shape) β€” a (4,) plus a (4,1) makes (4,4). Write one sentence on when this trap fires in real code (adding a "column" that is actually 1-D to a row vector).
  3. Center the columns using an explicit [None, :] instead of relying on automatic alignment, and confirm it is identical.
  4. The axis mantra: the axis you pass is the one that disappears. Verify on data.sum(axis=0) β†’ (3,) and data.sum(axis=1) β†’ (1000,).
  5. Finish with the view/copy drill: window = data[:10], set window[:] = -1, and check data[:3]. Then redo with .copy() and confirm the original survives.
🐍 python β€” editable, runs in your browser
Ctrl/⌘+Enter runs · Tab indents · numpy/pandas/sklearn auto-load on import (torch and network calls need a local run)

On your own

The image is just an array

18 min

Build a synthetic 128Γ—128 grayscale "image" without any files: img = np.clip(xx*0.5 + yy*0.5 + rng.normal(0, 0.05, (128,128)), 0, 1) where xx, yy = np.meshgrid(np.linspace(0,1,128), np.linspace(0,1,128)) β€” a noisy diagonal gradient, values in [0, 1].

Your goals, all loop-free: (1) brighten by 0.3 WITH clipping to [0, 1] and explain what unclipped uint8 arithmetic would have done instead; (2) increase contrast: (img βˆ’ 0.5) Β· 1.8 + 0.5, clipped; (3) invert it; (4) extract the center 64Γ—64 crop with slicing and state whether it is a view or a copy; (5) apply a checkerboard mask (built from index parity via meshgrid or np.indices) that zeroes alternating 8Γ—8 blocks; (6) report mean brightness per row for the top 5 rows (one axis-aware call).

Hints: (row // 8 + col // 8) % 2 gives the checkerboard at block scale; a slice is always a view β€” mutate the crop and check the original. Print img.min(), img.max() after every step; leaving [0,1] silently is how image bugs are born.

Ship before you stop

numpy_drills.py β€” eight one-liners you will reuse all program

Create numpy_drills.py in your practice repo: eight functions, each essentially one vectorized expression, each with an assert proving it on a tiny hand-checked example: (1) standardize(X) per column; (2) minmax(X) per column; (3) pairwise_dist(A, B) via broadcasting (A[:, None, :] βˆ’ B[None, :, :], then norm over the last axis); (4) one_hot(labels, k); (5) moving_avg(x, w) via cumsum; (6) clip_outliers(x, k) replacing values beyond k standard deviations with the boundary; (7) softmax(Z) row-wise, stable (subtract row max β€” Day 62's exp in disguise); (8) accuracy(pred, y). No Python loops anywhere. Commit it: Day 69's features, Day 85's forward pass, and Day 92's nearest-neighbor search all reuse these exact moves.

Rubric β€” check what you completed (0/5)

Common mistakes & misconceptions

  • Mutating a slice and corrupting the original. Basic slices are views; call .copy() when you need independence, and know that boolean/fancy indexing already return copies.
  • The (n,) vs (n,1) confusion. Adding them broadcasts to (n, n); a "vector" and a "column" are different shapes. Use [:, None] to be explicit.
  • Reading axis=0 as "along the rows, per row". The passed axis is the one that COLLAPSES: axis=0 on (1000, 3) gives 3 column-aggregates.
  • Writing Python loops over arrays out of habit. If you typed "for" over an ndarray, stop and look for the vectorized form β€” it exists ~95% of the time.
  • Ignoring dtype until it bites: int8/uint8 overflow wraps silently, float32 loses precision in big sums, and integer division truncates.
  • Assigning into the result of fancy indexing and expecting the original to change: x[idx][0] = 5 writes into a temporary copy and is silently lost.
Knowledge check

Q1. a has shape (4,) and b has shape (4, 1). What is (a + b).shape?

Q2. For data of shape (1000, 3), what does data.mean(axis=0) return?

Q3. b = a[10:20]; b[:] = 0. What happened to a?

Go deeper β€” curated resources

docsNumPy β€” the absolute basics for beginners β†—25 mindocsNumPy β€” Broadcasting (official guide with figures) β†—20 mindocsNumPy β€” Random sampling (Generator API) β†—10 min
If you have a third hour
  • Strides β€” how NumPy walks memory β€” Shape Γ— strides explains why transposes are free, why some views are possible and others are not, and what "contiguous" means in PyTorch error messages later. A 15-minute read that demystifies a dozen future errors.
Done means
  • Timing lab run: both speedups recorded and the overflow demo observed
  • All broadcasting shapes predicted correctly before running (or misses noted and re-derived)
  • Image practice done loop-free with values verified in [0, 1] after each step
  • numpy_drills.py committed with all asserts passing; quiz β‰₯ 2/3
How this connects

← Back: Day 50 introduced these arrays gently; Day 57 used masks for probability; Day 22 explained why the loop dies at scale β€” today you measured the constant factor. The stable softmax is Day 62's cross-entropy machinery.

Forward β†’: Tomorrow pandas wraps these arrays in labels β€” every fast pandas operation is a NumPy operation underneath. Day 85's forward pass is matmul + broadcasting; Day 87's PyTorch tensors follow these exact rules on a GPU.

Unlocks: D65 pandas I β€” DataFrames Β· D69 Feature Engineering Β· D70 Week 10 Checkpoint: EDA Report Β· D79 PCA & Dimensionality