Index Ai Academy
Phase 3 · Machine learning · Evaluation

Bias and variance

The two ways a model can be wrong.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

When a model disappoints, your next move depends on why it's wrong. If it's too simple, more data won't help. If it's too flexible, a bigger model makes things worse. Bias and variance are the names for these two failures. A chart called a learning curve tells you which one you have. Then you can pick a fix that works instead of guessing. That holds for a churn model and for a fine-tuned LLM alike.

What you'll be able to do

  • Define bias and variance as two separate sources of prediction error
  • Measure both by refitting a model on many training samples
  • Read a learning curve to tell a high-bias model from a high-variance one
  • Match each fix (more data, regularization, ensembles, bigger models) to the error it reduces

What it covers

  1. Two ways to miss
  2. Splitting the error into three parts
  3. Seeing it: refit two hundred times
  4. Learning curves: diagnosing a real model
  5. The knobs you already know, by what they trade
  6. Worked example: which problem does each model have?

Two ways to miss

Picture two darts players. The first throws a tight little cluster, but it always lands low and to the left of the bullseye. The second player's darts land all around the bullseye. The middle of the spread is right on target, but the darts are scattered wide.

Both players miss. They miss for different reasons, so they need different coaching. The first has a steady aim pointed at the wrong spot. The second aims at the right spot but is shaky.

Models miss in the same two ways. Imagine you could collect a fresh set of training data, train the model again, and repeat that a hundred times. Each run gives a slightly different model, because each one saw a different sample of customers. Now ask all hundred models to make a prediction for the same customer.

  • Bias is how far the average prediction is from the truth. That's the first darts player. A model that's too simple misses in the same direction every time. A straight line can't follow a curve, however much data you give it.
  • Variance is how much the predictions scatter from one training set to the next. That's the second player. A model that's too flexible chases the noise in whichever sample it got. Noise means random quirks of those particular customers that won't repeat. So each run looks different.

Watch out: "bias" here is a technical word for a steady miss in one direction. It's not the same thing as social or demographic bias in AI, which is about unfair treatment of groups of people.

Quick check
You retrain a model on 100 fresh samples. Its predictions for one customer all land between 41 and 43. The customer's true value is 60. What's the problem?
  1. High variance: the predictions scatter too much
  2. Both high bias and high variance
  3. Neither: a spread of 2 is tiny
  4. High bias: the predictions agree but miss in the same direction
Show the answer

High bias: the predictions agree but miss in the same direction

The predictions agree closely, so variance is low. Their average is far below 60, so bias is high. That's the darts player with a steady aim at the wrong spot.

Splitting the error into three parts

There's a neat fact about these two kinds of miss. Suppose you measure error by squaring it, as mean squared error does. Then the total error splits exactly into three pieces:

E[(y−y^)2]=bias2+variance+σ2⏟noise\mathbb{E}\big[(y - \hat{y})^2\big] = \text{bias}^2 + \text{variance} + \underbrace{\sigma^2}_{\text{noise}}

Take it one symbol at a time. The letter yy is the real outcome, noise and all. The prediction is y^\hat{y}, said "y-hat". The E\mathbb{E} in front means "expected value", the long-run average you met in Phase 2. Here it averages over every training set you might have drawn, and over the noise too. So the left side is the average squared error. On the right, σ2\sigma^2 (said "sigma squared") measures the size of the noise.

In plain words: your average squared error is made of three parts. Bias squared is how far off the average prediction is, squared. Variance is how much the predictions spread out around their own average. Noise is the randomness no model can predict.

Bias shows up squared because the error itself was squared. So a steady miss too high and a steady miss too low both count as positive. The noise part is the same for every model. Two customers who look identical on paper can still behave differently, and nothing can predict that. So you can only fight the other two parts. They usually trade against each other: pushing one down tends to push the other up.

A small example

A delivery app predicts how long one order will take. You retrain its model on four different months of data, so you get four models. For the same order they predict 22, 26, 24 and 28 minutes. Say the true typical time for this order is 20 minutes. Leave noise out for now.

  • The average prediction is (22 + 26 + 24 + 28) / 4 = 25. That's 5 minutes too high, so bias² = 5² = 25.
  • Each prediction's distance from the average of 25 is −3, 1, −1 and 3. Squared, those are 9, 1, 1 and 9. Their average is 20 / 4 = 5. That's the variance.
  • The errors against the truth are 2, 6, 4 and 8. Squared, they're 4, 36, 16 and 64. Their average is 120 / 4 = 30.

And 25 + 5 = 30. The split is exact.

preds = [22, 26, 24, 28]   # four retrained models, one order
truth = 20                 # the true typical delivery time
avg = sum(preds) / len(preds)
bias_sq = (avg - truth) ** 2
variance = sum((p - avg) ** 2 for p in preds) / len(preds)
mse = sum((p - truth) ** 2 for p in preds) / len(preds)
print(avg, bias_sq, variance, mse)
It runs right here in your browser, with nothing to install.

It prints 25.0 25.0 5.0 30.0. Those are the average prediction, bias², variance, and the mean squared error.

Go deeper Why the split is exact

