use std::ops::Add; use std::ops::AddAssign; use std::ops::Div; use uom::num_traits; use uom::si::f64::Ratio; use uom::si::ratio::ratio; use super::{byte, second, Information, Time}; /// 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") } /// A mean function that assumes at least one element pub fn mean(values: impl IntoIterator) -> Q where Q: AddAssign + num_traits::Zero + Div, P: Into, { let mut sum = Q::zero(); let mut count = 0; for value in values { sum += value; count += 1; } let count = Ratio::new::(count as f64); (sum / count).into() } 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_by(|a, b| a.partial_cmp(b).expect("No NaN values")); let len = values.len(); if len % 2 == 0 { let mid = len / 2; let a = &values[mid - 1]; let b = &values[mid]; ((*a + *b) / 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) } } impl UnsafeRawValue for Ratio { fn unsafe_raw_value(&self) -> f64 { self.get::() } fn unsafe_from_raw_value(value: f64) -> Self { Ratio::new::(value) } } fn standard_deviation_f64(values: &[f64]) -> f64 { let mean_value = mean(values.iter().copied()); let mut squared_deviations = 0.; let mut n = 0; for value in values { let deviation = value - mean_value; squared_deviations += deviation * deviation; n += 1; } (1. / ((n - 1) as f64) * squared_deviations).sqrt() } pub fn standard_deviation(values: impl IntoIterator + Clone) -> Q { let values: Vec<_> = values.into_iter().map(|q| q.unsafe_raw_value()).collect(); let result = standard_deviation_f64(&values); Q::unsafe_from_raw_value(result) } /// 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() } #[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); assert_relative_eq!(result.get::(), 100.0); } }