Adopt the remaining statistics changes from #790

This commit is contained in:
David Peter committed 2026-10-05 13:57:31 +02:00
1 parent efd57b3a89
commit 8084a9eb7f
2 files changed
+35 -33

No files matched your search

+5 -4
View File
@@ -54,13 +54,14 @@ impl Measurements {
mean(self.wall_clock_times()) mean(self.wall_clock_times())
} }
/// The standard deviation of wall clock times, if at least two runs were measured. /// The standard deviation of all wall clock times. Not available if only one run has been performed
pub fn stddev(&self) -> Option<Time> { pub fn stddev(&self) -> Option<Time> {
if self.len() < 2 { let times: Vec<_> = self.wall_clock_times().collect(); // TODO: Avoid collecting
if times.len() < 2 {
None None
} else { } else {
let times: Vec<_> = self.wall_clock_times().collect(); Some(standard_deviation(times))
Some(standard_deviation(&times, self.time_wall_clock_mean()))
} }
} }
+30 -29
View File
@@ -1,8 +1,12 @@
//! Statistics for non-empty samples of finite measurements. use std::ops::Add;
use std::ops::AddAssign;
use std::ops::Div;
use std::ops::{Add, AddAssign, Div}; use uom::num_traits;
use uom::si::f64::Ratio;
use uom::si::ratio::ratio;
use crate::quantity::{byte, ratio, second, Information, Ratio, Time, Zero}; use super::{byte, second, Information, Time};
/// A min function that assumes no NaNs and at least one element /// A min function that assumes no NaNs and at least one element
pub fn min<Q: PartialOrd>(values: impl IntoIterator<Item = Q>) -> Q { pub fn min<Q: PartialOrd>(values: impl IntoIterator<Item = Q>) -> Q {
@@ -20,14 +24,14 @@ pub fn max<Q: PartialOrd>(values: impl IntoIterator<Item = Q>) -> Q {
.expect("'max' requires at least one element") .expect("'max' requires at least one element")
} }
/// Arithmetic mean, summing measurements in their original order. /// A mean function that assumes at least one element
pub fn mean<Q, P>(values: impl IntoIterator<Item = Q>) -> Q pub fn mean<Q, P>(values: impl IntoIterator<Item = Q>) -> Q
where where
Q: AddAssign + Zero + Div<Ratio, Output = P>, Q: AddAssign + num_traits::Zero + Div<Ratio, Output = P>,
P: Into<Q>, P: Into<Q>,
{ {
let mut sum = Q::zero(); let mut sum = Q::zero();
let mut count = 0usize; let mut count = 0;
for value in values { for value in values {
sum += value; sum += value;
count += 1; count += 1;
@@ -37,25 +41,20 @@ where
(sum / count).into() (sum / count).into()
} }
/// Median without modifying the input sample.
pub fn median<Q, P>(values: impl IntoIterator<Item = Q>) -> Q pub fn median<Q, P>(values: impl IntoIterator<Item = Q>) -> Q
where where
Q: Copy + PartialOrd + Add<Output = Q> + Div<Ratio, Output = P>, Q: Copy + PartialOrd + Add<Output = Q> + Div<Ratio, Output = P>,
P: Into<Q>, P: Into<Q>,
{ {
let mut values = values.into_iter().collect::<Vec<_>>(); let mut values = values.into_iter().collect::<Vec<_>>();
assert!( values.sort_by(|a, b| a.partial_cmp(b).expect("No NaN values"));
!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(); let len = values.len();
if len % 2 == 0 { if len % 2 == 0 {
let mid = len / 2; let mid = len / 2;
let a = &values[mid - 1]; let a = &values[mid - 1];
let b = &values[mid]; let b = &values[mid];
((*b + *a) / Ratio::new::<ratio>(2.)).into() ((*a + *b) / Ratio::new::<ratio>(2.)).into()
} else { } else {
values[len / 2] values[len / 2]
} }
@@ -88,22 +87,24 @@ impl UnsafeRawValue for Information {
} }
} }
/// Sample standard deviation, using an already computed mean. fn standard_deviation_f64(values: &[f64]) -> f64 {
pub fn standard_deviation<Q: UnsafeRawValue>(values: &[Q], mean: Q) -> Q { let mean_value = mean(values.iter().copied());
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 let mut squared_deviations = 0.;
// before taking the square root also preserves exported floating-point values. let mut n = 0;
Q::unsafe_from_raw_value((sum / (values.len() as f64 - 1.0)).sqrt()) 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<Q: UnsafeRawValue>(values: impl IntoIterator<Item = Q> + 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 /// Compute modified Z-scores for a given sample. A (unmodified) Z-score is defined by
@@ -219,7 +220,7 @@ mod tests {
Time::new::<millisecond>(200.0), Time::new::<millisecond>(200.0),
Time::new::<microsecond>(300_000.0), Time::new::<microsecond>(300_000.0),
]; ];
let result = standard_deviation(&values, mean(values)); let result = standard_deviation(values);
assert_relative_eq!(result.get::<millisecond>(), 100.0); assert_relative_eq!(result.get::<millisecond>(), 100.0);
} }
} }