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}