git_perf/
stats.rs

1use std::fmt::Display;
2
3use average::{self, concatenate, Estimate, Mean, Variance};
4use itertools::Itertools;
5
6use git_perf_cli_types::ReductionFunc;
7
8use readable::num::*;
9
10pub trait VecAggregation {
11    fn median(&mut self) -> Option<f64>;
12}
13
14concatenate!(AggStats, [Mean, mean], [Variance, sample_variance]);
15
16pub fn aggregate_measurements<'a>(measurements: impl Iterator<Item = &'a f64>) -> Stats {
17    let s: AggStats = measurements.collect();
18    Stats {
19        mean: s.mean(),
20        stddev: s.sample_variance().sqrt(),
21        len: s.mean.len() as usize,
22    }
23}
24
25#[derive(Debug)]
26pub struct Stats {
27    pub mean: f64,
28    pub stddev: f64,
29    pub len: usize,
30}
31
32impl Display for Stats {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        write!(
35            f,
36            "μ: {} σ: {} n: {}",
37            Float::from(self.mean),
38            Float::from(self.stddev),
39            Unsigned::from(self.len),
40        )
41    }
42}
43
44impl Stats {
45    pub fn z_score(&self, other: &Stats) -> f64 {
46        assert!(self.len == 1);
47        assert!(other.len >= 1);
48        (self.mean - other.mean).abs() / other.stddev
49    }
50}
51
52impl VecAggregation for Vec<f64> {
53    fn median(&mut self) -> Option<f64> {
54        self.sort_by(f64::total_cmp);
55        match self.len() {
56            0 => None,
57            even if even % 2 == 0 => {
58                let left = self[even / 2 - 1];
59                let right = self[even / 2];
60                Some((left + right) / 2.0)
61            }
62            odd => Some(self[odd / 2]),
63        }
64    }
65}
66
67pub trait NumericReductionFunc: Iterator<Item = f64> {
68    fn aggregate_by(&mut self, fun: ReductionFunc) -> Option<Self::Item> {
69        match fun {
70            ReductionFunc::Min => self.reduce(f64::min),
71            ReductionFunc::Max => self.reduce(f64::max),
72            ReductionFunc::Median => self.collect_vec().median(),
73            ReductionFunc::Mean => {
74                let stats: AggStats = self.collect();
75                if stats.mean.is_empty() {
76                    None
77                } else {
78                    Some(stats.mean())
79                }
80            }
81        }
82    }
83}
84
85impl<T> NumericReductionFunc for T where T: Iterator<Item = f64> {}
86
87#[cfg(test)]
88mod test {
89    use super::*;
90
91    #[test]
92    fn no_floating_error() {
93        let measurements = (0..100).map(|_| 0.1).collect_vec();
94        let stats = aggregate_measurements(measurements.iter());
95        assert_eq!(stats.mean, 0.1);
96        assert_eq!(stats.len, 100);
97        let naive_mean = (0..100).map(|_| 0.1).sum::<f64>() / 100.0;
98        assert_ne!(naive_mean, 0.1);
99    }
100
101    #[test]
102    fn single_measurement() {
103        let measurements = vec![1.0];
104        let stats = aggregate_measurements(measurements.iter());
105        assert_eq!(stats.len, 1);
106        assert_eq!(stats.mean, 1.0);
107        assert_eq!(stats.stddev, 0.0);
108    }
109
110    #[test]
111    fn no_measurement() {
112        let measurements = vec![];
113        let stats = aggregate_measurements(measurements.iter());
114        assert_eq!(stats.len, 0);
115        assert_eq!(stats.mean, 0.0);
116        assert_eq!(stats.stddev, 0.0);
117    }
118
119    #[test]
120    fn z_score_with_zero_stddev() {
121        let stddev = 0.0;
122        let mean = 30.0;
123        let higher_val = 50.0;
124        let lower_val = 10.0;
125        let z_high = ((higher_val - mean) / stddev as f64).abs();
126        let z_low = ((lower_val - mean) / stddev as f64).abs();
127        assert_eq!(z_high, f64::INFINITY);
128        assert_eq!(z_low, f64::INFINITY);
129    }
130
131    #[test]
132    fn verify_stats() {
133        let empty_vec = [];
134        assert_eq!(None, empty_vec.into_iter().aggregate_by(ReductionFunc::Min));
135        assert_eq!(None, empty_vec.into_iter().aggregate_by(ReductionFunc::Max));
136        assert_eq!(
137            None,
138            empty_vec.into_iter().aggregate_by(ReductionFunc::Median)
139        );
140        assert_eq!(
141            None,
142            empty_vec.into_iter().aggregate_by(ReductionFunc::Mean)
143        );
144
145        let single_el_vec = [3.0];
146        assert_eq!(
147            Some(3.0),
148            single_el_vec.into_iter().aggregate_by(ReductionFunc::Min)
149        );
150        assert_eq!(
151            Some(3.0),
152            single_el_vec.into_iter().aggregate_by(ReductionFunc::Max)
153        );
154        assert_eq!(
155            Some(3.0),
156            single_el_vec
157                .into_iter()
158                .aggregate_by(ReductionFunc::Median)
159        );
160        assert_eq!(
161            Some(3.0),
162            single_el_vec.into_iter().aggregate_by(ReductionFunc::Mean)
163        );
164
165        let two_el_vec = [3.0, 1.0];
166        assert_eq!(
167            Some(1.0),
168            two_el_vec.into_iter().aggregate_by(ReductionFunc::Min)
169        );
170        assert_eq!(
171            Some(3.0),
172            two_el_vec.into_iter().aggregate_by(ReductionFunc::Max)
173        );
174        assert_eq!(
175            Some(2.0),
176            two_el_vec.into_iter().aggregate_by(ReductionFunc::Median)
177        );
178        assert_eq!(
179            Some(2.0),
180            two_el_vec.into_iter().aggregate_by(ReductionFunc::Mean)
181        );
182
183        let three_el_vec = [2.0, 6.0, 1.0];
184        assert_eq!(
185            Some(1.0),
186            three_el_vec.into_iter().aggregate_by(ReductionFunc::Min)
187        );
188        assert_eq!(
189            Some(6.0),
190            three_el_vec.into_iter().aggregate_by(ReductionFunc::Max)
191        );
192        assert_eq!(
193            Some(2.0),
194            three_el_vec.into_iter().aggregate_by(ReductionFunc::Median)
195        );
196        assert_eq!(
197            Some(3.0),
198            three_el_vec.into_iter().aggregate_by(ReductionFunc::Mean)
199        );
200    }
201}