The bias-variance decomposition
What it is
Expected error on unseen data splits into three terms: bias squared (how wrong the average model of this family is), variance (how much the fitted model moves when the training sample changes), and irreducible noise. Capacity lowers bias and raises variance.
Why it matters
It explains why the best model is never the most flexible one available. The two terms move in opposite directions, so total error is U-shaped and the optimum is an interior point you have to locate rather than an extreme you can reach for.
In the real world: Explains why a model is off: too simple to capture seasonality (bias), or chasing noise in last month's data (variance).
Visual example
Model complexityTotal error is lowest where bias and variance trade off, not at either extreme — and the minimum is flat-bottomed.
Total error at three complexities
complexity 2: 6.0 bias + 0.8 var + 0.8 noise = 7.6
complexity 5: 2.3 bias + 2.5 var + 0.8 noise = 5.6 <- minimum
complexity 10: 0.5 bias + 9.0 var + 0.8 noise = 10.3
Why this matters
It is the map every later diagnosis is read against: knowing which side of the minimum you are on decides whether to add capacity or remove it, and those are opposite actions.
Common misconception
Wrong idea: a better model has both lower bias and lower variance.
Correct idea: for a fixed dataset they trade off. Lowering both at once requires changing the problem — more data, better features, a better-suited model family — not turning the complexity dial.
- 1Bias fallsMore capacity lets the family express the true shape.
- 2Variance risesMore capacity also lets it chase this sample's noise.
- 3Their sum turnsTotal error falls, flattens, then rises: minimise the sum.
The total curve is the sum of the three below it. Both ends are equally bad and for opposite reasons, which is why one-directional tuning goes wrong.
import numpy as np
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
rng = np.random.default_rng(0)
preds = []
for _ in range(200): # 200 resamples of the training data
idx = rng.integers(0, len(X_train), len(X_train))
m = make_pipeline(PolynomialFeatures(9), LinearRegression())
m.fit(X_train[idx], y_train[idx])
preds.append(m.predict(X_probe))
P = np.array(preds)
bias2 = np.mean((P.mean(axis=0) - y_probe) ** 2) # average model, averaged error
variance = np.mean(P.var(axis=0)) # spread across resamplesTreating bias and variance as qualities you can measure on one fitted model. Variance is defined across training samples, so estimating it needs resampling — bootstrap or repeated cross-validation. From a single fit you can only observe the symptoms.