stats_claw/streaming/moments.rs
1//! Welford's online mean/variance accumulator.
2
3use super::count_to_f64;
4
5/// Welford's online accumulator for the running mean and variance of a stream.
6///
7/// Maintains the count, running mean, and the sum of squared deviations (`M2`) so
8/// that the mean and the Bessel-corrected sample variance are available after any
9/// number of updates without storing the values themselves. Numerically stable:
10/// it avoids the catastrophic cancellation of the naive "sum of squares minus
11/// square of sum" formula.
12///
13/// # Invariants
14///
15/// The struct holds exactly three scalar fields, so `size_of::<RunningMoments>()`
16/// is constant regardless of stream length — the bounded-memory guarantee.
17///
18/// # Examples
19///
20/// ```
21/// use stats_claw::streaming::RunningMoments;
22///
23/// let mut m = RunningMoments::new();
24/// for x in [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
25/// m.update(x);
26/// }
27/// // Mean of the eight values is 5.0.
28/// assert!((m.mean() - 5.0).abs() < 1e-12);
29/// ```
30#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct RunningMoments {
32 /// Number of values consumed so far.
33 count: u64,
34 /// Running arithmetic mean of the values consumed so far.
35 mean: f64,
36 /// Running sum of squared deviations from the current mean (Welford's `M2`).
37 m2: f64,
38}
39
40impl RunningMoments {
41 /// Creates an empty accumulator that has consumed no values.
42 ///
43 /// # Returns
44 ///
45 /// A `RunningMoments` with zero count; [`Self::mean`] is `0.0` until the first
46 /// [`Self::update`].
47 #[must_use]
48 pub const fn new() -> Self {
49 Self {
50 count: 0,
51 mean: 0.0,
52 m2: 0.0,
53 }
54 }
55
56 /// Folds one observation into the running summary using Welford's update.
57 ///
58 /// # Arguments
59 ///
60 /// * `x` — the next value of the stream. Any finite `f64`; `NaN`/`±∞`
61 /// propagate into the running statistics unchanged.
62 pub fn update(&mut self, x: f64) {
63 self.count += 1;
64 let n = count_to_f64(self.count);
65 let delta = x - self.mean;
66 self.mean += delta / n;
67 let delta2 = x - self.mean;
68 self.m2 = delta.mul_add(delta2, self.m2);
69 }
70
71 /// Returns the running arithmetic mean of all values consumed so far.
72 ///
73 /// # Returns
74 ///
75 /// The mean, or `0.0` if no values have been consumed yet.
76 #[must_use]
77 pub const fn mean(&self) -> f64 {
78 self.mean
79 }
80
81 /// Returns the running Bessel-corrected sample variance.
82 ///
83 /// # Returns
84 ///
85 /// The sample variance `M2 / (n - 1)`, or `0.0` when fewer than two values
86 /// have been consumed (variance is undefined for a single observation, and
87 /// `0.0` is the natural, panic-free convention here).
88 #[must_use]
89 pub fn variance(&self) -> f64 {
90 if self.count < 2 {
91 return 0.0;
92 }
93 self.m2 / count_to_f64(self.count - 1)
94 }
95
96 /// Returns the running sample standard deviation (the square root of
97 /// [`Self::variance`]).
98 #[must_use]
99 pub fn std_dev(&self) -> f64 {
100 self.variance().sqrt()
101 }
102
103 /// Returns the number of values consumed so far.
104 #[must_use]
105 pub const fn count(&self) -> u64 {
106 self.count
107 }
108}
109
110impl Default for RunningMoments {
111 /// Returns an empty accumulator, equivalent to [`RunningMoments::new`].
112 fn default() -> Self {
113 Self::new()
114 }
115}