Skip to main content

quantik_core/bench/
metrics.rs

1//! Statistics helpers for benchmark reports (port of `benchmarks/metrics.py`).
2
3/// Wilson score interval for a binomial proportion (z = 1.96).
4pub fn wilson_ci(hits: u64, n: u64) -> (f64, f64) {
5    let z = 1.96f64;
6    if n == 0 {
7        return (0.0, 0.0);
8    }
9    let n = n as f64;
10    let p = hits as f64 / n;
11    let z2 = z * z;
12    let denom = 1.0 + z2 / n;
13    let centre = (p + z2 / (2.0 * n)) / denom;
14    let margin = z * (p * (1.0 - p) / n + z2 / (4.0 * n * n)).sqrt() / denom;
15    ((centre - margin).max(0.0), (centre + margin).min(1.0))
16}
17
18/// Mean and sample standard deviation, with std 0 for n < 2.
19pub fn mean_std(xs: &[f64]) -> (f64, f64) {
20    let n = xs.len();
21    if n == 0 {
22        return (0.0, 0.0);
23    }
24    let mean = xs.iter().sum::<f64>() / n as f64;
25    if n < 2 {
26        return (mean, 0.0);
27    }
28    let variance = xs.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n as f64 - 1.0);
29    (mean, variance.sqrt())
30}
31
32/// Linear-interpolated percentile, or 0.0 for empty input.
33pub fn percentile(xs: &[f64], p: f64) -> f64 {
34    if xs.is_empty() {
35        return 0.0;
36    }
37    let mut ordered = xs.to_vec();
38    ordered.sort_by(|a, b| a.total_cmp(b));
39    if ordered.len() == 1 {
40        return ordered[0];
41    }
42    let k = (ordered.len() - 1) as f64 * (p / 100.0);
43    let lo = k.floor() as usize;
44    let hi = k.ceil() as usize;
45    if lo == hi {
46        return ordered[k as usize];
47    }
48    ordered[lo] * (hi as f64 - k) + ordered[hi] * (k - lo as f64)
49}
50
51/// The 50th percentile.
52pub fn median(xs: &[f64]) -> f64 {
53    percentile(xs, 50.0)
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    fn close(a: f64, b: f64, tol: f64) -> bool {
61        (a - b).abs() < tol
62    }
63
64    #[test]
65    fn wilson_ci_matches_reference() {
66        // Reference values computed with the Python implementation.
67        let (lo, hi) = wilson_ci(8, 10);
68        assert!(close(lo, 0.4901, 1e-3), "lo {lo}");
69        assert!(close(hi, 0.9433, 1e-3), "hi {hi}");
70        assert_eq!(wilson_ci(0, 0), (0.0, 0.0));
71        let (lo, hi) = wilson_ci(0, 5);
72        assert_eq!(lo, 0.0);
73        assert!(hi > 0.0 && hi < 1.0);
74        let (lo, hi) = wilson_ci(5, 5);
75        assert!(lo > 0.0 && lo < 1.0);
76        assert_eq!(hi, 1.0);
77    }
78
79    #[test]
80    fn percentile_linear_interpolation() {
81        let xs = [1.0, 2.0, 3.0, 4.0];
82        assert!(close(percentile(&xs, 95.0), 3.85, 1e-12));
83        assert_eq!(percentile(&xs, 0.0), 1.0);
84        assert_eq!(percentile(&xs, 100.0), 4.0);
85        assert_eq!(percentile(&[], 50.0), 0.0);
86        assert_eq!(percentile(&[7.0], 95.0), 7.0);
87    }
88
89    #[test]
90    fn median_odd_even() {
91        assert_eq!(median(&[3.0, 1.0, 2.0]), 2.0);
92        assert_eq!(median(&[4.0, 1.0, 3.0, 2.0]), 2.5);
93    }
94
95    #[test]
96    fn mean_std_basics() {
97        assert_eq!(mean_std(&[]), (0.0, 0.0));
98        assert_eq!(mean_std(&[5.0]), (5.0, 0.0));
99        let (mean, std) = mean_std(&[1.0, 2.0, 3.0]);
100        assert_eq!(mean, 2.0);
101        assert_eq!(std, 1.0);
102    }
103}