u_analytics/smoothing/
ses.rs1use super::forecast::{
19 first_passage, one_step_mse, passage_arguments, residuals, valid_level, with_intervals,
20 FirstPassage, InnovationWeights, SmoothingForecast,
21};
22
23#[derive(Debug, Clone)]
25pub struct SesResult {
26 pub smoothed: Vec<f64>,
28 pub forecast: f64,
30 pub fitted: Vec<f64>,
33 pub alpha: f64,
35}
36
37impl SesResult {
38 pub const FIRST_ERROR: usize = 1;
41
42 pub fn residuals(&self, data: &[f64]) -> Vec<Option<f64>> {
44 residuals(data, &self.fitted, Self::FIRST_ERROR)
45 }
46
47 pub fn forecasts(
51 &self,
52 data: &[f64],
53 horizon: usize,
54 level: f64,
55 ) -> Option<Vec<SmoothingForecast>> {
56 if !valid_level(level) {
57 return None;
58 }
59 let weights = self.weights();
60 let c = move |j: usize| weights.c(j);
61 Some(with_intervals(
62 horizon,
63 level,
64 one_step_mse(data, &self.fitted, Self::FIRST_ERROR),
65 Some(&c),
66 |_| self.forecast,
67 ))
68 }
69 fn weights(&self) -> InnovationWeights {
71 InnovationWeights {
72 level: self.alpha,
73 slope: 0.0,
74 season: 0.0,
75 period: 0,
76 }
77 }
78
79 #[allow(clippy::too_many_arguments)]
87 pub fn first_passage(
88 &self,
89 data: &[f64],
90 horizon: usize,
91 threshold: f64,
92 quantiles: &[f64],
93 paths: usize,
94 seed: u64,
95 ) -> Option<FirstPassage> {
96 if !passage_arguments(horizon, threshold, quantiles, paths) {
97 return None;
98 }
99 let mse = one_step_mse(data, &self.fitted, Self::FIRST_ERROR)?;
100 let means = vec![self.forecast; horizon];
101 Some(first_passage(
102 &means,
103 mse,
104 self.weights(),
105 threshold,
106 quantiles,
107 paths,
108 seed,
109 ))
110 }
111}
112
113pub struct SimpleExponentialSmoothing {
115 alpha: f64,
116}
117
118impl SimpleExponentialSmoothing {
119 pub fn new(alpha: f64) -> Option<Self> {
126 if !alpha.is_finite() || alpha <= 0.0 || alpha >= 1.0 {
127 return None;
128 }
129 Some(Self { alpha })
130 }
131
132 pub fn fit(data: &[f64]) -> Option<Self> {
145 if data.len() < 2 || data.iter().any(|v| !v.is_finite()) {
146 return None;
147 }
148 let c = super::estimate::least_squares(&[None], |c| {
149 let r = Self { alpha: c[0] }
150 .smooth(data)
151 .expect("data is not empty");
152 super::estimate::one_step_sse(data, &r.fitted, SesResult::FIRST_ERROR)
153 });
154 Self::new(c[0])
155 }
156
157 pub fn alpha(&self) -> f64 {
159 self.alpha
160 }
161
162 pub fn smooth(&self, data: &[f64]) -> Option<SesResult> {
166 if data.is_empty() {
167 return None;
168 }
169
170 let mut smoothed = Vec::with_capacity(data.len());
171 let mut fitted = Vec::with_capacity(data.len());
172 let mut s = data[0];
173 fitted.push(s);
174 smoothed.push(s);
175
176 for &x in &data[1..] {
177 fitted.push(s);
178 s = self.alpha * x + (1.0 - self.alpha) * s;
179 smoothed.push(s);
180 }
181
182 let forecast = s;
183 Some(SesResult {
184 smoothed,
185 forecast,
186 fitted,
187 alpha: self.alpha,
188 })
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_ses_basic() {
198 let ses = SimpleExponentialSmoothing::new(0.3).unwrap();
199 let data = [10.0, 12.0, 13.0, 11.0, 14.0];
200 let result = ses.smooth(&data).unwrap();
201
202 assert_eq!(result.smoothed.len(), 5);
203 assert!((result.smoothed[0] - 10.0).abs() < 1e-10);
205 assert!((result.smoothed[1] - 10.6).abs() < 1e-10);
207 }
208
209 #[test]
210 fn test_ses_constant_series() {
211 let ses = SimpleExponentialSmoothing::new(0.5).unwrap();
212 let data = [5.0; 10];
213 let result = ses.smooth(&data).unwrap();
214
215 for &v in &result.smoothed {
216 assert!((v - 5.0).abs() < 1e-10);
217 }
218 assert!((result.forecast - 5.0).abs() < 1e-10);
219 }
220
221 #[test]
222 fn test_ses_alpha_effect() {
223 let data = [10.0, 20.0, 10.0];
225
226 let low = SimpleExponentialSmoothing::new(0.1).unwrap();
227 let high = SimpleExponentialSmoothing::new(0.9).unwrap();
228
229 let r_low = low.smooth(&data).unwrap();
230 let r_high = high.smooth(&data).unwrap();
231
232 assert!(r_high.smoothed[1] > r_low.smoothed[1]);
234 }
235
236 #[test]
237 fn test_ses_single_point() {
238 let ses = SimpleExponentialSmoothing::new(0.5).unwrap();
239 let result = ses.smooth(&[42.0]).unwrap();
240 assert_eq!(result.smoothed.len(), 1);
241 assert!((result.forecast - 42.0).abs() < 1e-10);
242 }
243
244 #[test]
245 fn test_ses_empty() {
246 let ses = SimpleExponentialSmoothing::new(0.5).unwrap();
247 assert!(ses.smooth(&[]).is_none());
248 }
249
250 #[test]
251 fn test_ses_invalid_alpha() {
252 assert!(SimpleExponentialSmoothing::new(0.0).is_none());
253 assert!(SimpleExponentialSmoothing::new(1.0).is_none());
254 assert!(SimpleExponentialSmoothing::new(-0.1).is_none());
255 assert!(SimpleExponentialSmoothing::new(f64::NAN).is_none());
256 }
257}