learn-intro-ml-with-phoebe / Builder session 4 of 10
Learn Intro to Machine Learning with Phoebe · Builder session 4 of 10

Over- and under-fitting, the single most important picture in ML

If you learn one shape this whole course, make it this one: as a model gets more complex, its training error keeps falling forever - but its test error falls, bottoms out, then rises again. That U is the difference between a model that generalises and one that has merely memorised. Today you draw it by hand: fit decision trees of rising depth on Lumen order value and watch the gap between train and test open up. There is a live tool below so you can turn the depth dial yourself.

🟡 Building Builders · DA / DE / DS Python · scikit-learn 45 min
0-2 · The U 2-16 · The curve by hand 16-38 · Build it in Python 38-45 · Wrap
Part 0

Low training error is not the goal - low test error is

A beginner's instinct is to push training error as low as possible. But a model that fits the training data perfectly has usually just memorised its noise, and it falls apart on new data. The honest question is never "how well does it fit what it has seen?" - it is "how well does it do on what it has not?" The gap between those two answers is overfitting, and today you will watch it grow, live, one tree-depth at a time.

Live - built in session Self-study - run after ★ The one big idea The case: Lumen Skincare
★ What you walk out with today The U-shaped test-error curve, drawn from your own Lumen run: a decision tree at depth 1 underfits (train R² 0.31, test 0.22), depth 3 hits the sweet spot (train 0.46, test 0.36 - the peak), and by depth 15 the tree badly overfits - train R² soars to 0.84 while test R² goes negative, worse than just predicting the mean. The gap between the two lines is the whole diagnosis.
Part 1 · the shape of the work

The U-curve, by hand 7 min live

We fit a DecisionTreeRegressor on Lumen order_value at rising max_depth and record two scores each time: train R² and test R². Train error keeps falling as depth rises - a deeper tree can always carve the training data finer. Test error is the honest one: it falls while the model is learning real signal, then rises once the model starts memorising noise. Where test error bottoms out is the sweet spot. This is the exact experiment in the live tool below - turn the dial and watch it happen.

error model complexity (tree depth) → underfit sweet spot overfit min test error = pick this depth test error (held-out) train error (always falls) high bias high variance The gap between the two lines is overfitting. Train error alone can't warn you - only the held-out test line bends back up.
🔍 Click to zoom - train error always falls; test error is a U
LiveReading the gap3 min▶

Diagnose a model from just two numbers - train score and test score:

  • Both low (train 0.31, test 0.22 at depth 1) - underfitting. The model is too simple to capture the signal; add complexity.
  • Both decent and close (train 0.46, test 0.36 at depth 3) - the sweet spot. This generalises.
  • Train high, test low or negative (train 0.84, test negative at depth 15) - overfitting. The model memorised the training rows; the gap is the tell.
Why this matters You cannot see overfitting by looking at training error - it just keeps dropping and looks like progress. Only the held-out test score bends back up to warn you. That is why b2's split was sacred: without it, this entire diagnosis is invisible.
Self-studyBias vs variance3 min▶

The U-curve has a name for each side. A too-simple model has high bias: it makes systematic errors because it cannot represent the real pattern (the underfit, left side). A too-complex model has high variance: it swings wildly to fit whatever noise happens to be in the training sample (the overfit, right side).

HIGH BIAS - UNDERFIT misses the real pattern in the data systematic errors, not noise train 0.31, test 0.22 (depth 1) HIGH VARIANCE - OVERFIT chases noise in the training rows swings wildly on new data train 0.84, test negative (depth 15) Neither extreme is right - the sweet spot sits between them: depth 3, test R² 0.36.
🔍 Click to zoom - bias and variance are two failure modes, not two choices

You cannot drive both to zero - lowering one tends to raise the other. The goal is not perfection on either side but the minimum of their sum, which is exactly where the test-error U bottoms out. The formal treatment and how regularization lets you tune this trade-off deliberately is the b5 story; today you just need to feel the shape.

★ everyone builds · draw the U

Fit rising depths and print the U-curve 22 min · everyone builds

We reload Lumen, take the converters for an order_value regression, split, then loop tree depth and record train and test R². Your table must match the course canon - it was fit with a real DecisionTreeRegressor, seed 42.

Reload and set up the regression. Order value only exists for buyers, so we regress on the converter subset - one feature, so the depth effect is clean and visible.

setup.py
import numpy as np, pandas as pd
from sklearn.model_selection import train_test_split

df = pd.read_parquet("lumen_sessions.parquet")

buyers = df[df.converted == 1]          # order_value is meaningful for buyers
X = buyers[["prior_30d_spend"]]
y = buyers["order_value"]

