irox_stats/
streaming.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2025 IROX Contributors
3//
4
5//!
6//! Streaming Statistics
7//!
8
9use core::fmt::{Debug, Formatter};
10use core::ops::{Add, Div, Mul, Sub};
11use irox_tools::f64::FloatExt;
12
13///
14/// A statistic type that can be incrementally calculated after each sample is added.  Does not
15/// necessarily have to have the full set of data available to it, and as such, should be fast.
16pub trait StreamingStatistic {
17    type Type;
18
19    /// adds a sample to this streaming statistic, returning a result of the operation.
20    fn add_sample(&mut self, sample: Self::Type) -> Self::Type;
21    /// Returns a copy of the last sample added
22    fn get_last_sample(&self) -> Self::Type;
23    /// Returns the last value returned by 'add_sample'
24    fn get_last_result(&self) -> Self::Type;
25    /// Returns the total number of samples pushed in
26    fn get_num_samples(&self) -> u64;
27}
28
29///
30/// Standard rolling average/mean of all the samples pushed into it.
31#[derive(Default, Debug, Clone, Copy)]
32pub struct Mean<Type> {
33    sample_count: u64,
34    last_sample: Option<Type>,
35    last_mean: Option<Type>,
36}
37
38impl<T> Mean<T>
39where
40    T: Copy,
41{
42    pub fn get_mean(&self) -> Option<T> {
43        self.last_mean
44    }
45}
46
47///
48/// Standard rolling average/mean of all the f32 samples pushed into it.
49pub type MeanF32 = Mean<f32>;
50///
51/// Standard rolling average/mean of all the f64 samples pushed into it.
52pub type MeanF64 = Mean<f64>;
53
54impl<Type> StreamingStatistic for Mean<Type>
55where
56    Type: Sub<Type, Output = Type>
57        + Copy
58        + Div<f64, Output = Type>
59        + Add<Type, Output = Type>
60        + Default,
61{
62    type Type = Type;
63
64    fn add_sample(&mut self, sample: Self::Type) -> Self::Type {
65        self.sample_count += 1;
66        self.last_sample = Some(sample);
67        let last = self.last_mean.get_or_insert(sample);
68        let mean = *last + (sample - *last) / self.sample_count as f64;
69        *last = mean;
70        mean
71    }
72
73    fn get_last_sample(&self) -> Self::Type {
74        self.last_sample.unwrap_or_default()
75    }
76
77    fn get_last_result(&self) -> Self::Type {
78        self.last_mean.unwrap_or_default()
79    }
80
81    fn get_num_samples(&self) -> u64 {
82        self.sample_count
83    }
84}
85
86/// Streaming maximum function
87#[derive(Default, Debug, Clone, Copy)]
88pub struct Max<T> {
89    max_val: Option<T>,
90    last_sample: Option<T>,
91    num_samples: u64,
92}
93impl<T: Copy> Max<T> {
94    pub fn get_max_val(&self) -> Option<T> {
95        self.max_val
96    }
97}
98
99impl<T: Default> StreamingStatistic for Max<T>
100where
101    T: PartialOrd + Copy,
102{
103    type Type = T;
104
105    fn add_sample(&mut self, sample: Self::Type) -> Self::Type {
106        self.num_samples += 1;
107        let mut max = self.max_val.get_or_insert(sample);
108        if sample.ge(max) {
109            max = self.max_val.insert(sample);
110        }
111        *max
112    }
113
114    fn get_last_sample(&self) -> Self::Type {
115        self.last_sample.unwrap_or_default()
116    }
117
118    fn get_last_result(&self) -> Self::Type {
119        self.max_val.unwrap_or_default()
120    }
121
122    fn get_num_samples(&self) -> u64 {
123        self.num_samples
124    }
125}
126
127/// Streaming minimum function
128#[derive(Default, Debug, Clone, Copy)]
129pub struct Min<T> {
130    min_val: Option<T>,
131    last_sample: Option<T>,
132    num_samples: u64,
133}
134
135impl<T: Copy> Min<T> {
136    pub fn get_min_val(&self) -> Option<T> {
137        self.min_val
138    }
139}
140
141impl<T: Default> StreamingStatistic for Min<T>
142where
143    T: PartialOrd + Copy,
144{
145    type Type = T;
146
147    fn add_sample(&mut self, sample: Self::Type) -> Self::Type {
148        self.num_samples += 1;
149        let mut min = self.min_val.get_or_insert(sample);
150        if sample.le(min) {
151            min = self.min_val.insert(sample);
152        }
153        *min
154    }
155
156    fn get_last_sample(&self) -> Self::Type {
157        self.last_sample.unwrap_or_default()
158    }
159
160    fn get_last_result(&self) -> Self::Type {
161        self.min_val.unwrap_or_default()
162    }
163
164    fn get_num_samples(&self) -> u64 {
165        self.num_samples
166    }
167}
168
169///
170/// Unweighted sum of squares / [Total Sum of Squares](https://en.wikipedia.org/wiki/Total_sum_of_squares) / SST
171/// using Chan, Golub, LeVeque's algorithm in TR222 1.3b
172#[derive(Default, Debug, Clone, Copy)]
173pub struct UnweightedSumOfSquares<T> {
174    means: Mean<T>,
175    last_ssq: Option<T>,
176}
177
178impl<T> UnweightedSumOfSquares<T>
179where
180    T: Copy + Default,
181{
182    pub fn get_mean(&self) -> T {
183        self.means.last_mean.unwrap_or_default()
184    }
185    pub fn get_unweighted_sum_of_squares(&self) -> T {
186        self.last_ssq.unwrap_or_default()
187    }
188}
189
190impl<Type> StreamingStatistic for UnweightedSumOfSquares<Type>
191where
192    Type: Sub<Type, Output = Type>
193        + Copy
194        + Default
195        + Div<f64, Output = Type>
196        + Add<Type, Output = Type>
197        + Mul<f64, Output = Type>
198        + Mul<Type, Output = Type>,
199{
200    type Type = Type;
201
202    fn add_sample(&mut self, sample: Self::Type) -> Self::Type {
203        let count = self.means.get_num_samples() as f64;
204        let var = sample - self.means.get_last_result();
205        let last = self.last_ssq.unwrap_or_default();
206        let ussq = last + var * (var / (count + 1.0)) * count;
207        self.last_ssq = Some(ussq);
208        let _ = self.means.add_sample(sample);
209        ussq
210    }
211
212    fn get_last_sample(&self) -> Self::Type {
213        self.means.get_last_sample()
214    }
215
216    fn get_last_result(&self) -> Self::Type {
217        self.last_ssq.unwrap_or_default()
218    }
219
220    fn get_num_samples(&self) -> u64 {
221        self.means.get_num_samples()
222    }
223}
224
225///
226/// Returns the [Biased Variance](https://en.wikipedia.org/wiki/Variance#Biased_sample_variance),
227/// which is the Sum of the Squares (SST) scaled by 1/N (the current count)
228#[derive(Default, Debug, Copy, Clone)]
229pub struct BiasedVariance<T> {
230    inner: UnweightedSumOfSquares<T>,
231    last_result: Option<T>,
232}
233
234impl<T> BiasedVariance<T>
235where
236    T: Copy + Default,
237{
238    pub fn get_mean(&self) -> T {
239        self.inner.get_mean()
240    }
241    pub fn get_unweighted_sum_of_squares(&self) -> Option<T> {
242        self.inner.last_ssq
243    }
244    pub fn get_biased_variance(&self) -> Option<T> {
245        self.last_result
246    }
247}
248
249impl<T> StreamingStatistic for BiasedVariance<T>
250where
251    T: Sub<T, Output = T>
252        + Copy
253        + Default
254        + Div<f64, Output = T>
255        + Add<T, Output = T>
256        + Mul<f64, Output = T>
257        + Mul<T, Output = T>,
258{
259    type Type = T;
260
261    fn add_sample(&mut self, sample: Self::Type) -> Self::Type {
262        let cnt = self.get_num_samples() + 1; // not incremented until add_sample
263        let variance = self.inner.add_sample(sample) / cnt as f64;
264        self.last_result = Some(variance);
265        variance
266    }
267
268    fn get_last_sample(&self) -> Self::Type {
269        self.inner.get_last_sample()
270    }
271
272    fn get_last_result(&self) -> Self::Type {
273        self.last_result.unwrap_or_default()
274    }
275
276    fn get_num_samples(&self) -> u64 {
277        self.inner.get_num_samples()
278    }
279}
280
281///
282/// Returns the [Unbiased Variance](https://en.wikipedia.org/wiki/Variance#Unbiased_sample_variance),
283/// which is the Sum of the Squares (SST) scaled by 1/(N-1) (the last count)
284#[derive(Default, Debug, Copy, Clone)]
285pub struct UnbiasedVariance<T> {
286    inner: UnweightedSumOfSquares<T>,
287    last_result: Option<T>,
288}
289
290impl<T> UnbiasedVariance<T>
291where
292    T: Copy + Default,
293{
294    pub fn get_mean(&self) -> T {
295        self.inner.get_mean()
296    }
297    pub fn get_unweighted_sum_of_squares(&self) -> Option<T> {
298        self.inner.last_ssq
299    }
300    pub fn get_unbiased_variance(&self) -> Option<T> {
301        self.last_result
302    }
303}
304
305impl<T> StreamingStatistic for UnbiasedVariance<T>
306where
307    T: Sub<T, Output = T>
308        + Copy
309        + Default
310        + Div<f64, Output = T>
311        + Add<T, Output = T>
312        + Mul<f64, Output = T>
313        + Mul<T, Output = T>,
314{
315    type Type = T;
316
317    fn add_sample(&mut self, sample: Self::Type) -> Self::Type {
318        let cnt = self.get_num_samples();
319        let variance = self.inner.add_sample(sample) / cnt as f64;
320        self.last_result = Some(variance);
321        variance
322    }
323
324    fn get_last_sample(&self) -> Self::Type {
325        self.inner.get_last_sample()
326    }
327
328    fn get_last_result(&self) -> Self::Type {
329        self.last_result.unwrap_or_default()
330    }
331
332    fn get_num_samples(&self) -> u64 {
333        self.inner.get_num_samples()
334    }
335}
336
337///
338/// Returns the Unbiased Standard Deviation, which is the square root of the unbiased Variance,
339/// also known as the 'Sample Standard Deviation'
340#[derive(Default, Debug, Copy, Clone)]
341pub struct UnbiasedStandardDeviation<T> {
342    inner: UnbiasedVariance<T>,
343    last_result: Option<T>,
344}
345
346impl<T> UnbiasedStandardDeviation<T>
347where
348    T: Copy + Default,
349{
350    pub fn get_mean(&self) -> T {
351        self.inner.get_mean()
352    }
353    pub fn get_unweighted_sum_of_squares(&self) -> Option<T> {
354        self.inner.get_unweighted_sum_of_squares()
355    }
356    pub fn get_unbiased_variance(&self) -> Option<T> {
357        self.inner.last_result
358    }
359    pub fn get_unbiased_stdev(&self) -> Option<T> {
360        self.last_result
361    }
362}
363
364impl<T> StreamingStatistic for UnbiasedStandardDeviation<T>
365where
366    T: Sub<T, Output = T>
367        + Copy
368        + Default
369        + Div<f64, Output = T>
370        + Add<T, Output = T>
371        + Mul<f64, Output = T>
372        + Mul<T, Output = T>
373        + FloatExt<Type = T>,
374{
375    type Type = T;
376
377    fn add_sample(&mut self, sample: Self::Type) -> Self::Type {
378        let val = self.inner.add_sample(sample).sqrt();
379        self.last_result = Some(val);
380        val
381    }
382
383    fn get_last_sample(&self) -> Self::Type {
384        self.inner.get_last_sample()
385    }
386
387    fn get_last_result(&self) -> Self::Type {
388        self.last_result.unwrap_or_default()
389    }
390
391    fn get_num_samples(&self) -> u64 {
392        self.inner.get_num_samples()
393    }
394}
395
396///
397/// Returns the Biased Standard Deviation, which is the square root of the biased Variance,
398/// also known as the 'Population Standard Deviation'
399#[derive(Default, Debug, Copy, Clone)]
400pub struct BiasedStandardDeviation<T> {
401    inner: BiasedVariance<T>,
402    last_result: Option<T>,
403}
404
405impl<T> BiasedStandardDeviation<T>
406where
407    T: Copy + Default,
408{
409    pub fn get_mean(&self) -> T {
410        self.inner.get_mean()
411    }
412    pub fn get_unweighted_sum_of_squares(&self) -> Option<T> {
413        self.inner.get_unweighted_sum_of_squares()
414    }
415    pub fn get_biased_variance(&self) -> Option<T> {
416        self.inner.get_biased_variance()
417    }
418    pub fn get_biased_stdev(&self) -> Option<T> {
419        self.last_result
420    }
421}
422
423impl<T> StreamingStatistic for BiasedStandardDeviation<T>
424where
425    T: Sub<T, Output = T>
426        + Copy
427        + Default
428        + Div<f64, Output = T>
429        + Add<T, Output = T>
430        + Mul<f64, Output = T>
431        + Mul<T, Output = T>
432        + FloatExt<Type = T>,
433{
434    type Type = T;
435
436    fn add_sample(&mut self, sample: Self::Type) -> Self::Type {
437        let val = self.inner.add_sample(sample).sqrt();
438        self.last_result = Some(val);
439        val
440    }
441
442    fn get_last_sample(&self) -> Self::Type {
443        self.inner.get_last_sample()
444    }
445
446    fn get_last_result(&self) -> Self::Type {
447        self.last_result.unwrap_or_default()
448    }
449
450    fn get_num_samples(&self) -> u64 {
451        self.inner.get_num_samples()
452    }
453}
454
455#[derive(Copy, Clone)]
456pub struct Summary<T> {
457    mean: Mean<T>,
458    min: Min<T>,
459    max: Max<T>,
460    stdev: UnbiasedStandardDeviation<T>,
461}
462impl<T: Default> Default for Summary<T> {
463    fn default() -> Self {
464        Summary {
465            mean: Mean::default(),
466            min: Min::default(),
467            max: Max::default(),
468            stdev: UnbiasedStandardDeviation::default(),
469        }
470    }
471}
472impl<T: Debug + Copy + Default> Debug for Summary<T> {
473    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
474        f.write_fmt(format_args!(
475            "avg: {:?} +/- {:?} (1std) [{:?}-{:?}]",
476            self.mean.get_mean(),
477            self.stdev.get_unbiased_stdev(),
478            self.min.get_min_val(),
479            self.max.get_max_val()
480        ))
481    }
482}
483impl<T> Summary<T>
484where
485    T: Sub<T, Output = T>
486        + PartialOrd
487        + Copy
488        + Default
489        + Div<f64, Output = T>
490        + Add<T, Output = T>
491        + Mul<f64, Output = T>
492        + Mul<T, Output = T>
493        + FloatExt<Type = T>,
494{
495    pub fn add_sample(&mut self, value: T) {
496        self.min.add_sample(value);
497        self.max.add_sample(value);
498        self.stdev.add_sample(value);
499        self.mean.add_sample(value);
500    }
501    #[must_use]
502    pub fn mean(&self) -> Option<T> {
503        self.mean.get_mean()
504    }
505    #[must_use]
506    pub fn min(&self) -> Option<T> {
507        self.min.get_min_val()
508    }
509    #[must_use]
510    pub fn max(&self) -> Option<T> {
511        self.max.get_max_val()
512    }
513    #[must_use]
514    pub fn stddev(&self) -> Option<T> {
515        self.stdev.get_unbiased_stdev()
516    }
517    #[must_use]
518    pub fn num_samples(&self) -> u64 {
519        self.mean.get_num_samples()
520    }
521}
522#[cfg(feature = "std")]
523pub struct OneSecondWindows {
524    epoch: irox_time::epoch::Epoch,
525    windows: alloc::collections::BTreeMap<irox_time::Time64, Summary<f64>>,
526}
527#[cfg(feature = "std")]
528impl OneSecondWindows {
529    pub fn new(epoch: irox_time::epoch::Epoch) -> Self {
530        Self {
531            epoch,
532            windows: alloc::collections::BTreeMap::new(),
533        }
534    }
535    pub fn add_sample(&mut self, time: irox_time::Time64, value: f64) {
536        let seconds = time.as_epoch(self.epoch).as_only_seconds();
537        self.windows.entry(seconds).or_default().add_sample(value);
538    }
539
540    pub fn iter(&self) -> impl Iterator<Item = (&irox_time::Time64, &Summary<f64>)> {
541        self.windows.iter()
542    }
543}
544
545#[cfg(test)]
546mod test {
547    use crate::streaming::*;
548    use irox_tools::assert_eq_eps;
549    use irox_units::units::duration::Duration;
550
551    #[test]
552    pub fn test() {
553        let mut mean = MeanF64::default();
554        let v = mean.add_sample(0.0);
555        assert_eq!(v, 0.0);
556        let v = mean.add_sample(1.0);
557        assert_eq!(v, 0.5);
558
559        let mut mean = Mean::<Duration>::default();
560        let v = mean.add_sample(Duration::from_seconds(1));
561        assert_eq!(v, Duration::from_seconds_f64(1.0));
562        let v = mean.add_sample(Duration::from_seconds(2));
563        assert_eq!(v, Duration::from_seconds_f64(1.5));
564
565        let mut samp_stddev = UnbiasedStandardDeviation::default();
566        let mut pop_stddev = BiasedStandardDeviation::default();
567        for val in [2, 4, 4, 4, 5, 5, 7, 9] {
568            let _ = samp_stddev.add_sample(val as f64);
569            let _ = pop_stddev.add_sample(val as f64);
570        }
571        assert_eq!(5.0, samp_stddev.get_mean());
572        assert_eq_eps!(
573            4.571428571428571,
574            samp_stddev.get_unbiased_variance().unwrap_or_default(),
575            1e-15
576        );
577        assert_eq_eps!(
578            2.1380899352993947,
579            samp_stddev.get_unbiased_stdev().unwrap_or_default(),
580            1e-15
581        );
582
583        assert_eq!(5.0, pop_stddev.get_mean());
584        assert_eq_eps!(
585            4.,
586            pop_stddev.get_biased_variance().unwrap_or_default(),
587            1e-15
588        );
589        assert_eq_eps!(2., pop_stddev.get_biased_stdev().unwrap_or_default(), 1e-15);
590    }
591}