learn-classification-regression-with-phoebe / Session 2 of 10
Learn Classification & Regression with Phoebe · Session 2 of 10

Linear regression, properly

In intro-ml's b8 you fit this model to judge it - RMSE, MAE, R², done. Today we open it up. You will see exactly which line least squares picks and why, read a slope as literal dollars in front of a stakeholder, add features and watch the fit honestly improve, and then bend the line with polynomial features - first to a shrug (the truth is linear, bending adds nothing) and then to one of the most spectacular failures in the whole course: a degree-12 polynomial whose test R² is minus 798,595. Same Lumen buyers, same seed 42, every number reproducible on your machine.

🟢 Foundations Builders · DA / DE / DS Python · scikit-learn 45 min
0-3 · Setup 3-18 · The line and its loss 18-40 · Build: line to explosion 40-45 · Wrap
Part 0

You have used this model. You have not met it.

In intro-ml, linear regression was the thing you evaluated: fit on the 1,306 Lumen buyers, print the metrics, move on. That was the right order - workflow first. But a model you can only score is a model you cannot defend in a meeting. This session is the meeting-proofing: what the line optimizes, what each learned number means in dollars, and where the whole idea breaks when you give it too much freedom.

Live - built in session Self-study - run after ★ The one big idea The case: Lumen Skincare
★ What you walk out with today The ability to read a coefficient in plain dollars ("44 cents of order value per prior-spend dollar"), an honest grip on R² vs RMSE vs MAE, and the judgement of when to bend the line with polynomial features - and when bending it detonates. The explosion table alone (degree 12, test R² -798,595) will change how you feel about flexible models forever.
Part 1 · the loss

The line and its loss 9 min live

Through any cloud of points you could draw infinitely many lines. Least squares picks one by a simple rule: measure each point's vertical miss (the residual), square it, add them all up - and choose the line that makes that sum smallest. Everything else in this session is consequences of that one choice.

Least squares - the line that loses least prior 30-day spend ($) order value ($) residual, squared: the penalty this point pays Reading the fitted line slope ~0.44 +44 cents of order value per prior-spend dollar intercept ~$42 a zero-history buyer's expected order R² 0.43 · RMSE ~$30 vs predict-the-mean baseline RMSE ~$40 Of every possible line, least squares keeps the one that makes the squared residuals smallest.
🔍 Click to zoom - residuals, their squares, and the line that minimizes the total
LiveResiduals and the loss - why squared, of all things?3 min▶

A residual is the vertical gap between a point and the line: actual minus predicted. Sum the raw residuals and positives cancel negatives - a terrible line can score zero. So we square first. Squaring does two jobs at once: it makes every miss count against you, and it makes big misses count disproportionately - a $20 miss costs 4x a $10 miss, not 2x. The line that wins is the one whose worst embarrassments are smallest.

There is a quieter reason too: the squared loss is smooth, so calculus can find the minimum exactly, in one closed-form step. That is why LinearRegression().fit() returns instantly with no learning rate, no iterations, no randomness. Session 4 shows the iterative alternative (gradient descent) that logistic regression needs - linear regression is the lucky one that does not.

Vocabulary check: "ordinary least squares" (OLS), "squared error loss", "minimizing the residual sum of squares" - all the same idea. When sklearn's docs say OLS, they mean this picture.
LiveReading coefficients in dollars3 min▶

Fit order value on prior 30-day spend alone (Lumen buyers, n=1,306) and the model learns two numbers: slope ~0.44 and intercept ~$42. Say them like a human: "every extra dollar a customer spent with us last month is worth about 44 cents on today's order, and a customer with no history lands around $42." That sentence is the entire model - and it is a sentence a CMO can act on.

Quality check: this one-feature model scores R² ~0.43 with RMSE ~$30 on the held-out test buyers. The floor from session 1 - just predict the mean order value - has RMSE ~$40. One feature bought you $10 of typical error back. Keep both numbers in the same breath; a metric without its baseline is decoration.

Real worldA retail analyst once presented "the coefficient on loyalty spend is 0.44" and watched the room glaze over. Same meeting, rephrased as "44 cents of today's basket per dollar of last month's spend", the CFO asked for the retention budget to be re-run by lunchtime. The model did not change. The units did.
LiveR², honestly2 min▶

R² is the share of variance explained: how much of the spread in order values the model accounts for, compared to just predicting the mean. R² 0.43 means the model explains ~43% of why orders differ - and 57% remains unexplained noise or missing features. It is NOT accuracy, it is not "the model is 43% right", and on its own it does not tell you the dollar size of your errors - that is RMSE's job.

The trio you already met in intro-ml, one line each: R² for "how much of the pattern did we catch" (unitless, baseline 0), RMSE for "typical miss in dollars, big misses amplified", MAE for "typical miss in dollars, everyone counted equally". Report R² to modelers, RMSE or MAE to the business.

