1use serde::{Deserialize, Serialize};
24
25use crate::error::AnalysisError;
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30#[serde(try_from = "DistributionData")]
31pub struct Distribution {
32 attempted: usize,
33 sorted: Vec<f64>,
34}
35
36#[derive(Deserialize)]
38#[serde(deny_unknown_fields)]
39struct DistributionData {
40 attempted: usize,
41 sorted: Vec<f64>,
42}
43
44impl TryFrom<DistributionData> for Distribution {
45 type Error = AnalysisError;
46
47 fn try_from(data: DistributionData) -> Result<Self, AnalysisError> {
48 Self::new(data.sorted, data.attempted)
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
54pub struct Summary {
55 pub attempted: usize,
57 pub count: usize,
59 pub mean: Option<f64>,
61 pub standard_deviation: Option<f64>,
63 pub min: Option<f64>,
65 pub p05: Option<f64>,
67 pub p50: Option<f64>,
69 pub p95: Option<f64>,
71 pub max: Option<f64>,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
78pub struct Share {
79 pub low: f64,
81 pub high: f64,
83}
84
85impl Distribution {
86 pub fn new(values: Vec<f64>, attempted: usize) -> Result<Self, AnalysisError> {
94 if values.len() > attempted {
95 return Err(AnalysisError::Count {
96 what: "values in a distribution, against the runs tried",
97 count: values.len(),
98 limit: attempted,
99 });
100 }
101 if let Some(&bad) = values.iter().find(|v| !v.is_finite()) {
102 return Err(AnalysisError::Domain {
103 what: "value in a distribution",
104 value: bad,
105 });
106 }
107 let mut sorted = values;
108 sorted.sort_by(f64::total_cmp);
109 Ok(Self { attempted, sorted })
110 }
111
112 pub fn attempted(&self) -> usize {
114 self.attempted
115 }
116
117 pub fn count(&self) -> usize {
119 self.sorted.len()
120 }
121
122 pub fn missing(&self) -> usize {
124 self.attempted - self.sorted.len()
125 }
126
127 pub fn sorted(&self) -> &[f64] {
129 &self.sorted
130 }
131
132 pub fn min(&self) -> Option<f64> {
134 self.sorted.first().copied()
135 }
136
137 pub fn max(&self) -> Option<f64> {
139 self.sorted.last().copied()
140 }
141
142 pub fn mean(&self) -> Option<f64> {
144 let shift = self.min()?;
145 Some(shift + self.shifted_mean(shift))
146 }
147
148 pub fn standard_deviation(&self) -> Option<f64> {
151 if self.sorted.len() < 2 {
152 return None;
153 }
154 let shift = self.min()?;
155 let mean = self.shifted_mean(shift);
156 let sum_squares: f64 = self
157 .sorted
158 .iter()
159 .map(|&x| {
160 let d = (x - shift) - mean;
161 d * d
162 })
163 .sum();
164 Some((sum_squares / (self.sorted.len() - 1) as f64).sqrt())
166 }
167
168 fn shifted_mean(&self, shift: f64) -> f64 {
170 let sum: f64 = self.sorted.iter().map(|&x| x - shift).sum();
171 sum / self.sorted.len() as f64
173 }
174
175 pub fn quantile(&self, p: f64) -> Result<Option<f64>, AnalysisError> {
182 if !(0.0..=1.0).contains(&p) {
183 return Err(AnalysisError::Domain {
184 what: "quantile probability",
185 value: p,
186 });
187 }
188 let Some(&last) = self.sorted.last() else {
189 return Ok(None);
190 };
191 let h = (self.sorted.len() - 1) as f64 * p;
193 let below = h.floor();
194 let index = below as usize;
195 let Some(&above) = self.sorted.get(index + 1) else {
196 return Ok(Some(last));
197 };
198 let low = self.sorted[index];
199 Ok(Some(low + (h - below) * (above - low)))
200 }
201
202 pub fn share_at_least(&self, threshold: f64) -> Result<Option<Share>, AnalysisError> {
210 if threshold.is_nan() {
211 return Err(AnalysisError::Domain {
212 what: "threshold of a share",
213 value: threshold,
214 });
215 }
216 if self.attempted == 0 {
217 return Ok(None);
218 }
219 let passed = self.sorted.len() - self.sorted.partition_point(|&x| x < threshold);
220 let attempted = self.attempted as f64;
222 Ok(Some(Share {
223 low: passed as f64 / attempted,
224 high: (passed + self.missing()) as f64 / attempted,
225 }))
226 }
227
228 pub fn summary(&self) -> Summary {
230 let quantile = |p: f64| self.quantile(p).ok().flatten();
232 Summary {
233 attempted: self.attempted,
234 count: self.count(),
235 mean: self.mean(),
236 standard_deviation: self.standard_deviation(),
237 min: self.min(),
238 p05: quantile(0.05),
239 p50: quantile(0.5),
240 p95: quantile(0.95),
241 max: self.max(),
242 }
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn equal_values_give_their_value_and_no_spread() {
252 let d = Distribution::new(vec![0.1; 7], 7).unwrap();
255 assert_eq!(d.mean(), Some(0.1));
256 assert_eq!(d.standard_deviation(), Some(0.0));
257 assert_eq!(d.quantile(0.37).unwrap(), Some(0.1));
258 }
259
260 #[test]
261 fn moments_and_quantiles_by_hand() {
262 let d = Distribution::new(vec![4.0, 1.0, 3.0, 2.0], 6).unwrap();
263 assert_eq!(d.sorted(), &[1.0, 2.0, 3.0, 4.0]);
264 assert_eq!((d.count(), d.missing(), d.attempted()), (4, 2, 6));
265 assert_eq!(d.mean(), Some(2.5));
266 assert_eq!(d.standard_deviation(), Some((5.0_f64 / 3.0).sqrt()));
268 assert_eq!(d.quantile(0.0).unwrap(), Some(1.0));
271 assert_eq!(d.quantile(0.5).unwrap(), Some(2.5));
272 assert!((d.quantile(0.9).unwrap().unwrap() - 3.7).abs() < 1e-15);
273 assert_eq!(d.quantile(1.0).unwrap(), Some(4.0));
274 let summary = d.summary();
275 assert_eq!((summary.min, summary.max), (Some(1.0), Some(4.0)));
276 assert_eq!(summary.p50, Some(2.5));
277 }
278
279 #[test]
280 fn a_share_is_bounded_by_the_runs_with_no_value() {
281 let d = Distribution::new(vec![1.0, 2.0, 3.0, 4.0], 6).unwrap();
283 let share = d.share_at_least(3.0).unwrap().unwrap();
284 assert_eq!((share.low, share.high), (2.0 / 6.0, 4.0 / 6.0));
285 let all = Distribution::new(vec![1.0, 2.0], 2).unwrap();
286 let share = all.share_at_least(1.0).unwrap().unwrap();
287 assert_eq!((share.low, share.high), (1.0, 1.0));
288 let share = all.share_at_least(f64::INFINITY).unwrap().unwrap();
289 assert_eq!((share.low, share.high), (0.0, 0.0));
290 let share = all.share_at_least(f64::NEG_INFINITY).unwrap().unwrap();
291 assert_eq!((share.low, share.high), (1.0, 1.0));
292 let none = Distribution::new(vec![], 0).unwrap();
294 assert_eq!(none.share_at_least(1.0).unwrap(), None);
295 assert!(matches!(
296 all.share_at_least(f64::NAN),
297 Err(AnalysisError::Domain {
298 what: "threshold of a share",
299 ..
300 })
301 ));
302 }
303
304 #[test]
305 fn empty_and_single_samples() {
306 let empty = Distribution::new(vec![], 3).unwrap();
307 assert_eq!(empty.mean(), None);
308 assert_eq!(empty.quantile(0.5).unwrap(), None);
309 assert_eq!(empty.missing(), 3);
310 let one = Distribution::new(vec![2.0], 1).unwrap();
311 assert_eq!(one.mean(), Some(2.0));
312 assert_eq!(one.standard_deviation(), None);
313 assert_eq!(one.quantile(0.3).unwrap(), Some(2.0));
314 }
315
316 #[test]
317 fn a_distribution_reads_back_through_its_checks() {
318 let d = Distribution::new(vec![3.0, 1.0, 2.0], 4).unwrap();
319 let json = serde_json::to_string(&d).unwrap();
320 assert_eq!(json, r#"{"attempted":4,"sorted":[1.0,2.0,3.0]}"#);
321 assert_eq!(serde_json::from_str::<Distribution>(&json).unwrap(), d);
322 let unsorted: Distribution =
324 serde_json::from_str(r#"{"attempted":3,"sorted":[3.0,1.0,2.0]}"#).unwrap();
325 assert_eq!(unsorted.sorted(), &[1.0, 2.0, 3.0]);
326 let error = serde_json::from_str::<Distribution>(r#"{"attempted":1,"sorted":[3.0,1.0]}"#)
327 .unwrap_err();
328 assert!(error.to_string().contains("more than 1"), "{error}");
329 }
330
331 #[test]
332 fn bad_inputs_are_refused() {
333 assert!(matches!(
334 Distribution::new(vec![1.0, f64::NAN], 2),
335 Err(AnalysisError::Domain { what: "value in a distribution", value }) if value.is_nan()
336 ));
337 assert!(matches!(
338 Distribution::new(vec![1.0, 2.0], 1),
339 Err(AnalysisError::Count {
340 count: 2,
341 limit: 1,
342 ..
343 })
344 ));
345 let d = Distribution::new(vec![1.0], 1).unwrap();
346 for p in [-0.1, 1.1, f64::NAN] {
347 assert!(matches!(
348 d.quantile(p),
349 Err(AnalysisError::Domain { what: "quantile probability", value })
350 if value.to_bits() == p.to_bits()
351 ));
352 }
353 }
354}