Before we dive into the concept of bias, let’s briefly recap some theoretical concepts you learned about in the lecture. When we talk about fitting machine learning models, we are referring to the process of estimating a function that best represents the relationship between an outcome and a set of labelled data (in supervised learning) or to uncover structural patterns in unlabelled data (in unsupervised learning). While the estimated function conveys important information about the data from which it was derived (the training data), our primary interest is in using this function to make accurate predictions for future cases in new, unseen data sets.
The fundamental question in statistical learning is how well will perform on these future data sets, which brings us to the concept of the bias-variance tradeoff. Bias occurs when a model is too simple to capture the underlying complexities of the data, leading to systematic inaccuracies in its predictions. Variance measures how much the model’s predictions fluctuate when trained on different subsets of the data.
This closely relates to the example introduced in 🔁 Recap: Regression Models. Let’s have another look and simulate some data with an underlying relationship in line with a cubic polynomial function. We can see that a linear regression does not capture the nuance of the cubic relationship in the data, while a 10th order model already overfits quite a lot:
import numpy as np
x = np.linspace(-3, 3, 30)
y = (x**3 + np.random.normal(0, 15, size=x.shape)) / 10
If we look at the mean squared error (MSE) on the training data, we can see that it decreases with increasing model flexibility:
Reminder: MSE
is the actual value for the -th observation
is the predicted value for the -th observation
is the total number of observations
The term represents the squared error for each observation. By averaging these squared errors, the MSE provides a single metric that quantifies how far off the predictions are from the true values.
import statsmodels.api as sm
from sklearn.preprocessing import PolynomialFeatures
# Create data
np.random.seed(42)
x = np.linspace(-3, 3, 30).reshape(-1, 1)
y = (x**3 + np.random.normal(0, 15, size=x.shape)) / 10
# Run the models and calculate the MSE
mse_list = []
model_list = []
degrees = [1, 3, 10]
for degree in degrees:
x_trans = PolynomialFeatures(degree=degree).fit_transform(x)
model = sm.OLS(y, x_trans).fit()
mse = np.mean(model.resid**2)
model_list.append(model)
mse_list.append(mse)
print("Degree Train MSE")
for degree, mse in zip(degrees, mse_list):
print(f"{degree:<7} {mse:.3f}")Degree Train MSE
1 1.809
3 1.417
10 1.094
However, if we evaluate the same models on new, unseen data, we see that the MSE is now increasing with increasing order of the polynomial regression model:

Degree Test MSE
1 2.203
3 2.046
10 2.478
Please compare the previous plots and outputs. What do you notice?
Show answer
Two things should become apparent:
In contrast to the training MSE, which decreases with the order of the model, the test MSE is lowest for the 3rd order model.
The test MSEs are generally higher than the training MSEs. This is to be expected, as the initial models did all, to some degree, fit to the noise in the training data.
This is because the 10th order model has too much variance — it is too close to the training data. If we fit such a model to multiple draws of samples from the population with a true association consistent with the cubic order polynomial, the model’s predictive performance will always look different:


Figure 1:The bias-variance tradeoff.
When we increase the flexibility of the model by adding more parameters, we are effectively trading between bias and variance. Initially, as the model becomes more flexible, its bias decreases quickly because it can capture more complex patterns in the data. However, this increased flexibility also makes the model more sensitive to the noise in the training data, which leads to a rise in variance.
Eventually, the reduction in bias is no longer sufficient to counterbalance the increase in variance. This is why a model with a very low training MSE may still suffer from a high test MSE: the low training error is primarily a result of fitting the noise (i.e. high variance), rather than capturing a true underlying pattern.
Decomposing the error yourself¶
The figure above is usually drawn on a whiteboard and taken on faith. Because we simulated the data, we actually know the true function, so we can measure bias and variance instead of asserting them.
The recipe is simple: draw many training sets from the same population, fit a model of a given degree to each of them, and then look at the predictions at a fixed test point :
Bias² is how far the average prediction is from the truth:
Variance is how much the individual predictions scatter around that average:
Irreducible error is the noise variance , which no model can remove
Their sum is the expected test MSE:
Source
import numpy as np
rng = np.random.default_rng(0)
# The true function and the noise level we simulate from
f_true = lambda x: x**3 / 10
sigma = 1.5
n_train = 30
n_sims = 400
degrees = np.arange(1, 13)
x_grid = np.linspace(-3, 3, 60) # test points
f_grid = f_true(x_grid)
bias2, variance = [], []
for degree in degrees:
# Each row holds the predictions of one model, fitted to one simulated dataset
preds = np.empty((n_sims, x_grid.size))
for s in range(n_sims):
x_s = np.linspace(-3, 3, n_train)
y_s = f_true(x_s) + rng.normal(0, sigma, size=n_train)
preds[s] = np.polyval(np.polyfit(x_s, y_s, degree), x_grid)
mean_pred = preds.mean(axis=0)
bias2.append(np.mean((mean_pred - f_grid) ** 2))
variance.append(np.mean(preds.var(axis=0)))
bias2 = np.array(bias2)
variance = np.array(variance)
total = bias2 + variance + sigma**2
for d, b, v, t in zip(degrees, bias2, variance, total):
print(f"degree {d:>2} bias² = {b:6.3f} variance = {v:7.3f} expected test MSE = {t:7.3f}")degree 1 bias² = 0.184 variance = 0.151 expected test MSE = 2.585
degree 2 bias² = 0.186 variance = 0.192 expected test MSE = 2.628
degree 3 bias² = 0.000 variance = 0.291 expected test MSE = 2.541
degree 4 bias² = 0.002 variance = 0.360 expected test MSE = 2.611
degree 5 bias² = 0.001 variance = 0.408 expected test MSE = 2.659
degree 6 bias² = 0.001 variance = 0.509 expected test MSE = 2.761
degree 7 bias² = 0.001 variance = 0.558 expected test MSE = 2.809
degree 8 bias² = 0.002 variance = 0.632 expected test MSE = 2.884
degree 9 bias² = 0.001 variance = 0.695 expected test MSE = 2.946
degree 10 bias² = 0.004 variance = 0.784 expected test MSE = 3.037
degree 11 bias² = 0.003 variance = 0.884 expected test MSE = 3.136
degree 12 bias² = 0.002 variance = 0.955 expected test MSE = 3.207

Notice that the measured curve has exactly the shape of the schematic figure: bias² collapses as soon as the model is flexible enough to represent a cubic, variance grows steadily, and their sum has a minimum at degree 3 — the degree we actually simulated from. The expected test MSE never drops below , no matter how good the model gets.