Skip to main content

llm_optimizer_processor/aggregation/
statistics.rs

1//! Enhanced statistical utilities for stream processing
2//!
3//! This module provides additional statistical functions and utilities
4//! optimized for stream processing scenarios.
5
6use crate::error::{AggregationError, AggregationResult};
7use serde::{Deserialize, Serialize};
8use statrs::distribution::{ContinuousCDF, Normal};
9use statrs::statistics::{Data, OrderStatistics, Statistics};
10use std::collections::VecDeque;
11
12/// Online variance calculator using Welford's algorithm
13///
14/// Provides numerically stable variance calculation for streaming data
15/// without storing all values.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct OnlineVariance {
18    count: u64,
19    mean: f64,
20    m2: f64,
21}
22
23impl OnlineVariance {
24    /// Create a new online variance calculator
25    pub fn new() -> Self {
26        Self {
27            count: 0,
28            mean: 0.0,
29            m2: 0.0,
30        }
31    }
32
33    /// Update with a new value
34    pub fn update(&mut self, value: f64) {
35        self.count += 1;
36        let delta = value - self.mean;
37        self.mean += delta / self.count as f64;
38        let delta2 = value - self.mean;
39        self.m2 += delta * delta2;
40    }
41
42    /// Get the current mean
43    pub fn mean(&self) -> f64 {
44        self.mean
45    }
46
47    /// Get the variance (sample variance)
48    pub fn variance(&self) -> Option<f64> {
49        if self.count < 2 {
50            None
51        } else {
52            Some(self.m2 / (self.count - 1) as f64)
53        }
54    }
55
56    /// Get the standard deviation
57    pub fn std_dev(&self) -> Option<f64> {
58        self.variance().map(|v| v.sqrt())
59    }
60
61    /// Get the population variance
62    pub fn population_variance(&self) -> Option<f64> {
63        if self.count == 0 {
64            None
65        } else {
66            Some(self.m2 / self.count as f64)
67        }
68    }
69
70    /// Get the count of values
71    pub fn count(&self) -> u64 {
72        self.count
73    }
74
75    /// Merge with another online variance calculator
76    pub fn merge(&mut self, other: &OnlineVariance) {
77        if other.count == 0 {
78            return;
79        }
80
81        let combined_count = self.count + other.count;
82        let delta = other.mean - self.mean;
83        let combined_mean = (self.count as f64 * self.mean + other.count as f64 * other.mean)
84            / combined_count as f64;
85
86        self.m2 = self.m2
87            + other.m2
88            + delta * delta * (self.count as f64 * other.count as f64) / combined_count as f64;
89
90        self.count = combined_count;
91        self.mean = combined_mean;
92    }
93
94    /// Reset to initial state
95    pub fn reset(&mut self) {
96        self.count = 0;
97        self.mean = 0.0;
98        self.m2 = 0.0;
99    }
100}
101
102impl Default for OnlineVariance {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108/// Exponential moving average calculator
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ExponentialMovingAverage {
111    alpha: f64,
112    current: Option<f64>,
113}
114
115impl ExponentialMovingAverage {
116    /// Create a new EMA with the given smoothing factor (0 < alpha <= 1)
117    pub fn new(alpha: f64) -> AggregationResult<Self> {
118        if alpha <= 0.0 || alpha > 1.0 {
119            return Err(AggregationError::InvalidValue {
120                value: alpha,
121                reason: "Alpha must be between 0 and 1".to_string(),
122            });
123        }
124
125        Ok(Self {
126            alpha,
127            current: None,
128        })
129    }
130
131    /// Create an EMA with a span (number of periods)
132    pub fn with_span(span: f64) -> AggregationResult<Self> {
133        let alpha = 2.0 / (span + 1.0);
134        Self::new(alpha)
135    }
136
137    /// Update with a new value
138    pub fn update(&mut self, value: f64) {
139        self.current = Some(match self.current {
140            Some(current) => self.alpha * value + (1.0 - self.alpha) * current,
141            None => value,
142        });
143    }
144
145    /// Get the current EMA value
146    pub fn value(&self) -> Option<f64> {
147        self.current
148    }
149
150    /// Reset to initial state
151    pub fn reset(&mut self) {
152        self.current = None;
153    }
154}
155
156/// Sliding window statistics
157///
158/// Maintains a fixed-size sliding window of recent values
159/// and computes statistics over that window.
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct SlidingWindowStats {
162    window_size: usize,
163    values: VecDeque<f64>,
164}
165
166impl SlidingWindowStats {
167    /// Create a new sliding window with the given size
168    pub fn new(window_size: usize) -> AggregationResult<Self> {
169        if window_size == 0 {
170            return Err(AggregationError::InvalidValue {
171                value: window_size as f64,
172                reason: "Window size must be greater than 0".to_string(),
173            });
174        }
175
176        Ok(Self {
177            window_size,
178            values: VecDeque::with_capacity(window_size),
179        })
180    }
181
182    /// Add a new value to the window
183    pub fn update(&mut self, value: f64) {
184        if self.values.len() >= self.window_size {
185            self.values.pop_front();
186        }
187        self.values.push_back(value);
188    }
189
190    /// Get the current window size (may be less than max if not full)
191    pub fn len(&self) -> usize {
192        self.values.len()
193    }
194
195    /// Check if the window is empty
196    pub fn is_empty(&self) -> bool {
197        self.values.is_empty()
198    }
199
200    /// Get the mean of values in the window
201    pub fn mean(&self) -> Option<f64> {
202        if self.values.is_empty() {
203            None
204        } else {
205            Some(self.values.iter().sum::<f64>() / self.values.len() as f64)
206        }
207    }
208
209    /// Get the min value in the window
210    pub fn min(&self) -> Option<f64> {
211        self.values.iter().copied().min_by(|a, b| {
212            a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
213        })
214    }
215
216    /// Get the max value in the window
217    pub fn max(&self) -> Option<f64> {
218        self.values.iter().copied().max_by(|a, b| {
219            a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
220        })
221    }
222
223    /// Get the median value in the window
224    pub fn median(&self) -> Option<f64> {
225        if self.values.is_empty() {
226            return None;
227        }
228
229        let mut sorted: Vec<f64> = self.values.iter().copied().collect();
230        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
231
232        let len = sorted.len();
233        if len % 2 == 0 {
234            Some((sorted[len / 2 - 1] + sorted[len / 2]) / 2.0)
235        } else {
236            Some(sorted[len / 2])
237        }
238    }
239
240    /// Get a percentile value (0-100)
241    pub fn percentile(&self, p: f64) -> AggregationResult<f64> {
242        if self.values.is_empty() {
243            return Err(AggregationError::InsufficientData {
244                aggregation_type: "percentile".to_string(),
245                required: 1,
246                actual: 0,
247            });
248        }
249
250        if p < 0.0 || p > 100.0 {
251            return Err(AggregationError::InvalidValue {
252                value: p,
253                reason: "Percentile must be between 0 and 100".to_string(),
254            });
255        }
256
257        let mut sorted: Vec<f64> = self.values.iter().copied().collect();
258        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
259
260        let index = (p / 100.0 * (sorted.len() - 1) as f64).round() as usize;
261        Ok(sorted[index])
262    }
263
264    /// Get standard deviation
265    pub fn std_dev(&self) -> Option<f64> {
266        if self.values.len() < 2 {
267            return None;
268        }
269
270        let mean = self.mean()?;
271        let variance = self
272            .values
273            .iter()
274            .map(|v| (v - mean).powi(2))
275            .sum::<f64>()
276            / (self.values.len() - 1) as f64;
277
278        Some(variance.sqrt())
279    }
280
281    /// Reset the window
282    pub fn reset(&mut self) {
283        self.values.clear();
284    }
285}
286
287/// Rate calculator for computing events per second
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct RateCalculator {
290    event_count: u64,
291    start_time: Option<chrono::DateTime<chrono::Utc>>,
292    last_time: Option<chrono::DateTime<chrono::Utc>>,
293}
294
295impl RateCalculator {
296    /// Create a new rate calculator
297    pub fn new() -> Self {
298        Self {
299            event_count: 0,
300            start_time: None,
301            last_time: None,
302        }
303    }
304
305    /// Record an event
306    pub fn record_event(&mut self, timestamp: chrono::DateTime<chrono::Utc>) {
307        self.event_count += 1;
308
309        if self.start_time.is_none() {
310            self.start_time = Some(timestamp);
311        }
312
313        self.last_time = Some(timestamp);
314    }
315
316    /// Get the overall rate (events per second)
317    pub fn overall_rate(&self) -> Option<f64> {
318        if self.event_count == 0 {
319            return Some(0.0);
320        }
321
322        let start = self.start_time?;
323        let end = self.last_time?;
324
325        let duration = end.signed_duration_since(start);
326        let seconds = duration.num_milliseconds() as f64 / 1000.0;
327
328        if seconds > 0.0 {
329            Some(self.event_count as f64 / seconds)
330        } else {
331            None
332        }
333    }
334
335    /// Get event count
336    pub fn count(&self) -> u64 {
337        self.event_count
338    }
339
340    /// Reset the calculator
341    pub fn reset(&mut self) {
342        self.event_count = 0;
343        self.start_time = None;
344        self.last_time = None;
345    }
346}
347
348impl Default for RateCalculator {
349    fn default() -> Self {
350        Self::new()
351    }
352}
353
354/// Anomaly detector using z-score
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct ZScoreAnomalyDetector {
357    variance: OnlineVariance,
358    threshold: f64,
359}
360
361impl ZScoreAnomalyDetector {
362    /// Create a new anomaly detector with the given z-score threshold
363    pub fn new(threshold: f64) -> Self {
364        Self {
365            variance: OnlineVariance::new(),
366            threshold,
367        }
368    }
369
370    /// Update with a new value and check if it's an anomaly
371    pub fn check(&mut self, value: f64) -> bool {
372        let is_anomaly = if let Some(std_dev) = self.variance.std_dev() {
373            if std_dev > 0.0 {
374                let z_score = (value - self.variance.mean()).abs() / std_dev;
375                z_score > self.threshold
376            } else {
377                false
378            }
379        } else {
380            false
381        };
382
383        self.variance.update(value);
384        is_anomaly
385    }
386
387    /// Get the current mean
388    pub fn mean(&self) -> f64 {
389        self.variance.mean()
390    }
391
392    /// Get the current standard deviation
393    pub fn std_dev(&self) -> Option<f64> {
394        self.variance.std_dev()
395    }
396
397    /// Reset the detector
398    pub fn reset(&mut self) {
399        self.variance.reset();
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    #[test]
408    fn test_online_variance() {
409        let mut ov = OnlineVariance::new();
410
411        let values = vec![10.0, 20.0, 30.0, 40.0, 50.0];
412        for &v in &values {
413            ov.update(v);
414        }
415
416        assert_eq!(ov.count(), 5);
417        assert_eq!(ov.mean(), 30.0);
418
419        let std_dev = ov.std_dev().unwrap();
420        assert!((std_dev - 15.811).abs() < 0.01);
421    }
422
423    #[test]
424    fn test_online_variance_merge() {
425        let mut ov1 = OnlineVariance::new();
426        ov1.update(10.0);
427        ov1.update(20.0);
428
429        let mut ov2 = OnlineVariance::new();
430        ov2.update(30.0);
431        ov2.update(40.0);
432
433        ov1.merge(&ov2);
434
435        assert_eq!(ov1.count(), 4);
436        assert_eq!(ov1.mean(), 25.0);
437    }
438
439    #[test]
440    fn test_ema() {
441        let mut ema = ExponentialMovingAverage::new(0.5).unwrap();
442
443        ema.update(10.0);
444        assert_eq!(ema.value(), Some(10.0));
445
446        ema.update(20.0);
447        assert_eq!(ema.value(), Some(15.0));
448
449        ema.update(30.0);
450        assert_eq!(ema.value(), Some(22.5));
451    }
452
453    #[test]
454    fn test_ema_with_span() {
455        let mut ema = ExponentialMovingAverage::with_span(9.0).unwrap();
456
457        for i in 1..=10 {
458            ema.update(i as f64);
459        }
460
461        assert!(ema.value().is_some());
462        let value = ema.value().unwrap();
463        assert!(value > 5.0 && value < 10.0);
464    }
465
466    #[test]
467    fn test_sliding_window_stats() {
468        let mut sw = SlidingWindowStats::new(3).unwrap();
469
470        sw.update(10.0);
471        sw.update(20.0);
472        sw.update(30.0);
473
474        assert_eq!(sw.len(), 3);
475        assert_eq!(sw.mean(), Some(20.0));
476        assert_eq!(sw.min(), Some(10.0));
477        assert_eq!(sw.max(), Some(30.0));
478
479        // Add another value (should evict 10.0)
480        sw.update(40.0);
481        assert_eq!(sw.len(), 3);
482        assert_eq!(sw.mean(), Some(30.0));
483        assert_eq!(sw.min(), Some(20.0));
484    }
485
486    #[test]
487    fn test_sliding_window_median() {
488        let mut sw = SlidingWindowStats::new(5).unwrap();
489
490        sw.update(10.0);
491        sw.update(20.0);
492        sw.update(30.0);
493        sw.update(40.0);
494        sw.update(50.0);
495
496        assert_eq!(sw.median(), Some(30.0));
497    }
498
499    #[test]
500    fn test_sliding_window_percentile() {
501        let mut sw = SlidingWindowStats::new(5).unwrap();
502
503        for i in 1..=5 {
504            sw.update(i as f64 * 10.0);
505        }
506
507        let p50 = sw.percentile(50.0).unwrap();
508        assert_eq!(p50, 30.0);
509
510        let p95 = sw.percentile(95.0).unwrap();
511        assert_eq!(p95, 50.0);
512    }
513
514    #[test]
515    fn test_rate_calculator() {
516        let mut rate = RateCalculator::new();
517
518        let base_time = chrono::Utc::now();
519
520        rate.record_event(base_time);
521        rate.record_event(base_time + chrono::Duration::milliseconds(500));
522        rate.record_event(base_time + chrono::Duration::seconds(1));
523
524        assert_eq!(rate.count(), 3);
525
526        let overall_rate = rate.overall_rate().unwrap();
527        assert!(overall_rate > 0.0);
528        assert!(overall_rate <= 3.0); // Should be around 3 events/sec
529    }
530
531    #[test]
532    fn test_zscore_anomaly_detector() {
533        let mut detector = ZScoreAnomalyDetector::new(2.0);
534
535        // Feed normal values
536        for i in 1..=10 {
537            let is_anomaly = detector.check(i as f64);
538            // First few values might be anomalies until we have enough data
539        }
540
541        // Feed an anomaly (way outside normal range)
542        let is_anomaly = detector.check(100.0);
543        assert!(is_anomaly);
544    }
545
546    #[test]
547    fn test_online_variance_reset() {
548        let mut ov = OnlineVariance::new();
549        ov.update(10.0);
550        ov.update(20.0);
551
552        assert_eq!(ov.count(), 2);
553
554        ov.reset();
555        assert_eq!(ov.count(), 0);
556        assert_eq!(ov.mean(), 0.0);
557    }
558
559    #[test]
560    fn test_sliding_window_empty() {
561        let sw = SlidingWindowStats::new(5).unwrap();
562
563        assert!(sw.is_empty());
564        assert_eq!(sw.len(), 0);
565        assert_eq!(sw.mean(), None);
566        assert_eq!(sw.median(), None);
567    }
568}