1use super::forecast::{
23 first_passage, one_step_mse, passage_arguments, residuals, valid_level, with_intervals,
24 FirstPassage, InnovationWeights, SmoothingForecast,
25};
26
27#[derive(Debug, Clone)]
29pub struct HoltResult {
30 pub level: Vec<f64>,
32 pub trend: Vec<f64>,
34 pub fitted: Vec<f64>,
36 pub alpha: f64,
38 pub beta: f64,
40}
41
42impl HoltResult {
43 pub const FIRST_ERROR: usize = 2;
47
48 pub fn forecast(&self, h: usize) -> f64 {
50 let last_l = *self.level.last().expect("level must be non-empty");
51 let last_t = *self.trend.last().expect("trend must be non-empty");
52 last_l + h as f64 * last_t
53 }
54
55 pub fn residuals(&self, data: &[f64]) -> Vec<Option<f64>> {
57 residuals(data, &self.fitted, Self::FIRST_ERROR)
58 }
59
60 pub fn forecasts(
64 &self,
65 data: &[f64],
66 horizon: usize,
67 level: f64,
68 ) -> Option<Vec<SmoothingForecast>> {
69 if !valid_level(level) {
70 return None;
71 }
72 let weights = self.weights();
73 let c = move |j: usize| weights.c(j);
74 Some(with_intervals(
75 horizon,
76 level,
77 one_step_mse(data, &self.fitted, Self::FIRST_ERROR),
78 Some(&c),
79 |h| self.forecast(h),
80 ))
81 }
82 fn weights(&self) -> InnovationWeights {
84 InnovationWeights {
85 level: self.alpha,
86 slope: self.alpha * self.beta,
87 season: 0.0,
88 period: 0,
89 }
90 }
91
92 #[allow(clippy::too_many_arguments)]
100 pub fn first_passage(
101 &self,
102 data: &[f64],
103 horizon: usize,
104 threshold: f64,
105 quantiles: &[f64],
106 paths: usize,
107 seed: u64,
108 ) -> Option<FirstPassage> {
109 if !passage_arguments(horizon, threshold, quantiles, paths) {
110 return None;
111 }
112 let mse = one_step_mse(data, &self.fitted, Self::FIRST_ERROR)?;
113 let means: Vec<f64> = (1..=horizon).map(|h| self.forecast(h)).collect();
114 let weights = self.weights();
115 Some(first_passage(
116 &means, mse, weights, threshold, quantiles, paths, seed,
117 ))
118 }
119}
120
121pub struct HoltLinear {
123 alpha: f64,
124 beta: f64,
125}
126
127impl HoltLinear {
128 pub fn new(alpha: f64, beta: f64) -> Option<Self> {
136 if !alpha.is_finite() || alpha <= 0.0 || alpha >= 1.0 {
137 return None;
138 }
139 if !beta.is_finite() || beta <= 0.0 || beta >= 1.0 {
140 return None;
141 }
142 Some(Self { alpha, beta })
143 }
144
145 pub fn fit(data: &[f64], alpha: Option<f64>, beta: Option<f64>) -> Option<Self> {
153 if data.len() < 3 || data.iter().any(|v| !v.is_finite()) {
154 return None;
155 }
156 for c in [alpha, beta].into_iter().flatten() {
157 Self::new(c, c)?;
158 }
159 let c = super::estimate::least_squares(&[alpha, beta], |c| {
160 let r = Self {
161 alpha: c[0],
162 beta: c[1],
163 }
164 .smooth(data)
165 .expect("at least two values");
166 super::estimate::one_step_sse(data, &r.fitted, HoltResult::FIRST_ERROR)
167 });
168 Self::new(c[0], c[1])
169 }
170
171 pub fn alpha(&self) -> f64 {
173 self.alpha
174 }
175
176 pub fn beta(&self) -> f64 {
178 self.beta
179 }
180
181 pub fn smooth(&self, data: &[f64]) -> Option<HoltResult> {
186 if data.len() < 2 {
187 return None;
188 }
189
190 let n = data.len();
191 let mut level = Vec::with_capacity(n);
192 let mut trend = Vec::with_capacity(n);
193 let mut fitted = Vec::with_capacity(n);
194
195 let l0 = data[0];
197 let t0 = data[1] - data[0];
198 level.push(l0);
199 trend.push(t0);
200 fitted.push(l0); for i in 1..n {
203 let l_prev = level[i - 1];
204 let t_prev = trend[i - 1];
205
206 let l = self.alpha * data[i] + (1.0 - self.alpha) * (l_prev + t_prev);
207 let t = self.beta * (l - l_prev) + (1.0 - self.beta) * t_prev;
208
209 fitted.push(l_prev + t_prev); level.push(l);
211 trend.push(t);
212 }
213
214 Some(HoltResult {
215 level,
216 trend,
217 fitted,
218 alpha: self.alpha,
219 beta: self.beta,
220 })
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn test_holt_linear_trend() {
230 let holt = HoltLinear::new(0.5, 0.5).unwrap();
232 let data = [10.0, 12.0, 14.0, 16.0, 18.0];
233 let result = holt.smooth(&data).unwrap();
234
235 let f1 = result.forecast(1);
237 assert!((f1 - 20.0).abs() < 2.0, "forecast(1) = {f1}, expected ~20");
238 }
239
240 #[test]
241 fn test_holt_constant_series() {
242 let holt = HoltLinear::new(0.3, 0.3).unwrap();
243 let data = [5.0; 20];
244 let result = holt.smooth(&data).unwrap();
245
246 let last_trend = *result.trend.last().unwrap();
248 assert!(
249 last_trend.abs() < 0.1,
250 "trend should be near 0, got {last_trend}"
251 );
252 }
253
254 #[test]
255 fn test_holt_forecast_multi_step() {
256 let holt = HoltLinear::new(0.5, 0.5).unwrap();
257 let data = [10.0, 12.0, 14.0, 16.0, 18.0];
258 let result = holt.smooth(&data).unwrap();
259
260 let f1 = result.forecast(1);
261 let f3 = result.forecast(3);
262 let f5 = result.forecast(5);
263
264 assert!(f1 < f3);
266 assert!(f3 < f5);
267 }
268
269 #[test]
270 fn test_holt_minimum_data() {
271 let holt = HoltLinear::new(0.5, 0.5).unwrap();
272 let result = holt.smooth(&[10.0, 15.0]).unwrap();
273 assert_eq!(result.level.len(), 2);
274 assert_eq!(result.fitted.len(), 2);
275 }
276
277 #[test]
278 fn test_holt_insufficient_data() {
279 let holt = HoltLinear::new(0.5, 0.5).unwrap();
280 assert!(holt.smooth(&[10.0]).is_none());
281 assert!(holt.smooth(&[]).is_none());
282 }
283
284 #[test]
285 fn test_holt_invalid_params() {
286 assert!(HoltLinear::new(0.0, 0.5).is_none());
287 assert!(HoltLinear::new(1.0, 0.5).is_none());
288 assert!(HoltLinear::new(0.5, 0.0).is_none());
289 assert!(HoltLinear::new(0.5, 1.0).is_none());
290 assert!(HoltLinear::new(f64::NAN, 0.5).is_none());
291 }
292
293 #[test]
294 fn test_holt_fitted_values() {
295 let holt = HoltLinear::new(0.5, 0.5).unwrap();
296 let data = [10.0, 12.0, 14.0, 16.0];
297 let result = holt.smooth(&data).unwrap();
298
299 assert_eq!(result.fitted.len(), data.len());
300 assert!((result.fitted[0] - 10.0).abs() < 1e-10);
302 }
303}