Calibration policy guide¶
Probability calibration in mlframe is governed by pick_best_calibrator (src/mlframe/calibration/policy.py), a small policy that selects among Sigmoid (Platt scaling), Isotonic (PAV), Beta (Kull et al. 2017), and Spline calibrators (CANDIDATE_NAMES) by comparing out-of-fold Expected Calibration Error (ECE), using an honest held-out inner-CV estimate by default.
When this fires¶
The policy is consulted at end-of-training, after the OOF predictions for every classifier in the ensemble are available. Defaults are picked so the policy works out-of-the-box without requiring user configuration.
Signature¶
from mlframe.calibration.policy import pick_best_calibrator
result = pick_best_calibrator(
None, None, # optional diagnostic-only held-out probs/labels (not used for the decision)
oof_probs, oof_y, # OOF probs/labels that drive the selection
candidates=None, # default: all of CANDIDATE_NAMES = (Sigmoid, Isotonic, Beta, Spline)
n_bootstrap=200, # bootstrap reps for the (reported, non-deciding) ECE CI
n_bins=10, # ECE bin count (DEFAULT_ECE_NBINS)
random_state=0,
)
# -> {"chosen": <name>, "ece_mean": float, "ece_ci": (lo, hi),
# "alternatives": {...}, "rule": <selection-rule>, "n_oof": int, "plot_path": Optional[str],
# "secondary_ece": Optional[float]}
ECE itself is computed by _ece_score(y_true, p_pred, n_bins=10) in the same module (DEFAULT_ECE_NBINS=10); the full Brier reliability / resolution / uncertainty decomposition is available via mlframe.metrics.core.compute_ece_and_brier_decomposition.
Decision rule¶
Given OOF oof_y and oof_probs, for each candidate calibrator C in CANDIDATE_NAMES = (Sigmoid, Isotonic, Beta, Spline):
Default selection="inner_cv":
- Build
inner_cv_splits(default 5) stratified inner folds of the OOF. - Fit each candidate on the fold complement, score ECE on the held-out fold, and average across folds — this is the honest held-out ECE (
rank_ece), immune to a flexible calibrator (Isotonic) interpolating its own in-sample score toward zero. - Pick the candidate with the lowest held-out ECE (
rule="lowest_heldout_ece"); refit it on the full OOF for deployment.
Legacy selection="same_oof" (fits AND scores every candidate on the same OOF rows — optimistic by ~0.006 ECE and Isotonic-biased; kept only for replay/A-B):
- Compute
ece_C = _ece_score(oof_y, calibrated_probs_C, n_bins=10)plus a bootstrap CI. - Sort by ECE mean ascending; if the top candidate's CI does not overlap the runner-up's, pick it directly (
rule="lowest_ece_ci_separated"). - If CIs overlap, apply the Kull-2017 default rule: prefer
Isotonicwhenn_oof >= 1000elseBeta, if that default is among the tied candidates (rule="default_isotonic"/"default_beta"); otherwise fall back to the lowest-mean candidate (rule="lowest_ece_ci_overlap").
Why ECE-with-CI rather than point ECE¶
Point-ECE on n<~5000 OOF rows is noisy; calibrators that fit a few extra parameters (Isotonic) routinely show a 0.5-1pp improvement that washes out under resampling. Bootstrap CI exposes that noise so the policy doesn't repeatedly flip between calibrators on minor data changes.
Reliability plot¶
A reliability plot can be emitted by pick_best_calibrator when emit_plot=True (with an optional plot_path=); the resolved path is returned under the plot_path key of the result dict. The plot overlays the candidate calibration curves alongside the ideal diagonal and annotates the chosen calibrator.
Limitations¶
- Binary classification only in the current implementation. Multiclass calibration is one-vs-rest in the underlying sklearn classes but the policy does not currently produce per-class CIs; skipping the auto-pick is the safer default for multiclass until that is wired.
- Requires OOF predictions. Classifiers trained without CV (i.e. with
train_eval_score=Falseshortcuts) cannot be calibrated by this policy and a WARN is logged. - n<200 rows: the bootstrap CI / held-out ECE is wide/noisy enough that candidates are barely distinguishable. This is by design — there is not enough data to fit a calibrator meaningfully on tiny samples.
Related modules¶
src/mlframe/calibration/policy.py—pick_best_calibrator,_ece_score, the reliability-plot emitter.src/mlframe/metrics/core.py—compute_ece_and_brier_decomposition(Brier REL/RES/UNC decomposition).src/mlframe/calibration/post.py— post-hocSigmoid/Isotonicwrappers around the chosen calibrator.src/mlframe/evaluation/bootstrap.py— genericbootstrap_metric+delong_testconsumed by the policy.src/mlframe/training/honest_diagnostics.py— aggregator that surfaces the calibration verdict alongside the rest of the honest-OOF diagnostics. Seedocs/honest_diagnostics_guide.md.
Sensors¶
tests/calibration/test_pick_best_calibrator.py— covers the bootstrap-CI selection logic, tie-breaker behaviour, candidate ordering, multiclass fallback.tests/evaluation/test_bootstrap.py— covers the genericbootstrap_metrichelper that the policy delegates to.
Commit reference¶
Landed in commit 783eae4c (W9A feat(calibration): pick_best_calibrator policy with OOF ECE + bootstrap CI) on 2026-05-24.