Take one prediction p, the average prediction a, and the truth t. The error p − t is the sum of two steps: (p − a) + (a − t). Squaring gives (p − a)² + (a − t)² + 2(p − a)(a − t).

Now average over all the models. The first term averages to the variance. The second term is the same for every model, and it's the bias². In the last term, (a − t) is fixed, and the values of p − a average to zero, because a is their average. So the last term drops out. In the example, the distances −3, 1, −1 and 3 add up to 0.

Noise adds its own term in the same way. It averages to zero and has nothing to do with which training set you drew, so its cross term drops out too.

Quick check
What does this print?
preds = [9, 11]     # two retrained models, one customer
truth = 7
avg = sum(preds) / len(preds)
print((avg - truth) ** 2, sum((p - avg) ** 2 for p in preds) / len(preds))
  1. 3.0 1.0
  2. 1.0 9.0
  3. 9.0 2.0
  4. 9.0 1.0
Show the answer

9.0 1.0

The average prediction is 10, which is 3 too high, so bias² is 3² = 9.0. Each prediction is 1 away from the average, so the squares are 1 and 1, and the variance is their average, 1.0.

Seeing it: refit two hundred times

Now watch the trade-off happen. In the code below, the true pattern is a smooth wave called a sine curve. It's the dashed gray line. Each training set is 25 points taken from the wave, with random noise added.

The code fits a polynomial to each training set. A polynomial is a curve whose degree sets how many bends it can make. Degree 1 is a straight line. Degree 3 can bend twice. Degree 7 can bend up to six times.

For each degree, the code fits 200 different noisy samples and draws 20 of the fits. Then it measures bias² and variance across all 200. It averages them over 50 points along the wave.

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(0)
f = lambda x: np.sin(2 * np.pi * x)
grid = np.linspace(0.05, 0.95, 50)

def fits(degree, n_sets=200, n=25, noise=0.3):
    preds = []
    for _ in range(n_sets):
        x = rng.uniform(0, 1, n)
        y = f(x) + rng.normal(0, noise, n)
        preds.append(np.polyval(np.polyfit(x, y, degree), grid))
    return np.array(preds)

fig, axes = plt.subplots(1, 3, figsize=(10, 3.2), sharey=True)
for ax, d in zip(axes, [1, 3, 7]):
    P = fits(d)
    for p in P[:20]:
        ax.plot(grid, p, alpha=0.35, lw=1)
    ax.plot(grid, f(grid), "--", color="gray", lw=2)
    ax.set_title(f"degree {d}")
    ax.set_ylim(-2, 2)
    bias2 = np.mean((P.mean(axis=0) - f(grid)) ** 2)
    var = np.mean(P.var(axis=0))
    print(f"degree {d}:  bias² {bias2:.3f}   variance {var:.3f}   sum {bias2 + var:.3f}")

Read the panels from left to right.

  • Degree 1: the lines agree with each other, and they're all wrong in the same way. That's high bias and low variance. Its bias² is about 0.15.
  • Degree 7: the curves are right on average, but each one wiggles differently. That's low bias and high variance. Its variance is about 0.72.
  • Degree 3: both parts are small, and it has the lowest sum, about 0.02. Its bends can follow the wave, without many spare bends left over to chase noise.
Try it now

Twelve refits on fresh samples. At degree 1 they agree and all miss. At degree 12 they disagree wildly.

Learning curves: diagnosing a real model

In real work you can't collect a hundred fresh datasets. You have one. A learning curve gets the same diagnosis from it. You train on bigger and bigger slices of the data. At each size, you score the model twice: once on the data it trained on, and once on validation data it didn't see. Then you plot both scores.

The code below does this for the customer data you've used before. Each row says whether a customer churned, which means they stopped buying. It compares two models. The first is logistic regression, which draws one simple, straight boundary between leavers and stayers. The second is an unpruned decision tree, one that's allowed to grow as deep as it likes.

The score here is AUC. It measures how well a model ranks customers who leave above customers who stay. A score of 0.5 is no better than a coin flip, and 1.0 is a perfect ranking. A later lesson covers it in full. The cv=5 setting repeats each fit on five different splits of the data and averages the results, so the curves are less jumpy.

import pandas as pd
import requests
import matplotlib.pyplot as plt
from sklearn.model_selection import learning_curve
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeClassifier

df = pd.DataFrame(requests.get("https://api.gradient.test/v1/customers").json()["data"])
df["email_open_rate"] = df["email_open_rate"].fillna(0)
features = ["tenure_months", "orders_last_year", "avg_order_value", "days_since_last_order",
            "discount_share", "email_open_rate", "support_tickets", "trail_club"]
X, y = df[features], df["churned"]

models = {"logistic regression": make_pipeline(StandardScaler(), LogisticRegression()),
          "unpruned tree": DecisionTreeClassifier(random_state=0)}
