use crate::{AprenderError, Vector};
#[derive(Debug, Clone)]
pub struct ARIMA {
p: usize,
d: usize,
q: usize,
ar_coef: Option<Vector<f64>>,
ma_coef: Option<Vector<f64>>,
intercept: f64,
original_data: Option<Vector<f64>>,
differenced_data: Option<Vector<f64>>,
residuals: Option<Vector<f64>>,
}
impl ARIMA {
#[must_use]
pub fn new(p: usize, d: usize, q: usize) -> Self {
Self {
p,
d,
q,
ar_coef: None,
ma_coef: None,
intercept: 0.0,
original_data: None,
differenced_data: None,
residuals: None,
}
}
pub fn fit(&mut self, data: &Vector<f64>) -> Result<(), AprenderError> {
let min_length = self.p.max(self.q) + self.d + 1;
if data.len() < min_length {
return Err(AprenderError::Other(format!(
"Insufficient data: need at least {} observations for ARIMA({}, {}, {})",
min_length, self.p, self.d, self.q
)));
}
self.original_data = Some(data.clone());
let mut working_data = data.clone();
for _ in 0..self.d {
working_data = Self::difference(&working_data)?;
}
self.differenced_data = Some(working_data.clone());
if self.p > 0 {
self.ar_coef = Some(self.estimate_ar_parameters(&working_data)?);
}
if self.q > 0 {
self.ma_coef = Some(self.estimate_ma_parameters(&working_data)?);
}
self.intercept = working_data.as_slice().iter().sum::<f64>() / working_data.len() as f64;
self.residuals = Some(self.calculate_residuals(&working_data)?);
Ok(())
}
pub fn forecast(&self, n_periods: usize) -> Result<Vector<f64>, AprenderError> {
let original_data = self
.original_data
.as_ref()
.ok_or_else(|| AprenderError::Other("ARIMA model not fitted".to_string()))?;
let differenced_data = self
.differenced_data
.as_ref()
.ok_or_else(|| AprenderError::Other("ARIMA model not fitted".to_string()))?;
let mut forecasts = Vec::with_capacity(n_periods);
let mut history = differenced_data.as_slice().to_vec();
let mut residual_history = self
.residuals
.as_ref()
.map_or_else(Vec::new, |r| r.as_slice().to_vec());
for _ in 0..n_periods {
let mut forecast = self.intercept;
if let Some(ar_coef) = &self.ar_coef {
for i in 0..self.p {
if i < history.len() {
forecast += ar_coef[i] * history[history.len() - 1 - i];
}
}
}
if let Some(ma_coef) = &self.ma_coef {
for i in 0..self.q {
if i < residual_history.len() {
forecast += ma_coef[i] * residual_history[residual_history.len() - 1 - i];
}
}
}
forecasts.push(forecast);
history.push(forecast);
residual_history.push(0.0); }
let mut integrated = forecasts;
for _ in 0..self.d {
let last_value = original_data.as_slice().last().copied().unwrap_or(0.0);
integrated = Self::integrate(&integrated, last_value)?;
}
Ok(Vector::from_vec(integrated))
}
fn difference(data: &Vector<f64>) -> Result<Vector<f64>, AprenderError> {
if data.len() < 2 {
return Err(AprenderError::Other(
"Need at least 2 observations for differencing".to_string(),
));
}
let diff: Vec<f64> = data.as_slice().windows(2).map(|w| w[1] - w[0]).collect();
Ok(Vector::from_vec(diff))
}
#[allow(clippy::unnecessary_wraps)]
fn integrate(diff_data: &[f64], last_value: f64) -> Result<Vec<f64>, AprenderError> {
let mut integrated = Vec::with_capacity(diff_data.len());
let mut current = last_value;
for &diff in diff_data {
current += diff;
integrated.push(current);
}
Ok(integrated)
}
#[allow(clippy::unnecessary_wraps)]
fn estimate_ar_parameters(&self, data: &Vector<f64>) -> Result<Vector<f64>, AprenderError> {
let n = data.len();
let mut coefs = vec![0.1; self.p];
for lag in 0..self.p {
if n > lag + 1 {
let mut sum_prod = 0.0;
let mut sum_sq = 0.0;
for i in (lag + 1)..n {
sum_prod += data[i] * data[i - lag - 1];
sum_sq += data[i - lag - 1] * data[i - lag - 1];
}
if sum_sq > 1e-10 {
coefs[lag] = sum_prod / sum_sq;
}
}
}
Ok(Vector::from_vec(coefs))
}
#[allow(clippy::unnecessary_wraps)]
fn estimate_ma_parameters(&self, _data: &Vector<f64>) -> Result<Vector<f64>, AprenderError> {
let coefs = vec![0.5 / (1.0 + self.q as f64); self.q];
Ok(Vector::from_vec(coefs))
}
#[allow(clippy::unnecessary_wraps)]
fn calculate_residuals(&self, data: &Vector<f64>) -> Result<Vector<f64>, AprenderError> {
let n = data.len();
let start_idx = self.p.max(self.q);
let mut residuals = vec![0.0; n];
for i in start_idx..n {
let mut prediction = self.intercept;
if let Some(ar_coef) = &self.ar_coef {
for j in 0..self.p {
if i > j {
prediction += ar_coef[j] * data[i - j - 1];
}
}
}
residuals[i] = data[i] - prediction;
}
Ok(Vector::from_vec(residuals))
}
#[must_use]
pub fn ar_coefficients(&self) -> Option<&Vector<f64>> {
self.ar_coef.as_ref()
}
#[must_use]
pub fn ma_coefficients(&self) -> Option<&Vector<f64>> {
self.ma_coef.as_ref()
}
#[must_use]
pub fn intercept(&self) -> f64 {
self.intercept
}
#[must_use]
pub fn order(&self) -> (usize, usize, usize) {
(self.p, self.d, self.q)
}
}
#[cfg(test)]
#[path = "time_series_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "tests_arima_contract.rs"]
mod tests_arima_contract;