Calibration Curve — Bokeh

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 Bokeh

Python source (Bokeh)

""" anyplot.ai
calibration-curve: Calibration Curve
Library: bokeh 3.9.0 | Python 3.13.13
Quality: 91/100 | Updated: 2026-05-10
"""

import os
import sys
import time
from pathlib import Path

import numpy as np
from bokeh.io import output_file, save
from bokeh.models import ColumnDataSource, HoverTool, Label
from bokeh.plotting import figure
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

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"
BRAND = "#009E73"
NEUTRAL = "#1A1A1A" if THEME == "light" else "#E8E8E0"

# Data - Simulate binary classification predictions
np.random.seed(42)
n_samples = 5000

y_prob = np.random.uniform(0, 1, n_samples)

calibration_factor = 1.3
adjusted_prob = 1 / (
    1 + np.exp(-calibration_factor * (np.log(y_prob / (1 - y_prob + 1e-10))))
)
adjusted_prob = np.clip(adjusted_prob, 0.01, 0.99)
y_true = (np.random.uniform(0, 1, n_samples) < adjusted_prob).astype(int)

# Calculate calibration curve (binned)
n_bins = 10
bin_edges = np.linspace(0, 1, n_bins + 1)

fraction_of_positives = []
mean_predicted_value = []
bin_counts = []

for i in range(n_bins):
    mask = (y_prob >= bin_edges[i]) & (y_prob < bin_edges[i + 1])
    if i == n_bins - 1:
        mask = (y_prob >= bin_edges[i]) & (y_prob <= bin_edges[i + 1])

    if mask.sum() > 0:
        fraction_of_positives.append(y_true[mask].mean())
        mean_predicted_value.append(y_prob[mask].mean())
        bin_counts.append(mask.sum())
    else:
        fraction_of_positives.append(np.nan)
        mean_predicted_value.append(
            bin_edges[i] + (bin_edges[i + 1] - bin_edges[i]) / 2
        )
        bin_counts.append(0)

valid_mask = np.array(bin_counts) > 0
mean_pred_valid = np.array(mean_predicted_value)[valid_mask]
frac_pos_valid = np.array(fraction_of_positives)[valid_mask]
counts_valid = np.array(bin_counts)[valid_mask]

# Calculate Brier score and ECE
brier_score = np.mean((y_prob - y_true) ** 2)

ece = 0
total_samples = sum(bin_counts)
for i in range(len(bin_counts)):
    if bin_counts[i] > 0:
        ece += (bin_counts[i] / total_samples) * abs(
            fraction_of_positives[i] - mean_predicted_value[i]
        )

# Create calibration plot
p = figure(
    width=4800,
    height=2700,
    title="calibration-curve · bokeh · anyplot.ai",
    x_axis_label="Mean Predicted Probability",
    y_axis_label="Fraction of Positives",
    x_range=(-0.02, 1.02),
    y_range=(-0.02, 1.02),
)

# Add diagonal reference line (perfect calibration)
p.line(
    [0, 1],
    [0, 1],
    line_color=INK_SOFT,
    line_dash="dashed",
    line_width=4,
    legend_label="Perfect Calibration",
)

# Create source for calibration curve
source = ColumnDataSource(
    data={"x": mean_pred_valid, "y": frac_pos_valid, "count": counts_valid}
)

# Add HoverTool for interactivity
hover = HoverTool(
    tooltips=[
        ("Predicted", "@x{0.00}"),
        ("Observed", "@y{0.00}"),
        ("Count", "@count"),
    ]
)
p.add_tools(hover)

# Plot calibration curve with markers
p.line("x", "y", source=source, line_color=BRAND, line_width=5, legend_label="Classifier")
p.scatter(
    "x",
    "y",
    source=source,
    size=25,
    color=BRAND,
    fill_alpha=0.9,
    line_color=BRAND,
    line_width=2,
)

# Add metrics annotation
metrics_text = f"Brier Score: {brier_score:.3f}\nECE: {ece:.3f}"
metrics_label = Label(
    x=0.05,
    y=0.95,
    x_units="data",
    y_units="data",
    text=metrics_text,
    text_font_size="22pt",
    text_color=INK,
    background_fill_color=ELEVATED_BG,
    background_fill_alpha=0.95,
    border_line_color=INK_SOFT,
    border_line_width=1,
)
p.add_layout(metrics_label)

# Style the plot
p.title.text_font_size = "28pt"
p.title.text_color = INK
p.xaxis.axis_label_text_font_size = "22pt"
p.yaxis.axis_label_text_font_size = "22pt"
p.xaxis.axis_label_text_color = INK
p.yaxis.axis_label_text_color = INK
p.xaxis.major_label_text_font_size = "18pt"
p.yaxis.major_label_text_font_size = "18pt"
p.xaxis.major_label_text_color = INK_SOFT
p.yaxis.major_label_text_color = INK_SOFT

# Grid styling
p.xgrid.grid_line_color = INK
p.ygrid.grid_line_color = INK
p.xgrid.grid_line_alpha = 0.10
p.ygrid.grid_line_alpha = 0.10

# Legend styling
if p.legend:
    p.legend.label_text_font_size = "20pt"
    p.legend.label_text_color = INK_SOFT
    p.legend.location = "bottom_right"
    p.legend.background_fill_color = ELEVATED_BG
    p.legend.background_fill_alpha = 0.95
    p.legend.border_line_color = INK_SOFT
    p.legend.border_line_width = 1
    p.legend.padding = 15
    p.legend.spacing = 10

# Axis styling
p.xaxis.axis_line_color = INK_SOFT
p.yaxis.axis_line_color = INK_SOFT
p.xaxis.axis_line_width = 1
p.yaxis.axis_line_width = 1
p.xaxis.major_tick_line_color = INK_SOFT
p.yaxis.major_tick_line_color = INK_SOFT
p.xaxis.major_tick_line_width = 1
p.yaxis.major_tick_line_width = 1

# Background
p.background_fill_color = PAGE_BG
p.border_fill_color = PAGE_BG
p.outline_line_color = INK_SOFT
p.outline_line_width = 1

# Save HTML
output_file(f"plot-{THEME}.html")
save(p)

# Screenshot with headless Chrome
W, H = 4800, 2700
opts = Options()
for arg in (
    "--headless=new",
    "--no-sandbox",
    "--disable-dev-shm-usage",
    "--disable-gpu",
    f"--window-size={W},{H}",
    "--hide-scrollbars",
):
    opts.add_argument(arg)
driver = webdriver.Chrome(options=opts)
driver.set_window_size(W, H)
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
time.sleep(3)
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()

Part of Calibration Curve on anyplot.ai.

Other implementations