The Scientific Python Stack: Equipment for the Loop You Already Understand
Module 1 finished the theory: model, loss, gradient, step. You can run that loop on paper. This lesson gives you the three tools that let you run it on real data at real speed — NumPy to hold numbers and do arithmetic on all of them at once, Pandas to hold a labelled table of mixed data, and Matplotlib to draw the loss curve you learned to read last lesson. This is a lesson about equipment, so it earns its place only by connecting every feature back to a question the theory already raised.
Because from Lesson 10 onward every algorithm in this course is built from scratch in NumPy before Scikit-learn is allowed anywhere near it. You cannot build a Linear Regression from scratch if the array that holds your data is a mystery. The from-scratch promise of this course — the thing that turns "I've heard of gradient descent" into "I have written gradient descent" — is cashed out entirely in these three libraries. So they are not a detour from the ML content; they are the surface the ML content is written on.
There is a sharper reason too. In Lesson 7 you computed a gradient over three houses by hand. Real datasets have millions of rows, and the gradient must be recomputed on every one of thousands of steps. Done with an ordinary Python for loop, that is slow enough to make learning impractical — not inconvenient, impractical, as in "the run finishes next week". NumPy is the reason it finishes in seconds instead. This lesson is where "training a model" stops being a thought experiment and becomes something your laptop can actually do.
What this lesson delivers
- Why plain Python lists are the wrong container for numeric ML, and what NumPy's array fixes.
- Vectorization: expressing a whole-dataset computation as one array operation — and why it is 10–100× faster, not just shorter.
- Broadcasting: the rule that lets arrays of different shapes combine, and how it removes loops you'd otherwise write.
- The shape discipline — the single habit that prevents most bugs you will meet from here to the end of the course.
- Pandas' DataFrame: why a labelled table of mixed columns is a different tool from an array, and where each belongs.
- Matplotlib: drawing the loss curve and the scatter plot that turn numbers into the diagnostics from Lesson 7.
- One end-to-end pass: CSV on disk → DataFrame → array → the training loop → a plotted loss curve.
1 · Why not just use plain Python?
Python already has lists, loops, and arithmetic. A house dataset is just a list of numbers; a gradient is just a sum. Why introduce a library at all? The honest answer has two halves, and both are worth seeing before you accept the tool.
Half one — lists are slow for numbers, structurally
A Python list is a list of objects. The number 200 inside a list is not eight bytes of value sitting next to its neighbours; it is a full Python object — a boxed integer with a type tag, a reference count, and a pointer — and the list holds a pointer to it, somewhere else in memory. Adding two lists element by element means, for every single element: follow a pointer, unbox the object, check its type, do the addition, box the result, store a new pointer. That per-element overhead dwarfs the actual arithmetic.
A NumPy array is the opposite: a single block of raw numbers, all the same type, laid out contiguously in memory — the same thing a C or Fortran array is. Arithmetic over it runs as a tight loop in compiled C, one machine instruction per element, with the CPU streaming cache-friendly memory and often doing several elements per instruction. The difference is not stylistic. It is the difference between an interpreter and a compiler, applied a million times.
| Task: add two sequences of 10 million numbers | Approx. time | Relative |
|---|---|---|
Python for loop over two lists | ~1,500 ms | ×1 (baseline) |
| List comprehension | ~800 ms | ~2× faster |
NumPy a + b | ~15 ms | ~100× faster |
The exact numbers depend on your machine; the order of magnitude does not. Two orders of magnitude is the gap between a training run you watch finish and one you abandon. And it compounds: the gradient loop from Lesson 7 does this kind of arithmetic on every step, thousands of times.
Half two — a dataset is not a bag of numbers
The speed argument gets you NumPy. But real data arrives as a table: rows are houses, columns are named quantities of different kinds — a floating-point size, an integer bedroom count, a text neighbourhood, a price with missing values scattered through it. A raw numeric array is the wrong shape for that: it has no column names, insists every cell share one type, and has no idea what a missing value is. That is Pandas' job. And once you have numbers, you need to see them — the loss curve, the scatter of points — which is Matplotlib's.
DataFrame) for loading, cleaning, and exploring data before it becomes a numeric array. Built on top of NumPy. This is where a dataset lives before training.You may know that Scikit-learn can do a linear regression in three lines. It can, and it sits on top of exactly these three libraries. We are holding it back on purpose: calling .fit() before you have written .fit() teaches you nothing about what it does. NumPy first, from scratch, so that when Scikit-learn arrives in Lesson 10's later half it is a convenience you understand rather than a black box you trust. The stack in this lesson is the foundation all of it — yours and theirs — is built on.
2 · NumPy: the array is the whole idea
Everything in NumPy is organised around one object, the ndarray (n-dimensional array). Learn its three defining attributes and most of the library follows.
An array is a grid of values, all of one type, indexed by integers. Three attributes describe it completely:
(3,) is three numbers in a line (a vector); (3, 2) is three rows by two columns (a matrix); (1000, 4) is a thousand rows of four features — the natural shape of a dataset.float64, int64, bool. Homogeneity is the price of speed and the reason arithmetic is one C loop.shape). A vector is 1-D, a matrix 2-D. In this course you will rarely go past 2-D.creating arrays — the handful of constructors you will actually use
import numpy as np # the universal alias; everyone writes np
prices = np.array([180, 200, 220]) # from a Python list -> shape (3,)
prices.shape # (3,)
prices.dtype # dtype('int64')
X = np.array([[60, 2], # a 2-D array: 3 houses, 2 features each
[95, 3], # column 0 = size m2, column 1 = bedrooms
[120, 4]])
X.shape # (3, 2) <- rows, then columns
np.zeros(5) # [0. 0. 0. 0. 0.] handy for initialising
np.ones((2, 3)) # a 2x3 block of ones
np.arange(0, 1, 0.25) # [0. 0.25 0.5 0.75] like range, but floats
np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1. ] 5 evenly spaced points
shape as "rows, then columns", alwaysThe convention that a dataset is (n_samples, n_features) — rows are examples, columns are features — is universal across NumPy, Pandas, and Scikit-learn. A dataset of 1,000 houses with 4 features each is (1000, 4). Fix this in your head now: nearly every array bug in the rest of the course is a shape you assumed was one thing and was another, and the fix always begins with printing .shape and reading it as rows-by-columns.
Indexing and slicing — grabbing rows, columns, and subsets
selecting parts of an array without copying loops
X[0] # first row: [60 2]
X[0, 1] # row 0, column 1: 2 (a single value)
X[:, 0] # EVERY row, column 0: [ 60 95 120] (all the sizes)
X[:, 1] # EVERY row, column 1: [ 2 3 4] (all the bedroom counts)
X[0:2] # first two rows (a slice, like a list)
# Boolean masking: select by a condition, no loop, no if.
prices[prices > 190] # [200 220] -> keeps only elements passing the test
prices[prices > 190] = 999 # ...and you can assign into the selection too
The comma inside the brackets is the thing to notice: X[:, 0] means "all rows, column zero", and it hands you an entire column as a 1-D array in one expression. In plain Python that is a loop; here it is punctuation. That colon-comma idiom — pulling a whole column out to feed the model — is one you will type hundreds of times.
3 · Vectorization & broadcasting: the loop that vanishes
This is the section that matters. Vectorization is the single skill that separates NumPy code that is fast and readable from NumPy code that is neither. The idea is a mental discipline as much as a technique.
Expressing an operation over an entire array at once, as a single expression, instead of looping over its elements in Python. You stop thinking "for each house, do X" and start thinking "do X to the whole column of houses". The loop still happens — but down in compiled C, where it is a hundred times faster, instead of up in the interpreter.
Arithmetic between arrays is element-wise by default. This is the foundation everything else builds on:
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
a + b # [11 22 33] pairs up, adds each pair
a * b # [10 40 90] element-wise product (NOT matrix multiply)
a ** 2 # [1 4 9] squares each element
a - 1 # [0 1 2] the 1 is applied to every element -> broadcasting
The rule that lets NumPy combine arrays of different but compatible shapes by conceptually stretching the smaller one to fit — without ever copying the data. a - 1 works because the scalar 1 is broadcast across all three elements of a. A (1000, 4) dataset minus a (4,) row of column-means subtracts the right mean from every column of every row, in one expression. It is how you apply one small thing to a whole big thing, loop-free.
Line up the two shapes from the right. Two dimensions are compatible when they are equal, or when one of them is 1 (the size-1 dimension is stretched to match). If every aligned pair is compatible, the arrays broadcast; otherwise NumPy raises a shape error — which is a gift, because it caught a bug you would otherwise have shipped.
The payoff: the Lesson 7 gradient, with no loop
Here is where the theory and the tool meet. In Lesson 7 you computed, by hand, the gradient of MSE for the line model ŷ = w·x + b. The math was:
Watch what those two formulas become in NumPy. Every sum-over-i disappears into a single array expression:
the same gradient, two ways — contrast them line for line
# --- the loop a beginner writes (correct, but slow and noisy) ---
def gradient_loop(x, y, w, b):
n = len(x)
dw, db = 0.0, 0.0
for i in range(n):
y_hat = w * x[i] + b # predict one house
error = y[i] - y_hat # its residual
dw += x[i] * error # accumulate the two sums by hand
db += error
return -2/n * dw, -2/n * db
# --- the vectorized version (fast, and reads like the math) ---
def gradient(x, y, w, b):
y_hat = w * x + b # ALL predictions at once -> array
error = y - y_hat # ALL residuals at once -> array
dw = -2 * np.mean(x * error) # one product, one mean -> scalar
db = -2 * np.mean(error) # one mean -> scalar
return dw, db
The two functions compute the identical numbers. But the second has no for, no index i, no running accumulators — and on a million rows it is roughly a hundred times faster. Read the vectorized version again and notice it is almost a transcription of the formula: y - y_hat is the residual y − ŷ, np.mean(x * error) is the average of xᵢ(yᵢ − ŷᵢ). This is the point of the whole lesson. When your NumPy reads like your mathematics, the code is easy to trust; when you're hand-managing indices and accumulators, it is easy to get subtly wrong.
The gradient used np.mean. That family — reductions — collapses an array to a summary, and you'll reach for them constantly: np.sum, np.mean, np.min, np.max, np.std. The axis argument controls the direction of the collapse, and it is worth burning in:
M = np.array([[1, 2],
[3, 4],
[5, 6]]) # shape (3, 2)
M.sum() # 21 everything, to one number
M.sum(axis=0) # [ 9 12] collapse the ROWS -> one total per column
M.sum(axis=1) # [ 3 7 11] collapse the COLUMNS -> one total per row
Mnemonic: axis=0 is the axis that disappears, so axis=0 crushes the 3 rows and leaves the 2 columns — a per-column result. That "the axis you name is the one that vanishes" rule saves you from guessing every time. Computing a per-feature mean to scale your data (Lesson 12) is exactly X.mean(axis=0).
A mismatched shape usually raises a loud error — good, you fix it immediately. The dangerous case is when shapes broadcast in a way you didn't intend and produce a wrong answer silently. The classic: subtracting a column vector of shape (n, 1) from a row vector of shape (n,) yields an (n, n) matrix, not the (n,) you wanted — and no error is raised because it is a legal broadcast. The defence is a habit, not a trick: print .shape whenever an array does something surprising, and keep a mental note of what shape each variable should be. This one discipline prevents the majority of bugs in the from-scratch lessons ahead.
4 · Pandas: the labelled table
NumPy holds numbers beautifully and knows nothing about what they mean. Real data needs names, mixed types, and a story for missing values. That is the DataFrame — think of it as a spreadsheet or a database table that lives in Python, or as a dictionary of equal-length columns where each column may have its own type.
A Series is a single labelled column: an array of values plus an index of labels. A DataFrame is a set of Series sharing one index — a two-dimensional table where every column is named and may hold a different type (one column float, another int, another text). The DataFrame is Pandas' whole reason to exist, and it is where a dataset lives from the moment you load it until the moment you hand clean numbers to the model.
loading and inspecting a dataset — the first five minutes with any data
import pandas as pd # the universal alias, like np
df = pd.read_csv("houses.csv") # a whole CSV file -> a DataFrame, one line
df.head() # first 5 rows -> always look before you leap
df.shape # (1000, 4) -> rows, columns, same convention as NumPy
df.columns # the column names
df.info() # types + how many non-null values per column (spot missing data)
df.describe() # count, mean, std, min/max per numeric column -> a fast sanity check
head, shape, info, and describe are not busywork — they are how you catch the disasters that silently ruin a model before you've trained anything. info reveals a column that is text when you assumed it was numeric, or that is missing 300 of its 1,000 values. describe reveals a "price" whose minimum is −1 or whose maximum is 10,000,000 — a data-entry error or a unit mixed up. Lesson 4 called data preparation most of the real work; this is the reconnaissance that starts it. A model trained on data you never looked at is a guess wearing a lab coat.
selecting columns, filtering rows, handling the missing
df["price"] # one column -> a Series
df[["size_m2", "bedrooms"]] # several columns -> a smaller DataFrame
df[df["price"] > 190] # rows passing a condition (boolean masking, as in NumPy)
df[df["bedrooms"] >= 3]["price"].mean() # chain: filter, pick a column, summarise
df.isna().sum() # how many missing values per column
df = df.dropna() # drop rows with any missing value...
df["size_m2"].fillna(df["size_m2"].mean()) # ...or fill them with the column mean
Here is the division of labour that ties the stack together. You explore, clean, and select in Pandas — because that is where names, types, and missing values are first-class. Then, at the moment of training, you hand plain numbers to NumPy — because the model and the gradient want a fast, homogeneous, nameless array, and nothing else. That handoff is one method call:
X = df[["size_m2", "bedrooms"]].to_numpy() # features -> shape (n, 2)
y = df["price"].to_numpy() # target -> shape (n,)
# From here on it is pure NumPy: X and y feed the exact loop from Lesson 7.
Keep the two roles distinct in your mind and the stack stops feeling like two overlapping tools. Pandas is the workshop where you prepare the material; NumPy is the engine you feed it into. Scikit-learn, when it arrives, will speak this same X, y language — which is why the convention is worth internalising now.
5 · Matplotlib: making the numbers visible
Lesson 7 told you the loss curve is your primary diagnostic instrument — that its shape tells you whether the learning rate is healthy, too small, or diverging. Matplotlib is how you actually draw it. Two plot types carry almost everything you'll need in this course.
the loss curve — the plot you learned to read before you could draw it
import matplotlib.pyplot as plt # the universal alias: plt
# Suppose training filled this list, one loss value per step (see Section 6).
plt.plot(loss_history) # x = step index (0,1,2,...), y = the losses
plt.xlabel("step")
plt.ylabel("cost J")
plt.title("Loss curve")
plt.show() # render the figure (or savefig to a file)
That handful of lines produces the exact curve from Lesson 7 — the steep initial drop, then the long flattening tail of a healthy run, or the jagged bouncing of an α set too high. You now have both halves: last lesson taught you what the curve means; this line teaches you how to see it. Reading the loss curve stops being a metaphor and becomes something on your screen.
the scatter plot — look at your data before you model it
plt.scatter(X[:, 0], y) # feature (size) on x, target (price) on y
plt.xlabel("size (m2)")
plt.ylabel("price (k EUR)")
# overlay the line the model learned, to SEE the fit
xs = np.linspace(X[:, 0].min(), X[:, 0].max(), 100)
plt.plot(xs, w * xs + b, color="red") # the fitted line y = wx + b
plt.show()
Before fitting anything, a scatter plot answers the question that decides whether a line is even the right shape: does price roughly rise with size? If the cloud of points trends upward, a linear model is a sensible bet (Lesson 5's "shape is a bet"). If it curves, or fans out, or splits into two clusters, you have learned that before wasting a training run — and you've learned which lesson you actually need (polynomial features, scaling, or a different model). Plotting is not decoration you add at the end. It is reconnaissance you do at the start, and a debugger you return to whenever a number surprises you.
A scatter plot shows one feature against the target. Real models have dozens of features, and you cannot plot a 50-dimensional cloud — your eyes stop at three axes. So visualization is indispensable for intuition and debugging on one or two features at a time, and useless as a way to "see" a high-dimensional fit directly. This limit is one of the motivations for dimensionality reduction (Lesson 26), which tries to squeeze many features down to two you can plot. For now, take the lesson as: plot relentlessly, but know that a good-looking 2-D plot is not proof the full-dimensional model is good.
6 · Putting the stack together: one honest pass
Everything so far, assembled into the shape every project in this course takes. This is the Lesson 7 training loop — unchanged — now fed by real data through Pandas and NumPy, and reporting itself through Matplotlib. Read it as the skeleton you'll flesh out from Lesson 10 onward.
load → prepare → train → plot — the whole stack in one screen
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 1. LOAD + PREPARE (Pandas): the workshop
df = pd.read_csv("houses.csv")
df = df.dropna() # deal with missing values honestly
x = df["size_m2"].to_numpy() # hand clean numbers to NumPy
y = df["price"].to_numpy() # shapes: both (n,)
# 2. TRAIN (NumPy): the engine -- this is the Lesson 7 loop, verbatim
w, b = 0.0, 0.0 # start anywhere on the landscape
alpha = 0.0001 # learning rate (needs scaling; see note)
loss_history = [] # we will plot this afterwards
for step in range(1000):
y_hat = w * x + b # MODEL -> all predictions (Lesson 5)
error = y - y_hat # residuals
J = np.mean(error ** 2) # LOSS: MSE, one number (Lesson 6)
loss_history.append(J)
dw = -2 * np.mean(x * error) # GRADIENT, vectorized (Lesson 7)
db = -2 * np.mean(error)
w = w - alpha * dw # STEP: the update rule (Lesson 7)
b = b - alpha * db
# 3. REPORT (Matplotlib): the instrument
plt.plot(loss_history) # is the curve healthy? (Lesson 7)
plt.xlabel("step"); plt.ylabel("cost J")
plt.show()
The entire middle block — the actual learning — is the four-line loop you already understand from Lesson 7, translated into NumPy. Pandas wraps the front (getting data in), Matplotlib wraps the back (seeing what happened), and NumPy is the engine in the centre. You have now seen a complete, runnable training program, and there is nothing mysterious left in it. From Lesson 10 the model and loss lines will change to become real Linear Regression, then Logistic Regression, then the rest — but this scaffold, and these three libraries, stay exactly as they are.
alpha = 0.0001The learning rate above is suspiciously tiny, and there is a reason: house sizes (tens to hundreds) and prices (hundreds of thousands) live on wildly different scales, which — exactly as Lesson 7 warned — creates a long, narrow ravine where any larger α diverges. The real fix isn't a microscopic learning rate; it's feature scaling, which flattens the ravine into a round bowl and lets a normal α work. That's Lesson 12. This snippet runs, but it runs badly on purpose — a reminder that having the tools is not the same as using them well, which is what the rest of the course is for.
7 · Limitations — what to keep honest about
- These are tools, not understanding. Knowing NumPy does not mean you know machine learning any more than knowing a text editor means you can write. The stack is the medium; the ideas from Modules 1 and 3–8 are the content. Never mistake fluency with the library for fluency with the subject.
- Vectorized code is fast but not free. A whole-array operation allocates a whole new array. On very large data, chaining many array operations can exhaust memory even while each one is individually fast. Speed and memory are different budgets.
- Broadcasting can be silently wrong. A shape mismatch that errors is a friend; a shape mismatch that broadcasts into the wrong answer is a bug that ships. Print
.shapewhen confused — the habit is cheaper than the debugging session it prevents. - Pandas is for preparation, not for the hot loop. Explore and clean in Pandas; convert to a NumPy array before the training loop. Doing arithmetic per-row inside a DataFrame in a Python loop throws away every speed advantage NumPy gave you.
- A plot persuades whether or not it is honest. Truncated axes, a hidden log scale, or cherry-picked ranges can make a bad model look good and a good one look bad. Label your axes, start counts at zero, and be as skeptical of your own plots as of anyone else's.
- This is a working subset, not the whole libraries. Each of these tools is vast. You have the parts this course needs; you have not "learned NumPy" in full, and that is fine. Depth comes from use, not from reading the API reference cover to cover.
How this connects to the next lesson
You can now hold a dataset as an array, express a whole-dataset computation as one loop-free expression, load and clean real data, and plot the diagnostics from Lesson 7. That is the entire toolbox the from-scratch lessons require. One piece of preparation remains, and it is the twin of this one: where this lesson sharpened your tools, Lesson 9 sharpens your vocabulary.
Lesson 9 covers just enough mathematics to make Lesson 10's derivations comfortable rather than intimidating: vectors and the dot product (so that w·x + b over many features is one operation, not a sum you write by hand), derivatives as slope (formalising the nudge you already did in Lesson 7), and probability (which Logistic Regression and Naive Bayes will need). Crucially, it is math with the NumPy already attached — you'll see the dot product as both a formula and a single np.dot call. Then Lesson 10 opens Part III and builds a real Linear Regression from scratch, and you will arrive holding both halves it needs: the equipment from this lesson, and the notation from the next.
Exercises & reflection
These are meant to be typed into a Python session and run. The stack is learned at the keyboard, not on the page — predict each answer before you run it, then check.
Given X = np.array([[1, 2, 3], [4, 5, 6]]), write down — before running anything — the value and shape of each: (a) X.shape, (b) X[:, 1], (c) X.sum(axis=0), (d) X.sum(axis=1), (e) X.mean(). Then run them and reconcile any surprises.
Show solution
(a) (2, 3) — two rows, three columns. (b) [2 5], shape (2,) — column 1 from each row. (c) [5 7 9], shape (3,) — axis=0 crushes the 2 rows, leaving a per-column total. (d) [6 15], shape (2,) — axis=1 crushes the 3 columns, leaving a per-row total. (e) 3.5 — a single scalar, the mean of all six. The trap is (c) vs (d): the named axis is the one that disappears.
Here is a slow, loop-based function that computes Mean Squared Error. Rewrite it as a single vectorized expression using np.mean, with no for and no index.
def mse_loop(y, y_hat):
total = 0.0
for i in range(len(y)):
total += (y[i] - y_hat[i]) ** 2
return total / len(y)
Show solution
def mse(y, y_hat):
return np.mean((y - y_hat) ** 2)
One line. y - y_hat broadcasts into the whole array of residuals, ** 2 squares each element, np.mean reduces to the average. It is faster and it reads like the definition of MSE from Lesson 6. If you can do this transformation fluently, you have the core skill this lesson set out to teach.
For each pair of shapes, say whether NumPy broadcasts them and, if so, the shape of the result: (a) (3, 4) and (4,); (b) (3, 4) and (3,); (c) (3, 1) and (1, 4); (d) (5,) and (5, 1).
Show solution
Align from the right; each pair must be equal or contain a 1. (a) Yes → (3, 4): 4 matches 4; the missing left dim of the second is treated as 1 and stretched to 3. (b) No: aligning from the right pairs 4 with 3 — unequal, neither is 1 — error. (This is the common bug: to subtract a per-row value you must reshape (3,) to (3, 1) first.) (c) Yes → (3, 4): the 1s each stretch — a classic "outer" combination. (d) Yes → (5, 5): (5,) becomes (1, 5), pairs with (5, 1), both 1s stretch. Note (d) is exactly the silent-bug shape from Section 3 — a legal broadcast that is almost never what you meant.
You are handed df = pd.read_csv("houses.csv") and nothing else. In four or five sentences, describe the exact sequence of commands you would run before training anything, and for each say what disaster it is designed to catch. Then explain, in your own words, why you would convert to a NumPy array with .to_numpy() rather than training directly on the DataFrame.
Plain Python is the wrong tool for numeric ML for two structural reasons: its lists box every number into a scattered object, making element-wise math roughly a hundred times slower than it needs to be, and it has no native notion of a labelled, mixed-type table. The scientific stack answers both. NumPy's ndarray — described by its shape, dtype, and ndim — stores numbers contiguously and does arithmetic on all of them at once in compiled C; vectorization expresses a whole-dataset operation as one loop-free expression, and broadcasting (align shapes from the right; each pair must be equal or one) lets differently-shaped arrays combine, which is exactly how the Lesson 7 gradient collapses from a hand-written loop into -2 * np.mean(x * error) that reads like its own formula. Pandas' DataFrame is the workshop where you load a CSV, inspect it with head/info/describe to catch missing values and unit errors, filter and clean — then hand plain numbers to the engine with .to_numpy(). Matplotlib makes the numbers visible: the loss curve whose shape Lesson 7 taught you to read, and the scatter plot that tests whether a line is even the right bet before you fit one. Assembled, they run the complete load→train→plot pass, in which the learning itself is still just the four-line loop you already knew — and the deliberately tiny learning rate in that pass is a standing reminder that the ravine from unscaled features (Lesson 12) is still waiting. Tools acquired; Lesson 9 sharpens the matching vocabulary, and Lesson 10 puts both to work building Linear Regression from scratch.