rill_ml/stats/
variance.rs1use crate::error::{RillError, checked_increment, ensure_finite};
8#[cfg(feature = "serde")]
9use crate::persistence::ValidateState;
10use crate::traits::OnlineStatistic;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub enum VarianceKind {
16 Population,
18 #[default]
20 Sample,
21}
22
23impl VarianceKind {
24 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#[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 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 pub fn value(&self) -> Option<f64> {
83 self.kind
84 .denominator(self.count)
85 .map(|denom| self.m2 / denom as f64)
86 }
87
88 pub fn std_dev(&self) -> Option<f64> {
90 self.value().map(|v| v.sqrt())
91 }
92
93 pub const fn mean(&self) -> f64 {
95 self.mean
96 }
97
98 pub const fn count(&self) -> u64 {
100 self.count
101 }
102
103 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 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}