Skip to main content

rill_ml/stats/
variance.rs

1//! Online variance using Welford's algorithm.
2//!
3//! Time complexity per update: `O(1)`. Space complexity: `O(1)`.
4//!
5//! See <https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm>.
6
7use crate::error::{RillError, checked_increment, ensure_finite};
8#[cfg(feature = "serde")]
9use crate::persistence::ValidateState;
10use crate::traits::OnlineStatistic;
11
12/// Whether to compute population or sample variance.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub enum VarianceKind {
16    /// Divide by `n`. This is the population (biased) variance.
17    Population,
18    /// Divide by `n - 1`. This is the sample (Bessel-corrected) variance.
19    #[default]
20    Sample,
21}
22
23impl VarianceKind {
24    /// Returns the denominator for the given number of observations.
25    fn denominator(self, n: u64) -> Option<u64> {
26        match self {
27            VarianceKind::Population => {
28                if n == 0 {
29                    None
30                } else {
31                    Some(n)
32                }
33            }
34            VarianceKind::Sample => {
35                if n < 2 {
36                    None
37                } else {
38                    Some(n - 1)
39                }
40            }
41        }
42    }
43}
44
45/// Online variance accumulator using Welford's algorithm.
46///
47/// Also exposes the running mean and population/sample standard deviation.
48///
49/// # Examples
50///
51/// ```
52/// use rill_ml::stats::{Variance, VarianceKind};
53/// use rill_ml::OnlineStatistic;
54///
55/// let mut v = Variance::new(VarianceKind::Population);
56/// for x in [1.0, 2.0, 3.0, 4.0, 5.0] {
57///     v.update(x).unwrap();
58/// }
59/// assert!((v.value().unwrap() - 2.0).abs() < 1e-12);
60/// ```
61#[derive(Debug, Clone)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
63pub struct Variance {
64    count: u64,
65    mean: f64,
66    m2: f64,
67    kind: VarianceKind,
68}
69
70impl Variance {
71    /// Create a new variance accumulator of the given kind.
72    pub const fn new(kind: VarianceKind) -> Self {
73        Self {
74            count: 0,
75            mean: 0.0,
76            m2: 0.0,
77            kind,
78        }
79    }
80
81    /// Current variance, or `None` when not enough data has been observed.
82    pub fn value(&self) -> Option<f64> {
83        self.kind
84            .denominator(self.count)
85            .map(|denom| self.m2 / denom as f64)
86    }
87
88    /// Current standard deviation, or `None` when not enough data has been observed.
89    pub fn std_dev(&self) -> Option<f64> {
90        self.value().map(|v| v.sqrt())
91    }
92
93    /// Current running mean.
94    pub const fn mean(&self) -> f64 {
95        self.mean
96    }
97
98    /// Number of observations seen so far.
99    pub const fn count(&self) -> u64 {
100        self.count
101    }
102
103    /// The configured variance kind.
104    pub const fn kind(&self) -> VarianceKind {
105        self.kind
106    }
107}
108
109#[cfg(feature = "serde")]
110impl ValidateState for Variance {
111    fn validate_state(&self) -> Result<(), RillError> {
112        ensure_finite("variance mean", self.mean)?;
113        ensure_finite("variance m2", self.m2)?;
114        if self.m2 < 0.0 {
115            return Err(RillError::InvalidState(format!(
116                "variance m2 must be non-negative, got {}",
117                self.m2
118            )));
119        }
120        Ok(())
121    }
122}
123
124impl OnlineStatistic for Variance {
125    fn update(&mut self, value: f64) -> Result<(), RillError> {
126        ensure_finite("value", value)?;
127        let next_count = checked_increment(self.count, "variance sample")?;
128        let n = next_count as f64;
129        let delta = value - self.mean;
130        ensure_finite("variance delta", delta)?;
131        let next_mean = self.mean + delta / n;
132        ensure_finite("variance mean", next_mean)?;
133        let delta2 = value - next_mean;
134        ensure_finite("variance delta", delta2)?;
135        let next_m2 = self.m2 + delta * delta2;
136        ensure_finite("variance M2", next_m2)?;
137
138        self.count = next_count;
139        self.mean = next_mean;
140        self.m2 = next_m2;
141        Ok(())
142    }
143
144    fn samples_seen(&self) -> u64 {
145        self.count
146    }
147
148    fn reset(&mut self) {
149        self.count = 0;
150        self.mean = 0.0;
151        self.m2 = 0.0;
152    }
153}
154
155impl Default for Variance {
156    fn default() -> Self {
157        Self::new(VarianceKind::Sample)
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use rand::SeedableRng;
165
166    #[test]
167    fn population_variance_of_simple_sequence() {
168        let mut v = Variance::new(VarianceKind::Population);
169        for x in [1.0, 2.0, 3.0, 4.0, 5.0] {
170            v.update(x).unwrap();
171        }
172        assert!((v.value().unwrap() - 2.0).abs() < 1e-12);
173        assert!((v.std_dev().unwrap() - 2.0_f64.sqrt()).abs() < 1e-12);
174        assert!((v.mean() - 3.0).abs() < 1e-12);
175    }
176
177    #[test]
178    fn sample_variance_of_simple_sequence() {
179        let mut v = Variance::new(VarianceKind::Sample);
180        for x in [1.0, 2.0, 3.0, 4.0, 5.0] {
181            v.update(x).unwrap();
182        }
183        // sample variance = 10 / 4 = 2.5
184        assert!((v.value().unwrap() - 2.5).abs() < 1e-12);
185    }
186
187    #[test]
188    fn variance_insufficient_data_returns_none() {
189        let pop = Variance::new(VarianceKind::Population);
190        assert!(pop.value().is_none());
191
192        let mut sample = Variance::new(VarianceKind::Sample);
193        sample.update(5.0).unwrap();
194        assert!(sample.value().is_none());
195    }
196
197    #[test]
198    fn variance_constant_sequence_is_zero() {
199        let mut v = Variance::new(VarianceKind::Population);
200        for _ in 0..100 {
201            v.update(7.0).unwrap();
202        }
203        assert_eq!(v.value().unwrap(), 0.0);
204    }
205
206    #[test]
207    fn variance_rejects_non_finite() {
208        let mut v = Variance::new(VarianceKind::Population);
209        assert!(v.update(f64::NAN).is_err());
210        assert_eq!(v.count(), 0);
211    }
212
213    #[test]
214    fn variance_rejects_overflow_without_mutating_state() {
215        let mut v = Variance::new(VarianceKind::Population);
216        v.update(f64::MAX).unwrap();
217        let before = v.clone();
218        assert!(v.update(-f64::MAX).is_err());
219        assert_eq!(v.count(), before.count());
220        assert_eq!(v.mean(), before.mean());
221        assert_eq!(v.value(), before.value());
222    }
223
224    #[test]
225    fn variance_matches_batch_formula() {
226        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(99);
227        let mut v = Variance::new(VarianceKind::Population);
228        let mut data = Vec::new();
229        for _ in 0..2000 {
230            let x = rand::Rng::gen_range(&mut rng, -50.0..50.0);
231            v.update(x).unwrap();
232            data.push(x);
233        }
234        let mean = data.iter().sum::<f64>() / data.len() as f64;
235        let pop_var = data.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / data.len() as f64;
236        assert!(
237            (v.value().unwrap() - pop_var).abs() < 1e-6,
238            "online vs batch variance mismatch"
239        );
240    }
241}