Skip to main content

llm_optimizer_processor/analyzer/
stats.rs

1//! Statistical utilities for analyzers
2//!
3//! This module provides common statistical functions used by analyzers,
4//! including percentile calculation, moving averages, anomaly detection, etc.
5
6use std::collections::VecDeque;
7
8/// Circular buffer for maintaining a sliding window of values
9#[derive(Debug, Clone)]
10pub struct CircularBuffer<T> {
11    buffer: VecDeque<T>,
12    capacity: usize,
13}
14
15impl<T> CircularBuffer<T> {
16    /// Create a new circular buffer with the given capacity
17    pub fn new(capacity: usize) -> Self {
18        Self {
19            buffer: VecDeque::with_capacity(capacity),
20            capacity,
21        }
22    }
23
24    /// Add a value to the buffer
25    pub fn push(&mut self, value: T) {
26        if self.buffer.len() >= self.capacity {
27            self.buffer.pop_front();
28        }
29        self.buffer.push_back(value);
30    }
31
32    /// Get the current length
33    pub fn len(&self) -> usize {
34        self.buffer.len()
35    }
36
37    /// Check if the buffer is empty
38    pub fn is_empty(&self) -> bool {
39        self.buffer.is_empty()
40    }
41
42    /// Get a reference to the buffer
43    pub fn as_slice(&self) -> &VecDeque<T> {
44        &self.buffer
45    }
46
47    /// Clear the buffer
48    pub fn clear(&mut self) {
49        self.buffer.clear();
50    }
51
52    /// Get the buffer capacity
53    pub fn capacity(&self) -> usize {
54        self.capacity
55    }
56
57    /// Check if the buffer is at capacity
58    pub fn is_full(&self) -> bool {
59        self.buffer.len() >= self.capacity
60    }
61}
62
63impl<T: Clone> CircularBuffer<T> {
64    /// Get all values as a vector
65    pub fn to_vec(&self) -> Vec<T> {
66        self.buffer.iter().cloned().collect()
67    }
68}
69
70/// Exponential Moving Average calculator
71#[derive(Debug, Clone)]
72pub struct ExponentialMovingAverage {
73    alpha: f64,
74    current: Option<f64>,
75}
76
77impl ExponentialMovingAverage {
78    /// Create a new EMA calculator with the given smoothing factor (0.0-1.0)
79    ///
80    /// Higher alpha means more weight on recent values.
81    /// Typical values: 0.1 (slow), 0.3 (medium), 0.5 (fast)
82    pub fn new(alpha: f64) -> Self {
83        Self {
84            alpha: alpha.clamp(0.0, 1.0),
85            current: None,
86        }
87    }
88
89    /// Update the EMA with a new value
90    pub fn update(&mut self, value: f64) {
91        self.current = Some(match self.current {
92            None => value,
93            Some(prev) => self.alpha * value + (1.0 - self.alpha) * prev,
94        });
95    }
96
97    /// Get the current EMA value
98    pub fn value(&self) -> Option<f64> {
99        self.current
100    }
101
102    /// Reset the EMA
103    pub fn reset(&mut self) {
104        self.current = None;
105    }
106}
107
108/// Simple Moving Average calculator
109#[derive(Debug, Clone)]
110pub struct SimpleMovingAverage {
111    window: CircularBuffer<f64>,
112    sum: f64,
113}
114
115impl SimpleMovingAverage {
116    /// Create a new SMA calculator with the given window size
117    pub fn new(window_size: usize) -> Self {
118        Self {
119            window: CircularBuffer::new(window_size),
120            sum: 0.0,
121        }
122    }
123
124    /// Add a new value
125    pub fn update(&mut self, value: f64) {
126        if self.window.is_full() {
127            // Remove oldest value from sum
128            if let Some(&oldest) = self.window.as_slice().front() {
129                self.sum -= oldest;
130            }
131        }
132
133        self.window.push(value);
134        self.sum += value;
135    }
136
137    /// Get the current average
138    pub fn value(&self) -> Option<f64> {
139        if self.window.is_empty() {
140            None
141        } else {
142            Some(self.sum / self.window.len() as f64)
143        }
144    }
145
146    /// Get the count of values
147    pub fn count(&self) -> usize {
148        self.window.len()
149    }
150
151    /// Reset the SMA
152    pub fn reset(&mut self) {
153        self.window.clear();
154        self.sum = 0.0;
155    }
156}
157
158/// Calculate percentiles from a dataset
159pub fn calculate_percentiles(mut values: Vec<f64>, percentiles: &[f64]) -> Vec<(f64, f64)> {
160    if values.is_empty() {
161        return percentiles.iter().map(|&p| (p, 0.0)).collect();
162    }
163
164    values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
165
166    percentiles
167        .iter()
168        .map(|&p| {
169            let idx = ((p / 100.0) * (values.len() - 1) as f64).round() as usize;
170            let value = values[idx.min(values.len() - 1)];
171            (p, value)
172        })
173        .collect()
174}
175
176/// Calculate mean of a dataset
177pub fn mean(values: &[f64]) -> f64 {
178    if values.is_empty() {
179        return 0.0;
180    }
181    values.iter().sum::<f64>() / values.len() as f64
182}
183
184/// Calculate standard deviation of a dataset
185pub fn std_dev(values: &[f64]) -> f64 {
186    if values.len() < 2 {
187        return 0.0;
188    }
189
190    let m = mean(values);
191    let variance = values.iter().map(|x| (x - m).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
192    variance.sqrt()
193}
194
195/// Calculate z-score for a value
196pub fn z_score(value: f64, mean: f64, std_dev: f64) -> f64 {
197    if std_dev == 0.0 {
198        return 0.0;
199    }
200    (value - mean) / std_dev
201}
202
203/// Check if a value is an outlier using z-score method
204///
205/// A value is considered an outlier if |z-score| > threshold
206/// Common threshold: 3.0 (99.7% of data within ±3σ)
207pub fn is_outlier_zscore(value: f64, mean: f64, std_dev: f64, threshold: f64) -> bool {
208    z_score(value, mean, std_dev).abs() > threshold
209}
210
211/// Calculate interquartile range (IQR)
212pub fn calculate_iqr(mut values: Vec<f64>) -> (f64, f64, f64) {
213    if values.is_empty() {
214        return (0.0, 0.0, 0.0);
215    }
216
217    values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
218
219    let q1_idx = (values.len() / 4).max(0);
220    let q3_idx = (3 * values.len() / 4).min(values.len() - 1);
221
222    let q1 = values[q1_idx];
223    let q3 = values[q3_idx];
224    let iqr = q3 - q1;
225
226    (q1, q3, iqr)
227}
228
229/// Check if a value is an outlier using IQR method
230///
231/// A value is an outlier if it's below Q1 - multiplier*IQR or above Q3 + multiplier*IQR
232/// Common multiplier: 1.5 (standard outlier), 3.0 (extreme outlier)
233pub fn is_outlier_iqr(value: f64, values: &[f64], multiplier: f64) -> bool {
234    if values.len() < 4 {
235        return false;
236    }
237
238    let (q1, q3, iqr) = calculate_iqr(values.to_vec());
239    let lower_bound = q1 - multiplier * iqr;
240    let upper_bound = q3 + multiplier * iqr;
241
242    value < lower_bound || value > upper_bound
243}
244
245/// Calculate rate (events per time unit)
246pub fn calculate_rate(count: u64, duration_secs: f64) -> f64 {
247    if duration_secs == 0.0 {
248        return 0.0;
249    }
250    count as f64 / duration_secs
251}
252
253/// Calculate percentage change
254pub fn percentage_change(old: f64, new: f64) -> f64 {
255    if old == 0.0 {
256        if new == 0.0 {
257            return 0.0;
258        }
259        return 100.0; // or f64::INFINITY
260    }
261    ((new - old) / old) * 100.0
262}
263
264/// Linear regression to find trend
265#[derive(Debug, Clone)]
266pub struct LinearTrend {
267    /// Slope of the trend line
268    pub slope: f64,
269    /// Intercept of the trend line
270    pub intercept: f64,
271    /// R-squared (coefficient of determination)
272    pub r_squared: f64,
273}
274
275/// Calculate linear trend from time series data
276pub fn calculate_linear_trend(values: &[(f64, f64)]) -> Option<LinearTrend> {
277    if values.len() < 2 {
278        return None;
279    }
280
281    let n = values.len() as f64;
282    let sum_x: f64 = values.iter().map(|(x, _)| x).sum();
283    let sum_y: f64 = values.iter().map(|(_, y)| y).sum();
284    let sum_xy: f64 = values.iter().map(|(x, y)| x * y).sum();
285    let sum_x2: f64 = values.iter().map(|(x, _)| x * x).sum();
286    let sum_y2: f64 = values.iter().map(|(_, y)| y * y).sum();
287
288    let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
289    let intercept = (sum_y - slope * sum_x) / n;
290
291    // Calculate R-squared
292    let mean_y = sum_y / n;
293    let ss_tot: f64 = values.iter().map(|(_, y)| (y - mean_y).powi(2)).sum();
294    let ss_res: f64 = values
295        .iter()
296        .map(|(x, y)| {
297            let predicted = slope * x + intercept;
298            (y - predicted).powi(2)
299        })
300        .sum();
301
302    let r_squared = if ss_tot == 0.0 {
303        1.0
304    } else {
305        1.0 - (ss_res / ss_tot)
306    };
307
308    Some(LinearTrend {
309        slope,
310        intercept,
311        r_squared,
312    })
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_circular_buffer() {
321        let mut buffer = CircularBuffer::new(3);
322        assert!(buffer.is_empty());
323
324        buffer.push(1);
325        buffer.push(2);
326        buffer.push(3);
327        assert!(buffer.is_full());
328        assert_eq!(buffer.len(), 3);
329
330        buffer.push(4);
331        assert_eq!(buffer.len(), 3);
332        assert_eq!(buffer.to_vec(), vec![2, 3, 4]);
333    }
334
335    #[test]
336    fn test_exponential_moving_average() {
337        let mut ema = ExponentialMovingAverage::new(0.5);
338        assert_eq!(ema.value(), None);
339
340        ema.update(10.0);
341        assert_eq!(ema.value(), Some(10.0));
342
343        ema.update(20.0);
344        assert_eq!(ema.value(), Some(15.0)); // 0.5 * 20 + 0.5 * 10
345
346        ema.reset();
347        assert_eq!(ema.value(), None);
348    }
349
350    #[test]
351    fn test_simple_moving_average() {
352        let mut sma = SimpleMovingAverage::new(3);
353        assert_eq!(sma.value(), None);
354
355        sma.update(10.0);
356        assert_eq!(sma.value(), Some(10.0));
357
358        sma.update(20.0);
359        assert_eq!(sma.value(), Some(15.0));
360
361        sma.update(30.0);
362        assert_eq!(sma.value(), Some(20.0));
363
364        sma.update(40.0); // Should drop 10.0
365        assert_eq!(sma.value(), Some(30.0)); // (20 + 30 + 40) / 3
366    }
367
368    #[test]
369    fn test_calculate_percentiles() {
370        let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
371        let percentiles = calculate_percentiles(values, &[50.0, 95.0, 99.0]);
372
373        assert_eq!(percentiles.len(), 3);
374        assert!((percentiles[0].1 - 5.5).abs() < 0.1); // p50 ≈ 5.5
375        assert!((percentiles[1].1 - 10.0).abs() < 0.1); // p95 ≈ 10.0
376    }
377
378    #[test]
379    fn test_mean_and_std_dev() {
380        let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
381        assert_eq!(mean(&values), 3.0);
382
383        let std = std_dev(&values);
384        assert!((std - 1.58).abs() < 0.01); // std dev ≈ 1.58
385    }
386
387    #[test]
388    fn test_z_score() {
389        let score = z_score(100.0, 50.0, 10.0);
390        assert_eq!(score, 5.0);
391
392        let score = z_score(40.0, 50.0, 10.0);
393        assert_eq!(score, -1.0);
394    }
395
396    #[test]
397    fn test_is_outlier_zscore() {
398        assert!(is_outlier_zscore(100.0, 50.0, 10.0, 3.0)); // z=5.0 > 3.0
399        assert!(!is_outlier_zscore(65.0, 50.0, 10.0, 3.0)); // z=1.5 < 3.0
400    }
401
402    #[test]
403    fn test_calculate_iqr() {
404        let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
405        let (q1, q3, iqr) = calculate_iqr(values);
406
407        assert_eq!(q1, 2.0);
408        assert_eq!(q3, 6.0);
409        assert_eq!(iqr, 4.0);
410    }
411
412    #[test]
413    fn test_is_outlier_iqr() {
414        let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
415
416        // Q1=2, Q3=6, IQR=4, bounds: [2-1.5*4, 6+1.5*4] = [-4, 12]
417        assert!(!is_outlier_iqr(10.0, &values, 1.5));
418        assert!(is_outlier_iqr(20.0, &values, 1.5));
419    }
420
421    #[test]
422    fn test_calculate_rate() {
423        assert_eq!(calculate_rate(100, 10.0), 10.0);
424        assert_eq!(calculate_rate(0, 10.0), 0.0);
425        assert_eq!(calculate_rate(100, 0.0), 0.0);
426    }
427
428    #[test]
429    fn test_percentage_change() {
430        assert_eq!(percentage_change(100.0, 150.0), 50.0);
431        assert_eq!(percentage_change(100.0, 50.0), -50.0);
432        assert_eq!(percentage_change(0.0, 100.0), 100.0);
433    }
434
435    #[test]
436    fn test_calculate_linear_trend() {
437        let data = vec![(1.0, 2.0), (2.0, 4.0), (3.0, 6.0), (4.0, 8.0)];
438        let trend = calculate_linear_trend(&data).unwrap();
439
440        assert!((trend.slope - 2.0).abs() < 0.01);
441        assert!((trend.intercept - 0.0).abs() < 0.01);
442        assert!((trend.r_squared - 1.0).abs() < 0.01); // Perfect linear fit
443    }
444}