AI→ML
Roadmap  ›  Part III · Supervised Learning — Regression  ›  Module 3 · Regression
Lesson 10 · From scratch

Linear Regression: The Reasoning, the Math, the Geometry

This is the first real algorithm — and we start here on purpose. Linear Regression is the cleanest possible instance of the entire Machine Learning machine: a model with parameters, a loss that measures how wrong it is, and a procedure that tunes the parameters to make it less wrong. Master this one and every later algorithm becomes a variation on a theme you already understand. We will define the model, choose its loss, derive the exact solution by hand, see the geometry two different ways, and build the whole thing from scratch in NumPy before letting Scikit-learn do it in three lines.

Reading: ~40 min Prerequisites: Lessons 5–9 · the <T, P, E> triple, MSE, gradient intuition, dot products Code: from scratch in NumPy, then the Scikit-learn equivalent
Why Linear Regression is the right first algorithm

Every concept from Module 1 shows up here at once, in its simplest form. The model is a straight line. The parameters are its slope and intercept — two numbers. The loss is the mean squared error you met in Lesson 6. And there is a rare bonus: this loss can be minimized exactly, with a formula, so you can check the answer the optimizer of Lesson 7 is groping toward. Nothing else in the course lets you see the whole apparatus — model, loss, optimization, geometry — laid flat and fully solvable. That is why we spend a careful lesson on a model you could almost guess.

What this lesson delivers

1 · The problem: predicting a number

Regression is supervised learning where the answer is a continuous number: a price, a temperature, a duration, a dosage. Contrast it with classification (Part IV), where the answer is a category. If the quantity you want lives on a number line and "a bit more" or "a bit less" is meaningful, you have a regression problem.

Let's make it concrete with the example we'll carry through the whole lesson. You have data on houses — each one's size in square metres and the price it sold for — and you want to predict the price of a house you haven't seen. In Mitchell's template:

The regression task as <T, P, E>

T (task) = predict a house's price from its size. E (experience) = a dataset of past houses with both size and known price — the labels that make this supervised. P (performance) = how close our predictions land to the true prices, made precise in a moment as the mean squared error. Filling in this triple is the first thing to do with any new problem; if you cannot name E as concrete labelled data, you do not yet have a supervised learning problem.

Notation we'll use throughout, borrowed straight from Lesson 9. Each house is a training example (x(i), y(i)): x(i) is its feature (size), y(i) is its target (the true price), and i runs from 1 to n, the number of examples. Our prediction for example i is written ŷ(i) ("y-hat") — the hat always means "the model's guess", never the truth.

2 · The model: a straight line, and what its knobs mean

A model is a family of functions, and "choosing a model" means choosing which family before you've seen which member fits. Linear Regression makes the simplest non-trivial choice: assume the target is a straight-line function of the feature.

ŷ = w · x + b

Two numbers define the whole model — they are the parameters, the things learning will tune:

w
the weight (slope) — how much the prediction rises for each extra unit of the feature. In our example, extra thousands of dollars per additional square metre. Its sign is the direction of the relationship, its size the strength.
b
the bias (intercept) — the prediction when the feature is zero; where the line crosses the vertical axis. It shifts the whole line up or down so it doesn't have to pass through the origin.
The mental model that survives all course

Every parametric model in this course has the same shape: a fixed functional form with a few free numbers inside it. "Training" means searching over those numbers for the ones that fit the data best. Here the form is a line and the free numbers are (w, b). In Logistic Regression the same wx + b gets wrapped in a sigmoid; in a neural network it's stacked and repeated. You are not learning "a line" — you are learning the template form + parameters + fit for the first time, on the friendliest possible form.

More than one feature: the vectorized form

Houses have more than a size — bedrooms, age, distance to centre. With d features, each example is a feature vector x = (x1, …, xd) and each feature gets its own weight. The model becomes a weighted vote of the features plus a bias — exactly the dot product from Lesson 9:

ŷ = w1x1 + w2x2 + … + wdxd + b  =  w · x + b

This is why we spent a lesson on the dot product: it collapses "multiply each feature by its weight and add them up" into a single operation, and stacked over every row of the dataset it becomes the matrix–vector product X w that predicts for all n houses in one line. We'll develop the single-feature case in full because you can see it, then write the code so it already handles the vector case.

3 · The loss: making "best fit" a number we can minimize

There are infinitely many lines. "Best fit" is meaningless until we pick a rule that scores every line with a single number, so that lower is better and there's something concrete to minimize. That rule is the loss function of Lesson 6, and for regression the standard choice is Mean Squared Error.

For one example, the error is the residual — the gap between prediction and truth, ŷ(i) − y(i). We square it (so overshooting and undershooting both count as positive error, and big misses hurt disproportionately), then average over all n examples:

