plotters-statistical 0.1.0

Statistical chart primitives (box, violin, ROC, PR, regularization-path, residual) as native plotters series
Documentation
//! Unit tests for the v0.2 stats additions (correlation, ECDF, normal quantile,
//! histogram, calibration, gain) against hand-computed reference values.

use approx::assert_abs_diff_eq;
use plotters_statistical::stats::{
    calibration_curve, correlation_matrix, dkw_epsilon, ecdf, gain_curve, histogram, norm_ppf,
    pearson, rank, spearman, BinRule, CorrelationMethod, StatsError,
};

#[test]
fn pearson_perfect_and_anti() {
    assert_abs_diff_eq!(pearson(&[1.0, 2.0, 3.0], &[2.0, 4.0, 6.0]).unwrap(), 1.0);
    assert_abs_diff_eq!(pearson(&[1.0, 2.0, 3.0], &[3.0, 2.0, 1.0]).unwrap(), -1.0);
}

#[test]
fn pearson_zero_variance_errors() {
    assert_eq!(
        pearson(&[1.0, 1.0, 1.0], &[1.0, 2.0, 3.0]),
        Err(StatsError::ZeroVariance)
    );
}

#[test]
fn rank_handles_ties() {
    // 20 and 20 are tied for ranks 2 and 3 -> both get 2.5.
    assert_eq!(rank(&[10.0, 20.0, 20.0, 40.0]), vec![1.0, 2.5, 2.5, 4.0]);
}

#[test]
fn spearman_monotonic_nonlinear() {
    // Perfectly monotonic but nonlinear -> Spearman 1.0 (Pearson would be < 1).
    let s = spearman(&[1.0, 2.0, 3.0, 4.0], &[1.0, 4.0, 9.0, 16.0]).unwrap();
    assert_abs_diff_eq!(s, 1.0);
}

#[test]
fn correlation_matrix_is_symmetric_unit_diagonal() {
    let cols = vec![
        vec![1.0, 2.0, 3.0, 4.0],
        vec![2.0, 4.0, 6.0, 8.0],
        vec![4.0, 3.0, 2.0, 1.0],
    ];
    let m = correlation_matrix(&cols, CorrelationMethod::Pearson).unwrap();
    for (i, row) in m.iter().enumerate() {
        assert_abs_diff_eq!(row[i], 1.0);
        for (j, &v) in row.iter().enumerate() {
            assert_abs_diff_eq!(v, m[j][i]); // symmetric
        }
    }
    assert_abs_diff_eq!(m[0][1], 1.0); // col1 = 2*col0
    assert_abs_diff_eq!(m[0][2], -1.0); // col2 is the reverse
}

#[test]
fn ecdf_basic() {
    let e = ecdf(&[3.0, 1.0, 2.0, 2.0]).unwrap();
    assert_eq!(e.x, vec![1.0, 2.0, 3.0]);
    assert_eq!(e.p, vec![0.25, 0.75, 1.0]);
    assert_eq!(e.n, 4);
}

#[test]
fn dkw_epsilon_reference() {
    // sqrt(ln(2/0.05)/(2*100)) = sqrt(ln(40)/200).
    let expected = ((2.0f64 / 0.05).ln() / 200.0).sqrt();
    assert_abs_diff_eq!(dkw_epsilon(100, 0.05), expected, epsilon = 1e-12);
}

#[test]
fn norm_ppf_reference_points() {
    assert_abs_diff_eq!(norm_ppf(0.5), 0.0, epsilon = 1e-9);
    assert_abs_diff_eq!(norm_ppf(0.975), 1.959963985, epsilon = 1e-6);
    assert_abs_diff_eq!(norm_ppf(0.025), -1.959963985, epsilon = 1e-6);
}

#[test]
fn histogram_counts_sum_to_n() {
    let data: Vec<f64> = (0..=10).map(|i| i as f64).collect();
    let h = histogram(&data, BinRule::Count(5)).unwrap();
    assert_eq!(h.edges.len(), 6);
    assert_eq!(h.counts.iter().sum::<usize>(), data.len());
    assert_abs_diff_eq!(h.bin_width(), 2.0);
}

#[test]
fn histogram_constant_errors() {
    assert_eq!(
        histogram(&[5.0, 5.0, 5.0], BinRule::Sturges),
        Err(StatsError::ZeroVariance)
    );
}

#[test]
fn calibration_bins_partition() {
    let scores = [0.05, 0.15, 0.85, 0.95];
    let labels = [false, false, true, true];
    let bins = calibration_curve(&scores, &labels, 2).unwrap();
    // Two occupied bins: low predictions all-negative, high predictions all-positive.
    assert_eq!(bins.len(), 2);
    assert_abs_diff_eq!(bins[0].observed_freq, 0.0);
    assert_abs_diff_eq!(bins[1].observed_freq, 1.0);
}

#[test]
fn gain_perfect_ranking() {
    // Positives ranked strictly above negatives -> gain hits 1.0 at fraction 0.5.
    let scores = [0.9, 0.8, 0.2, 0.1];
    let labels = [true, true, false, false];
    let g = gain_curve(&scores, &labels).unwrap();
    assert!(g[0].fraction == 0.0 && g[0].lift.is_nan());
    let at_half = g.iter().find(|p| (p.fraction - 0.5).abs() < 1e-9).unwrap();
    assert_abs_diff_eq!(at_half.gain, 1.0);
    assert_abs_diff_eq!(g.last().unwrap().gain, 1.0);
}

#[test]
fn gain_no_positives_errors() {
    assert_eq!(
        gain_curve(&[0.1, 0.2], &[false, false]),
        Err(StatsError::NoPositiveLabels)
    );
}