Skip to main content

koth_core/
stats.rs

1//! Small shared numeric helpers.
2//!
3//! These consolidate copy-pasted implementations that had *identical*
4//! semantics. Where existing local copies differ subtly (e.g. returning `0.0`
5//! rather than `NAN` for degenerate input, or computing a lower median instead
6//! of averaging the two central values) they are intentionally left in place —
7//! see the Phase-3b dedup notes.
8
9/// Sample standard deviation (divides by `n − 1`).
10///
11/// Returns `f64::NAN` when fewer than two values are supplied — matching the
12/// report/align-report summary helpers this replaces.
13pub fn std_dev(v: &[f64]) -> f64 {
14    if v.len() < 2 {
15        return f64::NAN;
16    }
17    let mean = v.iter().sum::<f64>() / v.len() as f64;
18    let var = v.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (v.len() - 1) as f64;
19    var.sqrt()
20}
21
22/// Median of `v`, sorting the slice in place.
23///
24/// Returns `0.0` for an empty slice; for even lengths returns the average of
25/// the two central values. Matches the median-of-ratios helper this replaces.
26pub fn median(v: &mut [f64]) -> f64 {
27    if v.is_empty() {
28        return 0.0;
29    }
30    v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
31    let n = v.len();
32    if n % 2 == 1 {
33        v[n / 2]
34    } else {
35        0.5 * (v[n / 2 - 1] + v[n / 2])
36    }
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn std_dev_nan_below_two() {
45        assert!(std_dev(&[]).is_nan());
46        assert!(std_dev(&[1.0]).is_nan());
47    }
48
49    #[test]
50    fn std_dev_sample_variance() {
51        // Sample std of [2, 4, 4, 4, 5, 5, 7, 9]: mean 5, var = 32/7, sqrt ≈ 2.1381
52        let s = std_dev(&[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
53        assert!((s - (32.0f64 / 7.0).sqrt()).abs() < 1e-12);
54    }
55
56    #[test]
57    fn median_empty_is_zero() {
58        let mut v: Vec<f64> = vec![];
59        assert_eq!(median(&mut v), 0.0);
60    }
61
62    #[test]
63    fn median_odd_and_even() {
64        let mut odd = [3.0, 1.0, 2.0];
65        assert_eq!(median(&mut odd), 2.0);
66        let mut even = [4.0, 1.0, 3.0, 2.0];
67        assert_eq!(median(&mut even), 2.5);
68    }
69
70    #[test]
71    fn median_sorts_in_place() {
72        let mut v = [3.0, 1.0, 2.0];
73        median(&mut v);
74        assert_eq!(v, [1.0, 2.0, 3.0]);
75    }
76}