use crate::error::{NumRs2Error, Result};
use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1};
#[derive(Debug, Clone)]
pub struct ArimaParams {
pub ar_coefs: Array1<f64>,
pub ma_coefs: Array1<f64>,
pub intercept: f64,
pub sigma2: f64,
pub log_likelihood: f64,
pub aic: f64,
pub bic: f64,
}
#[derive(Debug, Clone)]
pub struct SarimaParams {
pub arima_params: ArimaParams,
pub seasonal_ar_coefs: Array1<f64>,
pub seasonal_ma_coefs: Array1<f64>,
pub seasonal_period: usize,
}
#[derive(Debug, Clone)]
pub struct Arima {
pub p: usize,
pub d: usize,
pub q: usize,
pub include_intercept: bool,
}
impl Arima {
pub fn new(p: usize, d: usize, q: usize) -> Self {
Self {
p,
d,
q,
include_intercept: true,
}
}
pub fn with_intercept(mut self, include: bool) -> Self {
self.include_intercept = include;
self
}
pub fn fit(&self, data: &ArrayView1<f64>) -> Result<ArimaParams> {
let n = data.len();
if n < self.p + self.d + self.q + 1 {
return Err(NumRs2Error::ValueError(format!(
"Insufficient data: need at least {} observations",
self.p + self.d + self.q + 1
)));
}
let diff_data = self.difference(data)?;
let init_params = self.estimate_css(&diff_data.view())?;
let mle_params = self.estimate_mle(&diff_data.view(), &init_params)?;
Ok(mle_params)
}
fn difference(&self, data: &ArrayView1<f64>) -> Result<Array1<f64>> {
let mut result = data.to_owned();
for _ in 0..self.d {
if result.len() < 2 {
return Err(NumRs2Error::ValueError(
"Series too short for differencing".to_string(),
));
}
let n = result.len();
let mut diff = Array1::zeros(n - 1);
for i in 0..(n - 1) {
diff[i] = result[i + 1] - result[i];
}
result = diff;
}
Ok(result)
}
fn estimate_css(&self, data: &ArrayView1<f64>) -> Result<ArimaParams> {
let n = data.len();
let max_lag = self.p.max(self.q);
if n <= max_lag {
return Err(NumRs2Error::ValueError(
"Insufficient data for CSS estimation".to_string(),
));
}
let mut ar_coefs = Array1::zeros(self.p);
let mut ma_coefs = Array1::zeros(self.q);
let mut intercept = 0.0;
if self.include_intercept {
intercept = data.iter().sum::<f64>() / n as f64;
}
if self.p > 0 {
ar_coefs = self.estimate_ar_yule_walker(data)?;
}
if self.q > 0 {
ma_coefs = self.estimate_ma_innovations(data, &ar_coefs)?;
}
let residuals = self.compute_residuals(data, &ar_coefs, &ma_coefs, intercept)?;
let sigma2 = residuals.iter().map(|&r| r * r).sum::<f64>() / residuals.len() as f64;
let log_likelihood = self.compute_log_likelihood(&residuals, sigma2);
let k = self.p + self.q + if self.include_intercept { 1 } else { 0 };
let aic = -2.0 * log_likelihood + 2.0 * k as f64;
let bic = -2.0 * log_likelihood + (k as f64) * (n as f64).ln();
Ok(ArimaParams {
ar_coefs,
ma_coefs,
intercept,
sigma2,
log_likelihood,
aic,
bic,
})
}
fn estimate_ar_yule_walker(&self, data: &ArrayView1<f64>) -> Result<Array1<f64>> {
if self.p == 0 {
return Ok(Array1::zeros(0));
}
use crate::new_modules::timeseries::autocorrelation;
let acf = autocorrelation(data, self.p)?;
let mut r_matrix = Array2::zeros((self.p, self.p));
let mut r_vec = Array1::zeros(self.p);
for i in 0..self.p {
r_vec[i] = acf[i + 1];
for j in 0..self.p {
let lag = i.abs_diff(j);
r_matrix[[i, j]] = acf[lag];
}
}
match scirs2_linalg::solve(&r_matrix.view(), &r_vec.view(), None) {
Ok(phi) => Ok(phi),
Err(_) => {
Ok(Array1::zeros(self.p))
}
}
}
fn estimate_ma_innovations(
&self,
data: &ArrayView1<f64>,
ar_coefs: &Array1<f64>,
) -> Result<Array1<f64>> {
if self.q == 0 {
return Ok(Array1::zeros(0));
}
let intercept = if self.include_intercept {
data.iter().sum::<f64>() / data.len() as f64
} else {
0.0
};
let residuals = if self.p > 0 {
self.compute_ar_residuals(data, ar_coefs, intercept)?
} else {
data.iter().map(|&x| x - intercept).collect()
};
use crate::new_modules::timeseries::autocorrelation;
let res_array = Array1::from_vec(residuals);
let res_acf = autocorrelation(&res_array.view(), self.q)?;
let mut ma_coefs = Array1::zeros(self.q);
for i in 0..self.q {
ma_coefs[i] = -res_acf[i + 1]; }
Ok(ma_coefs)
}
fn compute_ar_residuals(
&self,
data: &ArrayView1<f64>,
ar_coefs: &Array1<f64>,
intercept: f64,
) -> Result<Vec<f64>> {
let n = data.len();
let mut residuals = Vec::with_capacity(n - self.p);
for t in self.p..n {
let mut prediction = intercept;
for i in 0..self.p {
prediction += ar_coefs[i] * (data[t - i - 1] - intercept);
}
residuals.push(data[t] - prediction);
}
Ok(residuals)
}
fn compute_residuals(
&self,
data: &ArrayView1<f64>,
ar_coefs: &Array1<f64>,
ma_coefs: &Array1<f64>,
intercept: f64,
) -> Result<Array1<f64>> {
let n = data.len();
let max_lag = self.p.max(self.q);
let mut residuals = Array1::zeros(n);
for t in 0..max_lag {
residuals[t] = data[t] - intercept;
}
for t in max_lag..n {
let mut prediction = intercept;
for i in 0..self.p {
if t > i {
prediction += ar_coefs[i] * (data[t - i - 1] - intercept);
}
}
for i in 0..self.q {
if t > i {
prediction += ma_coefs[i] * residuals[t - i - 1];
}
}
residuals[t] = data[t] - prediction;
}
Ok(residuals.slice(s![max_lag..]).to_owned())
}
fn estimate_mle(
&self,
data: &ArrayView1<f64>,
init_params: &ArimaParams,
) -> Result<ArimaParams> {
Ok(init_params.clone())
}
fn compute_log_likelihood(&self, residuals: &Array1<f64>, sigma2: f64) -> f64 {
let n = residuals.len() as f64;
let ss = residuals.iter().map(|&r| r * r).sum::<f64>();
-0.5 * n * (2.0 * std::f64::consts::PI * sigma2).ln() - 0.5 * ss / sigma2
}
pub fn forecast(
&self,
data: &ArrayView1<f64>,
params: &ArimaParams,
steps: usize,
) -> Result<Array1<f64>> {
let diff_data = self.difference(data)?;
let n = diff_data.len();
let mut extended = diff_data.to_owned();
let mut forecasts = Array1::zeros(steps);
for h in 0..steps {
let mut prediction = params.intercept;
for i in 0..self.p {
let idx = n + h - i - 1;
if idx < extended.len() {
prediction += params.ar_coefs[i] * (extended[idx] - params.intercept);
}
}
forecasts[h] = prediction;
extended = concatenate_arrays(&extended, &Array1::from_vec(vec![prediction]));
}
let integrated = self.integrate(&forecasts, data)?;
Ok(integrated)
}
fn integrate(
&self,
forecasts: &Array1<f64>,
original_data: &ArrayView1<f64>,
) -> Result<Array1<f64>> {
if self.d == 0 {
return Ok(forecasts.clone());
}
let mut result = forecasts.clone();
let n_orig = original_data.len();
for _ in 0..self.d {
let mut integrated = Array1::zeros(result.len());
let last_value = if n_orig > 0 {
original_data[n_orig - 1]
} else {
0.0
};
integrated[0] = last_value + result[0];
for i in 1..result.len() {
integrated[i] = integrated[i - 1] + result[i];
}
result = integrated;
}
Ok(result)
}
}
fn concatenate_arrays(a: &Array1<f64>, b: &Array1<f64>) -> Array1<f64> {
let mut result = Array1::zeros(a.len() + b.len());
result.slice_mut(s![..a.len()]).assign(a);
result.slice_mut(s![a.len()..]).assign(b);
result
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use scirs2_core::ndarray::Array1;
#[test]
fn test_arima_creation() {
let model = Arima::new(1, 1, 1);
assert_eq!(model.p, 1);
assert_eq!(model.d, 1);
assert_eq!(model.q, 1);
assert!(model.include_intercept);
}
#[test]
fn test_differencing() {
let data = Array1::from_vec(vec![1.0, 2.0, 4.0, 7.0, 11.0]);
let model = Arima::new(0, 1, 0);
let diff = model
.difference(&data.view())
.expect("differencing should succeed");
assert_eq!(diff.len(), 4);
assert_relative_eq!(diff[0], 1.0, epsilon = 1e-10);
assert_relative_eq!(diff[1], 2.0, epsilon = 1e-10);
assert_relative_eq!(diff[2], 3.0, epsilon = 1e-10);
assert_relative_eq!(diff[3], 4.0, epsilon = 1e-10);
}
#[test]
fn test_double_differencing() {
let data = Array1::from_vec(vec![1.0, 2.0, 4.0, 7.0, 11.0, 16.0]);
let model = Arima::new(0, 2, 0);
let diff = model
.difference(&data.view())
.expect("double differencing should succeed");
assert_eq!(diff.len(), 4);
assert_relative_eq!(diff[0], 1.0, epsilon = 1e-10);
assert_relative_eq!(diff[1], 1.0, epsilon = 1e-10);
}
#[test]
fn test_arima_fit_simple() {
let data = Array1::from_vec(vec![1.0, 1.5, 2.0, 2.3, 2.5, 2.7, 2.8, 2.9, 3.0, 3.05]);
let model = Arima::new(1, 0, 0);
let params = model.fit(&data.view()).expect("ARIMA fit should succeed");
assert!(params.ar_coefs.len() == 1);
assert!(params.ma_coefs.is_empty());
assert!(params.sigma2 > 0.0);
}
#[test]
fn test_arima_forecast() {
let data = Array1::from_vec(vec![1.0, 2.1, 2.9, 4.2, 4.8, 6.1, 6.9, 8.0, 9.1, 9.8]);
let model = Arima::new(1, 1, 0);
let params = model.fit(&data.view()).expect("fit should succeed");
let forecast = model
.forecast(&data.view(), ¶ms, 3)
.expect("forecast should succeed");
assert_eq!(forecast.len(), 3);
assert!(
forecast[0] > 8.0 && forecast[0] < 13.0,
"First forecast {} should be reasonable",
forecast[0]
);
}
#[test]
fn test_information_criteria() {
let data = Array1::from_vec(vec![1.0, 1.5, 2.2, 2.8, 3.1, 3.5, 3.8, 4.0, 4.3, 4.5]);
let model = Arima::new(1, 0, 1);
let params = model.fit(&data.view()).expect("fit should succeed");
assert!(params.aic.is_finite());
assert!(params.bic.is_finite());
assert!(params.bic > params.aic);
}
#[test]
fn test_insufficient_data_error() {
let data = Array1::from_vec(vec![1.0, 2.0]);
let model = Arima::new(2, 1, 2);
let result = model.fit(&data.view());
assert!(result.is_err());
}
}