plotters-statistical 0.1.0

Statistical chart primitives (box, violin, ROC, PR, regularization-path, residual) as native plotters series
Documentation
//! Calibration (reliability) binning for probabilistic classifiers.

use super::StatsError;

/// One reliability-diagram bin: the mean predicted probability, the observed
/// positive frequency, and how many samples fell in the bin.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CalibrationBin {
    /// Mean predicted probability of the samples in this bin (its x position).
    pub mean_predicted: f64,
    /// Observed fraction of positives in this bin (its y position).
    pub observed_freq: f64,
    /// Number of samples in the bin.
    pub count: usize,
}

/// Bin predicted probabilities into `n_bins` equal-width bins over `[0, 1]` and
/// compute the observed positive frequency in each. Empty bins are omitted.
/// A perfectly calibrated model has every point on the `y = x` diagonal.
///
/// # Errors
/// * [`StatsError::LengthMismatch`] if `scores`/`labels` differ in length.
/// * [`StatsError::EmptyInput`] if empty or `n_bins == 0`.
pub fn calibration_curve(
    scores: &[f64],
    labels: &[bool],
    n_bins: usize,
) -> Result<Vec<CalibrationBin>, StatsError> {
    if scores.len() != labels.len() {
        return Err(StatsError::LengthMismatch {
            scores: scores.len(),
            labels: labels.len(),
        });
    }
    if scores.is_empty() || n_bins == 0 {
        return Err(StatsError::EmptyInput);
    }
    let mut sum_pred = vec![0.0; n_bins];
    let mut sum_pos = vec![0usize; n_bins];
    let mut count = vec![0usize; n_bins];
    for (&s, &l) in scores.iter().zip(labels) {
        // Clamp into [0,1] then find the bin; score == 1.0 lands in the last bin.
        let c = s.clamp(0.0, 1.0);
        let mut idx = (c * n_bins as f64).floor() as usize;
        if idx >= n_bins {
            idx = n_bins - 1;
        }
        sum_pred[idx] += c;
        sum_pos[idx] += l as usize;
        count[idx] += 1;
    }
    Ok((0..n_bins)
        .filter(|&b| count[b] > 0)
        .map(|b| CalibrationBin {
            mean_predicted: sum_pred[b] / count[b] as f64,
            observed_freq: sum_pos[b] as f64 / count[b] as f64,
            count: count[b],
        })
        .collect())
}