u_analytics/smoothing/
holt.rs1use super::forecast::{one_step_mse, residuals, valid_level, with_intervals, SmoothingForecast};
23
24#[derive(Debug, Clone)]
26pub struct HoltResult {
27 pub level: Vec<f64>,
29 pub trend: Vec<f64>,
31 pub fitted: Vec<f64>,
33 pub alpha: f64,
35 pub beta: f64,
37}
38
39impl HoltResult {
40 pub const FIRST_ERROR: usize = 2;
44
45 pub fn forecast(&self, h: usize) -> f64 {
47 let last_l = *self.level.last().expect("level must be non-empty");
48 let last_t = *self.trend.last().expect("trend must be non-empty");
49 last_l + h as f64 * last_t
50 }
51
52 pub fn residuals(&self, data: &[f64]) -> Vec<Option<f64>> {
54 residuals(data, &self.fitted, Self::FIRST_ERROR)
55 }
56
57 pub fn forecasts(
61 &self,
62 data: &[f64],
63 horizon: usize,
64 level: f64,
65 ) -> Option<Vec<SmoothingForecast>> {
66 if !valid_level(level) {
67 return None;
68 }
69 let (alpha, beta) = (self.alpha, self.beta);
70 let c = move |j: usize| alpha * (1.0 + j as f64 * beta);
71 Some(with_intervals(
72 horizon,
73 level,
74 one_step_mse(data, &self.fitted, Self::FIRST_ERROR),
75 Some(&c),
76 |h| self.forecast(h),
77 ))
78 }
79}
80
81pub struct HoltLinear {
83 alpha: f64,
84 beta: f64,
85}
86
87impl HoltLinear {
88 pub fn new(alpha: f64, beta: f64) -> Option<Self> {
96 if !alpha.is_finite() || alpha <= 0.0 || alpha >= 1.0 {
97 return None;
98 }
99 if !beta.is_finite() || beta <= 0.0 || beta >= 1.0 {
100 return None;
101 }
102 Some(Self { alpha, beta })
103 }
104
105 pub fn fit(data: &[f64], alpha: Option<f64>, beta: Option<f64>) -> Option<Self> {
113 if data.len() < 3 || data.iter().any(|v| !v.is_finite()) {
114 return None;
115 }
116 for c in [alpha, beta].into_iter().flatten() {
117 Self::new(c, c)?;
118 }
119 let c = super::estimate::least_squares(&[alpha, beta], |c| {
120 let r = Self {
121 alpha: c[0],
122 beta: c[1],
123 }
124 .smooth(data)
125 .expect("at least two values");
126 super::estimate::one_step_sse(data, &r.fitted, HoltResult::FIRST_ERROR)
127 });
128 Self::new(c[0], c[1])
129 }
130
131 pub fn alpha(&self) -> f64 {
133 self.alpha
134 }
135
136 pub fn beta(&self) -> f64 {
138 self.beta
139 }
140
141 pub fn smooth(&self, data: &[f64]) -> Option<HoltResult> {
146 if data.len() < 2 {
147 return None;
148 }
149
150 let n = data.len();
151 let mut level = Vec::with_capacity(n);
152 let mut trend = Vec::with_capacity(n);
153 let mut fitted = Vec::with_capacity(n);
154
155 let l0 = data[0];
157 let t0 = data[1] - data[0];
158 level.push(l0);
159 trend.push(t0);
160 fitted.push(l0); for i in 1..n {
163 let l_prev = level[i - 1];
164 let t_prev = trend[i - 1];
165
166 let l = self.alpha * data[i] + (1.0 - self.alpha) * (l_prev + t_prev);
167 let t = self.beta * (l - l_prev) + (1.0 - self.beta) * t_prev;
168
169 fitted.push(l_prev + t_prev); level.push(l);
171 trend.push(t);
172 }
173
174 Some(HoltResult {
175 level,
176 trend,
177 fitted,
178 alpha: self.alpha,
179 beta: self.beta,
180 })
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn test_holt_linear_trend() {
190 let holt = HoltLinear::new(0.5, 0.5).unwrap();
192 let data = [10.0, 12.0, 14.0, 16.0, 18.0];
193 let result = holt.smooth(&data).unwrap();
194
195 let f1 = result.forecast(1);
197 assert!((f1 - 20.0).abs() < 2.0, "forecast(1) = {f1}, expected ~20");
198 }
199
200 #[test]
201 fn test_holt_constant_series() {
202 let holt = HoltLinear::new(0.3, 0.3).unwrap();
203 let data = [5.0; 20];
204 let result = holt.smooth(&data).unwrap();
205
206 let last_trend = *result.trend.last().unwrap();
208 assert!(
209 last_trend.abs() < 0.1,
210 "trend should be near 0, got {last_trend}"
211 );
212 }
213
214 #[test]
215 fn test_holt_forecast_multi_step() {
216 let holt = HoltLinear::new(0.5, 0.5).unwrap();
217 let data = [10.0, 12.0, 14.0, 16.0, 18.0];
218 let result = holt.smooth(&data).unwrap();
219
220 let f1 = result.forecast(1);
221 let f3 = result.forecast(3);
222 let f5 = result.forecast(5);
223
224 assert!(f1 < f3);
226 assert!(f3 < f5);
227 }
228
229 #[test]
230 fn test_holt_minimum_data() {
231 let holt = HoltLinear::new(0.5, 0.5).unwrap();
232 let result = holt.smooth(&[10.0, 15.0]).unwrap();
233 assert_eq!(result.level.len(), 2);
234 assert_eq!(result.fitted.len(), 2);
235 }
236
237 #[test]
238 fn test_holt_insufficient_data() {
239 let holt = HoltLinear::new(0.5, 0.5).unwrap();
240 assert!(holt.smooth(&[10.0]).is_none());
241 assert!(holt.smooth(&[]).is_none());
242 }
243
244 #[test]
245 fn test_holt_invalid_params() {
246 assert!(HoltLinear::new(0.0, 0.5).is_none());
247 assert!(HoltLinear::new(1.0, 0.5).is_none());
248 assert!(HoltLinear::new(0.5, 0.0).is_none());
249 assert!(HoltLinear::new(0.5, 1.0).is_none());
250 assert!(HoltLinear::new(f64::NAN, 0.5).is_none());
251 }
252
253 #[test]
254 fn test_holt_fitted_values() {
255 let holt = HoltLinear::new(0.5, 0.5).unwrap();
256 let data = [10.0, 12.0, 14.0, 16.0];
257 let result = holt.smooth(&data).unwrap();
258
259 assert_eq!(result.fitted.len(), data.len());
260 assert!((result.fitted[0] - 10.0).abs() < 1e-10);
262 }
263}