use super::forecast::{
first_passage, one_step_mse, passage_arguments, residuals, valid_level, with_intervals,
FirstPassage, InnovationWeights, SmoothingForecast,
};
#[derive(Debug, Clone)]
pub struct SesResult {
pub smoothed: Vec<f64>,
pub forecast: f64,
pub fitted: Vec<f64>,
pub alpha: f64,
}
impl SesResult {
pub const FIRST_ERROR: usize = 1;
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 weights = self.weights();
let c = move |j: usize| weights.c(j);
Some(with_intervals(
horizon,
level,
one_step_mse(data, &self.fitted, Self::FIRST_ERROR),
Some(&c),
|_| self.forecast,
))
}
fn weights(&self) -> InnovationWeights {
InnovationWeights {
level: self.alpha,
slope: 0.0,
season: 0.0,
period: 0,
}
}
#[allow(clippy::too_many_arguments)]
pub fn first_passage(
&self,
data: &[f64],
horizon: usize,
threshold: f64,
quantiles: &[f64],
paths: usize,
seed: u64,
) -> Option<FirstPassage> {
if !passage_arguments(horizon, threshold, quantiles, paths) {
return None;
}
let mse = one_step_mse(data, &self.fitted, Self::FIRST_ERROR)?;
let means = vec![self.forecast; horizon];
Some(first_passage(
&means,
mse,
self.weights(),
threshold,
quantiles,
paths,
seed,
))
}
}
pub struct SimpleExponentialSmoothing {
alpha: f64,
}
impl SimpleExponentialSmoothing {
pub fn new(alpha: f64) -> Option<Self> {
if !alpha.is_finite() || alpha <= 0.0 || alpha >= 1.0 {
return None;
}
Some(Self { alpha })
}
pub fn fit(data: &[f64]) -> Option<Self> {
if data.len() < 2 || data.iter().any(|v| !v.is_finite()) {
return None;
}
let c = super::estimate::least_squares(&[None], |c| {
let r = Self { alpha: c[0] }
.smooth(data)
.expect("data is not empty");
super::estimate::one_step_sse(data, &r.fitted, SesResult::FIRST_ERROR)
});
Self::new(c[0])
}
pub fn alpha(&self) -> f64 {
self.alpha
}
pub fn smooth(&self, data: &[f64]) -> Option<SesResult> {
if data.is_empty() {
return None;
}
let mut smoothed = Vec::with_capacity(data.len());
let mut fitted = Vec::with_capacity(data.len());
let mut s = data[0];
fitted.push(s);
smoothed.push(s);
for &x in &data[1..] {
fitted.push(s);
s = self.alpha * x + (1.0 - self.alpha) * s;
smoothed.push(s);
}
let forecast = s;
Some(SesResult {
smoothed,
forecast,
fitted,
alpha: self.alpha,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ses_basic() {
let ses = SimpleExponentialSmoothing::new(0.3).unwrap();
let data = [10.0, 12.0, 13.0, 11.0, 14.0];
let result = ses.smooth(&data).unwrap();
assert_eq!(result.smoothed.len(), 5);
assert!((result.smoothed[0] - 10.0).abs() < 1e-10);
assert!((result.smoothed[1] - 10.6).abs() < 1e-10);
}
#[test]
fn test_ses_constant_series() {
let ses = SimpleExponentialSmoothing::new(0.5).unwrap();
let data = [5.0; 10];
let result = ses.smooth(&data).unwrap();
for &v in &result.smoothed {
assert!((v - 5.0).abs() < 1e-10);
}
assert!((result.forecast - 5.0).abs() < 1e-10);
}
#[test]
fn test_ses_alpha_effect() {
let data = [10.0, 20.0, 10.0];
let low = SimpleExponentialSmoothing::new(0.1).unwrap();
let high = SimpleExponentialSmoothing::new(0.9).unwrap();
let r_low = low.smooth(&data).unwrap();
let r_high = high.smooth(&data).unwrap();
assert!(r_high.smoothed[1] > r_low.smoothed[1]);
}
#[test]
fn test_ses_single_point() {
let ses = SimpleExponentialSmoothing::new(0.5).unwrap();
let result = ses.smooth(&[42.0]).unwrap();
assert_eq!(result.smoothed.len(), 1);
assert!((result.forecast - 42.0).abs() < 1e-10);
}
#[test]
fn test_ses_empty() {
let ses = SimpleExponentialSmoothing::new(0.5).unwrap();
assert!(ses.smooth(&[]).is_none());
}
#[test]
fn test_ses_invalid_alpha() {
assert!(SimpleExponentialSmoothing::new(0.0).is_none());
assert!(SimpleExponentialSmoothing::new(1.0).is_none());
assert!(SimpleExponentialSmoothing::new(-0.1).is_none());
assert!(SimpleExponentialSmoothing::new(f64::NAN).is_none());
}
}