use core::num::Saturating;
mod statistics_channel_sealed {
pub trait Sealed: Copy {}
}
pub trait StatisticsChannel: statistics_channel_sealed::Sealed + PartialOrd + Copy {
fn to_f64(self) -> f64;
fn is_nan(self) -> bool;
}
macro_rules! impl_integer_statistics_channel {
($($t:ty => |$value:ident| $widen:expr),+ $(,)?) => {
$(
impl statistics_channel_sealed::Sealed for $t {}
impl StatisticsChannel for $t {
#[inline(always)]
fn to_f64(self) -> f64 {
let $value = self;
$widen as f64
}
#[inline(always)]
fn is_nan(self) -> bool {
false
}
}
)+
};
}
impl_integer_statistics_channel! {
u8 => |v| v,
u16 => |v| v,
u32 => |v| v,
u64 => |v| v,
i8 => |v| v,
i16 => |v| v,
i32 => |v| v,
i64 => |v| v,
Saturating<u8> => |v| v.0,
Saturating<u16> => |v| v.0,
Saturating<u32> => |v| v.0,
Saturating<u64> => |v| v.0,
Saturating<i8> => |v| v.0,
Saturating<i16> => |v| v.0,
Saturating<i32> => |v| v.0,
Saturating<i64> => |v| v.0,
}
impl statistics_channel_sealed::Sealed for f32 {}
impl StatisticsChannel for f32 {
#[inline(always)]
fn to_f64(self) -> f64 {
f64::from(self)
}
#[inline(always)]
fn is_nan(self) -> bool {
f32::is_nan(self)
}
}
impl statistics_channel_sealed::Sealed for f64 {}
impl StatisticsChannel for f64 {
#[inline(always)]
fn to_f64(self) -> f64 {
self
}
#[inline(always)]
fn is_nan(self) -> bool {
f64::is_nan(self)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ChannelStatistics<C> {
pub count: u64,
pub nan_count: u64,
min: Option<C>,
max: Option<C>,
mean: f64,
sum_squared_deviations: f64,
}
impl<C: StatisticsChannel> ChannelStatistics<C> {
#[inline]
pub(crate) fn empty() -> Self {
Self {
count: 0,
nan_count: 0,
min: None,
max: None,
mean: 0.0,
sum_squared_deviations: 0.0,
}
}
#[inline]
pub(crate) fn push(&mut self, value: C) {
if value.is_nan() {
self.nan_count += 1;
return;
}
self.min = Some(match self.min {
Some(current) if current <= value => current,
_ => value,
});
self.max = Some(match self.max {
Some(current) if current >= value => current,
_ => value,
});
self.count += 1;
let sample = value.to_f64();
let delta = sample - self.mean;
self.mean += delta / self.count as f64;
self.sum_squared_deviations += delta * (sample - self.mean);
}
#[must_use]
pub fn min(&self) -> Option<C> {
self.min
}
#[must_use]
pub fn max(&self) -> Option<C> {
self.max
}
#[must_use]
pub fn mean(&self) -> Option<f64> {
(self.count > 0).then_some(self.mean)
}
#[must_use]
pub fn variance(&self) -> Option<f64> {
(self.count > 0).then(|| self.sum_squared_deviations / self.count as f64)
}
#[must_use]
pub fn std_dev(&self) -> Option<f64> {
self.variance().map(f64::sqrt)
}
#[must_use]
pub fn sample_variance(&self) -> Option<f64> {
(self.count > 1).then(|| self.sum_squared_deviations / (self.count - 1) as f64)
}
#[must_use]
pub fn sample_std_dev(&self) -> Option<f64> {
self.sample_variance().map(f64::sqrt)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fold<C: StatisticsChannel>(values: &[C]) -> ChannelStatistics<C> {
let mut stats = ChannelStatistics::empty();
for &value in values {
stats.push(value);
}
stats
}
#[test]
fn an_empty_summary_reports_absence_not_zero() {
let stats = fold::<f32>(&[]);
assert_eq!(stats.count, 0);
assert_eq!(stats.nan_count, 0);
assert_eq!(stats.min(), None);
assert_eq!(stats.max(), None);
assert_eq!(stats.mean(), None);
assert_eq!(stats.variance(), None);
assert_eq!(stats.std_dev(), None);
assert_eq!(stats.sample_variance(), None);
}
#[test]
fn a_single_sample_has_a_mean_and_zero_variance() {
let stats = fold(&[7.0f64]);
assert_eq!(stats.mean(), Some(7.0));
assert_eq!(stats.min(), Some(7.0));
assert_eq!(stats.max(), Some(7.0));
assert_eq!(stats.variance(), Some(0.0));
assert_eq!(stats.sample_variance(), None);
}
#[test]
fn the_two_variance_forms_differ_by_the_bessel_factor() {
let stats = fold(&[2.0f64, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
assert_eq!(stats.mean(), Some(5.0));
assert_eq!(stats.variance(), Some(4.0));
assert_eq!(stats.std_dev(), Some(2.0));
let sample = stats.sample_variance().unwrap();
assert!((sample - 32.0 / 7.0).abs() < 1e-12, "{sample}");
}
#[test]
fn nan_samples_are_counted_and_excluded() {
let stats = fold(&[1.0f32, f32::NAN, 3.0, f32::NAN]);
assert_eq!(stats.count, 2);
assert_eq!(stats.nan_count, 2);
assert_eq!(stats.min(), Some(1.0));
assert_eq!(stats.max(), Some(3.0));
assert_eq!(stats.mean(), Some(2.0));
assert_eq!(stats.variance(), Some(1.0));
}
#[test]
fn an_all_nan_channel_reports_absence() {
let stats = fold(&[f64::NAN, f64::NAN]);
assert_eq!(stats.count, 0);
assert_eq!(stats.nan_count, 2);
assert_eq!(stats.mean(), None);
assert_eq!(stats.min(), None);
}
#[test]
fn welford_survives_a_large_offset_that_defeats_the_textbook_form() {
let values: Vec<f64> = (0..1000).map(|i| 65_000.0 + (i % 3) as f64).collect();
let stats = fold(&values);
let expected_mean = 65_000.0 + (333 + 666) as f64 / 1000.0;
assert!((stats.mean().unwrap() - expected_mean).abs() < 1e-9);
let reference = values
.iter()
.map(|v| (v - expected_mean) * (v - expected_mean))
.sum::<f64>()
/ 1000.0;
let variance = stats.variance().unwrap();
assert!(
(variance - reference).abs() < 1e-9,
"{variance} vs {reference}"
);
assert!(
variance > 0.6,
"a real spread must not collapse: {variance}"
);
}
#[test]
fn integer_channels_have_no_nan_and_report_exact_extremes() {
let stats = fold(&[
Saturating(10u8),
Saturating(200u8),
Saturating(0u8),
Saturating(50u8),
]);
assert_eq!(stats.nan_count, 0);
assert_eq!(stats.min(), Some(Saturating(0u8)));
assert_eq!(stats.max(), Some(Saturating(200u8)));
assert_eq!(stats.mean(), Some(65.0));
}
#[test]
fn a_u64_extreme_survives_in_the_channel_type_where_the_mean_cannot() {
let big = (1u64 << 53) + 1;
let stats = fold(&[big, big - 2]);
assert_eq!(stats.max(), Some(big));
assert_eq!(stats.min(), Some(big - 2));
}
#[test]
fn signed_channels_are_summarised_too() {
let stats = fold(&[Saturating(-5i16), Saturating(15i16)]);
assert_eq!(stats.min(), Some(Saturating(-5i16)));
assert_eq!(stats.max(), Some(Saturating(15i16)));
assert_eq!(stats.mean(), Some(5.0));
assert_eq!(stats.variance(), Some(100.0));
}
}