Skip to main content

wowlab_types/stats/summary/
streaming.rs

1const MIN_SAMPLES_FOR_VARIANCE: u64 = 2;
2
3/// Streaming mean/variance via Welford's online algorithm (O(1) memory).
4#[derive(Clone, Debug, Default)]
5pub struct Streaming {
6    n: u64,
7    mean: f64,
8    m2: f64,
9    min: f64,
10    max: f64,
11}
12
13impl Streaming {
14    #[must_use]
15    pub fn new() -> Self {
16        Self {
17            n: 0,
18            mean: 0.0,
19            m2: 0.0,
20            min: f64::INFINITY,
21            max: f64::NEG_INFINITY,
22        }
23    }
24
25    #[inline]
26    #[expect(
27        clippy::cast_precision_loss,
28        reason = "Welford updates require the integer sample count in the f64 calculation domain"
29    )]
30    pub fn push(&mut self, x: f64) {
31        self.n += 1;
32        let delta = x - self.mean;
33
34        self.mean += delta / self.n as f64;
35        let delta2 = x - self.mean;
36
37        self.m2 += delta * delta2;
38        self.min = self.min.min(x);
39        self.max = self.max.max(x);
40    }
41
42    pub fn push_all(&mut self, values: impl IntoIterator<Item = f64>) {
43        for x in values {
44            self.push(x);
45        }
46    }
47
48    #[expect(
49        clippy::cast_precision_loss,
50        reason = "Welford merging requires integer sample counts in the f64 calculation domain"
51    )]
52    pub fn merge(&mut self, other: &Streaming) {
53        if other.n == 0 {
54            return;
55        }
56
57        if self.n == 0 {
58            *self = other.clone();
59
60            return;
61        }
62
63        let n = self.n + other.n;
64        let delta = other.mean - self.mean;
65        let mean = self.mean + delta * other.n as f64 / n as f64;
66        let m2 = self.m2 + other.m2 + delta * delta * (self.n as f64 * other.n as f64 / n as f64);
67
68        self.n = n;
69        self.mean = mean;
70        self.m2 = m2;
71        self.min = self.min.min(other.min);
72        self.max = self.max.max(other.max);
73    }
74
75    #[inline]
76    #[must_use]
77    pub fn count(&self) -> u64 {
78        self.n
79    }
80
81    #[inline]
82    #[must_use]
83    pub fn mean(&self) -> f64 {
84        if self.n == 0 { f64::NAN } else { self.mean }
85    }
86
87    /// Sample variance (Bessel's correction: n-1 denominator).
88    #[inline]
89    #[must_use]
90    #[expect(
91        clippy::cast_precision_loss,
92        reason = "sample variance requires the integer sample count as an f64 divisor"
93    )]
94    pub fn variance(&self) -> f64 {
95        if self.n < MIN_SAMPLES_FOR_VARIANCE {
96            f64::NAN
97        } else {
98            self.m2 / (self.n - 1) as f64
99        }
100    }
101
102    #[inline]
103    #[must_use]
104    #[expect(
105        clippy::cast_precision_loss,
106        reason = "population variance requires the integer sample count as an f64 divisor"
107    )]
108    pub fn variance_pop(&self) -> f64 {
109        if self.n == 0 {
110            f64::NAN
111        } else {
112            self.m2 / self.n as f64
113        }
114    }
115
116    #[inline]
117    #[must_use]
118    pub fn std_dev(&self) -> f64 {
119        self.variance().sqrt()
120    }
121
122    #[inline]
123    #[must_use]
124    pub fn std_dev_pop(&self) -> f64 {
125        self.variance_pop().sqrt()
126    }
127
128    #[inline]
129    #[must_use]
130    pub fn min(&self) -> f64 {
131        if self.n == 0 { f64::NAN } else { self.min }
132    }
133
134    #[inline]
135    #[must_use]
136    pub fn max(&self) -> f64 {
137        if self.n == 0 { f64::NAN } else { self.max }
138    }
139
140    /// Coefficient of variation.
141    #[inline]
142    #[must_use]
143    pub fn cv(&self) -> f64 {
144        let mean = self.mean();
145
146        if mean.abs() < f64::EPSILON {
147            f64::NAN
148        } else {
149            self.std_dev() / mean
150        }
151    }
152}
153
154impl FromIterator<f64> for Streaming {
155    fn from_iter<I>(iter: I) -> Self
156    where
157        I: IntoIterator<Item = f64>,
158    {
159        let mut s = Streaming::new();
160
161        s.extend(iter);
162
163        s
164    }
165}
166
167impl Extend<f64> for Streaming {
168    fn extend<I>(&mut self, iter: I)
169    where
170        I: IntoIterator<Item = f64>,
171    {
172        self.push_all(iter);
173    }
174}