plotters-statistical 0.1.0

Statistical chart primitives (box, violin, ROC, PR, regularization-path, residual) as native plotters series
Documentation
//! Cumulative gain and lift curves for ranking/classification evaluation.

use super::StatsError;

/// One point on a cumulative gain / lift curve.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GainPoint {
    /// Fraction of the population targeted (ranked by descending score).
    pub fraction: f64,
    /// Cumulative gain: fraction of all positives captured so far, in `[0, 1]`.
    pub gain: f64,
    /// Lift: `gain / fraction` — how many times better than random targeting.
    /// `1.0` at `fraction == 1.0`; the leading `fraction == 0` point carries
    /// `f64::NAN` lift (undefined) and is expected to be skipped when plotting
    /// lift.
    pub lift: f64,
}

/// Compute the cumulative gain / lift curve from predicted `scores` and true
/// binary `labels`. Points are ordered by descending score and always start at
/// `fraction == 0, gain == 0`.
///
/// # Errors
/// * [`StatsError::LengthMismatch`] if the inputs differ in length.
/// * [`StatsError::EmptyInput`] if empty.
/// * [`StatsError::NoPositiveLabels`] if there are no positives (gain undefined).
pub fn gain_curve(scores: &[f64], labels: &[bool]) -> Result<Vec<GainPoint>, StatsError> {
    if scores.len() != labels.len() {
        return Err(StatsError::LengthMismatch {
            scores: scores.len(),
            labels: labels.len(),
        });
    }
    if scores.is_empty() {
        return Err(StatsError::EmptyInput);
    }
    let total_p = labels.iter().filter(|&&l| l).count();
    if total_p == 0 {
        return Err(StatsError::NoPositiveLabels);
    }

    let mut order: Vec<usize> = (0..scores.len()).collect();
    order.sort_by(|&a, &b| {
        scores[b]
            .partial_cmp(&scores[a])
            .unwrap_or(std::cmp::Ordering::Equal)
    });

    let n = scores.len() as f64;
    let p = total_p as f64;
    let mut points = Vec::with_capacity(order.len() + 1);
    points.push(GainPoint {
        fraction: 0.0,
        gain: 0.0,
        lift: f64::NAN,
    });
    let mut cum_pos = 0usize;
    for (k, &i) in order.iter().enumerate() {
        if labels[i] {
            cum_pos += 1;
        }
        let fraction = (k + 1) as f64 / n;
        let gain = cum_pos as f64 / p;
        points.push(GainPoint {
            fraction,
            gain,
            lift: gain / fraction,
        });
    }
    Ok(points)
}