Choose a calibrator¶
Data these snippets assume
Blocks on this page use a fixed set of names for held-out data instead of re-deriving it each time. To run them, define the names first:
# docs: no-run — the definitions the snippets on this page assume
import numpy as np
from probcal import BetaCalibrator, Masterscale, make_pd_portfolio
from probcal.monitor import CalibrationMonitor
cal_set, new_set = make_pd_portfolio(n=3000, random_state=0), make_pd_portfolio(n=1000, random_state=1)
s_cal, y_cal = cal_set.scores, cal_set.y # held-out calibration scores and outcomes
w_cal = np.ones_like(y_cal) # uniform sample weights
s_new = new_set.scores # scores of new obligors
ms = Masterscale.from_edges([0.01, 0.05], names=["G1", "G2", "G3"]) # the masterscale
p_cal = BetaCalibrator().fit(s_cal, y_cal).predict_proba(s_cal) # calibrated PDs
grades = ms.assign(p_cal) # rating labels
segments = np.array(["seg-a", "seg-b", "seg-c"])[np.arange(len(s_cal)) % 3] # segment labels
model = ... # any object with predict_proba(X); the docs use a stub that reads X[:, 0]
mon = CalibrationMonitor(alpha=0.05) # with a few batches of a calibrated forecast applied
Every block also names, in its first comment line, which of these it uses.
The catalog, as a table you can decide from. Every column that is a property of the code (monotonicity, which inverse exists, what serializes) is read off the fitted object and pinned by a test, so a column that stops being true breaks a build rather than misleading a reader.
The catalog is two tables sharing the class column: what each object is
(parameters, monotonicity, inverses, serialization, fit cost), then when to
reach for it. Two fit-cost entries change decisions rather than schedules: ENIR is quadratic in unique scores (its fit warns above
~50,000) and Venn–Abers stores its calibration set, so its JSON grows with
n.
The catalog¶
Properties¶
| Class | Fitted parameters | Monotone? | Exact point_inverse? |
interval_inverse? |
Serialization | Fit cost |
|---|---|---|---|---|---|---|
TemperatureCalibrator |
T_ (one) |
by construction (T > 0 always) |
yes: affine, (1/T, 0) |
yes, closed form | JSON, golden-pinned | O(n) per IRLS iteration |
PlattCalibrator |
a_, b_, converged_ |
checked at fit (is_monotone_ = a_ > 0) |
yes: affine, (a_, b_) |
yes, closed form | JSON, golden-pinned | O(n) per IRLS iteration |
BetaCalibrator ("abm") |
a_, b_, c_, constraint_active_ |
by construction (a_, b_ >= 0 enforced) |
yes: its own exact construction (seed + certified Halley) | yes, bisection | JSON, golden-pinned | O(n) per IRLS iteration |
BetaCalibrator(variant="ab"/"a") |
same attributes, tied exponents | by construction | yes: affine (affine_logit_coeffs_ is not None) |
yes | JSON, golden-pinned | O(n) per IRLS iteration |
IsotonicCalibrator |
block_mean_, block_first_s_, block_last_s_, block_center_s_, n_blocks_ |
by construction (PAVA) | no: NotImplementedError naming interval_inverse |
yes, searchsorted on blocks; plateau semantics |
JSON, golden-pinned | O(n log n) fit (sort + PAVA) |
CenteredIsotonicCalibrator |
as isotonic (interpolates between block centers) | by construction | no | yes: a point preimage, not a plateau | JSON, golden-pinned | O(n log n) fit |
HistogramBinningCalibrator |
edges_, bin_rate_, bin_weight_, is_monotone_ |
checked at fit: bin rates need not increase, and often do not | no | only when the fit came out monotone; otherwise NotImplementedError |
JSON, golden-pinned | O(n log n) fit |
ScalingBinningCalibrator |
platt_ (nested), edges_, bin_value_ |
declared (class default; the bin values inherit the Platt stage's order and are not re-checked) | no | yes: pulls the bin edge back through the Platt stage | JSON, golden-pinned (nested Platt envelope) | O(n log n) fit |
BBQCalibrator |
bins_grid_, weights_, is_monotone_ |
checked at fit on a probe grid | no | only when the fit came out monotone | JSON, golden-pinned | O(n log n) fit per candidate binning |
ENIRCalibrator |
path_lambdas_, path_solutions_, kept_breakpoints_, weights_, dropped_weight_ |
never: is_monotone_ = False at class level |
no | no: NotImplementedError, the preimage may be a union of intervals |
JSON, golden-pinned | quadratic in unique scores; intended for m ≲ 50,000 (fit warns above) |
SplineCalibrator |
n_knots_, lambda_, edof_, spline coefficients, is_monotone_ |
checked at fit on a probe grid (warns when it fails) | no | yes when monotone, by bisection | JSON, golden-pinned | O(n · k) per IRLS iteration (k knots), repeated over the inner CV for lambda |
VennAbersCalibrator (IVAP) |
the sorted calibration set plus both sweeps F0_, F1_ |
by construction | no | yes, by bisection | JSON, golden-pinned, but O(n): the calibration set is the fitted map | O(n log n) fit, O(log n) per prediction |
CrossVennAbersCalibrator (CVAP) |
_ivaps, one fitted IVAP per fold |
by construction | no | yes, inherited bisection | JSON, golden-pinned; O(n) for the same reason | one IVAP fit per fold |
SegmentedCalibrator |
base_ (nested), segments_, delta_hat_, se_, delta_tilde_, shrink_, tau2_ |
inherited from base_ |
yes whenever base_ has one (default base: beta), taking segment= |
yes, taking segment=, inverting through base_ plus that segment's shrunk offset |
JSON, golden-pinned (nested base envelope) | base cost, plus O(n) for the per-segment offsets |
CalibratorSelector |
best_name_, best_calibrator_ (nested), report_, is_monotone_ |
inherited from the winner | no: the selector exposes no affine coefficients even when the winner is affine; reach for sel.best_calibrator_.point_inverse |
yes, by bisection through the winner | JSON, golden-pinned (winner envelope plus the ranked report) | every candidate fitted K times (inner CV), plus the winner's refit |
Guidance¶
| Class | Data appetite | Use when | Avoid when |
|---|---|---|---|
TemperatureCalibrator |
~a dozen events | Confidence too soft or too sharp with the base rate already right | cannot move the level at all: s = 0.5 is a fixed point |
PlattCalibrator |
a few dozen events | Level and slope both off, and you want two auditable numbers | curvature that a straight logit line cannot follow |
BetaCalibrator ("abm") |
a few dozen events | Asymmetric tail distortion, the low-PD default | nothing to gain when a_ ≈ b_, where the tie-break drops you to Platt anyway |
BetaCalibrator(variant="ab"/"a") |
a few dozen events | You want the beta family with fewer parameters to defend | the tail asymmetry is the whole problem |
IsotonicCalibrator |
hundreds of events | Visible curvature and events to fund it | recourse downstream, where plateaus and moving block edges make counterfactuals fragile |
CenteredIsotonicCalibrator |
hundreds of events | Isotonic's fit without its step function, the nonparametric choice when recourse is in scope | very few blocks, where interpolation barely differs |
HistogramBinningCalibrator |
hundreds of events, per bin | Grade-shaped output that mirrors a masterscale | thin bins: a non-monotone fit also loses every inverse |
ScalingBinningCalibrator |
between Platt's and binning's: a Platt fit plus a bin count | You want binned output whose calibration error is measurable at O(1/ε² + B) samples |
you need a continuous map |
BBQCalibrator |
hundreds of events; strong on small samples (see the benchmarks) | Small portfolio where a single binning is a coin flip | thresholding or recourse, where a non-monotone average refuses to invert |
ENIRCalibrator |
hundreds of events; quadratic in unique scores | Pure predictive accuracy with no downstream inversion | anything on this page's cutoff and recourse paths |
SplineCalibrator |
hundreds of events (a penalized basis plus CV over lambda) |
Smooth curvature you want to show a validator as a curve, not a staircase | small samples, where the penalty is chosen on very little data |
VennAbersCalibrator (IVAP) |
hundreds of events | You need a distribution-free interval, not just a number | file size and load time matter |
CrossVennAbersCalibrator (CVAP) |
hundreds of events, spent across folds instead of carved out | The calibration sample is too small to carve and you still want the VA guarantee | the fold ensemble is harder to describe in a model document |
SegmentedCalibrator |
the base's appetite, plus ~a dozen events per segment | Segments that genuinely differ in level and a shrinkage story you can defend | segments that differ in slope, which is a re-fit, not an offset |
CalibratorSelector |
enough for an inner stratified K-fold (default cv=5) |
You want the choice made on out-of-fold evidence and written down | you already know the answer and the extra machinery only adds a layer to explain |
Three readings of the monotone column, and the difference matters:
by construction means the fit cannot produce a decreasing map;
checked at fit means is_monotone_ is derived from this fit's
parameters, so the same class can invert on one portfolio and refuse on
the next; declared means the class default stands and nothing re-derives
it. Only is_monotone_ = True buys you interval_inverse; everything
else raises NotImplementedError, which is the whole reason the column is
in the table. LogitOffset is not a calibrator and so is not a row here,
but it carries the same protocol (closed-form both inverses) and composes
onto any of the above through Chain, which is not a row either, having
no fit of its own to choose: it inherits every column from the stages you
put in it.
Count events, not rows: "hundreds of events" means events, not rows, and 500 observations at a 3% base rate is fifteen. The reasoning is in Data splitting; head-to-head evidence at event rates from 1.5% to 30% is in Benchmarks.
Reading the columns off your own fit¶
The table is a summary of what the objects report. On your data, ask them:
# s_cal, y_cal: held-out calibration scores and outcomes
from probcal import BetaCalibrator, HistogramBinningCalibrator
for cal in (BetaCalibrator().fit(s_cal, y_cal),
HistogramBinningCalibrator(n_bins=10).fit(s_cal, y_cal)):
print(type(cal).__name__,
"monotone:", cal.is_monotone_,
"affine:", cal.affine_logit_coeffs_ is not None,
"rank:", cal.complexity_rank)
A binning fit that prints monotone: False has just told you it cannot
serve a cutoff or a counterfactual on this portfolio, before anyone
builds a policy on it. That check belongs in the same script as the fit.
The decision path¶
Start from the diagnosis, not the catalog. A reliability curve on the logit scale plus the guardrails (slope, intercept, Spiegelhalter) says which failure you have; Why calibration and Metrics and tests cover the reading.
- Pure level error, and few events. One parameter is all the data can
fund:
LogitOffset, anchored to a long-run central tendency. This is the disciplined retreat, not a failure. - Slope error. The parametric family: temperature if the base rate is already right, Platt when the level moved too.
- Curvature, with events in the hundreds. Isotonic, CIR, or the spline. Prefer CIR or the spline where a cutoff or recourse consumes the output: step functions turn a decision boundary into a plateau edge that moves at every refit (see Set cutoffs).
- You need an interval, not a point. Venn–Abers:
predict_interval()carries a distribution-free guarantee under exchangeability; CVAP when the sample is too small to carve a calibration split. - The output feeds a masterscale or grade table. Histogram binning or
scaling-binning produce grade-shaped output natively, but check
is_monotone_before promising anyone a grade-to-score translation. - Unsure, the usual case. Hand the decision to
CalibratorSelector, which scores every candidate on out-of-fold data only and writes down the contest. On a small portfolio give it a restricted menu: every extra candidate spends selection power.
# s_cal, y_cal: held-out calibration scores, outcomes
from probcal import (
BetaCalibrator,
CalibratorSelector,
PlattCalibrator,
TemperatureCalibrator,
)
sel = CalibratorSelector(
candidates={"temperature": TemperatureCalibrator(),
"platt": PlattCalibrator(),
"beta": BetaCalibrator()},
scoring="log_loss",
cv=5,
).fit(s_cal, y_cal)
print(sel.best_name_)
print(sel.report_) # ranked table, fold sd, guardrail flags
print(sel.interpret()) # the winner's parameters, in words
Read the runner-up's margin in units of the printed fold standard
deviations: inside one, the contest was a tie and parsimony already
adjudicated it through complexity_rank.
What the choice commits you to¶
Picking a calibrator picks a downstream contract, which is why the inverse columns sit in the same table as the fit quality:
- Cutoffs, masterscale bands, and counterfactual targets all run through
the inverse: the how-to is Set cutoffs and invert
maps, the theory
Inverse maps. A masterscale is a
Masterscale, one object for assignment, translation, and the report. - Whatever you fit, its parameters, provenance, and reproduction are evidence you can hand to a validator: see Auditability and Build a validation report.
- After deployment the map is watched, not assumed: Monitor and act.