J(w, b) = (1/n) ∑i=1n ( w·x(i) + b − y(i) )2

Read this the way it's built. Inside: the residual for one house. Squared: the penalty for that house. Summed and divided by n: the average penalty across the dataset. J is written as a function of (w, b) because that is what we get to change — the data is fixed; the parameters are the variables. Training is now a precise sentence: find the (w, b) that make J as small as possible.

Why squared error, and not something else

Three reasons, in order of importance for us:

The price of squaring: sensitivity to outliers

Because a residual of 20 contributes 400 to the loss, a single freak data point — a mansion mispriced in the records, a data-entry typo — can drag the whole line toward itself to avoid that one enormous squared penalty. Squared error is not robust to outliers. This is a genuine weakness, not a detail: real pipelines inspect and clean the data first, and where outliers are expected, practitioners switch to more robust losses (mean absolute error, Huber loss). Keep this in your pocket — "MSE chases outliers" explains a lot of surprising fits.

4 · Solving it exactly: the line, by hand

Here is what makes Linear Regression special enough to teach first: we don't have to search for the minimum — we can write it down. The loss J(w, b) is a smooth bowl, so its lowest point is where the ground is flat in every direction: where both partial derivatives are zero. This is the calculus fact from Lesson 9 — at a minimum, the slope is zero — applied in two variables at once.

Set the derivative with respect to the bias to zero first. Differentiating J with respect to b (chain rule: the square's exponent drops to a factor of 2, the inside's derivative w.r.t. b is 1):

∂J/∂b = (2/n) ∑ ( w x(i) + b − y(i) ) = 0

Divide out the 2/n and split the sum: it says the average residual is zero, which rearranges to b = ȳ − wẁ, where ẁ and ȳ are the mean feature and mean target. In words: the best line always passes through the centre of mass of the data (ẁ, ȳ). That single fact is worth remembering on its own.

Now the derivative with respect to the slope (inside's derivative w.r.t. w is x(i)):

∂J/∂w = (2/n) ∑ x(i) ( w x(i) + b − y(i) ) = 0

Substitute b = ȳ − wẁ into that and solve for w. The algebra (worth doing once by hand as an exercise) collapses to a strikingly interpretable ratio:

w = ∑(x(i) − ẁ)(y(i) − ȳ)  /  ∑(x(i) − ẁ)2
What that formula is actually saying

The numerator is how feature and target vary together (the covariance): when a house is bigger than average, is its price also above average? The denominator is how much the feature varies on its own (the variance). So the slope is co-variation divided by variation — "when x moves, how much does y move with it, per unit of x's own spread." If size and price rise together, w is positive; if they're unrelated, the numerator averages to zero and w is zero — a flat line predicting the mean price for everyone, which is the best you can do when the feature tells you nothing.

The general case: the normal equation

With many features you can't isolate one weight at a time, but the same idea — set all the partial derivatives to zero simultaneously — produces one compact matrix equation. Fold the bias in by adding a column of 1s to the data matrix X (so b becomes just another weight), and the minimizer is:

θ = ( XTX )−1 XTy

This is the normal equation. It gives the exact best weights in one shot, no iteration — we'll use it in the code. But notice the matrix inverse: computing it costs on the order of d3 for d features, which becomes ruinous when d is large (thousands of features), and XTX can be numerically fragile. That cost is the entire motivation for the next lesson. When you can't afford to solve exactly, you descend iteratively instead — gradient descent — which is why Lesson 11 exists.

Two roads to the same minimum

You now have the exact answer (the normal equation) for a loss that also happens to be the textbook case for the approximate method (gradient descent). That's a gift: in Lesson 11 you'll implement gradient descent on this very same model and loss, and you'll be able to check that it converges to the numbers the formula gives here. Few settings let you grade an optimizer against ground truth. Linear Regression is the calibration rig for everything iterative that follows.

5 · The geometry — two ways to see it

Geometry one: the best-fit line (data space)

The picture everyone knows. Plot each example as a point — feature on the horizontal axis, target on the vertical. The model is a line through that cloud. For any point, the residual is the vertical gap from the point up or down to the line. Squaring a residual is, literally, the area of a square built on that gap; minimizing MSE means choosing the line that makes the total area of those squares as small as possible. Hence the classic name, least squares.

price
  |                                    •   ← residual = vertical gap
  |                              ______/|        from point to line
  |                        _____/    • |
  |                  _____/  |
  |            _____/     • | ← another residual
  |      •____/  •
  |  •__/    the fitted line  y_hat = w·x + b
  |_/________________________________________ size
       least squares picks the line that minimizes
       the total squared vertical distance to the points

Two things this picture makes obvious. First, the residuals are measured vertically, not perpendicular to the line — because we're predicting y from x, and error means error in the thing we predict. Second, the line pivots around the data's centre of mass (ẁ, ȳ), exactly as the b = ȳ − wẁ result promised.

Geometry two: least squares as a projection (the deeper view)

There's a second geometry that feels unrelated but is the same math from another angle — it's the one that makes linear algebra people smile. Stack all n targets into one vector y living in n-dimensional space. Every possible prediction vector Xθ you could produce, as you vary the weights, sweeps out a flat subspace (the column space of X). Your target y almost certainly doesn't lie in that subspace — no line hits every point — so the best you can do is the point in the subspace closest to y. That closest point is the orthogonal projection of y onto the subspace, and "closest" in the straight-line-distance sense is exactly least squares. The residual vector then sticks out perpendicular to the subspace — and setting that perpendicularity condition gives you the normal equation directly. Same answer, two pictures: minimizing vertical gaps in data space is projecting y onto a subspace in example space.

6 · From scratch in NumPy

Now we earn the title. Everything above — model, loss, closed-form solution — is a handful of NumPy lines. We'll use the toy house dataset, fit it two ways (the single-feature formula and the general normal equation), and confirm they agree.

Start with the model and the loss, each a direct transcription of its equation:

import numpy as np

# Toy dataset: 6 houses. Feature = size (m2), target = price (thousands $).
x = np.array([ 50,  60,  80,  95, 120, 150], dtype=float)   # x^(i)
y = np.array([150, 175, 210, 260, 300, 360], dtype=float)   # y^(i)

# The model:  y_hat = w * x + b   (a straight line)
def predict(x, w, b):
    return w * x + b                # works element-wise on the whole array

# The loss:  mean squared error, exactly J(w, b) from section 3
def mse(y_hat, y):
    return np.mean((y_hat - y) ** 2)

Next, the single-feature closed-form solution — the covariance-over-variance ratio, then the intercept through the centre of mass:

# --- Fit by the closed-form solution (simple linear regression) ---
x_bar, y_bar = x.mean(), y.mean()

w = np.sum((x - x_bar) * (y - y_bar)) / np.sum((x - x_bar) ** 2)   # cov / var
b = y_bar - w * x_bar                                          # centre of mass

print(w, b)                       # ~ 2.104  47.87   (k$ per m2, and k$ intercept)
print(mse(predict(x, w, b), y))   # ~ 35.1   (training MSE at the optimum; RMSE ~ 5.9 k$)

The slope says each extra square metre is worth about $2,100, and the model's predictions land, on average, within roughly $5,900 of the truth (the root of the MSE, back in the target's own units). Now the same fit via the general normal equation, which will already handle many features when you swap in a wider X:

