Getting started¶
Installation¶
pip install probcal # runtime dependency: numpy only
pip install "probcal[viz]" # + matplotlib for probcal.plots
To work on the package itself:
git clone https://github.com/wlazlod/probcal && cd probcal
uv sync --extra dev # tests, lint, type-check
probcal requires Python ≥ 3.11. The core import path touches nothing beyond numpy and the
standard library — scipy, scikit-learn, pandas, and matplotlib are never imported by
probcal itself (matplotlib only inside the optional probcal.plots).
Score-level quickstart¶
Calibrators work directly on scores — no model object required. The example uses the built-in synthetic PD portfolio (3% event rate, asymmetric tail distortion):
from probcal import BetaCalibrator, make_pd_portfolio
from probcal.metrics import calibration_guardrails
port = make_pd_portfolio(n=8000, random_state=42)
g_before = calibration_guardrails(port.y, port.scores)
print(f"before: slope={g_before.slope:.3f} intercept={g_before.intercept:+.3f} ok={g_before.all_ok}")
cal = BetaCalibrator().fit(port.scores, port.y)
p = cal.predict_proba(port.scores)
g_after = calibration_guardrails(port.y, p)
print(f"after: slope={g_after.slope:.3f} intercept={g_after.intercept:+.3f} ok={g_after.all_ok}")
print()
print(cal.interpret())
Output:
before: slope=0.968 intercept=-0.765 ok=False
after: slope=1.000 intercept=+0.000 ok=True
Interpretation[BetaCalibrator]
parameter value
--------- --------
a 0.875054
b 1.58922
c -1.15227
- a = 0.875: sensitivity near s -> 0; a < 1 raises the smallest probabilities (model was overconfident in the low tail), a > 1 deepens them
- b = 1.589: sensitivity near s -> 1; the mirrored reading for the high tail
- c = -1.152: base-rate shift of -1.152 log-odds, odds factor 0.316
- identity map corresponds to (a, b, c) = (1, 1, 0)
- a != b (gap -0.714): asymmetric tail distortion that no symmetric (Platt/temperature) map could express
Every calibrator follows the same contract: fit(s, y, sample_weight=None),
predict_proba(s), interpret(). Inputs are probabilities in (0, 1); if your model
emits raw logits, convert first with the exported probcal.expit.
Model-level wrapper¶
CalibratedModel wraps any object with predict_proba(X) or decision_function(X):
from probcal import CalibratedModel, PlattCalibrator
# Prefit flow: the model is already trained, a separate calibration set exists.
wrapped = CalibratedModel(model, PlattCalibrator(), flow="prefit").fit(X_cal, y_cal)
p = wrapped.predict_proba(X_new)
# CV flow: no calibration set to spare — clone/retrain per fold, pool out-of-fold scores.
wrapped = CalibratedModel(model, PlattCalibrator(), flow="cv", cv=5).fit(X_train, y_train)
# Central-tendency re-anchoring, kept as a separate auditable stage:
wrapped.offset_to(target_mean=0.031)
print(wrapped.offsets_[0].interpret())
Automatic selection¶
from probcal import CalibratorSelector
sel = CalibratorSelector().fit(s_cal, y_cal) # nested CV, log-loss criterion
print(sel.report_) # ranked table with guardrail flags
p = sel.predict_proba(s_new) # the refitted winner
The selector never scores a candidate on the data it was fitted on — see Automatic selection for the protocol and Data splitting for why the nesting is mandatory.
Where to go next¶
- The Concepts chapters are the package's theoretical foundation — method derivations, metric pathologies, and the selection rules.
- The tutorial notebook walks a full PD calibration cycle: diagnose, select, fit, re-anchor, backtest, and translate cutoffs back to raw scores.
- The FAQ covers the inverse-map protocol and interop with counterfactual engines.