Skip to main content

llm_optimizer_processor/aggregation/
stddev.rs

1use super::trait_::{Aggregator, ToF64};
2use anyhow::anyhow;
3use serde::{Deserialize, Serialize};
4
5/// Accumulator for standard deviation aggregation
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub struct StdDevAccumulator {
8    count: u64,
9    mean: f64,
10    m2: f64, // Sum of squared differences from mean
11}
12
13/// Standard deviation aggregator - computes sample standard deviation
14///
15/// This aggregator uses Welford's online algorithm for numerical stability.
16/// Computes the sample standard deviation (using Bessel's correction, n-1).
17///
18/// Useful for measuring variability in latency, cost, throughput, etc.
19///
20/// # Examples
21///
22/// ```
23/// use processor::aggregation::{Aggregator, StandardDeviationAggregator};
24///
25/// let mut agg = StandardDeviationAggregator::new();
26/// agg.update(10.0).unwrap();
27/// agg.update(12.0).unwrap();
28/// agg.update(14.0).unwrap();
29/// agg.update(16.0).unwrap();
30/// agg.update(18.0).unwrap();
31///
32/// let stddev = agg.finalize().unwrap();
33/// assert!((stddev - 3.1622).abs() < 0.01); // ~3.16
34/// ```
35#[derive(Debug, Clone)]
36pub struct StandardDeviationAggregator<T> {
37    count: u64,
38    mean: f64,
39    m2: f64,
40    sample: bool, // If true, use sample std dev (n-1), otherwise population (n)
41    _phantom: std::marker::PhantomData<T>,
42}
43
44impl<T: ToF64 + Clone> StandardDeviationAggregator<T> {
45    /// Create a new sample standard deviation aggregator (uses n-1)
46    pub fn new() -> Self {
47        Self {
48            count: 0,
49            mean: 0.0,
50            m2: 0.0,
51            sample: true,
52            _phantom: std::marker::PhantomData,
53        }
54    }
55
56    /// Create a population standard deviation aggregator (uses n)
57    pub fn population() -> Self {
58        Self {
59            count: 0,
60            mean: 0.0,
61            m2: 0.0,
62            sample: false,
63            _phantom: std::marker::PhantomData,
64        }
65    }
66
67    /// Get the current variance
68    pub fn variance(&self) -> Option<f64> {
69        if self.count == 0 {
70            return None;
71        }
72
73        let divisor = if self.sample {
74            if self.count < 2 {
75                return None;
76            }
77            self.count - 1
78        } else {
79            self.count
80        } as f64;
81
82        Some(self.m2 / divisor)
83    }
84
85    /// Get the current standard deviation
86    pub fn std_dev(&self) -> Option<f64> {
87        self.variance().map(|v| v.sqrt())
88    }
89
90    /// Get the current mean
91    pub fn mean(&self) -> Option<f64> {
92        if self.count == 0 {
93            None
94        } else {
95            Some(self.mean)
96        }
97    }
98}
99
100impl<T: ToF64 + Clone> Default for StandardDeviationAggregator<T> {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl<T: ToF64 + Clone + Send + Sync + std::fmt::Debug> Aggregator for StandardDeviationAggregator<T> {
107    type Input = T;
108    type Output = f64;
109    type Accumulator = StdDevAccumulator;
110
111    fn new() -> Self {
112        Self::new()
113    }
114
115    fn update(&mut self, value: <Self as Aggregator>::Input) -> anyhow::Result<()> {
116        let x = value.to_f64();
117        self.count += 1;
118
119        // Welford's online algorithm
120        let delta = x - self.mean;
121        self.mean += delta / self.count as f64;
122        let delta2 = x - self.mean;
123        self.m2 += delta * delta2;
124
125        Ok(())
126    }
127
128    fn finalize(&self) -> anyhow::Result<<Self as Aggregator>::Output> {
129        self.std_dev()
130            .ok_or_else(|| anyhow!("Cannot compute standard deviation with less than 2 values"))
131    }
132
133    fn accumulator(&self) -> <Self as Aggregator>::Accumulator {
134        StdDevAccumulator {
135            count: self.count,
136            mean: self.mean,
137            m2: self.m2,
138        }
139    }
140
141    fn merge(&mut self, other: <Self as Aggregator>::Accumulator) -> anyhow::Result<()> {
142        if other.count == 0 {
143            return Ok(());
144        }
145
146        if self.count == 0 {
147            self.count = other.count;
148            self.mean = other.mean;
149            self.m2 = other.m2;
150            return Ok(());
151        }
152
153        // Parallel variance formula (Chan et al.)
154        let delta = other.mean - self.mean;
155        let total_count = self.count + other.count;
156
157        self.m2 = self.m2 + other.m2
158            + delta * delta * (self.count as f64 * other.count as f64) / total_count as f64;
159
160        self.mean = (self.count as f64 * self.mean + other.count as f64 * other.mean)
161            / total_count as f64;
162
163        self.count = total_count;
164
165        Ok(())
166    }
167
168    fn reset(&mut self) {
169        self.count = 0;
170        self.mean = 0.0;
171        self.m2 = 0.0;
172    }
173
174    fn count(&self) -> u64 {
175        self.count
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_stddev_basic() {
185        let mut agg = StandardDeviationAggregator::<f64>::new();
186
187        // Values: 10, 12, 14, 16, 18
188        // Mean: 14
189        // Variance: ((4 + 4 + 0 + 4 + 16) / 4) = 10
190        // StdDev: sqrt(10) = 3.162...
191        agg.update(10.0).unwrap();
192        agg.update(12.0).unwrap();
193        agg.update(14.0).unwrap();
194        agg.update(16.0).unwrap();
195        agg.update(18.0).unwrap();
196
197        assert_eq!(agg.count(), 5);
198        assert!((agg.mean().unwrap() - 14.0).abs() < 0.01);
199        assert!((agg.variance().unwrap() - 10.0).abs() < 0.01);
200        assert!((agg.std_dev().unwrap() - 3.162).abs() < 0.01);
201        assert!((agg.finalize().unwrap() - 3.162).abs() < 0.01);
202    }
203
204    #[test]
205    fn test_stddev_population() {
206        let mut agg = StandardDeviationAggregator::<f64>::population();
207
208        agg.update(10.0).unwrap();
209        agg.update(12.0).unwrap();
210        agg.update(14.0).unwrap();
211        agg.update(16.0).unwrap();
212        agg.update(18.0).unwrap();
213
214        // Population variance: 28/5 = 5.6
215        // Population stddev: sqrt(5.6) = 2.828...
216        assert!((agg.variance().unwrap() - 8.0).abs() < 0.01);
217        assert!((agg.std_dev().unwrap() - 2.828).abs() < 0.01);
218    }
219
220    #[test]
221    fn test_stddev_batch() {
222        let mut agg = StandardDeviationAggregator::<f64>::new();
223        agg.update_batch(&[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]).unwrap();
224
225        assert_eq!(agg.count(), 8);
226        assert!((agg.mean().unwrap() - 5.0).abs() < 0.01);
227        assert!((agg.std_dev().unwrap() - 2.138).abs() < 0.01);
228    }
229
230    #[test]
231    fn test_stddev_merge() {
232        let mut agg1 = StandardDeviationAggregator::<f64>::new();
233        agg1.update(10.0).unwrap();
234        agg1.update(12.0).unwrap();
235        agg1.update(14.0).unwrap();
236
237        let mut agg2 = StandardDeviationAggregator::<f64>::new();
238        agg2.update(16.0).unwrap();
239        agg2.update(18.0).unwrap();
240
241        let acc2 = agg2.accumulator();
242        agg1.merge(acc2).unwrap();
243
244        assert_eq!(agg1.count(), 5);
245        assert!((agg1.mean().unwrap() - 14.0).abs() < 0.01);
246        assert!((agg1.std_dev().unwrap() - 3.162).abs() < 0.01);
247    }
248
249    #[test]
250    fn test_stddev_reset() {
251        let mut agg = StandardDeviationAggregator::<f64>::new();
252        agg.update(10.0).unwrap();
253        agg.update(20.0).unwrap();
254        agg.update(30.0).unwrap();
255
256        assert_eq!(agg.count(), 3);
257
258        agg.reset();
259        assert_eq!(agg.count(), 0);
260        assert!(agg.mean().is_none());
261        assert!(agg.variance().is_none());
262        assert!(agg.std_dev().is_none());
263    }
264
265    #[test]
266    fn test_stddev_serialization() {
267        let mut agg = StandardDeviationAggregator::<f64>::new();
268        agg.update(10.0).unwrap();
269        agg.update(20.0).unwrap();
270
271        let acc = agg.accumulator();
272        let serialized = serde_json::to_string(&acc).unwrap();
273        let deserialized: StdDevAccumulator = serde_json::from_str(&serialized).unwrap();
274
275        assert_eq!(acc, deserialized);
276    }
277
278    #[test]
279    fn test_stddev_empty() {
280        let agg = StandardDeviationAggregator::<f64>::new();
281        assert!(agg.finalize().is_err());
282        assert!(agg.mean().is_none());
283        assert!(agg.variance().is_none());
284    }
285
286    #[test]
287    fn test_stddev_single_value() {
288        let mut agg = StandardDeviationAggregator::<f64>::new();
289        agg.update(42.0).unwrap();
290
291        // Sample stddev requires at least 2 values
292        assert!(agg.finalize().is_err());
293        assert_eq!(agg.mean().unwrap(), 42.0);
294    }
295
296    #[test]
297    fn test_stddev_two_values() {
298        let mut agg = StandardDeviationAggregator::<f64>::new();
299        agg.update(10.0).unwrap();
300        agg.update(20.0).unwrap();
301
302        // Mean: 15, variance: 50, stddev: 7.071
303        assert!((agg.mean().unwrap() - 15.0).abs() < 0.01);
304        assert!((agg.variance().unwrap() - 50.0).abs() < 0.01);
305        assert!((agg.std_dev().unwrap() - 7.071).abs() < 0.01);
306    }
307
308    #[test]
309    fn test_stddev_zero_variance() {
310        let mut agg = StandardDeviationAggregator::<f64>::new();
311        agg.update(5.0).unwrap();
312        agg.update(5.0).unwrap();
313        agg.update(5.0).unwrap();
314
315        assert_eq!(agg.mean().unwrap(), 5.0);
316        assert_eq!(agg.variance().unwrap(), 0.0);
317        assert_eq!(agg.std_dev().unwrap(), 0.0);
318    }
319
320    #[test]
321    fn test_stddev_negative_values() {
322        let mut agg = StandardDeviationAggregator::<f64>::new();
323        agg.update(-10.0).unwrap();
324        agg.update(-5.0).unwrap();
325        agg.update(0.0).unwrap();
326        agg.update(5.0).unwrap();
327        agg.update(10.0).unwrap();
328
329        assert!((agg.mean().unwrap() - 0.0).abs() < 0.01);
330        assert!((agg.std_dev().unwrap() - 7.905).abs() < 0.01);
331    }
332
333    #[test]
334    fn test_stddev_with_integer_types() {
335        let mut agg = StandardDeviationAggregator::<i32>::new();
336        agg.update(10).unwrap();
337        agg.update(12).unwrap();
338        agg.update(14).unwrap();
339        agg.update(16).unwrap();
340        agg.update(18).unwrap();
341
342        assert!((agg.mean().unwrap() - 14.0).abs() < 0.01);
343        assert!((agg.std_dev().unwrap() - 3.162).abs() < 0.01);
344    }
345}