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