use crate::error::{ForecastError, Result};
use crate::utils::optimization::{nelder_mead, NelderMeadConfig};
#[derive(Debug, Clone)]
struct ThetaState {
level: f64,
b: f64,
}
#[derive(Debug, Clone)]
pub struct GlobalTheta {
theta: f64,
alpha: f64,
states: Vec<ThetaState>,
fitted: bool,
}
impl GlobalTheta {
pub fn new() -> Self {
Self {
theta: 2.0,
alpha: 0.5,
states: Vec::new(),
fitted: false,
}
}
pub fn with_theta(theta: f64) -> Self {
Self {
theta,
..Self::new()
}
}
pub fn alpha(&self) -> f64 {
self.alpha
}
pub fn fit(&mut self, all_series: &[Vec<f64>]) -> Result<()> {
if all_series.is_empty() {
return Err(ForecastError::InsufficientData {
needed: 1,
got: 0,
hint: Some("GlobalTheta requires at least one series".into()),
});
}
let result = nelder_mead(
|params| {
let alpha = params[0];
Self::total_sse(all_series, alpha)
},
&[0.5],
Some(&[(0.0001, 0.9999)]),
NelderMeadConfig {
max_iter: 500,
tolerance: 1e-8,
stagnation_window: 100,
..Default::default()
},
);
self.alpha = result.optimal_point[0].clamp(0.0001, 0.9999);
self.states = all_series
.iter()
.map(|values| {
let mut level = values[0];
for &y in values.iter().skip(1) {
level = self.alpha * y + (1.0 - self.alpha) * level;
}
let b = Self::ols_slope(values);
ThetaState { level, b }
})
.collect();
self.fitted = true;
Ok(())
}
pub fn predict(&self, horizon: usize) -> Vec<Vec<f64>> {
if !self.fitted {
return vec![];
}
self.states
.iter()
.map(|state| {
let mut forecasts = Vec::with_capacity(horizon);
for h in 1..=horizon {
let fc = state.level
+ (1.0 - 1.0 / self.theta) * state.b * (1.0 / self.alpha + h as f64 - 1.0);
forecasts.push(fc);
}
forecasts
})
.collect()
}
fn total_sse(all_series: &[Vec<f64>], alpha: f64) -> f64 {
if alpha <= 0.0001 || alpha >= 0.9999 {
return f64::MAX;
}
let mut total = 0.0;
for values in all_series {
if values.len() < 2 {
continue;
}
let mut level = values[0];
for &y in values.iter().skip(1) {
let error = y - level;
total += error * error;
level = alpha * y + (1.0 - alpha) * level;
}
}
total
}
fn ols_slope(values: &[f64]) -> f64 {
let n = values.len();
if n < 2 {
return 0.0;
}
let n_f = n as f64;
let sum_y: f64 = values.iter().sum();
let mean_y = sum_y / n_f;
let mean_x = (n_f - 1.0) / 2.0;
let mut ss_xy = 0.0;
let mut ss_xx = 0.0;
for (i, &y) in values.iter().enumerate() {
let dx = i as f64 - mean_x;
ss_xy += dx * (y - mean_y);
ss_xx += dx * dx;
}
if ss_xx.abs() < 1e-10 {
0.0
} else {
ss_xy / ss_xx
}
}
}
impl Default for GlobalTheta {
fn default() -> Self {
Self::new()
}
}