Self-studyMulticollinearity - when coefficients get wobbly3 min▶

sklearn's OLS guide (section 1.1.1) carries one standing warning: least squares assumes your features are reasonably independent. When two features are strongly correlated - say you added both "prior 30-day spend" and "prior 90-day spend" - the design matrix becomes nearly singular and the fit can split credit between the twins almost arbitrarily. Predictions stay fine; the coefficients become unstable, swinging wildly with tiny changes in the data.

The practical rule: the moment you care about reading coefficients (and today you do), check correlations among your features first. If two features tell the same story, keep one, or accept that their individual coefficients are not individually meaningful. Ridge regression (end of Part 3) is the other standard fix - it calms the swinging at the price of a little bias.

Part 2 · more features

More features, more truth 8 min live

Spend alone explains 43% of the variance. Add what else we know about each buyer - product category, channel, new vs returning - and the full linear model reaches R² ~0.59, RMSE ~$25, MAE ~$20 on the test buyers. Same algorithm, same closed-form fit; the only thing that changed is how much of the story the model was allowed to hear.

LiveDesign matrices - how categories become columns3 min▶

A line cannot multiply "skincare" by a coefficient. pd.get_dummies(..., drop_first=True) turns each categorical into 0/1 columns, dropping one level per feature. The dropped level becomes the reference category baked into the intercept, and every dummy coefficient reads as "dollars relative to that reference". If affiliate is the dropped channel, the email coefficient means "email buyers order this many dollars more (or less) than affiliate buyers, all else equal".

Why drop one? With all levels present plus an intercept, the columns sum to a constant - perfect multicollinearity, the pathological case of the warning above. StatQuest calls this whole encoded table the design matrix; sklearn just calls it X. Inside pipelines we use OneHotEncoder(handle_unknown="ignore") instead, which keeps all levels but pairs with regularized models - today's get_dummies path is the interpretability convention this course uses whenever we read coefficients.

Self-studyWhy MAE sits below RMSE - always, here2 min▶

Full model: RMSE ~$25, MAE ~$20. That gap is not an accident - RMSE squares before averaging, so a handful of badly-missed orders dominate it, while MAE treats a $50 miss as exactly five $10 misses. Lumen order values are right-skewed (mean $82, SD $41 on buyers - a long tail of big baskets), and the model misses biggest on exactly those tail orders. Skewed target, so RMSE above MAE, every time.

The gap itself is diagnostic: if RMSE towers over MAE, your errors have a heavy tail - go look at which rows the model misses worst before reaching for a fancier algorithm.

LiveA note on intro-ml's printed numbers2 min▶

Sharp-eyed alumni will notice: intro-ml's b8 page printed ~0.52 / $28 for this same full model. Re-validated in July 2026, the same code in the current environment produces 0.59 / $25 - the numpy random stream behind the generator drifted across library versions, so the "same" dataset is no longer bit-identical. The numbers on this page are the ones that reproduce today, and a fix for the intro-ml page is flagged.

The real lesson: models are code. When your stack moves - a numpy upgrade, a new sklearn - re-run your numbers before you re-present them. A metric you cannot reproduce is a rumor.
Part 3 · bending the line

Bending the line - and the explosion 8 min live

PolynomialFeatures adds x², x³, ... as new columns, letting the line curve. Here is the twist most people miss: the result is still linear regression - linear in the weights. The features bend; the model does not. Same least squares, same closed-form fit, just a wider design matrix. On the full 1,306 buyers, degrees 2-3 add nothing: R² stays at 0.43 with spend as the input, exactly matching degree 1. That flatness is a finding - the spend-to-order relationship really is a straight line, and no amount of bending buys anything. Then we shrink the data, and everything changes.

Polynomial degreeTrain R²Test R²Verdict
1 (straight line)0.37+0.43honest
20.37+0.43harmless
30.37+0.34starting to slip
60.43-26.7worse than predicting the mean
120.47-798,595the explosion

The setup: 80 buyers subsampled (seed 0), 50 train / 30 test, polynomial on scaled spend. Watch the train column climb politely while the test column falls off a cliff six orders of magnitude deep. Train R² 0.47 looks like the "best" model. It is the worst model on this page by a factor of about two million.

Same 50 training points. Two very different lines. Degree 1 - calm misses every point a little, nothing a lot Test R² +0.43 boring, and right Degree 12 - the explosion hits the points, detonates at the edges Test R² -798,595 confident, and catastrophic Flexibility without restraint is a liability - the wiggle swings between points and explodes past the data's edges.
🔍 Click to zoom - the calm line vs the degree-12 detonation
LiveThe explosion, explained3 min▶

