1#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct SmoothingForecast {
30 pub step: usize,
32 pub mean: f64,
34 pub lower: Option<f64>,
38 pub upper: Option<f64>,
40}
41
42pub(crate) fn one_step_mse(data: &[f64], fitted: &[f64], first: usize) -> Option<f64> {
45 let errors: Vec<f64> = data
46 .iter()
47 .zip(fitted)
48 .skip(first)
49 .map(|(x, f)| x - f)
50 .collect();
51 (!errors.is_empty()).then(|| errors.iter().map(|e| e * e).sum::<f64>() / errors.len() as f64)
52}
53
54pub(crate) fn residuals(data: &[f64], fitted: &[f64], first: usize) -> Vec<Option<f64>> {
57 data.iter()
58 .zip(fitted)
59 .enumerate()
60 .map(|(t, (x, f))| (t >= first).then(|| x - f))
61 .collect()
62}
63
64pub(crate) fn with_intervals(
67 horizon: usize,
68 level: f64,
69 mse: Option<f64>,
70 c: Option<&dyn Fn(usize) -> f64>,
71 mean: impl Fn(usize) -> f64,
72) -> Vec<SmoothingForecast> {
73 let z = u_numflow::special::inverse_normal_cdf(0.5 + level / 2.0);
74 let mut sum_c2 = 0.0;
75 (1..=horizon)
76 .map(|h| {
77 let m = mean(h);
78 let half = match (mse, c) {
79 (Some(mse), Some(c)) => {
80 if h > 1 {
81 sum_c2 += c(h - 1).powi(2);
82 }
83 Some(z * (mse * (1.0 + sum_c2)).sqrt())
84 }
85 _ => None,
86 };
87 SmoothingForecast {
88 step: h,
89 mean: m,
90 lower: half.map(|w| m - w),
91 upper: half.map(|w| m + w),
92 }
93 })
94 .collect()
95}
96
97pub(crate) fn valid_level(level: f64) -> bool {
99 level > 0.0 && level < 1.0
100}
101
102#[cfg(test)]
109#[allow(clippy::approx_constant)] mod statsmodels_reference {
111 use super::*;
112 use crate::smoothing::{
113 HoltLinear, HoltResult, HoltWinters, Seasonality, SesResult, SimpleExponentialSmoothing,
114 };
115
116 const SES_DATA: &[f64] = &[
118 49.695, 48.755, 48.385, 48.678, 47.146, 48.813, 49.806, 49.983, 50.957, 49.731, 48.969,
119 49.761, 48.5, 49.557, 50.796, 51.74, 52.288, 55.501, 55.645, 55.541, 55.655, 55.734,
120 56.497, 55.544, 53.348, 52.352, 53.479, 52.5, 53.481, 55.59,
121 ];
122 const SES_FITTED: &[f64] = &[
123 49.695,
124 49.366,
125 49.02265,
126 48.9020225,
127 48.287414625000004,
128 48.47136950625,
129 48.9384901790625,
130 49.30406861639062,
131 49.8825946006539,
132 49.82953649042504,
133 49.52834871877628,
134 49.609776667204585,
135 49.22135483368298,
136 49.338830641893935,
137 49.848839917231054,
138 50.51074594620019,
139 51.13278486503012,
140 52.66166016226957,
141 53.70582910547522,
142 54.34813891855889,
143 54.80554029706328,
144 55.130501193091135,
145 55.60877577550924,
146 55.586104254081,
147 54.80276776515265,
148 53.94499904734923,
149 53.781899380777,
150 53.33323459750505,
151 53.38495248837828,
152 ];
153 const SES_MEAN: &[f64] = &[
154 54.15671911744588,
155 54.15671911744588,
156 54.15671911744588,
157 54.15671911744588,
158 54.15671911744588,
159 54.15671911744588,
160 ];
161 const SES_LOWER: &[f64] = &[
162 51.50271152380141,
163 51.344848484798455,
164 51.195388909183464,
165 51.053118506866284,
166 50.917089987805795,
167 50.786547453385566,
168 ];
169 const SES_UPPER: &[f64] = &[
170 56.81072671109035,
171 56.96858975009331,
172 57.1180493257083,
173 57.26031972802548,
174 57.39634824708597,
175 57.5268907815062,
176 ];
177 const HOLT_DATA: &[f64] = &[
178 18.624, 22.028, 22.022, 22.77, 23.168, 25.438, 23.856, 26.135, 25.839, 25.885, 27.652,
179 29.208, 30.853, 32.14, 31.311, 31.097, 32.108, 31.913, 36.539, 34.939, 34.931, 36.448,
180 37.208, 36.854, 38.229, 38.696, 41.158, 43.842, 42.405, 41.971,
181 ];
182 const HOLT_FITTED: &[f64] = &[
183 25.432,
184 27.199199999999998,
185 28.204383999999997,
186 28.563783679999997,
187 29.4373607936,
188 28.882198198272,
189 29.24092478521344,
190 29.065606754561227,
191 28.724367395805,
192 29.140634388886866,
193 30.018183833625034,
194 31.269498793777935,
195 32.60472786636744,
196 32.97076708061175,
197 32.954889242709385,
198 33.28201140055122,
199 33.29076378321223,
200 35.406274110151855,
201 35.99819837750347,
202 36.26477706771417,
203 37.04618211642346,
204 37.83197057633515,
205 38.08360600617535,
206 38.796218783585445,
207 39.402568947344676,
208 40.89161352981263,
209 43.094671196908386,
210 43.78653210141317,
211 ];
212 const HOLT_MEAN: &[f64] = &[
213 43.88280607600299,
214 44.70529289115808,
215 45.52777970631317,
216 46.35026652146826,
217 47.172753336623344,
218 47.99524015177843,
219 ];
220 const HOLT_LOWER: &[f64] = &[
221 39.881805953904774,
222 40.26724764211682,
223 40.55622511518835,
224 40.758018971303315,
225 40.88214280288195,
226 40.93713118743697,
227 ];
228 const HOLT_UPPER: &[f64] = &[
229 47.88380619810121,
230 49.14333814019934,
231 50.49933429743799,
232 51.9425140716332,
233 53.46336387036474,
234 55.0533491161199,
235 ];
236 const HW_DATA: &[f64] = &[
237 107.195, 99.797, 99.111, 104.953, 111.536, 103.86, 105.869, 111.842, 117.337, 112.456,
238 110.451, 115.115, 124.234, 116.183, 117.719, 123.434, 127.431, 122.666, 123.802, 130.548,
239 135.536, 127.843, 129.797, 133.84, 142.645, 133.886, 134.436, 141.34, 148.881, 140.176,
240 141.191, 145.134,
241 ];
242 const HW_FITTED: &[f64] = &[
243 108.57318749999999,
244 103.53110312499999,
245 104.42071096874999,
246 112.21758513031247,
247 116.37452167699682,
248 110.342324187491,
249 112.08772508093094,
250 118.67183234829378,
251 122.04503373510259,
252 116.55551472827399,
253 116.797425126349,
254 123.78843943015733,
255 129.19938105911905,
256 122.02718587096355,
257 122.78208766197088,
258 129.56537788125988,
259 135.15618721562316,
260 129.1403441795491,
261 129.4136407564024,
262 136.01289275800133,
263 140.4703440485413,
264 134.67208484162995,
265 135.38085344677242,
266 141.08242228813216,
267 147.0508746050361,
268 140.6418295369387,
269 141.43920083128222,
270 147.6014010922094,
271 ];
272 const HW_MEAN: &[f64] = &[
273 152.98701643046797,
274 145.53169868698552,
275 146.42543580819583,
276 152.19971424007915,
277 158.7573450971372,
278 151.30202735365475,
279 ];
280 const HW_LOWER: &[f64] = &[
281 150.4614216897758,
282 142.8721382966387,
283 143.61475408277198,
284 149.22143937008624,
285 155.42143213962436,
286 147.77782707686427,
287 ];
288 const HW_UPPER: &[f64] = &[
289 155.51261117116016,
290 148.19125907733235,
291 149.23611753361968,
292 155.17798911007205,
293 162.09325805465005,
294 154.82622763044523,
295 ];
296
297 fn close(got: f64, want: f64, what: &str) {
298 let tol = 1e-9 * want.abs().max(1.0);
299 assert!((got - want).abs() <= tol, "{what}: {got} vs {want}");
300 }
301
302 fn compare(
303 fitted: &[f64],
304 first: usize,
305 forecasts: &[SmoothingForecast],
306 want_fitted: &[f64],
307 want: [&[f64]; 3],
308 ) {
309 assert_eq!(fitted.len() - first, want_fitted.len());
310 for (got, want) in fitted[first..].iter().zip(want_fitted) {
311 close(*got, *want, "fitted");
312 }
313 for (i, f) in forecasts.iter().enumerate() {
314 close(f.mean, want[0][i], "mean");
315 close(f.lower.expect("interval"), want[1][i], "lower");
316 close(f.upper.expect("interval"), want[2][i], "upper");
317 }
318 }
319
320 #[test]
321 fn ses_matches_ets_ann() {
322 let r = SimpleExponentialSmoothing::new(0.35)
323 .unwrap()
324 .smooth(SES_DATA)
325 .unwrap();
326 let f = r.forecasts(SES_DATA, 6, 0.9).unwrap();
327 compare(
328 &r.fitted,
329 SesResult::FIRST_ERROR,
330 &f,
331 SES_FITTED,
332 [SES_MEAN, SES_LOWER, SES_UPPER],
333 );
334 }
335
336 #[test]
337 fn holt_matches_ets_aan() {
338 let r = HoltLinear::new(0.4, 0.2)
339 .unwrap()
340 .smooth(HOLT_DATA)
341 .unwrap();
342 let f = r.forecasts(HOLT_DATA, 6, 0.9).unwrap();
343 compare(
344 &r.fitted,
345 HoltResult::FIRST_ERROR,
346 &f,
347 HOLT_FITTED,
348 [HOLT_MEAN, HOLT_LOWER, HOLT_UPPER],
349 );
350 }
351
352 #[test]
353 fn additive_holt_winters_matches_ets_aaa() {
354 let r = HoltWinters::new(0.3, 0.1, 0.25, 4, Seasonality::Additive)
355 .unwrap()
356 .smooth(HW_DATA)
357 .unwrap();
358 let f = r.forecasts(HW_DATA, 6, 0.9).unwrap();
359 compare(
360 &r.fitted,
361 r.first_error(),
362 &f,
363 HW_FITTED,
364 [HW_MEAN, HW_LOWER, HW_UPPER],
365 );
366 }
367}