use crate::linalg::LinalgInverse as _;
use crate::GreenersError;
use ndarray::{Array1, Array2, Axis};
use std::fmt;
#[derive(Debug)]
pub struct JohansenBreakResult {
pub trace_stats: Array1<f64>,
pub lambda_max_stats: Array1<f64>,
pub trace_cv_5: Array1<f64>,
pub lambda_max_cv_5: Array1<f64>,
pub eigenvalues: Array1<f64>,
pub cointegrating_vectors: Array2<f64>,
pub cointegration_rank: usize,
pub n_vars: usize,
pub lags: usize,
pub n_breaks: usize,
pub break_points: Vec<usize>,
pub n_obs: usize,
}
impl fmt::Display for JohansenBreakResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"\n{:=^78}",
" Johansen Cointegration Test (with Structural Breaks) "
)?;
writeln!(f, "{:<20} {:>12}", "Variables:", self.n_vars)?;
writeln!(f, "{:<20} {:>12}", "Lags:", self.lags)?;
writeln!(f, "{:<20} {:>12}", "Observations:", self.n_obs)?;
writeln!(f, "{:<20} {:>12}", "Structural breaks:", self.n_breaks)?;
if self.n_breaks > 0 {
let bp: Vec<String> = self.break_points.iter().map(|b| b.to_string()).collect();
writeln!(f, "{:<20} {}", "Break points:", bp.join(", "))?;
}
writeln!(f, "\n{:-^78}", "")?;
writeln!(
f,
"{:<6} {:>12} {:>12} {:>12} {:>12} {:>10}",
"Rank", "Trace", "Trace 5%", "L-max", "L-max 5%", "Sig?"
)?;
writeln!(f, "{:-^78}", "")?;
for r in 0..self.n_vars {
let sig = if self.trace_stats[r] > self.trace_cv_5[r] {
"**"
} else {
""
};
writeln!(
f,
"{:<6} {:>12.4} {:>12.4} {:>12.4} {:>12.4} {:>10}",
r,
self.trace_stats[r],
self.trace_cv_5[r],
self.lambda_max_stats[r],
self.lambda_max_cv_5[r],
sig
)?;
}
writeln!(
f,
"\n Selected cointegration rank: {} (at 5% level)",
self.cointegration_rank
)?;
if self.cointegration_rank > 0 {
writeln!(f, "\n Cointegrating vectors:")?;
for j in 0..self.cointegration_rank {
let mut row = format!(" Vector {j}: ");
for i in 0..self.n_vars {
row.push_str(&format!("{:>10.4} ", self.cointegrating_vectors[(i, j)]));
}
writeln!(f, "{row}")?;
}
}
write!(f, "{:=^78}", "")
}
}
pub struct JohansenBreak;
impl JohansenBreak {
pub fn fit(
y: &Array2<f64>,
lags: usize,
break_points: &[usize],
) -> Result<JohansenBreakResult, GreenersError> {
let t = y.nrows();
let k = y.ncols();
if t < (lags + k + 1) * 2 {
return Err(GreenersError::InvalidOperation(
"Johansen: too few observations".into(),
));
}
if lags == 0 {
return Err(GreenersError::InvalidOperation(
"Johansen: lags must be >= 1".into(),
));
}
let n_eff = t - lags - 1;
let n_breaks = break_points.len();
let mut d_shift = Array2::zeros((t, n_breaks));
for (b, &bp) in break_points.iter().enumerate() {
for i in bp..t {
d_shift[(i, b)] = 1.0;
}
}
let mut dy = Array2::zeros((n_eff, k));
let mut y_lag1 = Array2::zeros((n_eff, k)); let mut dy_lags = Array2::zeros((n_eff, k * (lags - 1).max(1))); let mut d_shift_reg = Array2::zeros((n_eff, n_breaks));
for i in 0..n_eff {
let t_i = lags + 1 + i;
dy.row_mut(i).assign(&(&y.row(t_i) - &y.row(t_i - 1)));
y_lag1.row_mut(i).assign(&y.row(t_i - 1));
for p in 1..lags {
for j in 0..k {
dy_lags[(i, (p - 1) * k + j)] = y[(t_i - p, j)] - y[(t_i - p - 1, j)];
}
}
for b in 0..n_breaks {
d_shift_reg[(i, b)] = d_shift[(t_i, b)];
}
}
let n_reg = 1 + k * (lags - 1).max(1) + n_breaks;
let mut z = Array2::zeros((n_eff, n_reg));
for i in 0..n_eff {
z[(i, 0)] = 1.0;
for j in 0..k * (lags - 1).max(1) {
z[(i, 1 + j)] = dy_lags[(i, j)];
}
for b in 0..n_breaks {
z[(i, 1 + k * (lags - 1).max(1) + b)] = d_shift_reg[(i, b)];
}
}
let zt = z.t();
let ztz = zt.dot(&z);
let ztz_reg = &ztz + Array2::eye(n_reg) * 1e-8;
let ztz_inv = ztz_reg.inv()?;
let r0 = &dy - z.dot(&ztz_inv.dot(&zt.dot(&dy)));
let r1 = &y_lag1 - z.dot(&ztz_inv.dot(&zt.dot(&y_lag1)));
let s00 = r0.t().dot(&r0) / n_eff as f64;
let s01 = r0.t().dot(&r1) / n_eff as f64;
let s10 = r1.t().dot(&r0) / n_eff as f64;
let s11 = r1.t().dot(&r1) / n_eff as f64;
let s00_inv = (&s00 + Array2::eye(k) * 1e-10).inv()?;
let m = s10.dot(&s00_inv).dot(&s01);
let s11_inv = (&s11 + Array2::eye(k) * 1e-10).inv()?;
let eig_matrix = s11_inv.dot(&m);
let (eigenvalues, eigenvectors) = Self::eigen_decomposition(&eig_matrix, k);
let mut sorted: Vec<(usize, f64)> = eigenvalues
.iter()
.enumerate()
.map(|(i, &v)| (i, v))
.collect();
sorted.sort_by(|a, b| b.1.total_cmp(&a.1));
let sorted_eigenvalues: Array1<f64> =
sorted.iter().map(|&(_, v)| v.clamp(0.0, 1.0)).collect();
let mut sorted_eigenvectors = Array2::zeros((k, k));
for (new_idx, &(orig_idx, _)) in sorted.iter().enumerate() {
for i in 0..k {
sorted_eigenvectors[(i, new_idx)] = eigenvectors[(i, orig_idx)];
}
}
let mut trace_stats = Array1::zeros(k);
let mut lambda_max_stats = Array1::zeros(k);
for r in 0..k {
let mut trace = 0.0;
for i in r..k {
let lambda = sorted_eigenvalues[i].min(0.99999);
trace += (1.0 - lambda).ln();
}
trace_stats[r] = -(n_eff as f64) * trace;
if r < k {
let lambda = sorted_eigenvalues[r].min(0.99999);
lambda_max_stats[r] = -(n_eff as f64) * (1.0 - lambda).ln();
}
}
let break_adj = n_breaks as f64 * 0.5;
let trace_cv_5 = Self::trace_cv_5(k, break_adj);
let lambda_max_cv_5 = Self::lambda_max_cv_5(k, break_adj);
let mut rank = 0;
for r in 0..k {
if trace_stats[r] > trace_cv_5[r] {
rank = r + 1;
}
}
let cointegrating_vectors = if rank > 0 {
sorted_eigenvectors
.slice(ndarray::s![.., 0..rank])
.to_owned()
} else {
Array2::zeros((k, 0))
};
Ok(JohansenBreakResult {
trace_stats,
lambda_max_stats,
trace_cv_5,
lambda_max_cv_5,
eigenvalues: sorted_eigenvalues,
cointegrating_vectors,
cointegration_rank: rank,
n_vars: k,
lags,
n_breaks,
break_points: break_points.to_vec(),
n_obs: n_eff,
})
}
fn trace_cv_5(k: usize, break_adj: f64) -> Array1<f64> {
let cv_table: Vec<Vec<f64>> = vec![
vec![
27.58, 29.96, 32.45, 34.87, 37.52, 40.15, 42.68, 45.10, 47.51, 49.85,
],
vec![
13.31, 15.09, 16.85, 18.63, 20.39, 22.17, 23.95, 25.72, 27.49, 29.25,
],
vec![2.71, 3.76, 4.13, 4.40, 4.65, 4.88, 5.09, 5.30, 5.50, 5.69],
];
let mut cv = Array1::zeros(k);
for r in 0..k {
let idx = (k - r - 1).min(9);
let row = if k - r <= 1 {
2
} else if k - r <= 2 {
1
} else {
0
};
cv[r] = cv_table[row.min(2)][idx] + break_adj * (k - r) as f64;
}
cv
}
fn lambda_max_cv_5(k: usize, break_adj: f64) -> Array1<f64> {
let cv_table: Vec<Vec<f64>> = vec![
vec![
12.25, 14.26, 16.26, 18.17, 20.16, 22.12, 24.04, 25.90, 27.73, 29.52,
],
vec![
9.24, 11.22, 13.17, 15.09, 17.04, 18.97, 20.89, 22.79, 24.67, 26.54,
],
vec![2.71, 3.76, 4.13, 4.40, 4.65, 4.88, 5.09, 5.30, 5.50, 5.69],
];
let mut cv = Array1::zeros(k);
for r in 0..k {
let idx = (k - r - 1).min(9);
let row = if k - r <= 1 {
2
} else if k - r <= 2 {
1
} else {
0
};
cv[r] = cv_table[row.min(2)][idx] + break_adj;
}
cv
}
fn eigen_decomposition(mat: &Array2<f64>, k: usize) -> (Array1<f64>, Array2<f64>) {
let mut eigenvalues = Array1::zeros(k);
let mut eigenvectors = Array2::zeros((k, k));
let mut remaining = mat.clone();
for i in 0..k {
let (eigval, eigvec) = Self::power_iteration(&remaining, 200);
eigenvalues[i] = eigval;
for j in 0..k {
eigenvectors[(j, i)] = eigvec[j];
}
remaining = &remaining
- &eigvec
.clone()
.insert_axis(Axis(1))
.dot(&eigvec.clone().insert_axis(Axis(0)))
* eigval;
}
(eigenvalues, eigenvectors)
}
fn power_iteration(mat: &Array2<f64>, n_iter: usize) -> (f64, Array1<f64>) {
let k = mat.ncols();
let mut v = Array1::ones(k) / (k 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)
}
}