use super::forecast::{one_step_mse, residuals, valid_level, with_intervals, SmoothingForecast};
#[derive(Debug, Clone)]
pub struct HoltResult {
pub level: Vec<f64>,
pub trend: Vec<f64>,
pub fitted: Vec<f64>,
pub alpha: f64,
pub beta: f64,
}
impl HoltResult {
pub const FIRST_ERROR: usize = 2;
pub fn forecast(&self, h: usize) -> f64 {
let last_l = *self.level.last().expect("level must be non-empty");
let last_t = *self.trend.last().expect("trend must be non-empty");
last_l + h as f64 * last_t
}
pub fn residuals(&self, data: &[f64]) -> Vec<Option<f64>> {
residuals(data, &self.fitted, Self::FIRST_ERROR)
}
pub fn forecasts(
&self,
data: &[f64],
horizon: usize,
level: f64,
) -> Option<Vec<SmoothingForecast>> {
if !valid_level(level) {
return None;
}
let (alpha, beta) = (self.alpha, self.beta);
let c = move |j: usize| alpha * (1.0 + j as f64 * beta);
Some(with_intervals(
horizon,
level,
one_step_mse(data, &self.fitted, Self::FIRST_ERROR),
Some(&c),
|h| self.forecast(h),
))
}
}
pub struct HoltLinear {
alpha: f64,
beta: f64,
}
impl HoltLinear {
pub fn new(alpha: f64, beta: f64) -> Option<Self> {
if !alpha.is_finite() || alpha <= 0.0 || alpha >= 1.0 {
return None;
}
if !beta.is_finite() || beta <= 0.0 || beta >= 1.0 {
return None;
}
Some(Self { alpha, beta })
}
pub fn fit(data: &[f64], alpha: Option<f64>, beta: Option<f64>) -> Option<Self> {
if data.len() < 3 || data.iter().any(|v| !v.is_finite()) {
return None;
}
for c in [alpha, beta].into_iter().flatten() {
Self::new(c, c)?;
}
let c = super::estimate::least_squares(&[alpha, beta], |c| {
let r = Self {
alpha: c[0],
beta: c[1],
}
.smooth(data)
.expect("at least two values");
super::estimate::one_step_sse(data, &r.fitted, HoltResult::FIRST_ERROR)
});
Self::new(c[0], c[1])
}
pub fn alpha(&self) -> f64 {
self.alpha
}
pub fn beta(&self) -> f64 {
self.beta
}
pub fn smooth(&self, data: &[f64]) -> Option<HoltResult> {
if data.len() < 2 {
return None;
}
let n = data.len();
let mut level = Vec::with_capacity(n);
let mut trend = Vec::with_capacity(n);
let mut fitted = Vec::with_capacity(n);
let l0 = data[0];
let t0 = data[1] - data[0];
level.push(l0);
trend.push(t0);
fitted.push(l0);
for i in 1..n {
let l_prev = level[i - 1];
let t_prev = trend[i - 1];
let l = self.alpha * data[i] + (1.0 - self.alpha) * (l_prev + t_prev);
let t = self.beta * (l - l_prev) + (1.0 - self.beta) * t_prev;
fitted.push(l_prev + t_prev); level.push(l);
trend.push(t);
}
Some(HoltResult {
level,
trend,
fitted,
alpha: self.alpha,
beta: self.beta,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_holt_linear_trend() {
let holt = HoltLinear::new(0.5, 0.5).unwrap();
let data = [10.0, 12.0, 14.0, 16.0, 18.0];
let result = holt.smooth(&data).unwrap();
let f1 = result.forecast(1);
assert!((f1 - 20.0).abs() < 2.0, "forecast(1) = {f1}, expected ~20");
}
#[test]
fn test_holt_constant_series() {
let holt = HoltLinear::new(0.3, 0.3).unwrap();
let data = [5.0; 20];
let result = holt.smooth(&data).unwrap();
let last_trend = *result.trend.last().unwrap();
assert!(
last_trend.abs() < 0.1,
"trend should be near 0, got {last_trend}"
);
}
#[test]
fn test_holt_forecast_multi_step() {
let holt = HoltLinear::new(0.5, 0.5).unwrap();
let data = [10.0, 12.0, 14.0, 16.0, 18.0];
let result = holt.smooth(&data).unwrap();
let f1 = result.forecast(1);
let f3 = result.forecast(3);
let f5 = result.forecast(5);
assert!(f1 < f3);
assert!(f3 < f5);
}
#[test]
fn test_holt_minimum_data() {
let holt = HoltLinear::new(0.5, 0.5).unwrap();
let result = holt.smooth(&[10.0, 15.0]).unwrap();
assert_eq!(result.level.len(), 2);
assert_eq!(result.fitted.len(), 2);
}
#[test]
fn test_holt_insufficient_data() {
let holt = HoltLinear::new(0.5, 0.5).unwrap();
assert!(holt.smooth(&[10.0]).is_none());
assert!(holt.smooth(&[]).is_none());
}
#[test]
fn test_holt_invalid_params() {
assert!(HoltLinear::new(0.0, 0.5).is_none());
assert!(HoltLinear::new(1.0, 0.5).is_none());
assert!(HoltLinear::new(0.5, 0.0).is_none());
assert!(HoltLinear::new(0.5, 1.0).is_none());
assert!(HoltLinear::new(f64::NAN, 0.5).is_none());
}
#[test]
fn test_holt_fitted_values() {
let holt = HoltLinear::new(0.5, 0.5).unwrap();
let data = [10.0, 12.0, 14.0, 16.0];
let result = holt.smooth(&data).unwrap();
assert_eq!(result.fitted.len(), data.len());
assert!((result.fitted[0] - 10.0).abs() < 1e-10);
}
}