CORP Evaluation¶
A binned reliability diagram makes the analyst pick the bins, and the picture changes with the choice. The CORP approach of Dimitriadis, Gneiting & Jordan (PNAS 2021) removes the choice: conditional event probabilities are estimated by isotonic regression via PAV, so the algorithm determines the number and position of the flat segments and there is nothing left to tune in your favor.
The diagram and score decomposition match R’s reliabilitydiag on calibrated,
overconfident, squashed, heavily tied, and rare-event datasets within a tight
floating-point tolerance.
Reliability Diagram¶
- class calibre.evaluation.ReliabilityDiagram(prediction_values, event_probabilities, prediction_weights)[source]¶
A fitted CORP reliability diagram.
- Parameters:
prediction_values (np.ndarray)
event_probabilities (np.ndarray)
prediction_weights (np.ndarray)
- prediction_values¶
The distinct forecast values, ascending.
- event_probabilities¶
PAV-recalibrated conditional event probability at each forecast value.
- prediction_weights¶
Evaluation mass carrying each forecast value.
Notes
Points where the diagram is flat are the CORP bins: the PAV algorithm chose them, so no bin count needs to be supplied and none can be tuned to flatter the forecaster.
- as_function()[source]¶
Return the recalibration map as a callable.
- Returns:
- Piecewise-linear interpolation of
the diagram, matching the paper’s display convention. A single distinct forecast value gives a step function, since there is nothing to interpolate between.
- Return type:
PiecewiseLinear or StepFunction
- plot(**kwargs)[source]¶
Draw this diagram.
Convenience wrapper around
calibre.plots.plot_reliability_diagram(), which documents every keyword. Needs matplotlib:pip install 'calibre[plots]'.- Parameters:
**kwargs (Any) – Passed straight through to
plot_reliability_diagram()–ax,bands,density,style,diagonal,colorandlabel.- Returns:
The axes drawn on.
- Return type:
Examples
>>> import matplotlib >>> matplotlib.use("Agg") >>> import numpy as np >>> from calibre import corp_reliability >>> rng = np.random.default_rng(0) >>> x = rng.uniform(0, 1, 200) >>> y = rng.binomial(1, x).astype(float) >>> ax = corp_reliability(y, x).plot(density="none") >>> ax.get_xlabel() 'forecast probability'
- calibre.corp_reliability(y_true, y_pred, *, sample_weight=None)[source]¶
Estimate conditional event probabilities by isotonic regression.
This is the CORP reliability diagram: the PAV-recalibrated forecast probabilities plotted against the original forecast values. Unlike a binned diagram it needs no bin count, because PAV determines the number and position of the flat segments itself.
- Parameters:
y_true (ndarray) – Binary outcomes in
{0, 1}from evaluation data not used to fit the forecaster.y_pred (ndarray) – Forecast probabilities in
[0, 1]on the same evaluation data.sample_weight (ndarray | None) – Non-negative frequency or evaluation-mass weights. Zero-weight observations are ignored. Defaults to 1.
- Returns:
The fitted diagram.
- Return type:
Examples
>>> import numpy as np >>> from calibre.evaluation import corp_reliability >>> x = np.array([0.2, 0.4, 0.6, 0.8]) >>> y = np.array([0.0, 1.0, 0.0, 1.0]) >>> diagram = corp_reliability(y, x)
The middle pair violates monotonicity, so PAV pools it to its mean:
>>> diagram.event_probabilities array([0. , 0.5, 0.5, 1. ])
Notes
The unweighted estimator follows Dimitriadis, Gneiting & Jordan (2021) and is pinned against their
reliabilitydiagR package.sample_weightis calibre’s frequency-weight extension: integer weights give exactly the same diagram as literal row replication.See also
score_decomposition : The score decomposition built on this estimate. calibre.CenteredIsotonicCalibrator : Recalibration, rather than diagnosis.
Score Decomposition¶
- calibre.score_decomposition(y_true, y_pred, *, score='brier', sample_weight=None)[source]¶
Decompose a mean score into miscalibration, discrimination and uncertainty.
Returns the CORP decomposition
mean_score = miscalibration - discrimination + uncertainty, where the calibrated forecasts are the PAV-recalibrated probabilities and the reference forecast is the marginal event frequency.Miscalibration is what recalibration would save you, discrimination is what your forecasts buy over always predicting the base rate, and uncertainty is the difficulty of the problem, which no forecaster can change.
- Parameters:
y_true (np.ndarray) – Binary outcomes in
{0, 1}from evaluation data not used to fit the forecaster or calibrator.y_pred (np.ndarray) – Forecast probabilities in
[0, 1]on the same evaluation data.score (str | Callable[[np.ndarray, np.ndarray], np.ndarray]) – Proper scoring rule:
"brier"(default),"log", or a vectorized callable accepting(y_true, y_pred)and returning one finite loss per observation. The caller is responsible for ensuring a custom score is proper for binary probability forecasts.sample_weight (np.ndarray | None) – Non-negative frequency or evaluation-mass weights. Zero-weight observations are ignored. Defaults to 1.
- Returns:
mean_score,miscalibration,discrimination, anduncertainty. Miscalibration and discrimination are non-negative for proper scoring rules, guaranteed by the optimality of the PAV solution.
- Return type:
Examples
>>> import numpy as np >>> from calibre.evaluation import score_decomposition >>> rng = np.random.default_rng(0) >>> x = rng.uniform(0, 1, 2000) >>> y = rng.binomial(1, x).astype(float)
These forecasts are calibrated by construction, so miscalibration is small while discrimination is substantial:
>>> d = score_decomposition(y, x) >>> bool(d["miscalibration"] < 0.01), bool(d["discrimination"] > 0.05) (True, True)
The identity holds exactly:
>>> reconstructed = ( ... d["miscalibration"] ... - d["discrimination"] ... + d["uncertainty"] ... ) >>> bool(abs(d["mean_score"] - reconstructed) < 1e-12) True
Warning
Evaluate held-out or out-of-fold predictions. For an isotonic-family calibrator, in-sample miscalibration is zero by construction because the calibrator and this diagnostic use the same idempotent PAV projection.
Notes
This follows Dimitriadis, Gneiting & Jordan (2021) and is pinned against their
reliabilitydiagR package for the Brier score. Evaluation weights are calibre’s extension; integer weights are equivalent to row replication.See also
corp_reliability : The recalibration this decomposition is built on.
Uncertainty Bands¶
Both are resampling-based. The paper’s asymptotic route is not implemented.
- calibre.consistency_bands(y_pred, *, level=0.9, n_resamples=1000, random_state=0)[source]¶
Pointwise null bands for a perfectly calibrated reliability diagram.
Forecast rows are sampled with replacement from their empirical distribution. For each sample, outcomes are drawn as
y* ~ Bernoulli(y_pred)and the CORP diagram is refit. Pointwise percentiles of those refits show how far the estimated diagram can wander under the calibration hypothesis.Use
confidence_bands()instead for an interval around the estimate.- Parameters:
y_pred (ndarray) – Forecast probabilities in
[0, 1]. Observed outcomes are not needed because the calibration null supplies their distribution.level (float) – Nominal coverage, default 0.9.
n_resamples (int) – Number of resamples, default 1000.
random_state (int | None) – Seed. Defaults to 0 so results are reproducible.
- Returns:
prediction_values(the original forecast support),lowerandupperpointwise bounds.
- Return type:
Notes
These are pointwise bands, not a simultaneous envelope or a global calibration test.
The method follows Dimitriadis, Gneiting & Jordan (2021), “Stable reliability diagrams for probabilistic classifiers”, and their
reliabilitydiagR implementation. Only its resampling method is implemented here; the paper also derives asymptotic bands.Each resampled fit contributes only on its observed forecast support. This also applies to singleton fits, avoiding extrapolation to forecast values absent from that resample.
- calibre.confidence_bands(y_true, y_pred, *, level=0.9, n_resamples=1000, random_state=0)[source]¶
Pointwise bootstrap intervals around a CORP reliability estimate.
Forecast rows are sampled with replacement. Outcomes are then drawn from the PAV-recalibrated probabilities and a CORP diagram is refit. The returned bounds are pointwise percentiles of those refits, with the boundary correction used by the authors’ reference implementation.
- Parameters:
- Returns:
prediction_values(the original forecast support),lowerandupperpointwise bounds.
- Return type:
Warning
Evaluate held-out or out-of-fold predictions. These are pointwise bootstrap intervals, not a simultaneous envelope. Their finite-sample coverage can differ from
leveland should be validated for the intended data-generating setting.Notes
This implements the resampling confidence-region method of Dimitriadis, Gneiting & Jordan (2021), “Stable reliability diagrams for probabilistic classifiers”, following their
reliabilitydiagR implementation. Each resampled fit contributes only on its observed forecast support, including when that fit contains a single forecast value.
Usage¶
Decomposing a proper score¶
mean_score = MCB - DSC + UNC holds exactly, and both MCB and DSC are
non-negative by construction:
import numpy as np
from calibre import score_decomposition
rng = np.random.default_rng(0)
scores = rng.uniform(0, 1, 3000)
labels = rng.binomial(1, scores).astype(float)
overconfident = np.clip(1.6 * (scores - 0.5) + 0.5, 0, 1)
for name, x in (("honest", scores), ("overconfident", overconfident)):
d = score_decomposition(labels, x)
print(
f"{name:14s} Brier {d['mean_score']:.4f} = "
f"MCB {d['miscalibration']:.4f} - "
f"DSC {d['discrimination']:.4f} + UNC {d['uncertainty']:.4f}"
)
MCB is what recalibration would save you, DSC is what your scores buy
over always predicting the base rate, and UNC is the difficulty of the
problem, which no forecaster can change. A plain Brier score tells you the model
got worse; this tells you which part you can fix.
Measuring honestly¶
Scoring a calibrator on the data it was fit to does not merely flatter it. For any isotonic-family calibrator it reports perfect calibration by construction, because the calibrator and the diagnostic are the same PAV projection and PAV is idempotent:
import numpy as np
from calibre import IsotonicCalibrator, cross_val_calibrate, score_decomposition
rng = np.random.default_rng(0)
scores = rng.uniform(0, 1, 1500)
labels = rng.binomial(1, scores).astype(float)
in_sample = IsotonicCalibrator().fit(scores, labels).transform(scores)
out_of_fold = cross_val_calibrate(IsotonicCalibrator(), scores, labels, cv=5)
print(
f"MCB in-sample "
f"{score_decomposition(labels, in_sample)['miscalibration']:.4f}"
)
print(
f"MCB out-of-fold "
f"{score_decomposition(labels, out_of_fold)['miscalibration']:.4f}"
)
The in-sample number is zero no matter how badly the model generalizes. Use
cross_val_calibrate() for any number you intend to believe.
References¶
Dimitriadis, T., Gneiting, T. & Jordan, A. I. (2021), “Stable reliability diagrams for probabilistic classifiers”, PNAS 118(8).