use crate::core::error::{Error, Result};
use crate::time_series::core::{DateTimeIndex, Frequency, TimeSeries, TimeSeriesData};
use crate::time_series::decomposition::{DecompositionMethod, SeasonalDecomposition};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForecastResult {
pub forecast: TimeSeries,
pub lower_ci: TimeSeries,
pub upper_ci: TimeSeries,
pub method: String,
pub parameters: HashMap<String, f64>,
pub metrics: ForecastMetrics,
pub confidence_level: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForecastMetrics {
pub mae: Option<f64>,
pub mse: Option<f64>,
pub rmse: Option<f64>,
pub mape: Option<f64>,
pub smape: Option<f64>,
pub aic: Option<f64>,
pub bic: Option<f64>,
pub log_likelihood: Option<f64>,
}
pub trait Forecaster {
fn fit(&mut self, ts: &TimeSeries) -> Result<()>;
fn forecast(&self, periods: usize, confidence_level: f64) -> Result<ForecastResult>;
fn name(&self) -> &str;
fn parameters(&self) -> HashMap<String, f64>;
fn fit_metrics(&self, ts: &TimeSeries) -> Result<ForecastMetrics>;
}
#[derive(Debug, Clone)]
pub struct SimpleMovingAverageForecaster {
window: usize,
fitted_values: Option<Vec<f64>>,
last_values: Option<Vec<f64>>,
index: Option<DateTimeIndex>,
}
impl SimpleMovingAverageForecaster {
pub fn new(window: usize) -> Self {
Self {
window,
fitted_values: None,
last_values: None,
index: None,
}
}
}
impl Forecaster for SimpleMovingAverageForecaster {
fn fit(&mut self, ts: &TimeSeries) -> Result<()> {
if ts.len() < self.window {
return Err(Error::InvalidInput(
"Time series must be longer than window size".to_string(),
));
}
let mut fitted = Vec::new();
for i in 0..ts.len() {
if i < self.window - 1 {
fitted.push(f64::NAN);
} else {
let window_sum: f64 = (i + 1 - self.window..=i)
.filter_map(|idx| ts.values.get_f64(idx))
.sum();
fitted.push(window_sum / self.window as f64);
}
}
let last_values: Vec<f64> = (ts.len() - self.window..ts.len())
.filter_map(|idx| ts.values.get_f64(idx))
.collect();
self.fitted_values = Some(fitted);
self.last_values = Some(last_values);
self.index = Some(ts.index.clone());
Ok(())
}
fn forecast(&self, periods: usize, confidence_level: f64) -> Result<ForecastResult> {
let last_values = self
.last_values
.as_ref()
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
let index = self
.index
.as_ref()
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
let forecast_value = last_values.iter().sum::<f64>() / last_values.len() as f64;
let residual_std = self.calculate_residual_std()?;
let z_score = self.get_z_score(confidence_level);
let margin = z_score * residual_std;
let last_date = *index
.end()
.ok_or_else(|| Error::InvalidInput("Time series index has no end date".to_string()))?;
let frequency = index.frequency.clone().unwrap_or(Frequency::Daily);
let duration = frequency.to_duration();
let mut forecast_dates = Vec::new();
for i in 1..=periods {
forecast_dates.push(last_date + duration * i as i32);
}
let forecast_index = DateTimeIndex::with_frequency(forecast_dates, frequency);
let forecast_values = vec![forecast_value; periods];
let lower_values = vec![forecast_value - margin; periods];
let upper_values = vec![forecast_value + margin; periods];
let forecast_ts = TimeSeries::new(
forecast_index.clone(),
TimeSeriesData::from_vec(forecast_values),
)?;
let lower_ci_ts = TimeSeries::new(
forecast_index.clone(),
TimeSeriesData::from_vec(lower_values),
)?;
let upper_ci_ts = TimeSeries::new(forecast_index, TimeSeriesData::from_vec(upper_values))?;
let mut parameters = HashMap::new();
parameters.insert("window".to_string(), self.window as f64);
parameters.insert("forecast_value".to_string(), forecast_value);
Ok(ForecastResult {
forecast: forecast_ts,
lower_ci: lower_ci_ts,
upper_ci: upper_ci_ts,
method: "Simple Moving Average".to_string(),
parameters,
metrics: ForecastMetrics {
mae: None,
mse: None,
rmse: None,
mape: None,
smape: None,
aic: None,
bic: None,
log_likelihood: None,
},
confidence_level,
})
}
fn name(&self) -> &str {
"Simple Moving Average"
}
fn parameters(&self) -> HashMap<String, f64> {
let mut params = HashMap::new();
params.insert("window".to_string(), self.window as f64);
params
}
fn fit_metrics(&self, ts: &TimeSeries) -> Result<ForecastMetrics> {
let fitted = self
.fitted_values
.as_ref()
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
calculate_forecast_metrics(ts, fitted)
}
}
impl SimpleMovingAverageForecaster {
fn calculate_residual_std(&self) -> Result<f64> {
Ok(1.0) }
fn get_z_score(&self, confidence_level: f64) -> f64 {
match (confidence_level * 100.0) as i32 {
90 => 1.645,
95 => 1.96,
99 => 2.576,
_ => 1.96, }
}
}
#[derive(Debug, Clone)]
pub struct LinearTrendForecaster {
slope: Option<f64>,
intercept: Option<f64>,
fitted_values: Option<Vec<f64>>,
index: Option<DateTimeIndex>,
residual_std: Option<f64>,
}
impl LinearTrendForecaster {
pub fn new() -> Self {
Self {
slope: None,
intercept: None,
fitted_values: None,
index: None,
residual_std: None,
}
}
}
impl Default for LinearTrendForecaster {
fn default() -> Self {
Self::new()
}
}
impl Forecaster for LinearTrendForecaster {
fn fit(&mut self, ts: &TimeSeries) -> Result<()> {
if ts.len() < 2 {
return Err(Error::InvalidInput(
"Time series must have at least 2 points".to_string(),
));
}
let n = ts.len() as f64;
let x_values: Vec<f64> = (0..ts.len()).map(|i| i as f64).collect();
let y_values: Vec<f64> = (0..ts.len()).filter_map(|i| ts.values.get_f64(i)).collect();
if y_values.len() != ts.len() {
return Err(Error::InvalidInput(
"Missing values not supported".to_string(),
));
}
let sum_x = x_values.iter().sum::<f64>();
let sum_y = y_values.iter().sum::<f64>();
let sum_xy = x_values
.iter()
.zip(&y_values)
.map(|(x, y)| x * y)
.sum::<f64>();
let sum_x2 = x_values.iter().map(|x| x * x).sum::<f64>();
let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
let intercept = (sum_y - slope * sum_x) / n;
let fitted: Vec<f64> = x_values.iter().map(|&x| slope * x + intercept).collect();
let residuals: Vec<f64> = y_values
.iter()
.zip(&fitted)
.map(|(actual, predicted)| actual - predicted)
.collect();
let residual_std = (residuals.iter().map(|r| r * r).sum::<f64>() / (n - 2.0)).sqrt();
self.slope = Some(slope);
self.intercept = Some(intercept);
self.fitted_values = Some(fitted);
self.index = Some(ts.index.clone());
self.residual_std = Some(residual_std);
Ok(())
}
fn forecast(&self, periods: usize, confidence_level: f64) -> Result<ForecastResult> {
let slope = self
.slope
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
let intercept = self
.intercept
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
let index = self
.index
.as_ref()
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
let residual_std = self
.residual_std
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
let last_date = *index
.end()
.ok_or_else(|| Error::InvalidInput("Time series index has no end date".to_string()))?;
let frequency = index.frequency.clone().unwrap_or(Frequency::Daily);
let duration = frequency.to_duration();
let mut forecast_dates = Vec::new();
let mut forecast_values = Vec::new();
let start_x = index.len() as f64;
let z_score = self.get_z_score(confidence_level);
for i in 1..=periods {
forecast_dates.push(last_date + duration * i as i32);
let x = start_x + i as f64 - 1.0;
let forecast_value = slope * x + intercept;
forecast_values.push(forecast_value);
}
let margin = z_score * residual_std;
let lower_values: Vec<f64> = forecast_values.iter().map(|v| v - margin).collect();
let upper_values: Vec<f64> = forecast_values.iter().map(|v| v + margin).collect();
let forecast_index = DateTimeIndex::with_frequency(forecast_dates, frequency);
let forecast_ts = TimeSeries::new(
forecast_index.clone(),
TimeSeriesData::from_vec(forecast_values),
)?;
let lower_ci_ts = TimeSeries::new(
forecast_index.clone(),
TimeSeriesData::from_vec(lower_values),
)?;
let upper_ci_ts = TimeSeries::new(forecast_index, TimeSeriesData::from_vec(upper_values))?;
let mut parameters = HashMap::new();
parameters.insert("slope".to_string(), slope);
parameters.insert("intercept".to_string(), intercept);
parameters.insert("residual_std".to_string(), residual_std);
Ok(ForecastResult {
forecast: forecast_ts,
lower_ci: lower_ci_ts,
upper_ci: upper_ci_ts,
method: "Linear Trend".to_string(),
parameters,
metrics: ForecastMetrics {
mae: None,
mse: None,
rmse: None,
mape: None,
smape: None,
aic: None,
bic: None,
log_likelihood: None,
},
confidence_level,
})
}
fn name(&self) -> &str {
"Linear Trend"
}
fn parameters(&self) -> HashMap<String, f64> {
let mut params = HashMap::new();
if let Some(slope) = self.slope {
params.insert("slope".to_string(), slope);
}
if let Some(intercept) = self.intercept {
params.insert("intercept".to_string(), intercept);
}
params
}
fn fit_metrics(&self, ts: &TimeSeries) -> Result<ForecastMetrics> {
let fitted = self
.fitted_values
.as_ref()
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
calculate_forecast_metrics(ts, fitted)
}
}
impl LinearTrendForecaster {
fn get_z_score(&self, confidence_level: f64) -> f64 {
match (confidence_level * 100.0) as i32 {
90 => 1.645,
95 => 1.96,
99 => 2.576,
_ => 1.96,
}
}
}
#[derive(Debug, Clone)]
pub struct ExponentialSmoothingForecaster {
alpha: f64, beta: Option<f64>, gamma: Option<f64>, seasonal_periods: Option<usize>,
fitted_values: Option<Vec<f64>>,
level: Option<f64>,
trend: Option<f64>,
seasonal: Option<Vec<f64>>,
index: Option<DateTimeIndex>,
residual_std: Option<f64>,
}
impl ExponentialSmoothingForecaster {
pub fn simple(alpha: f64) -> Self {
Self {
alpha,
beta: None,
gamma: None,
seasonal_periods: None,
fitted_values: None,
level: None,
trend: None,
seasonal: None,
index: None,
residual_std: None,
}
}
pub fn double(alpha: f64, beta: f64) -> Self {
Self {
alpha,
beta: Some(beta),
gamma: None,
seasonal_periods: None,
fitted_values: None,
level: None,
trend: None,
seasonal: None,
index: None,
residual_std: None,
}
}
pub fn triple(alpha: f64, beta: f64, gamma: f64, seasonal_periods: usize) -> Self {
Self {
alpha,
beta: Some(beta),
gamma: Some(gamma),
seasonal_periods: Some(seasonal_periods),
fitted_values: None,
level: None,
trend: None,
seasonal: None,
index: None,
residual_std: None,
}
}
}
impl Forecaster for ExponentialSmoothingForecaster {
fn fit(&mut self, ts: &TimeSeries) -> Result<()> {
if ts.is_empty() {
return Err(Error::InvalidInput("Empty time series".to_string()));
}
let values: Vec<f64> = (0..ts.len()).filter_map(|i| ts.values.get_f64(i)).collect();
if values.len() != ts.len() {
return Err(Error::InvalidInput(
"Missing values not supported".to_string(),
));
}
match (&self.beta, &self.gamma, &self.seasonal_periods) {
(None, None, None) => self.fit_simple(&values)?,
(Some(_), None, None) => self.fit_double(&values)?,
(Some(_), Some(_), Some(_)) => self.fit_triple(&values)?,
_ => {
return Err(Error::InvalidInput(
"Invalid smoothing configuration".to_string(),
))
}
}
self.index = Some(ts.index.clone());
Ok(())
}
fn forecast(&self, periods: usize, confidence_level: f64) -> Result<ForecastResult> {
let level = self
.level
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
let index = self
.index
.as_ref()
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
let mut forecast_values = Vec::new();
match (&self.beta, &self.gamma, &self.seasonal_periods) {
(None, None, None) => {
forecast_values = vec![level; periods];
}
(Some(beta), None, None) => {
let trend = self.trend.unwrap_or(0.0);
for h in 1..=periods {
forecast_values.push(level + h as f64 * trend);
}
}
(Some(beta), Some(gamma), Some(seasonal_periods)) => {
let trend = self.trend.unwrap_or(0.0);
let seasonal = self.seasonal.as_ref().ok_or_else(|| {
Error::InvalidOperation("Seasonal component not available".into())
})?;
for h in 1..=periods {
let seasonal_idx = (h - 1) % seasonal_periods;
let forecast = level + h as f64 * trend + seasonal[seasonal_idx];
forecast_values.push(forecast);
}
}
_ => return Err(Error::InvalidOperation("Invalid model state".to_string())),
}
let last_date = *index
.end()
.ok_or_else(|| Error::InvalidInput("Time series index has no end date".to_string()))?;
let frequency = index.frequency.clone().unwrap_or(Frequency::Daily);
let duration = frequency.to_duration();
let mut forecast_dates = Vec::new();
for i in 1..=periods {
forecast_dates.push(last_date + duration * i as i32);
}
let residual_std = self.residual_std.unwrap_or(1.0);
let z_score = self.get_z_score(confidence_level);
let margin = z_score * residual_std;
let lower_values: Vec<f64> = forecast_values.iter().map(|v| v - margin).collect();
let upper_values: Vec<f64> = forecast_values.iter().map(|v| v + margin).collect();
let forecast_index = DateTimeIndex::with_frequency(forecast_dates, frequency);
let forecast_ts = TimeSeries::new(
forecast_index.clone(),
TimeSeriesData::from_vec(forecast_values),
)?;
let lower_ci_ts = TimeSeries::new(
forecast_index.clone(),
TimeSeriesData::from_vec(lower_values),
)?;
let upper_ci_ts = TimeSeries::new(forecast_index, TimeSeriesData::from_vec(upper_values))?;
Ok(ForecastResult {
forecast: forecast_ts,
lower_ci: lower_ci_ts,
upper_ci: upper_ci_ts,
method: self.method_name(),
parameters: self.parameters(),
metrics: ForecastMetrics {
mae: None,
mse: None,
rmse: None,
mape: None,
smape: None,
aic: None,
bic: None,
log_likelihood: None,
},
confidence_level,
})
}
fn name(&self) -> &str {
match (&self.beta, &self.gamma) {
(None, None) => "Simple Exponential Smoothing",
(Some(_), None) => "Double Exponential Smoothing",
(Some(_), Some(_)) => "Triple Exponential Smoothing",
_ => "Exponential Smoothing",
}
}
fn parameters(&self) -> HashMap<String, f64> {
let mut params = HashMap::new();
params.insert("alpha".to_string(), self.alpha);
if let Some(beta) = self.beta {
params.insert("beta".to_string(), beta);
}
if let Some(gamma) = self.gamma {
params.insert("gamma".to_string(), gamma);
}
if let Some(level) = self.level {
params.insert("level".to_string(), level);
}
if let Some(trend) = self.trend {
params.insert("trend".to_string(), trend);
}
params
}
fn fit_metrics(&self, ts: &TimeSeries) -> Result<ForecastMetrics> {
let fitted = self
.fitted_values
.as_ref()
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
calculate_forecast_metrics(ts, fitted)
}
}
impl ExponentialSmoothingForecaster {
fn fit_simple(&mut self, values: &[f64]) -> Result<()> {
let mut level = values[0];
let mut fitted = vec![level];
for &value in &values[1..] {
level = self.alpha * value + (1.0 - self.alpha) * level;
fitted.push(level);
}
let residuals: Vec<f64> = values
.iter()
.zip(&fitted)
.map(|(actual, predicted)| actual - predicted)
.collect();
let residual_std =
(residuals.iter().map(|r| r * r).sum::<f64>() / residuals.len() as f64).sqrt();
self.level = Some(level);
self.fitted_values = Some(fitted);
self.residual_std = Some(residual_std);
Ok(())
}
fn fit_double(&mut self, values: &[f64]) -> Result<()> {
let beta = self.beta.ok_or_else(|| {
Error::InvalidInput(
"Beta parameter not set for double exponential smoothing".to_string(),
)
})?;
let mut level = values[0];
let mut trend = if values.len() > 1 {
values[1] - values[0]
} else {
0.0
};
let mut fitted = vec![level];
for &value in &values[1..] {
let prev_level = level;
level = self.alpha * value + (1.0 - self.alpha) * (level + trend);
trend = beta * (level - prev_level) + (1.0 - beta) * trend;
fitted.push(level + trend);
}
let residuals: Vec<f64> = values
.iter()
.zip(&fitted)
.map(|(actual, predicted)| actual - predicted)
.collect();
let residual_std =
(residuals.iter().map(|r| r * r).sum::<f64>() / residuals.len() as f64).sqrt();
self.level = Some(level);
self.trend = Some(trend);
self.fitted_values = Some(fitted);
self.residual_std = Some(residual_std);
Ok(())
}
fn fit_triple(&mut self, values: &[f64]) -> Result<()> {
let beta = self.beta.ok_or_else(|| {
Error::InvalidInput(
"Beta parameter not set for triple exponential smoothing".to_string(),
)
})?;
let gamma = self.gamma.ok_or_else(|| {
Error::InvalidInput(
"Gamma parameter not set for triple exponential smoothing".to_string(),
)
})?;
let seasonal_periods = self.seasonal_periods.ok_or_else(|| {
Error::InvalidInput(
"Seasonal periods not set for triple exponential smoothing".to_string(),
)
})?;
if values.len() < seasonal_periods {
return Err(Error::InvalidInput(
"Time series must be longer than seasonal periods".to_string(),
));
}
let mut seasonal = vec![0.0; seasonal_periods];
for i in 0..seasonal_periods {
seasonal[i] = values[i]
/ (values.iter().take(seasonal_periods).sum::<f64>() / seasonal_periods as f64);
}
let mut level = values.iter().take(seasonal_periods).sum::<f64>() / seasonal_periods as f64;
let mut trend = 0.0;
let mut fitted = Vec::new();
for (t, &value) in values.iter().enumerate() {
let seasonal_idx = t % seasonal_periods;
if t < seasonal_periods {
fitted.push(level * seasonal[seasonal_idx]);
} else {
let prev_level = level;
level = self.alpha * (value / seasonal[seasonal_idx])
+ (1.0 - self.alpha) * (level + trend);
trend = beta * (level - prev_level) + (1.0 - beta) * trend;
seasonal[seasonal_idx] =
gamma * (value / level) + (1.0 - gamma) * seasonal[seasonal_idx];
fitted.push((level + trend) * seasonal[seasonal_idx]);
}
}
let residuals: Vec<f64> = values
.iter()
.zip(&fitted)
.map(|(actual, predicted)| actual - predicted)
.collect();
let residual_std =
(residuals.iter().map(|r| r * r).sum::<f64>() / residuals.len() as f64).sqrt();
self.level = Some(level);
self.trend = Some(trend);
self.seasonal = Some(seasonal);
self.fitted_values = Some(fitted);
self.residual_std = Some(residual_std);
Ok(())
}
fn method_name(&self) -> String {
match (&self.beta, &self.gamma) {
(None, None) => "Simple Exponential Smoothing".to_string(),
(Some(_), None) => "Double Exponential Smoothing (Holt)".to_string(),
(Some(_), Some(_)) => "Triple Exponential Smoothing (Holt-Winters)".to_string(),
_ => "Exponential Smoothing".to_string(),
}
}
fn get_z_score(&self, confidence_level: f64) -> f64 {
match (confidence_level * 100.0) as i32 {
90 => 1.645,
95 => 1.96,
99 => 2.576,
_ => 1.96,
}
}
}
#[derive(Debug, Clone)]
pub struct ArimaForecaster {
p: usize, d: usize, q: usize, ar_params: Option<Vec<f64>>,
ma_params: Option<Vec<f64>>,
fitted_values: Option<Vec<f64>>,
residuals: Option<Vec<f64>>,
index: Option<DateTimeIndex>,
differenced_series: Option<Vec<f64>>,
residual_std: Option<f64>,
}
impl ArimaForecaster {
pub fn new(p: usize, d: usize, q: usize) -> Self {
Self {
p,
d,
q,
ar_params: None,
ma_params: None,
fitted_values: None,
residuals: None,
index: None,
differenced_series: None,
residual_std: None,
}
}
}
impl Forecaster for ArimaForecaster {
fn fit(&mut self, ts: &TimeSeries) -> Result<()> {
if ts.len() < self.p + self.d + self.q + 1 {
return Err(Error::InvalidInput(
"Time series too short for ARIMA model".to_string(),
));
}
let mut series = ts.clone();
for _ in 0..self.d {
series = series.diff(1)?;
}
let differenced_values: Vec<f64> = (self.d..series.len())
.filter_map(|i| series.values.get_f64(i))
.collect();
let ar_params = vec![0.5; self.p]; let ma_params = vec![0.3; self.q];
let mut fitted = Vec::new();
let mut residuals = Vec::new();
for i in 0..differenced_values.len() {
let mut forecast = 0.0;
for j in 0..self.p {
if i >= j + 1 {
forecast += ar_params[j] * differenced_values[i - j - 1];
}
}
for j in 0..self.q {
if i >= j + 1 && j < residuals.len() {
forecast += ma_params[j] * residuals[residuals.len() - j - 1];
}
}
fitted.push(forecast);
residuals.push(differenced_values[i] - forecast);
}
let residual_std =
(residuals.iter().map(|r| r * r).sum::<f64>() / residuals.len() as f64).sqrt();
self.ar_params = Some(ar_params);
self.ma_params = Some(ma_params);
self.fitted_values = Some(fitted);
self.residuals = Some(residuals);
self.index = Some(ts.index.clone());
self.differenced_series = Some(differenced_values);
self.residual_std = Some(residual_std);
Ok(())
}
fn forecast(&self, periods: usize, confidence_level: f64) -> Result<ForecastResult> {
let ar_params = self
.ar_params
.as_ref()
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
let ma_params = self
.ma_params
.as_ref()
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
let index = self
.index
.as_ref()
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
let differenced = self
.differenced_series
.as_ref()
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
let residuals = self
.residuals
.as_ref()
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
let mut forecasts = Vec::new();
let mut last_values = differenced
.iter()
.rev()
.take(self.p)
.cloned()
.collect::<Vec<_>>();
last_values.reverse();
let mut last_residuals = residuals
.iter()
.rev()
.take(self.q)
.cloned()
.collect::<Vec<_>>();
last_residuals.reverse();
for _ in 0..periods {
let mut forecast = 0.0;
for j in 0..self.p {
if j < last_values.len() {
forecast += ar_params[j] * last_values[last_values.len() - 1 - j];
}
}
for j in 0..self.q {
if j < last_residuals.len() {
forecast += ma_params[j] * last_residuals[last_residuals.len() - 1 - j];
}
}
forecasts.push(forecast);
last_values.push(forecast);
if last_values.len() > self.p {
last_values.remove(0);
}
last_residuals.push(0.0); if last_residuals.len() > self.q {
last_residuals.remove(0);
}
}
let last_date = *index
.end()
.ok_or_else(|| Error::InvalidInput("Time series index has no end date".to_string()))?;
let frequency = index.frequency.clone().unwrap_or(Frequency::Daily);
let duration = frequency.to_duration();
let mut forecast_dates = Vec::new();
for i in 1..=periods {
forecast_dates.push(last_date + duration * i as i32);
}
let residual_std = self.residual_std.unwrap_or(1.0);
let z_score = self.get_z_score(confidence_level);
let margin = z_score * residual_std;
let lower_values: Vec<f64> = forecasts.iter().map(|v| v - margin).collect();
let upper_values: Vec<f64> = forecasts.iter().map(|v| v + margin).collect();
let forecast_index = DateTimeIndex::with_frequency(forecast_dates, frequency);
let forecast_ts =
TimeSeries::new(forecast_index.clone(), TimeSeriesData::from_vec(forecasts))?;
let lower_ci_ts = TimeSeries::new(
forecast_index.clone(),
TimeSeriesData::from_vec(lower_values),
)?;
let upper_ci_ts = TimeSeries::new(forecast_index, TimeSeriesData::from_vec(upper_values))?;
Ok(ForecastResult {
forecast: forecast_ts,
lower_ci: lower_ci_ts,
upper_ci: upper_ci_ts,
method: format!("ARIMA({},{},{})", self.p, self.d, self.q),
parameters: self.parameters(),
metrics: ForecastMetrics {
mae: None,
mse: None,
rmse: None,
mape: None,
smape: None,
aic: None,
bic: None,
log_likelihood: None,
},
confidence_level,
})
}
fn name(&self) -> &str {
"ARIMA"
}
fn parameters(&self) -> HashMap<String, f64> {
let mut params = HashMap::new();
params.insert("p".to_string(), self.p as f64);
params.insert("d".to_string(), self.d as f64);
params.insert("q".to_string(), self.q as f64);
params
}
fn fit_metrics(&self, ts: &TimeSeries) -> Result<ForecastMetrics> {
let fitted = self
.fitted_values
.as_ref()
.ok_or_else(|| Error::InvalidOperation("Model not fitted".to_string()))?;
let mut original_fitted = vec![f64::NAN; self.d];
original_fitted.extend(fitted.iter());
calculate_forecast_metrics(ts, &original_fitted)
}
}
impl ArimaForecaster {
fn get_z_score(&self, confidence_level: f64) -> f64 {
match (confidence_level * 100.0) as i32 {
90 => 1.645,
95 => 1.96,
99 => 2.576,
_ => 1.96,
}
}
}
fn calculate_forecast_metrics(ts: &TimeSeries, fitted: &[f64]) -> Result<ForecastMetrics> {
let actual_values: Vec<f64> = (0..ts.len()).filter_map(|i| ts.values.get_f64(i)).collect();
if actual_values.len() != fitted.len() {
return Err(Error::DimensionMismatch(
"Actual and fitted values must have same length".to_string(),
));
}
let valid_pairs: Vec<(f64, f64)> = actual_values
.iter()
.zip(fitted.iter())
.filter(|(a, f)| a.is_finite() && f.is_finite())
.map(|(&a, &f)| (a, f))
.collect();
if valid_pairs.is_empty() {
return Ok(ForecastMetrics {
mae: None,
mse: None,
rmse: None,
mape: None,
smape: None,
aic: None,
bic: None,
log_likelihood: None,
});
}
let n = valid_pairs.len() as f64;
let mae = valid_pairs
.iter()
.map(|(actual, fitted)| (actual - fitted).abs())
.sum::<f64>()
/ n;
let mse = valid_pairs
.iter()
.map(|(actual, fitted)| (actual - fitted).powi(2))
.sum::<f64>()
/ n;
let rmse = mse.sqrt();
let mape = valid_pairs
.iter()
.filter(|(actual, _)| *actual != 0.0)
.map(|(actual, fitted)| ((actual - fitted) / actual).abs())
.sum::<f64>()
/ n
* 100.0;
let smape = valid_pairs
.iter()
.map(|(actual, fitted)| {
let denominator = (actual.abs() + fitted.abs()) / 2.0;
if denominator != 0.0 {
(actual - fitted).abs() / denominator
} else {
0.0
}
})
.sum::<f64>()
/ n
* 100.0;
Ok(ForecastMetrics {
mae: Some(mae),
mse: Some(mse),
rmse: Some(rmse),
mape: Some(mape),
smape: Some(smape),
aic: None, bic: None, log_likelihood: None, })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::time_series::core::{Frequency, TimeSeriesBuilder};
use chrono::{TimeZone, Utc};
fn create_test_series_with_trend() -> TimeSeries {
let mut builder = TimeSeriesBuilder::new();
for i in 0..50 {
let timestamp = Utc
.timestamp_opt(1640995200 + i * 86400, 0)
.single()
.expect("operation should succeed");
let value = 10.0 + i as f64 * 0.5 + (i as f64 * 0.1).sin(); builder = builder.add_point(timestamp, value);
}
builder
.frequency(Frequency::Daily)
.build()
.expect("operation should succeed")
}
#[test]
fn test_simple_moving_average_forecaster() {
let ts = create_test_series_with_trend();
let mut forecaster = SimpleMovingAverageForecaster::new(5);
forecaster.fit(&ts).expect("operation should succeed");
let result = forecaster
.forecast(10, 0.95)
.expect("operation should succeed");
assert_eq!(result.forecast.len(), 10);
assert_eq!(result.method, "Simple Moving Average");
assert!(result.confidence_level == 0.95);
}
#[test]
fn test_linear_trend_forecaster() {
let ts = create_test_series_with_trend();
let mut forecaster = LinearTrendForecaster::new();
forecaster.fit(&ts).expect("operation should succeed");
let result = forecaster
.forecast(10, 0.95)
.expect("operation should succeed");
assert_eq!(result.forecast.len(), 10);
assert_eq!(result.method, "Linear Trend");
let first_forecast = result
.forecast
.values
.get_f64(0)
.expect("operation should succeed");
let last_forecast = result
.forecast
.values
.get_f64(9)
.expect("operation should succeed");
assert!(
last_forecast > first_forecast,
"Should show increasing trend"
);
}
#[test]
fn test_simple_exponential_smoothing() {
let ts = create_test_series_with_trend();
let mut forecaster = ExponentialSmoothingForecaster::simple(0.3);
forecaster.fit(&ts).expect("operation should succeed");
let result = forecaster
.forecast(5, 0.95)
.expect("operation should succeed");
assert_eq!(result.forecast.len(), 5);
assert!(result.method.contains("Simple Exponential Smoothing"));
}
#[test]
fn test_double_exponential_smoothing() {
let ts = create_test_series_with_trend();
let mut forecaster = ExponentialSmoothingForecaster::double(0.3, 0.1);
forecaster.fit(&ts).expect("operation should succeed");
let result = forecaster
.forecast(5, 0.95)
.expect("operation should succeed");
assert_eq!(result.forecast.len(), 5);
assert!(result.method.contains("Double Exponential Smoothing"));
}
#[test]
fn test_arima_forecaster() {
let ts = create_test_series_with_trend();
let mut forecaster = ArimaForecaster::new(1, 1, 1);
forecaster.fit(&ts).expect("operation should succeed");
let result = forecaster
.forecast(5, 0.95)
.expect("operation should succeed");
assert_eq!(result.forecast.len(), 5);
assert_eq!(result.method, "ARIMA(1,1,1)");
}
#[test]
fn test_forecast_metrics() {
let ts = create_test_series_with_trend();
let mut forecaster = LinearTrendForecaster::new();
forecaster.fit(&ts).expect("operation should succeed");
let metrics = forecaster
.fit_metrics(&ts)
.expect("operation should succeed");
assert!(metrics.mae.is_some());
assert!(metrics.mse.is_some());
assert!(metrics.rmse.is_some());
assert!(metrics.mae.expect("operation should succeed") >= 0.0);
assert!(metrics.mse.expect("operation should succeed") >= 0.0);
assert!(metrics.rmse.expect("operation should succeed") >= 0.0);
}
#[test]
fn test_confidence_intervals() {
let ts = create_test_series_with_trend();
let mut forecaster = LinearTrendForecaster::new();
forecaster.fit(&ts).expect("operation should succeed");
let result = forecaster
.forecast(5, 0.95)
.expect("operation should succeed");
for i in 0..result.forecast.len() {
let forecast = result
.forecast
.values
.get_f64(i)
.expect("operation should succeed");
let lower = result
.lower_ci
.values
.get_f64(i)
.expect("operation should succeed");
let upper = result
.upper_ci
.values
.get_f64(i)
.expect("operation should succeed");
assert!(lower < forecast, "Lower CI should be less than forecast");
assert!(upper > forecast, "Upper CI should be greater than forecast");
}
}
}