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