Bias-Variance Tradeoff Curve — lets-plot

A theoretical visualization of the bias-variance tradeoff showing how total prediction error decomposes into bias squared, variance, and irreducible noise as a function of model complexity. The plot displays multiple curves: bias squared (decreasing with complexity), variance (increasing with complexity), irreducible error (constant), and total error (U-shaped). This is one of the most fundamental conceptual plots in machine learning for understanding model selection, overfitting, and underfitting.

Bias-Variance Tradeoff Curve rendered with lets-plot

Python source (lets-plot)

""" anyplot.ai
curve-bias-variance-tradeoff: Bias-Variance Tradeoff Curve
Library: letsplot 4.10.1 | Python 3.13.13
Quality: 88/100 | Updated: 2026-05-28
"""

import os

import numpy as np
import pandas as pd
from lets_plot import *


LetsPlot.setup_html()

THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

# Data — formula distinct from seaborn sibling (change request)
np.random.seed(42)
complexity = np.linspace(0.5, 10.0, 100)
bias_squared = 3.0 / (1 + 0.7 * complexity)
variance = 0.08 * complexity**1.3
irreducible_error = np.full_like(complexity, 0.25)
total_error = bias_squared + variance + irreducible_error

optimal_idx = int(np.argmin(total_error))
optimal_complexity = float(complexity[optimal_idx])
optimal_error = float(total_error[optimal_idx])

component_order = ["Bias²", "Variance", "Irreducible Error", "Total Error"]
df = pd.DataFrame(
    {
        "complexity": np.tile(complexity, 4),
        "error": np.concatenate([bias_squared, variance, irreducible_error, total_error]),
        "component": pd.Categorical(
            ["Bias²"] * 100 + ["Variance"] * 100 + ["Irreducible Error"] * 100 + ["Total Error"] * 100,
            categories=component_order,
        ),
    }
)

# Curve values at x=10 for right-side direct labels
x_lbl = 10.0
bias_at_lbl = float(3.0 / (1 + 0.7 * x_lbl))
var_at_lbl = float(0.08 * x_lbl**1.3)
irred_at_lbl = 0.25
total_at_lbl = bias_at_lbl + var_at_lbl + irred_at_lbl

label_df = pd.DataFrame(
    {
        "complexity": [x_lbl + 0.3] * 4,
        "error": [bias_at_lbl + 0.15, var_at_lbl + 0.05, irred_at_lbl - 0.22, total_at_lbl + 0.04],
        "label": ["Bias²", "Variance", "Irred.\nError", "Total\nError"],
        "component": pd.Categorical(component_order, categories=component_order),
    }
)

# Shaded underfitting / overfitting zones
under_df = pd.DataFrame({"xmin": [0.4], "xmax": [optimal_complexity], "ymin": [-0.15], "ymax": [3.38]})
over_df = pd.DataFrame({"xmin": [optimal_complexity], "xmax": [10.5], "ymin": [-0.15], "ymax": [3.38]})

# Zone labels
zone_df = pd.DataFrame(
    {
        "x": [optimal_complexity * 0.42, (optimal_complexity + 10.5) * 0.5],
        "y": [3.12, 3.12],
        "label": ["← Underfitting\n(High Bias)", "Overfitting →\n(High Variance)"],
    }
)

# Optimal complexity annotation
opt_df = pd.DataFrame({"x": [optimal_complexity + 0.25], "y": [optimal_error + 0.40], "label": ["Optimal\nComplexity"]})

# Formula annotation
formula_df = pd.DataFrame({"x": [5.25], "y": [2.80], "label": ["Total Error = Bias² + Variance + Irreducible Error"]})

# Imprint palette: Bias²=pos1 (green), Variance=pos2 (purple), Irred.=pos3 (blue), Total=red (semantic error)
color_values = ["#009E73", "#C475FD", "#4467A3", "#AE3030"]
linetype_values = ["dashed", "longdash", "dotted", "solid"]

title_str = "curve-bias-variance-tradeoff · python · letsplot · anyplot.ai"
title_n = len(title_str)
title_size = max(11, round(16 * 67 / title_n)) if title_n > 67 else 16

anyplot_theme = theme(
    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
    panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
    panel_border=element_blank(),
    panel_grid_major_x=element_blank(),
    panel_grid_major_y=element_line(color=INK_SOFT, size=0.12),
    panel_grid_minor=element_blank(),
    axis_title=element_text(color=INK, size=12),
    axis_text=element_text(color=INK_SOFT, size=10),
    axis_line=element_line(color=INK_SOFT, size=0.5),
    plot_title=element_text(color=INK, size=title_size),
    legend_position="none",
)

plot = (
    ggplot(df, aes(x="complexity", y="error", color="component"))
    + geom_rect(
        data=under_df,
        mapping=aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax"),
        fill="#009E73",
        color=PAGE_BG,
        alpha=0.06,
        inherit_aes=False,
    )
    + geom_rect(
        data=over_df,
        mapping=aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax"),
        fill="#AE3030",
        color=PAGE_BG,
        alpha=0.06,
        inherit_aes=False,
    )
    + geom_line(aes(linetype="component"), size=1.0, tooltips=layer_tooltips(["component", "error"]))
    + geom_vline(xintercept=optimal_complexity, linetype="dotdash", color=INK_SOFT, size=0.65)
    + geom_text(
        data=zone_df, mapping=aes(x="x", y="y", label="label"), inherit_aes=False, color=INK_SOFT, size=3.3, hjust=0.5
    )
    + geom_text(data=opt_df, mapping=aes(x="x", y="y", label="label"), inherit_aes=False, color=INK, size=3.2, hjust=0)
    + geom_text(
        data=formula_df,
        mapping=aes(x="x", y="y", label="label"),
        inherit_aes=False,
        color=INK_SOFT,
        size=3.3,
        hjust=0.5,
    )
    + geom_text(
        data=label_df,
        mapping=aes(x="complexity", y="error", label="label", color="component"),
        inherit_aes=False,
        size=3.2,
        hjust=0,
    )
    + scale_color_manual(values=color_values)
    + scale_linetype_manual(values=linetype_values)
    + scale_x_continuous(limits=[0.4, 12.0], breaks=[2, 4, 6, 8, 10])
    + scale_y_continuous(limits=[-0.15, 3.45])
    + labs(x="Model Complexity", y="Prediction Error", title=title_str)
    + ggsize(800, 450)
    + theme_classic()
    + anyplot_theme
)

ggsave(plot, f"plot-{THEME}.png", path=".", scale=4)
ggsave(plot, f"plot-{THEME}.html", path=".")

Part of Bias-Variance Tradeoff Curve on anyplot.ai.

Other implementations