Skip to content

API: calibrators

base

BaseCalibrator: the common fit / predict_proba / interpret contract.

UnattainableTargetError

Bases: ValueError

The requested calibrated target is unattainable.

Raised instead of silently clamping when an interval does not intersect the calibrator's output range (or was emptied by buffer_logit), when a point-inverse target lies outside the open interval (0, 1), or when a probability-space point-inverse result would round to 0.0/1.0 (raw logit beyond logit(1 - 1e-12)).

BaseCalibrator

Bases: ABC

Common contract for all probcal calibrators.

Subclasses implement _fit (estimation on validated arrays), _predict (the fitted map on clipped scores), and interpret. Everything else — validation, sklearn-style parameter handling without an sklearn import, the 2-D probability helper — lives here.

ATTRIBUTE DESCRIPTION
is_monotone_

Whether the fitted map is guaranteed non-decreasing. Class-level default True; non-monotone calibrators (e.g. ENIR) override it.

TYPE: bool

fitted_

Set by :meth:fit.

TYPE: bool

affine_logit_coeffs_ property

affine_logit_coeffs_: tuple[float, float] | None

Coefficients (a, b) of logit g(s) = a * logit(s) + b, if affine.

None for calibrators that are not affine on the logit scale. Consumed by the attribution adjustment.

complexity_rank property

complexity_rank: float

Parsimony rank for selector tie-breaks; lower wins a tie.

Default 100.0 means "unknown — override in subclasses". Custom calibrators declare their place in the tie-break by overriding this property.

fit

fit(s: object, y: object, sample_weight: object = None) -> Self

Fit the calibration map on scores and binary outcomes.

PARAMETER DESCRIPTION
s

Raw scores/probabilities in [0, 1] (clipped to [1e-12, 1 - 1e-12]). Users holding raw logits convert with :func:probcal.expit first.

TYPE: array_like

y

Binary outcomes in {0, 1}; both classes must be present.

TYPE: array_like

sample_weight

Positive observation weights.

TYPE: array_like or None DEFAULT: None

RETURNS DESCRIPTION
Self

The fitted calibrator.

Source code in src/probcal/base.py
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
133
134
135
136
137
138
139
140
def fit(self, s: object, y: object, sample_weight: object = None) -> Self:
    """Fit the calibration map on scores and binary outcomes.

    Parameters
    ----------
    s : array_like
        Raw scores/probabilities in ``[0, 1]`` (clipped to
        ``[1e-12, 1 - 1e-12]``). Users holding raw logits convert with
        :func:`probcal.expit` first.
    y : array_like
        Binary outcomes in ``{0, 1}``; both classes must be present.
    sample_weight : array_like or None
        Positive observation weights.

    Returns
    -------
    Self
        The fitted calibrator.
    """
    s_arr = validate_scores(s)
    y_arr = validate_binary_y(y)
    if s_arr.shape[0] != y_arr.shape[0]:
        raise ValueError(
            f"s and y must have equal length, got {s_arr.shape[0]} and {y_arr.shape[0]}"
        )
    w_arr = validate_weights(sample_weight, s_arr.shape[0])
    self._fit(s_arr, y_arr, w_arr)
    self.fit_meta_ = {
        "n_obs": int(s_arr.shape[0]),
        "n_events": float(np.sum(w_arr * y_arr)),
        "weight_sum": float(w_arr.sum()),
        "fitted_at_utc": datetime.now(UTC).isoformat(timespec="seconds"),
        "data_fingerprint": data_fingerprint(s_arr, y_arr, w_arr),
    }
    self.fitted_ = True
    return self

predict_proba

predict_proba(s: object) -> ndarray

Calibrated probabilities P(y = 1) for new scores.

PARAMETER DESCRIPTION
s

Raw scores/probabilities in [0, 1].

TYPE: array_like

RETURNS DESCRIPTION
numpy.ndarray of shape (n,)

Calibrated probabilities.

Source code in src/probcal/base.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def predict_proba(self, s: object) -> np.ndarray:
    """Calibrated probabilities ``P(y = 1)`` for new scores.

    Parameters
    ----------
    s : array_like
        Raw scores/probabilities in ``[0, 1]``.

    Returns
    -------
    numpy.ndarray of shape (n,)
        Calibrated probabilities.
    """
    self._check_fitted()
    return self._predict(validate_scores(s))

predict_proba_2d

predict_proba_2d(s: object) -> ndarray

Sklearn-style (n, 2) probability matrix [P(y=0), P(y=1)].

Source code in src/probcal/base.py
164
165
166
167
def predict_proba_2d(self, s: object) -> np.ndarray:
    """Sklearn-style ``(n, 2)`` probability matrix ``[P(y=0), P(y=1)]``."""
    p = self.predict_proba(s)
    return np.column_stack([1.0 - p, p])

__sklearn_is_fitted__

__sklearn_is_fitted__() -> bool

Fitted state for sklearn's check_is_fitted (sklearn >= 1.6).

RETURNS DESCRIPTION
bool

True once :meth:fit has run.

Source code in src/probcal/base.py
179
180
181
182
183
184
185
186
187
def __sklearn_is_fitted__(self) -> bool:
    """Fitted state for sklearn's ``check_is_fitted`` (sklearn >= 1.6).

    Returns
    -------
    bool
        ``True`` once :meth:`fit` has run.
    """
    return bool(self.fitted_)

__sklearn_tags__

__sklearn_tags__() -> Tags

Estimator tags for sklearn >= 1.6, built from the public constructors.

sklearn is imported inside the body, never at module or class level: import probcal stays numpy-only and this hook costs nothing until sklearn itself calls it. Only the fields that are actually true of a calibrator are set — it takes 1-D scores, not a 2-D feature matrix, it is neither a classifier nor a regressor, and it must be fitted.

RETURNS DESCRIPTION
Tags

The tag object sklearn's get_tags expects.

RAISES DESCRIPTION
ImportError

If sklearn is not installed (only reachable by calling the hook by hand).

Source code in src/probcal/base.py
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
def __sklearn_tags__(self) -> "Tags":
    """Estimator tags for sklearn >= 1.6, built from the public constructors.

    sklearn is imported inside the body, never at module or class level:
    ``import probcal`` stays numpy-only and this hook costs nothing until
    sklearn itself calls it. Only the fields that are actually true of a
    calibrator are set — it takes 1-D scores, not a 2-D feature matrix,
    it is neither a classifier nor a regressor, and it must be fitted.

    Returns
    -------
    sklearn.utils.Tags
        The tag object sklearn's ``get_tags`` expects.

    Raises
    ------
    ImportError
        If sklearn is not installed (only reachable by calling the hook
        by hand).
    """
    from sklearn.utils import InputTags, Tags, TargetTags

    return Tags(
        estimator_type=None,
        target_tags=TargetTags(required=True),
        requires_fit=True,
        input_tags=InputTags(two_d_array=False),
    )

interpret abstractmethod

interpret() -> Interpretation

Fitted parameters with a plain-language, domain-aware reading.

Source code in src/probcal/base.py
220
221
222
@abstractmethod
def interpret(self) -> Interpretation:
    """Fitted parameters with a plain-language, domain-aware reading."""

interval_inverse

interval_inverse(lo: float, hi: float, *, space: str = 'probability', buffer_logit: float = 0.0) -> tuple[float, float]

Generalized-inverse preimage (raw_lo, raw_hi) of a calibrated interval.

For a non-decreasing fitted map g: raw_lo = inf{s : g(s) >= lo} and raw_hi = sup{s : g(s) <= hi}.

PARAMETER DESCRIPTION
lo

Calibrated-probability bounds; lo=0/hi=1 map to the full raw range (−inf/+inf on the logit scale).

TYPE: float

hi

Calibrated-probability bounds; lo=0/hi=1 map to the full raw range (−inf/+inf on the logit scale).

TYPE: float

space

