Skip to main content

plotters_statistical/stats/
calibration.rs

1//! Calibration (reliability) binning for probabilistic classifiers.
2
3use super::StatsError;
4
5/// One reliability-diagram bin: the mean predicted probability, the observed
6/// positive frequency, and how many samples fell in the bin.
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct CalibrationBin {
9    /// Mean predicted probability of the samples in this bin (its x position).
10    pub mean_predicted: f64,
11    /// Observed fraction of positives in this bin (its y position).
12    pub observed_freq: f64,
13    /// Number of samples in the bin.
14    pub count: usize,
15}
16
17/// Bin predicted probabilities into `n_bins` equal-width bins over `[0, 1]` and
18/// compute the observed positive frequency in each. Empty bins are omitted.
19/// A perfectly calibrated model has every point on the `y = x` diagonal.
20///
21/// # Errors
22/// * [`StatsError::LengthMismatch`] if `scores`/`labels` differ in length.
23/// * [`StatsError::EmptyInput`] if empty or `n_bins == 0`.
24pub fn calibration_curve(
25    scores: &[f64],
26    labels: &[bool],
27    n_bins: usize,
28) -> Result<Vec<CalibrationBin>, StatsError> {
29    if scores.len() != labels.len() {
30        return Err(StatsError::LengthMismatch {
31            scores: scores.len(),
32            labels: labels.len(),
33        });
34    }
35    if scores.is_empty() || n_bins == 0 {
36        return Err(StatsError::EmptyInput);
37    }
38    let mut sum_pred = vec![0.0; n_bins];
39    let mut sum_pos = vec![0usize; n_bins];
40    let mut count = vec![0usize; n_bins];
41    for (&s, &l) in scores.iter().zip(labels) {
42        // Clamp into [0,1] then find the bin; score == 1.0 lands in the last bin.
43        let c = s.clamp(0.0, 1.0);
44        let mut idx = (c * n_bins as f64).floor() as usize;
45        if idx >= n_bins {
46            idx = n_bins - 1;
47        }
48        sum_pred[idx] += c;
49        sum_pos[idx] += l as usize;
50        count[idx] += 1;
51    }
52    Ok((0..n_bins)
53        .filter(|&b| count[b] > 0)
54        .map(|b| CalibrationBin {
55            mean_predicted: sum_pred[b] / count[b] as f64,
56            observed_freq: sum_pos[b] as f64 / count[b] as f64,
57            count: count[b],
58        })
59        .collect())
60}