The Story: Vikram Needs to Predict Home Prices
He tries three approaches: a simple straight line, a highly complex wiggly curve, and a smooth balanced curve. Each produces wildly different results on unseen homes.
His manager asks one question: "Which model do I trust?" The answer requires understanding Bias and Variance — not as abstract terms, but as numbers you can measure from your train and test errors.
The Data — Home Price vs Square Footage
Vikram plots all 21 homes. As square footage rises, price rises — but not perfectly. Real data always has noise. This is the dataset every model will be trained and tested on.
21 homes plotted. Clear upward trend with scatter — price generally rises with size, but with noise from other factors (location, age, condition).
The Train / Test Split — The Foundation of All Evaluation
Before training any model, Vikram splits his 21 homes into two groups. Blue dots are the training set — the model learns from these. Orange dots are the test set — hidden during training, used only to measure real-world performance. This split is the key to revealing bias and variance.
The model never sees orange dots while training. Test error measures how well the model generalises. Train error measures how well it fits what it was shown.
The Complex Model — "The Memoriser" (Overfitting)
Vikram's first attempt is an extremely complex curve — high-degree polynomial that snakes through every single training point. It memorises the training data perfectly. Train error = 0.
The wiggly green curve snakes through every blue training dot — Train Error = 0. But the orange test points are far from the curve. Test Error = 100. The model memorised training noise instead of learning the real pattern.
Proving High Variance — The Same Model, a Different Training Set
Variance means: how much does the model's performance change when you train it on a different sample of the same data? Vikram shuffles his data and creates a second train/test split — different blue dots, different orange dots. He trains the same type of complex wiggly model again.
Same model type — different training set. Left: Test Error = 100. Right: Test Error = 27. The 73-point swing IS high variance. The model's test performance is unpredictable — it fully depends on which homes happened to be in the training set.
The complex wiggly curve memorised every training point — including the random noise specific to that sample. When the training set changes, the wiggles change too. The model learned the noise, not the underlying price pattern. Every new training set produces a completely different wiggly curve, so test errors vary wildly. That instability across datasets is the definition of high variance.
🎬 Watch It Live — The Variance Dance of the Complex Model
Two static snapshots are one thing — but variance is a behaviour, not a photograph. Watch the same complex model type retrained on four different training samples of the same underlying data. Notice how the green curve twists into a wildly different shape each time, even though the true trend (behind the noise) never changed.
The dotted grey line is the true underlying trend. The green curve should be tracking it — instead it thrashes around chasing noise. This is what "unstable" means in machine learning: the model's shape depends on random luck in the training sample, not on the real pattern.
Four training samples drawn from the same population produced four completely different green curves. If Vikram deployed the model built on Sample A, he'd get a system that behaves nothing like the one built on Sample B — even though both were trained on "the same kind of data". High variance means unpredictable behaviour in production. No manager will sign off on a model whose predictions change based on which random rows landed in this quarter's training refresh.
The Simple Model — "The Oversimplifier" (Underfitting)
Vikram's second attempt is the simplest possible model: a straight line. It cannot capture the slight upward curve in the data. Even on the training set, it makes consistent errors because a line simply cannot fit a curve.
The straight green line misses the curve of the data — it cannot bend. Both the blue training dots and the orange test dots are scattered far from the line. Train Error = 43, Test Error = 47. The model fails on data it was trained on — that is high bias.
Proving High Bias + Low Variance — The Linear Model on Two Splits
Split 1: Test = 47. Split 2: Test = 37. A small 10-point difference — LOW VARIANCE. The line is consistent because a straight line can't memorise training noise. BUT both train errors are high (43 and 41) — HIGH BIAS. The model is consistently wrong in the same way.
A straight line has only two parameters: slope and intercept. No matter which 14 homes Vikram uses to train it, the line will always be drawn roughly the same way through the general cloud of data — consistent but wrong. The line cannot bend to fit the curvature in the data, so it makes the same type of systematic error on every training set. That systematic wrongness is bias. The consistency across training sets is low variance.
🎬 Watch It Live — The Stubborn Line of the Simple Model
Now watch the opposite behaviour. Same four training samples, same underlying data — but this time Vikram fits a straight line. Notice how the line barely twitches between samples. It is stable, predictable... and wrong in exactly the same way every single time.
Contrast this with the variance dance in Section 6. The line is stable — its slope and intercept are almost identical across all four samples. But it never touches the true (curved) trend. It is a reliable, repeatable failure. High bias, low variance.
Underfitting looks calm. There are no dramatic swings, no wild curves, no test-error surprises. Everything is stable — and everything is wrong. This is why underfit models often survive in production far too long: they don't crash, they don't spike, they just quietly deliver mediocre predictions forever. A model with train error of 43 and test error of 47 might look "consistent" — but consistency without accuracy is just repeatable failure.
The Balanced Fit — "The Generalist" (The Sweet Spot)
Vikram's third model is a smooth, moderately curved function — complex enough to follow the general upward curve but not complex enough to memorise every noise fluctuation. This is the balanced fit.
The smooth curve follows the upward trend without memorising noise. Train Error = 10, Test Error = 12 — the gap is tiny. Both errors are low. This is the balanced fit.
Split 1: Test = 12. Split 2: Test = 15. Only 3-point difference — LOW VARIANCE. Train errors are 10 and 11 — LOW BIAS. This is the goal: a model that generalises reliably regardless of which homes it was trained on.
🎮 The Complexity Dial — Press "Next" to Step Through Underfit → Balanced → Overfit
This time you control the model complexity. Press the Next button below to move through five stages, one at a time, watching exactly how the fitted green curve, the train/test errors, and the bias-variance verdict change with each turn of the dial.
Press Next to step through five stages of increasing model complexity. Watch the green curve, the two error bars, and the verdict banner update on every click. Notice how train error keeps falling — but test error only falls partway, then explodes.
The Complete Picture — All Three Models Side by Side
| Model | Split 1 Train | Split 1 Test | Split 2 Train | Split 2 Test | Bias | Variance | Verdict |
|---|---|---|---|---|---|---|---|
| Wiggly Curve (Overfit) | 0 | 100 | 0 | 27 | Low | High | Deploy? Never |
| Straight Line (Underfit) | 43 | 47 | 41 | 37 | High | Low | Consistent but wrong |
| Smooth Curve (Balanced) | 10 | 12 | 11 | 15 | Low | Low | ✓ Production-ready |
Low train error (0–15) → model fits the training data → Low Bias.
High train error (40+) → model can't even fit the training data → High Bias (Underfitting).
Small gap, consistent across splits → Low Variance.
Large gap OR wildly different test errors across splits → High Variance (Overfitting).
This means the model learned the real pattern — not the noise, and not an oversimplified approximation. Find this sweet spot by tuning model complexity.
Why — The Mechanism Behind Each Behaviour
- Few parameters (2 for a line)
- Cannot capture non-linear patterns
- Makes same error regardless of which data it trains on
- Train error is high — "can't even fit the training data"
- Test error matches train error — stable but wrong
- Result: High Bias · Low Variance
- Many parameters (e.g. degree-10 polynomial has 11)
- Can fit any shape — including random noise
- Memorises the specific training set completely
- Train error = 0 — "memorised every data point"
- Test error explodes — real-world patterns weren't learned
- Result: Low Bias · High Variance
- Right number of parameters for the data pattern
- Captures the real trend without memorising noise
- Robust to which specific points are in the training set
- Train error is low but not zero — accepts some noise
- Test error is close to train error — real generalisation
- Result: Low Bias · Low Variance
🎬 The Bias-Variance Dartboard — Every Model in Four Quadrants
Every trained machine-learning model can be summarised by a dart pattern on a target. The bullseye is the true answer. Each dart is one model trained on one training set. Where the darts land tells you everything about bias and variance. Watch the darts hit live below.
Each dart is one model trained on one random sample of data. The bullseye is the true prediction. Bias = distance from bullseye to the dart cluster's centre. Variance = spread of the darts around their own centre. The top-left target is the goal.
Top-right (low bias, high variance) = Vikram's wiggly overfit curve. The darts centre near the bullseye (on average, the model is right) but scatter wildly.
Bottom-left (high bias, low variance) = Vikram's straight line. The darts cluster tightly, but far from the bullseye — reliably wrong.
Top-left (low bias, low variance) = Vikram's balanced curve. Tight cluster, hits the bullseye. Ship this one.
Bottom-right = A model that is both too simple in the wrong way AND unstable — e.g. a decision tree trained with almost no data. Fix everything.
🎬 The U-Shape — Total Error vs Model Complexity
If you plot bias, variance, and total error against model complexity, you get the single most important graph in machine learning. Bias falls as complexity rises. Variance climbs. Total error dips into a U-shape — and the bottom of that U is the sweet spot every practitioner is chasing. Watch a marker travel along the complexity axis to see where each of Vikram's models lands.
The pulsing green ring is the sweet spot — where total error bottoms out. Vikram's straight line sits far to the left with high bias-driven error. His wiggly curve sits far to the right with high variance-driven error. His smooth balanced curve lands right at the sweet spot. This is the single graph every ML practitioner must internalise.
Look at the red and purple curves. As you move right (more complexity), bias goes down and variance goes up. You cannot reduce both simultaneously by changing complexity alone — improving one costs the other. That is the bias-variance tradeoff. The only way out of this tradeoff is to change the game: get more data (shrinks variance without hurting bias), reduce noise in the data (shrinks both), or use techniques like regularisation, ensembling, or cross-validation that specifically target one side without wrecking the other.
Python — Reproducing Vikram's Three Models
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split, ShuffleSplit
from sklearn.metrics import mean_squared_error
np.random.seed(42)
# ── Generate home price data (quadratic + noise) ─────────────
sqft = np.random.uniform(600, 2500, 80)
price = 0.00004 * sqft ** 2 + 0.05 * sqft + np.random.randn(80) * 12
X = sqft.reshape(-1, 1)
y = price
# ── Three model pipelines ─────────────────────────────────────
models = {
'Underfit (degree=1, linear)' : Pipeline([
('poly', PolynomialFeatures(1)),
('model', LinearRegression()),
]),
'Balanced (degree=3)' : Pipeline([
('poly', PolynomialFeatures(3)),
('model', LinearRegression()),
]),
'Overfit (degree=15, complex)' : Pipeline([
('poly', PolynomialFeatures(15)),
('model', LinearRegression()),
]),
}
# ── Run 2 different train/test splits (like the PDF slides) ──
splits = [42, 99] # two different random seeds = two different training sets
print(f"{'Model':<32} {'Split':>6} {'Train MSE':>10} {'Test MSE':>10} {'Gap':>8}")
print("-" * 72)
for name, model in models.items():
test_errors = []
for i, seed in enumerate(splits, 1):
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.25, random_state=seed
)
model.fit(X_tr, y_tr)
tr_mse = mean_squared_error(y_tr, model.predict(X_tr))
te_mse = mean_squared_error(y_te, model.predict(X_te))
test_errors.append(te_mse)
print(f"{name:<32} {'Split '+str(i):>6} {tr_mse:>10.1f} {te_mse:>10.1f} {te_mse-tr_mse:>8.1f}")
variance_signal = abs(test_errors[1] - test_errors[0])
print(f"{'':32} {'Variance (test diff):':>18} {variance_signal:>9.1f}")
print()
Underfit: Train MSE is high (86, 83) — High Bias. Test difference is tiny (3.4) — Low Variance.
Overfit: Train MSE is near zero — Low Bias. Test difference is massive (630!) — High Variance.
Balanced: Train MSE is low (18, 17) — Low Bias. Test difference is tiny (3.3) — Low Variance.
This is the model to deploy.
Golden Rules
cross_val_score) and look at the standard deviation of the scores.
A high standard deviation across folds = high variance = your model is
unstable and cannot be trusted.