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)
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
107 108 109 | |
affine_logit_coeffs_
property
¶
affine_logit_coeffs_: tuple[float, float]
(1, delta): the offset is affine on the logit scale.
fit
¶
fit(p: object, sample_weight: object = None, *, y: 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:
|
sample_weight
|
Weights for the portfolio mean.
TYPE:
|
y
|
Ignored; accepted for compatibility with the chain fit protocol.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Self
|
The fitted offset. |
Source code in src/probcal/offset.py
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 | |
transform
¶
transform(p: object) -> ndarray
Apply the fitted shift to probabilities.
Source code in src/probcal/offset.py
156 157 158 159 160 | |
__sklearn_is_fitted__
¶
__sklearn_is_fitted__() -> bool
Fitted state for sklearn >= 1.6 (delta_ fixed or solved).
Source code in src/probcal/offset.py
164 165 166 | |
get_params
¶
get_params(deep: bool = True) -> dict[str, object]
Constructor parameters as a dict (manual sklearn-compatible clone info).
Source code in src/probcal/offset.py
173 174 175 176 177 178 179 180 | |
set_params
¶
set_params(**params: object) -> Self
Set constructor parameters; unknown names raise ValueError.
Source code in src/probcal/offset.py
182 183 184 185 186 187 188 189 190 191 192 | |
interpret
¶
interpret() -> Interpretation
Read delta in log-odds, odds-factor, and central-tendency terms.
Source code in src/probcal/offset.py
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | |
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.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
tuple of float
|
|
| RAISES | DESCRIPTION |
|---|---|
UnattainableTargetError
|
If a crossed |
ValueError
|
If |
Source code in src/probcal/offset.py
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 | |
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 as
:meth:BaseCalibrator.point_inverse — LogitOffset is not a
BaseCalibrator subclass, so the fit-guard and validation are
duplicated here rather than shared (the existing offset.py
precedent, e.g. :meth:interval_inverse).
| 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
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 | |
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
349 350 351 352 353 354 355 356 357 358 359 360 361 362 | |
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.
Source code in src/probcal/offset.py
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 | |
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
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 | |
to_json
¶
to_json(path: str | PathLike[str] | None = None, *, indent: int = 2) -> str | None
Serialize to JSON text, or to path when given (returns None then).
Source code in src/probcal/offset.py
414 415 416 417 418 419 420 421 422 423 | |
from_json
classmethod
¶
from_json(path_or_str: object) -> LogitOffset
Load from a JSON string or a filesystem path.
Source code in src/probcal/offset.py
425 426 427 428 429 430 431 432 | |
fingerprint
¶
fingerprint() -> str
SHA-256 of the canonical serialized form, blind to versions and
to the audit timestamp_ — identical fits fingerprint identically.
Source code in src/probcal/offset.py
434 435 436 437 | |
estimate_offset
¶
estimate_offset(y: object, p: object, *, sample_weight: object = None) -> OffsetEstimate
Offset-only logistic MLE of delta given p, with a Fisher 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). The Fisher information for
this one-parameter model is sum(w * q * (1 - q)) at
q = sigma(logit(p) + delta), so the standard error is its inverse
square root. That reading of the weights is the frequency one — w
counts observations — so the SE is only valid for frequency weights;
importance (or otherwise non-count) weights inflate the information and
understate the SE.
| 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
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 | |
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
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 | |
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)
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).
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:
|
Source code in src/probcal/wrapper.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | |
is_monotone_
property
¶
is_monotone_: bool
Monotone iff the 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_: object
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.
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 to the model (
TYPE:
|
y
|
Binary outcomes in
TYPE:
|
sample_weight
|
Positive observation weights.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Self
|
The fitted wrapper. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
TypeError
|
If |
Source code in src/probcal/wrapper.py
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 | |
__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
198 199 200 | |
predict_proba
¶
predict_proba(X: object) -> ndarray
Calibrated (and offset) probabilities P(y=1) for new inputs.
| PARAMETER | DESCRIPTION |
|---|---|
X
|
New inputs, passed 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
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | |
predict_proba_2d
¶
predict_proba_2d(X: object) -> ndarray
Sklearn-style (n, 2) probability matrix.
Source code in src/probcal/wrapper.py
228 229 230 231 | |
offset_to
¶
offset_to(target_mean: float | None = None, delta: float | None = None, X: 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, else on the stored
calibration scores. 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:
|
| RETURNS | DESCRIPTION |
|---|---|
Self
|
The wrapper, with the new offset appended to |
Source code in src/probcal/wrapper.py
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 | |
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 the full pipeline.
Composes right-to-left: the buffer shrinks the final interval, each
offset subtracts its delta on the logit scale, and the calibrator's
own inverse finishes the job. Returns bounds on the model's
probability output (space="probability") or their logits.
| 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.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
tuple of float
|
|
| RAISES | DESCRIPTION |
|---|---|
NotImplementedError
|
If the wrapper was fitted with |
UnattainableTargetError
|
If the (buffered) interval does not intersect the pipeline's output range. |
ValueError
|
If |
Source code in src/probcal/wrapper.py
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 | |
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
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 | |
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=...). 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
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 | |
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
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 | |
to_json
¶
to_json(path: str | PathLike[str] | None = None, *, indent: int = 2) -> str | None
Serialize to JSON text, or to path when given (returns None then).
Source code in src/probcal/wrapper.py
533 534 535 536 537 538 539 540 541 542 | |
from_json
classmethod
¶
from_json(path_or_str: object, model: Any = None) -> CalibratedModel
Load from a JSON string or a filesystem path (see :meth:from_dict).
Source code in src/probcal/wrapper.py
544 545 546 547 548 549 550 551 | |
fingerprint
¶
fingerprint() -> str
SHA-256 of the canonical serialized form, version- and timestamp-blind.
Source code in src/probcal/wrapper.py
553 554 555 | |
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: 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).
| 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.
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
79 80 81 82 83 84 85 86 87 88 89 | |
fit
¶
fit(s: object, y: object, sample_weight: object = None) -> CalibratorSelector
Run the nested selection and refit the winner on all data.
| PARAMETER | DESCRIPTION |
|---|---|
s
|
Raw scores/probabilities in
TYPE:
|
y
|
Binary outcomes in
TYPE:
|
sample_weight
|
Positive observation weights.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
CalibratorSelector
|
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
Source code in src/probcal/selection.py
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 | |
interpret
¶
interpret() -> Interpretation
Delegate to the refitted winner.
Source code in src/probcal/selection.py
265 266 267 | |
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)
Cumulative-deviation walk over predictions sorted ascending (ECCE).
| ATTRIBUTE | DESCRIPTION |
|---|---|
frac |
Cumulative fraction of observations,
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
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 | |
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
|
Validated (must match
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
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 | |
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 non-negative 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
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 | |
reliability_smooth
¶
reliability_smooth(y: object, p: object, *, sample_weight: object = None, grid_size: int = 200, n_boot: int = 100, level: 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._lattice / _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).
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Outcomes and predicted probabilities.
TYPE:
|
p
|
Outcomes and predicted probabilities.
TYPE:
|
sample_weight
|
Optional non-negative 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:
|
level
|
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
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 | |
corp_reliability
¶
corp_reliability(y: object, p: object, *, sample_weight: object = None, bands: str | None = 'consistency', level: 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 non-negative weights, same length as
TYPE:
|
bands
|
Band type to compute around the PAV fit.
TYPE:
|
level
|
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, level 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
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 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 | |
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, mirroring
metrics.ecce exactly so stat_max agrees with the metric.
sd_null is the pointwise standard deviation of the walk under
calibration — an envelope for reading, not a simultaneous band.
| PARAMETER | DESCRIPTION |
|---|---|
y
|
Outcomes and predicted probabilities.
TYPE:
|
p
|
Outcomes and predicted probabilities.
TYPE:
|
sample_weight
|
Optional non-negative 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
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 | |
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), then draws pointwise
confidence bands from the information-matrix ellipsoid — a Wald
approximation of the LR-region inversion. The
associated p-value tests the fitted polynomial against the identity.
Where the band excludes the diagonal, the data reject calibration in
that region.
| 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 non-negative weights, same length as
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
BeltResult
|
Grid coordinates, both confidence bands, selected polynomial degree, and the associated calibration-test p-value. |
Source code in src/probcal/curves.py
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 | |
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.
Theory: docs/concepts/visualization.md.
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 = 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).
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 the
0.2.0 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 restores 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 (matching :func:probcal.metrics.evaluate's
by= convention) 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. Each panel is drawn by a recursive
call with rug=False, annotate=False (light default panels; pass
stats=True for a per-panel stats box), 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).
"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
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 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 394 395 396 397 398 | |
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
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 | |
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
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 | |
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.
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
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 | |
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
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 | |
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 out of scope for this release.
| 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
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 | |
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
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 722 723 724 725 726 727 728 729 730 731 732 733 | |
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
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 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 | |
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
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 828 829 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 | |
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
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 | |
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
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 | |
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 245 246 247 248 249 | |
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
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 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 | |
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
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 543 544 545 546 547 548 549 550 551 552 553 554 555 556 | |
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)
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
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 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 | |
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
18 19 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 | |
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.
| 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.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
dict
|
Mapping of grade label to |
Source code in src/probcal/thresholds.py
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 | |
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.
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 (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
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 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 | |
datasets
¶
Synthetic dataset generators (make_pd_portfolio).
PdPortfolio
dataclass
¶
PdPortfolio(scores: ndarray, y: ndarray, p_true: ndarray)
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 absorbs intercept plus a
portfolio-level anchor solved so that mean(p_true) == event_rate
(unique by monotonicity, via bisection). With slope=1, asymmetry=0,
intercept=0 the scores are exactly calibrated.
| PARAMETER | DESCRIPTION |
|---|---|
n
|
Portfolio size.
TYPE:
|
event_rate
|
Target mean of
TYPE:
|
slope
|
Base exponent of the distortion;
TYPE:
|
intercept
|
Additional log-odds shift applied before the mean anchor is solved.
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
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 | |
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])
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
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
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, as before.
Source code in src/probcal/chain.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |
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
109 110 111 112 113 114 115 116 117 | |
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
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 | |
predict_proba
¶
predict_proba(s: object) -> ndarray
The composed calibrated probability, applied stage by stage.
Source code in src/probcal/chain.py
172 173 174 175 176 177 178 | |
__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
180 181 182 | |
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
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 | |
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
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 | |
interpret
¶
interpret() -> Interpretation
Concatenated interpretation of every stage.
Source code in src/probcal/chain.py
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | |
to_dict
¶
to_dict() -> dict[str, object]
Versioned snapshot: the stages' own envelopes, in order.
Source code in src/probcal/chain.py
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | |
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
318 319 320 321 322 323 324 325 | |
to_json
¶
to_json(path: str | PathLike[str] | None = None, *, indent: int = 2) -> str | None
Serialize to JSON text, or to path when given (returns None then).
Source code in src/probcal/chain.py
327 328 329 330 331 332 333 334 335 336 | |
from_json
classmethod
¶
from_json(path_or_str: object) -> Chain
Load from a JSON string or a filesystem path.
Source code in src/probcal/chain.py
338 339 340 341 342 343 344 345 | |
fingerprint
¶
fingerprint() -> str
SHA-256 of the canonical serialized form (stages included).
Source code in src/probcal/chain.py
347 348 349 | |
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)
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
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 | |
from_dict
classmethod
¶
from_dict(d: dict, *, model: object = None) -> AppliedAction
Rebuild from :meth:to_dict output.
| 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
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 | |
to_json
¶
to_json(path: str | PathLike[str] | None = None, *, indent: int = 2) -> str | None
Serialize to JSON text, or to path when given (returns None then).
Source code in src/probcal/monitor/_actions.py
328 329 330 331 332 333 334 335 336 337 | |
from_json
classmethod
¶
from_json(path_or_str: object, *, model: object = None) -> AppliedAction
Load from a JSON string or a filesystem path (see :meth:from_dict).
Source code in src/probcal/monitor/_actions.py
339 340 341 342 343 344 345 346 | |
fingerprint
¶
fingerprint() -> str
SHA-256 of the canonical serialized form (version/timestamp blind).
Source code in src/probcal/monitor/_actions.py
348 349 350 | |
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')
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 (per-grade
processes join automatically when
TYPE:
|
grades
|
Optional explicit grade universe;
TYPE:
|
mixture_grid
|
Positive 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 required before the plug-ins engage (before that they are the identity and their factors equal 1).
TYPE:
|
plug_in_window
|
Trailing number of past batches used by the plug-ins, and by the
recommendation rule when
TYPE:
|
recommendation_window
|
Which batches feed
TYPE:
|
| ATTRIBUTE | DESCRIPTION |
|---|---|
steps_ |
The processed batches, in arrival order.
TYPE:
|
masterscale_fingerprint_ |
Fingerprint of the
TYPE:
|
Source code in src/probcal/monitor/_monitor.py
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 | |
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; non-uniform weights break the exact martingale property and warn once (reporting parity).
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. |
Source code in src/probcal/monitor/_monitor.py
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 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 | |
report
¶
report() -> MonitorReport
Trajectory plus the diagnostic re-offset/re-fit recommendation.
Source code in src/probcal/monitor/_monitor.py
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 | |
apply_recommendation
¶
apply_recommendation(target: object = None) -> object
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 from the
recommendation window onward (:meth:_onset_index and
:meth:_recommendation_window_start -- the same window
:meth:report uses for its trailing-window diagnostics; the onset
index is recomputed directly rather than looked up by
rep.onset_label, since batch labels are opaque and may repeat,
and is unavailable altogether when any step came from a pre-0.3
payload -- the window is then the trailing one, as in
:meth:report),
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, and continuing to accumulate it
would test a null nobody deploys any more -- the same reasoning
the monitoring chapter gives for starting a new monitor after any
re-calibration.
"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-- a deep copy oftargetwith the offset appended via.offset_to(delta=est.delta);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: :meth:report and the estimation below
read only the retained batch arrays; 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
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 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 780 | |
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
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 | |
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
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 | |
to_json
¶
to_json(path: str | PathLike[str] | None = None, *, indent: int = 2) -> str | None
Serialize to JSON text, or to path when given (returns None then).
Source code in src/probcal/monitor/_monitor.py
947 948 949 950 951 952 953 954 955 956 | |
from_json
classmethod
¶
from_json(path_or_str: object) -> CalibrationMonitor
Load from a JSON string or a filesystem path.
Source code in src/probcal/monitor/_monitor.py
958 959 960 961 962 963 964 965 | |
fingerprint
¶
fingerprint() -> str
SHA-256 of the canonical serialized state (version-blind).
Source code in src/probcal/monitor/_monitor.py
967 968 969 | |
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
138 139 140 141 142 143 144 145 | |
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] = dict(), log_e_increment: float | None = None)
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 offset
plug-in's
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 state (mon._cs_grid[mon._cs_max < -log(1 - level)]) and take their max. This needs the live monitor object (its_cs_grid/_cs_maxarrays), 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)
>>> for seed in range(3):
... d = make_pd_portfolio(n=500, random_state=seed)
... rng = np.random.default_rng(seed)
... y = (rng.random(500) < d.scores).astype(float) # drift injected
... _ = 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
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 136 137 138 | |
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 k = sum(w * y)
and n = sum(w) -- 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
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 | |
Numerical core: PAVA, IRLS logistic regression, special functions, LOESS, spline basis.
Pure numpy + stdlib. Special functions are hand-rolled (continued fractions, series,
rational approximations) 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
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | |
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
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | |
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) keeps closed intervals,
since boundary points have measure zero on the raw scale; a validator
reconciling counts uses the assignment rule above.
GradeTable
dataclass
¶
GradeTable(grades: tuple[str, ...], lo: ndarray, hi: ndarray, n: ndarray, events: ndarray, mean_pd: ndarray, observed_rate: ndarray)
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)
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
keeps closed intervals; the two conventions differ only at a shared
edge, which has measure zero on the raw scale.
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
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 | |
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).
from_edges
classmethod
¶
from_edges(edges: object, names: object = None, lo: float = 0.0, hi: float = 1.0) -> 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:
|
Source code in src/probcal/masterscale.py
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 | |
index
¶
index(p: object) -> ndarray
Zero-based grade index per observation, lo <= p < hi, top band closed.
p goes through :func:probcal._validation.validate_scores, so a
two-column predict_proba matrix is accepted.
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If any |
Source code in src/probcal/masterscale.py
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | |
assign
¶
assign(p: object) -> ndarray
Grade name per observation (see :meth:index for the rule).
Source code in src/probcal/masterscale.py
235 236 237 | |
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
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 | |
interpret
¶
interpret() -> Interpretation
The band table in words, with the boundary convention and provenance.
Source code in src/probcal/masterscale.py
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 | |
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
295 296 297 298 299 300 301 302 303 304 305 306 | |
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
308 309 310 311 312 313 314 315 316 317 318 319 320 321 | |
to_json
¶
to_json(path: str | PathLike[str] | None = None, *, indent: int = 2) -> str | None
Serialize to JSON text, or to path when given (returns None then).
Source code in src/probcal/masterscale.py
323 324 325 326 327 328 329 330 331 332 | |
from_json
classmethod
¶
from_json(path_or_str: object) -> Masterscale
Load from a JSON string or a filesystem path.
Source code in src/probcal/masterscale.py
334 335 336 337 338 339 340 341 | |
fingerprint
¶
fingerprint() -> str
SHA-256 of the canonical serialized form, blind to the writing version.
Source code in src/probcal/masterscale.py
343 344 345 | |