Contents
, where with , , and the new noise is independent of the training set .
Deep Breakdown
What it tells me (Situation Awareness): For squared-error loss, the expected test error of a learning procedure at a point splits exactly into three non-negative pieces: squared bias (how far the average model is from the truth), variance (how much the model moves between training sets), and irreducible noise (randomness in that no model can predict). Making a model more flexible typically lowers bias and raises variance; regularising or simplifying does the opposite. The best model is not the one with the smallest bias or the smallest variance, but the one where their sum is smallest — the bottom of the U-shaped test-error curve. The noise floor sets a hard lower bound on achievable MSE. In DA/DS: choosing polynomial degree, tree depth, in k-NN, ridge , number of boosting rounds; explaining why validation error goes up when I add capacity; deciding between "get more data" (fights variance) and "get better features / a richer model" (fights bias); and setting realistic targets (no forecast of daily demand can beat the noise floor). Modern over-parameterised models (deep nets, "double descent") complicate the U-shape, but the decomposition itself still holds.
Flowchart
Real-world use
Where it is used
Hyperparameter tuning of every model with a complexity knob (degree, depth, , , dropout, number of trees or boosting rounds); deciding whether to invest in more data or better features; ensembles (bagging attacks variance, boosting attacks bias); shrinkage estimators in statistics (James–Stein, empirical Bayes) that deliberately accept bias; demand forecasting and pricing models where the noise floor determines what is achievable.
Problem it solves
A test error number alone does not tell me what to do next. The decomposition separates error into causes with different cures: bias needs more flexibility or features, variance needs more data, regularisation or averaging, and noise cannot be reduced by modelling at all (only by measuring better or changing the target).
How it solves the problem
Write , square, and take expectations: all cross terms vanish because and has mean zero and is independent of . In practice the terms are estimated by simulation on synthetic data, or diagnosed via learning curves and the train/validation gap on real data.
Example in a business
A retailer forecasts daily store sales. A linear model on calendar features has validation MSE close to its training MSE, both high: bias-dominated, so the team adds holiday and promotion features. A gradient-boosted model with 2,000 deep trees then shows training MSE near zero but validation MSE worse than the simpler model: variance-dominated, so they reduce depth, add early stopping and subsample rows. The final model sits near the bottom of the U, and its remaining error is close to the day-to-day noise in footfall.
The formula, piece by piece
- : a new observed outcome = true signal + noise
- : prediction from a model trained on a random training set
- : expected squared prediction error at , averaging over both training sets and new noise
- : squared gap between the average model and the truth
- : spread of the model around its own average
- : variance of the noise; the floor no model can go below
- Averaging over (the test distribution) gives the same split for overall expected test MSE
- Exactness: the identity is exact for squared loss; for 0–1 loss or log-loss there are analogous but less clean decompositions
The formula in words
"Expected squared test error = (how wrong the model is on average)² + (how much it jumps around between training sets) + (how noisy the target is anyway)." Tiny example: three equally likely training sets give predictions at a store where the true expected revenue is , and daily noise has variance . Bias² , variance , noise , so expected MSE .
Critical Concepts
- Only for squared error (cleanly): the three-term decomposition is exact for MSE. Classification losses have their own, messier versions.
- Trade-off, not law: more flexibility usually lowers bias and raises variance, but not always monotonically. With very little capacity, misfit can act like extra noise and inflate variance too; heavily over-parameterised models can show "double descent".
- Noise is a floor: if the validation MSE is already near , no model change will help much; only better measurements or new informative features (which change what counts as noise) will.
- Different cures: bias → richer model, more features, less regularisation, boosting. Variance → more data, regularisation, feature selection, bagging, early stopping.
- Cannot be computed directly on real data: and are unknown. Use cross-validation for the total, and learning curves or bootstrap refits as proxies for the parts.
- Connection to generalisation gap: high variance shows up as overfitting (large gap between test and train error); high bias as underfitting (both errors high).
Step-by-step calculation (tiny example)
Predictions at from three equally likely training sets: . Truth . Noise .
Step 1: Average prediction .
Step 2: Bias² .
Step 3: Variance .
Step 4: Add the noise .
Step 5: Verify directly Squared distance from the truth for each model: , , ; average . Adding gives .
Code examples
Beginner example (Python)
import numpy as np
rng = np.random.default_rng(0)
# Tiny example: 3 equally likely training sets -> 3 predictions at x0
preds = np.array([9.0, 11.0, 13.0]) # f_hat(x0) for each training set ($k)
f_x0 = 12.0 # true expected revenue at x0
sigma2 = 1.0 # irreducible noise variance
bias2 = (preds.mean() - f_x0) ** 2
var = preds.var(ddof=0)
print(f"Bias^2 = {bias2:.3f}, Variance = {var:.3f}, Noise = {sigma2:.3f}")
print(f"Sum (theory) = {bias2 + var + sigma2:.3f}")
# Brute-force check: pick a training set at random, draw a new noisy y, square the error
n_sim = 1_000_000
pick = rng.integers(0, 3, n_sim)
y_new = f_x0 + rng.normal(0, np.sqrt(sigma2), n_sim)
mse = np.mean((y_new - preds[pick]) ** 2)
print(f"Simulated E[(y - f_hat)^2] = {mse:.3f}")Beginner example (R)
# Tiny example: bias^2 + variance + noise
preds <- c(9, 11, 13); f_x0 <- 12; sigma2 <- 1
bias2 <- (mean(preds) - f_x0)^2
variance <- mean((preds - mean(preds))^2)
cat(sprintf("Bias^2 = %.3f, Var = %.3f, Noise = %.3f, Total = %.3f\n",
bias2, variance, sigma2, bias2 + variance + sigma2))
# Brute-force check
set.seed(0)
n_sim <- 200000
pick <- sample(1:3, n_sim, replace = TRUE)
y_new <- f_x0 + rnorm(n_sim, sd = sqrt(sigma2))
cat(sprintf("Simulated MSE = %.3f\n", mean((y_new - preds[pick])^2)))Professional example (Python)
import numpy as np
rng = np.random.default_rng(0)
def true_f(x: np.ndarray) -> np.ndarray:
"""Seasonal demand curve over one year (x in [0, 1])."""
return np.sin(2 * np.pi * x)
def decompose(deg: int, n_train: int = 30, n_sets: int = 500, sigma: float = 0.3):
"""Monte Carlo bias-variance decomposition of polynomial regression of a given degree.
Returns (bias^2, variance, noise, sum, empirical expected test MSE), each averaged
over a grid of test inputs.
"""
x_test = np.linspace(0.05, 0.95, 50)
preds = np.empty((n_sets, x_test.size))
for r in range(n_sets):
x = rng.uniform(0, 1, n_train)
y = true_f(x) + rng.normal(0, sigma, n_train)
preds[r] = np.polyval(np.polyfit(x, y, deg), x_test)
mean_pred = preds.mean(axis=0)
bias2 = np.mean((mean_pred - true_f(x_test)) ** 2)
variance = np.mean(preds.var(axis=0))
noise = sigma ** 2
# Empirical check: fresh noisy targets for each (training set, test point)
y_test = true_f(x_test) + rng.normal(0, sigma, preds.shape)
mse = np.mean((preds - y_test) ** 2)
return bias2, variance, noise, bias2 + variance + noise, mse
print(f"{'degree':>6}{'Bias^2':>9}{'Var':>9}{'Noise':>8}{'Sum':>9}{'Test MSE':>10}")
for deg in [0, 1, 3, 5, 9]:
b2, v, s2, total, mse = decompose(deg)
print(f"{deg:>6}{b2:>9.4f}{v:>9.4f}{s2:>8.2f}{total:>9.4f}{mse:>10.4f}")
print("\nBias^2 falls with degree, variance rises; the sum is minimised at degree 3.")Reading the table: a constant (degree 0) and a line (degree 1) are bias-dominated (bias² about and ). The cubic hits the sweet spot: bias² about , variance about , total about , just above the noise floor . Degree 9 on 30 points has almost no bias but variance about , giving a test MSE about 27 times worse than the cubic. In every row the Monte Carlo test MSE matches Bias² + Variance + Noise to within simulation error. The constant model's variance () is higher than the cubic's because its misfit behaves like extra noise in each fit — the trade-off is a tendency, not a strict law.
Source: domain knowledge