Skip to content

optbinning scorecards

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.

probcal.integrations.optbinning (extra: pip install "probcal[optbinning]", optbinning ≥ 0.21) calibrates a fitted Scorecard without touching the deployed points, and translates calibrated PD policies to the points scale exactly. Scorecard.score is affine in the logistic regression's log-odds unless rounding=True, and the integration recovers and verifies that affine map to machine precision.

Scorecard → calibration → masterscale → monitoring, end to end:

# s_cal, y_cal, s_new: held-out calibration scores, outcomes, scores of new obligors
import pandas as pd
from optbinning import BinningProcess, Scorecard
from sklearn.linear_model import LogisticRegression

from probcal import Masterscale
from probcal._math import logit
from probcal.integrations.optbinning import calibrate_scorecard
from probcal.monitor import CalibrationMonitor

# X_train, y_train: training split; X_cal, y_cal: held-out calibration
# split; X_new: new applicants — built here from s_cal/s_new (one numeric
# feature, the model's own logit) since the walkthrough carries no raw
# tabular features.
X_train = pd.DataFrame({"z": logit(s_cal)})
y_train = y_cal.astype(int)
X_cal = X_train
X_new = pd.DataFrame({"z": logit(s_new)})

# 1. A fitted scorecard (train split).
sc = Scorecard(
    binning_process=BinningProcess(variable_names=list(X_train.columns)),
    estimator=LogisticRegression(),
    scaling_method="pdo_odds",
    scaling_method_params={"pdo": 20, "odds": 50, "scorecard_points": 600},
).fit(X_train, y_train)

# 2. Calibrate on held-out data. Points are untouched; predict_proba is
#    the calibrated PD; the fitted probcal object is one attribute away.
cs = calibrate_scorecard(sc, X_cal, y_cal)
print(cs.interpret())
pd_cal = cs.predict_proba(X_new)          # calibrated PD
points = cs.score(X_new)                  # unchanged deployed points

# 3. Masterscale: calibrated PD bands -> exact point cut-offs.
ms = Masterscale.from_edges([0.005, 0.02, 0.08], names=list("ABCD"))
print(cs.masterscale(ms))                 # cut-offs on the points scale
# -> {'A': (638.68, inf), 'B': (598.96, 638.68), ...}

# 4. Provenance: the JSON names both layers.
cs.to_json("scorecard-calibration.json")   # calibrator envelope +
cs.fingerprint()                           # scorecard-table fingerprint

# 5. Monitoring: matured cohorts through the anytime-valid monitor.
cohorts = {"m0": (X_cal.iloc[:1500], y_cal[:1500]), "m1": (X_cal.iloc[1500:], y_cal[1500:])}
mon = CalibrationMonitor(alpha=0.05)
for label, (X_b, y_b) in cohorts.items():
    mon.update(y_b, cs.predict_proba(X_b), label=label)
print(mon.report().recommendation)

With rounding=True the points are no longer affine in log-odds: calibrate_scorecard warns, points_affine_coeffs_ is None, and masterscale refuses. Use interval_inverse on the raw model probability instead (the exact escape hatch; no silent approximation).

Reattaching after a reload: persist the scorecard with optbinning's own save/load and the calibration layer as JSON, then CalibratedScorecard.from_json("scorecard-calibration.json", scorecard=sc) (or from_dict(d, scorecard=sc)). The stored scorecard-table fingerprint is checked, so a calibration layer can never be silently attached to a different scorecard. A scorecard whose probability is constant on the calibration data gets the same treatment as rounding=True: a warning, points_affine_coeffs_ = None, and a refusing masterscale.