//! Statistics for non-empty samples of finite measurements. use std::ops::{Add, AddAssign, Div}; use crate::quantity::{byte, ratio, second, Information, Ratio, Time, Zero}; /// A min function that assumes no NaNs and at least one element pub fn min(values: impl IntoIterator) -> Q { values .into_iter() .min_by(|a, b| a.partial_cmp(b).expect("No NaN values")) .expect("'min' requires at least one element") } /// A max function that assumes no NaNs and at least one element pub fn max(values: impl IntoIterator) -> Q { values .into_iter() .max_by(|a, b| a.partial_cmp(b).expect("No NaN values")) .expect("'max' requires at least one element") } /// Arithmetic mean, summing measurements in their original order. pub fn mean(values: impl IntoIterator) -> Q where Q: AddAssign + Zero + Div, P: Into, { let mut sum = Q::zero(); let mut count = 0usize; for value in values { sum += value; count += 1; } let count = Ratio::new::(count as f64); (sum / count).into() } /// Median without modifying the input sample. pub fn median(values: impl IntoIterator) -> Q where Q: Copy + PartialOrd + Add + Div, P: Into, { let mut values = values.into_iter().collect::>(); assert!( !values.is_empty(), "median requires at least one measurement" ); values.sort_unstable_by(|a, b| a.partial_cmp(b).expect("No NaN measurements")); let len = values.len(); if len % 2 == 0 { let mid = len / 2; let a = &values[mid - 1]; let b = &values[mid]; ((*b + *a) / Ratio::new::(2.)).into() } else { values[len / 2] } } /// This is just sad, but after several hours trying to figure out how to write /// a generic `standard_deviation` function using `uom` quantities, I gave up. pub trait UnsafeRawValue { fn unsafe_raw_value(&self) -> f64; fn unsafe_from_raw_value(value: f64) -> Self; } impl UnsafeRawValue for Time { fn unsafe_raw_value(&self) -> f64 { self.get::() } fn unsafe_from_raw_value(value: f64) -> Self { Time::new::(value) } } impl UnsafeRawValue for Information { fn unsafe_raw_value(&self) -> f64 { self.get::() } fn unsafe_from_raw_value(value: f64) -> Self { Information::new::(value) } } /// Sample standard deviation, using an already computed mean. pub fn standard_deviation(values: &[Q], mean: Q) -> Q { let mean = mean.unsafe_raw_value(); assert!( values.len() > 1, "standard deviation requires at least two measurements" ); let sum = values .iter() .map(|value| (value.unsafe_raw_value() - mean) * (value.unsafe_raw_value() - mean)) .fold(0.0, |sum, deviation| sum + deviation); assert!(sum >= 0.0, "invalid sum of squared deviations"); // Preserve the operation order of statistical 1.0: dividing by n - 1 // before taking the square root also preserves exported floating-point values. Q::unsafe_from_raw_value((sum / (values.len() as f64 - 1.0)).sqrt()) } /// Compute modified Z-scores for a given sample. A (unmodified) Z-score is defined by /// `(x_i - x_mean)/x_stddev` whereas the modified Z-score is defined by `(x_i - x_median)/MAD` /// where MAD is the median absolute deviation. /// /// References: /// - pub fn modified_zscores_f64(xs: &[f64]) -> Vec { assert!(!xs.is_empty()); // Compute sample median: let x_median = median(xs.iter().copied()); // Compute the absolute deviations from the median: let deviations: Vec = xs.iter().map(|x| (x - x_median).abs()).collect(); // Compute median absolute deviation: let mad = median(deviations.iter().copied()); // Handle MAD == 0 case let mad = if mad > 0.0 { mad } else { f64::EPSILON }; // Compute modified Z-scores (x_i - x_median) / MAD xs.iter().map(|&x| (x - x_median) / mad).collect() } pub fn modified_zscores(values: &[Q]) -> Vec { let values: Vec<_> = values.iter().map(|q| q.unsafe_raw_value()).collect(); modified_zscores_f64(&values) } #[cfg(test)] mod tests { use super::*; use approx::assert_relative_eq; use uom::si::information::kibibyte; use uom::si::time::{microsecond, millisecond}; #[test] fn test_min() { assert_eq!(1.0, min([1.0])); assert_eq!(-1.0, min([-1.0])); assert_eq!(-2.0, min([-2.0, -1.0])); assert_eq!(-1.0, min([-1.0, 1.0])); assert_eq!(-1.0, min([1.0, -1.0, 0.0])); let values = vec![ Information::new::(1.0), Information::new::(2.0), Information::new::(3.0), ]; assert_eq!(min(&values).get::(), 2.0); } #[test] fn test_max() { assert_eq!(1.0, max([1.0])); assert_eq!(-1.0, max([-1.0])); assert_eq!(-1.0, max([-2.0, -1.0])); assert_eq!(1.0, max([-1.0, 1.0])); assert_eq!(1.0, max([-1.0, 1.0, 0.0])); let values = vec![ Information::new::(1.0), Information::new::(2.0), Information::new::(3.0), ]; assert_eq!(max(&values).get::(), 2.0); } #[test] fn test_mean() { assert_eq!(1.0, mean([1.0])); assert_relative_eq!(2.0, mean([1.0, 3.0])); let values = [ Time::new::(100.0), Time::new::(200.0), Time::new::(600_000.0), ]; let result = mean(values); assert_relative_eq!(result.get::(), 300.0); } #[test] fn test_median() { assert_eq!(1.0, median([1.0])); assert_relative_eq!(2.0, median([1.0, 3.0])); let values = [ Time::new::(100.0), Time::new::(200.0), Time::new::(600_000.0), ]; let result = median(values); assert_relative_eq!(result.get::(), 200.0); let values = [ Time::new::(100.0), Time::new::(200.0), Time::new::(300_000.0), Time::new::(600_000.0), ]; let result = median(values); assert_relative_eq!(result.get::(), 250.0); } #[test] fn test_standard_deviation() { let values = [ Time::new::(100.0), Time::new::(200.0), Time::new::(300_000.0), ]; let result = standard_deviation(&values, mean(values)); assert_relative_eq!(result.get::(), 100.0); } }