fig, axes = plt.subplots(1, 2, figsize=(10, 3.2), sharey=True)
for ax, (name, model) in zip(axes, models.items()):
    sizes, tr, va = learning_curve(model, X, y, train_sizes=[0.1, 0.25, 0.5, 0.75, 1.0],
                                   cv=5, scoring="roc_auc")
    ax.plot(sizes, tr.mean(axis=1), marker="o", label="training")
    ax.plot(sizes, va.mean(axis=1), marker="o", label="validation")
    ax.set_title(name)
    ax.set_xlabel("training examples")
    print(f"{name}: final train AUC {tr.mean(axis=1)[-1]:.2f}; validation AUC as data grows "
          + " → ".join(f"{v:.2f}" for v in va.mean(axis=1)))
axes[0].set_ylabel("AUC")
axes[0].legend()

Here's how to read the two charts.

  • Logistic regression: the training and validation curves sit close together. More data won't move them much. If the score isn't good enough, bias is the limit. The model needs better features or a more flexible design.
  • Unpruned tree: training AUC is a perfect 1.0 at every size, and validation is far below. That big gap is the classic sign of high variance. The validation score is noisy, but it ends highest with the most data. The fixes are more data, pruning (limiting how deep the tree can grow), or averaging many trees, which is what a random forest does.

The table below sums up the two patterns. Two of its fixes may be new. Regularization adds a penalty that pushes a model toward simpler answers. Bagging trains many models on different random samples of the data and averages them.

What you seeProblemWhat helps
Both scores poor and close togetherhigh biasmore flexible model, better features, less regularization, train longer
Training great, validation much worsehigh variancemore data, regularization, simpler model, bagging, fewer features
Quick check
Training and validation AUC are both 0.68 and barely change as you add data. What's the best next step?
  1. Try a more flexible model or better features (it's high bias)
  2. Collect much more data (it's high variance)
  3. Increase regularization
  4. Average many copies of the same model
Show the answer

Try a more flexible model or better features (it's high bias)

Close, flat, poor curves mean the model can't capture the pattern. That's high bias, and more data won't fix bias.

The knobs you already know, by what they trade

Most settings you tune move a model along the same seesaw. Turn a knob one way, and bias falls while variance rises. Turn it the other way, and the reverse happens.

  • Regularization strength λ (said "lambda"): how hard the penalty pushes the model toward simpler answers. Turning it up means less variance and more bias.
  • Tree depth, polynomial degree, network size: turning these up means less bias and more variance.
  • k in nearest neighbors: a nearest-neighbors model predicts from the k most similar past examples. A small k is flexible, so it has high variance. A large k averages more neighbors, so it has higher bias.
  • Bagging and random forests average many high-variance models. Their scattered misses partly cancel out, which cuts variance.
  • Boosting adds models one after another. Each new one works on the errors the earlier ones left behind. That cuts bias.

A caveat for deep learning

The U-shaped picture holds well for classical models. As you add complexity, validation error first falls, then rises again. Very large neural networks often break this pattern. Past the point where a model can fit its training data exactly, test error can fall again as the model keeps growing. This is called double descent. It's part of why LLMs with billions of weights, trained on enormous datasets, generalize well.

The diagnosis tools still apply. Say a fine-tune's training loss keeps falling while its validation loss climbs. That model is showing high variance.

For marketers: a forecast that's always 10% low has a bias problem. One that swings wildly whenever you add a month of data has a variance problem. The fixes are different, so find out which one you have before you change anything.

Worked example

Worked example: which problem does each model have?

A shop retrains two sales models on four different months of data. Each model predicts Saturday sales for the same store, in thousands of dollars. The true typical figure is 20. The simple model predicted 14, 15, 14 and 15. The flexible model predicted 12, 29, 16 and 23. Work out bias² and variance for each, then decide which fix each one needs.

truth = 20
models = {"simple": [14, 15, 14, 15], "flexible": [12, 29, 16, 23]}
for name, preds in models.items():
    avg = sum(preds) / len(preds)
    bias_sq = (avg - truth) ** 2
    variance = sum((p - avg) ** 2 for p in preds) / len(preds)
    print(f"{name:<9} average {avg:5.1f}  bias² {bias_sq:5.2f}  variance {variance:5.2f}  total {bias_sq + variance:5.2f}")
In the lesson you can change this and run it.

The simple model's predictions hardly move, so its variance is only 0.25. But their average, 14.5, is 5.5 below the truth, so its bias² is 5.5² = 30.25. That's high bias. More data won't help it; it needs better features or more flexibility. The flexible model is right on average, with a bias² of 0. But its predictions swing from 12 to 29, for a variance of 42.5. That's high variance, so more data, regularization or averaging several models would help. Its total of 42.5 is worse than the simple model's 30.5, even though it's right on average.

Quick check
You turn the regularization strength λ up a lot. What usually happens to bias and variance?
  1. Both fall
  2. Bias falls and variance rises
  3. Variance falls and bias rises
  4. Neither changes; only the noise does
Show the answer

Variance falls and bias rises

A stronger penalty pushes the model toward simpler answers. Simpler models scatter less between training sets, but they miss in a steadier direction.

Practice what you just read

  • 4 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

Which model is most likely to have high variance?

  1. A decision tree with no depth limit
  2. Logistic regression with strong regularization
  3. Predicting the average for everyone
  4. k-NN with k = 200

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson