use crate::linalg::LinalgInverse as _;
use crate::GreenersError;
use ndarray::{Array1, Array2, Array3, Axis};
use std::fmt;
#[derive(Debug)]
pub struct FavarResult {
pub factors: Array2<f64>,
pub loadings: Array2<f64>,
pub var_coeffs: Array2<f64>,
pub var_sigma: Array2<f64>,
pub irf: Array3<f64>,
pub variance_explained: Array1<f64>,
pub total_variance_explained: f64,
pub aic: f64,
pub bic: f64,
pub n_obs: usize,
pub n_series: usize,
pub n_factors: usize,
pub n_observed: usize,
pub lags: usize,
pub factor_names: Vec<String>,
pub observed_names: Vec<String>,
}
impl fmt::Display for FavarResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n{:=^78}", " FAVAR (Factor-Augmented VAR) ")?;
writeln!(f, "{:<20} {:>12}", "Observations:", self.n_obs)?;
writeln!(f, "{:<20} {:>12}", "Series (panel):", self.n_series)?;
writeln!(f, "{:<20} {:>12}", "Factors:", self.n_factors)?;
writeln!(f, "{:<20} {:>12}", "Observed vars:", self.n_observed)?;
writeln!(f, "{:<20} {:>12}", "VAR lags:", self.lags)?;
writeln!(
f,
"{:<20} {:>12.4}",
"Total var explained:", self.total_variance_explained
)?;
writeln!(f, "{:<20} {:>12.4}", "AIC:", self.aic)?;
writeln!(f, "{:<20} {:>12.4}", "BIC:", self.bic)?;
writeln!(f, "\n{:-^78}", "")?;
writeln!(f, " Variance explained by factor:")?;
for i in 0..self.n_factors {
writeln!(
f,
" Factor {:<6} {:>10.4}% ({})",
i + 1,
self.variance_explained[i] * 100.0,
self.factor_names.get(i).map(|s| s.as_str()).unwrap_or("")
)?;
}
writeln!(f, "\n VAR coefficients (first row = intercept):")?;
let k = self.n_factors + self.n_observed;
let mut header = String::from(" {:<12} ");
for j in 0..k {
let name = if j < self.n_factors {
self.factor_names.get(j).map(|s| s.as_str()).unwrap_or("?")
} else {
self.observed_names
.get(j - self.n_factors)
.map(|s| s.as_str())
.unwrap_or("?")
};
header.push_str(&format!("{:>10} ", name));
}
writeln!(f, "{header}")?;
for i in 0..self.var_coeffs.nrows() {
let label = if i == 0 {
"const".to_string()
} else {
let lag = (i - 1) / k + 1;
let var_idx = (i - 1) % k;
let name = if var_idx < self.n_factors {
self.factor_names
.get(var_idx)
.map(|s| s.as_str())
.unwrap_or("?")
} else {
self.observed_names
.get(var_idx - self.n_factors)
.map(|s| s.as_str())
.unwrap_or("?")
};
format!("L{lag}.{name}")
};
let mut row = format!(" {:<12} ", label);
for j in 0..k {
row.push_str(&format!("{:>10.4} ", self.var_coeffs[(i, j)]));
}
writeln!(f, "{row}")?;
}
write!(f, "{:=^78}", "")
}
}
pub struct FAVAR;
impl FAVAR {
pub fn fit(
x: &Array2<f64>,
observed: &Array2<f64>,
n_factors: usize,
lags: usize,
irf_steps: usize,
factor_names: Option<Vec<String>>,
observed_names: Option<Vec<String>>,
) -> Result<FavarResult, GreenersError> {
let t = x.nrows();
let n_series = x.ncols();
let n_obs = observed.ncols();
if observed.nrows() != t {
return Err(GreenersError::ShapeMismatch(
"FAVAR: x and observed must have same number of rows".into(),
));
}
if n_factors == 0 || lags == 0 {
return Err(GreenersError::InvalidOperation(
"FAVAR: n_factors and lags must be >= 1".into(),
));
}
if t < (lags + 1) * 3 {
return Err(GreenersError::InvalidOperation(
"FAVAR: too few observations".into(),
));
}
let x_std = Self::standardize(x);
let (factors, loadings, variance_explained, total_var) = Self::pca(&x_std, n_factors)?;
let k = n_factors + n_obs;
let mut combined = Array2::zeros((t, k));
for i in 0..t {
for j in 0..n_factors {
combined[(i, j)] = factors[(i, j)];
}
for j in 0..n_obs {
combined[(i, n_factors + j)] = observed[(i, j)];
}
}
let n_eff = t - lags;
let mut z = Array2::zeros((n_eff, 1 + k * lags));
let mut y_dep = Array2::zeros((n_eff, k));
for i in 0..n_eff {
let t_i = lags + i;
y_dep.row_mut(i).assign(&combined.row(t_i));
z[(i, 0)] = 1.0;
for p in 0..lags {
for j in 0..k {
z[(i, 1 + p * k + j)] = combined[(t_i - 1 - p, j)];
}
}
}
let zt = z.t();
let ztz = zt.dot(&z);
let ztz_reg = &ztz + Array2::eye(1 + k * lags) * 1e-8;
let ztz_inv = ztz_reg.inv()?;
let zty = zt.dot(&y_dep);
let var_coeffs: Array2<f64> = ztz_inv.dot(&zty);
let residuals = &y_dep - z.dot(&var_coeffs);
let var_sigma: Array2<f64> = residuals.t().dot(&residuals) / n_eff as f64;
let irf = if irf_steps > 0 {
Self::compute_irf(&var_coeffs, &var_sigma, lags, k, irf_steps)?
} else {
Array3::zeros((0, k, k))
};
let n_params = (1 + k * lags) * k;
let log_det_sigma = var_sigma
.inv()
.ok()
.map(|inv| {
let det = inv.mapv(|v| 1.0 / v).product();
det.ln()
})
.unwrap_or(0.0);
let ll = -(n_eff as f64) / 2.0
* (k as f64 * (2.0 * std::f64::consts::PI).ln() + log_det_sigma)
- 0.5 * residuals.mapv(|v| v * v).sum();
let aic = -2.0 * ll + 2.0 * n_params as f64;
let bic = -2.0 * ll + (n_eff as f64) * n_params as f64;
let f_names =
factor_names.unwrap_or_else(|| (0..n_factors).map(|i| format!("F{}", i + 1)).collect());
let o_names =
observed_names.unwrap_or_else(|| (0..n_obs).map(|i| format!("R{}", i + 1)).collect());
Ok(FavarResult {
factors,
loadings,
var_coeffs,
var_sigma,
irf,
variance_explained,
total_variance_explained: total_var,
aic,
bic,
n_obs: n_eff,
n_series,
n_factors,
n_observed: n_obs,
lags,
factor_names: f_names,
observed_names: o_names,
})
}
fn standardize(x: &Array2<f64>) -> Array2<f64> {
let t = x.nrows();
let n = x.ncols();
let mut x_std = Array2::zeros((t, n));
for j in 0..n {
let mean: f64 = x.column(j).mean().unwrap_or(0.0);
let std_val: f64 = x.column(j).std(0.0).max(1e-10);
for i in 0..t {
x_std[(i, j)] = (x[(i, j)] - mean) / std_val;
}
}
x_std
}
#[allow(clippy::type_complexity)]
fn pca(
x: &Array2<f64>,
n_factors: usize,
) -> Result<(Array2<f64>, Array2<f64>, Array1<f64>, f64), GreenersError> {
let t = x.nrows();
let n = x.ncols();
let xt = x.t();
let cov = xt.dot(x) / t as f64;
let mut factors = Array2::zeros((t, n_factors));
let mut loadings = Array2::zeros((n, n_factors));
let mut eigenvalues = Array1::zeros(n_factors);
let mut remaining = cov.clone();
let total_variance: f64 = cov.diag().sum();
let mut explained_sum = 0.0;
for f in 0..n_factors {
let (eigval, eigvec) = Self::power_iteration(&remaining, 100);
eigenvalues[f] = eigval;
loadings.column_mut(f).assign(&eigvec);
for i in 0..t {
factors[(i, f)] = x.row(i).dot(&eigvec);
}
remaining = &remaining
- &eigvec
.clone()
.insert_axis(Axis(1))
.dot(&eigvec.clone().insert_axis(Axis(0)))
* eigval;
explained_sum += eigval;
}
let variance_explained = eigenvalues.mapv(|v| v / total_variance.max(1e-10));
let total_var = explained_sum / total_variance.max(1e-10);
Ok((factors, loadings, variance_explained, total_var))
}
fn power_iteration(mat: &Array2<f64>, n_iter: usize) -> (f64, Array1<f64>) {
let n = mat.ncols();
let mut v = Array1::ones(n) / (n as f64).sqrt();
for _ in 0..n_iter {
let v_new = mat.dot(&v);
let norm = v_new.mapv(|x| x * x).sum().sqrt().max(1e-10);
v = v_new / norm;
}
let lambda = v.dot(&mat.dot(&v));
(lambda, v)
}
fn compute_irf(
coeffs: &Array2<f64>,
sigma: &Array2<f64>,
lags: usize,
k: usize,
steps: usize,
) -> Result<Array3<f64>, GreenersError> {
let chol = Self::cholesky_simple(sigma, k)?;
let mut phi = Array2::zeros((k * lags, k * lags));
for p in 0..lags {
for j in 0..k {
for i in 0..k {
phi[(p * k + j, i)] = coeffs[(1 + p * k + i, j)];
}
}
}
if lags > 1 {
for p in 1..lags {
for j in 0..k {
phi[(p * k + j, (p - 1) * k + j)] = 1.0;
}
}
}
let mut irf = Array3::zeros((steps, k, k));
let mut phi_power = Array2::eye(k * lags);
for h in 0..steps {
let phi_h = phi_power.slice(ndarray::s![0..k, 0..k]).to_owned();
irf.slice_mut(ndarray::s![h, .., ..])
.assign(&phi_h.dot(&chol));
phi_power = phi.dot(&phi_power);
}
Ok(irf)
}
fn cholesky_simple(sigma: &Array2<f64>, k: usize) -> Result<Array2<f64>, GreenersError> {
let mut chol = Array2::zeros((k, k));
for i in 0..k {
for j in 0..=i {
let mut sum = 0.0;
for l in 0..j {
sum += chol[(i, l)] * chol[(j, l)];
}
if i == j {
let val = sigma[(i, i)] - sum;
if val < 0.0 {
chol[(i, j)] = 1e-10;
} else {
chol[(i, j)] = val.sqrt();
}
} else {
let denom = chol[(j, j)].max(1e-10);
chol[(i, j)] = (sigma[(i, j)] - sum) / denom;
}
}
}
Ok(chol)
}
}