A degree-12 polynomial has enough freedom to pass near every one of 50 training points - and it uses that freedom by swinging violently between them. Between two nearby points it can dip or spike hundreds of dollars; the training loss never sees those in-between regions, so nothing stops it. Worse, past the edge of the training data a high-degree polynomial's leading term takes over and the curve rockets toward plus or minus infinity - and the test set has points near those edges.

That is why test R² is not merely "bad" but -798,595: R² of 0 means "as good as predicting the mean", negative means worse, and this is six orders of magnitude worse. One test buyer sitting near an edge gets a prediction thousands of dollars wrong, and squared errors do the rest. The overfitting tell from session 1 - train up, test down - at maximum volume.

Self-studyRidge and lasso in one card - the leash for flexible models4 min▶

Regularization adds a penalty on the weights to the loss, so the fit pays for flexibility it does not earn. Ridge (L2, sklearn 1.1.2) penalizes squared weights: everything shrinks toward zero, nothing reaches it - the wild polynomial coefficients get pulled back to sane sizes. Lasso (L1, sklearn 1.1.3) penalizes absolute weights: it drives some coefficients exactly to zero, so it doubles as automatic feature selection. Elastic-net (1.1.5) mixes both. One dial rules them all: alpha - higher means a shorter leash, alpha 0 means plain least squares.

You met the concept in intro-ml b5; the API lives in the sklearn sections above; the intuition (why L1 gives corners that zero things out while L2 gives a circle that only shrinks) is beautifully told in StatQuest's Regularization Parts 1, 2, 2.5 and 3. Tonight's homework has you point Ridge at the degree-6 explosion and watch it get tamed.

Build-along

From one honest line to the explosion ★ 18 min · everyone builds

Four fits, one arc: read a line in dollars, feed it more truth, try to bend it on plenty of data (nothing happens - and that is information), then bend it on scarce data and watch it detonate. Buyers only (n=1,306), split 979 train / 327 test, no stratify - matching b8's convention.

One feature, read aloud. Fit order value on prior spend alone. Print slope and intercept and say the sentence: 44 cents per prior-spend dollar, ~$42 baseline. Confirm R² 0.43, RMSE ~$30 against the $40 mean-baseline floor.

The full model. Add category, channel, and returning via get_dummies(drop_first=True). R² climbs to ~0.59, RMSE falls to ~$25, MAE ~$20. Skim the coefficients - each dummy reads as dollars vs its reference category.

Bend on full data - the shrug. Polynomial degrees 2 and 3 on spend, all 1,306 buyers: test R² stays 0.43. The truth is linear; flexibility has nothing to find. Flat is a finding.

Bend on 80 buyers - the explosion. Subsample 80 buyers (seed 0), 50 train / 30 test, sweep degrees 1 / 2 / 3 / 6 / 12. Watch train R² climb to 0.47 while test R² falls to -798,595.

line_to_explosion.py - the whole session in one scriptimport numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.preprocessing import PolynomialFeatures, StandardScaler from sklearn.pipeline import make_pipeline from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error df = pd.read_parquet("lumen_sessions.parquet") buyers = df[df["converted"] == 1].copy() # n = 1,306 # --- Step 1: one feature, read the line --- X, y = buyers[["prior_30d_spend"]], buyers["order_value"] X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25, random_state=42) lin = LinearRegression().fit(X_tr, y_tr) pred = lin.predict(X_te) print(f"slope {lin.coef_[0]:.2f} intercept ${lin.intercept_:.0f}") print(f"R2 {r2_score(y_te, pred):.2f} RMSE ${mean_squared_error(y_te, pred)**0.5:.0f}") # slope 0.44 intercept $42 # R2 0.43 RMSE $30 (predict-the-mean baseline: RMSE ~$40) # --- Step 2: the full model (b8's four features) --- Xf = pd.get_dummies( buyers[["prior_30d_spend", "product_category", "channel", "new_vs_returning"]], drop_first=True) Xf_tr, Xf_te = Xf.loc[X_tr.index], Xf.loc[X_te.index] full = LinearRegression().fit(Xf_tr, y_tr) pf = full.predict(Xf_te) print(f"R2 {r2_score(y_te, pf):.2f} RMSE ${mean_squared_error(y_te, pf)**0.5:.0f}" f" MAE ${mean_absolute_error(y_te, pf):.0f}") # R2 0.59 RMSE $25 MAE $20 # --- Step 3: poly on ALL buyers - flat on purpose --- for d in [1, 2, 3]: poly = make_pipeline(StandardScaler(), PolynomialFeatures(d), LinearRegression()) poly.fit(X_tr, y_tr) print(f"degree {d} test R2 {r2_score(y_te, poly.predict(X_te)):.2f}") # degree 1 test R2 0.43 # degree 2 test R2 0.43 # degree 3 test R2 0.43 <- the truth is linear; bending adds nothing # --- Step 4: poly on 80 buyers - the explosion --- small = buyers.sample(80, random_state=0) Xs, ys = small[["prior_30d_spend"]], small["order_value"] Xs_tr, Xs_te, ys_tr, ys_te = train_test_split(Xs, ys, test_size=30, random_state=0) for d in [1, 2, 3, 6, 12]: poly = make_pipeline(StandardScaler(), PolynomialFeatures(d), LinearRegression()) poly.fit(Xs_tr, ys_tr) tr, te = r2_score(ys_tr, poly.predict(Xs_tr)), r2_score(ys_te, poly.predict(Xs_te)) print(f"degree {d:2d} train R2 {tr:5.2f} test R2 {te:,.1f}") # degree 1 train R2 0.37 test R2 0.4 # degree 2 train R2 0.37 test R2 0.4 # degree 3 train R2 0.37 test R2 0.3 # degree 6 train R2 0.43 test R2 -26.7 # degree 12 train R2 0.47 test R2 -798,595.0
If your explosion has a different blast radius: the -798,595 depends on exactly which 80 buyers land in the subsample - seed 0 in both the sample and the split reproduces it. Any correct run shows the same shape: degrees 1-2 fine, 6 badly negative, 12 astronomically negative. The shape is the lesson; the exact crater depth is trivia.
Real worldA demand-forecasting team shipped a flexible model tuned on six weeks of data. It fit history beautifully and then priced next month's promotion using an extrapolation past anything it had seen - the degree-12 edge problem wearing a business suit. The postmortem line became team folklore: "the model was never wrong where we could check it, only where it mattered".
Homework

