Skip to content

Commit e35fc60

Browse files
github-actions[bot]claudeMarkusNeusinger
authored
feat(letsplot): implement calibration-curve (#6258)
## Implementation: `calibration-curve` - python/letsplot Implements the **python/letsplot** version of `calibration-curve`. **File:** `plots/calibration-curve/implementations/python/letsplot.py` **Parent Issue:** #2331 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25626801514)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 3164e85 commit e35fc60

2 files changed

Lines changed: 205 additions & 144 deletions

File tree

plots/calibration-curve/implementations/python/letsplot.py

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
calibration-curve: Calibration Curve
3-
Library: letsplot 4.8.2 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-26
3+
Library: letsplot 4.9.0 | Python 3.13.13
4+
Quality: 91/100 | Updated: 2026-05-10
55
"""
66

7+
import os
8+
79
import numpy as np
810
import pandas as pd
911
from lets_plot import *
12+
from lets_plot.export import ggsave
1013

1114

1215
LetsPlot.setup_html()
1316

17+
# Theme tokens
18+
THEME = os.getenv("ANYPLOT_THEME", "light")
19+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
20+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
21+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
22+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
23+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
24+
RULE = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)"
25+
26+
BRAND = "#009E73" # Okabe-Ito position 1 — first series
27+
SECONDARY = "#D55E00" # Okabe-Ito position 2 — reference line
28+
1429
# Data - Generate realistic binary classification predictions
1530
np.random.seed(42)
1631
n_samples = 1000
@@ -70,6 +85,12 @@
7085
{"mean_predicted": mean_predicted, "fraction_positive": fraction_positive, "bin_count": bin_counts}
7186
)
7287
df_calibration = df_calibration.dropna()
88+
df_calibration["tooltip"] = df_calibration.apply(
89+
lambda row: (
90+
f"Predicted: {row['mean_predicted']:.3f}\nObserved: {row['fraction_positive']:.3f}\nBin size: {int(row['bin_count'])}"
91+
),
92+
axis=1,
93+
)
7394

7495
# Create dataframe for diagonal (perfect calibration)
7596
df_diagonal = pd.DataFrame({"x": [0, 1], "y": [0, 1]})
@@ -78,46 +99,49 @@
7899
hist_bins = 20
79100
hist_counts, hist_edges = np.histogram(y_prob, bins=hist_bins, range=(0, 1))
80101
hist_centers = (hist_edges[:-1] + hist_edges[1:]) / 2
81-
df_histogram = pd.DataFrame(
82-
{
83-
"prob_center": hist_centers,
84-
"count": hist_counts / hist_counts.max(), # Normalize for subplot
85-
}
86-
)
102+
df_histogram = pd.DataFrame({"prob_center": hist_centers, "count": hist_counts / hist_counts.max()})
87103

88104
# Plot
89105
plot = (
90106
ggplot()
91107
# Perfect calibration diagonal line
92-
+ geom_line(aes(x="x", y="y"), data=df_diagonal, color="#888888", size=1.5, linetype="dashed")
108+
+ geom_line(aes(x="x", y="y"), data=df_diagonal, color=INK_SOFT, size=1.5, linetype="dashed")
93109
# Calibration curve
94-
+ geom_line(aes(x="mean_predicted", y="fraction_positive"), data=df_calibration, color="#306998", size=2)
110+
+ geom_line(aes(x="mean_predicted", y="fraction_positive"), data=df_calibration, color=BRAND, size=2)
95111
+ geom_point(
96-
aes(x="mean_predicted", y="fraction_positive"), data=df_calibration, color="#306998", size=5, alpha=0.9
112+
aes(x="mean_predicted", y="fraction_positive", tooltip="tooltip"),
113+
data=df_calibration,
114+
color=BRAND,
115+
size=5,
116+
alpha=0.9,
97117
)
98118
# Histogram bars at bottom showing prediction distribution
99119
+ geom_bar(
100-
aes(x="prob_center", y="count"), data=df_histogram, stat="identity", fill="#FFD43B", alpha=0.6, width=0.045
120+
aes(x="prob_center", y="count"), data=df_histogram, stat="identity", fill=INK_MUTED, alpha=0.4, width=0.045
101121
)
102122
# Labels and styling
103123
+ labs(
104124
x="Mean Predicted Probability",
105125
y="Fraction of Positives",
106-
title=f"calibration-curve · letsplot · pyplots.ai\nBrier Score: {brier_score:.4f} | ECE: {ece:.4f}",
126+
title=f"calibration-curve · letsplot · anyplot.ai\nBrier Score: {brier_score:.4f} | ECE: {ece:.4f}",
107127
)
108128
+ scale_x_continuous(limits=[0, 1], breaks=[0, 0.2, 0.4, 0.6, 0.8, 1.0])
109129
+ scale_y_continuous(limits=[0, 1], breaks=[0, 0.2, 0.4, 0.6, 0.8, 1.0])
110-
+ theme_minimal()
111130
+ theme(
112-
plot_title=element_text(size=22),
113-
axis_title=element_text(size=18),
114-
axis_text=element_text(size=14),
115-
panel_grid_major=element_line(color="#CCCCCC", size=0.5),
131+
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
132+
panel_background=element_rect(fill=PAGE_BG),
133+
panel_grid_major=element_line(color=RULE, size=0.4),
116134
panel_grid_minor=element_blank(),
135+
axis_title=element_text(size=20, color=INK),
136+
axis_text=element_text(size=16, color=INK_SOFT),
137+
axis_line=element_line(color=INK_SOFT, size=0.6),
138+
plot_title=element_text(size=24, color=INK),
139+
axis_ticks_length_x=6,
140+
axis_ticks_length_y=6,
117141
)
118142
+ ggsize(1600, 900)
119143
)
120144

121145
# Save outputs
122-
ggsave(plot, "plot.png", path=".", scale=3)
123-
ggsave(plot, "plot.html", path=".")
146+
ggsave(plot, f"plot-{THEME}.png", path=".", scale=3)
147+
ggsave(plot, f"plot-{THEME}.html", path=".")

0 commit comments

Comments
 (0)