u_analytics/smoothing/
holt.rs1#[derive(Debug, Clone)]
24pub struct HoltResult {
25 pub level: Vec<f64>,
27 pub trend: Vec<f64>,
29 pub fitted: Vec<f64>,
31}
32
33impl HoltResult {
34 pub fn forecast(&self, h: usize) -> f64 {
36 let last_l = *self.level.last().expect("level must be non-empty");
37 let last_t = *self.trend.last().expect("trend must be non-empty");
38 last_l + h as f64 * last_t
39 }
40}
41
42pub struct HoltLinear {
44 alpha: f64,
45 beta: f64,
46}
47
48impl HoltLinear {
49 pub fn new(alpha: f64, beta: f64) -> Option<Self> {
57 if !alpha.is_finite() || alpha <= 0.0 || alpha >= 1.0 {
58 return None;
59 }
60 if !beta.is_finite() || beta <= 0.0 || beta >= 1.0 {
61 return None;
62 }
63 Some(Self { alpha, beta })
64 }
65
66 pub fn alpha(&self) -> f64 {
68 self.alpha
69 }
70
71 pub fn beta(&self) -> f64 {
73 self.beta
74 }
75
76 pub fn smooth(&self, data: &[f64]) -> Option<HoltResult> {
81 if data.len() < 2 {
82 return None;
83 }
84
85 let n = data.len();
86 let mut level = Vec::with_capacity(n);
87 let mut trend = Vec::with_capacity(n);
88 let mut fitted = Vec::with_capacity(n);
89
90 let l0 = data[0];
92 let t0 = data[1] - data[0];
93 level.push(l0);
94 trend.push(t0);
95 fitted.push(l0); for i in 1..n {
98 let l_prev = level[i - 1];
99 let t_prev = trend[i - 1];
100
101 let l = self.alpha * data[i] + (1.0 - self.alpha) * (l_prev + t_prev);
102 let t = self.beta * (l - l_prev) + (1.0 - self.beta) * t_prev;
103
104 fitted.push(l_prev + t_prev); level.push(l);
106 trend.push(t);
107 }
108
109 Some(HoltResult {
110 level,
111 trend,
112 fitted,
113 })
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn test_holt_linear_trend() {
123 let holt = HoltLinear::new(0.5, 0.5).unwrap();
125 let data = [10.0, 12.0, 14.0, 16.0, 18.0];
126 let result = holt.smooth(&data).unwrap();
127
128 let f1 = result.forecast(1);
130 assert!((f1 - 20.0).abs() < 2.0, "forecast(1) = {f1}, expected ~20");
131 }
132
133 #[test]
134 fn test_holt_constant_series() {
135 let holt = HoltLinear::new(0.3, 0.3).unwrap();
136 let data = [5.0; 20];
137 let result = holt.smooth(&data).unwrap();
138
139 let last_trend = *result.trend.last().unwrap();
141 assert!(
142 last_trend.abs() < 0.1,
143 "trend should be near 0, got {last_trend}"
144 );
145 }
146
147 #[test]
148 fn test_holt_forecast_multi_step() {
149 let holt = HoltLinear::new(0.5, 0.5).unwrap();
150 let data = [10.0, 12.0, 14.0, 16.0, 18.0];
151 let result = holt.smooth(&data).unwrap();
152
153 let f1 = result.forecast(1);
154 let f3 = result.forecast(3);
155 let f5 = result.forecast(5);
156
157 assert!(f1 < f3);
159 assert!(f3 < f5);
160 }
161
162 #[test]
163 fn test_holt_minimum_data() {
164 let holt = HoltLinear::new(0.5, 0.5).unwrap();
165 let result = holt.smooth(&[10.0, 15.0]).unwrap();
166 assert_eq!(result.level.len(), 2);
167 assert_eq!(result.fitted.len(), 2);
168 }
169
170 #[test]
171 fn test_holt_insufficient_data() {
172 let holt = HoltLinear::new(0.5, 0.5).unwrap();
173 assert!(holt.smooth(&[10.0]).is_none());
174 assert!(holt.smooth(&[]).is_none());
175 }
176
177 #[test]
178 fn test_holt_invalid_params() {
179 assert!(HoltLinear::new(0.0, 0.5).is_none());
180 assert!(HoltLinear::new(1.0, 0.5).is_none());
181 assert!(HoltLinear::new(0.5, 0.0).is_none());
182 assert!(HoltLinear::new(0.5, 1.0).is_none());
183 assert!(HoltLinear::new(f64::NAN, 0.5).is_none());
184 }
185
186 #[test]
187 fn test_holt_fitted_values() {
188 let holt = HoltLinear::new(0.5, 0.5).unwrap();
189 let data = [10.0, 12.0, 14.0, 16.0];
190 let result = holt.smooth(&data).unwrap();
191
192 assert_eq!(result.fitted.len(), data.len());
193 assert!((result.fitted[0] - 10.0).abs() < 1e-10);
195 }
196}