git_perf/
stats.rs

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