# --- The general form: fold the bias into the weights ---
# Design matrix: a column of 1s (multiplying b) beside the feature column(s).
X = np.column_stack([np.ones_like(x), x])     # shape (6, 2): [1, size] per row

# Normal equation:  theta = (X^T X)^-1 X^T y  -- but SOLVE, never invert.
theta = np.linalg.solve(X.T @ X, X.T @ y)     # @ is matrix multiply
b, w = theta                                  # -> 47.87, 2.104  (same as above)
One habit worth forming now: solve, don't invert

The math says (XTX)−1XTy, but you should almost never call np.linalg.inv. Forming the inverse explicitly is slower and numerically shakier than asking the library to solve the linear system (XTX)θ = XTy directly with np.linalg.solve. Same answer, better conditioned. This is a small thing that separates code that merely runs from code a numerical practitioner trusts.

Finally, use the fitted model to predict — the whole point. A 100 m² house:

predict(np.array([100.0]), w, b)    # array([258.28])  -> about $258k

The Scikit-learn equivalent

Everything above is what a library does for you. Now that you've built it, here's the three-line version — and it's no longer a black box, because you know precisely what coef_ and intercept_ are and how they were found.

from sklearn.linear_model import LinearRegression

model = LinearRegression()                # fits an intercept by default
model.fit(x.reshape(-1, 1), y)          # sklearn wants X as 2-D: (n_samples, n_features)

model.coef_          # array([2.104...])  <- our w
model.intercept_     # 47.87...           <- our b
model.predict([[100]])   # array([258.28...]) <- identical to our from-scratch answer
Why we built it first anyway

Scikit-learn's LinearRegression is the normal equation (via a stable least-squares solver) with the bias handled for you — nothing more mysterious than what you just wrote. Building it from scratch buys you something the three lines can't: when a fit misbehaves later — wild coefficients, a line that ignores the data — you'll reason about it in terms of covariance, variance, conditioning, and outliers, not shrug at an API. This is the course's whole bet: the library is easy once the machine underneath it is not a mystery.

7 · What we quietly assumed (and when it breaks)

The fine print behind a clean line

How this connects to the next lesson

Bridge to Lesson 11

