API: tools¶
offset
¶
Logit-offset (central tendency) adjustment with audit trail.
Theory — including the King–Zeng / Elkan / Tasche equivalences and the
uniqueness of the mode-B root: docs/concepts/offset.md.
References
King & Zeng (2001); Elkan (2001); Tasche (2013) — full records in the documentation.
AuditReport
dataclass
¶
AuditReport(delta: float, pre_mean: float, post_mean: float, timestamp: str, guardrails_before: GuardrailReport, guardrails_after: GuardrailReport)
Pre/post record of a logit-offset application, for validators.
| ATTRIBUTE | DESCRIPTION |
|---|---|
delta |
Applied log-odds shift.
TYPE:
|
pre_mean, post_mean |
Portfolio mean probability before and after the shift.
TYPE:
|
timestamp |
ISO-8601 UTC time at which the offset was fitted.
TYPE:
|
guardrails_before, guardrails_after |
The three-flag calibration health summary on the input and output probabilities.
TYPE:
|
LogitOffset
¶
LogitOffset(delta: float | None = None, target_mean: float | None = None)
Bases: JsonIO
Uniform log-odds shift: p' = sigma(logit(p) + delta).
Mode A takes delta explicitly; mode B takes target_mean and
solves mean(p') = target_mean for delta by bisection — the
portfolio mean is strictly increasing in delta, so the root is
unique (stated in the offset chapter and unit-tested). Exactly one of
the two arguments must be given.
The offset is deliberately not folded into any calibrator's
parameters: CalibratedModel.offset_to appends it as a separate,
inspectable pipeline stage.
| PARAMETER | DESCRIPTION |
|---|---|
delta
|
Mode A: the log-odds shift to apply directly. Mutually exclusive
with
TYPE:
|
target_mean
|
Mode B: the desired post-shift portfolio mean probability in
TYPE:
|
| ATTRIBUTE | DESCRIPTION |
|---|---|
delta_ |
Fitted (or given) shift in log-odds.
TYPE:
|
pre_mean_, post_mean_ |
Portfolio mean before and after, recorded at fit time.
TYPE:
|
timestamp_ |
ISO-8601 UTC fit time — part of the audit trail.
TYPE:
|
Source code in src/probcal/offset.py
148 149 150 | |
affine_logit_coeffs_
property
¶
affine_logit_coeffs_: tuple[float, float]
(1, delta): the offset is affine on the logit scale.
fit
¶
fit(p: object, *, y: object = None, sample_weight: object = None) -> Self
Fix delta (mode A) or solve it against the target mean (mode B).
| PARAMETER | DESCRIPTION |
|---|---|
p
|
Current calibrated probabilities of the portfolio.
TYPE:
|
y
|
Ignored; accepted for compatibility with the chain fit protocol.
TYPE:
|
sample_weight
|
Weights for the portfolio mean.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Self
|
The fitted offset. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If not exactly one of |
UnattainableTargetError
|
If |
Source code in src/probcal/offset.py
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | |
transform
¶
transform(p: object) -> ndarray
Apply the fitted shift to probabilities.
Source code in src/probcal/offset.py
206 207 208 209 | |
__sklearn_is_fitted__
¶
__sklearn_is_fitted__() -> bool
Fitted state for sklearn >= 1.6 (delta_ fixed or solved).
Source code in src/probcal/offset.py
213 214 215 | |
interpret
¶
interpret() -> Interpretation
Read delta in log-odds, odds-factor, and central-tendency terms.
Source code in src/probcal/offset.py
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | |
interval_inverse
¶
interval_inverse(lo: float, hi: float, *, space: str = 'probability', buffer_logit: float = 0.0) -> tuple[float, float]
Closed-form preimage: subtract delta on the logit scale.
Same protocol as BaseCalibrator.interval_inverse; the offset's
output range is the full unit interval, so only a crossed buffer can
make a target unattainable.
| PARAMETER | DESCRIPTION |
|---|---|
lo
|
Calibrated-probability bounds;
TYPE:
|
hi
|
Calibrated-probability bounds;
TYPE:
|
space
|
Scale of the returned raw bounds.
TYPE:
|
buffer_logit
|
Shrink the calibrated interval by this margin in logit space
before inverting (must be finite and
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
tuple of float
|
|
| RAISES | DESCRIPTION |
|---|---|
UnattainableTargetError
|
If a crossed |
ValueError
|
If |
Source code in src/probcal/offset.py
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 | |
point_inverse
¶
point_inverse(p: object, *, space: str = 'probability') -> ndarray
Raw scores whose shifted probabilities equal p (exact preimage).
Closed form: subtract delta on the logit scale. Same protocol
and boundary doctrine as :meth:BaseCalibrator.point_inverse
(shared validation helpers).
| PARAMETER | DESCRIPTION |
|---|---|
p
|
Shifted probabilities strictly inside
TYPE:
|
space
|
Scale of the returned raw values.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ndarray
|
Raw scores (or logits, if |
| RAISES | DESCRIPTION |
|---|---|
RuntimeError
|
If not yet fitted. |
ValueError
|
If |
UnattainableTargetError
|
If any element of |
Source code in src/probcal/offset.py
316 317 318 319 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 | |
audit_report
¶
audit_report(y: object, p: object, *, sample_weight: object = None) -> AuditReport
Pre/post guardrail comparison for the validator's one-table view.
Source code in src/probcal/offset.py
358 359 360 361 362 363 364 365 366 367 368 369 370 | |
to_dict
¶
to_dict() -> dict[str, object]
Versioned JSON-native snapshot (see BaseCalibrator.to_dict).
fit_meta records n_obs, weight_sum, fitted_at_utc,
and the data_fingerprint of the (p, w) pair — no n_events
because the offset is fitted on probabilities alone. fingerprint
is blind to the audit timestamp_: identical fits fingerprint
identically.
Source code in src/probcal/offset.py
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 | |
from_dict
classmethod
¶
from_dict(d: dict) -> LogitOffset
Rebuild a fitted offset from :meth:to_dict output.
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If the schema version is unknown or the payload class differs. |
Source code in src/probcal/offset.py
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 | |
estimate_offset
¶
estimate_offset(y: object, p: object, *, sample_weight: object = None) -> OffsetEstimate
Offset-only logistic MLE of delta given p, with its standard error.
Fits the single-parameter model y ~ Bernoulli(sigma(logit(p) + delta))
by maximum likelihood. The score equation
sum(w * (y - sigma(logit(p) + delta))) = 0 is exactly the mean-matching
condition solved by LogitOffset(target_mean=mean_w(y)), so delta is
found by the same bisection root-finder (:func:_offset_mle, shared with
probcal.monitor._processes.plug_in_delta). Weights are relative
frequency weights (:mod:probcal.metrics._common): delta and se
do not change when every weight is multiplied by a constant. With
q = sigma(logit(p) + delta), I = sum(w * q * (1 - q)) and
J = sum(w**2 * q * (1 - q)) (the exact variance of the weighted score
when each row is one Bernoulli draw), the standard error is
sqrt(J) / I; for unit weights that is the Fisher 1 / sqrt(I).
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Binary outcomes in
TYPE:
|
p
|
Predicted probabilities in
TYPE:
|
sample_weight
|
Optional non-negative weights, same length as
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
OffsetEstimate
|
The fitted |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
Examples:
>>> import numpy as np
>>> from probcal.offset import estimate_offset
>>> from probcal._math import expit
>>> rng = np.random.default_rng(0)
>>> z = rng.normal(0.0, 1.0, 2000)
>>> p = expit(z)
>>> y = (rng.random(2000) < expit(z + 0.5)).astype(float)
>>> est = estimate_offset(y, p)
>>> est.n
2000
>>> abs(est.delta - 0.5) < 3 * est.se
True
Source code in src/probcal/offset.py
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 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 | |
offset_from_estimate
¶
offset_from_estimate(est: OffsetEstimate, p: object) -> LogitOffset
Build a fitted :class:LogitOffset (mode A) from an :class:OffsetEstimate.
Equivalent to LogitOffset(delta=est.delta).fit(p) — a convenience for
turning the audited MLE into the same offset object used elsewhere in
the package (transform, interpret, to_dict, ...).
| PARAMETER | DESCRIPTION |
|---|---|
est
|
Result of :func:
TYPE:
|
p
|
Probabilities to fit the offset's audit trail (
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
LogitOffset
|
Fitted with |
Examples:
>>> import numpy as np
>>> from probcal.offset import estimate_offset, offset_from_estimate
>>> rng = np.random.default_rng(0)
>>> p = rng.uniform(0.01, 0.5, 500)
>>> y = (rng.random(500) < p).astype(float)
>>> est = estimate_offset(y, p)
>>> off = offset_from_estimate(est, p)
>>> off.delta_ == est.delta
True
Source code in src/probcal/offset.py
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 | |
OffsetEstimate
dataclass
¶
OffsetEstimate(delta: float, se: float, n: int, events: float, weight_sum: float)
Bases: _ResultBase
Offset-only logistic MLE of delta given p, with its standard error.
| ATTRIBUTE | DESCRIPTION |
|---|---|
delta |
MLE of the logit-offset shift: the mean-matching root of
TYPE:
|
se |
Asymptotic standard error of
TYPE:
|
n |
Number of observations.
TYPE:
|
events |
Weighted event count,
TYPE:
|
weight_sum |
Sum of weights (equals
TYPE:
|
wrapper
¶
CalibratedModel: model-level wrapper with prefit and cross-validation flows.
Theory of the flows (why prefit is the credit-risk canon, why the pooled cv
variant is the recommended default): docs/concepts/data-splitting.md.
CalibratedModel
¶
CalibratedModel(model: Any, calibrator: BaseCalibrator, flow: str = 'prefit', cv: int = 5, ensemble: bool = False, random_state: int = 42, *, model_id: str | None = None)
Bases: CalibratorProtocolMixin, JsonIO
Wrap any scoring model with a probcal calibrator (and optional offsets).
| PARAMETER | DESCRIPTION |
|---|---|
model
|
Duck-typed model with
TYPE:
|
calibrator
|
Unfitted calibrator instance (its parameters are cloned per fold in
the cv flow via
TYPE:
|
flow
|
TYPE:
|
cv
|
Fold count for the cv flow (stratified, seeded); an integer
TYPE:
|
ensemble
|
TYPE:
|
random_state
|
Seed for the fold assignment.
TYPE:
|
| ATTRIBUTE | DESCRIPTION |
|---|---|
model_ |
The deployed model (the input model for prefit; the full-data refit for pooled cv).
TYPE:
|
calibrator_ |
The fitted calibrator (pooled/prefit flows).
TYPE:
|
ensemble_ |
The fold pairs (ensemble flow only).
TYPE:
|
offsets_ |
Appended offset stages, each separately inspectable.
TYPE:
|
Notes
interval_inverse and point_inverse invert the full pipeline
(calibrator, then every offset) through :attr:chain_ — see
:meth:Chain.interval_inverse and :meth:Chain.point_inverse; the
bounds are on the model's probability output (or its logit). The
ensemble flow (K distinct maps) has no single preimage, so both raise
NotImplementedError there.
Source code in src/probcal/wrapper.py
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | |
is_monotone_
property
¶
is_monotone_: bool
Monotone iff every calibrator stage is (offsets always are).
affine_logit_coeffs_
property
¶
affine_logit_coeffs_: tuple[float, float] | None
Composed (a, b + sum(deltas)) when the calibrator stage is affine.
chain_
property
¶
chain_: Chain
The equivalent model-free :class:probcal.Chain (calibrator + offsets).
Hand this to a recourse engine when the base model stays behind:
the chain calibrates on the model probability, so its
space="logit" bounds are bounds on the raw margin.
The returned chain aliases this wrapper's own fitted calibrator and
offsets rather than copying them, so calling fit on the chain
refits this :class:CalibratedModel's calibrator and offsets in
place.
| RAISES | DESCRIPTION |
|---|---|
NotImplementedError
|
For the ensemble flow. |
fit
¶
fit(X: object, y: object, sample_weight: object = None) -> Self
Fit the calibration stage per the configured flow.
| PARAMETER | DESCRIPTION |
|---|---|
X
|
Calibration-set inputs, passed untouched to the model
(
TYPE:
|
y
|
Binary outcomes in
TYPE:
|
sample_weight
|
Positive observation weights. Always used by the calibrator; in
the cv flow also forwarded to
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Self
|
The fitted wrapper. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
TypeError
|
If |
Source code in src/probcal/wrapper.py
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 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 | |
__sklearn_is_fitted__
¶
__sklearn_is_fitted__() -> bool
Fitted state for sklearn >= 1.6 (model and calibrator both fitted).
Source code in src/probcal/wrapper.py
333 334 335 | |
predict_proba
¶
predict_proba(X: object) -> ndarray
Calibrated (and offset) probabilities P(y=1) for new inputs.
| PARAMETER | DESCRIPTION |
|---|---|
X
|
New inputs, passed untouched to the deployed model (or every ensemble fold's model, averaged).
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
numpy.ndarray of shape (n,)
|
Calibrated probabilities, after any appended offset stages. |
Source code in src/probcal/wrapper.py
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 | |
offset_to
¶
offset_to(*, target_mean: float | None = None, delta: float | None = None, X: object = None, sample_weight: object = None) -> Self
Append an inspectable :class:LogitOffset stage.
Mode B (target_mean) anchors the portfolio mean of the current
pipeline output — computed on X when given (weighted by
sample_weight), else on the stored calibration scores, weighted
by the sample_weight given to :meth:fit. The offset is never
folded into the calibrator's parameters.
| PARAMETER | DESCRIPTION |
|---|---|
target_mean
|
Mode B: desired post-shift portfolio mean; mutually exclusive
with
TYPE:
|
delta
|
Mode A: the log-odds shift to apply directly; mutually exclusive
with
TYPE:
|
X
|
Inputs to compute the current pipeline output on;
TYPE:
|
sample_weight
|
Positive weights for the portfolio mean on
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Self
|
The wrapper, with the new offset appended to |
Source code in src/probcal/wrapper.py
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 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 | |
with_offset
¶
with_offset(offset: LogitOffset) -> CalibratedModel
A deep copy of this wrapper with an already-fitted offset appended.
Needs no data: unlike :meth:offset_to, nothing is fitted — the
given offset (e.g. from :func:probcal.offset_from_estimate or a
monitor recommendation) is copied and appended as the last stage.
self is left untouched.
| PARAMETER | DESCRIPTION |
|---|---|
offset
|
A fitted offset.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
CalibratedModel
|
The new wrapper ( |
| RAISES | DESCRIPTION |
|---|---|
TypeError
|
If |
RuntimeError
|
If this wrapper or the offset is not fitted. |
Source code in src/probcal/wrapper.py
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 | |
interpret
¶
interpret() -> Interpretation
Concatenated interpretation of the calibrator and every offset stage.
| RETURNS | DESCRIPTION |
|---|---|
Interpretation
|
Parameters and messages concatenated across the calibrator stage(s) and every appended offset, in application order. |
Source code in src/probcal/wrapper.py
482 483 484 485 486 487 488 489 490 491 492 493 | |
to_dict
¶
to_dict() -> dict[str, object]
Versioned snapshot: nested calibrator, offsets, and a model reference.
The base model is never serialized — only a reference (class name,
the user-supplied model_id, and get_params() when available
and JSON-encodable); reattach it on load via
CalibratedModel.from_dict(d, model=...). A wrapper loaded without
a model writes back the reference it was loaded with, so its
fingerprint is unchanged. The stored calibration
scores are not serialized either: after a reload,
offset_to needs an explicit X.
| RAISES | DESCRIPTION |
|---|---|
RuntimeError
|
If not yet fitted. |
NotImplementedError
|
For the ensemble flow: K fold models cannot be referenced. |
Source code in src/probcal/wrapper.py
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 560 561 562 563 564 565 566 567 568 569 | |
from_dict
classmethod
¶
from_dict(d: dict, model: Any = None) -> CalibratedModel
Rebuild a fitted wrapper, reattaching the base model.
| PARAMETER | DESCRIPTION |
|---|---|
d
|
Output of :meth:
TYPE:
|
model
|
The base model to reattach (matched against the stored reference
is the caller's responsibility). With
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If the schema version is unknown or the payload class differs. |
Source code in src/probcal/wrapper.py
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 | |
from_json
classmethod
¶
from_json(path_or_str: object, model: Any = None) -> Self
Load from a JSON string or a filesystem path (see :meth:from_dict).
Source code in src/probcal/wrapper.py
619 620 621 622 | |
interval_inverse
¶
interval_inverse(lo: float, hi: float, *, space: str = 'probability', buffer_logit: float = 0.0) -> tuple[float, float]
Preimage of a calibrated interval on the calibrator's input scale (delegated).
Source code in src/probcal/_protocol.py
39 40 41 42 43 44 45 | |
point_inverse
¶
point_inverse(p: object, *, space: str = 'probability') -> ndarray
Exact preimage of calibrated probabilities (delegated).
Source code in src/probcal/_protocol.py
47 48 49 | |
selection
¶
CalibratorSelector: automatic method selection under nested validation.
The selector's scoring path only ever receives out-of-fold predictions —
selection on fitting data is an unrepresentable state, not a documented
misuse. Protocol, criteria, and report reading:
docs/concepts/auto-selection.md.
CalibratorSelector
¶
CalibratorSelector(candidates: dict[str, BaseCalibrator] | None = None, scoring: str = 'log_loss', cv: int = 5, random_state: int = 42)
Bases: CalibratorProtocolMixin, BaseCalibrator
Choose a calibrator by inner cross-validation on the calibration data.
Custom candidates declare their tie-break position by overriding
complexity_rank (lower = simpler; default 100.0 ranks last).
Once fitted, the selector is its winner for every protocol question:
predict_proba, interpret, is_monotone_,
affine_logit_coeffs_, complexity_rank, point_inverse and
interval_inverse all delegate to best_calibrator_. fit raises
ValueError if scoring is not one of the accepted criteria (plain
ECE and Hosmer–Lemeshow are refused as selection criteria), cv is
not an integer >= 2, or a class has fewer than cv members (every
held-out fold is scored on its own and needs both classes; lower cv
for low-default data).
| PARAMETER | DESCRIPTION |
|---|---|
candidates
|
Candidate instances (cloned per fold via
TYPE:
|
scoring
|
Out-of-fold selection criterion, lower is better. Plain ECE and Hosmer–Lemeshow are refused — see the metrics chapter's table.
TYPE:
|
cv
|
Inner stratified fold count; an integer
TYPE:
|
random_state
|
Seed for the fold assignment.
TYPE:
|
| ATTRIBUTE | DESCRIPTION |
|---|---|
best_name_ |
Winning candidate's name.
TYPE:
|
best_calibrator_ |
The winner refitted on the full calibration set.
TYPE:
|
report_ |
Ranked table: mean ± sd of the criterion, guardrail flags, chosen marker.
TYPE:
|
Source code in src/probcal/selection.py
93 94 95 96 97 98 99 100 101 102 103 | |
complexity_rank
property
¶
complexity_rank: float
The winner's parsimony rank once fitted; 100.0 before.
affine_logit_coeffs_
property
¶
affine_logit_coeffs_: tuple[float, float] | None
Affine-logit coefficients of the calibration map, if any (delegated).
interpret
¶
interpret() -> Interpretation
The fitted calibrator's plain-language reading (delegated).
Source code in src/probcal/_protocol.py
51 52 53 | |
interval_inverse
¶
interval_inverse(lo: float, hi: float, *, space: str = 'probability', buffer_logit: float = 0.0) -> tuple[float, float]
Preimage of a calibrated interval on the calibrator's input scale (delegated).
Source code in src/probcal/_protocol.py
39 40 41 42 43 44 45 | |
point_inverse
¶
point_inverse(p: object, *, space: str = 'probability') -> ndarray
Exact preimage of calibrated probabilities (delegated).
Source code in src/probcal/_protocol.py
47 48 49 | |
curves
¶
Reliability-curve builders and the GiViTI-style calibration belt.
Numpy-only; every result is a frozen dataclass carrying both probability- and
logit-scale coordinates, plotting-backend-agnostic (rendering lives in
probcal.plots). Theory: docs/concepts/visualization.md.
References
Austin & Steyerberg (2014); Nattino, Finazzi & Bertolini (2014); Nattino, Lemeshow, Phillips, Finazzi & Bertolini (2017) — full records in the documentation. The belt is reimplemented from the papers; no GPL code is used.
EcceCurve
dataclass
¶
EcceCurve(frac: ndarray, cumdev: ndarray, sd_null: ndarray, stat_max: float, argmax_frac: float)
Bases: _ResultBase
Cumulative-deviation walk over predictions sorted ascending (ECCE).
Compares by value (arrays element-wise); unhashable.
| ATTRIBUTE | DESCRIPTION |
|---|---|
frac |
Cumulative weight fraction at the end of each block of tied
predictions (
TYPE:
|
cumdev |
Cumulative-deviation walk value at each
TYPE:
|
sd_null |
Pointwise standard deviation of the walk under calibration.
TYPE:
|
stat_max |
Maximum absolute value of
TYPE:
|
argmax_frac |
TYPE:
|
reliability_binned
¶
reliability_binned(y: object, p: object, *, n_bins: int = 10, strategy: str = 'mass', sample_weight: object = None) -> ReliabilityCurve
Binned reliability curve with Wilson confidence intervals.
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Outcomes and predicted probabilities.
TYPE:
|
p
|
Outcomes and predicted probabilities.
TYPE:
|
n_bins
|
Requested bin count.
TYPE:
|
strategy
|
Equal-count (default) or equal-width bins.
TYPE:
|
sample_weight
|
Weights for the bin means; Wilson CIs use raw counts.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ReliabilityCurve
|
Per-bin mean prediction, event rate, count, Wilson CI, and the logit-scale coordinates. |
Source code in src/probcal/curves.py
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
reliability_loess
¶
reliability_loess(y: object, p: object, *, frac: float = 0.75, grid_size: int = 100, sample_weight: object = None) -> SmoothReliabilityCurve
LOESS-smoothed reliability curve on a grid (Austin & Steyerberg, 2014).
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Outcomes and predicted probabilities.
TYPE:
|
p
|
Outcomes and predicted probabilities.
TYPE:
|
frac
|
LOESS smoothing fraction.
TYPE:
|
grid_size
|
Number of evaluation points, spanning the 0.5th to 99.5th percentile
of
TYPE:
|
sample_weight
|
Optional positive weights. Non-uniform weights multiply the tricube
kernel weights of each local-linear fit (same nearest-neighbour
windows);
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
SmoothReliabilityCurve
|
Grid coordinates (probability and logit scale) and the smoothed event rate at each point. |
Source code in src/probcal/curves.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
reliability_spline
¶
reliability_spline(y: object, p: object, *, grid_size: int = 100, sample_weight: object = None) -> SmoothReliabilityCurve
Spline-smoothed reliability curve on a grid.
Penalized natural cubic spline of the outcome on the logit prediction.
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Outcomes and predicted probabilities.
TYPE:
|
p
|
Outcomes and predicted probabilities.
TYPE:
|
grid_size
|
Number of evaluation points, spanning the 0.5th to 99.5th percentile
of
TYPE:
|
sample_weight
|
Optional positive weights passed to the spline fit.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
SmoothReliabilityCurve
|
Grid coordinates (probability and logit scale) and the smoothed event rate at each point. |
Source code in src/probcal/curves.py
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | |
reliability_smooth
¶
reliability_smooth(y: object, p: object, *, sample_weight: object = None, grid_size: int = 200, n_boot: int = 100, confidence: float = 0.9, random_state: int = 42, bins: int | None = 8192) -> KernelReliabilityCurve
smECE-consistent kernel reliability curve (Blasiok-Nakkiran).
Shares its bandwidth and lattice with metrics.smooth_ece: both solve
the same fixed point sigma_star on the same equal-width logit
lattice (metrics.smooth._smece_solve), so
curve.smooth_ece reproduces metrics.smooth_ece(y, p, bins=bins)
exactly instead of merely agreeing with it. The event rate and
prediction density are then Nadaraya-Watson kernel estimates at that one
fixed sigma_star — rate = K*bincount(w*y) / K*bincount(w) on the
lattice, interpolated onto grid_logit — using the same truncated
Gaussian kernel smooth_ece used to reach sigma_star
(metrics.smooth._lattice_kernel_smooth). When smooth_ece's path
selection falls back to its exact (non-lattice) computation — degenerate
logit range, bins=None, or an infeasible/under-resolved refinement —
the curve falls back the same way, to direct O(n * grid_size) Gaussian
smoothing on logit(p) at sigma_star.
The confidence ribbon bootstraps (y, p, sample_weight) triples
(numpy.random.default_rng(random_state), resampling with
replacement) and recomputes the rate at the point estimate's fixed
sigma_star — the ribbon conditions on the bandwidth, it does not
reflect uncertainty in choosing it. The ribbon is clamped to contain the
point estimate (ci_low <= event_rate <= ci_high), so a bootstrap
quantile falling on the wrong side of it is pulled back to it.
n_boot=0 disables the ribbon (ci_low and ci_high both equal
event_rate).
Grid points with no observation within the kernel's +-5 sigma_star
reach (data gaps, on the logit scale) have no estimate: event_rate,
ci_low and ci_high are nan there. Bootstrap resamples that leave a grid point
uncovered are skipped in that point's quantiles.
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Outcomes and predicted probabilities.
TYPE:
|
p
|
Outcomes and predicted probabilities.
TYPE:
|
sample_weight
|
Optional positive weights, same length as
TYPE:
|
grid_size
|
Number of evaluation points, spanning the 0.5th to 99.5th percentile
of
TYPE:
|
n_boot
|
Number of bootstrap resamples for the confidence ribbon;
TYPE:
|
confidence
|
Nominal coverage level of the ribbon; must satisfy
TYPE:
|
random_state
|
Seed for
TYPE:
|
bins
|
Lattice bin count passed through to the shared smECE solve; see
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
KernelReliabilityCurve
|
Grid coordinates, kernel-smoothed event rate and density, the
bootstrap ribbon, and the shared |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
Examples:
>>> import numpy as np
>>> from probcal import make_pd_portfolio
>>> from probcal.curves import reliability_smooth
>>> d = make_pd_portfolio(n=2000, random_state=0)
>>> curve = reliability_smooth(d.y, d.scores, n_boot=0)
>>> len(curve.grid_p) == 200
True
>>> abs(float(curve.density.sum()) - 1.0) < 1e-10
True
Source code in src/probcal/curves.py
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 319 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 385 386 387 388 389 390 391 392 393 394 395 396 | |
corp_reliability
¶
corp_reliability(y: object, p: object, *, sample_weight: object = None, bands: str | None = 'consistency', confidence: float = 0.9, n_resamples: int = 200, random_state: int = 42) -> CorpResult
CORP reliability diagram with the Brier/log-loss MCB-DSC-UNC decomposition.
Fits the isotonic (PAV) recalibration map of y on p — the unique
"consistent, optimally binned, reproducible" reliability diagram of
Dimitriadis, Gneiting & Jordan (2021) — and decomposes both the Brier
score and log loss into miscalibration (MCB), discrimination (DSC), and
uncertainty (UNC) terms, with score == mcb - dsc + unc holding
exactly. Log loss clips PAV levels and predictions to
[1e-12, 1 - 1e-12] before taking logarithms, so degenerate blocks
(exact 0 or 1 event rate) stay finite.
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Outcomes and predicted probabilities.
TYPE:
|
p
|
Outcomes and predicted probabilities.
TYPE:
|
sample_weight
|
Optional positive weights, same length as
TYPE:
|
bands
|
Band type to compute around the PAV fit.
TYPE:
|
confidence
|
Nominal coverage level of the bands; must satisfy
TYPE:
|
n_resamples
|
Number of resamples used to build the bands;
TYPE:
|
random_state
|
Seed for
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
CorpResult
|
PAV block structure, the pointwise fit, the Brier/log-loss decomposition, and the (possibly empty) bands. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
Notes
Bands are pointwise: at each grid point, confidence of resamples fall
inside, not that the whole curve does so simultaneously (the
docs/scripts/corp_sim.py coverage simulation reports the gap between
pointwise and uniform coverage). corp_reliability with
n=10_000, n_resamples=200 takes about 3.5 s (measured once on the
development machine) — the PAV step is a Python loop over unique scores
(_math.pava), and the bands refit PAV n_resamples times.
Examples:
>>> import numpy as np
>>> from probcal import corp_reliability
>>> rng = np.random.default_rng(0)
>>> p = rng.uniform(0.1, 0.9, 200)
>>> y = (rng.random(200) < p).astype(float)
>>> r = corp_reliability(y, p, bands=None)
>>> abs(r.brier - (r.brier_mcb - r.brier_dsc + r.brier_unc)) < 1e-12
True
Source code in src/probcal/curves.py
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 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 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 | |
ecce_curve
¶
ecce_curve(y: object, p: object, *, sample_weight: object = None) -> EcceCurve
Cumulative-deviation walk for the ECCE plot (Arrieta-Ibarra et al., 2022).
Sorts by prediction and accumulates weighted residuals with the same
helper as metrics.ecce, so stat_max agrees with the metric. The
walk is reported only at the end of each block of tied predictions (the
order of tied rows is arbitrary, so intermediate points carry no
information); frac is the cumulative weight fraction. sd_null is
the pointwise standard deviation of the walk under calibration — an
envelope for reading, not a simultaneous band — the exact null SD
sqrt(cumsum(w**2 p (1 - p))) / sum(w) of the weighted walk
(sqrt(cumsum(p (1 - p))) / n for unit weights; invariant to
rescaling the weights).
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Outcomes and predicted probabilities.
TYPE:
|
p
|
Outcomes and predicted probabilities.
TYPE:
|
sample_weight
|
Optional positive weights, same length as
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
EcceCurve
|
Cumulative walk, null-envelope SD, and the max-deviation summary. |
Source code in src/probcal/curves.py
540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 | |
calibration_belt
¶
calibration_belt(y: object, p: object, *, confidence: tuple[float, float] = (0.8, 0.95), grid_size: int = 100, sample_weight: object = None) -> BeltResult
GiViTI-style calibration belt (Nattino et al., 2014, 2017).
Fits a polynomial logistic recalibration of the outcome on
logit(p), selecting the degree by forward likelihood-ratio testing
(p < 0.05 to add a term, capped at degree 4). The polynomial is fitted on
the centred and scaled logit (same model, better-conditioned design).
Both outputs are approximations of the published construction:
- Belt. At each confidence level the band is the image of the Wald
(information-matrix) ellipsoid with radius
chi2_ppf(level, degree + 1)— a quadratic approximation of the likelihood-ratio confidence region the papers invert. Using thedegree + 1-df radius makes it a simultaneous (Scheffé-type) band over the grid for the selected polynomial, not a pointwise one; it conditions on the selected degree. - p-value. The likelihood-ratio test of the selected polynomial
against the identity, referred to
chi2(degree + 1); it ignores the forward selection of the degree, whose exact null distribution (Nattino et al., 2014) puts more mass in the upper tail, so the p-value is anti-conservative.
Where the band excludes the diagonal, the data reject calibration in
that region. With non-uniform weights (package convention,
:mod:probcal.metrics._common) the fits maximise the weighted
likelihood, the band uses the sandwich covariance I**-1 J I**-1
with J = X' diag(w**2 mu (1 - mu)) X (the exact variance of the
weighted score under the fitted model), the forward-selection steps
divide each 1-df LR statistic by the added coefficient's design effect
(sandwich over model-based variance), and the p-value is second-order
Rao-Scott corrected (:func:probcal.metrics._common.rao_scott, its
reference distribution chi2 with the corrected, possibly fractional,
degrees of freedom). All of it is invariant to rescaling the weights;
unit weights give the unweighted belt unchanged.
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Outcomes and predicted probabilities.
TYPE:
|
p
|
Outcomes and predicted probabilities.
TYPE:
|
confidence
|
The two (low, high) confidence levels for the bands, e.g.
TYPE:
|
grid_size
|
Number of evaluation points, spanning the 0.5th to 99.5th percentile
of
TYPE:
|
sample_weight
|
Optional positive weights, same length as
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
BeltResult
|
Grid coordinates, the band |
Source code in src/probcal/curves.py
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 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 | |
plots
¶
Matplotlib plotting helpers (requires the [viz] extra; import-guarded).
All computation lives in probcal.curves and probcal.metrics; this
module only renders. The logit-scale views are the flagship for low-PD
portfolios: axis ticks sit at logit positions but are labeled in
probabilities, so the low-probability region stays readable. Styling is
applied per call via rc_context — global rcParams are never touched —
and the axes-level part of it (spines, grid) is applied explicitly as well, so
a caller-supplied ax is styled the same as one created here. Figures this
module creates are made through pyplot (so plt.show() and notebook
display work) and are therefore registered in pyplot's figure manager; close
them with plt.close(fig) when rendering many in a loop. A call that raises
closes any figure it created before failing. scale is "probability"
(the default) or "logit"; anything else raises ValueError.
Theory: docs/concepts/visualization.md.
plot_attributes
¶
plot_attributes(y: object, p: object, *, method: str = 'binned', n_bins: int = 10, scale: str = 'probability', sample_weight: object = None, ax: Any = None) -> Any
Attributes diagram: reliability curve against climatology and no-skill references.
Draws the classic Hsu & Murphy (1986) attributes diagram: the identity
(perfect calibration), the horizontal and vertical climatology
references at the weighted base rate :math:\bar y, the no-skill line
:math:y = (x + \bar y) / 2 (equidistant between the climatology
level and the identity), and a light shading of the region where a
point beats climatology, :math:(y - x)^2 \le (x - \bar y)^2 — i.e.
where the point sits closer to the identity than the horizontal
no-resolution line, the geometric criterion for positive Brier skill.
The reliability curve is drawn on top: method="binned" overlays
:func:probcal.curves.reliability_binned as markers sized by bin
count; method="corp" overlays the PAV step fit from
:func:probcal.curves.corp_reliability (bands=None), the same
convention as :func:probcal.plots.plot_corp. scale="logit"
transforms every drawn quantity (clipped to [1e-12, 1 - 1e-12])
through :func:probcal._math.logit and relabels the axes in
probabilities.
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Outcomes and predicted probabilities.
TYPE:
|
p
|
Outcomes and predicted probabilities.
TYPE:
|
method
|
Reliability construction to overlay.
TYPE:
|
n_bins
|
Bin count passed to :func:
TYPE:
|
scale
|
Axis scale;
TYPE:
|
sample_weight
|
Optional non-negative weights, same length as
TYPE:
|
ax
|
Axes to draw on; a new figure and axes are created if
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Axes
|
The axes the diagram was drawn on. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
Notes
The identity, climatology, no-skill, and shading layers are evaluated on
a fixed 400-point grid over [0, 1] — a plotting-fidelity choice
(dense enough to render as smooth curves at the figure's default
figsize=(6.5, 6)), not a statistical one; it does not depend on
n_bins or the data.
Examples:
>>> import numpy as np
>>> from probcal.plots import plot_attributes
>>> rng = np.random.default_rng(0)
>>> p = rng.uniform(0.05, 0.5, 300)
>>> y = (rng.random(300) < p).astype(float)
>>> ax = plot_attributes(y, p, method="corp")
Source code in src/probcal/_plots_diag.py
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 319 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 | |
plot_corp
¶
plot_corp(result: CorpResult, *, scale: str = 'probability', show_decomposition: bool = True, ax: Any = None) -> Any
CORP reliability diagram: PAV step fit, resampled bands, and the Brier decomposition.
Draws the PAV-recalibrated step function (each block's [block_lo,
block_hi] at its block_level, joined vertically between
consecutive blocks) against the identity, with the resampled bands from
:func:probcal.curves.corp_reliability shaded around it. Grey tick
marks along the x-axis show each PAV block's centre, scaled to its
weight share of the portfolio. scale="logit" clips edges to
[1e-12, 1 - 1e-12] before the logit transform and stretches the
low-probability region — the recommended view for PD portfolios.
| PARAMETER | DESCRIPTION |
|---|---|
result
|
Result of :func:
TYPE:
|
scale
|
Axis scale;
TYPE:
|
show_decomposition
|
If
TYPE:
|
ax
|
Axes to draw on; a new figure and axes are created if
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Axes
|
The axes the diagram was drawn on. |
Examples:
>>> import numpy as np
>>> from probcal.curves import corp_reliability
>>> from probcal.plots import plot_corp
>>> rng = np.random.default_rng(0)
>>> p = rng.uniform(0.05, 0.5, 300)
>>> y = (rng.random(300) < p).astype(float)
>>> ax = plot_corp(corp_reliability(y, p, n_resamples=20))
Source code in src/probcal/_plots_diag.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | |
plot_mcb_dsc
¶
plot_mcb_dsc(candidates: Mapping[str, tuple[Any, Any]] | SelectionReport, *, score: str = 'brier', ax: Any = None) -> Any
MCB-DSC plane: CORP miscalibration vs. discrimination, one point per candidate.
Each candidate is a point at (DSC, MCB) from its CORP decomposition
(:func:probcal.curves.corp_reliability). Dashed grey iso-score
diagonals trace MCB = DSC + (S̄ - UNC) for five values of the mean
score S̄ spaced between the candidates' min and max — candidates on the
same diagonal tie on score despite different miscalibration/
discrimination splits, so the plane separates "worse calibrated" from
"less discriminating" for two methods that score the same. Lower-right
is better: more discrimination (DSC) for no more miscalibration (MCB).
| PARAMETER | DESCRIPTION |
|---|---|
candidates
|
Either a
TYPE:
|
score
|
Which CORP decomposition to plot for a mapping input.
TYPE:
|
ax
|
Axes to draw on; a new figure and axes are created if
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Axes
|
The axes the plane was drawn on. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
Examples:
>>> import numpy as np
>>> from probcal.plots import plot_mcb_dsc
>>> rng = np.random.default_rng(0)
>>> p_a = rng.uniform(0.05, 0.5, 300)
>>> y = (rng.random(300) < p_a).astype(float)
>>> p_b = np.clip(p_a * 0.9, 1e-6, 1 - 1e-6)
>>> ax = plot_mcb_dsc({"a": (y, p_a), "b": (y, p_b)})
Source code in src/probcal/_plots_diag.py
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | |
plot_murphy
¶
plot_murphy(curves: MurphyCurve | Mapping[str, MurphyCurve] | Mapping[str, tuple[Any, Any]], *, diff: bool = False, n_boot: int = 200, random_state: int = 42, ax: Any = None) -> Any
Murphy diagram: mean elementary score across a threshold grid, or a paired difference.
diff=False (default) draws one line per curve: a single
:class:probcal.metrics.MurphyCurve, or a {name: MurphyCurve}
mapping with a legend. diff=True instead requires a mapping of
exactly two {name: (y, p)} raw-data entries — a MurphyCurve
does not retain y/p, so the pointwise difference and its
bootstrap band are recomputed from the paired data — and draws
S_theta(A) - S_theta(B) (on A's default 513-point threshold
grid) with a seeded pointwise bootstrap band (paired-index resampling,
5th/95th percentile) and a zero reference line.
| PARAMETER | DESCRIPTION |
|---|---|
curves
|
See above; the last form only when
TYPE:
|
diff
|
If
TYPE:
|
n_boot
|
Bootstrap resamples for the difference band (
TYPE:
|
random_state
|
Seed for
TYPE:
|
ax
|
Axes to draw on; a new figure and axes are created if
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Axes
|
The axes the diagram was drawn on. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
Examples:
>>> import numpy as np
>>> from probcal.metrics import murphy_curve
>>> from probcal.plots import plot_murphy
>>> rng = np.random.default_rng(0)
>>> p = rng.uniform(0.05, 0.5, 300)
>>> y = (rng.random(300) < p).astype(float)
>>> ax = plot_murphy({"model": murphy_curve(y, p)})
Source code in src/probcal/_plots_diag.py
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 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 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 | |
plot_reliability
¶
plot_reliability(curve: ReliabilityCurve, *, smooth: SmoothReliabilityCurve | KernelReliabilityCurve | None = None, scale: str = 'probability', y: object = None, p: object = None, annotate: bool = True, rug: bool = True, counts: bool = False, ax: Any = None, stats: bool | MetricReport | GroupedMetricReport = False, risk_dist: str | None = 'rug', by: object = None) -> Any
Annotated reliability diagram.
Binned points with Wilson CIs, optional smooth overlay, stats box, and event/non-event risk distribution.
scale="logit" stretches the low-probability region — the recommended
view for PD portfolios. Bins whose event rate is exactly 0 or 1 have no
finite logit and are omitted from the logit-scale point layer; they remain
visible in the risk distribution (or the counts=True margin).
Confidence bounds at exactly 0 or 1 are clipped to [1e-6, 1 - 1e-6]
before the logit transform.
Passing a :class:probcal.curves.KernelReliabilityCurve (from
:func:probcal.curves.reliability_smooth) as smooth renders the
density-weighted variable-width curve instead of a plain line: a
LineCollection whose width tracks the local prediction density (one
width per segment, density[:-1] — the density at the left endpoint
of each [grid[i], grid[i+1]] segment, since a LineCollection of
len(grid) - 1 segments needs exactly that many widths), the shaded
miscalibration area between the curve and the identity, the bootstrap
ribbon, and an smECE = ... readout in the bottom-right corner.
Passing the raw y/p enables the stats box and the risk
distribution; both are silently skipped when y/p are absent.
annotate=True (default) draws the classic stats box, computed by
:func:probcal.metrics.reliability_summary. stats=True replaces it
with a box reporting n, events, intercept, slope, ICI, smECE, Brier
instead (annotate is then ignored); stats=<MetricReport> instead
reports name = value [ci_low, ci_high] for whichever of
{"intercept", "slope", "ici", "smooth_ece", "brier"} the report
carries, plus n/events computed from y.
risk_dist selects the density layer: "rug" (default) draws
event/non-event tick marks along the top/bottom edges,
deterministically thinned to at most 1000 marks per class; "split"
replaces it with a 30-equal-mass-bin spike histogram of p (events
up, non-events down, from a y=0.12 baseline in axis-fraction
coordinates, heights scaled so the taller class reaches the full 0.12 —
axis coordinates cannot go below 0, so both classes share the one
baseline); None draws no density layer. rug=False disables the
density layer regardless of risk_dist (equivalent to
risk_dist=None). counts=True adds the twin-axis count-bar
margin, independent of risk_dist.
Passing by switches to a faceted grid: one panel per sorted,
stringified group in by (grouped exactly as by
:func:probcal.metrics.evaluate's by=, which raises when two distinct
labels such as 1 and "1" display alike) plus a leading "pooled" panel, each a fresh
:func:probcal.curves.reliability_binned panel built from that group's
slice of y/p — the given curve is ignored for the panels
(it would otherwise be ambiguous which group it represents). y and
p are required in this mode. Every panel honours scale,
annotate, stats, rug, risk_dist and counts exactly as
the single-panel diagram does (pass annotate=False, rug=False for
light panels), with one exception for stats: stats=True computes
each panel's box from that panel's data, and a pooled
stats=<MetricReport> raises (it cannot describe each group) — pass the
:class:probcal.metrics.GroupedMetricReport from
evaluate(..., by=...) instead, whose pooled and per-group reports fill
the matching panels; sharing x/y limits across the grid; the function then
returns the Figure, not an Axes (unlike the by=None default,
matching :func:plot_comparison). ax and smooth are not
supported with by and raise ValueError.
"pooled" is a reserved panel title: a group of your own by that name
is indistinguishable from the pooled panel. Group-conditional
statistical testing is out of scope here — see
docs/guide/groups.md.
| PARAMETER | DESCRIPTION |
|---|---|
curve
|
Binned curve, e.g. from :func:
TYPE:
|
smooth
|
Optional smooth overlay, e.g. from
:func:
TYPE:
|
scale
|
Axis scale;
TYPE:
|
y
|
Raw outcomes and predictions; must be given together (or not at all).
Enables the stats box and risk distribution; required when
TYPE:
|
p
|
Raw outcomes and predictions; must be given together (or not at all).
Enables the stats box and risk distribution; required when
TYPE:
|
annotate
|
If
TYPE:
|
rug
|
If
TYPE:
|
counts
|
If
TYPE:
|
ax
|
Axes to draw on; a new figure and axes are created if
TYPE:
|
stats
|
If truthy and
TYPE:
|
risk_dist
|
Density-layer style; see above. Anything else raises
TYPE:
|
by
|
Optional group labels, one per observation (same length as
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Axes or Figure
|
The axes the diagram was drawn on ( |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
Examples:
>>> import numpy as np
>>> from probcal.curves import reliability_binned
>>> from probcal.plots import plot_reliability
>>> rng = np.random.default_rng(0)
>>> p = rng.uniform(0.05, 0.5, 300)
>>> y = (rng.random(300) < p).astype(float)
>>> curve = reliability_binned(y, p, n_bins=10)
>>> ax = plot_reliability(curve, scale="logit", y=y, p=p)
>>> segment = np.where(p < 0.2, "low", "high")
>>> fig = plot_reliability(curve, y=y, p=p, by=segment)
Source code in src/probcal/plots.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 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 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 319 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 385 386 387 388 389 390 391 392 393 | |
plot_belt
¶
plot_belt(belt: BeltResult, *, scale: str = 'probability', ax: Any = None) -> Any
GiViTI-style calibration belt with 80/95% bands and the test p-value.
| PARAMETER | DESCRIPTION |
|---|---|
belt
|
Result of :func:
TYPE:
|
scale
|
Axis scale;
TYPE:
|
ax
|
Axes to draw on; a new figure and axes are created if
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Axes
|
The axes the belt was drawn on. |
Source code in src/probcal/plots.py
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 | |
plot_comparison
¶
plot_comparison(before: ReliabilityCurve, after: ReliabilityCurve, *, scale: str = 'probability', labels: tuple[str, str] = ('before', 'after')) -> Any
Side-by-side reliability diagrams (pre/post calibration or offset).
| PARAMETER | DESCRIPTION |
|---|---|
before
|
Binned curves to compare, e.g. raw vs calibrated.
TYPE:
|
after
|
Binned curves to compare, e.g. raw vs calibrated.
TYPE:
|
scale
|
Axis scale;
TYPE:
|
labels
|
Panel titles for
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Figure
|
The figure containing both panels. |
Source code in src/probcal/plots.py
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 | |
plot_interval
¶
plot_interval(intervals: ndarray, s: ndarray, *, ax: Any = None) -> Any
Venn–Abers interval widths against the score: where is calibration uncertain?
| PARAMETER | DESCRIPTION |
|---|---|
intervals
|
TYPE:
|
s
|
Scores the intervals are plotted against; need not be sorted (the
pairs are drawn in ascending order of
TYPE:
|
ax
|
Axes to draw on; a new figure and axes are created if
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Axes
|
The axes the intervals were drawn on. |
Source code in src/probcal/plots.py
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 | |
plot_selection
¶
plot_selection(report: SelectionReport, *, ax: Any = None) -> Any
SelectionReport as a ranked dot plot with fold-spread whiskers.
| PARAMETER | DESCRIPTION |
|---|---|
report
|
Result of :meth:
TYPE:
|
ax
|
Axes to draw on; a new figure and axes are created if
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Axes
|
The axes the dot plot was drawn on. |
Source code in src/probcal/plots.py
652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 | |
plot_ecce
¶
plot_ecce(curves: Any, *, labels: Any = None, show_band: bool = True, ax: Any = None) -> Any
ECCE cumulative-drift walk(s) from :func:probcal.curves.ecce_curve.
Accepts a single EcceCurve or a sequence (e.g. raw vs calibrated).
The grey envelope (show_band=True, from the first curve) is ±2
pointwise standard deviations under calibration — an aid for reading
the walk, NOT a simultaneous confidence band; the formal max-statistic
test of Arrieta-Ibarra et al. (2022) is not implemented.
| PARAMETER | DESCRIPTION |
|---|---|
curves
|
One or more cumulative-drift walks to overlay.
TYPE:
|
labels
|
Legend labels, aligned with
TYPE:
|
show_band
|
If
TYPE:
|
ax
|
Axes to draw on; a new figure and axes are created if
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Axes
|
The axes the walk(s) were drawn on. |
Source code in src/probcal/plots.py
692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 | |
plot_grade_backtest
¶
plot_grade_backtest(result: Any, *, log_scale: bool = True, ax: Any = None) -> Any
Per-grade traffic-light backtest chart (Jeffreys or exact binomial).
Observed default rates as circles colored by the grade's traffic light,
grey 90% display intervals (ci_low/ci_high), and the assigned PDs
as wide blue dashes. The intervals are display companions only — the
verdict is carried by the lights from the unchanged one-sided tests, so
no p-values are printed on the canvas. log_scale=True is the right
default for PD grades spanning orders of magnitude.
| PARAMETER | DESCRIPTION |
|---|---|
result
|
Per-grade backtest result, from
:func:
TYPE:
|
log_scale
|
If
TYPE:
|
ax
|
Axes to draw on; a new figure and axes are created if
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Axes
|
The axes the backtest chart was drawn on. |
Source code in src/probcal/plots.py
756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 | |
plot_offset_audit
¶
plot_offset_audit(offset: Any, *, ax: Any = None) -> Any
Audit chart for a fitted :class:probcal.offset.LogitOffset stage.
Draws the offset map t -> t + delta on the logit scale against the
identity, marks the pre- and post-adjustment central tendencies, and
prints the audit numbers read directly from the fitted attributes. This
chart audits the stage, not the outcomes — for the before/after
guardrail comparison use LogitOffset.audit_report(y, p).
| PARAMETER | DESCRIPTION |
|---|---|
offset
|
A fitted :class:
TYPE:
|
ax
|
Axes to draw on; a new figure and axes are created if
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Axes
|
The axes the audit chart was drawn on. |
Source code in src/probcal/plots.py
830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 | |
plot_e_process
¶
plot_e_process(report: Any, *, grades_panel: bool = False, ax: Any = None) -> Any
Monitoring wealth per component on a log scale, with the 1/alpha line.
| PARAMETER | DESCRIPTION |
|---|---|
report
|
Result of :meth:
TYPE:
|
grades_panel
|
Add a second, shorter axes below the main plot showing each grade's
offset confidence-sequence band (
TYPE:
|
ax
|
Axes to draw on; a new figure and axes are created if
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Axes
|
The main axes the e-processes were drawn on (unchanged even when
|
Source code in src/probcal/plots.py
892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 | |
report
¶
Self-contained HTML/markdown validation report.
:func:validation_report assembles a single document — one HTML file with
base64-embedded PNG figures, or a markdown file plus a sibling directory of
PNGs — out of the diagnostics already computed elsewhere in the package:
the reliability diagrams (curves/plots), the metric catalog
(metrics.evaluate), the CORP score decomposition, the per-grade
Jeffreys/Pluto-Tasche backtests, grouped evaluation, and monitor
trajectories. Nothing here computes new statistics; every number and figure
is produced by the existing public API and merely rendered into one
document for handoff (a model-risk file, an audit trail, a stakeholder
readout).
Import cost: this module is stdlib + numpy + probcal at import time —
matplotlib is only ever imported lazily, inside the figure-rendering path,
so import probcal.report never pulls in the [viz] extra even when
it is installed. Calling :func:validation_report does require it (every
section renders at least one figure from y/p alone); the
ImportError it raises without the extra names probcal[viz].
Determinism: every resampling site (metrics.evaluate,
curves.corp_reliability, curves.reliability_smooth) is driven by
the single seed keyword, and n_boot sizes all of them at once — the
report is bit-reproducible given the same inputs and seed, apart from
the one Generated ... UTC timestamp line.
validation_report
¶
validation_report(y: object, p: object, *, calibrator: Any = None, monitor: Any = None, grades: object = None, by: object = None, title: str | None = None, path: str | PathLike[str] | None = None, format: str = 'html', n_boot: int = 200, seed: int = 42) -> str
Self-contained validation report: reliability, metrics, grades, monitoring.
One document — HTML with base64-embedded PNG figures, or markdown with a
sibling directory of PNGs — built entirely from the existing public API
(curves, metrics, plots, monitor): nothing here computes
a new statistic. Sections are omitted, not left blank, when their input
is absent: reliability, the metric report, and the CORP decomposition
always render (they need only y/p); the rating-grade backtests
render only when grades is given, the grouped-evaluation panel only
when by is given, the monitoring trajectory only when monitor is
given, and the calibrator appendix only when calibrator is given.
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Outcomes and predicted probabilities.
TYPE:
|
p
|
Outcomes and predicted probabilities.
TYPE:
|
calibrator
|
A fitted calibrator; adds its fingerprint to the header and an
appendix with its serialized state (:meth:
TYPE:
|
monitor
|
Adds its fingerprint to the header and a monitoring section with the
e-process trajectory (:func:
TYPE:
|
grades
|
Rating grade label per observation, or a :class:
TYPE:
|
by
|
Group labels, one per observation. Adds a grouped-evaluation
section: the faceted reliability panel
(:func:
TYPE:
|
title
|
Document title;
TYPE:
|
path
|
When given, the rendered document is written here in addition to
being returned. Required when
TYPE:
|
format
|
Output format.
TYPE:
|
n_boot
|
Bootstrap/resample count shared by every resampling site in the
report (
TYPE:
|
seed
|
RNG seed shared by the same resampling sites; the report is
bit-reproducible given the same inputs and
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
str
|
The rendered document text (also written to |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
ImportError
|
If matplotlib is not installed (every section renders at least one
figure); names the |
Examples:
>>> from probcal import make_pd_portfolio
>>> from probcal.report import validation_report
>>> d = make_pd_portfolio(n=500, random_state=0)
>>> html = validation_report(d.y, d.scores, n_boot=20)
>>> "Generated" in html
True
Source code in src/probcal/report.py
390 391 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 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 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 | |
attribution
¶
SHAP / additive-attribution adjustment to calibrated outputs.
Post-hoc calibration breaks SHAP local accuracy: base + sum(phi)
reconstructs the raw score, not the calibrated probability. This module
restores additivity on the calibrated scale — exactly for calibrators affine
on the logit scale, and by the Aumann–Shapley secant rescaling in general.
Theory, identifiability limits, and invariance properties:
docs/concepts/shap-calibration.md.
No shap import: plain arrays are accepted, and shap.Explanation objects
are duck-typed via their .values / .base_values attributes.
References
Lundberg & Lee (2017); Lundberg et al. (2020); Sundararajan, Taly & Yan (2017); Aumann & Shapley (1974) — full records in the documentation.
AdjustedAttribution
dataclass
¶
AdjustedAttribution(phi_adj: ndarray, base_adj: ndarray, target: ndarray, method_used: str, max_reconstruction_error: float)
Bases: _ResultBase
Attributions rescaled to the calibrated output scale.
| ATTRIBUTE | DESCRIPTION |
|---|---|
phi_adj |
Adjusted per-feature attributions.
TYPE:
|
base_adj |
Adjusted base values.
TYPE:
|
target |
The calibrated output each row reconstructs
(
TYPE:
|
method_used |
TYPE:
|
max_reconstruction_error |
TYPE:
|
adjust_attributions
¶
adjust_attributions(phi: object, base_value: object, calibrator: Any, *, scale: str = 'logit', method: str = 'auto') -> AdjustedAttribution
Rescale additive attributions so they sum to the calibrated output.
| PARAMETER | DESCRIPTION |
|---|---|
phi
|
Raw attributions on the model's score scale (log-odds margins for
TYPE:
|
base_value
|
SHAP base value(s) on the same scale as
TYPE:
|
calibrator
|
Any object with
TYPE:
|
scale
|
Working scale of the attributions. Affine-exactness exists only on the logit scale.
TYPE:
|
method
|
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
AdjustedAttribution
|
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
Source code in src/probcal/attribution.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 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 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
thresholds
¶
Calibrated-to-raw interval and masterscale-band mapping.
Thin functional wrappers over the calibrators' interval_inverse protocol:
numpy-only, arrays and floats, no knowledge of any consumer. The
canonical rating-grade workflow — masterscale bands defined on calibrated PD,
translated once per recalibration into raw-score intervals — is
calibrated_bands_to_raw; its output plugs directly into band-style raw
targets of a counterfactual engine. build_masterscale runs the other way:
it designs the bands from data.
calibrated_interval_to_raw
¶
calibrated_interval_to_raw(calibrator: object, lo: float, hi: float, *, space: str = 'probability', buffer_logit: float = 0.0) -> tuple[float, float]
Translate one calibrated-probability interval into raw-score bounds.
| PARAMETER | DESCRIPTION |
|---|---|
calibrator
|
Any object implementing the duck-typed protocol
TYPE:
|
lo
|
Calibrated bounds;
TYPE:
|
hi
|
Calibrated bounds;
TYPE:
|
space
|
Scale of the returned bounds.
TYPE:
|
buffer_logit
|
Robustness margin applied in logit space before inversion.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
tuple of float
|
|
Source code in src/probcal/thresholds.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | |
calibrated_bands_to_raw
¶
calibrated_bands_to_raw(calibrator: object, bands: object, *, space: str = 'probability', buffer_logit: float = 0.0) -> dict
Translate a masterscale {grade: (lo, hi)} on calibrated PD to raw intervals.
Grade edges are policy artifacts that outlive model versions; this translation is what changes when the calibrator is refitted.
The raw intervals follow the masterscale's half-open convention
(lo <= p < hi, top band closed): for a non-decreasing fitted map
g, band [lo, hi) becomes
[raw_lo, raw_hi) = [inf{s : g(s) >= lo}, inf{s : g(s) >= hi}),
and the top band (the one with the largest hi) becomes the closed
[inf{s : g(s) >= lo}, sup{s : g(s) <= hi}]. Adjacent bands therefore
share their raw edge and the intervals partition the raw axis: a raw
score s lies in the interval of exactly the grade that
Masterscale.assign(g(s)) gives it — also for step calibrators
(isotonic, histogram binning), where a calibrated edge sitting on a
plateau carries positive raw-score mass, which half-open inversion
assigns to exactly one grade. For a single interval with closed-bound semantics use
:func:calibrated_interval_to_raw.
| PARAMETER | DESCRIPTION |
|---|---|
calibrator
|
Any object implementing the duck-typed protocol
TYPE:
|
bands
|
Mapping of grade label to
TYPE:
|
space
|
Scale of the returned bounds.
TYPE:
|
buffer_logit
|
Robustness margin applied in logit space before inversion: each band
shrinks to
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
dict
|
Mapping of grade label to |
| RAISES | DESCRIPTION |
|---|---|
UnattainableTargetError
|
If a band does not intersect the calibrator's output range. |
Source code in src/probcal/thresholds.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
build_masterscale
¶
build_masterscale(y: object, p: object, *, n_grades: int, min_count: float = 0, min_events: float = 0, objective: str = 'likelihood', target_shares: object = None, prebins: int = 512, sample_weight: object = None, names: object = None) -> Masterscale
Design a masterscale from data by exact dynamic programming.
p is sorted and cut into prebins equal-mass pre-bins whose
boundaries are observed values (ties merge pre-bins). Grades are runs of
consecutive pre-bins, so the returned :class:Masterscale carries exact
edges and its half-open assign reproduces the partition the optimizer
scored. Complexity O(n_grades * prebins^2) after an O(n log n)
sort; at the default 512 pre-bins that is well under a second.
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Outcomes and calibrated probabilities.
TYPE:
|
p
|
Outcomes and calibrated probabilities.
TYPE:
|
n_grades
|
Number of grades (a positive integer).
TYPE:
|
min_count
|
Floors per grade on the (weighted) observation and event counts.
TYPE:
|
min_events
|
Floors per grade on the (weighted) observation and event counts.
TYPE:
|
objective
|
TYPE:
|
target_shares
|
Required with
TYPE:
|
prebins
|
Pre-bin count (a positive integer; an upper bound on the number of candidate edges).
TYPE:
|
sample_weight
|
Weights; counts become weighted sums.
TYPE:
|
names
|
Grade names best to worst;
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Masterscale
|
With |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If no partition satisfies the floors (the message names the binding
floor), or on an invalid |
Source code in src/probcal/thresholds.py
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 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 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 | |
datasets
¶
Synthetic dataset generators (make_pd_portfolio).
PdPortfolio
dataclass
¶
PdPortfolio(scores: ndarray, y: ndarray, p_true: ndarray)
Bases: _ResultBase
Synthetic PD portfolio: model scores, outcomes, and the true probabilities.
| ATTRIBUTE | DESCRIPTION |
|---|---|
scores |
The model's reported PDs — miscalibrated unless generated with
TYPE:
|
y |
Bernoulli outcomes drawn from
TYPE:
|
p_true |
True conditional probabilities (mean anchored at
TYPE:
|
make_pd_portfolio
¶
make_pd_portfolio(n: int = 5000, *, event_rate: float = 0.03, slope: float = 0.7, intercept: float = 0.0, asymmetry: float = 0.4, score_location: float = -3.2, score_scale: float = 1.1, random_state: int = 42) -> PdPortfolio
Generate a synthetic, controllably miscalibrated PD portfolio.
The model's scores are drawn as s = sigma(N(score_location,
score_scale)); the true probability follows the beta-calibration family
logit p_true = a_lo * ln(s) - a_hi * ln(1 - s) + c
with a_lo = slope * (1 + asymmetry) (low-PD tail) and a_hi = slope
(high tail), so asymmetry != 0 produces exactly the one-sided tail
distortion low-event-rate portfolios exhibit, and BetaCalibrator can
recover the generative exponents. c = c_anchor + intercept:
c_anchor is solved so that the undistorted-by-intercept portfolio
has mean(p_true) == event_rate (unique by monotonicity, via
bisection), and intercept is then added on top — a deliberate
calibration-in-the-large error of the scores (the true mean moves away
from event_rate; the scores do not).
The identity distortion (slope=1, asymmetry=0) anchors the scores
instead: their logits are shifted so that mean(scores) ==
event_rate, and p_true = sigma(logit(scores) + intercept). With
intercept=0 too, the scores are exactly calibrated and the
portfolio mean is event_rate.
| PARAMETER | DESCRIPTION |
|---|---|
n
|
Portfolio size.
TYPE:
|
event_rate
|
Target mean of
TYPE:
|
slope
|
Base exponent of the distortion;
TYPE:
|
intercept
|
Log-odds shift of
TYPE:
|
asymmetry
|
Relative extra distortion of the low-PD tail (
TYPE:
|
score_location
|
Parameters of the normal generating the score logits.
TYPE:
|
score_scale
|
Parameters of the normal generating the score logits.
TYPE:
|
random_state
|
Seed.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
PdPortfolio
|
|
Source code in src/probcal/datasets.py
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | |
chain
¶
Chain: model-free composition of a calibrator with logit-offset stages.
The object a recourse engine inverts after a macro re-offset: recourse must
run through offset ∘ calibrator exactly, and every stage stays separately
inspectable. CalibratedModel.chain_ builds the equivalent chain for users
who fitted through the wrapper and want to hand it on without the model.
Chain
¶
Chain(stages: Sequence[object])
Bases: JsonIO
A calibrator followed by zero or more LogitOffset stages.
Exposes the full calibrator protocol — forward map, exact inverse maps,
monotonicity, affine coefficients, interpretation, serialization — for
the composed map sigma(logit(g(s)) + delta_1 + ... + delta_m).
Stages may be given fitted (the chain is then immediately usable) or
unfitted (the chain must be fitted with :meth:fit before any reading
method is called). fit always refits every stage in place,
sequentially: the calibrator on (s, y, sample_weight), then each
offset in turn on the running calibrated probabilities.
| PARAMETER | DESCRIPTION |
|---|---|
stages
|
A :class:
TYPE:
|
| ATTRIBUTE | DESCRIPTION |
|---|---|
stages |
The stages, in application order, stored verbatim.
TYPE:
|
calibrator_ |
Read-only view of
TYPE:
|
offsets_ |
Read-only view of
TYPE:
|
fitted_ |
TYPE:
|
is_monotone_ |
True iff every stage is monotone (offsets always are).
TYPE:
|
Source code in src/probcal/chain.py
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | |
offsets_
property
¶
offsets_: tuple[LogitOffset, ...]
Read-only view of the offset stages, in application order.
is_monotone_
property
¶
is_monotone_: bool
True iff the calibrator stage is monotone (offsets always are).
affine_logit_coeffs_
property
¶
affine_logit_coeffs_: tuple[float, float] | None
(a, b + sum(delta)) when the calibrator is affine on the logit scale.
fit
¶
fit(s: object, y: object, sample_weight: object = None) -> Chain
Fit every stage sequentially on the same calibration data.
The head calibrator is fitted on (s, y, sample_weight); each
offset is then fitted on the running calibrated probabilities, so
the offset anchors the calibrator's in-sample output — exactly what
CalibratedModel.offset_to does. There is no cross-fitting inside
a chain and no automatic MLE offset (estimate_offset remains an
explicit choice). fit always refits every stage, including
stages that were already fitted at construction; to keep a stage
frozen, compose fitted objects and skip fit.
Source code in src/probcal/chain.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |
get_params
¶
get_params(deep: bool = True) -> dict[str, object]
Constructor parameters, with stages__i[__param] nesting when deep.
Source code in src/probcal/chain.py
138 139 140 141 142 143 144 145 146 | |
set_params
¶
set_params(**params: object) -> Chain
Set stages wholesale, one stage (stages__i), or a nested stage param.
Stage replacements (stages__i) validate the whole candidate
list before anything on the chain changes, so a rejected
replacement leaves the chain as it was; a wholesale stages=
key is applied first and independently of the indexed keys in the
same call. Like the stages' own set_params, setting a nested
parameter (stages__i__param) does not clear fitted_; call
fit again for the new value to take effect.
Source code in src/probcal/chain.py
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | |
predict_proba
¶
predict_proba(s: object) -> ndarray
The composed calibrated probability, applied stage by stage.
Source code in src/probcal/chain.py
201 202 203 204 205 206 207 | |
__sklearn_is_fitted__
¶
__sklearn_is_fitted__() -> bool
Fitted state for sklearn >= 1.6 (True iff every stage is fitted).
Source code in src/probcal/chain.py
209 210 211 | |
interval_inverse
¶
interval_inverse(lo: float, hi: float, *, space: str = 'probability', buffer_logit: float = 0.0) -> tuple[float, float]
Preimage of a calibrated interval through every stage.
The buffer applies to the final calibrated scale, then the bounds travel back through the offsets on the logit scale, then the calibrator's own generalized inverse finishes the job — every refusal (empty buffered interval, unattainable target) is raised by the same doctrine as the underlying stages.
| PARAMETER | DESCRIPTION |
|---|---|
lo
|
Calibrated-probability bounds on the chain's output scale.
TYPE:
|
hi
|
Calibrated-probability bounds on the chain's output scale.
TYPE:
|
space
|
Scale of the returned raw bounds.
TYPE:
|
buffer_logit
|
Logit-space shrinkage applied before inverting.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
tuple of float
|
|
| RAISES | DESCRIPTION |
|---|---|
UnattainableTargetError
|
If the buffered interval is empty or does not intersect the chain's output range. |
Source code in src/probcal/chain.py
233 234 235 236 237 238 239 240 241 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 | |
point_inverse
¶
point_inverse(p: object, *, space: str = 'probability') -> ndarray
Exact preimage of composed calibrated probabilities.
Shifts the targets back through the offsets on the logit scale, then
the calibrator's own exact point inverse finishes; the boundary
doctrine (strict (0, 1) targets, representable probability-space
results) is inherited from the stages.
| RAISES | DESCRIPTION |
|---|---|
UnattainableTargetError
|
If a target lies outside |
Source code in src/probcal/chain.py
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | |
interpret
¶
interpret() -> Interpretation
Concatenated interpretation of every stage.
Source code in src/probcal/chain.py
301 302 303 304 305 | |
to_dict
¶
to_dict() -> dict[str, object]
Versioned snapshot: the stages' own envelopes, in order.
Source code in src/probcal/chain.py
309 310 311 312 313 314 315 316 317 318 319 | |
from_dict
classmethod
¶
from_dict(d: dict) -> Chain
Rebuild the chain by loading every stage through the registry.
Source code in src/probcal/chain.py
321 322 323 324 325 326 | |
monitor
¶
Anytime-valid calibration monitoring by e-processes.
Theory, validity conditions, and the simulation verification:
docs/concepts/monitoring.md. numpy + stdlib only, like the core.
AppliedAction
dataclass
¶
AppliedAction(kind: str, offset: LogitOffset | None, composed: object | None, monitor: CalibrationMonitor | None, window: tuple[str, ...], audit: dict)
Bases: JsonIO
The result of :meth:CalibrationMonitor.apply_recommendation.
| ATTRIBUTE | DESCRIPTION |
|---|---|
kind |
The recommendation :meth:
TYPE:
|
offset |
The fitted correction; only for
TYPE:
|
composed |
TYPE:
|
monitor |
A fresh monitor with the same constructor parameters, ready to
watch the corrected pipeline; only for
TYPE:
|
window |
Batch labels the offset (or the suggested re-fit window) was
estimated from; empty when
TYPE:
|
audit |
Provenance:
TYPE:
|
Examples:
>>> import numpy as np
>>> from probcal._math import expit, logit
>>> from probcal.datasets import make_pd_portfolio
>>> from probcal.monitor import CalibrationMonitor
>>> mon = CalibrationMonitor(alpha=0.05)
>>> for k in range(6):
... d = make_pd_portfolio(n=1000, random_state=k)
... rng = np.random.default_rng(k + 1000)
... y = (rng.random(1000) < expit(logit(d.scores) + 0.8)).astype(float)
... _ = mon.update(y, d.scores, label=f"m{k}")
>>> action = mon.apply_recommendation()
>>> action.kind
're-offset'
>>> action.offset.delta_ > 0
True
to_dict
¶
to_dict() -> dict[str, object]
Versioned snapshot; offset/composed/monitor are nested envelopes.
Each nested field is stored via its own to_dict (None stays
None): a CalibratedModel composed target stores only a
model reference, reattached on load via
AppliedAction.from_dict(d, model=...) -- see
CalibratedModel.to_dict.
Source code in src/probcal/monitor/_actions.py
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 | |
from_dict
classmethod
¶
from_dict(d: dict, *, model: object = None) -> AppliedAction
Rebuild from :meth:to_dict output (from_json forwards model=).
| PARAMETER | DESCRIPTION |
|---|---|
d
|
Output of :meth:
TYPE:
|
model
|
Passed through to
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If the schema version is unknown or the payload class differs. |
Source code in src/probcal/monitor/_actions.py
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 393 394 395 396 397 398 399 400 401 402 403 404 | |
CalibrationMonitor
¶
CalibrationMonitor(alpha: float = 0.05, components: tuple[str, ...] = ('offset', 'shape'), grades: tuple | None = None, mixture_grid: tuple[float, ...] = (0.1, 0.25, 0.5, 1.0), delta_ci_grid: tuple[float, float, int] = (-3.0, 3.0, 241), min_history: int = 1, plug_in_window: int | None = None, *, recommendation_window: str = 'since_onset')
Bases: JsonIO
Anytime-valid calibration monitoring by e-processes.
Feed matured outcome batches in arrival order; the alarm rule
"E >= 1/alpha" has type-I error at most alpha at every stopping
time (Ville's inequality), however long monitoring runs. Persist the
state between batches with :meth:to_json — never re-run or reorder
past batches. Theory: docs/concepts/monitoring.md.
| PARAMETER | DESCRIPTION |
|---|---|
alpha
|
Alarm level in
TYPE:
|
components
|
Which portfolio-level processes drive the global alarm.
TYPE:
|
grades
|
The grade universe, fixed for the monitor's lifetime. When given,
the per-grade component (the equal-weight average of one offset
e-process per declared grade) is part of the global mixture from
the first batch on — a grade without data yet contributes
TYPE:
|
mixture_grid
|
Non-empty tuple of positive, finite shifts for the offset mixture (symmetrized to ±).
TYPE:
|
delta_ci_grid
|
Grid of offset nulls for the confidence sequence:
TYPE:
|
min_history
|
Number of past batches (
TYPE:
|
plug_in_window
|
Trailing number of past batches (
TYPE:
|
recommendation_window
|
Which batches feed
TYPE:
|
| ATTRIBUTE | DESCRIPTION |
|---|---|
steps_ |
The processed batches, in arrival order.
TYPE:
|
masterscale_fingerprint_ |
Fingerprint of the
TYPE:
|
Notes
Anytime validity needs the global mixture's components and weights to
be fixed in advance: a grade first seen mid-stream that joined the
average with e = 1 would re-weight the existing components, and the
global value could then jump toward 1 without any evidence. Declaring
grades fixes the universe up front; without it the per-grade
processes are diagnostics only.
Source code in src/probcal/monitor/_monitor.py
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 319 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 | |
update
¶
update(y: object, p: object, sample_weight: object = None, grade: object = None, label: str | None = None) -> MonitorStep
Process one matured batch (arrival order is the process order).
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Matured binary outcomes in
TYPE:
|
p
|
The probabilities the deployed forecast assigned to this batch.
TYPE:
|
sample_weight
|
Positive weights, fixed before the outcomes are seen. In every
batch factor they enter as likelihood-ratio exponents
TYPE:
|
grade
|
Optional per-observation grade labels, or a
:class:
TYPE:
|
label
|
Batch label for reporting; defaults to
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
MonitorStep
|
The running record after this batch. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
On an empty batch, mismatched lengths, invalid values, or a
grade label outside the declared |
Source code in src/probcal/monitor/_monitor.py
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 560 561 562 563 564 565 566 567 568 569 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 615 616 617 618 | |
report
¶
report() -> MonitorReport
Trajectory plus the diagnostic re-offset/re-fit recommendation.
Source code in src/probcal/monitor/_monitor.py
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 | |
apply_recommendation
¶
apply_recommendation(target: object = None) -> Any
Apply :meth:report's recommendation once, closing the re-offset loop.
"re-offset": estimates the log-odds shift by maximum likelihood
(:func:~probcal.offset.estimate_offset) on the batches of the
recommendation window (the same window :meth:report uses for its
trailing-window diagnostics), composes the fitted offset onto target (see
below), and returns a fresh monitor with the same constructor
parameters (:meth:_ctor_params) to watch the corrected pipeline.
The monitor is fresh, not continued: its e-process is a martingale
under the null "the CURRENTLY DEPLOYED forecast is calibrated";
once target changes, the accumulated evidence describes a
forecast that no longer exists.
"re-fit"/"none": no offset, composed target, or fresh
monitor is produced. Automatic re-fitting is deliberately out of
scope: a slope drift needs a human to choose and validate a new
calibrator, not a mechanical action this method could take safely.
Composing the fitted offset onto target:
None(default) --composedisNone; only the offset (and the fresh monitor) come back.- :class:
~probcal.chain.Chain-- a newChain([target.calibrator_, *target.offsets_, offset]);targetitself is untouched. - :class:
~probcal.wrapper.CalibratedModel--target.with_offset(offset): a copy with the fitted offset appended (works for a model loaded from JSON too, since no calibration data is needed);targetitself is untouched.
| PARAMETER | DESCRIPTION |
|---|---|
target
|
The currently deployed pipeline to correct.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
AppliedAction
|
|
| RAISES | DESCRIPTION |
|---|---|
TypeError
|
If |
Notes
self is never mutated: the returned monitor is a brand-new
object.
Examples:
>>> import numpy as np
>>> from probcal._math import expit, logit
>>> from probcal.datasets import make_pd_portfolio
>>> from probcal.monitor import CalibrationMonitor
>>> mon = CalibrationMonitor(alpha=0.05)
>>> for k in range(6):
... d = make_pd_portfolio(n=1000, random_state=k)
... rng = np.random.default_rng(k + 1000)
... y = (rng.random(1000) < expit(logit(d.scores) + 0.8)).astype(float)
... _ = mon.update(y, d.scores, label=f"m{k}")
>>> action = mon.apply_recommendation()
>>> action.kind
're-offset'
>>> action.monitor is not mon
True
Source code in src/probcal/monitor/_monitor.py
701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 | |
to_dict
¶
to_dict() -> dict[str, object]
Versioned snapshot; the state includes every past batch — that is what makes each decision reproducible (spec invariant).
Source code in src/probcal/monitor/_monitor.py
801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 | |
from_dict
classmethod
¶
from_dict(d: dict) -> CalibrationMonitor
Rebuild a monitor mid-stream; the trajectory continues bit-for-bit.
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If the schema version is unknown or the payload class differs. |
Source code in src/probcal/monitor/_monitor.py
853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 | |
MonitorReport
dataclass
¶
MonitorReport(steps: tuple[MonitorStep, ...], alarm_at: str | None, recommendation: str, reasoning: tuple[str, ...], alpha: float = 0.05, grade_table: dict[str, float] = dict(), onset_label: str | None = None)
Full monitoring trajectory with the diagnostic recommendation.
| ATTRIBUTE | DESCRIPTION |
|---|---|
steps |
Every processed batch, in arrival order.
TYPE:
|
alarm_at |
Label of the first batch at which the alarm fired.
TYPE:
|
recommendation |
Diagnostic (no error guarantee — the component e-values are the evidence; see the monitoring chapter).
TYPE:
|
reasoning |
Plain-language trail behind the recommendation.
TYPE:
|
alpha |
The monitor's alarm level (drawn as the 1/alpha line by
TYPE:
|
grade_table |
Latest per-grade e-values.
TYPE:
|
onset_label |
Label of the batch :func:
TYPE:
|
to_frame
¶
to_frame() -> object
Steps as a list of dicts, or a pandas DataFrame when pandas is importable.
Source code in src/probcal/monitor/_monitor.py
206 207 208 | |
MonitorStep
dataclass
¶
MonitorStep(label: str, n: int, n_events: float, e_offset: float, e_shape: float, e_grades: dict[str, float], e_global: float, p_anytime: float, alarm: bool, delta_ci: tuple[float, float] | None, delta_hat: float, slope_hat: float, grade_delta_ci: dict[str, tuple[float, float] | None], log_e_increment: float)
One matured batch's monitoring record (all e-values are running values).
| ATTRIBUTE | DESCRIPTION |
|---|---|
label |
Caller-supplied batch label (opaque; arrival order is what counts).
TYPE:
|
n |
Batch size.
TYPE:
|
n_events |
Weighted event count of the batch.
TYPE:
|
e_offset, e_shape |
Running component e-values after this batch (
TYPE:
|
e_grades |
Running per-grade offset e-values (empty when no grades were given).
TYPE:
|
e_global |
Running mean of the active components — the alarm statistic.
TYPE:
|
p_anytime |
TYPE:
|
alarm |
Whether
TYPE:
|
delta_ci |
Time-uniform confidence sequence for the current offset (grid
endpoints still surviving);
TYPE:
|
delta_hat, slope_hat |
The predictable plug-ins used for this batch (from past batches only) — recorded for auditability.
TYPE:
|
grade_delta_ci |
Per-grade time-uniform confidence sequence for that grade's own
offset, same construction and grid as
TYPE:
|
log_e_increment |
This batch's additive plug-in log-LR increment: the sum of the
plug-in
TYPE:
|
moc_offset
¶
moc_offset(monitor_or_report: CalibrationMonitor | MonitorReport, *, level: float | None = None) -> LogitOffset
Margin-of-conservatism offset from a monitor's confidence sequence.
CalibrationMonitor maintains, at every batch, a time-uniform
confidence sequence (CS) for the current offset: the set of shifts
delta such that sigma(z + delta) -- applying that shift to the
monitored logits -- would itself be calibrated is covered with
probability >= 1 - alpha simultaneously at every stopping time
(MonitorStep.delta_ci, the surviving grid nulls' hull; None if
every grid null has been rejected). Its upper end, hi, is a
margin-of-conservatism offset: applying delta=hi shifts the
portfolio at least as far as the CS says drift plausibly runs, so
(loosely) it corrects for the drift with high confidence rather than
only for its point estimate.
Two ways to get hi:
level=None(default): takehifromsteps[-1].delta_cias-is, at the monitor's ownalpha.levelgiven: recompute the surviving grid nulls at that confidence level directly from the monitor's own running confidence-sequence state (the nulls whose running maximum log-e stays below-log(1 - level)) and take their max. This needs the live monitor object, not a frozen :class:~probcal.monitor.MonitorReportsnapshot, so it raisesTypeErrorfor a report.
The returned :class:~probcal.offset.LogitOffset is fit on the last
monitored batch's probabilities (expit(mon._z[-1])), which fixes
its pre_mean_/post_mean_ audit fields and its data fingerprint
to that batch. A :class:~probcal.monitor.MonitorReport retains no
batch data at all, so in that case the offset is fit on the
placeholder np.array([0.5]) instead -- delta_ is exact either
way, but pre_mean_/post_mean_ and the fingerprint are then
placeholders, not a real portfolio's summary.
| PARAMETER | DESCRIPTION |
|---|---|
monitor_or_report
|
The monitor (or its report) to read the confidence sequence from.
TYPE:
|
level
|
Confidence level in
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
LogitOffset
|
Fitted offset with |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If no batches have been processed yet, or the surviving grid-null
set is empty (every null rejected) -- widen |
TypeError
|
If |
Examples:
>>> import numpy as np
>>> from probcal.datasets import make_pd_portfolio
>>> from probcal.monitor import CalibrationMonitor, moc_offset
>>> mon = CalibrationMonitor(alpha=0.05)
>>> from probcal import expit, logit
>>> for seed in range(3):
... d = make_pd_portfolio(n=500, random_state=seed)
... rng = np.random.default_rng(seed)
... true_pd = expit(logit(d.scores) + 0.3) # +0.3 log-odds drift injected
... y = (rng.random(500) < true_pd).astype(float)
... _ = mon.update(y, d.scores, label=f"b{seed}")
>>> off = moc_offset(mon)
>>> off.delta_ >= mon.steps_[-1].delta_hat
True
Source code in src/probcal/monitor/_actions.py
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 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 | |
moc_offset_from_counts
¶
moc_offset_from_counts(y: object, p: object, *, level: float = 0.9, sample_weight: object = None) -> LogitOffset
Margin-of-conservatism offset from raw event counts (mode B, no monitor).
The Jeffreys posterior upper bound on the observed event rate,
q = beta_ppf(level, k + 0.5, n - k + 0.5) with the Kish effective
counts n = sum(w)^2 / sum(w^2) and k = n * sum(w * y) / sum(w)
(unit weights give the raw counts; rescaling every weight by a constant
leaves q unchanged) -- the same one-sided Jeffreys quantile
metrics.jeffreys_grade_test/metrics.jeffreys_upper_bands use --
becomes the offset's target mean: LogitOffset(target_mean=q) (mode
B) solves for the log-odds shift that re-anchors p's mean at
q, a conservative re-anchoring against the observed outcomes
rather than a shift read off a monitor's confidence sequence.
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Binary outcomes in
TYPE:
|
p
|
Predicted probabilities in
TYPE:
|
level
|
Confidence level in
TYPE:
|
sample_weight
|
Optional non-negative weights, same length as
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
LogitOffset
|
Fitted offset with |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
Examples:
>>> import numpy as np
>>> from probcal.monitor import moc_offset_from_counts
>>> y = np.array([0.0] * 970 + [1.0] * 30)
>>> p = np.full(1000, 0.02)
>>> off = moc_offset_from_counts(y, p, level=0.9)
>>> off.post_mean_ > 0.03
True
Source code in src/probcal/monitor/_actions.py
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |
Numerical core: PAVA, IRLS logistic regression, special functions, LOESS, spline basis.
Pure numpy + stdlib. Special functions are hand-rolled (continued fractions, series,
math.erfc/math.lgamma) and verified against scipy in
tests/test_math_reference.py.
expit
¶
expit(z: object) -> ndarray
Logistic sigmoid 1 / (1 + exp(-z)), overflow-safe.
| PARAMETER | DESCRIPTION |
|---|---|
z
|
Logits; any real values, including large magnitudes.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ndarray
|
Probabilities in |
Source code in src/probcal/_math.py
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | |
logit
¶
logit(p: object) -> ndarray
Log-odds of p, clipped to keep the output finite.
| PARAMETER | DESCRIPTION |
|---|---|
p
|
Probabilities; values outside
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ndarray
|
|
Source code in src/probcal/_math.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | |
masterscale
¶
A masterscale as one value object: the grade ladder on the calibrated scale.
A masterscale is the fixed ladder of PD bands that defines rating grades.
probcal consumes it in two shapes -- a {name: (lo, hi)} dict for
translation and a per-observation label array for testing and monitoring --
and :class:Masterscale is the single place both shapes come from.
Boundary convention, stated once: :meth:Masterscale.assign is half-open,
lo <= p < hi, with the top band closed at its upper edge, so every p
in [edges[0], edges[-1]] belongs to exactly one grade. Band inversion
(:func:probcal.thresholds.calibrated_bands_to_raw) follows the same rule on
the raw axis: band [lo, hi) maps to [inf{s: g(s) >= lo}, inf{s: g(s) >=
hi}), top band closed, so the raw intervals partition the raw axis and
counting raw scores per interval reproduces :meth:Masterscale.assign
counts exactly — also for step calibrators (isotonic, histogram binning),
whose plateaus give a shared edge positive raw-score mass.
GradeTable
dataclass
¶
GradeTable(grades: tuple[str, ...], lo: ndarray, hi: ndarray, n: ndarray, events: ndarray, mean_pd: ndarray, observed_rate: ndarray)
Bases: _ResultBase
Per-grade counts against a masterscale: the standard grade table.
| ATTRIBUTE | DESCRIPTION |
|---|---|
grades |
Every grade of the masterscale, best to worst (empty grades included).
TYPE:
|
lo, hi |
Band bounds per grade.
TYPE:
|
n |
Observation count per grade (weighted sum when
TYPE:
|
events |
Event count per grade (weighted).
TYPE:
|
mean_pd |
Mean assigned probability per grade;
TYPE:
|
observed_rate |
TYPE:
|
Masterscale
¶
Masterscale(bands: dict[str, tuple[float, float]], *, provenance: dict | None = None)
Bases: JsonIO
Frozen grade ladder on the calibrated-probability scale.
| PARAMETER | DESCRIPTION |
|---|---|
bands
|
Grade name to
TYPE:
|
provenance
|
Optional record of how the scale was built (set by
:func:
TYPE:
|
| ATTRIBUTE | DESCRIPTION |
|---|---|
names |
Grade names, best (lowest PD) to worst.
TYPE:
|
edges |
The
TYPE:
|
n_grades |
Number of grades.
TYPE:
|
Notes
Assignment is half-open, lo <= p < hi, with the top band closed at its
upper edge. Values outside [edges[0], edges[-1]] raise; a masterscale
is expected to cover [0, 1] (pass lo and hi to
:meth:from_edges explicitly if yours does not, or accept the error).
Band inversion through :func:probcal.thresholds.calibrated_bands_to_raw
uses the same half-open rule on the raw axis, so raw-interval counts and
:meth:assign counts agree exactly.
Equality and hashing cover the bands and the provenance, so
a == b exactly when a.fingerprint() == b.fingerprint(); compare
a.bands == b.bands for "same ladder, however built". Instances are
immutable and support :func:copy.deepcopy and pickling.
Examples:
>>> ms = Masterscale.from_edges([0.01, 0.05], names=["A", "B", "C"])
>>> ms.assign([0.005, 0.01, 0.05, 1.0]).tolist()
['A', 'B', 'C', 'C']
Source code in src/probcal/masterscale.py
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | |
bands
property
¶
bands: dict[str, tuple[float, float]]
{name: (lo, hi)} best to worst: the shape every band consumer accepts.
provenance
property
¶
provenance: dict | None
How the scale was built (None for a hand-built scale); a fresh copy.
from_edges
classmethod
¶
from_edges(edges: object, names: object = None, lo: float = 0.0, hi: float = 1.0, *, provenance: dict | None = None) -> Masterscale
Build from interior edges: len(edges) + 1 grades on [lo, hi].
| PARAMETER | DESCRIPTION |
|---|---|
edges
|
Interior band edges (any order; sorted here).
TYPE:
|
names
|
One name per grade, best to worst;
TYPE:
|
lo
|
Outer bounds,
TYPE:
|
hi
|
Outer bounds,
TYPE:
|
provenance
|
Passed to the constructor (see :class:
TYPE:
|
Source code in src/probcal/masterscale.py
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | |
index
¶
index(p: object) -> ndarray
Zero-based grade index per observation, lo <= p < hi, top band closed.
p is validated like any probability input (a two-column
predict_proba matrix is accepted) but not clipped away from 0
and 1, so an edge closer than 1e-12 to either end still decides.
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If any |
Source code in src/probcal/masterscale.py
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 | |
assign
¶
assign(p: object) -> ndarray
Grade name per observation (see :meth:index for the rule).
Source code in src/probcal/masterscale.py
315 316 317 | |
table
¶
table(y: object, p: object, sample_weight: object = None) -> GradeTable
Per-grade counts of y against p assigned through this scale.
Every grade is listed, including empty ones (n == 0, rates nan);
weights, when given, turn counts into weighted sums.
Source code in src/probcal/masterscale.py
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 | |
interpret
¶
interpret() -> Interpretation
The band table in words, with the boundary convention and provenance.
param_names/param_values list each grade's band as two
entries, "<grade>.lo" and "<grade>.hi".
Source code in src/probcal/masterscale.py
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 | |
to_dict
¶
to_dict() -> dict[str, object]
Versioned JSON-native snapshot (schema 1, the envelope every class uses).
Source code in src/probcal/masterscale.py
380 381 382 383 384 385 386 387 | |
from_dict
classmethod
¶
from_dict(d: dict) -> Masterscale
Rebuild from :meth:to_dict output.
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If the schema version is unknown or the payload class differs. |
Source code in src/probcal/masterscale.py
389 390 391 392 393 394 395 396 397 398 399 400 401 | |