Before session 3 ◐ 25 min

Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · What exactly does least squares minimize?

Raw residuals cancel (a terrible line can sum to zero), so we square first: every miss counts, and big misses count disproportionately. The smooth squared loss is also what makes the closed-form, no-iteration fit possible.

2 · The degree-12 fit scored train R² 0.47 but test R² -798,595. Which failure is this, and why so extreme?

The classic tell: train climbs, test collapses. High-degree polynomials swing violently between points and rocket toward infinity beyond the training range - and squared errors turn a few edge misses into a six-orders-of-magnitude crater.

3 · "Polynomial regression is still linear regression." In what sense?

The features bend; the model does not. PolynomialFeatures widens X, then ordinary least squares runs unchanged - same loss, same closed form. That is why the whole thing lives in sklearn's linear_model module.

Source material

What this session covers

Session 2 covers the linear-regression core of the anchor sources - least squares, reading fits, multiple regression, and the polynomial trap - with regularization handed to a self-study card. Videos, labs, and certificates stay with the official providers.

✓StatQuest Linear Models Part 0 - least squares and the main ideasPart 1
✓StatQuest Linear Models Part 1 - R² and fit qualityPart 1
✓StatQuest Linear Models Part 1.5 - multiple regressionPart 2
◐StatQuest Linear Models Part 3 - design matricesone-hot card in Part 2
✓Ng C1 W2 - multiple features + polynomial regressionParts 2-3 + demo
✓sklearn 1.1.1 - OLS + multicollinearity warningPart 1 self-study card
✓sklearn 1.1.16 - polynomial features via pipelinePart 3 + demo steps 3-4
◐sklearn 1.1.2 / 1.1.3 / 1.1.5 - Ridge, Lasso, Elastic-Netintro-ml b5 concept + self-study card + StatQuest Regularization 1/2/2.5/3
◐Statistical inference on coefficients (p-values, CIs)not covered by design - learn-statistics owns it; sklearn does not compute them anyway

Session 2 cheat sheet · pin this

Least squaresPick the line minimizing the sum of squared residuals. Squaring stops cancellation and punishes big misses; the smooth loss gives a closed-form, no-iteration fit.
Slope in dollarsSlope 0.44 = "44 cents of order value per prior-spend dollar"; intercept ~$42 = the zero-history baseline. Always say the units, never just the number.
The metric trioR² = share of variance explained (not accuracy). RMSE = dollar miss, big errors amplified. MAE = dollar miss, all equal. Skewed target means RMSE above MAE. Full model: 0.59 / $25 / $20.
Design matrixget_dummies(drop_first=True): each dummy coefficient = dollars vs the dropped reference category. Keep all levels + intercept and you get perfect multicollinearity.
The explosion table80 buyers, poly on spend - test R² by degree: 1 and 2 give +0.43, 3 gives +0.34, 6 gives -26.7, 12 gives -798,595. Train R² rose the whole time. Trust the test column.
Ridge / lassoPenalize weights so flexibility is paid for: L2 (ridge) shrinks everything, L1 (lasso) zeroes some out (feature selection), elastic-net mixes. One dial: alpha.