u_analytics/smoothing/
ses.rs1use super::forecast::{one_step_mse, residuals, valid_level, with_intervals, SmoothingForecast};
19
20#[derive(Debug, Clone)]
22pub struct SesResult {
23 pub smoothed: Vec<f64>,
25 pub forecast: f64,
27 pub fitted: Vec<f64>,
30 pub alpha: f64,
32}
33
34impl SesResult {
35 pub const FIRST_ERROR: usize = 1;
38
39 pub fn residuals(&self, data: &[f64]) -> Vec<Option<f64>> {
41 residuals(data, &self.fitted, Self::FIRST_ERROR)
42 }
43
44 pub fn forecasts(
48 &self,
49 data: &[f64],
50 horizon: usize,
51 level: f64,
52 ) -> Option<Vec<SmoothingForecast>> {
53 if !valid_level(level) {
54 return None;
55 }
56 let alpha = self.alpha;
57 let c = move |_: usize| alpha;
58 Some(with_intervals(
59 horizon,
60 level,
61 one_step_mse(data, &self.fitted, Self::FIRST_ERROR),
62 Some(&c),
63 |_| self.forecast,
64 ))
65 }
66}
67
68pub struct SimpleExponentialSmoothing {
70 alpha: f64,
71}
72
73impl SimpleExponentialSmoothing {
74 pub fn new(alpha: f64) -> Option<Self> {
81 if !alpha.is_finite() || alpha <= 0.0 || alpha >= 1.0 {
82 return None;
83 }
84 Some(Self { alpha })
85 }
86
87 pub fn fit(data: &[f64]) -> Option<Self> {
100 if data.len() < 2 || data.iter().any(|v| !v.is_finite()) {
101 return None;
102 }
103 let c = super::estimate::least_squares(&[None], |c| {
104 let r = Self { alpha: c[0] }
105 .smooth(data)
106 .expect("data is not empty");
107 super::estimate::one_step_sse(data, &r.fitted, SesResult::FIRST_ERROR)
108 });
109 Self::new(c[0])
110 }
111
112 pub fn alpha(&self) -> f64 {
114 self.alpha
115 }
116
117 pub fn smooth(&self, data: &[f64]) -> Option<SesResult> {
121 if data.is_empty() {
122 return None;
123 }
124
125 let mut smoothed = Vec::with_capacity(data.len());
126 let mut fitted = Vec::with_capacity(data.len());
127 let mut s = data[0];
128 fitted.push(s);
129 smoothed.push(s);
130
131 for &x in &data[1..] {
132 fitted.push(s);
133 s = self.alpha * x + (1.0 - self.alpha) * s;
134 smoothed.push(s);
135 }
136
137 let forecast = s;
138 Some(SesResult {
139 smoothed,
140 forecast,
141 fitted,
142 alpha: self.alpha,
143 })
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_ses_basic() {
153 let ses = SimpleExponentialSmoothing::new(0.3).unwrap();
154 let data = [10.0, 12.0, 13.0, 11.0, 14.0];
155 let result = ses.smooth(&data).unwrap();
156
157 assert_eq!(result.smoothed.len(), 5);
158 assert!((result.smoothed[0] - 10.0).abs() < 1e-10);
160 assert!((result.smoothed[1] - 10.6).abs() < 1e-10);
162 }
163
164 #[test]
165 fn test_ses_constant_series() {
166 let ses = SimpleExponentialSmoothing::new(0.5).unwrap();
167 let data = [5.0; 10];
168 let result = ses.smooth(&data).unwrap();
169
170 for &v in &result.smoothed {
171 assert!((v - 5.0).abs() < 1e-10);
172 }
173 assert!((result.forecast - 5.0).abs() < 1e-10);
174 }
175
176 #[test]
177 fn test_ses_alpha_effect() {
178 let data = [10.0, 20.0, 10.0];
180
181 let low = SimpleExponentialSmoothing::new(0.1).unwrap();
182 let high = SimpleExponentialSmoothing::new(0.9).unwrap();
183
184 let r_low = low.smooth(&data).unwrap();
185 let r_high = high.smooth(&data).unwrap();
186
187 assert!(r_high.smoothed[1] > r_low.smoothed[1]);
189 }
190
191 #[test]
192 fn test_ses_single_point() {
193 let ses = SimpleExponentialSmoothing::new(0.5).unwrap();
194 let result = ses.smooth(&[42.0]).unwrap();
195 assert_eq!(result.smoothed.len(), 1);
196 assert!((result.forecast - 42.0).abs() < 1e-10);
197 }
198
199 #[test]
200 fn test_ses_empty() {
201 let ses = SimpleExponentialSmoothing::new(0.5).unwrap();
202 assert!(ses.smooth(&[]).is_none());
203 }
204
205 #[test]
206 fn test_ses_invalid_alpha() {
207 assert!(SimpleExponentialSmoothing::new(0.0).is_none());
208 assert!(SimpleExponentialSmoothing::new(1.0).is_none());
209 assert!(SimpleExponentialSmoothing::new(-0.1).is_none());
210 assert!(SimpleExponentialSmoothing::new(f64::NAN).is_none());
211 }
212}