Source code for bayesflow.diagnostics.metrics.expected_calibration_error
from typing import Any
from collections.abc import Sequence
import numpy as np
from ...utils.exceptions import ShapeError
from ...utils.classification import calibration_curve
[docs]
def expected_calibration_error(
estimates: np.ndarray,
targets: np.ndarray,
model_names: Sequence[str] = None,
num_bins: int = 10,
return_probs: bool = False,
) -> dict[str, Any]:
"""
Estimates the expected calibration error (ECE) of a model comparison network according to [1].
[1] Naeini, M. P., Cooper, G., & Hauskrecht, M. (2015). Obtaining well calibrated probabilities using
Bayesian binning. In Proceedings of the AAAI conference on artificial intelligence (Vol. 29, No. 1).
Notes
-----
Make sure that ``targets`` are **one-hot encoded** classes (i.e., model indices)!
Parameters
----------
estimates : array of shape (num_sim, num_models)
The predicted posterior model probabilities.
targets : array of shape (num_sim, num_models)
The one-hot-encoded true model indices.
model_names : Sequence[str], optional (default = None)
Optional model names to show in the output. By default, models are called "M_" + model index.
num_bins : int, optional, default: 10
The number of bins to use for the calibration curves (and marginal histograms).
Passed into ``bayesflow.utils.calibration_curve()``.
return_probs : bool (default = False)
Do you want to obtain the output of ``bayesflow.utils.calibration_curve()``?
Returns
-------
result : dict
Dictionary containing:
- "values" : np.ndarray
The expected calibration error per model
- "metric_name" : str
The name of the metric ("Expected Calibration Error").
- "model_names" : str
The (inferred) variable names.
- "probs_true": (optional) list[np.ndarray]:
Outputs of ``bayesflow.utils.calibration.calibration_curve()`` per model
- "probs_pred": (optional) list[np.ndarray]:
Outputs of ``bayesflow.utils.calibration.calibration_curve()`` per model
"""
if estimates.shape != targets.shape:
raise ShapeError("`estimates` and `targets` must have the same shape.")
if model_names is None:
model_names = ["M_" + str(i) for i in range(1, estimates.shape[-1] + 1)]
elif len(model_names) != estimates.shape[-1]:
raise ShapeError("There must be exactly one `model_name` for each model in `estimates`")
# Extract number of models and prepare containers
ece = []
probs_true = []
probs_pred = []
targets = targets.argmax(axis=-1)
# Loop for each model and compute calibration errs per bin
for model_index in range(estimates.shape[-1]):
y_true = (targets == model_index).astype(np.float32)
y_prob = estimates[..., model_index]
prob_true, prob_pred = calibration_curve(y_true, y_prob, num_bins=num_bins)
# Compute ECE by weighting bin errors by bin size
bins = np.linspace(0.0, 1.0, num_bins + 1)
binids = np.searchsorted(bins[1:-1], y_prob)
bin_total = np.bincount(binids, minlength=len(bins))
nonzero = bin_total != 0
error = np.sum(np.abs(prob_true - prob_pred) * (bin_total[nonzero] / len(y_true)))
ece.append(error)
probs_true.append(prob_true)
probs_pred.append(prob_pred)
output = dict(values=np.array(ece), metric_name="Expected Calibration Error", model_names=model_names)
if return_probs:
output["probs_true"] = probs_true
output["probs_pred"] = probs_pred
return output