use crate::error::{ForecastError, Result};
use std::collections::HashMap;
use crate::utils::ols::ols_fit;
#[derive(Debug, Clone)]
pub struct VAR {
order: usize,
n_vars: usize,
coefficients: Option<Vec<Vec<Vec<f64>>>>,
intercepts: Option<Vec<f64>>,
residuals: Option<Vec<Vec<f64>>>,
fitted_values: Option<Vec<Vec<f64>>>,
training_data: Option<Vec<Vec<f64>>>,
n_effective: usize,
}
impl VAR {
pub fn new(order: usize) -> Self {
Self {
order,
n_vars: 0,
coefficients: None,
intercepts: None,
residuals: None,
fitted_values: None,
training_data: None,
n_effective: 0,
}
}
pub fn fit(&mut self, data: &[Vec<f64>]) -> Result<()> {
let k = data.len();
if k == 0 {
return Err(ForecastError::EmptyData);
}
if self.order == 0 {
return Err(ForecastError::InvalidParameter(
"VAR order must be at least 1".into(),
));
}
let n = data[0].len();
if n == 0 {
return Err(ForecastError::EmptyData);
}
for (i, series) in data.iter().enumerate() {
if series.len() != n {
return Err(ForecastError::DimensionMismatch {
expected: n,
got: series.len(),
});
}
if series.iter().any(|v| v.is_nan() || v.is_infinite()) {
return Err(ForecastError::InvalidParameter(format!(
"Variable {} contains NaN or Inf values",
i
)));
}
}
let p = self.order;
if n <= p {
return Err(ForecastError::InsufficientData {
needed: p + 1,
got: n,
hint: Some(format!(
"VAR({}) requires at least {} observations",
p,
p + 1
)),
});
}
let n_eff = n - p;
self.n_vars = k;
self.n_effective = n_eff;
let regressor_map = build_regressor_map(data, k, p, n);
let mut coefficients = vec![vec![vec![0.0; p]; k]; k];
let mut intercepts = vec![0.0; k];
let mut residuals = vec![vec![0.0; n_eff]; k];
let mut fitted_vals = vec![vec![0.0; n_eff]; k];
for eq in 0..k {
let y: Vec<f64> = data[eq][p..n].to_vec();
let ols_result = ols_fit(&y, ®ressor_map)?;
intercepts[eq] = ols_result.intercept;
for lag in 1..=p {
for var in 0..k {
let name = regressor_name(var, lag);
if let Some(idx) = ols_result.regressor_names.iter().position(|n| n == &name) {
coefficients[eq][var][lag - 1] = ols_result.coefficients[idx];
}
}
}
let predictions = ols_result.predict(®ressor_map)?;
for t in 0..n_eff {
fitted_vals[eq][t] = predictions[t];
residuals[eq][t] = y[t] - predictions[t];
}
}
self.coefficients = Some(coefficients);
self.intercepts = Some(intercepts);
self.residuals = Some(residuals);
self.fitted_values = Some(fitted_vals);
self.training_data = Some(data.to_vec());
Ok(())
}
pub fn predict(&self, horizon: usize) -> Result<Vec<Vec<f64>>> {
let coefficients = self
.coefficients
.as_ref()
.ok_or(ForecastError::FitRequired { model: None })?;
let intercepts = self
.intercepts
.as_ref()
.ok_or(ForecastError::FitRequired { model: None })?;
let training_data = self
.training_data
.as_ref()
.ok_or(ForecastError::FitRequired { model: None })?;
if horizon == 0 {
return Err(ForecastError::InvalidParameter(
"Forecast horizon must be at least 1".into(),
));
}
let k = self.n_vars;
let p = self.order;
let n = training_data[0].len();
let mut history: Vec<Vec<f64>> = (0..k)
.map(|var| training_data[var][(n - p)..n].to_vec())
.collect();
let mut forecasts = vec![vec![0.0; horizon]; k];
for h in 0..horizon {
let mut y_new = vec![0.0; k];
for eq in 0..k {
let mut val = intercepts[eq];
for lag in 1..=p {
for var in 0..k {
let hist_len = history[var].len();
val += coefficients[eq][var][lag - 1] * history[var][hist_len - lag];
}
}
y_new[eq] = val;
}
for var in 0..k {
history[var].push(y_new[var]);
forecasts[var][h] = y_new[var];
}
}
Ok(forecasts)
}
pub fn fitted_values(&self) -> Option<&Vec<Vec<f64>>> {
self.fitted_values.as_ref()
}
pub fn residuals(&self) -> Option<&Vec<Vec<f64>>> {
self.residuals.as_ref()
}
pub fn coefficients(&self) -> Option<&Vec<Vec<Vec<f64>>>> {
self.coefficients.as_ref()
}
pub fn intercepts(&self) -> Option<&Vec<f64>> {
self.intercepts.as_ref()
}
pub fn order(&self) -> usize {
self.order
}
pub fn n_vars(&self) -> usize {
self.n_vars
}
pub fn granger_causality_test(&self, cause: usize, effect: usize) -> Result<f64> {
let residuals = self
.residuals
.as_ref()
.ok_or(ForecastError::FitRequired { model: None })?;
let training_data = self
.training_data
.as_ref()
.ok_or(ForecastError::FitRequired { model: None })?;
let k = self.n_vars;
let p = self.order;
if cause >= k {
return Err(ForecastError::IndexOutOfBounds {
index: cause,
size: k,
});
}
if effect >= k {
return Err(ForecastError::IndexOutOfBounds {
index: effect,
size: k,
});
}
if cause == effect {
return Err(ForecastError::InvalidParameter(
"Cause and effect variables must be different".into(),
));
}
let n_eff = self.n_effective;
let n = training_data[0].len();
let rss_u: f64 = residuals[effect].iter().map(|r| r * r).sum();
let y_effect: Vec<f64> = training_data[effect][p..n].to_vec();
let mut restricted_regressors: HashMap<String, Vec<f64>> = HashMap::new();
for lag in 1..=p {
for var in 0..k {
if var == cause {
continue;
}
let name = regressor_name(var, lag);
let values: Vec<f64> = (p..n).map(|t| training_data[var][t - lag]).collect();
restricted_regressors.insert(name, values);
}
}
let restricted_ols = ols_fit(&y_effect, &restricted_regressors)?;
let restricted_predictions = if restricted_regressors.is_empty() {
vec![restricted_ols.intercept; n_eff]
} else {
restricted_ols.predict(&restricted_regressors)?
};
let rss_r: f64 = (0..n_eff)
.map(|t| {
let r = y_effect[t] - restricted_predictions[t];
r * r
})
.sum();
let df_num = p as f64;
let df_den = n_eff as f64 - (k * p) as f64 - 1.0;
if df_den <= 0.0 {
return Err(ForecastError::InsufficientData {
needed: k * p + 2,
got: n_eff,
hint: Some("Not enough observations for Granger causality test".into()),
});
}
let f_stat = ((rss_r - rss_u) / df_num) / (rss_u / df_den);
Ok(f_stat)
}
}
fn build_regressor_map(
data: &[Vec<f64>],
k: usize,
p: usize,
n: usize,
) -> HashMap<String, Vec<f64>> {
let mut regressor_map: HashMap<String, Vec<f64>> = HashMap::new();
for lag in 1..=p {
for var in 0..k {
let name = regressor_name(var, lag);
let values: Vec<f64> = (p..n).map(|t| data[var][t - lag]).collect();
regressor_map.insert(name, values);
}
}
regressor_map
}
fn regressor_name(var: usize, lag: usize) -> String {
format!("y{}_lag{}", var, lag)
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_var1_data(n: usize, c: [f64; 2], a: [[f64; 2]; 2], seed: u64) -> Vec<Vec<f64>> {
use rand::rngs::StdRng;
use rand::Rng;
use rand::SeedableRng;
let mut rng = StdRng::seed_from_u64(seed);
let mut y1 = vec![0.0; n];
let mut y2 = vec![0.0; n];
y1[0] = rng.gen_range(-1.0..1.0);
y2[0] = rng.gen_range(-1.0..1.0);
for t in 1..n {
let e1: f64 = rng.gen_range(-0.01..0.01);
let e2: f64 = rng.gen_range(-0.01..0.01);
y1[t] = c[0] + a[0][0] * y1[t - 1] + a[0][1] * y2[t - 1] + e1;
y2[t] = c[1] + a[1][0] * y1[t - 1] + a[1][1] * y2[t - 1] + e2;
}
vec![y1, y2]
}
#[test]
fn fit_and_predict_basic() {
let data = generate_var1_data(100, [0.5, 0.3], [[0.6, 0.1], [0.05, 0.7]], 42);
let mut model = VAR::new(1);
model.fit(&data).unwrap();
let forecasts = model.predict(5).unwrap();
assert_eq!(forecasts.len(), 2);
assert_eq!(forecasts[0].len(), 5);
assert_eq!(forecasts[1].len(), 5);
}
#[test]
fn coefficient_recovery() {
let c = [0.5, 0.3];
let a = [[0.6, 0.1], [0.05, 0.7]];
let data = generate_var1_data(500, c, a, 123);
let mut model = VAR::new(1);
model.fit(&data).unwrap();
let coefs = model.coefficients().unwrap();
let intercepts = model.intercepts().unwrap();
assert!(
(intercepts[0] - c[0]).abs() < 0.1,
"intercept[0]: expected ~{}, got {}",
c[0],
intercepts[0]
);
assert!(
(intercepts[1] - c[1]).abs() < 0.1,
"intercept[1]: expected ~{}, got {}",
c[1],
intercepts[1]
);
assert!(
(coefs[0][0][0] - a[0][0]).abs() < 0.05,
"a[0][0]: expected ~{}, got {}",
a[0][0],
coefs[0][0][0]
);
assert!(
(coefs[0][1][0] - a[0][1]).abs() < 0.05,
"a[0][1]: expected ~{}, got {}",
a[0][1],
coefs[0][1][0]
);
assert!(
(coefs[1][0][0] - a[1][0]).abs() < 0.05,
"a[1][0]: expected ~{}, got {}",
a[1][0],
coefs[1][0][0]
);
assert!(
(coefs[1][1][0] - a[1][1]).abs() < 0.05,
"a[1][1]: expected ~{}, got {}",
a[1][1],
coefs[1][1][0]
);
}
#[test]
fn forecast_dimensions() {
let data = generate_var1_data(50, [0.1, 0.2], [[0.5, 0.0], [0.0, 0.5]], 99);
let mut model = VAR::new(2);
model.fit(&data).unwrap();
for h in [1, 5, 10, 20] {
let forecasts = model.predict(h).unwrap();
assert_eq!(forecasts.len(), 2, "should have 2 variables");
assert_eq!(forecasts[0].len(), h, "horizon mismatch for h={}", h);
assert_eq!(forecasts[1].len(), h, "horizon mismatch for h={}", h);
}
}
#[test]
fn single_variable_degenerates_to_ar() {
use rand::rngs::StdRng;
use rand::Rng;
use rand::SeedableRng;
let mut rng = StdRng::seed_from_u64(77);
let n = 200;
let phi = 0.8;
let c = 1.0;
let mut y = vec![0.0; n];
y[0] = rng.gen_range(-1.0..1.0);
for t in 1..n {
y[t] = c + phi * y[t - 1] + rng.gen_range(-0.01..0.01);
}
let mut model = VAR::new(1);
model.fit(&[y]).unwrap();
let coefs = model.coefficients().unwrap();
let intercepts = model.intercepts().unwrap();
assert_eq!(coefs.len(), 1);
assert_eq!(coefs[0].len(), 1);
assert_eq!(coefs[0][0].len(), 1);
assert!(
(coefs[0][0][0] - phi).abs() < 0.05,
"phi: expected ~{}, got {}",
phi,
coefs[0][0][0]
);
assert!(
(intercepts[0] - c).abs() < 0.15,
"c: expected ~{}, got {}",
c,
intercepts[0]
);
}
#[test]
fn insufficient_data_error() {
let data = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
let mut model = VAR::new(2);
let result = model.fit(&data);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ForecastError::InsufficientData { .. }
));
}
#[test]
fn empty_data_error() {
let mut model = VAR::new(1);
assert!(matches!(
model.fit(&[]).unwrap_err(),
ForecastError::EmptyData
));
}
#[test]
fn zero_order_error() {
let mut model = VAR::new(0);
let data = vec![vec![1.0, 2.0, 3.0]];
assert!(matches!(
model.fit(&data).unwrap_err(),
ForecastError::InvalidParameter(_)
));
}
#[test]
fn predict_before_fit_error() {
let model = VAR::new(1);
assert!(matches!(
model.predict(5).unwrap_err(),
ForecastError::FitRequired { .. }
));
}
#[test]
fn zero_horizon_error() {
let data = generate_var1_data(50, [0.0, 0.0], [[0.5, 0.0], [0.0, 0.5]], 1);
let mut model = VAR::new(1);
model.fit(&data).unwrap();
assert!(matches!(
model.predict(0).unwrap_err(),
ForecastError::InvalidParameter(_)
));
}
#[test]
fn dimension_mismatch_error() {
let data = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0]];
let mut model = VAR::new(1);
assert!(matches!(
model.fit(&data).unwrap_err(),
ForecastError::DimensionMismatch { .. }
));
}
#[test]
fn residuals_and_fitted_values() {
let data = generate_var1_data(50, [0.1, 0.2], [[0.5, 0.1], [0.1, 0.5]], 55);
let mut model = VAR::new(1);
model.fit(&data).unwrap();
let residuals = model.residuals().unwrap();
let fitted = model.fitted_values().unwrap();
assert_eq!(residuals.len(), 2);
assert_eq!(fitted.len(), 2);
assert_eq!(residuals[0].len(), 49);
assert_eq!(fitted[0].len(), 49);
for var in 0..2 {
for t in 0..49 {
let reconstructed = fitted[var][t] + residuals[var][t];
let original = data[var][t + 1]; assert!(
(reconstructed - original).abs() < 1e-10,
"Reconstruction mismatch at var={}, t={}",
var,
t
);
}
}
}
#[test]
fn granger_causality_on_causal_system() {
let data = generate_var1_data(300, [0.0, 0.0], [[0.5, 0.0], [0.3, 0.5]], 42);
let mut model = VAR::new(1);
model.fit(&data).unwrap();
let f_12 = model.granger_causality_test(0, 1).unwrap();
let f_21 = model.granger_causality_test(1, 0).unwrap();
assert!(
f_12 > f_21,
"y1->y2 F-stat ({}) should be larger than y2->y1 F-stat ({})",
f_12,
f_21
);
assert!(
f_12 > 4.0,
"y1->y2 F-stat ({}) should be significant (> 4.0)",
f_12
);
}
#[test]
fn granger_causality_same_variable_error() {
let data = generate_var1_data(50, [0.0, 0.0], [[0.5, 0.0], [0.0, 0.5]], 1);
let mut model = VAR::new(1);
model.fit(&data).unwrap();
assert!(matches!(
model.granger_causality_test(0, 0).unwrap_err(),
ForecastError::InvalidParameter(_)
));
}
#[test]
fn granger_causality_out_of_bounds_error() {
let data = generate_var1_data(50, [0.0, 0.0], [[0.5, 0.0], [0.0, 0.5]], 1);
let mut model = VAR::new(1);
model.fit(&data).unwrap();
assert!(matches!(
model.granger_causality_test(2, 0).unwrap_err(),
ForecastError::IndexOutOfBounds { .. }
));
assert!(matches!(
model.granger_causality_test(0, 2).unwrap_err(),
ForecastError::IndexOutOfBounds { .. }
));
}
#[test]
fn var2_model() {
use rand::rngs::StdRng;
use rand::Rng;
use rand::SeedableRng;
let mut rng = StdRng::seed_from_u64(88);
let n = 500;
let mut y1 = vec![0.0; n];
let mut y2 = vec![0.0; n];
y1[0] = 0.1;
y1[1] = 0.2;
y2[0] = -0.1;
y2[1] = 0.0;
for t in 2..n {
y1[t] =
0.4 * y1[t - 1] + 0.1 * y1[t - 2] + 0.05 * y2[t - 1] + rng.gen_range(-0.01..0.01);
y2[t] =
0.3 * y2[t - 1] + 0.15 * y2[t - 2] + 0.1 * y1[t - 1] + rng.gen_range(-0.01..0.01);
}
let data = vec![y1, y2];
let mut model = VAR::new(2);
model.fit(&data).unwrap();
let coefs = model.coefficients().unwrap();
assert!(
(coefs[0][0][0] - 0.4).abs() < 0.05,
"y1 lag1 in eq0: {}",
coefs[0][0][0]
);
assert!(
(coefs[0][0][1] - 0.1).abs() < 0.05,
"y1 lag2 in eq0: {}",
coefs[0][0][1]
);
assert!(
(coefs[0][1][0] - 0.05).abs() < 0.05,
"y2 lag1 in eq0: {}",
coefs[0][1][0]
);
let forecasts = model.predict(10).unwrap();
assert_eq!(forecasts.len(), 2);
assert_eq!(forecasts[0].len(), 10);
}
#[test]
fn three_variable_system() {
use rand::rngs::StdRng;
use rand::Rng;
use rand::SeedableRng;
let mut rng = StdRng::seed_from_u64(55);
let n = 150;
let mut y = vec![vec![0.0; n]; 3];
for var in 0..3 {
y[var][0] = rng.gen_range(-0.5..0.5);
}
for t in 1..n {
y[0][t] = 0.5 * y[0][t - 1] + rng.gen_range(-0.01..0.01);
y[1][t] = 0.3 * y[0][t - 1] + 0.4 * y[1][t - 1] + rng.gen_range(-0.01..0.01);
y[2][t] = 0.2 * y[1][t - 1] + 0.3 * y[2][t - 1] + rng.gen_range(-0.01..0.01);
}
let mut model = VAR::new(1);
model.fit(&y).unwrap();
assert_eq!(model.n_vars(), 3);
let forecasts = model.predict(5).unwrap();
assert_eq!(forecasts.len(), 3);
for var in 0..3 {
assert_eq!(forecasts[var].len(), 5);
}
let coefs = model.coefficients().unwrap();
assert_eq!(coefs.len(), 3);
assert_eq!(coefs[0].len(), 3);
assert_eq!(coefs[0][0].len(), 1);
}
#[test]
fn nan_in_data_error() {
let data = vec![vec![1.0, f64::NAN, 3.0], vec![4.0, 5.0, 6.0]];
let mut model = VAR::new(1);
assert!(model.fit(&data).is_err());
}
#[test]
fn granger_before_fit_error() {
let model = VAR::new(1);
assert!(matches!(
model.granger_causality_test(0, 1).unwrap_err(),
ForecastError::FitRequired { .. }
));
}
}