wowlab_types/stats/summary/
streaming.rs1const MIN_SAMPLES_FOR_VARIANCE: u64 = 2;
2
3#[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 #[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 #[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}