Calibration Curve — Pygal

A calibration curve (reliability diagram) visualizes how well the predicted probabilities of a binary classifier match actual outcomes. By plotting the fraction of positives against mean predicted probability in binned intervals, it reveals whether a model is well-calibrated, overconfident, or underconfident. A perfectly calibrated model follows the diagonal line where predicted probability equals observed frequency.

Calibration Curve rendered with Pygal

Python source (Pygal)

""" anyplot.ai
calibration-curve: Calibration Curve
Library: pygal 3.1.0 | Python 3.13.13
Quality: 90/100 | Updated: 2026-05-10
"""

import os

import numpy as np
import pygal
from pygal.style import Style


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

IMPRINT = ("#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477")

# Data: Generate synthetic binary classification with realistic calibration
np.random.seed(42)
n_samples = 2000
n_bins = 10

# Generate true probabilities spread across 0-1 range
true_prob = np.random.beta(2, 2, n_samples)
y_true = (np.random.random(n_samples) < true_prob).astype(int)

# Model 1: Well-calibrated model (Logistic Regression style)
noise1 = np.random.randn(n_samples) * 0.08
y_prob_model1 = np.clip(true_prob + noise1, 0.01, 0.99)

# Model 2: Overconfident model (Random Forest / Neural Network style)
y_prob_model2 = 1 / (1 + np.exp(-12 * (true_prob - 0.5)))
y_prob_model2 = np.clip(y_prob_model2 + np.random.randn(n_samples) * 0.02, 0.02, 0.98)

# Compute calibration data inline
bin_edges = np.linspace(0, 1, n_bins + 1)

# Model 1 calibration
bin_indices1 = np.digitize(y_prob_model1, bin_edges[1:-1])
mean_pred1 = []
frac_pos1 = []
for i in range(n_bins):
    mask = bin_indices1 == i
    if mask.sum() > 0:
        mean_pred1.append(np.mean(y_prob_model1[mask]))
        frac_pos1.append(np.mean(y_true[mask]))

# Model 2 calibration
bin_indices2 = np.digitize(y_prob_model2, bin_edges[1:-1])
mean_pred2 = []
frac_pos2 = []
for i in range(n_bins):
    mask = bin_indices2 == i
    if mask.sum() > 0:
        mean_pred2.append(np.mean(y_prob_model2[mask]))
        frac_pos2.append(np.mean(y_true[mask]))

# Compute Brier scores
brier1 = np.mean((y_prob_model1 - y_true) ** 2)
brier2 = np.mean((y_prob_model2 - y_true) ** 2)

# Custom style with theme-adaptive colors
custom_style = Style(
    background=PAGE_BG,
    plot_background=PAGE_BG,
    foreground=INK,
    foreground_strong=INK,
    foreground_subtle=INK_MUTED,
    colors=IMPRINT,
    title_font_size=28,
    label_font_size=22,
    major_label_font_size=18,
    legend_font_size=16,
    value_font_size=14,
    stroke_width=3,
)

# Create XY chart for calibration curve
chart = pygal.XY(
    style=custom_style,
    width=4800,
    height=2700,
    title="calibration-curve · pygal · anyplot.ai",
    x_title="Mean Predicted Probability",
    y_title="Fraction of Positives",
    show_dots=True,
    dots_size=16,
    stroke_style={"width": 5},
    show_x_guides=True,
    show_y_guides=True,
    x_value_formatter=lambda x: f"{x:.1f}",
    range=(0, 1),
    xrange=(0, 1),
    legend_at_bottom=True,
    legend_at_bottom_columns=3,
    legend_box_size=28,
    truncate_legend=-1,
    margin=50,
    margin_top=80,
    margin_bottom=200,
)

# Perfect calibration line (diagonal reference)
perfect_calibration = [
    {"value": (0, 0), "label": "Perfect calibration reference"},
    {"value": (0.25, 0.25), "label": "Predicted = Observed"},
    {"value": (0.5, 0.5), "label": "Ideal: 50% predicted → 50% positive"},
    {"value": (0.75, 0.75), "label": "Predicted = Observed"},
    {"value": (1.0, 1.0), "label": "Perfect calibration reference"},
]
chart.add("Perfect Calibration", perfect_calibration, stroke_dasharray="15,8", dots_size=0, stroke_style={"width": 4})

# Model 1 calibration curve - well-calibrated
model1_points = [{"value": (0.0, 0.0), "label": "Curve start"}]
model1_points.extend(
    [
        {"value": (pred, obs), "label": f"Bin: {pred:.2f} pred → {obs:.2f} actual ({int(obs * 100)}% positive)"}
        for pred, obs in zip(mean_pred1, frac_pos1, strict=False)
    ]
)
model1_points.append({"value": (1.0, 1.0), "label": "Curve end"})
chart.add(f"Logistic Regression (Brier: {brier1:.3f})", model1_points)

# Model 2 calibration curve - overconfident
model2_points = [{"value": (0.0, 0.0), "label": "Curve start"}]
model2_points.extend(
    [
        {"value": (pred, obs), "label": f"Bin: {pred:.2f} pred → {obs:.2f} actual (Δ={pred - obs:+.2f})"}
        for pred, obs in zip(mean_pred2, frac_pos2, strict=False)
    ]
)
model2_points.append({"value": (1.0, 1.0), "label": "Curve end"})
chart.add(f"Overconfident Model (Brier: {brier2:.3f})", model2_points)

# Save output files
output_dir = os.path.dirname(os.path.abspath(__file__))
chart.render_to_png(os.path.join(output_dir, f"plot-{THEME}.png"))
chart.render_to_file(os.path.join(output_dir, f"plot-{THEME}.html"))

Part of Calibration Curve on anyplot.ai.

Other implementations