X_tr, X_te, y_tr, y_te = train_test_split(
    X, y, test_size=0.25, random_state=42)

Loop the depth, record both scores. Fit a fresh tree at each depth and keep the train and test R².

ucurve.py
from sklearn.tree import DecisionTreeRegressor

rows = []
for depth in [1, 2, 3, 5, 8, 15]:
    tree = DecisionTreeRegressor(max_depth=depth, random_state=0).fit(X_tr, y_tr)
    rows.append((depth,
                 round(tree.score(X_tr, y_tr), 2),    # train R^2
                 round(tree.score(X_te, y_te), 2)))   # test R^2
for d, tr, te in rows:
    print("depth %2d | train R2 %5.2f | test R2 %5.2f" % (d, tr, te))

Read the table - it must match canon. This is the U in numbers.

The U-curve in your own numbers: depth 1, 3, 15 train R² test R² DEPTH 1: UNDERFIT 0.31 0.22 underfit DEPTH 3: PEAK FIT 0.46 0.36 sweet spot DEPTH 15: OVERFIT 0.84 negative overfit Test R² peaks at depth 3 (0.36); by depth 15 it turns negative - worse than the mean baseline.
🔍 Click to zoom - test R² is the only score that tells you when to stop adding depth
max_depthTrain R²Test R²Diagnosis
10.310.22underfit - too simple, both low
20.400.31improving
30.460.36sweet spot - test R² peaks
50.510.29starting to overfit
80.620.15overfit - gap widening
150.84negativebadly overfit - worse than the mean

Train R² climbs the whole way to 0.84. Test R² peaks at depth 3, then falls, and by depth 15 it is negative - the deep tree has memorised the training rows so thoroughly that on new data it does worse than just predicting the average order value.

Plot both curves and pick depth 3. Seeing the U beats reading it. This draws the exact picture from Part 1 on your own numbers.

plot.py
import matplotlib.pyplot as plt

depths = [r[0] for r in rows]
plt.plot(depths, [r[1] for r in rows], marker="o", label="train R2")
plt.plot(depths, [r[2] for r in rows], marker="o", label="test R2")
plt.axvline(3, linestyle="--")     # the sweet spot
plt.xlabel("max_depth"); plt.ylabel("R2"); plt.legend(); plt.show()
Real world

A pricing team crowned a deep gradient-boosted model because it "won" on last month's data - it explained almost every historical order. The next month it lost badly to a far simpler model. It had not learned how customers behave; it had memorised last month's specific rows, right down to the noise. The depth-15 negative test R² above is that story in miniature - and the reason you always pick complexity by the held-out score, never the training one.

Before Session 5

This week ◐ 40 min total

Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · What is the signature of overfitting?

Overfitting means the model has learned the training rows (including noise) but fails to generalise: train R² keeps rising while test R² falls. The gap between the two is the tell - at depth 15, train 0.84 versus a negative test R².

2 · At depth 1 the tree scores train R² 0.31 and test R² 0.22 - both low. What is that?

When both scores are low, the model is too simple to represent the real pattern - high bias, underfitting. The fix is more complexity (more depth), which moves you toward the sweet spot at depth 3.

3 · How do you honestly pick the tree depth?

Training R² always favours the deepest tree, so it is useless for choosing. You pick by performance on data the model did not train on - the test set, or better still cross-validation (b6). Here the test R² peaks at depth 3.

Source material

What this session covers

This builder session draws the U-shaped test-error curve on Lumen order value and teaches the train/test gap as the overfitting diagnosis. The formal bias-variance decomposition and regularization as the tuning lever come in b5.

✓Over/underfitting, the train/test gap, the U-shaped test curve, picking depth by test score; the live fit toolPart 1 + demo
◐scikit-learn "Underfitting vs. Overfitting" / model generalizationconcept spine
◐Bias-variance formalized + regularization as the deliberate tuning leverBuilder Session 5

Builder Session 4 cheat sheet · pin this

The U-curveAs complexity rises, train error always falls but test error falls then rises. The bottom of the U is the sweet spot.
The gap = overfittingTrain high, test low/negative. Only the held-out score bends back up to warn you - training error never will.
UnderfittingBoth scores low (depth 1: train 0.31, test 0.22). Too simple - add complexity.
Sweet spotDepth 3: train 0.46, test 0.36 (peak). Test R² collapses to negative by depth 15 - worse than the mean.
Bias vs varianceSimple = high bias (underfit); complex = high variance (overfit). Minimise the sum, not either alone.
Pick honestlyChoose depth by the test / cross-validated score, never the training score - training always favours the deepest tree.