use std::{
collections::VecDeque,
num::NonZeroUsize,
};
use getset::CopyGetters;
use num::Float;
use crate::View;
#[derive(Debug, Clone, CopyGetters)]
pub struct WelfordOnline<T: Float, V> {
view: V,
#[getset(get_copy = "pub")]
window_len: NonZeroUsize,
q_vals: VecDeque<T>,
#[getset(get_copy = "pub")]
mean: T,
m2: T,
count: usize,
}
impl<T, V> WelfordOnline<T, V>
where
V: View<T>,
T: Float,
{
pub fn new(view: V, window_len: NonZeroUsize) -> Self {
Self {
view,
window_len,
q_vals: VecDeque::with_capacity(window_len.get()),
mean: T::zero(),
m2: T::zero(),
count: 0,
}
}
#[inline]
fn update_stats_add(&mut self, x: T) {
let delta = x - self.mean;
self.mean = self.mean + (delta / T::from(self.count + 1).unwrap());
self.m2 = self.m2 + (delta * (x - self.mean));
self.count += 1;
}
#[inline]
fn update_stats_remove(&mut self, old_value: T) {
let n = T::from(self.count).unwrap();
let sum = self.mean * n;
let sum_sq = self.m2 + sum * self.mean;
let sum_new = sum - old_value;
let sum_sq_new = sum_sq - old_value * old_value;
self.count -= 1;
if self.count > 0 {
let n_new = T::from(self.count).unwrap();
self.mean = sum_new / n_new;
self.m2 = sum_sq_new - n_new * self.mean * self.mean;
if self.m2 < T::zero() {
self.m2 = T::zero();
}
} else {
self.mean = T::zero();
self.m2 = T::zero();
}
}
#[inline]
pub fn variance(&self) -> T {
if self.count > 1 {
self.m2 / T::from(self.count - 1).expect("can convert")
} else {
T::zero()
}
}
}
impl<T, V> View<T> for WelfordOnline<T, V>
where
V: View<T>,
T: Float,
{
fn update(&mut self, val: T) {
debug_assert!(val.is_finite(), "value must be finite");
self.view.update(val);
let Some(val) = self.view.last() else { return };
debug_assert!(val.is_finite(), "value must be finite");
self.q_vals.push_back(val);
if self.q_vals.len() > self.window_len.get() {
let old_val = self.q_vals.pop_front().unwrap();
self.update_stats_remove(old_val);
}
self.update_stats_add(val);
}
#[inline]
fn last(&self) -> Option<T> {
if self.count < self.window_len.get() - 1 {
return None;
}
let var = self.variance();
if var <= T::zero() {
return Some(T::zero());
}
debug_assert!(var >= T::zero(), "Variance must be positive");
let out = var.sqrt();
debug_assert!(out.is_finite(), "value must be finite");
Some(out)
}
}
#[cfg(test)]
mod tests {
use round::round;
use super::*;
use crate::{
plot::plot_values,
pure_functions::Echo,
test_data::TEST_DATA,
};
#[test]
fn welford_online() {
let mut wo = WelfordOnline::new(Echo::new(), NonZeroUsize::new(TEST_DATA.len()).unwrap());
for v in &TEST_DATA {
wo.update(*v);
if let Some(val) = wo.last() {
assert!(!val.is_nan());
}
}
let w_std_dev = wo.last().expect("Is some");
let avg: f64 = TEST_DATA.iter().sum::<f64>() / TEST_DATA.len() as f64;
let std_dev: f64 = ((1.0 / (TEST_DATA.len() as f64 - 1.0))
* TEST_DATA.iter().map(|v| (v - avg).powi(2)).sum::<f64>())
.sqrt();
assert_eq!(round(w_std_dev, 4), round(std_dev, 4));
}
#[test]
fn welford_online_sliding_matches_direct_computation() {
let mut wo = WelfordOnline::new(Echo::new(), NonZeroUsize::new(3).unwrap());
let all: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let direct_sd = |vs: &[f64]| -> f64 {
let n = vs.len() as f64;
let m = vs.iter().sum::<f64>() / n;
let var = vs.iter().map(|v| (v - m).powi(2)).sum::<f64>() / (n - 1.0);
var.sqrt()
};
for i in 0..all.len() {
wo.update(all[i]);
let start = if i < 2 { 0 } else { i - 2 }; let window = &all[start..=i];
let expected = direct_sd(&window);
if let Some(got) = wo.last() {
let diff = (got - expected).abs();
if diff > 1e-12 {
panic!(
"step {} window {:?}: expected sd={}, got={}, diff={}",
i, window, expected, got, diff
);
}
} else {
assert!(
i < 2,
"step {}: got None but window is full ({:?})",
i,
window
);
}
}
}
#[test]
fn welford_online_plot() {
let mut wo = WelfordOnline::new(Echo::new(), NonZeroUsize::new(16).unwrap());
let mut out: Vec<f64> = Vec::new();
for v in &TEST_DATA {
wo.update(*v);
if let Some(val) = wo.last() {
out.push(val);
}
}
let filename = "img/welford_online_sliding.png";
plot_values(out, filename).unwrap();
}
}