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: JsonIO, 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
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
229
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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
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))

__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
263
264
265
266
267
268
269
270
271
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
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
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
304
305
306
@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
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
385
386
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()
    self._check_monotone()
    validate_interval(lo, hi)
    validate_space(space)
    lo_b, hi_b = shrink_interval(lo, hi, buffer_logit)
    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 logit1(raw_lo)
        hi_out = np.inf if raw_hi >= 1.0 else logit1(raw_hi)
        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
392
393
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
433
434
435
436
437
438
439
440
441
442
443
444
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()
    self._check_monotone()
    validate_space(space)
    arr = _validate_point_targets(p)
    z = self._point_inverse_logit(arr)
    _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
513
514
515
516
517
518
519
520
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
522
523
524
525
526
527
528
529
530
531
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__}; 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
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
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()
    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 envelope(
        self,
        params=encode_value(self._params_for_dict()),
        state=self._state(),
        fit_meta=encode_value(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
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
615
616
617
618
619
620
621
622
@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.
    """
    if cls is BaseCalibrator:
        from ._registry import load

        return load(d)  # type: ignore[return-value]
    check_payload(cls, d)
    params = cls._params_from_dict(
        decode_value(d.get("params", {}), arrays=False)  # type: ignore[arg-type]
    )
    obj = cls(**params)
    obj._set_state(dict(d.get("state", {})))
    obj.fit_meta_ = dict(decode_value(d.get("fit_meta", {}), arrays=False))  # type: ignore[call-overload]
    obj.fitted_ = True
    return obj

shrink_interval

shrink_interval(lo: float, hi: float, buffer_logit: float) -> tuple[float, float]

Shrink [lo, hi] by buffer_logit on the logit scale (0/1 ends stay put).

Shared by every interval_inverse: robustness against future recalibration drift of magnitude <= buffer_logit.

RAISES DESCRIPTION
ValueError

If buffer_logit is negative or not finite.

UnattainableTargetError

If the buffer empties the interval.

Source code in src/probcal/base.py
 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
def shrink_interval(lo: float, hi: float, buffer_logit: float) -> tuple[float, float]:
    """Shrink ``[lo, hi]`` by ``buffer_logit`` on the logit scale (0/1 ends stay put).

    Shared by every ``interval_inverse``: robustness against future
    recalibration drift of magnitude ``<= buffer_logit``.

    Raises
    ------
    ValueError
        If ``buffer_logit`` is negative or not finite.
    UnattainableTargetError
        If the buffer empties the interval.
    """
    buffer_logit = float(buffer_logit)
    if not buffer_logit >= 0.0 or buffer_logit == float("inf"):
        raise ValueError(f"buffer_logit must be a finite value >= 0, got {buffer_logit!r}")
    lo_b, hi_b = float(lo), float(hi)
    if buffer_logit > 0.0:
        if lo > 0.0:
            lo_b = expit1(logit1(lo) + buffer_logit)
        if hi < 1.0:
            hi_b = expit1(logit1(hi) - buffer_logit)
        if lo_b > hi_b:
            raise UnattainableTargetError(
                f"buffer_logit={buffer_logit} empties the calibrated interval [{lo}, {hi}]"
            )
    return lo_b, hi_b

validate_interval

validate_interval(lo: float, hi: float) -> None

0 <= lo <= hi <= 1.

Source code in src/probcal/base.py
118
119
120
121
def validate_interval(lo: float, hi: float) -> None:
    """``0 <= lo <= hi <= 1``."""
    if not 0.0 <= lo <= hi <= 1.0:
        raise ValueError(f"need 0 <= lo <= hi <= 1, got lo={lo}, hi={hi}")

clone_unfitted

clone_unfitted(obj: object) -> object

A fresh, unfitted copy built from obj.get_params(deep=False).

Parameters that are themselves estimators (anything with get_params) — directly or inside a list, tuple, or dict, such as a :class:probcal.Chain's stages or a selector's candidates — are cloned recursively, so the copy never shares a sub-object with obj.

Source code in src/probcal/base.py
135
136
137
138
139
140
141
142
143
144
145
146
147
def clone_unfitted(obj: object) -> object:
    """A fresh, unfitted copy built from ``obj.get_params(deep=False)``.

    Parameters that are themselves estimators (anything with
    ``get_params``) — directly or inside a list, tuple, or dict, such as a
    :class:`probcal.Chain`'s stages or a selector's candidates — are cloned
    recursively, so the copy never shares a sub-object with ``obj``.
    """
    try:
        params = obj.get_params(deep=False)  # type: ignore[attr-defined]
    except TypeError:
        params = obj.get_params()  # type: ignore[attr-defined]
    return type(obj)(**{k: _clone_param(v) for k, v in params.items()})

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
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
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: the score in u = 1/T is monotone, so its root is found by bisection on u in [1e-6, 1e6]; without an interior minimum the nearer boundary is used and a UserWarning is raised.

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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
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. :meth:point_inverse is exact for every variant, including the non-affine "abm" (see :meth:_point_inverse_logit).

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
350
351
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.

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
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
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
68
69
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
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
141
142
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
173
174
def __init__(self) -> None:
    super().__init__(interpolation="linear")

interpret

interpret() -> Interpretation

Isotonic reading plus the strictness property CIR adds.

Source code in src/probcal/isotonic.py
180
181
182
183
184
185
186
187
188
189
190
191
192
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-mass, recommended default: lower estimator bias, no empty bins; with sample_weight the bins carry equal weight) 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
60
61
62
63
64
65
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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 (equal weight under sample_weight).

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

is_monotone_

Derived from the fitted state: True when the Platt stage is increasing (a > 0) — the bin means are then non-decreasing by construction. A decreasing Platt stage makes the composite map decreasing, and interval_inverse refuses it.

TYPE: bool

References

Kumar, Liang & Ma (2019).

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

is_monotone_ property

is_monotone_: bool

True iff the Platt stage is increasing (see class docstring).

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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
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, monotone: bool = False)

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.

Nothing in the model forces the bin rates to increase with the score, and on low-default data a sparse bin often dips below its neighbours, so the averaged map is usually not monotone (is_monotone_ = False) and refuses to invert. monotone=True projects each candidate's bin rates onto the non-decreasing cone (weighted PAVA, weights = posterior counts) before averaging; a non-negative mixture of non-decreasing step functions is non-decreasing, so the averaged map is then monotone by construction. The model weights are unchanged (they score the unconstrained binnings).

PARAMETER DESCRIPTION
min_bins

Range of candidate bin counts; defaults to [2, ceil(sqrt(n))] capped at 50. Bins are equal-mass in the sample weights.

TYPE: int or None DEFAULT: None

max_bins

Range of candidate bin counts; defaults to [2, ceil(sqrt(n))] capped at 50. Bins are equal-mass in the sample weights.

TYPE: int or None DEFAULT: None

monotone

Isotonize each candidate's bin rates before averaging, so the map is guaranteed non-decreasing and interval_inverse / calibrated_bands_to_raw are available. Leaves fits that are already monotone in every candidate unchanged.

TYPE: bool DEFAULT: False

ATTRIBUTE DESCRIPTION
bins_grid_

Candidate bin counts.

TYPE: ndarray

model_weights_

Posterior weights over the candidates (sum to 1).

TYPE: ndarray

model_edges_, model_rates_

Interior edges and posterior-mean bin rates of each candidate binning (isotonized when monotone=True).

TYPE: list of numpy.ndarray

is_monotone_

Whether the averaged step map is non-decreasing over the whole score domain — checked exactly at every bin edge of every candidate.

TYPE: bool

References

Naeini, Cooper & Hauskrecht (2015).

Source code in src/probcal/bayesian.py
78
79
80
81
82
83
def __init__(
    self, min_bins: int | None = None, max_bins: int | None = None, monotone: bool = False
) -> None:
    self.min_bins = min_bins
    self.max_bins = max_bins
    self.monotone = monotone

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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
def interpret(self) -> Interpretation:
    """Read the posterior weights as uncertainty about the data's resolution."""
    self._check_fitted()
    wts = self.model_weights_
    top = np.argsort(wts)[::-1][:3]
    top_txt = ", ".join(f"B={int(self.bins_grid_[i])} (weight {wts[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,)

model_weights_

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

TYPE: numpy.ndarray of shape (K,)

scores_

Distinct calibration scores: the grid path_solutions_ lives on.

TYPE: numpy.ndarray of shape (m,)

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
245
246
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
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
def interpret(self) -> Interpretation:
    """Read the path length and BIC weights; warn about non-monotonicity."""
    self._check_fitted()
    wts = self.model_weights_
    top = np.argsort(wts)[::-1][:3]
    top_txt = ", ".join(
        f"lambda={self.path_lambdas_[self.kept_breakpoints_[i]]:.4g} (weight {wts[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.

Tied calibration scores are pooled into one point (weights summed), as isotonic regression requires, and a query whose score equals a calibration score joins that point; the output therefore does not depend on row order. A query labeled 1 (resp. 0) strictly between two calibration scores has the same fitted value as when merged into the next (resp. previous) point, so the two tables are read at searchsorted "left" (F1_) and "right" (F0_).

ATTRIBUTE DESCRIPTION
scores_

Distinct calibration scores, sorted.

TYPE: numpy.ndarray of shape (m,)

F0_, F1_

Fitted probabilities for a unit-weight query labeled 0 (resp. 1) inserted at each of the m+1 gaps between the distinct calibration scores. Both are non-decreasing, and F0_ <= F1_ elementwise.

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

ties_pooled_

True for every fit. False only for a payload re-saved by probcal 0.3.4/0.3.5 from an older fit (stored tables, unpooled lookup).

TYPE: bool

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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
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()
    return self._interval(validate_scores(s))

interpret

interpret() -> Interpretation

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

Source code in src/probcal/vennabers.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def interpret(self) -> Interpretation:
    """Report interval widths over the calibration scores — where to trust the map."""
    self._check_fitted()
    intervals = self._interval(self.scores_)
    widths = intervals[:, 1] - intervals[:, 0]
    mean_w, max_w = float(widths.mean()), float(widths.max())
    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 "
            "distinct 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; an integer >= 2.

TYPE: int DEFAULT: 5

random_state

Seed for the fold assignment.

TYPE: int DEFAULT: 42

ATTRIBUTE DESCRIPTION
ivaps_

One fitted IVAP per fold, each trained on the other cv - 1 folds.

TYPE: list of VennAbersCalibrator

References

Vovk & Petej (2014).

Source code in src/probcal/vennabers.py
225
226
227
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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
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()
    p0, p1 = self._fold_pairs(validate_scores(s))
    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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
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 package's safeguarded Newton solver (:func:probcal._math.irls_logistic with a penalty matrix: step-halving, full-step convergence test, separation fallback). 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 (finite, non-negative); defaults to logspace(-4, 4, 17).

TYPE: array_like or None DEFAULT: None

cv

Inner fold count for the lambda search; an integer >= 2.

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

knots_

Knot locations on the logit scale.

TYPE: ndarray

theta_

Spline coefficients on the natural cubic basis.

TYPE: ndarray

converged_

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

TYPE: bool

is_monotone_

Checked exactly after fitting: the logit-scale slope is evaluated at every knot (which also gives both linear tails) and at each interior extremum of the piecewise-quadratic slope. The penalty does not enforce monotonicity, and a 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
 99
100
101
102
103
104
105
106
107
108
109
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
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
232
233
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 (warned at fit)")
    if not self.converged_:
        messages.append("penalized IRLS did not converge; the curve may be unreliable")
    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 (:func:~probcal.base.clone_unfitted) and fitted on the pooled data at :meth:fit time. 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 standard error (:func:~probcal.offset.estimate_offset; se=inf for single-class segments). Invariant to the scale of sample_weight.

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_ (read through, so it survives serialization).

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

is_monotone_ property

is_monotone_: bool

The fitted base map's is_monotone_ (True before fitting).

A per-segment logit shift is increasing, so it cannot break monotonicity; the base map alone decides.

complexity_rank property

complexity_rank: float

The base map's parsimony rank (fitted base_, else the prototype).

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)
    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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
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
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
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
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
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
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
433
434
435
436
437
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,
    ):
        messages.append(
            f"segment {g!r}: n={int(n)}, events={ev:.1f}, delta_hat={dh:+.4f}, "
            f"se={se:.4f}, delta_tilde={dt:+.4f}, shrink={sh:.3f}"
        )
    policy = "use delta=0" if self.unseen == "global" else "raise ValueError"
    messages.append(
        f"unseen segments at predict/inverse time {policy} (unseen={self.unseen!r})"
    )
    return Interpretation(
        method=type(self).__name__,
        param_names=param_names,
        param_values=param_values,
        messages=tuple(messages),
    )