#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use num_traits::Float;
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[allow(clippy::cast_precision_loss)]
pub struct OnlineStats {
pub count: u64,
pub mean: f64,
m2: f64,
}
impl OnlineStats {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[allow(clippy::cast_precision_loss)]
pub fn update(&mut self, value: f64) {
self.count += 1;
let delta = value - self.mean;
self.mean += delta / self.count as f64;
let delta2 = value - self.mean;
self.m2 += delta * delta2;
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn variance(&self) -> f64 {
if self.count < 2 {
return 0.0;
}
self.m2 / (self.count - 1) as f64
}
#[must_use]
pub fn std_dev(&self) -> f64 {
self.variance().sqrt()
}
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::cast_precision_loss)]
mod tests {
use super::*;
#[test]
fn new_and_default_match() {
let a = OnlineStats::new();
let b = OnlineStats::default();
assert_eq!(a.count, b.count);
assert_eq!(a.mean, b.mean);
assert_eq!(a.variance(), b.variance());
}
#[test]
fn empty_has_zero_variance() {
let s = OnlineStats::default();
assert_eq!(s.count, 0);
assert_eq!(s.mean, 0.0);
assert_eq!(s.variance(), 0.0);
assert_eq!(s.std_dev(), 0.0);
}
#[test]
fn single_sample_has_zero_variance() {
let mut s = OnlineStats::default();
s.update(42.0);
assert_eq!(s.count, 1);
assert!((s.mean - 42.0).abs() < f64::EPSILON);
assert_eq!(s.variance(), 0.0);
}
#[test]
fn three_sample_known_mean_and_std() {
let mut s = OnlineStats::default();
for v in [10.0, 20.0, 30.0] {
s.update(v);
}
assert_eq!(s.count, 3);
assert!((s.mean - 20.0).abs() < 1e-12);
assert!((s.variance() - 100.0).abs() < 1e-9);
assert!((s.std_dev() - 10.0).abs() < 1e-9);
}
#[test]
fn matches_two_pass_on_uniform_sequence() {
use alloc::vec::Vec;
let xs: Vec<f64> = (0..1000).map(|i| f64::from(i) * 0.5).collect();
let mut s = OnlineStats::default();
for &x in &xs {
s.update(x);
}
let n = xs.len() as f64;
let mean = xs.iter().sum::<f64>() / n;
let var = xs.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
assert!((s.mean - mean).abs() < 1e-9);
assert!((s.variance() - var).abs() < 1e-6);
}
#[test]
fn clone_preserves_state() {
let mut s = OnlineStats::default();
for v in [1.0, 2.0, 3.0, 4.0, 5.0] {
s.update(v);
}
let c = s.clone();
assert_eq!(c.count, s.count);
assert_eq!(c.mean, s.mean);
assert_eq!(c.variance(), s.variance());
}
#[cfg(all(feature = "serde", feature = "postcard"))]
#[test]
fn postcard_roundtrip_preserves_variance() {
let mut s = OnlineStats::default();
for v in [10.0, 20.0, 30.0] {
s.update(v);
}
let bytes = postcard::to_allocvec(&s).expect("serde ok");
let back: OnlineStats = postcard::from_bytes(&bytes).expect("serde ok");
assert_eq!(back.count, s.count);
assert!((back.mean - s.mean).abs() < f64::EPSILON);
assert!((back.variance() - s.variance()).abs() < f64::EPSILON);
}
}