Skip to content

API: integrations

Optional extras; each module imports its dependency only when used.

sklearn

scikit-learn adapter: probcal calibrators as sklearn estimators.

Requires the probcal[sklearn] extra; import probcal itself stays numpy-only — this subpackage is imported explicitly by its users.

SklearnCalibrator

SklearnCalibrator(calibrator: BaseCalibrator | None = None, *, input: str = 'probability', positive_column: int = 1)

Bases: BinaryCalibratedMixin, ClassifierMixin, TransformerMixin, BaseEstimator

Probability calibration over a single score column, sklearn-style.

Wraps any probcal calibrator as a scikit-learn classifier/transformer whose X is the score itself — shape (n,), (n, 1), or (in probability mode) a two-column predict_proba-style matrix. Use it to end a Pipeline (via :meth:transform) or anywhere an sklearn estimator is expected; the fitted probcal object stays one attribute away (calibrator_) with its full audit surface (interpret(), interval_inverse, to_dict, fingerprint()). The prototype passed as calibrator may also be a :class:~probcal.Chain. The calibrator protocol (is_monotone_, interval_inverse, point_inverse, affine_logit_coeffs_, interpret) and the JSON surface (to_dict, to_json, fingerprint) are delegated to calibrator_, exactly as on :class:CalibratedClassifier.

PARAMETER DESCRIPTION
calibrator

Unfitted probcal prototype, cloned via get_params at fit time; None uses BetaCalibrator().

TYPE: BaseCalibrator or None DEFAULT: None

input