You have the model, the loss, its exact minimizer, and its geometry — and you've met the reason the exact route doesn't scale: inverting XTX is too expensive when features are many. Lesson 11 — Gradient Descent in Depth takes the alternative road. Instead of jumping to the bottom of the loss bowl with a formula, you'll roll down it step by step: compute the gradient of this exact J(w, b), take a step opposite to it, repeat. You'll implement it in NumPy on this same house dataset and watch it converge to w ≈ 2.10, b ≈ 47.9 — the numbers you already trust from the closed form. That's the payoff of doing the solvable case first: you get to grade the optimizer you're about to rely on for every model that has no formula.

Exercises & reflection

The first two are with pencil; the rest are at the keyboard. If you can do them, Linear Regression is genuinely yours.

Exercise 1 · Finish the derivation

Starting from the two zero-derivative equations in section 4, substitute b = ȳ − wẁ into the slope equation and grind the algebra until you reach w = ∑(x(i)−ẁ)(y(i)−ȳ) / ∑(x(i)−ẁ)2. Do it once; you'll never fear a normal equation again.

Show the key step

Expand the slope equation ∑ x(i)(w x(i) + b − y(i)) = 0 and replace b with ȳ − wẁ. Collecting the w terms gives w ∑ x(i)(x(i) − ẁ) = ∑ x(i)(y(i) − ȳ). The trick is that ∑(x(i) − ẁ) = 0, which lets you replace ∑ x(i)(x(i) − ẁ) with ∑(x(i) − ẁ)2 (and similarly on the right), delivering the covariance-over-variance ratio.

Exercise 2 · The line through the mean

Without touching code, what price does the fitted model predict for a house of exactly average size (ẁ = 92.5 m²)? Answer from the geometry alone, then verify with the numbers.

Show solution

The mean price, ȳ = 242.5 k$. The best line always passes through the centre of mass (ẁ, ȳ) — that's precisely what b = ȳ − wẁ encodes — so at x = ẁ the prediction is ȳ exactly. Check: 2.104 × 92.5 + 47.87 = 242.5. You never needed the slope.

Exercise 3 · Reproduce and inspect

Type the from-scratch code into a notebook and confirm you get w ≈ 2.104, b ≈ 47.87, MSE ≈ 35.1. Then print the six residuals (predict(x, w, b) - y). Verify that they sum to (almost) zero — and explain, from section 4, why they must.

Show discussion

The residuals should sum to essentially zero (up to floating-point dust). That's not luck: the zero-derivative condition for b was the statement "the average residual is zero." A nonzero sum would mean you could lower the loss by shifting b — so at the optimum it can't happen.

Exercise 4 · Break it with an outlier

Change the last price from 360 to 900 (a data-entry error — someone typed an extra digit) and refit. How much do w and b move? Relate what you see to the "MSE chases outliers" warning.

Show discussion

Both the slope and the fit degrade sharply — the line tilts up to chase the single inflated point, worsening its predictions for all five honest houses, because one residual of ~500 squared (250,000) dwarfs everything else in the loss. This is the outlier-sensitivity of squared error made visceral, and the reason "look at the data first" is not a platitude.

Exercise 5 · Add a second feature

Give each house a second feature — say bedrooms [1, 2, 2, 3, 3, 4] — and fit with the normal-equation code by stacking a second column into X (np.column_stack([np.ones_like(x), x, bedrooms])). Read off the three coefficients. Which feature does the model lean on more, and why can't you compare the raw weights directly?

Show discussion

You'll get an intercept plus one weight per feature. You cannot compare the raw weights to judge importance, because they're in different units (dollars per m² vs dollars per bedroom) and the features have different scales — a weight is "price change per one unit of that feature." Making weights comparable requires putting features on a common scale first, which is exactly the feature-scaling topic of Lesson 12.

One-paragraph summary to lock it in

Linear Regression is the entire Machine Learning machine at its simplest and, uniquely, fully solvable. The model is a line ŷ = wx + b — or, with many features, the dot product w·x + b — whose two knobs are a slope and an intercept. The loss is mean squared error, chosen because it is smooth, differentiable, and single-minimum'd, at the known cost of chasing outliers. Because that loss is a convex bowl, its minimum is where both partial derivatives vanish, and solving those gives an exact answer: the slope is covariance over variance, the intercept forces the line through the data's centre of mass, and in general the normal equation θ = (XTX)−1XTy delivers all the weights at once. Two geometries agree on it — the least-squares best-fit line in data space, and the orthogonal projection of y onto the column space of X. In NumPy the whole thing is a dozen lines that match Scikit-learn to the digit. And the one flaw in the exact route — that inverting XTX is too costly at scale — is the doorway to Lesson 11, where we reach the same minimum by descending toward it instead of solving for it.