Skip to main content

wowlab_types/stats/summary/
batch.rs

1use super::Streaming;
2use crate::constants::HUNDRED;
3
4const PERCENTILE_50: usize = 50;
5const PERCENTILE_MAX: usize = 100;
6const PERCENTILE_75: usize = 75;
7const PERCENTILE_25: usize = 25;
8
9/// Batch statistics for a complete dataset.
10#[derive(Debug)]
11pub struct Batch {
12    data: Vec<f64>,
13    sorted: bool,
14    streaming: Streaming,
15}
16
17impl Batch {
18    #[must_use]
19    pub fn new(values: Vec<f64>) -> Self {
20        let streaming: Streaming = values.iter().copied().collect();
21
22        Self {
23            data: values,
24            sorted: false,
25            streaming,
26        }
27    }
28
29    #[inline]
30    #[must_use]
31    pub fn count(&self) -> usize {
32        self.data.len()
33    }
34
35    #[inline]
36    #[must_use]
37    pub fn mean(&self) -> f64 {
38        self.streaming.mean()
39    }
40
41    #[inline]
42    #[must_use]
43    pub fn variance(&self) -> f64 {
44        self.streaming.variance()
45    }
46
47    #[inline]
48    #[must_use]
49    pub fn std_dev(&self) -> f64 {
50        self.streaming.std_dev()
51    }
52
53    #[inline]
54    #[must_use]
55    pub fn min(&self) -> f64 {
56        self.streaming.min()
57    }
58
59    #[inline]
60    #[must_use]
61    pub fn max(&self) -> f64 {
62        self.streaming.max()
63    }
64
65    #[inline]
66    #[must_use]
67    pub fn cv(&self) -> f64 {
68        self.streaming.cv()
69    }
70
71    pub fn median(&mut self) -> f64 {
72        self.percentile(PERCENTILE_50)
73    }
74
75    /// Percentile using linear interpolation (0-100).
76    #[expect(
77        clippy::cast_possible_truncation,
78        clippy::cast_precision_loss,
79        clippy::cast_sign_loss,
80        reason = "percentile positions are clamped to the nonnegative bounds of the data vector"
81    )]
82    pub fn percentile(&mut self, p: usize) -> f64 {
83        if self.data.is_empty() {
84            return f64::NAN;
85        }
86
87        self.ensure_sorted();
88
89        let p = p.min(PERCENTILE_MAX) as f64 / HUNDRED;
90        let idx = p * (self.data.len() - 1) as f64;
91        let lo = idx.floor() as usize;
92        let hi = idx.ceil() as usize;
93
94        if lo == hi {
95            self.data.get(lo).copied().unwrap_or(f64::NAN)
96        } else {
97            let frac = idx - lo as f64;
98            let lo_val = self.data.get(lo).copied().unwrap_or(f64::NAN);
99            let hi_val = self.data.get(hi).copied().unwrap_or(f64::NAN);
100
101            lo_val * (1.0 - frac) + hi_val * frac
102        }
103    }
104
105    /// Interquartile range.
106    pub fn iqr(&mut self) -> f64 {
107        self.percentile(PERCENTILE_75) - self.percentile(PERCENTILE_25)
108    }
109
110    fn ensure_sorted(&mut self) {
111        if !self.sorted {
112            self.data
113                .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
114            self.sorted = true;
115        }
116    }
117}
118
119/// Alias for [`Batch`].
120pub type Summary = Batch;