Skip to main content

u_analytics/smoothing/
ses.rs

1//! Simple Exponential Smoothing (SES).
2//!
3//! Level-only smoothing for stationary time series (no trend, no seasonality).
4//!
5//! # Algorithm
6//!
7//! ```text
8//! S_1 = x_1
9//! S_t = α x_t + (1 - α) S_{t-1}
10//! ```
11//!
12//! where α ∈ (0, 1) is the smoothing constant.
13//!
14//! # Reference
15//!
16//! Brown, R.G. (1956). *Exponential Smoothing for Predicting Demand*.
17
18/// Result of simple exponential smoothing at each time step.
19#[derive(Debug, Clone)]
20pub struct SesResult {
21    /// Smoothed values.
22    pub smoothed: Vec<f64>,
23    /// One-step-ahead forecast for the next period.
24    pub forecast: f64,
25}
26
27/// Simple Exponential Smoothing.
28pub struct SimpleExponentialSmoothing {
29    alpha: f64,
30}
31
32impl SimpleExponentialSmoothing {
33    /// Creates a new SES smoother.
34    ///
35    /// # Parameters
36    /// - `alpha`: smoothing constant, must be in (0, 1)
37    ///
38    /// Returns `None` if alpha is out of range or non-finite.
39    pub fn new(alpha: f64) -> Option<Self> {
40        if !alpha.is_finite() || alpha <= 0.0 || alpha >= 1.0 {
41            return None;
42        }
43        Some(Self { alpha })
44    }
45
46    /// Returns the smoothing constant α.
47    pub fn alpha(&self) -> f64 {
48        self.alpha
49    }
50
51    /// Applies SES to the given data.
52    ///
53    /// Returns `None` if data is empty.
54    pub fn smooth(&self, data: &[f64]) -> Option<SesResult> {
55        if data.is_empty() {
56            return None;
57        }
58
59        let mut smoothed = Vec::with_capacity(data.len());
60        let mut s = data[0];
61        smoothed.push(s);
62
63        for &x in &data[1..] {
64            s = self.alpha * x + (1.0 - self.alpha) * s;
65            smoothed.push(s);
66        }
67
68        let forecast = s;
69        Some(SesResult { smoothed, forecast })
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_ses_basic() {
79        let ses = SimpleExponentialSmoothing::new(0.3).unwrap();
80        let data = [10.0, 12.0, 13.0, 11.0, 14.0];
81        let result = ses.smooth(&data).unwrap();
82
83        assert_eq!(result.smoothed.len(), 5);
84        // S1 = 10
85        assert!((result.smoothed[0] - 10.0).abs() < 1e-10);
86        // S2 = 0.3*12 + 0.7*10 = 10.6
87        assert!((result.smoothed[1] - 10.6).abs() < 1e-10);
88    }
89
90    #[test]
91    fn test_ses_constant_series() {
92        let ses = SimpleExponentialSmoothing::new(0.5).unwrap();
93        let data = [5.0; 10];
94        let result = ses.smooth(&data).unwrap();
95
96        for &v in &result.smoothed {
97            assert!((v - 5.0).abs() < 1e-10);
98        }
99        assert!((result.forecast - 5.0).abs() < 1e-10);
100    }
101
102    #[test]
103    fn test_ses_alpha_effect() {
104        // Higher alpha → more responsive to recent data
105        let data = [10.0, 20.0, 10.0];
106
107        let low = SimpleExponentialSmoothing::new(0.1).unwrap();
108        let high = SimpleExponentialSmoothing::new(0.9).unwrap();
109
110        let r_low = low.smooth(&data).unwrap();
111        let r_high = high.smooth(&data).unwrap();
112
113        // After step-up, high alpha should be closer to 20
114        assert!(r_high.smoothed[1] > r_low.smoothed[1]);
115    }
116
117    #[test]
118    fn test_ses_single_point() {
119        let ses = SimpleExponentialSmoothing::new(0.5).unwrap();
120        let result = ses.smooth(&[42.0]).unwrap();
121        assert_eq!(result.smoothed.len(), 1);
122        assert!((result.forecast - 42.0).abs() < 1e-10);
123    }
124
125    #[test]
126    fn test_ses_empty() {
127        let ses = SimpleExponentialSmoothing::new(0.5).unwrap();
128        assert!(ses.smooth(&[]).is_none());
129    }
130
131    #[test]
132    fn test_ses_invalid_alpha() {
133        assert!(SimpleExponentialSmoothing::new(0.0).is_none());
134        assert!(SimpleExponentialSmoothing::new(1.0).is_none());
135        assert!(SimpleExponentialSmoothing::new(-0.1).is_none());
136        assert!(SimpleExponentialSmoothing::new(f64::NAN).is_none());
137    }
138}