use super::auto_ets::ModelPool;
use super::ets::{ETSSpec, ErrorType, SeasonalType, TrendType};
use crate::error::{ForecastError, Result};
use crate::utils::optimization::{nelder_mead, NelderMeadConfig};
#[derive(Debug, Clone)]
pub struct GlobalETS {
spec: ETSSpec,
period: usize,
alpha: f64,
beta: Option<f64>,
gamma: Option<f64>,
phi: Option<f64>,
states: Vec<SeriesState>,
final_states: Vec<SeriesState>,
fitted: bool,
}
#[derive(Debug, Clone)]
struct SeriesState {
level: f64,
trend: f64,
seasonals: Vec<f64>,
}
impl GlobalETS {
pub fn new(spec: ETSSpec, period: usize) -> Self {
Self {
spec,
period,
alpha: 0.3,
beta: if spec.has_trend() { Some(0.1) } else { None },
gamma: if spec.has_seasonal() { Some(0.1) } else { None },
phi: if spec.is_damped() { Some(0.98) } else { None },
states: Vec::new(),
final_states: Vec::new(),
fitted: false,
}
}
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("GlobalETS requires at least one series".into()),
});
}
let n = all_series[0].len();
let _n_series = all_series.len();
let period = self.period;
let start_idx = if self.spec.has_seasonal() { period } else { 0 };
if n <= start_idx + 2 {
return Err(ForecastError::InsufficientData {
needed: start_idx + 3,
got: n,
hint: Some("Series too short for this ETS spec".into()),
});
}
self.states = all_series
.iter()
.map(|values| Self::initialize_state(values, self.spec, period))
.collect();
let has_trend = self.spec.has_trend();
let has_seasonal = self.spec.has_seasonal();
let is_damped = self.spec.is_damped();
let mut params = vec![0.3]; let mut bounds = vec![(0.0001, 0.9999)];
if has_trend {
params.push(0.1); bounds.push((0.0001, 0.9999));
}
if has_seasonal {
params.push(0.1); bounds.push((0.0001, 0.9999));
}
if is_damped {
params.push(0.98); bounds.push((0.8, 0.98));
}
let states_ref = &self.states;
let spec = self.spec;
let result = nelder_mead(
|p| {
let alpha = p[0];
let mut idx = 1;
let beta = if has_trend {
idx += 1;
Some(p[idx - 1])
} else {
None
};
let gamma = if has_seasonal {
idx += 1;
Some(p[idx - 1])
} else {
None
};
let phi = if is_damped { Some(p[idx]) } else { None };
Self::total_nll(
all_series, states_ref, spec, period, alpha, beta, gamma, phi,
)
},
¶ms,
Some(&bounds),
NelderMeadConfig {
max_iter: 500,
tolerance: 1e-8,
stagnation_window: 100,
..Default::default()
},
);
self.alpha = result.optimal_point[0].clamp(0.0001, 0.9999);
let mut idx = 1;
if has_trend {
self.beta = Some(result.optimal_point[idx].clamp(0.0001, 0.9999));
idx += 1;
}
if has_seasonal {
self.gamma = Some(result.optimal_point[idx].clamp(0.0001, 0.9999));
idx += 1;
}
if is_damped {
self.phi = Some(result.optimal_point[idx].clamp(0.8, 0.98));
}
self.final_states = all_series
.iter()
.zip(self.states.iter())
.map(|(values, init)| {
Self::run_states(
values, init, spec, period, self.alpha, self.beta, self.gamma, self.phi,
)
})
.collect();
self.fitted = true;
Ok(())
}
pub fn params(&self) -> (f64, Option<f64>, Option<f64>, Option<f64>) {
(self.alpha, self.beta, self.gamma, self.phi)
}
pub fn predict(&self, horizon: usize) -> Vec<Vec<f64>> {
if !self.fitted {
return vec![];
}
self.final_states
.iter()
.map(|state| {
Self::forecast_from_state(state, self.spec, self.period, self.phi, horizon)
})
.collect()
}
fn total_nll(
all_series: &[Vec<f64>],
states: &[SeriesState],
spec: ETSSpec,
period: usize,
alpha: f64,
beta: Option<f64>,
gamma: Option<f64>,
phi: Option<f64>,
) -> f64 {
let mut total = 0.0;
for (values, init) in all_series.iter().zip(states.iter()) {
let nll = Self::series_nll(values, init, spec, period, alpha, beta, gamma, phi);
if !nll.is_finite() {
return f64::MAX;
}
total += nll;
}
total
}
fn series_nll(
values: &[f64],
init: &SeriesState,
spec: ETSSpec,
period: usize,
alpha: f64,
beta: Option<f64>,
gamma: Option<f64>,
phi: Option<f64>,
) -> f64 {
let n = values.len();
let start_idx = if spec.has_seasonal() { period } else { 0 };
if n <= start_idx + 1 {
return f64::MAX;
}
let mut level = init.level;
let mut trend = init.trend;
let mut seasonal_buf = init.seasonals.clone();
let phi = phi.unwrap_or(1.0);
let beta = beta.unwrap_or(0.0);
let gamma = gamma.unwrap_or(0.0);
let is_mult_error = spec.error == ErrorType::Multiplicative;
let mut sum_sq = 0.0_f64;
let mut sum_log = 0.0_f64;
let mut count = 0_usize;
for (t, &y) in values.iter().enumerate().skip(start_idx) {
let fc = match (spec.trend, spec.seasonal) {
(TrendType::None, SeasonalType::None) => level,
(TrendType::None, SeasonalType::Additive) => level + seasonal_buf[t % period],
(TrendType::None, SeasonalType::Multiplicative) => level * seasonal_buf[t % period],
(TrendType::Additive, SeasonalType::None) => level + trend,
(TrendType::Additive, SeasonalType::Additive) => {
level + trend + seasonal_buf[t % period]
}
(TrendType::Additive, SeasonalType::Multiplicative) => {
(level + trend) * seasonal_buf[t % period]
}
(TrendType::AdditiveDamped, SeasonalType::None) => level + phi * trend,
(TrendType::AdditiveDamped, SeasonalType::Additive) => {
level + phi * trend + seasonal_buf[t % period]
}
(TrendType::AdditiveDamped, SeasonalType::Multiplicative) => {
(level + phi * trend) * seasonal_buf[t % period]
}
};
if !fc.is_finite() {
return f64::MAX;
}
let err = y - fc;
let se = if is_mult_error && fc.abs() > 1e-10 {
err / fc
} else {
err
};
sum_sq += se * se;
if !sum_sq.is_finite() {
return f64::MAX;
}
if is_mult_error {
sum_log += y.abs().ln();
}
count += 1;
let level_prev = level;
let s_idx = t % period;
let s = if spec.has_seasonal() {
seasonal_buf[s_idx]
} else {
0.0
};
match (spec.trend, spec.seasonal) {
(TrendType::None, SeasonalType::None) => {
level = alpha * y + (1.0 - alpha) * level;
}
(TrendType::None, SeasonalType::Additive) => {
level = alpha * (y - s) + (1.0 - alpha) * level;
seasonal_buf[s_idx] = gamma * (y - level) + (1.0 - gamma) * s;
}
(TrendType::None, SeasonalType::Multiplicative) => {
let y_des = if s.abs() > 1e-10 { y / s } else { y };
level = alpha * y_des + (1.0 - alpha) * level;
seasonal_buf[s_idx] = if level.abs() > 1e-10 {
gamma * (y / level) + (1.0 - gamma) * s
} else {
s
};
}
(TrendType::Additive, SeasonalType::None) => {
level = alpha * y + (1.0 - alpha) * (level_prev + trend);
trend = beta * (level - level_prev) + (1.0 - beta) * trend;
}
(TrendType::Additive, SeasonalType::Additive) => {
level = alpha * (y - s) + (1.0 - alpha) * (level_prev + trend);
trend = beta * (level - level_prev) + (1.0 - beta) * trend;
seasonal_buf[s_idx] = gamma * (y - level) + (1.0 - gamma) * s;
}
(TrendType::Additive, SeasonalType::Multiplicative) => {
let y_des = if s.abs() > 1e-10 { y / s } else { y };
level = alpha * y_des + (1.0 - alpha) * (level_prev + trend);
trend = beta * (level - level_prev) + (1.0 - beta) * trend;
seasonal_buf[s_idx] = if level.abs() > 1e-10 {
gamma * (y / level) + (1.0 - gamma) * s
} else {
s
};
}
(TrendType::AdditiveDamped, SeasonalType::None) => {
level = alpha * y + (1.0 - alpha) * (level_prev + phi * trend);
trend = beta * (level - level_prev) + (1.0 - beta) * phi * trend;
}
(TrendType::AdditiveDamped, SeasonalType::Additive) => {
level = alpha * (y - s) + (1.0 - alpha) * (level_prev + phi * trend);
trend = beta * (level - level_prev) + (1.0 - beta) * phi * trend;
seasonal_buf[s_idx] = gamma * (y - level) + (1.0 - gamma) * s;
}
(TrendType::AdditiveDamped, SeasonalType::Multiplicative) => {
let y_des = if s.abs() > 1e-10 { y / s } else { y };
level = alpha * y_des + (1.0 - alpha) * (level_prev + phi * trend);
trend = beta * (level - level_prev) + (1.0 - beta) * phi * trend;
seasonal_buf[s_idx] = if level.abs() > 1e-10 {
gamma * (y / level) + (1.0 - gamma) * s
} else {
s
};
}
}
}
if count == 0 {
return f64::MAX;
}
let sigma2 = sum_sq / count as f64;
if sigma2 <= 0.0 || !sigma2.is_finite() {
return f64::MAX;
}
let nll = if is_mult_error {
-0.5 * count as f64 * (1.0 + sigma2.ln() + (2.0 * std::f64::consts::PI).ln()) - sum_log
} else {
-0.5 * count as f64 * (1.0 + sigma2.ln() + (2.0 * std::f64::consts::PI).ln())
};
-nll }
fn run_states(
values: &[f64],
init: &SeriesState,
spec: ETSSpec,
period: usize,
alpha: f64,
beta: Option<f64>,
gamma: Option<f64>,
phi: Option<f64>,
) -> SeriesState {
let start_idx = if spec.has_seasonal() { period } else { 0 };
let mut level = init.level;
let mut trend = init.trend;
let mut seasonal_buf = init.seasonals.clone();
let phi_val = phi.unwrap_or(1.0);
let beta_val = beta.unwrap_or(0.0);
let gamma_val = gamma.unwrap_or(0.0);
for (t, &y) in values.iter().enumerate().skip(start_idx) {
let level_prev = level;
let s_idx = t % period;
let s = if spec.has_seasonal() {
seasonal_buf[s_idx]
} else {
0.0
};
match (spec.trend, spec.seasonal) {
(TrendType::None, SeasonalType::None) => {
level = alpha * y + (1.0 - alpha) * level;
}
(TrendType::None, SeasonalType::Additive) => {
level = alpha * (y - s) + (1.0 - alpha) * level;
seasonal_buf[s_idx] = gamma_val * (y - level) + (1.0 - gamma_val) * s;
}
(TrendType::None, SeasonalType::Multiplicative) => {
let y_des = if s.abs() > 1e-10 { y / s } else { y };
level = alpha * y_des + (1.0 - alpha) * level;
seasonal_buf[s_idx] = if level.abs() > 1e-10 {
gamma_val * (y / level) + (1.0 - gamma_val) * s
} else {
s
};
}
(TrendType::Additive, SeasonalType::None) => {
level = alpha * y + (1.0 - alpha) * (level_prev + trend);
trend = beta_val * (level - level_prev) + (1.0 - beta_val) * trend;
}
(TrendType::Additive, SeasonalType::Additive) => {
level = alpha * (y - s) + (1.0 - alpha) * (level_prev + trend);
trend = beta_val * (level - level_prev) + (1.0 - beta_val) * trend;
seasonal_buf[s_idx] = gamma_val * (y - level) + (1.0 - gamma_val) * s;
}
(TrendType::Additive, SeasonalType::Multiplicative) => {
let y_des = if s.abs() > 1e-10 { y / s } else { y };
level = alpha * y_des + (1.0 - alpha) * (level_prev + trend);
trend = beta_val * (level - level_prev) + (1.0 - beta_val) * trend;
seasonal_buf[s_idx] = if level.abs() > 1e-10 {
gamma_val * (y / level) + (1.0 - gamma_val) * s
} else {
s
};
}
(TrendType::AdditiveDamped, SeasonalType::None) => {
level = alpha * y + (1.0 - alpha) * (level_prev + phi_val * trend);
trend = beta_val * (level - level_prev) + (1.0 - beta_val) * phi_val * trend;
}
(TrendType::AdditiveDamped, SeasonalType::Additive) => {
level = alpha * (y - s) + (1.0 - alpha) * (level_prev + phi_val * trend);
trend = beta_val * (level - level_prev) + (1.0 - beta_val) * phi_val * trend;
seasonal_buf[s_idx] = gamma_val * (y - level) + (1.0 - gamma_val) * s;
}
(TrendType::AdditiveDamped, SeasonalType::Multiplicative) => {
let y_des = if s.abs() > 1e-10 { y / s } else { y };
level = alpha * y_des + (1.0 - alpha) * (level_prev + phi_val * trend);
trend = beta_val * (level - level_prev) + (1.0 - beta_val) * phi_val * trend;
seasonal_buf[s_idx] = if level.abs() > 1e-10 {
gamma_val * (y / level) + (1.0 - gamma_val) * s
} else {
s
};
}
}
}
SeriesState {
level,
trend,
seasonals: seasonal_buf,
}
}
fn forecast_from_state(
state: &SeriesState,
spec: ETSSpec,
period: usize,
phi: Option<f64>,
horizon: usize,
) -> Vec<f64> {
let phi_val = phi.unwrap_or(1.0);
let mut forecasts = Vec::with_capacity(horizon);
for h in 1..=horizon {
let trend_component = match spec.trend {
TrendType::None => 0.0,
TrendType::Additive => state.trend * h as f64,
TrendType::AdditiveDamped => {
let mut sum = 0.0;
let mut phi_pow = phi_val;
for _ in 0..h {
sum += phi_pow;
phi_pow *= phi_val;
}
state.trend * sum
}
};
let seasonal = if spec.has_seasonal() && !state.seasonals.is_empty() {
state.seasonals[(state.seasonals.len() - period + (h - 1) % period) % period]
} else {
match spec.seasonal {
SeasonalType::Multiplicative => 1.0,
_ => 0.0,
}
};
let fc = match spec.seasonal {
SeasonalType::Multiplicative => (state.level + trend_component) * seasonal,
_ => state.level + trend_component + seasonal,
};
forecasts.push(fc);
}
forecasts
}
fn initialize_state(values: &[f64], spec: ETSSpec, period: usize) -> SeriesState {
let n = values.len();
let level = if period > 0 && n >= period {
values[..period].iter().sum::<f64>() / period as f64
} else {
values[0]
};
let trend = if spec.has_trend() && n >= 2 * period && period > 0 {
let first_mean = values[..period].iter().sum::<f64>() / period as f64;
let second_mean = values[period..2 * period].iter().sum::<f64>() / period as f64;
(second_mean - first_mean) / period as f64
} else {
0.0
};
let seasonals = if spec.has_seasonal() && period > 0 && n >= period {
match spec.seasonal {
SeasonalType::Additive => (0..period).map(|i| values[i] - level).collect(),
SeasonalType::Multiplicative => (0..period)
.map(|i| {
if level.abs() > 1e-10 {
values[i] / level
} else {
1.0
}
})
.collect(),
SeasonalType::None => vec![],
}
} else {
vec![]
};
SeriesState {
level,
trend,
seasonals,
}
}
}
#[derive(Debug, Clone)]
pub struct GlobalAutoETS {
period: usize,
pool: ModelPool,
per_series: Vec<(
ETSSpec,
SeriesState,
f64,
Option<f64>,
Option<f64>,
Option<f64>,
)>,
fitted: bool,
}
impl GlobalAutoETS {
pub fn new(period: usize, pool: ModelPool) -> Self {
Self {
period,
pool,
per_series: Vec::new(),
fitted: false,
}
}
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("GlobalAutoETS requires at least one series".into()),
});
}
let n_series = all_series.len();
let period = self.period;
let has_non_positive = all_series.iter().any(|s| s.iter().any(|&v| v <= 0.0));
let has_seasonal = period > 1 && all_series[0].len() >= 2 * period;
let candidates = Self::generate_candidates(self.pool, has_seasonal, has_non_positive);
let mut best_nll: Vec<f64> = vec![f64::MAX; n_series];
let mut best_spec: Vec<ETSSpec> = vec![ETSSpec::ann(); n_series];
let mut best_states: Vec<SeriesState> = (0..n_series)
.map(|_| SeriesState {
level: 0.0,
trend: 0.0,
seasonals: vec![],
})
.collect();
let mut best_params: Vec<(f64, Option<f64>, Option<f64>, Option<f64>)> =
vec![(0.3, None, None, None); n_series];
for spec in &candidates {
let mut global = GlobalETS::new(*spec, period);
if global.fit(all_series).is_err() {
continue;
}
let (alpha, beta, gamma, phi) = global.params();
for (s, values) in all_series.iter().enumerate() {
let init = &global.states[s];
let nll =
GlobalETS::series_nll(values, init, *spec, period, alpha, beta, gamma, phi);
if nll < best_nll[s] {
best_nll[s] = nll;
best_spec[s] = *spec;
best_states[s] = global.final_states[s].clone();
best_params[s] = (alpha, beta, gamma, phi);
}
}
}
self.per_series = best_spec
.into_iter()
.zip(best_states)
.zip(best_params)
.map(|((spec, state), (a, b, g, p))| (spec, state, a, b, g, p))
.collect();
self.fitted = true;
Ok(())
}
pub fn predict(&self, horizon: usize) -> Vec<Vec<f64>> {
if !self.fitted {
return vec![];
}
self.per_series
.iter()
.map(|(spec, state, _, _, _, phi)| {
GlobalETS::forecast_from_state(state, *spec, self.period, *phi, horizon)
})
.collect()
}
pub fn selected_specs(&self) -> Vec<ETSSpec> {
self.per_series.iter().map(|(spec, ..)| *spec).collect()
}
fn generate_candidates(
pool: ModelPool,
has_seasonal: bool,
has_non_positive: bool,
) -> Vec<ETSSpec> {
let error_types = if has_non_positive {
vec![ErrorType::Additive]
} else {
vec![ErrorType::Additive, ErrorType::Multiplicative]
};
let trend_types = vec![
TrendType::None,
TrendType::Additive,
TrendType::AdditiveDamped,
];
let seasonal_types = if !has_seasonal {
vec![SeasonalType::None]
} else if has_non_positive {
vec![SeasonalType::None, SeasonalType::Additive]
} else {
vec![
SeasonalType::None,
SeasonalType::Additive,
SeasonalType::Multiplicative,
]
};
let mut candidates = Vec::new();
for &error in &error_types {
for &trend in &trend_types {
for &seasonal in &seasonal_types {
if error == ErrorType::Multiplicative
&& (trend == TrendType::Additive || trend == TrendType::AdditiveDamped)
&& seasonal == SeasonalType::Additive
{
continue;
}
match pool {
ModelPool::Complete => {}
ModelPool::NoMultiplicativeTrend => {}
ModelPool::DampedTrendOnly => {
if trend == TrendType::Additive {
continue;
}
}
ModelPool::MatchErrorSeasonal => {
if error == ErrorType::Multiplicative
&& seasonal == SeasonalType::Additive
{
continue;
}
if error == ErrorType::Additive
&& seasonal == SeasonalType::Multiplicative
{
continue;
}
}
ModelPool::Reduced => {
if trend == TrendType::Additive {
continue;
}
if error == ErrorType::Multiplicative
&& seasonal == SeasonalType::Additive
{
continue;
}
if error == ErrorType::Additive
&& seasonal == SeasonalType::Multiplicative
{
continue;
}
}
}
candidates.push(ETSSpec::new(error, trend, seasonal));
}
}
}
candidates
}
}