Scale of the score column. "probability" requires values in [0, 1] (probcal's forward-entry convention) and additionally accepts a two-column probability matrix; "logit" stays single-column and accepts any reals, mapped through expit exactly first.

TYPE: (probability, logit) DEFAULT: "probability"

positive_column

Which column of a two-column probability matrix holds P(y=1) — 0 or 1 (default 1, matching predict_proba output). Ignored for single-column input.

TYPE: (int, keyword - only) DEFAULT: 1

ATTRIBUTE DESCRIPTION
calibrator_

The fitted probcal calibrator.

TYPE: BaseCalibrator

classes_

Class labels in numpy.unique order; column 1 of :meth:predict_proba is classes_[1].

TYPE: numpy.ndarray of shape (2,)

n_features_in_

1 or 2, depending on the X shape seen at fit; enforced at predict/transform time.

TYPE: int

Source code in src/probcal/sklearn/_calibrator.py
57
58
59
60
61
62
63
64
65
66
def __init__(
    self,
    calibrator: BaseCalibrator | None = None,
    *,
    input: str = "probability",
    positive_column: int = 1,
) -> None:
    self.calibrator = calibrator
    self.input = input
    self.positive_column = positive_column

fit

fit(X: object, y: object, sample_weight: object = None) -> SklearnCalibrator

Fit the wrapped calibrator on the score column.

PARAMETER DESCRIPTION
X

Scores (probabilities, or logits with input="logit"), or a two-column probability matrix (input="probability" only).

TYPE: array_like of shape (n,), (n, 1), or (n, 2)

y

Binary target; any two label values.

TYPE: array_like of shape (n,)

sample_weight

Non-negative observation weights; zero-weight rows are excluded (sklearn semantics).

TYPE: array_like or None DEFAULT: None

RETURNS DESCRIPTION
SklearnCalibrator

The fitted adapter.

RAISES DESCRIPTION
ValueError

If X's column count/input combination is unsupported, input or positive_column is invalid, y has more than two classes, or a weight is negative.

Source code in src/probcal/sklearn/_calibrator.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def fit(self, X: object, y: object, sample_weight: object = None) -> "SklearnCalibrator":
    """Fit the wrapped calibrator on the score column.

    Parameters
    ----------
    X : array_like of shape (n,), (n, 1), or (n, 2)
        Scores (probabilities, or logits with ``input="logit"``), or a
        two-column probability matrix (``input="probability"`` only).

    y : array_like of shape (n,)
        Binary target; any two label values.
    sample_weight : array_like or None
        Non-negative observation weights; zero-weight rows are excluded
        (sklearn semantics).

    Returns
    -------
    SklearnCalibrator
        The fitted adapter.

    Raises
    ------
    ValueError
        If ``X``'s column count/``input`` combination is unsupported,
        ``input`` or ``positive_column`` is invalid, ``y`` has more
        than two classes, or a weight is negative.
    """
    if self.input not in ("probability", "logit"):
        raise ValueError(f"input must be 'probability' or 'logit', got {self.input!r}")
    if self.positive_column not in (0, 1):
        raise ValueError(f"positive_column must be 0 or 1, got {self.positive_column!r}")
    X_arr, y_arr = validate_X_y(self, X, y, reset=True, allow_1d=True)
    sw = check_sample_weight(sample_weight, X_arr)
    y_bin = self._binary_target(y_arr)
    s = self._scores(X_arr)
    if s.ndim == 2:
        _, (col, y_kept) = drop_zero_weight(sw, s[:, 1], y_bin)
        both = np.unique(y_kept).size == 2
        if both and float(col[y_kept == 1.0].mean()) < float(col[y_kept == 0.0].mean()):
            warnings.warn(
                "the selected positive-probability column has a lower mean among "
                "events than among non-events; if the matrix is ordered the other "
                f"way, positive_column={1 - self.positive_column} is the likely fix",
                UserWarning,
                stacklevel=2,
            )
    self._fit_calibrator(s, y_bin, sw)
    return self

transform

transform(X: object) -> ndarray

Calibrated-probability column (n, 1) — lets the adapter end a Pipeline.

Source code in src/probcal/sklearn/_calibrator.py
138
139
140
def transform(self, X: object) -> np.ndarray:
    """Calibrated-probability column ``(n, 1)`` — lets the adapter end a Pipeline."""
    return self.predict_proba(X)[:, [1]]

CalibratedClassifier

CalibratedClassifier(estimator: object = None, *, calibrator: BaseCalibrator | None = None, cv: object = 5, method: str = 'predict_proba', stratify: bool = True, random_state: int | None = None)

Bases: BinaryCalibratedMixin, ClassifierMixin, BaseEstimator

Cross-validated probability calibration of a classifier, probcal-style.

The drop-in for sklearn.calibration.CalibratedClassifierCV with ensemble=False: out-of-fold scores via cross_val_predict, one probcal calibrator fitted on the pooled OOF scores, and the estimator refit on all data (unless cv="prefit"). What probcal adds on top: the fitted calibrator's audit surface (interpret(), bootstrap CIs via probcal.metrics.evaluate), exact inverse maps, JSON serialization, and fingerprints — see guide/sklearn.md.

Also exposes the probcal calibrator protocol (is_monotone_, interval_inverse, point_inverse, affine_logit_coeffs_, fingerprint) by delegation to calibrator_, so a fitted instance can be handed directly to consumers of that protocol (e.g. treecf's Target.calibrated).

X is passed to the estimator untouched (no float conversion), like CalibratedClassifierCV: DataFrames, mixed-dtype columns for a ColumnTransformer pipeline, and sparse matrices all work.

PARAMETER DESCRIPTION
estimator

Classifier to calibrate. None resolves to LogisticRegression(max_iter=1000) at fit time (the sklearn precedent for a default-constructible wrapper).

TYPE: object or None DEFAULT: None

calibrator

Unfitted probcal prototype (cloned via get_params); None uses BetaCalibrator().

TYPE: (BaseCalibrator or None, keyword - only) DEFAULT: None

cv

An integer >= 2 is a fold count (see stratify); a splitter or an iterable of (train, test) splits is used as given (through sklearn.model_selection.check_cv; it must partition the rows, as cross_val_predict requires). "prefit" scores the calibration set with the already-fitted estimator directly.

TYPE: int, cross-validation splitter, iterable, or "prefit", keyword-only DEFAULT: 5

method

Score source. "decision_function" margins are mapped through expit before calibration — the calibrator then absorbs any monotone distortion this introduces.

TYPE: (predict_proba, decision_function) DEFAULT: "predict_proba"

stratify

For an integer cv only: True uses StratifiedKFold(cv, shuffle=True, random_state=random_state) (recommended for rare events), False uses KFold(cv, shuffle=True, random_state=random_state). Ignored for a splitter cv.

TYPE: (bool, keyword - only) DEFAULT: True

random_state

Fold-shuffling seed for an integer cv.

TYPE: (int or None, keyword - only) DEFAULT: None

ATTRIBUTE DESCRIPTION
estimator_

The deployed classifier (input estimator for cv="prefit", full-data refit otherwise).

TYPE: object

calibrator_

The fitted probcal calibrator (one map, pooled OOF scores).

TYPE: BaseCalibrator

classes_

Class labels; column 1 of :meth:predict_proba is classes_[1].

TYPE: numpy.ndarray of shape (2,)

n_features_in_

Copied from estimator_ when it defines one.

TYPE: int

Notes

:class:probcal.CalibratedModel (flow="cv") runs the same out-of-fold protocol without sklearn, with deliberate differences: its folds are always class-stratified and drawn with numpy's default_rng(random_state) (so the same seed assigns different folds than StratifiedKFold), and it alone offers ensemble=True (one calibrator per fold). Both pass X to the model untouched and hand sample_weight to the model fits when the estimator accepts it. Use this class inside sklearn pipelines and searches, CalibratedModel for a numpy-only deployment wrapper.

Source code in src/probcal/sklearn/_classifier.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def __init__(
    self,
    estimator: object = None,
    *,
    calibrator: BaseCalibrator | None = None,
    cv: object = 5,
    method: str = "predict_proba",
    stratify: bool = True,
    random_state: int | None = None,
) -> None:
    self.estimator = estimator
    self.calibrator = calibrator
    self.cv = cv
    self.method = method
    self.stratify = stratify
    self.random_state = random_state

fit

fit(X: object, y: object, sample_weight: object = None) -> CalibratedClassifier

Fit per the out-of-fold protocol (or score directly when prefit).

PARAMETER DESCRIPTION
X

Features, passed to the wrapped estimator untouched.

TYPE: array_like, DataFrame, or sparse matrix

y

Binary target; any two label values.

TYPE: array_like of shape (n,)

sample_weight

Non-negative observation weights (zero excludes a row from the calibrator fit). Always used for the calibrator stage; also handed to the cross-validated fits and the refit when the estimator can take them. When it cannot, a UserWarning names it and those fits run unweighted.

TYPE: array_like or None DEFAULT: None

RETURNS DESCRIPTION
CalibratedClassifier

The fitted wrapper.

RAISES DESCRIPTION
ValueError

If method or cv is invalid, y has more than two classes, a weight is negative, or (cv="prefit") the estimator's classes_ differ from the labels in y.

WARNS DESCRIPTION
UserWarning

If sample_weight is given and the estimator's fit cannot consume it — see guide/sklearn.md.

Source code in src/probcal/sklearn/_classifier.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
def fit(self, X: object, y: object, sample_weight: object = None) -> "CalibratedClassifier":
    """Fit per the out-of-fold protocol (or score directly when prefit).

    Parameters
    ----------
    X : array_like, DataFrame, or sparse matrix
        Features, passed to the wrapped estimator untouched.
    y : array_like of shape (n,)
        Binary target; any two label values.
    sample_weight : array_like or None
        Non-negative observation weights (zero excludes a row from the
        calibrator fit). Always used for the calibrator stage; also
        handed to the cross-validated fits and the refit when the
        estimator can take them. When it cannot, a ``UserWarning``
        names it and those fits run unweighted.

    Returns
    -------
    CalibratedClassifier
        The fitted wrapper.

    Raises
    ------
    ValueError
        If ``method`` or ``cv`` is invalid, ``y`` has more than two
        classes, a weight is negative, or (``cv="prefit"``) the
        estimator's ``classes_`` differ from the labels in ``y``.

    Warns
    -----
    UserWarning
        If ``sample_weight`` is given and the estimator's ``fit`` cannot
        consume it — see ``guide/sklearn.md``.
    """
    if self.method not in ("predict_proba", "decision_function"):
        raise ValueError(
            f"method must be 'predict_proba' or 'decision_function', got {self.method!r}"
        )
    X, y = indexable(X, y)
    y_arr = column_or_1d(y, warn=True)
    check_consistent_length(X, y_arr)
    if _num_samples(X) == 0:
        raise ValueError("Found array with 0 sample(s); a minimum of 1 is required.")
    sw = check_sample_weight(sample_weight, X)
    y_bin = self._binary_target(y_arr)

    if self._is_prefit():
        check_is_fitted(self.estimator)
        est_classes = getattr(self.estimator, "classes_", None)
        if est_classes is not None and not np.array_equal(
            np.asarray(est_classes), self.classes_
        ):
            raise ValueError(
                f"the prefit estimator was trained on classes {list(est_classes)}, but y "
                f"has classes {list(self.classes_)}; its score columns would be "
                "misread as P(classes_[1])"
            )
        self.estimator_ = self.estimator
        oof = self._estimator_scores(self.estimator_, X)
    else:
        base = self.estimator if self.estimator is not None else self._default_estimator()
        splitter = self._splitter(y_arr)
        fit_kw = {} if sw is None else {"sample_weight": sw}
        if sw is not None and not _accepts_sample_weight(base):
            fit_kw = {}
            warnings.warn(
                f"{type(base).__name__}.fit does not accept sample_weight: the "
                "cross-validated fits and the full-data refit are unweighted, "
                "while the calibrator is fitted with the weights. The calibration "
                "map is still weighted, but the scores it calibrates are not.",
                UserWarning,
                stacklevel=2,
            )
        raw = cross_val_predict(
            clone(base), X, y_arr, cv=splitter, method=self.method, params=fit_kw or None
        )
        oof = self._to_scores(np.asarray(raw))
        refit = clone(base)
        refit.fit(X, y_arr, **fit_kw)
        self.estimator_ = refit

    for attr in ("n_features_in_", "feature_names_in_"):
        if hasattr(self.estimator_, attr):
            setattr(self, attr, getattr(self.estimator_, attr))
    self._fit_calibrator(oof, y_bin, sw)
    return self

SklearnOffset

SklearnOffset(delta: float | None = None, target_mean: float | None = None, *, positive_column: int = 1)

Bases: TransformerMixin, BaseEstimator

A logit offset over a probability column, sklearn-style.

Wraps :class:~probcal.offset.LogitOffset as an sklearn transformer whose X is the probability itself — shape (n,), (n, 1), or a two-column predict_proba-style matrix — so it can end (or sit inside) a Pipeline right after a :class:~probcal.sklearn.SklearnCalibrator step. The offset is deliberately a separate pipeline step rather than a parameter folded into the calibrator: it keeps the central-tendency re-anchoring inspectable and swappable on its own, exactly as :class:~probcal.chain.Chain keeps it a separate stage. No y is consumed (LogitOffset ignores it), so there is no orientation check to run — the column-1 convention for two-column input is documented (guide/sklearn.md), not checked.

PARAMETER DESCRIPTION
delta

Mode A: the log-odds shift to apply directly. Mutually exclusive with target_mean.

TYPE: float or None DEFAULT: None

target_mean

Mode B: the desired post-shift portfolio mean probability, solved by bisection. Mutually exclusive with delta.

TYPE: float or None DEFAULT: None

positive_column

Which column of a two-column probability matrix holds P(y=1) — 0 or 1 (default 1, matching predict_proba output). Ignored for single-column input.

TYPE: (int, keyword - only) DEFAULT: 1

ATTRIBUTE DESCRIPTION
offset_

The fitted inner offset.

TYPE: LogitOffset

n_features_in_

1 or 2, depending on the X shape seen at fit; enforced at predict/transform time.

TYPE: int

Source code in src/probcal/sklearn/_offset.py
49
50
51
52
53
54
55
56
57
58
def __init__(
    self,
    delta: float | None = None,
    target_mean: float | None = None,
    *,
    positive_column: int = 1,
) -> None:
    self.delta = delta
    self.target_mean = target_mean
    self.positive_column = positive_column

fit

fit(X: object, y: object = None, sample_weight: object = None) -> SklearnOffset

Fit the wrapped offset on the probability column.

PARAMETER DESCRIPTION
X

Probabilities, or a two-column probability matrix.

TYPE: array_like of shape (n,), (n, 1), or (n, 2)

y

Ignored; accepted for pipeline/estimator compatibility.

TYPE: array_like or None DEFAULT: None

sample_weight

Non-negative observation weights; zero-weight rows are excluded (sklearn semantics), a negative weight raises.

TYPE: array_like or None DEFAULT: None

RETURNS DESCRIPTION
SklearnOffset

The fitted adapter.

RAISES DESCRIPTION
ValueError

If X's column count is unsupported, positive_column is invalid, or a weight is negative.

Source code in src/probcal/sklearn/_offset.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def fit(self, X: object, y: object = None, sample_weight: object = None) -> "SklearnOffset":
    """Fit the wrapped offset on the probability column.

    Parameters
    ----------
    X : array_like of shape (n,), (n, 1), or (n, 2)
        Probabilities, or a two-column probability matrix.
    y : array_like or None
        Ignored; accepted for pipeline/estimator compatibility.
    sample_weight : array_like or None
        Non-negative observation weights; zero-weight rows are excluded
        (sklearn semantics), a negative weight raises.

    Returns
    -------
    SklearnOffset
        The fitted adapter.

    Raises
    ------
    ValueError
        If ``X``'s column count is unsupported, ``positive_column``
        is invalid, or a weight is negative.
    """
    if self.positive_column not in (0, 1):
        raise ValueError(f"positive_column must be 0 or 1, got {self.positive_column!r}")
    X_arr = validate_X(self, X, reset=True, allow_1d=True)
    sw, (p,) = drop_zero_weight(check_sample_weight(sample_weight, X_arr), self._probs(X_arr))
    self.offset_ = LogitOffset(delta=self.delta, target_mean=self.target_mean)
    self.offset_.fit(p, sample_weight=sw, y=y)
    return self

transform

transform(X: object) -> ndarray

Shifted probability column (n, 1) — lets the adapter end a Pipeline.

Source code in src/probcal/sklearn/_offset.py
107
108
109
def transform(self, X: object) -> np.ndarray:
    """Shifted probability column ``(n, 1)`` — lets the adapter end a Pipeline."""
    return self.predict_proba(X)[:, [1]]

predict_proba

predict_proba(X: object) -> ndarray

(n, 2) matrix [1 - p_shifted, p_shifted].

Source code in src/probcal/sklearn/_offset.py
111
112
113
114
115
116
def predict_proba(self, X: object) -> np.ndarray:
    """``(n, 2)`` matrix ``[1 - p_shifted, p_shifted]``."""
    check_is_fitted(self, "offset_")
    X_arr = validate_X(self, X, allow_1d=True)
    p = self.offset_.transform(self._probs(X_arr))
    return np.column_stack([1.0 - p, p])

to_dict

to_dict() -> dict[str, object]

The fitted inner offset's own envelope (loads back as a LogitOffset).

Source code in src/probcal/sklearn/_offset.py
118
119
120
121
def to_dict(self) -> dict[str, object]:
    """The fitted inner offset's own envelope (loads back as a LogitOffset)."""
    check_is_fitted(self, "offset_")
    return self.offset_.to_dict()

CalibratedScorecard serialization and scope

CalibratedScorecard (from calibrate_scorecard) serializes its calibration layer with to_dict/to_json and checks provenance with fingerprint(), like every probcal artifact. The scorecard itself is optbinning's object and is not serialized, so loading needs it back: CalibratedScorecard.from_json(path_or_str, scorecard=sc) (or from_dict(d, scorecard=sc)). For the same reason the class is deliberately not in the serialization registry that class-name dispatch such as BaseCalibrator.from_dict(d) goes through (see Serialization): a payload alone could not rebuild it without the scorecard object.

The stored scorecard fingerprint is SHA-256 of a canonical JSON rendering of table(style="detailed") (floats to 12 significant digits), so it does not depend on pandas' formatting. A mismatch raises ValueError.

If the scorecard's probability is constant on the calibration data, the points-to-log-odds map is unidentifiable: calibrate_scorecard warns, points_affine_coeffs_ is None, and masterscale() refuses, exactly as for rounding=True. predict_proba returns a 1-D array of calibrated PDs (the probcal convention, so it feeds probcal.metrics directly); a scorecard is not an sklearn estimator, and probcal.sklearn.CalibratedClassifier is the route when sklearn's (n, 2) semantics are needed.

optbinning

Calibrated optbinning scorecards.

Requires the probcal[optbinning] extra (optbinning >= 0.21). The scorecard stays the deployed artifact — points are untouched; probcal adds a calibrated PD layer on top plus exact translation between calibrated PD bands and point cut-offs, possible because Scorecard.score is affine in the fitted logistic regression's log-odds unless rounding=True.

CalibratedScorecard

CalibratedScorecard(scorecard: object, calibrator: BaseCalibrator, points_affine_coeffs: tuple[float, float] | None)

Bases: JsonIO, CalibratorProtocolMixin

An optbinning Scorecard with a probcal calibration layer on top.

Built by :func:calibrate_scorecard. Points (score) are unchanged — the scorecard remains the deployed artifact; predict_proba returns the calibrated PD, and the calibrator protocol (interval_inverse, point_inverse, ...) operates on the scorecard's model-probability scale, so calibrated policies translate to raw probabilities and — via points_affine_coeffs_ — exactly to the points scale.

predict_proba returns the 1-D calibrated PD (the probcal calibrator convention), not sklearn's (n, 2) matrix: a scorecard is not an sklearn estimator, and the 1-D form feeds probcal.metrics directly. Persist with :meth:to_json and reload with CalibratedScorecard.from_json(path, scorecard=...); the class is not in the generic probcal load registry because a payload cannot be rebuilt without the scorecard object, which optbinning persists itself.

ATTRIBUTE DESCRIPTION
scorecard_

The wrapped, fitted scorecard.

TYPE: Scorecard

calibrator_

The fitted probcal calibrator over the scorecard's probabilities.

TYPE: BaseCalibrator

points_affine_coeffs_

score = A + B * logit(p_model), recovered from the calibration data and verified to machine precision; None (with a warning at build time) when the relation is not affine (rounding=True).

TYPE: tuple(A, B) or None

Source code in src/probcal/integrations/optbinning.py
 96
 97
 98
 99
100
101
102
103
104
def __init__(
    self,
    scorecard: object,
    calibrator: BaseCalibrator,
    points_affine_coeffs: tuple[float, float] | None,
) -> None:
    self.scorecard_ = scorecard
    self.calibrator_ = calibrator
    self.points_affine_coeffs_ = points_affine_coeffs

predict_proba

predict_proba(X: object) -> ndarray

Calibrated PD for scorecard inputs (1-D, probcal convention).

Source code in src/probcal/integrations/optbinning.py
111
112
113
def predict_proba(self, X: object) -> np.ndarray:
    """Calibrated PD for scorecard inputs (1-D, probcal convention)."""
    return self.calibrator_.predict_proba(self._model_proba(X))

score

score(X: object) -> ndarray

Unchanged scorecard points — the deployed artifact is untouched.

Source code in src/probcal/integrations/optbinning.py
115
116
117
def score(self, X: object) -> np.ndarray:
    """Unchanged scorecard points — the deployed artifact is untouched."""
    return np.asarray(self.scorecard_.score(X))  # type: ignore[attr-defined]

masterscale

masterscale(bands: object) -> dict[str, tuple[float, float]]

Calibrated PD bands -> scorecard point cut-offs, exactly.

Composes :func:probcal.thresholds.calibrated_bands_to_raw (bands on the calibrated scale to raw log-odds intervals) with the verified affine points map. Point intervals are returned as (lo, hi) with lo <= hi (the affine slope is negative for the usual higher-points-safer scaling), and the cut-offs are checked for monotone ordering across bands.

PARAMETER DESCRIPTION
bands

Calibrated PD bands, e.g. {"A": (0.0, 0.01), "B": (0.01, 0.05)}, or a :class:probcal.Masterscale (its bands are read).

TYPE: dict[str, tuple(lo, hi)] or Masterscale

RETURNS DESCRIPTION
dict[str, tuple(points_lo, points_hi)]

Point cut-offs per band, as plain floats (inf for an open end).

RAISES DESCRIPTION
RuntimeError

If the scorecard is not affine in log-odds (rounding=True) — use :meth:interval_inverse on the raw probability instead — or if the resulting cut-offs are not monotone across bands.

Source code in src/probcal/integrations/optbinning.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def masterscale(self, bands: object) -> dict[str, tuple[float, float]]:
    """Calibrated PD bands -> scorecard point cut-offs, exactly.

    Composes :func:`probcal.thresholds.calibrated_bands_to_raw` (bands on
    the calibrated scale to raw log-odds intervals) with the verified
    affine points map. Point intervals are returned as ``(lo, hi)`` with
    ``lo <= hi`` (the affine slope is negative for the usual
    higher-points-safer scaling), and the cut-offs are checked for
    monotone ordering across bands.

    Parameters
    ----------
    bands : dict[str, tuple(lo, hi)] or Masterscale
        Calibrated PD bands, e.g. ``{"A": (0.0, 0.01), "B": (0.01, 0.05)}``,
        or a :class:`probcal.Masterscale` (its ``bands`` are read).

    Returns
    -------
    dict[str, tuple(points_lo, points_hi)]
        Point cut-offs per band, as plain floats (``inf`` for an open end).

    Raises
    ------
    RuntimeError
        If the scorecard is not affine in log-odds (``rounding=True``) —
        use :meth:`interval_inverse` on the raw probability instead —
        or if the resulting cut-offs are not monotone across bands.
    """
    if self.points_affine_coeffs_ is None:
        raise RuntimeError(
            "this scorecard is not affine in log-odds (rounding=True); the exact "
            "masterscale is unavailable — use interval_inverse on the raw "
            "probability instead"
        )
    a_pts, b_pts = self.points_affine_coeffs_
    bands_d: dict[str, tuple[float, float]] = (
        bands.bands if isinstance(bands, Masterscale) else bands  # type: ignore[assignment]
    )
    raw = calibrated_bands_to_raw(self.calibrator_, bands_d, space="logit")
    out: dict[str, tuple[float, float]] = {}
    for name, (z_lo, z_hi) in raw.items():
        p_lo = a_pts + b_pts * z_lo if np.isfinite(z_lo) else np.inf * -np.sign(b_pts)
        p_hi = a_pts + b_pts * z_hi if np.isfinite(z_hi) else np.inf * np.sign(b_pts)
        out[name] = (float(min(p_lo, p_hi)), float(max(p_lo, p_hi)))
    # Bands ordered by rising calibrated PD must map to monotone point
    # ranges (falling when B < 0, i.e. higher points = safer).
    order = sorted(out, key=lambda k: bands_d[k][0])
    cuts = [out[k] for k in order]
    if b_pts < 0:
        mono = all(cuts[i][1] >= cuts[i + 1][1] - 1e-9 for i in range(len(cuts) - 1))
    else:
        mono = all(cuts[i][0] <= cuts[i + 1][0] + 1e-9 for i in range(len(cuts) - 1))
    if not mono:
        raise RuntimeError("masterscale cut-offs are not monotone across bands")
    return out

to_dict

to_dict() -> dict[str, object]

Calibrator envelope plus a fingerprint of the scorecard table.

The scorecard object itself is not serialized (it is optbinning's artifact); rebuild with CalibratedScorecard.from_dict(d, scorecard=...) (or from_json(path, scorecard=...)) after loading the scorecard through optbinning's own save/load.

Source code in src/probcal/integrations/optbinning.py
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
def to_dict(self) -> dict[str, object]:
    """Calibrator envelope plus a fingerprint of the scorecard table.

    The scorecard object itself is not serialized (it is optbinning's
    artifact); rebuild with ``CalibratedScorecard.from_dict(d,
    scorecard=...)`` (or ``from_json(path, scorecard=...)``) after
    loading the scorecard through optbinning's own ``save``/``load``.
    """
    return envelope(
        self,
        params={},
        state={
            "calibrator": self.calibrator_.to_dict(),
            "points_affine_coeffs": (
                list(self.points_affine_coeffs_)
                if self.points_affine_coeffs_ is not None
                else None
            ),
            "scorecard_fingerprint": self.scorecard_fingerprint(),
        },
        fit_meta={},
    )

from_dict classmethod

from_dict(d: dict, scorecard: object) -> CalibratedScorecard

Rebuild around a scorecard loaded through optbinning's own tooling.

The stored scorecard-table fingerprint must match scorecard.

RAISES DESCRIPTION
ValueError

If the payload class differs, or the supplied scorecard's table fingerprint does not match the stored one.

Source code in src/probcal/integrations/optbinning.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
@classmethod
def from_dict(cls, d: dict, scorecard: object) -> "CalibratedScorecard":
    """Rebuild around a scorecard loaded through optbinning's own tooling.

    The stored scorecard-table fingerprint must match ``scorecard``.

    Raises
    ------
    ValueError
        If the payload class differs, or the supplied scorecard's table
        fingerprint does not match the stored one.
    """
    from .._registry import load

    check_payload(cls, d)
    state = d["state"]
    coeffs = state["points_affine_coeffs"]
    obj = cls(
        scorecard,
        load(state["calibrator"]),  # type: ignore[arg-type]
        tuple(coeffs) if coeffs is not None else None,
    )
    if obj.scorecard_fingerprint() != state["scorecard_fingerprint"]:
        raise ValueError(
            "the supplied scorecard's table fingerprint does not match the stored "
            "one — this calibration layer was fitted against a different scorecard"
        )
    return obj

scorecard_fingerprint

scorecard_fingerprint() -> str

SHA-256 of the scorecard table — names the deployed artifact.

Computed over a canonical JSON form of the detailed table (column names, and every cell with floats kept to 12 significant digits), so it does not depend on the pandas version's CSV formatting.

Source code in src/probcal/integrations/optbinning.py
233
234
235
236
237
238
239
240
def scorecard_fingerprint(self) -> str:
    """SHA-256 of the scorecard table — names the deployed artifact.

    Computed over a canonical JSON form of the detailed table (column
    names, and every cell with floats kept to 12 significant digits),
    so it does not depend on the pandas version's CSV formatting.
    """
    return _table_fingerprint(self.scorecard_.table(style="detailed"))  # type: ignore[attr-defined]

calibrate_scorecard

calibrate_scorecard(scorecard: object, X_cal: object, y_cal: object, *, calibrator: BaseCalibrator | None = None, sample_weight: object = None) -> CalibratedScorecard

Fit a probcal calibration layer on a fitted optbinning scorecard.

PARAMETER DESCRIPTION
scorecard

Fitted scorecard with predict_proba, score, and table.

TYPE: Scorecard

X_cal

Held-out calibration data (never the scorecard's training data — see the data-splitting chapter).

TYPE: array_like

y_cal

Held-out calibration data (never the scorecard's training data — see the data-splitting chapter).

TYPE: array_like

calibrator

Unfitted probcal prototype; None uses BetaCalibrator().

TYPE: (BaseCalibrator or None, keyword - only) DEFAULT: None

sample_weight

Positive observation weights for the calibration fit.

TYPE: (array_like or None, keyword - only) DEFAULT: None

WARNS DESCRIPTION
UserWarning

When the points are not affine in log-odds (rounding=True), or the scorecard's probability is constant on X_cal (the points map is then unidentifiable); points_affine_coeffs_ is None in both cases.

RETURNS DESCRIPTION
CalibratedScorecard

Calibrated PD layer over the unchanged scorecard, with the affine points map recovered and verified (or refused with a warning when rounding=True breaks affinity).

Source code in src/probcal/integrations/optbinning.py
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
def calibrate_scorecard(
    scorecard: object,
    X_cal: object,
    y_cal: object,
    *,
    calibrator: BaseCalibrator | None = None,
    sample_weight: object = None,
) -> CalibratedScorecard:
    """Fit a probcal calibration layer on a fitted optbinning scorecard.

    Parameters
    ----------
    scorecard : optbinning.Scorecard
        Fitted scorecard with ``predict_proba``, ``score``, and ``table``.
    X_cal, y_cal : array_like
        Held-out calibration data (never the scorecard's training data —
        see the data-splitting chapter).
    calibrator : BaseCalibrator or None, keyword-only
        Unfitted probcal prototype; ``None`` uses ``BetaCalibrator()``.
    sample_weight : array_like or None, keyword-only
        Positive observation weights for the calibration fit.

    Warns
    -----
    UserWarning
        When the points are not affine in log-odds (``rounding=True``), or
        the scorecard's probability is constant on ``X_cal`` (the points map
        is then unidentifiable); ``points_affine_coeffs_`` is ``None`` in
        both cases.

    Returns
    -------
    CalibratedScorecard
        Calibrated PD layer over the unchanged scorecard, with the affine
        points map recovered and verified (or refused with a warning when
        ``rounding=True`` breaks affinity).
    """
    proto = calibrator if calibrator is not None else BetaCalibrator()
    cal = cast(BaseCalibrator, clone_unfitted(proto))
    p_model = np.asarray(scorecard.predict_proba(X_cal))[:, 1]  # type: ignore[attr-defined]
    cal.fit(p_model, y_cal, sample_weight=sample_weight)

    points = np.asarray(scorecard.score(X_cal), dtype=np.float64)  # type: ignore[attr-defined]
    z = logit(p_model)
    coeffs: tuple[float, float] | None = None
    if np.ptp(z) == 0.0:
        # One distinct log-odds value: the points map is unidentifiable (any
        # slope through the single point fits), so no masterscale is offered.
        warnings.warn(
            "the scorecard assigns one probability to every calibration row; the "
            "points-to-log-odds map is unidentifiable and masterscale is unavailable",
            UserWarning,
            stacklevel=2,
        )
        return CalibratedScorecard(scorecard, cal, coeffs)
    b_pts, a_pts = np.polyfit(z, points, 1)
    resid = float(np.max(np.abs(points - (a_pts + b_pts * z))))
    coeffs = (float(a_pts), float(b_pts))
    if resid > _AFFINE_ATOL:
        warnings.warn(
            f"scorecard points are not affine in log-odds (max residual {resid:.3g}, "
            "e.g. rounding=True); masterscale is unavailable — falling back to "
            "interval_inverse on the raw probability",
            UserWarning,
            stacklevel=2,
        )
        coeffs = None
    return CalibratedScorecard(scorecard, cal, coeffs)