Scale of the returned raw bounds. "logit" is what a SIGMOID-link raw-margin consumer (e.g. a counterfactual engine's Target.raw) expects.

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

buffer_logit

Shrink the calibrated interval by this margin in logit space before inverting — robustness against future recalibration drift (a central-tendency update of magnitude <= buffer cannot invalidate the result).

TYPE: float DEFAULT: 0.0

RETURNS DESCRIPTION
tuple of float

(raw_lo, raw_hi) preimage bounds, on the scale requested by space.

RAISES DESCRIPTION
UnattainableTargetError

If the (buffered) interval does not intersect the output range — never silently clamped.

NotImplementedError

For non-monotone calibrators (is_monotone_ = False), whose preimage may be a union of intervals.

Source code in src/probcal/base.py
242
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
311
312
313
314
315
316
317
318
def interval_inverse(
    self,
    lo: float,
    hi: float,
    *,
    space: str = "probability",
    buffer_logit: float = 0.0,
) -> tuple[float, float]:
    """Generalized-inverse preimage ``(raw_lo, raw_hi)`` of a calibrated interval.

    For a non-decreasing fitted map ``g``:
    ``raw_lo = inf{s : g(s) >= lo}`` and ``raw_hi = sup{s : g(s) <= hi}``.

    Parameters
    ----------
    lo, hi : float
        Calibrated-probability bounds; ``lo=0``/``hi=1`` map to the full
        raw range (−inf/+inf on the logit scale).
    space : {"probability", "logit"}
        Scale of the returned raw bounds. ``"logit"`` is what a
        SIGMOID-link raw-margin consumer (e.g. a counterfactual engine's
        ``Target.raw``) expects.
    buffer_logit : float
        Shrink the calibrated interval by this margin in logit space
        *before* inverting — robustness against future recalibration
        drift (a central-tendency update of magnitude <= buffer cannot
        invalidate the result).

    Returns
    -------
    tuple of float
        ``(raw_lo, raw_hi)`` preimage bounds, on the scale requested by
        ``space``.

    Raises
    ------
    UnattainableTargetError
        If the (buffered) interval does not intersect the output range —
        never silently clamped.
    NotImplementedError
        For non-monotone calibrators (``is_monotone_ = False``), whose
        preimage may be a union of intervals.
    """
    self._check_fitted()
    if not self.is_monotone_:
        raise NotImplementedError(
            f"{type(self).__name__} is not monotone (is_monotone_=False); its preimage "
            "may be a union of intervals. Use a monotone calibrator for thresholding "
            "and recourse."
        )
    if not 0.0 <= lo <= hi <= 1.0:
        raise ValueError(f"need 0 <= lo <= hi <= 1, got lo={lo}, hi={hi}")
    if space not in ("probability", "logit"):
        raise ValueError(f"space must be 'probability' or 'logit', got {space!r}")
    lo_b, hi_b = float(lo), float(hi)
    if buffer_logit > 0.0:
        if lo > 0.0:
            lo_b = float(expit(np.array([logit(np.array([lo]))[0] + buffer_logit]))[0])
        if hi < 1.0:
            hi_b = float(expit(np.array([logit(np.array([hi]))[0] - buffer_logit]))[0])
        if lo_b > hi_b:
            raise UnattainableTargetError(
                f"buffer_logit={buffer_logit} empties the calibrated interval " f"[{lo}, {hi}]"
            )
    gmin, gmax = self._output_range()
    if lo_b > gmax or hi_b < gmin:
        raise UnattainableTargetError(
            f"calibrated target [{lo_b:.6g}, {hi_b:.6g}] does not intersect the "
            f"calibrator's output range [{gmin:.6g}, {gmax:.6g}]"
        )
    raw_lo = 0.0 if lo_b <= gmin else float(self._inverse_left(lo_b))
    raw_hi = 1.0 if hi_b >= gmax else float(self._inverse_right(hi_b))
    if space == "logit":
        lo_out = -np.inf if raw_lo <= 0.0 else float(logit(np.array([raw_lo]))[0])
        hi_out = np.inf if raw_hi >= 1.0 else float(logit(np.array([raw_hi]))[0])
        return lo_out, hi_out
    return raw_lo, raw_hi

point_inverse

point_inverse(p: object, *, space: str = 'probability') -> ndarray

Raw scores whose calibrated probabilities equal p (exact preimage).

Defined only for strictly monotone calibrators with an exact inverse: affine-logit maps (logit g(s) = a * logit(s) + b) invert in closed form here, covering Platt scaling, temperature scaling, and the tied Beta variants ("a", "ab"); BetaCalibrator overrides this method with its own exact construction for the full "abm" variant. Others raise NotImplementedError and should use :meth:interval_inverse instead.

PARAMETER DESCRIPTION
p

Calibrated probabilities strictly inside (0, 1); boundary and out-of-range values raise UnattainableTargetError (all-or-nothing, no silent clamp).

TYPE: array_like

space

Scale of the returned raw values. "logit" returns the raw logit directly; "probability" (default) returns the raw score.

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

RETURNS DESCRIPTION
ndarray

Raw scores (or logits, if space="logit") whose calibrated probability equals p.

RAISES DESCRIPTION
RuntimeError

If not yet fitted.

ValueError

If space is not "probability" or "logit".

NotImplementedError

If the calibrator is not monotone (is_monotone_ = False), or has no affine-logit closed form (affine_logit_coeffs_ is None).

UnattainableTargetError

If any element of p lies outside the open interval (0, 1); or if space="probability" and the raw logit of any result exceeds logit(1 - 1e-12) in magnitude — the probability representation would round to 0.0/1.0 and silently fail to round-trip; space="logit" is exact there.

Source code in src/probcal/base.py
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
def point_inverse(self, p: object, *, space: str = "probability") -> np.ndarray:
    """Raw scores whose calibrated probabilities equal ``p`` (exact preimage).

    Defined only for strictly monotone calibrators with an exact
    inverse: affine-logit maps (``logit g(s) = a * logit(s) + b``)
    invert in closed form here, covering Platt scaling, temperature
    scaling, and the tied Beta variants (``"a"``, ``"ab"``);
    ``BetaCalibrator`` overrides this method with its own exact
    construction for the full ``"abm"`` variant. Others
    raise ``NotImplementedError`` and should use :meth:`interval_inverse`
    instead.

    Parameters
    ----------
    p : array_like
        Calibrated probabilities strictly inside ``(0, 1)``; boundary
        and out-of-range values raise ``UnattainableTargetError``
        (all-or-nothing, no silent clamp).
    space : {"probability", "logit"}, keyword-only
        Scale of the returned raw values. ``"logit"`` returns the raw
        logit directly; ``"probability"`` (default) returns the raw
        score.

    Returns
    -------
    numpy.ndarray
        Raw scores (or logits, if ``space="logit"``) whose calibrated
        probability equals ``p``.

    Raises
    ------
    RuntimeError
        If not yet fitted.
    ValueError
        If ``space`` is not ``"probability"`` or ``"logit"``.
    NotImplementedError
        If the calibrator is not monotone (``is_monotone_ = False``), or
        has no affine-logit closed form (``affine_logit_coeffs_`` is
        ``None``).
    UnattainableTargetError
        If any element of ``p`` lies outside the open interval
        ``(0, 1)``; or if ``space="probability"`` and the raw logit of
        any result exceeds ``logit(1 - 1e-12)`` in magnitude — the
        probability representation would round to 0.0/1.0 and silently
        fail to round-trip; ``space="logit"`` is exact there.
    """
    self._check_fitted()
    if not self.is_monotone_:
        raise NotImplementedError(
            f"{type(self).__name__} is not monotone (is_monotone_=False); its preimage "
            "may be a union of intervals. Use a monotone calibrator for thresholding "
            "and recourse."
        )
    if space not in ("probability", "logit"):
        raise ValueError(f"space must be 'probability' or 'logit', got {space!r}")
    arr = _validate_point_targets(p)
    coeffs = self.affine_logit_coeffs_
    if coeffs is None:
        raise NotImplementedError(
            f"{type(self).__name__} has no exact point inverse; use interval_inverse"
        )
    a, b = coeffs
    z = (logit(arr) - b) / a
    _check_representable(z, space)
    return z if space == "logit" else expit(z)

get_params

get_params(deep: bool = True) -> dict[str, object]

Constructor parameters as a dict (manual sklearn-compatible clone info).

Source code in src/probcal/base.py
423
424
425
426
427
428
429
430
def get_params(self, deep: bool = True) -> dict[str, object]:
    """Constructor parameters as a dict (manual sklearn-compatible clone info)."""
    sig = inspect.signature(type(self).__init__)
    return {
        name: getattr(self, name)
        for name in sig.parameters
        if name not in ("self", "args", "kwargs")
    }

set_params

set_params(**params: object) -> Self

Set constructor parameters; unknown names raise ValueError.

Source code in src/probcal/base.py
432
433
434
435
436
437
438
439
440
441
442
def set_params(self, **params: object) -> Self:
    """Set constructor parameters; unknown names raise ``ValueError``."""
    valid = self.get_params()
    for key, value in params.items():
        if key not in valid:
            raise ValueError(
                f"unknown parameter {key!r} for {type(self).__name__}; "
                f"valid: {sorted(valid)}"
            )
        setattr(self, key, value)
    return self

to_dict

to_dict() -> dict[str, object]

Versioned JSON-native snapshot of the fitted object.

RETURNS DESCRIPTION
dict

{"probcal_schema", "probcal_version", "class", "params", "state", "fit_meta"}. fit_meta records n_obs, n_events, weight_sum, fitted_at_utc (ISO 8601), the data_fingerprint (SHA-256 of the sorted (s, y, w) triple), and convergence flags where they exist.

RAISES DESCRIPTION
RuntimeError

If not yet fitted.

Source code in src/probcal/base.py
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
def to_dict(self) -> dict[str, object]:
    """Versioned JSON-native snapshot of the fitted object.

    Returns
    -------
    dict
        ``{"probcal_schema", "probcal_version", "class", "params",
        "state", "fit_meta"}``. ``fit_meta`` records ``n_obs``,
        ``n_events``, ``weight_sum``, ``fitted_at_utc`` (ISO 8601), the
        ``data_fingerprint`` (SHA-256 of the sorted ``(s, y, w)``
        triple), and convergence flags where they exist.

    Raises
    ------
    RuntimeError
        If not yet fitted.
    """
    self._check_fitted()
    from . import __version__

    fit_meta = dict(getattr(self, "fit_meta_", {}))
    for flag in ("converged_", "separation_fallback_"):
        if hasattr(self, flag):
            fit_meta[flag] = bool(getattr(self, flag))
    return {
        "probcal_schema": SCHEMA_VERSION,
        "probcal_version": __version__,
        "class": type(self).__name__,
        "params": encode_value(self._params_for_dict()),
        "state": self._state(),
        "fit_meta": fit_meta,
    }

from_dict classmethod

from_dict(d: dict) -> BaseCalibrator

Rebuild a fitted object from :meth:to_dict output.

Called on :class:BaseCalibrator itself, dispatches through the class registry to whatever class wrote d; called on a subclass, requires d["class"] to match that subclass.

PARAMETER DESCRIPTION
d

Output of :meth:to_dict (parsed JSON).

TYPE: dict

RETURNS DESCRIPTION
BaseCalibrator

A fitted instance.

RAISES DESCRIPTION
ValueError

If the schema version is unknown (naming the writing version), the class is not registered, or d["class"] does not match the subclass this was called on.

Source code in src/probcal/base.py
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
@classmethod
def from_dict(cls, d: dict) -> "BaseCalibrator":
    """Rebuild a fitted object from :meth:`to_dict` output.

    Called on :class:`BaseCalibrator` itself, dispatches through the
    class registry to whatever class wrote ``d``; called on a subclass,
    requires ``d["class"]`` to match that subclass.

    Parameters
    ----------
    d : dict
        Output of :meth:`to_dict` (parsed JSON).

    Returns
    -------
    BaseCalibrator
        A fitted instance.

    Raises
    ------
    ValueError
        If the schema version is unknown (naming the writing version),
        the class is not registered, or ``d["class"]`` does not match
        the subclass this was called on.
    """
    check_schema(d)
    if cls is BaseCalibrator:
        from ._registry import load

        return load(d)  # type: ignore[return-value]
    if cls.__name__ != d.get("class"):
        raise ValueError(f"payload was written by {d.get('class')!r}, not {cls.__name__}")
    params = cls._params_from_dict(decode_value(d.get("params", {})))  # type: ignore[arg-type]
    obj = cls(**params)  # type: ignore[arg-type]
    obj._set_state(dict(d.get("state", {})))  # type: ignore[arg-type]
    obj.fit_meta_ = dict(d.get("fit_meta", {}))  # type: ignore[arg-type]
    obj.fitted_ = True
    return obj

to_json

to_json(path: str | PathLike[str] | None = None, *, indent: int = 2) -> str | None

Serialize to JSON — never pickle (auditable, no code execution on load).

PARAMETER DESCRIPTION
path

When given, write to this file and return None; otherwise return the JSON text.

TYPE: path - like or None DEFAULT: None

indent

JSON indentation.

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

RETURNS DESCRIPTION
str or None

JSON text, or None when written to path.

Source code in src/probcal/base.py
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
def to_json(
    self, path: "str | os.PathLike[str] | None" = None, *, indent: int = 2
) -> str | None:
    """Serialize to JSON — never pickle (auditable, no code execution on load).

    Parameters
    ----------
    path : path-like or None
        When given, write to this file and return ``None``; otherwise
        return the JSON text.
    indent : int, keyword-only
        JSON indentation.

    Returns
    -------
    str or None
        JSON text, or ``None`` when written to ``path``.
    """
    text = json.dumps(self.to_dict(), indent=indent)
    if path is None:
        return text
    with open(path, "w", encoding="utf-8") as fh:
        fh.write(text)
    return None

from_json classmethod

from_json(path_or_str: object) -> BaseCalibrator

Load from a JSON string or a filesystem path.

PARAMETER DESCRIPTION
path_or_str

JSON text (starting with {) or a path to a JSON file.

TYPE: str or path - like

RETURNS DESCRIPTION
BaseCalibrator

A fitted instance (see :meth:from_dict).

Source code in src/probcal/base.py
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
@classmethod
def from_json(cls, path_or_str: object) -> "BaseCalibrator":
    """Load from a JSON string or a filesystem path.

    Parameters
    ----------
    path_or_str : str or path-like
        JSON text (starting with ``{``) or a path to a JSON file.

    Returns
    -------
    BaseCalibrator
        A fitted instance (see :meth:`from_dict`).
    """
    text = str(path_or_str)
    if not text.lstrip().startswith("{"):
        with open(text, encoding="utf-8") as fh:
            text = fh.read()
    return cls.from_dict(json.loads(text))

fingerprint

fingerprint() -> str

SHA-256 of the canonical serialized form, version- and timestamp-blind.

Two identical fits on identical data produce the same fingerprint; consumers (model registries, monitors, recourse engines) record it as provenance.

RETURNS DESCRIPTION
str

Hex digest.

Source code in src/probcal/base.py
584
585
586
587
588
589
590
591
592
593
594
595
596
def fingerprint(self) -> str:
    """SHA-256 of the canonical serialized form, version- and timestamp-blind.

    Two identical fits on identical data produce the same fingerprint;
    consumers (model registries, monitors, recourse engines) record it
    as provenance.

    Returns
    -------
    str
        Hex digest.
    """
    return fingerprint_of_dict(self.to_dict())

parametric

Parametric calibrators: Platt, temperature, and beta calibration.

Theory, derivations, and parameter interpretation: docs/concepts/methods-parametric.md.

References

Platt (1999); Lin, Lin & Weng (2007); Guo et al. (2017); Kull, Silva Filho & Flach (2017, AISTATS and EJS) — full records in the documentation.

PlattCalibrator

Bases: BaseCalibrator

Logistic recalibration on the logit scale (Platt scaling).

Fits logit g(s) = a * logit(s) + b by IRLS with Lin–Lin–Weng smoothed targets (N+ + 1)/(N+ + 2) and 1/(N- + 2) for stability on small samples, where N+/N- are the weighted class masses (row counts under unit weights), so that integer weights match row duplication. The identity map is (a, b) = (1, 0).

ATTRIBUTE DESCRIPTION
a_

Fitted slope — spread correction: a < 1 shrinks overconfident scores toward the base rate, a > 1 sharpens underconfident ones.

TYPE: float

b_

Fitted intercept — calibration-in-the-large shift in log-odds.

TYPE: float

converged_

Whether IRLS converged; if False a warning was raised at fit time and interpret() records it.

TYPE: bool

References

Platt (1999); Lin, Lin & Weng (2007). The logistic family fitted on raw SVM outputs (Platt's original setting) does not contain the identity; on logits it does — see the parametric-methods chapter.

affine_logit_coeffs_ property

affine_logit_coeffs_: tuple[float, float] | None

(a, b): Platt scaling is affine on the logit scale.

complexity_rank property

complexity_rank: float

Parsimony rank 2.0: a two-parameter map, simpler than the nonparametric methods.

interpret

interpret() -> Interpretation

Read the fitted slope and intercept against the identity (1, 0).

If IRLS did not converge at fit time (a UserWarning was raised), the messages include a note not to trust the coefficients.

Source code in src/probcal/parametric.py
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
def interpret(self) -> Interpretation:
    """Read the fitted slope and intercept against the identity ``(1, 0)``.

    If IRLS did not converge at fit time (a ``UserWarning`` was raised),
    the messages include a note not to trust the coefficients.
    """
    self._check_fitted()
    if self.a_ < 1.0:
        slope_msg = (
            f"slope a = {self.a_:.3f} < 1: scores were overconfident (too spread out); "
            "predictions are shrunk toward the base rate"
        )
    else:
        slope_msg = (
            f"slope a = {self.a_:.3f} >= 1: scores were underconfident (too flat); "
            "predictions are sharpened"
        )
    int_msg = (
        f"intercept b = {self.b_:.3f}: base-rate (calibration-in-the-large) shift of "
        f"{self.b_:+.3f} log-odds, odds factor {np.exp(self.b_):.3f}"
    )
    messages = [slope_msg, int_msg, "identity map corresponds to (a, b) = (1, 0)"]
    if not self.converged_:
        messages.append("IRLS did not converge; coefficients may be unreliable")
    return Interpretation(
        method=type(self).__name__,
        param_names=("a", "b"),
        param_values=(self.a_, self.b_),
        messages=tuple(messages),
    )

TemperatureCalibrator

Bases: BaseCalibrator

Temperature scaling: g(s) = sigma(logit(s) / T).

T minimizes the calibration-set negative log-likelihood via a safeguarded 1-D Newton iteration (bisection fallback) on u = 1/T.

ATTRIBUTE DESCRIPTION
T_

Fitted temperature. T > 1: the model was overconfident and is softened; T < 1: underconfident and sharpened. Temperature cannot fix base-rate error — s = 0.5 is a fixed point for every T; use Platt scaling or LogitOffset for level shifts.

TYPE: float

References

Guo, Pleiss, Sun & Weinberger (2017).

affine_logit_coeffs_ property

affine_logit_coeffs_: tuple[float, float] | None

(1/T, 0): temperature scaling is affine on the logit scale.

complexity_rank property

complexity_rank: float

Parsimony rank 1.0: the simplest map, a single parameter.

interpret

interpret() -> Interpretation

Read the fitted temperature against the identity T = 1.

Source code in src/probcal/parametric.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
def interpret(self) -> Interpretation:
    """Read the fitted temperature against the identity ``T = 1``."""
    self._check_fitted()
    if self.T_ > 1.0:
        msg = (
            f"T = {self.T_:.3f} > 1: the model was overconfident; logits are divided "
            "by T (softening toward 1/2)"
        )
    else:
        msg = (
            f"T = {self.T_:.3f} <= 1: the model was underconfident; logits are divided "
            "by T (sharpening away from 1/2)"
        )
    return Interpretation(
        method=type(self).__name__,
        param_names=("T",),
        param_values=(self.T_,),
        messages=(
            msg,
            "temperature cannot fix base-rate error: s = 0.5 maps to 0.5 for every T "
            "(use PlattCalibrator or LogitOffset for level shifts)",
        ),
    )

BetaCalibrator

BetaCalibrator(variant: str = 'abm')

Bases: BaseCalibrator

Beta calibration: logit g(s) = a·ln s − b·ln(1 − s) + c.

Variants: "abm" fits (a, b, c); "ab" ties a = b (equivalent to Platt scaling on logits); "a" additionally fixes c = 0 (a single-parameter map, the temperature family in a different parameterization). The monotonicity constraint a, b >= 0 is enforced by the betacal refit strategy: a negative exponent drops its feature and refits.

PARAMETER DESCRIPTION
variant

"abm" (default) fits the full (a, b, c); "ab" ties a = b; "a" additionally fixes c = 0.

TYPE: (abm, ab, a) DEFAULT: "abm"

ATTRIBUTE DESCRIPTION
a_

Sensitivity near s -> 0 — governs the low-probability tail (critical for low-PD credit portfolios).

TYPE: float

b_

Sensitivity near s -> 1.

TYPE: float

c_

Base-rate shift in log-odds.

TYPE: float

constraint_active_

Whether the a, b >= 0 constraint forced a refit.

TYPE: bool

converged_

Whether the fit whose coefficients survive converged (True for closed-form paths); if False a warning was raised at fit time and interpret() records it.

TYPE: bool

separation_fallback_

Whether any IRLS call during fitting detected separation and fell back to the ridge-regularized fit; recorded by interpret().

TYPE: bool

References

Kull, Silva Filho & Flach (2017), AISTATS 54 and EJS 11(2). The identity is (a, b, c) = (1, 1, 0): beta calibration cannot un-calibrate an already calibrated model. a != b captures asymmetric tail distortion; temperature is the special case a = b = 1/T, c = 0.

Source code in src/probcal/parametric.py
366
367
def __init__(self, variant: str = "abm") -> None:
    self.variant = variant

affine_logit_coeffs_ property

affine_logit_coeffs_: tuple[float, float] | None

(a, c) for the tied variants; None for "abm".

With a = b the map reduces to logit g = a * logit(s) + c, which is affine on the logit scale; the full three-parameter map is not (see the shap-calibration chapter).

complexity_rank property

complexity_rank: float

Parsimony rank by variant: 1.5 ("a"), 2.5 ("ab"), 3.0 ("abm").

.get with a fallback because variant is validated only in _fit; the property must not raise pre-fit.

point_inverse

point_inverse(p: object, *, space: str = 'probability') -> ndarray

Raw scores whose calibrated probabilities equal p (exact preimage).

Overrides :meth:BaseCalibrator.point_inverse with the beta family's own exact construction, so the "abm" variant — not affine on the logit scale — still gets a closed-form inverse instead of falling back to :meth:interval_inverse's bisection. With z = logit(s) and K = logit(p) - c, the forward map is a*z + (b-a)*softplus(z) = K, solved by a minimax-hyperbola seed refined by up to 4 certified Halley steps (:func:_beta_point_inverse_z). Degenerate exponents are handled by dedicated closed forms: a == b collapses to the affine formula z = K/a; a == 0 (h ranges over (0, inf), attainable probability range (sigma(c), 1)) gives z = ln(expm1(K/b)); b == 0 (range (-inf, 0), attainable range (0, sigma(c))) gives z = -ln(expm1(-K/a)); a == b == 0 is a constant map with no point inverse.

PARAMETER DESCRIPTION
p

Calibrated probabilities strictly inside (0, 1); boundary and out-of-range values raise UnattainableTargetError (all-or-nothing, no silent clamp).

TYPE: array_like

space

Scale of the returned raw values.

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

RETURNS DESCRIPTION
ndarray

Raw scores (or logits, if space="logit") whose calibrated probability equals p.

RAISES DESCRIPTION
RuntimeError

If not yet fitted; or if the general (a != b, both nonzero) case fails to certify to machine precision after 4 Halley steps (only reachable at exponent ratios far outside the numerically verified domain, a, b in (0, 5] and ratio <= 50 — see :func:_beta_point_inverse_z).

ValueError

If space is not "probability" or "logit".

NotImplementedError

If the calibrator is not monotone, or the fit collapsed to a constant map (a == b == 0): a constant map has no point inverse.

UnattainableTargetError

If any element of p lies outside the open interval (0, 1), or outside the attainable probability range of a degenerate (a == 0 or b == 0) fit — p is validated all-or-nothing: if any element is outside the range (named in the error message), the whole call raises and no element is silently clamped. Also raised when space="probability" and the raw logit of any result exceeds logit(1 - 1e-12) in magnitude — the probability representation would round to 0.0/1.0 and silently fail to round-trip; space="logit" is exact there.

Source code in src/probcal/parametric.py
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
def point_inverse(self, p: object, *, space: str = "probability") -> np.ndarray:
    """Raw scores whose calibrated probabilities equal ``p`` (exact preimage).

    Overrides :meth:`BaseCalibrator.point_inverse` with the beta
    family's own exact construction, so the ``"abm"``
    variant — not affine on the logit scale — still gets a closed-form
    inverse instead of falling back to :meth:`interval_inverse`'s
    bisection. With ``z = logit(s)`` and ``K = logit(p) - c``, the
    forward map is ``a*z + (b-a)*softplus(z) = K``, solved by a
    minimax-hyperbola seed refined by up to 4 certified Halley steps
    (:func:`_beta_point_inverse_z`). Degenerate exponents are handled by
    dedicated closed forms: ``a == b`` collapses to the affine formula
    ``z = K/a``; ``a == 0`` (``h`` ranges over ``(0, inf)``, attainable
    probability range ``(sigma(c), 1)``) gives ``z = ln(expm1(K/b))``;
    ``b == 0`` (range ``(-inf, 0)``, attainable range ``(0, sigma(c))``)
    gives ``z = -ln(expm1(-K/a))``; ``a == b == 0`` is a constant map
    with no point inverse.

    Parameters
    ----------
    p : array_like
        Calibrated probabilities strictly inside ``(0, 1)``; boundary
        and out-of-range values raise ``UnattainableTargetError``
        (all-or-nothing, no silent clamp).
    space : {"probability", "logit"}, keyword-only
        Scale of the returned raw values.

    Returns
    -------
    numpy.ndarray
        Raw scores (or logits, if ``space="logit"``) whose calibrated
        probability equals ``p``.

    Raises
    ------
    RuntimeError
        If not yet fitted; or if the general (``a != b``, both nonzero)
        case fails to certify to machine precision after 4 Halley steps
        (only reachable at exponent ratios far outside the numerically
        verified domain, ``a, b in (0, 5]`` and ratio ``<= 50`` — see
        :func:`_beta_point_inverse_z`).
    ValueError
        If ``space`` is not ``"probability"`` or ``"logit"``.
    NotImplementedError
        If the calibrator is not monotone, or the fit collapsed to a
        constant map (``a == b == 0``): a constant map has no point
        inverse.
    UnattainableTargetError
        If any element of ``p`` lies outside the open interval
        ``(0, 1)``, or outside the attainable probability range of a
        degenerate (``a == 0`` or ``b == 0``) fit — ``p`` is validated
        all-or-nothing: if any element is outside the range (named in
        the error message), the whole call raises and no element is
        silently clamped. Also raised when ``space="probability"`` and
        the raw logit of any result exceeds ``logit(1 - 1e-12)`` in
        magnitude — the probability representation would round to
        0.0/1.0 and silently fail to round-trip; ``space="logit"`` is
        exact there.
    """
    self._check_fitted()
    if not self.is_monotone_:
        raise NotImplementedError(
            f"{type(self).__name__} is not monotone (is_monotone_=False); its preimage "
            "may be a union of intervals. Use a monotone calibrator for thresholding "
            "and recourse."
        )
    if space not in ("probability", "logit"):
        raise ValueError(f"space must be 'probability' or 'logit', got {space!r}")
    arr = _validate_point_targets(p)
    a, b = self.a_, self.b_
    K = logit(arr) - self.c_
    if a == 0.0 and b == 0.0:
        raise NotImplementedError(
            f"{type(self).__name__} fitted a constant map (a=b=0); it has no exact point "
            "inverse; use interval_inverse"
        )
    if a == b:
        z = K / a
    elif a == 0.0:
        lo = float(expit(np.array([self.c_]))[0])
        if np.any(K <= 0.0):
            raise UnattainableTargetError(
                f"calibrated target is outside the attainable probability range "
                f"({lo:.6g}, 1) of this degenerate (a=0) beta fit"
            )
        z = np.log(np.expm1(K / b))
    elif b == 0.0:
        hi = float(expit(np.array([self.c_]))[0])
        if np.any(K >= 0.0):
            raise UnattainableTargetError(
                f"calibrated target is outside the attainable probability range "
                f"(0, {hi:.6g}) of this degenerate (b=0) beta fit"
            )
        z = -np.log(np.expm1(-K / a))
    else:
        z = _beta_point_inverse_z(K, a, b)
    _check_representable(z, space)
    return z if space == "logit" else expit(z)

interpret

interpret() -> Interpretation

Read the fitted exponents and intercept against the identity (1, 1, 0).

If IRLS did not converge at fit time (a UserWarning was raised), the messages include a note not to trust the coefficients.

Source code in src/probcal/parametric.py
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
def interpret(self) -> Interpretation:
    """Read the fitted exponents and intercept against the identity (1, 1, 0).

    If IRLS did not converge at fit time (a ``UserWarning`` was raised),
    the messages include a note not to trust the coefficients.
    """
    self._check_fitted()
    messages = [
        (
            f"a = {self.a_:.3f}: sensitivity near s -> 0; a < 1 raises the smallest "
            "probabilities (model was overconfident in the low tail), a > 1 deepens them"
        ),
        (
            f"b = {self.b_:.3f}: sensitivity near s -> 1; the mirrored reading for the "
            "high tail"
        ),
        (
            f"c = {self.c_:.3f}: base-rate shift of {self.c_:+.3f} log-odds, odds factor "
            f"{np.exp(self.c_):.3f}"
        ),
        "identity map corresponds to (a, b, c) = (1, 1, 0)",
    ]
    if abs(self.a_ - self.b_) > 0.1:
        messages.append(
            f"a != b (gap {self.a_ - self.b_:+.3f}): asymmetric tail distortion that no "
            "symmetric (Platt/temperature) map could express"
        )
    if self.constraint_active_:
        messages.append(
            "monotonicity constraint a, b >= 0 was active: a negative exponent was "
            "dropped and the model refitted (betacal strategy)"
        )
    if not self.converged_:
        messages.append("IRLS did not converge; coefficients may be unreliable")
    if self.separation_fallback_:
        messages.append(
            "separation was detected during fitting; at least one fit fell back to "
            "the ridge-regularized solution (ridge=1e-6)"
        )
    return Interpretation(
        method=type(self).__name__,
        param_names=("a", "b", "c"),
        param_values=(self.a_, self.b_, self.c_),
        messages=tuple(messages),
    )

isotonic

Isotonic calibrators: PAVA-based isotonic and centered isotonic regression (CIR).

Theory and worked example: docs/concepts/methods-nonparametric.md.

References

Barlow, Bartholomew, Bremner & Brunk (1972); Zadrozny & Elkan (2002); Oron & Flournoy (2017) — full records in the documentation.

IsotonicCalibrator

IsotonicCalibrator(interpolation: str = 'none')

Bases: BaseCalibrator

Isotonic calibration: the PAVA step function.

Fits the least-squares non-decreasing map of outcomes on scores. The fitted map is a right-continuous step function with one level per pooled block; scores outside the calibration range clamp to the first/last level. interpolation="linear" instead joins block midpoints, removing the discontinuities.

PARAMETER DESCRIPTION
interpolation

"none" (default) keeps the raw step function; "linear" joins the block midpoints, removing the tied-prediction plateaus.

TYPE: (none, linear) DEFAULT: "none"

ATTRIBUTE DESCRIPTION
n_blocks_

Number of pooled blocks — the effective complexity estimated from the data.

TYPE: int

block_mean_

Event rate of each pooled block (the step levels).

TYPE: ndarray

block_first_s_, block_last_s_

Score range covered by each block.

TYPE: ndarray

block_center_s_

Weight-centered score coordinate of each block (used by CIR).

TYPE: ndarray

References

Barlow et al. (1972) for PAVA; Zadrozny & Elkan (2002) for its use in classifier calibration.

Source code in src/probcal/isotonic.py
73
74
def __init__(self, interpolation: str = "none") -> None:
    self.interpolation = interpolation

complexity_rank property

complexity_rank: float

Parsimony rank 50.0: nonparametric, data-driven block count.

interpret

interpret() -> Interpretation

Read the block structure as effective complexity and local event rates.

Source code in src/probcal/isotonic.py
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
def interpret(self) -> Interpretation:
    """Read the block structure as effective complexity and local event rates."""
    self._check_fitted()
    flats = int(np.sum(np.isclose(np.diff(self.block_mean_), 0.0)))
    messages = [
        (
            f"{self.n_blocks_} pooled blocks: each step level is the empirical event "
            "rate of a score region the data could not subdivide further"
        ),
        (
            f"block count = effective complexity actually estimated from the data "
            f"(range of levels: {self.block_mean_[0]:.4g} to {self.block_mean_[-1]:.4g})"
        ),
    ]
    if flats:
        messages.append(
            f"{flats} adjacent block pairs share a level: expect tied predictions there"
        )
    messages.append(
        "output range is limited to the span of block levels; targets outside it are "
        "unattainable (relevant for interval_inverse)"
    )
    return Interpretation(
        method=type(self).__name__,
        param_names=("n_blocks",),
        param_values=(float(self.n_blocks_),),
        messages=tuple(messages),
    )

CenteredIsotonicCalibrator

CenteredIsotonicCalibrator()

Bases: IsotonicCalibrator

Centered isotonic regression (CIR): strictly increasing where data permit.

Post-processes the PAVA solution by collapsing each block to its weight-centered score coordinate and interpolating linearly through the points (Oron & Flournoy, 2017). Removes the step function's tied predictions — preferred when downstream ranking must be strict.

ATTRIBUTE DESCRIPTION
n_blocks_

Number of pooled blocks — the effective complexity estimated from the data. Inherited from the PAVA fit.

TYPE: int

block_mean_

Event rate of each pooled block (the interpolation y-values).

TYPE: ndarray

block_first_s_, block_last_s_

Score range covered by each block (inherited; not used for prediction, which interpolates through block_center_s_ instead).

TYPE: ndarray

block_center_s_

Weight-centered score coordinate of each block — the interpolation x-values that make CIR strictly increasing.

TYPE: ndarray

References

Oron & Flournoy (2017).

Source code in src/probcal/isotonic.py
182
183
def __init__(self) -> None:
    super().__init__(interpolation="none")

interpret

interpret() -> Interpretation

Isotonic reading plus the strictness property CIR adds.

Source code in src/probcal/isotonic.py
204
205
206
207
208
209
210
211
212
213
214
215
216
def interpret(self) -> Interpretation:
    """Isotonic reading plus the strictness property CIR adds."""
    base = super().interpret()
    return Interpretation(
        method=type(self).__name__,
        param_names=base.param_names,
        param_values=base.param_values,
        messages=base.messages
        + (
            "centered isotonic interpolation is strictly increasing wherever block "
            "levels differ: distinct scores keep distinct predictions (no ties)",
        ),
    )

binning

Binning calibrators: histogram binning and scaling-binning.

Theory: docs/concepts/methods-nonparametric.md.

References

Zadrozny & Elkan (2001); Kumar, Liang & Ma (2019) — full records in the documentation.

HistogramBinningCalibrator

HistogramBinningCalibrator(n_bins: int = 10, strategy: str = 'mass', shrinkage: str | None = 'jeffreys')

Bases: BaseCalibrator

Histogram binning: per-bin event rates with optional Jeffreys shrinkage.

PARAMETER DESCRIPTION
n_bins

Requested number of bins B — the bias–variance dial.

TYPE: int DEFAULT: 10

strategy

"mass" (equal-count, recommended default: lower estimator bias, no empty bins) or "width" (equal-width over [0, 1]).

TYPE: (mass, width) DEFAULT: "mass"

shrinkage

"jeffreys" replaces the raw rate k/n with (k + 1/2)/(n + 1) — the posterior mean under the Beta(1/2, 1/2) prior — keeping small bins away from 0 and 1.

TYPE: (jeffreys, None) DEFAULT: "jeffreys"

ATTRIBUTE DESCRIPTION
bin_rate_

Calibrated value per (non-degenerate) bin.

TYPE: ndarray

is_monotone_

Computed after fitting: binning does not assume monotonicity, so the flag reports whether the fitted rates happen to be non-decreasing.

TYPE: bool

References

Zadrozny & Elkan (2001).

Source code in src/probcal/binning.py
56
57
58
59
60
61
def __init__(
    self, n_bins: int = 10, strategy: str = "mass", shrinkage: str | None = "jeffreys"
) -> None:
    self.n_bins = n_bins
    self.strategy = strategy
    self.shrinkage = shrinkage

complexity_rank property

complexity_rank: float

Parsimony rank 10.0, for either strategy ("mass" or "width").

interpret

interpret() -> Interpretation

Read bin rates as local event frequencies and B as the complexity dial.

Source code in src/probcal/binning.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def interpret(self) -> Interpretation:
    """Read bin rates as local event frequencies and B as the complexity dial."""
    self._check_fitted()
    messages = [
        (
            f"{len(self.bin_rate_)} bins ({self.strategy} strategy): each calibrated "
            "value is the (shrunken) empirical event rate of its score bin"
        ),
        "B controls bias-variance: few bins are stable but coarse, many are sharp but noisy",
    ]
    if self.shrinkage == "jeffreys":
        messages.append("Jeffreys shrinkage (k+1/2)/(n+1) keeps sparse bins away from 0 and 1")
    if not self.is_monotone_:
        messages.append(
            "fitted bin rates are not monotone: binning does not enforce ranking "
            "preservation — read inversions as noise, not signal"
        )
    return Interpretation(
        method=type(self).__name__,
        param_names=("n_bins",),
        param_values=(float(len(self.bin_rate_)),),
        messages=tuple(messages),
    )

ScalingBinningCalibrator

ScalingBinningCalibrator(n_bins: int = 10)

Bases: BaseCalibrator

Scaling-binning (Kumar–Liang–Ma): Platt stage, then bin the fitted values.

Fits Platt scaling first, then forms equal-mass bins of the fitted function values and outputs the mean of the fitted values within each bin. Achieves measurable calibration error with O(1/eps^2 + B) samples versus O(B/eps^2) for histogram binning.

PARAMETER DESCRIPTION
n_bins

Requested number of equal-mass bins of the Platt-fitted values.

TYPE: int DEFAULT: 10

ATTRIBUTE DESCRIPTION
platt_

The fitted first-stage Platt calibrator.

TYPE: PlattCalibrator

edges_

Interior quantile edges of the Platt-fitted values.

TYPE: ndarray

bin_value_

Mean Platt-fitted value per bin (the calibrated output for that bin).

TYPE: ndarray

References

Kumar, Liang & Ma (2019).

Source code in src/probcal/binning.py
171
172
def __init__(self, n_bins: int = 10) -> None:
    self.n_bins = n_bins

complexity_rank property

complexity_rank: float

Parsimony rank 4.0: a Platt stage plus a bin count, still lightweight.

interpret

interpret() -> Interpretation

Two-stage reading: Platt map, then the error-measurability discretization.

Source code in src/probcal/binning.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
def interpret(self) -> Interpretation:
    """Two-stage reading: Platt map, then the error-measurability discretization."""
    self._check_fitted()
    platt_interp = self.platt_.interpret()
    return Interpretation(
        method=type(self).__name__,
        param_names=platt_interp.param_names + ("n_bins",),
        param_values=platt_interp.param_values + (float(len(self.bin_value_)),),
        messages=platt_interp.messages
        + (
            (
                f"binning stage: {len(self.bin_value_)} equal-mass bins of the fitted "
                "Platt values; outputs are bin means, which makes the residual "
                "calibration error estimable with O(1/eps^2 + B) samples "
                "(vs O(B/eps^2) for histogram binning)"
            ),
        ),
    )

bayesian

Bayesian-ensemble calibrators: BBQ and ENIR.

Theory: docs/concepts/methods-nonparametric.md.

References

Naeini, Cooper & Hauskrecht (2015); Naeini & Cooper (2016); Tibshirani, Hoefling & Tibshirani (2011) — full records in the documentation.

BBQCalibrator

BBQCalibrator(min_bins: int | None = None, max_bins: int | None = None)

Bases: BaseCalibrator

Bayesian Binning into Quantiles: model averaging over equal-mass binnings.

Considers equal-mass binning models over a range of bin counts, scores each by its Beta-Binomial log marginal likelihood under a per-bin Jeffreys Beta(1/2, 1/2) prior, and predicts with the posterior-weighted average of the models' (posterior-mean) bin rates.

PARAMETER DESCRIPTION
min_bins

Range of candidate bin counts; defaults to [2, ceil(sqrt(n))] capped at 50.

TYPE: int or None DEFAULT: None

max_bins

Range of candidate bin counts; defaults to [2, ceil(sqrt(n))] capped at 50.

TYPE: int or None DEFAULT: None

ATTRIBUTE DESCRIPTION
bins_grid_

Candidate bin counts.

TYPE: ndarray

weights_

Posterior weights over the candidates (sum to 1).

TYPE: ndarray

References

Naeini, Cooper & Hauskrecht (2015).

Source code in src/probcal/bayesian.py
57
58
59
def __init__(self, min_bins: int | None = None, max_bins: int | None = None) -> None:
    self.min_bins = min_bins
    self.max_bins = max_bins

complexity_rank property

complexity_rank: float

Parsimony rank 40.0: a Bayesian model average over binnings.

interpret

interpret() -> Interpretation

Read the posterior weights as uncertainty about the data's resolution.

Source code in src/probcal/bayesian.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def interpret(self) -> Interpretation:
    """Read the posterior weights as uncertainty about the data's resolution."""
    self._check_fitted()
    top = np.argsort(self.weights_)[::-1][:3]
    top_txt = ", ".join(
        f"B={int(self.bins_grid_[i])} (weight {self.weights_[i]:.3f})" for i in top
    )
    return Interpretation(
        method=type(self).__name__,
        param_names=("n_models",),
        param_values=(float(len(self.bins_grid_)),),
        messages=(
            f"top-3 binning models by posterior weight: {top_txt}",
            "concentrated weight = the data speak clearly about their own resolution; "
            "diffuse weight = the averaging is doing real work",
        ),
    )

ENIRCalibrator

ENIRCalibrator(max_solutions: int | None = 256)

Bases: BaseCalibrator

Ensemble of near-isotonic regressions (ENIR).

Computes the full nearly-isotonic solution path (modified PAVA of Tibshirani, Hoefling & Tibshirani, 2011) from the raw data (lambda = 0) to the fully isotonic fit, then averages the breakpoint solutions with BIC weights. The combined map may be non-monotone: is_monotone_ is False and consumers requiring order preservation should prefer a monotone calibrator. Fitting is quadratic in the number of unique scores and intended for m <= 50,000; above that, fit emits a single UserWarning stating the expected minutes.

PARAMETER DESCRIPTION
max_solutions

Number of path solutions to keep for the ensemble, chosen by best (lowest) BIC; None keeps every breakpoint. Retention is what bounds memory: the path has up to m breakpoints, so keeping all of them costs O(m^2).

TYPE: int or None DEFAULT: 256

ATTRIBUTE DESCRIPTION
path_lambdas_

Breakpoints of the penalty parameter, starting at 0. All breakpoints are recorded, whether or not their solution is retained.

TYPE: numpy.ndarray of shape (T,)

path_solutions_

Fitted values on the tie-aggregated score grid at the retained breakpoints, in breakpoint order. K is the number of retained breakpoints: at most max_solutions, and fewer when breakpoints are pruned (a breakpoint whose BIC weight is provably below 1e-15 relative is skipped without being scored, and is retained only when max_solutions is None).

TYPE: numpy.ndarray of shape (K, m)

kept_breakpoints_

Indices into path_lambdas_ of the retained breakpoints.

TYPE: numpy.ndarray of shape (K,)

weights_

BIC weights over the retained solutions, renormalized to sum to 1.

TYPE: numpy.ndarray of shape (K,)

dropped_weight_

BIC weight lost to retention — the weight of scored solutions that the max_solutions cap evicted, before renormalization; a UserWarning is raised above 1e-6. Pruned breakpoints do not count towards it: their weight is exactly 0 in double precision.

TYPE: float

References

Naeini & Cooper (2016); Tibshirani, Hoefling & Tibshirani (2011).

Source code in src/probcal/bayesian.py
205
206
def __init__(self, max_solutions: int | None = 256) -> None:
    self.max_solutions = max_solutions

complexity_rank property

complexity_rank: float

Parsimony rank 80.0: an ensemble over the full near-isotonic path.

interpret

interpret() -> Interpretation

Read the path length and BIC weights; warn about non-monotonicity.

Source code in src/probcal/bayesian.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
def interpret(self) -> Interpretation:
    """Read the path length and BIC weights; warn about non-monotonicity."""
    self._check_fitted()
    top = np.argsort(self.weights_)[::-1][:3]
    top_txt = ", ".join(
        f"lambda={self.path_lambdas_[self.kept_breakpoints_[i]]:.4g} "
        f"(weight {self.weights_[i]:.3f})"
        for i in top
    )
    return Interpretation(
        method=type(self).__name__,
        param_names=("n_path_solutions",),
        param_values=(float(len(self.path_lambdas_)),),
        messages=(
            f"top-3 path solutions by BIC weight: {top_txt}",
            "lambda trades monotonicity strictness against fit; BIC weights are model "
            "plausibility along the path",
            "the ensemble output may be non-monotone (is_monotone_ = False): consumers "
            "requiring order preservation should use a monotone calibrator",
        ),
    )

vennabers

Venn–Abers calibrators: inductive (IVAP) and cross (CVAP).

Theory, validity guarantee scope, and the scalarization caveat: docs/concepts/methods-distribution-free.md. The guarantee attaches to the interval returned by :meth:VennAbersCalibrator.predict_interval; the scalar from predict_proba is the log-loss-minimax merger and is not itself covered by the validity theorem.

References

Vovk & Petej (2014) — full record in the documentation.

VennAbersCalibrator

Bases: BaseCalibrator

Inductive Venn–Abers predictor (IVAP).

For a query score, two isotonic fits on the calibration set augmented with the query labeled 0 (resp. 1) yield the interval [p0, p1]; predict_proba scalarizes it as p1 / (1 - p0 + p1).

Both fits are precomputed at fit time by the Vovk & Petej (2014) cumulative- sum-diagram sweep, so prediction is a searchsorted gather rather than a pair of PAVA refits per unique query score.

ATTRIBUTE DESCRIPTION
F0_, F1_

Fitted probabilities for a unit-weight query labeled 0 (resp. 1) inserted at each of the n+1 positions of the sorted calibration set. Both are non-decreasing, and F0_ <= F1_ elementwise.

TYPE: numpy.ndarray of shape (n + 1,)

Notes

With non-unit sample weights the query still enters at weight 1, which is the natural generalization but sits outside the validity theorem as proved; see the scope note in docs/concepts/methods-distribution-free.md.

complexity_rank property

complexity_rank: float

Parsimony rank 60.0: a distribution-free interval predictor.

predict_interval

predict_interval(s: object) -> ndarray

Venn–Abers intervals [p0, p1] for new scores.

PARAMETER DESCRIPTION
s

Raw scores/probabilities in [0, 1].

TYPE: array_like

RETURNS DESCRIPTION
numpy.ndarray of shape (n, 2)

Columns p0 (lower) and p1 (upper). The distribution-free validity guarantee attaches to this pair, not to the scalarized predict_proba output.

Source code in src/probcal/vennabers.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def predict_interval(self, s: object) -> np.ndarray:
    """Venn–Abers intervals ``[p0, p1]`` for new scores.

    Parameters
    ----------
    s : array_like
        Raw scores/probabilities in ``[0, 1]``.

    Returns
    -------
    numpy.ndarray of shape (n, 2)
        Columns ``p0`` (lower) and ``p1`` (upper). The distribution-free
        validity guarantee attaches to this pair, not to the scalarized
        ``predict_proba`` output.
    """
    self._check_fitted()
    from ._validation import validate_scores

    arr = validate_scores(s)
    idx = np.searchsorted(self._s, arr, side="left")
    return np.column_stack([self.F0_[idx], self.F1_[idx]])

interpret

interpret() -> Interpretation

Report interval widths over the calibration scores — where to trust the map.

Source code in src/probcal/vennabers.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
def interpret(self) -> Interpretation:
    """Report interval widths over the calibration scores — where to trust the map."""
    self._check_fitted()
    if self._widths_cache is None:
        intervals = self.predict_interval(self._s)
        widths = intervals[:, 1] - intervals[:, 0]
        self._widths_cache = (float(widths.mean()), float(widths.max()))
    mean_w, max_w = self._widths_cache
    return Interpretation(
        method=type(self).__name__,
        param_names=("mean_width", "max_width"),
        param_values=(mean_w, max_w),
        messages=(
            f"mean Venn–Abers interval width {mean_w:.4f}, maximum {max_w:.4f} over the "
            "calibration scores: width is per-score calibration uncertainty",
            "the validity guarantee holds for the interval [p0, p1] from "
            "predict_interval(); the scalar from predict_proba() is the log-loss-minimax "
            "merger p1/(1-p0+p1) and is not itself covered by the guarantee",
        ),
    )

CrossVennAbersCalibrator

CrossVennAbersCalibrator(cv: int = 5, random_state: int = 42)

Bases: BaseCalibrator

Cross Venn–Abers predictor (CVAP): fold-wise IVAPs, geometric-mean merge.

Splits the calibration data into cv stratified folds; each fold's IVAP is fitted on the remaining folds. The scalar output merges the fold-wise pairs by the log-loss rule of Vovk & Petej: GM(p1) / (GM(1 - p0) + GM(p1)). predict_interval returns the conservative envelope [min_k p0_k, max_k p1_k] (a probcal design choice — the paper defines only the scalar merge).

PARAMETER DESCRIPTION
cv

Number of stratified folds; must be at least 2.

TYPE: int DEFAULT: 5

random_state

Seed for the fold assignment.

TYPE: int DEFAULT: 42

ATTRIBUTE DESCRIPTION
_ivaps

Internal per-fold state: one fitted IVAP per fold, each trained on the other cv - 1 folds. No public fitted attribute is exposed; read fitted state through :meth:predict_interval instead.

TYPE: list of VennAbersCalibrator

References

Vovk & Petej (2014).

Source code in src/probcal/vennabers.py
219
220
221
def __init__(self, cv: int = 5, random_state: int = 42) -> None:
    self.cv = cv
    self.random_state = random_state

complexity_rank property

complexity_rank: float

Parsimony rank 60.0: same tier as IVAP, folded across cv splits.

predict_interval

predict_interval(s: object) -> ndarray

Conservative fold envelope [min_k p0_k, max_k p1_k] for new scores.

PARAMETER DESCRIPTION
s

Raw scores/probabilities in [0, 1].

TYPE: array_like

RETURNS DESCRIPTION
numpy.ndarray of shape (n, 2)

Columns p0 (lower) and p1 (upper): the envelope across fold-wise IVAP intervals (a probcal design choice — the paper defines only the scalar merge, not an interval for CVAP).

Source code in src/probcal/vennabers.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
def predict_interval(self, s: object) -> np.ndarray:
    """Conservative fold envelope ``[min_k p0_k, max_k p1_k]`` for new scores.

    Parameters
    ----------
    s : array_like
        Raw scores/probabilities in ``[0, 1]``.

    Returns
    -------
    numpy.ndarray of shape (n, 2)
        Columns ``p0`` (lower) and ``p1`` (upper): the envelope across
        fold-wise IVAP intervals (a probcal design choice — the paper
        defines only the scalar merge, not an interval for CVAP).
    """
    self._check_fitted()
    from ._validation import validate_scores

    arr = validate_scores(s)
    p0, p1 = self._fold_pairs(arr)
    return np.column_stack([p0.min(axis=0), p1.max(axis=0)])

interpret

interpret() -> Interpretation

Report fold count and envelope widths over a probe grid.

Source code in src/probcal/vennabers.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
def interpret(self) -> Interpretation:
    """Report fold count and envelope widths over a probe grid."""
    self._check_fitted()
    probe = np.linspace(0.01, 0.99, 99)
    env = self.predict_interval(probe)
    widths = env[:, 1] - env[:, 0]
    return Interpretation(
        method=type(self).__name__,
        param_names=("cv", "mean_envelope_width"),
        param_values=(float(self.cv), float(widths.mean())),
        messages=(
            f"{self.cv} stratified folds, one IVAP per fold; scalar output is the "
            "geometric-mean merge GM(p1)/(GM(1-p0)+GM(p1))",
            "predict_interval() returns the conservative fold envelope "
            "[min p0, max p1]; per-fold IVAP intervals carry the validity guarantee",
        ),
    )

spline

Spline calibrator: penalized natural cubic splines on the logit scale.

Theory: docs/concepts/methods-nonparametric.md.

References

Lucena (2018); Hastie, Tibshirani & Friedman (2009), §5.2.1 — full records in the documentation.

SplineCalibrator

SplineCalibrator(n_knots: int | None = None, lambdas: object = None, cv: int = 5, random_state: int = 42)

Bases: BaseCalibrator

Natural cubic spline calibration on the logit scale.

Models logit g(s) = sum_k theta_k N_k(logit s) with the natural cubic basis (linear beyond the boundary knots), fitted by penalized IRLS with a second-difference roughness penalty. The penalty weight is chosen by K-fold cross-validated log loss within the calibration set.

PARAMETER DESCRIPTION
n_knots

Number of knots (placed at equally spaced quantiles of the logit scores); defaults to clip(ceil(n^(1/3)), 4, 12).

TYPE: int or None DEFAULT: None

lambdas

Candidate penalty weights; defaults to logspace(-4, 4, 17).

TYPE: array_like or None DEFAULT: None

cv

Inner fold count for the lambda search.

TYPE: int DEFAULT: 5

random_state

Seed for the stratified fold assignment.

TYPE: int DEFAULT: 42

ATTRIBUTE DESCRIPTION
lambda_

Selected penalty weight.

TYPE: float

edof_

Effective degrees of freedom — trace of the smoother matrix at the fitted solution; the honest complexity measure.

TYPE: float

n_knots_

Number of knots actually used.

TYPE: int

is_monotone_

Checked on a dense grid after fitting; the penalty does not enforce monotonicity, and a rare non-monotone fit is flagged with a warning.

TYPE: bool

References

Lucena (2018); Hastie, Tibshirani & Friedman (2009), §5.2.1.

Source code in src/probcal/spline.py
109
110
111
112
113
114
115
116
117
118
119
def __init__(
    self,
    n_knots: int | None = None,
    lambdas: object = None,
    cv: int = 5,
    random_state: int = 42,
) -> None:
    self.n_knots = n_knots
    self.lambdas = lambdas
    self.cv = cv
    self.random_state = random_state

complexity_rank property

complexity_rank: float

Parsimony rank 12.0: a penalized basis expansion, more flexible than binning.

interpret

interpret() -> Interpretation

Read effective degrees of freedom as the honest complexity measure.

Source code in src/probcal/spline.py
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
def interpret(self) -> Interpretation:
    """Read effective degrees of freedom as the honest complexity measure."""
    self._check_fitted()
    messages = [
        (
            f"effective degrees of freedom {self.edof_:.2f} (trace of the smoother): "
            "values near 2 mean a parametric family would have sufficed; larger values "
            "mean the curvature is real"
        ),
        (
            f"penalty lambda = {self.lambda_:.4g} chosen by {self.cv}-fold "
            f"cross-validated log loss over {len(self.lambdas_grid_)} candidates; "
            f"{self.n_knots_} knots at logit-score quantiles"
        ),
        (
            "regions where the fitted curve runs steeper than the identity are locally "
            "underconfident score regions; shallower, locally overconfident"
        ),
    ]
    if not self.is_monotone_:
        messages.append("fitted curve is NOT monotone on the probe grid (warned at fit)")
    return Interpretation(
        method=type(self).__name__,
        param_names=("edof", "lambda", "n_knots"),
        param_values=(self.edof_, self.lambda_, float(self.n_knots_)),
        messages=tuple(messages),
    )

segmented

SegmentedCalibrator: empirical-Bayes shrunken per-segment logit offsets.

Theory and the DerSimonian-Laird method-of-moments derivation: docs/concepts/segmented.md.

References

DerSimonian & Laird (1986) — random-effects meta-analysis method-of-moments heterogeneity estimator, reused here across segments instead of studies.

SegmentedCalibrator

SegmentedCalibrator(base: BaseCalibrator | None = None, *, unseen: str = 'global')

Bases: BaseCalibrator

Per-segment logit offsets on top of a shared base map, empirical-Bayes shrunk.

Fits one shared base calibrator on all data, then an offset-only logistic MLE (:func:probcal.offset.estimate_offset) of each segment's residual log-odds shift against the base map's predictions. Segments with few observations have a noisy, high-variance delta_hat; rather than use it directly (no pooling — overfits small segments) or discard it (complete pooling — ignores real heterogeneity), each segment's estimate is shrunk toward the across-segment mean by the classic empirical-Bayes/random-effects factor tau2 / (tau2 + se**2), where tau2 is the between-segment heterogeneity variance estimated by the DerSimonian-Laird (1986) method of moments. A small, noisy segment (large se) shrinks toward 0 (the base map); a large, precise segment (small se) keeps most of its own estimate.

Segments with only one outcome class have no offset MLE (estimate_offset raises); they are recorded as delta_hat=0.0, se=inf — fully shrunk, since an infinite-variance estimate carries no weight in the DerSimonian-Laird pooling and tau2 / (tau2 + inf) = 0.

fit and predict_proba add a keyword-only segments argument on top of the base signature (segments=None degrades to a single segment "__all__" at fit time, and to the plain base map — no segment-specific offset — at predict time), so the zero-argument protocol calls (SegmentedCalibrator().fit(s, y), cal.predict_proba(s)) still work. Because :class:~probcal.chain.Chain has no segments= slot, Chain([seg, ...]) always predicts through seg's global map (segments=None, delta=0) — the per-segment shift is not baked into a Chain; use SegmentedCalibrator directly (with segments=) when the per-segment offset must apply.

Segment labels are compared as strings (_coerce_segments calls .astype(str)): fit-time labels 0, 1 (int) become "0", "1", but predict-time labels 0.0, 1.0 (float) become "0.0", "1.0" — a mismatch that never raises (every label looks "unseen") and, under unseen="global", silently falls back to the base map for every row. Pass segments with the same representation (e.g. cast to str yourself) at fit and predict time. When every row of a predict_proba/inverse call is unseen and unseen="global", a UserWarning is raised naming the fitted segments_ — a partial overlap (some rows match, some are genuinely new segments) stays silent.

PARAMETER DESCRIPTION
base

Unfitted calibrator cloned and fitted on the pooled data at :meth:fit time (type(base)(**base.get_params()), the wrapper.py clone pattern). None (default) uses :class:~probcal.parametric.BetaCalibrator.

TYPE: BaseCalibrator or None DEFAULT: None

unseen

Policy for a segment label at predict/inverse time that was not seen at fit time. "global" (default) applies delta=0 (the base map); "raise" raises ValueError.

TYPE: ('global', 'raise') DEFAULT: "global"

ATTRIBUTE DESCRIPTION
base_

The fitted clone of base (or a fitted BetaCalibrator()).

TYPE: BaseCalibrator

segments_

Segment labels seen at fit time, sorted.

TYPE: tuple of str

n_, events_

Per-segment observation count and weighted event count, aligned with segments_.

TYPE: ndarray

delta_hat_, se_

Per-segment offset MLE and its Fisher standard error (se=inf for single-class segments).

TYPE: ndarray

tau2_

Between-segment heterogeneity variance (DerSimonian-Laird method-of-moments estimate); 0.0 when fewer than two segments have a finite se (complete pooling).

TYPE: float

shrink_

Per-segment shrinkage factor tau2 / (tau2 + se**2) in [0, 1).

TYPE: ndarray

delta_tilde_

Per-segment shrunk offset, delta_hat * shrink: the offset actually applied at predict time.

TYPE: ndarray

is_monotone_

base_.is_monotone_.

TYPE: bool

Examples:

>>> import numpy as np
>>> from probcal import SegmentedCalibrator, make_pd_portfolio
>>> d = make_pd_portfolio(n=900, random_state=0)
>>> segments = np.array(["a", "b", "c"])[np.arange(900) % 3]
>>> cal = SegmentedCalibrator().fit(d.scores, d.y, segments=segments)
>>> cal.segments_
('a', 'b', 'c')
>>> p_global = cal.predict_proba(d.scores)  # segments=None: the base map
>>> p_seg = cal.predict_proba(d.scores, segments=segments)
>>> p_global.shape == p_seg.shape == d.scores.shape
True
Source code in src/probcal/segmented.py
145
146
147
def __init__(self, base: BaseCalibrator | None = None, *, unseen: str = "global") -> None:
    self.base = base
    self.unseen = unseen

affine_logit_coeffs_ property

affine_logit_coeffs_: tuple[float, float] | None

(a, b + delta_tilde) only for a single fitted segment; else None.

With more than one segment the map is segment-dependent (there is no single affine map on the logit scale that fits every segment), so :meth:point_inverse (which relies on this property) is unavailable then — use :meth:interval_inverse with segment=.

fit

fit(s: object, y: object, sample_weight: object = None, *, segments: object = None) -> SegmentedCalibrator

Fit the shared base map, then per-segment shrunk offsets.

PARAMETER DESCRIPTION
s

Raw scores/probabilities in [0, 1].

TYPE: array_like

y

Binary outcomes in {0, 1}; both classes must be present overall (individual segments may be single-class — see class docstring).

TYPE: array_like

sample_weight

Positive observation weights.

TYPE: array_like or None DEFAULT: None

segments

Segment label per observation, same length as s. None (default) fits a single segment "__all__".

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

RETURNS DESCRIPTION
SegmentedCalibrator

The fitted calibrator.

Source code in src/probcal/segmented.py
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
def fit(
    self,
    s: object,
    y: object,
    sample_weight: object = None,
    *,
    segments: object = None,
) -> "SegmentedCalibrator":
    """Fit the shared base map, then per-segment shrunk offsets.

    Parameters
    ----------
    s : array_like
        Raw scores/probabilities in ``[0, 1]``.
    y : array_like
        Binary outcomes in ``{0, 1}``; both classes must be present
        overall (individual segments may be single-class — see class
        docstring).
    sample_weight : array_like or None
        Positive observation weights.
    segments : array_like or None, keyword-only
        Segment label per observation, same length as ``s``. ``None``
        (default) fits a single segment ``"__all__"``.

    Returns
    -------
    SegmentedCalibrator
        The fitted calibrator.
    """
    self._segments_arg = segments
    try:
        return super().fit(s, y, sample_weight)  # type: ignore[return-value]
    finally:
        self.__dict__.pop("_segments_arg", None)

predict_proba

predict_proba(s: object, *, segments: object = None) -> ndarray

Calibrated probabilities, with an optional per-observation segment offset.

PARAMETER DESCRIPTION
s

Raw scores/probabilities in [0, 1].

TYPE: array_like

segments

Segment label per observation, same length as s. None (default) returns the plain base map (delta=0); a label not in :attr:segments_ is handled per unseen.

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

RETURNS DESCRIPTION
numpy.ndarray of shape (n,)

Calibrated probabilities.

Source code in src/probcal/segmented.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
def predict_proba(self, s: object, *, segments: object = None) -> np.ndarray:
    """Calibrated probabilities, with an optional per-observation segment offset.

    Parameters
    ----------
    s : array_like
        Raw scores/probabilities in ``[0, 1]``.
    segments : array_like or None, keyword-only
        Segment label per observation, same length as ``s``. ``None``
        (default) returns the plain base map (``delta=0``); a label not
        in :attr:`segments_` is handled per ``unseen``.

    Returns
    -------
    numpy.ndarray of shape (n,)
        Calibrated probabilities.
    """
    self._check_fitted()
    s_arr = validate_scores(s)
    p0 = self.base_.predict_proba(s_arr)
    if segments is None:
        return p0
    deltas = self._lookup_deltas(_coerce_segments(segments, s_arr.shape[0]))
    return expit(logit(p0) + deltas)

interval_inverse

interval_inverse(lo: float, hi: float, *, space: str = 'probability', buffer_logit: float = 0.0, segment: object = None) -> tuple[float, float]

Preimage of a calibrated interval, optionally for one fitted segment.

segment=None (default) uses the global map (delta=0), matching :meth:predict_proba's segments=None convention; otherwise the preimage is through base_ composed with that segment's delta_tilde (Chain([base_, LogitOffset(delta=...)])).

PARAMETER DESCRIPTION
lo

Calibrated-probability bounds.

TYPE: float

hi

Calibrated-probability bounds.

TYPE: float

space

Scale of the returned raw bounds.

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

buffer_logit

Logit-space shrinkage applied before inverting.

TYPE: (float, keyword - only) DEFAULT: 0.0

segment

Segment label to invert through; None for the global map.

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

RETURNS DESCRIPTION
tuple of float

(raw_lo, raw_hi) on the requested scale.

Source code in src/probcal/segmented.py
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
def interval_inverse(
    self,
    lo: float,
    hi: float,
    *,
    space: str = "probability",
    buffer_logit: float = 0.0,
    segment: object = None,
) -> tuple[float, float]:
    """Preimage of a calibrated interval, optionally for one fitted segment.

    ``segment=None`` (default) uses the global map (``delta=0``),
    matching :meth:`predict_proba`'s ``segments=None`` convention;
    otherwise the preimage is through ``base_`` composed with that
    segment's ``delta_tilde`` (``Chain([base_, LogitOffset(delta=...)])``).

    Parameters
    ----------
    lo, hi : float
        Calibrated-probability bounds.
    space : {"probability", "logit"}, keyword-only
        Scale of the returned raw bounds.
    buffer_logit : float, keyword-only
        Logit-space shrinkage applied before inverting.
    segment : str or None, keyword-only
        Segment label to invert through; ``None`` for the global map.

    Returns
    -------
    tuple of float
        ``(raw_lo, raw_hi)`` on the requested scale.
    """
    self._check_fitted()
    target = self._segment_chain_or_base(segment)
    return target.interval_inverse(lo, hi, space=space, buffer_logit=buffer_logit)

point_inverse

point_inverse(p: object, *, space: str = 'probability', segment: object = None) -> ndarray

Exact preimage of calibrated probabilities, optionally for one segment.

Same segment= convention as :meth:interval_inverse: inverts through base_ (composed with the segment's delta_tilde when segment is given), so this works for any number of fitted segments as long as base_ itself has an exact point inverse (base_.affine_logit_coeffs_ is not None) — unlike :attr:affine_logit_coeffs_ on self, which is only defined for a single fitted segment.

PARAMETER DESCRIPTION
p

Calibrated probabilities strictly inside (0, 1).

TYPE: array_like

space

Scale of the returned raw values.

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

segment

Segment label to invert through; None for the global map.

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

RETURNS DESCRIPTION
ndarray

Raw scores (or logits) whose calibrated probability equals p.

Source code in src/probcal/segmented.py
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
def point_inverse(
    self, p: object, *, space: str = "probability", segment: object = None
) -> np.ndarray:
    """Exact preimage of calibrated probabilities, optionally for one segment.

    Same ``segment=`` convention as :meth:`interval_inverse`: inverts
    through ``base_`` (composed with the segment's ``delta_tilde`` when
    ``segment`` is given), so this works for any number of fitted
    segments as long as ``base_`` itself has an exact point inverse
    (``base_.affine_logit_coeffs_`` is not ``None``) — unlike
    :attr:`affine_logit_coeffs_` on ``self``, which is only defined for
    a single fitted segment.

    Parameters
    ----------
    p : array_like
        Calibrated probabilities strictly inside ``(0, 1)``.
    space : {"probability", "logit"}, keyword-only
        Scale of the returned raw values.
    segment : str or None, keyword-only
        Segment label to invert through; ``None`` for the global map.

    Returns
    -------
    numpy.ndarray
        Raw scores (or logits) whose calibrated probability equals ``p``.
    """
    self._check_fitted()
    target = self._segment_chain_or_base(segment)
    return target.point_inverse(p, space=space)

interpret

interpret() -> Interpretation

Per-segment shrinkage table plus the fitted heterogeneity variance.

Source code in src/probcal/segmented.py
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
def interpret(self) -> Interpretation:
    """Per-segment shrinkage table plus the fitted heterogeneity variance."""
    self._check_fitted()
    param_names = ("tau2", *(f"delta.{g}" for g in self.segments_))
    param_values = (self.tau2_, *(float(d) for d in self.delta_tilde_))
    messages = [
        f"tau2 = {self.tau2_:.4f}: between-segment heterogeneity variance "
        "(DerSimonian-Laird method of moments on the per-segment offset MLEs); "
        "tau2 = 0 means complete pooling (every delta_tilde = 0)",
    ]
    for g, n, ev, dh, se, dt, sh in zip(
        self.segments_,
        self.n_,
        self.events_,
        self.delta_hat_,
        self.se_,
        self.delta_tilde_,
        self.shrink_,
        strict=True,
    ):
        se_str = "inf" if not np.isfinite(se) else f"{se:.4f}"
        messages.append(
            f"segment {g!r}: n={int(n)}, events={ev:.1f}, delta_hat={dh:+.4f}, "
            f"se={se_str}, delta_tilde={dt:+.4f}, shrink={sh:.3f}"
        )
    if self.unseen == "global":
        messages.append(
            f"unseen segments at predict/inverse time use delta=0 (unseen={self.unseen!r})"
        )
    else:
        messages.append(
            f"unseen segments at predict/inverse time raise ValueError (unseen={self.unseen!r})"
        )
    return Interpretation(
        method="SegmentedCalibrator",
        param_names=param_names,
        param_values=param_values,
        messages=tuple(messages),
    )