use crate::linalg::LinalgInverse as _;
use crate::GreenersError;
use ndarray::{Array1, Array2};
use std::fmt;
pub struct MarkovAutoregression;
#[derive(Debug)]
pub struct MarkovAutoregResult {
pub regime_means: Array1<f64>,
pub ar_params: Array2<f64>,
pub regime_sigmas: Array1<f64>,
pub transition_matrix: Array2<f64>,
pub smoothed_probs: Array2<f64>,
pub filtered_probs: Array2<f64>,
pub log_likelihood: f64,
pub aic: f64,
pub bic: f64,
pub n_obs: usize,
pub k_regimes: usize,
pub ar_order: usize,
}
impl MarkovAutoregResult {
pub fn predict_regime(&self) -> Array1<usize> {
let t = self.smoothed_probs.nrows();
let mut regimes = Array1::<usize>::zeros(t);
for i in 0..t {
let mut best_j = 0;
let mut best_p = self.smoothed_probs[[i, 0]];
for j in 1..self.k_regimes {
if self.smoothed_probs[[i, j]] > best_p {
best_p = self.smoothed_probs[[i, j]];
best_j = j;
}
}
regimes[i] = best_j;
}
regimes
}
}
impl fmt::Display for MarkovAutoregResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"\n{:=^78}",
format!(
" Markov Autoregression AR({}) — {} regimes ",
self.ar_order, self.k_regimes
)
)?;
writeln!(f, "{:<20} {:>10}", "Observations:", self.n_obs)?;
writeln!(f, "{:<20} {:>10.4}", "Log-likelihood:", self.log_likelihood)?;
writeln!(f, "{:<20} {:>10.4}", "AIC:", self.aic)?;
writeln!(f, "{:<20} {:>10.4}", "BIC:", self.bic)?;
writeln!(f, "\n{:-^78}", " Transition Matrix ")?;
for row in self.transition_matrix.rows() {
write!(f, "[ ")?;
for val in row {
write!(f, "{:>8.4} ", val)?;
}
writeln!(f, "]")?;
}
for j in 0..self.k_regimes {
writeln!(f, "\n{:-^78}", format!(" Regime {} ", j))?;
writeln!(f, "{:<20} {:>10.4}", "Sigma:", self.regime_sigmas[j])?;
writeln!(f, "{:<20} {:>10.4}", "Intercept:", self.regime_means[j])?;
for lag in 0..self.ar_order {
writeln!(
f,
"{:<20} {:>10.4}",
format!("AR.L{}", lag + 1),
self.ar_params[[j, lag]]
)?;
}
}
writeln!(f, "\n{:-^78}", " Expected Durations ")?;
for j in 0..self.k_regimes {
let p_jj = self.transition_matrix[[j, j]];
let dur = if (1.0 - p_jj).abs() < 1e-15 {
f64::INFINITY
} else {
1.0 / (1.0 - p_jj)
};
writeln!(f, "Regime {}: {:.1} periods", j, dur)?;
}
writeln!(f, "{:=^78}", "")
}
}
impl MarkovAutoregression {
pub fn fit(
y: &Array1<f64>,
k_regimes: usize,
ar_order: usize,
) -> Result<MarkovAutoregResult, GreenersError> {
let n = y.len();
let p = ar_order;
let k = k_regimes;
if n < p + 10 {
return Err(GreenersError::ShapeMismatch(
"Series too short for MarkovAutoregression".into(),
));
}
if k < 2 {
return Err(GreenersError::ShapeMismatch(
"Need at least 2 regimes".into(),
));
}
let effective_n = n - p;
let y_mean = y.mean().unwrap_or(0.0);
let y_var = y.iter().map(|v| (v - y_mean).powi(2)).sum::<f64>() / n as f64;
let mut regime_means = Array1::<f64>::zeros(k);
for j in 0..k {
regime_means[j] = y_mean + (j as f64 - (k - 1) as f64 / 2.0) * y_var.sqrt();
}
let mut ar_params = Array2::<f64>::zeros((k, p.max(1)));
let ar_params_cols = p;
if p > 0 {
ar_params = Array2::<f64>::zeros((k, p));
for j in 0..k {
for lag in 0..p {
ar_params[[j, lag]] = 0.1 / (1 + lag) as f64;
}
}
}
let mut regime_sigmas = Array1::<f64>::zeros(k);
for j in 0..k {
regime_sigmas[j] = (y_var * (0.5 + j as f64)).sqrt();
}
let mut trans = Array2::<f64>::zeros((k, k));
for i in 0..k {
for j in 0..k {
if i == j {
trans[[i, j]] = 0.9;
} else {
trans[[i, j]] = 0.1 / (k - 1) as f64;
}
}
}
let max_iter = 200;
let tol = 1e-6;
let mut prev_ll = f64::NEG_INFINITY;
let mut filtered_probs = Array2::<f64>::zeros((effective_n, k));
let mut smoothed_probs = Array2::<f64>::zeros((effective_n, k));
let mut log_likelihood = 0.0;
for _iter in 0..max_iter {
let mut xi_filtered = Array2::<f64>::zeros((effective_n, k));
log_likelihood = 0.0;
let mut xi_prev = Array1::from_elem(k, 1.0 / k as f64);
for t in 0..effective_n {
let t_idx = t + p;
let mut eta = Array1::<f64>::zeros(k);
for j in 0..k {
let mut y_hat = regime_means[j];
for lag in 0..p {
y_hat += ar_params[[j, lag]] * y[t_idx - 1 - lag];
}
let resid = y[t_idx] - y_hat;
let var = (regime_sigmas[j] * regime_sigmas[j]).max(1e-10);
eta[j] = (-0.5 * resid * resid / var).exp()
/ (2.0 * std::f64::consts::PI * var).sqrt();
}
let xi_pred = trans.t().dot(&xi_prev);
let joint = &eta * &xi_pred;
let f_t: f64 = joint.sum();
if f_t > 1e-30 {
let xi_filt = &joint / f_t;
xi_filtered.row_mut(t).assign(&xi_filt);
xi_prev = xi_filt;
log_likelihood += f_t.ln();
} else {
xi_filtered.row_mut(t).fill(1.0 / k as f64);
xi_prev = Array1::from_elem(k, 1.0 / k as f64);
}
}
filtered_probs = xi_filtered.clone();
smoothed_probs = xi_filtered.clone();
for t in (0..effective_n - 1).rev() {
let xi_filt_t = xi_filtered.row(t).to_owned();
let xi_pred_next = trans.t().dot(&xi_filt_t);
for j in 0..k {
let mut sum = 0.0;
for l in 0..k {
let pred_l = xi_pred_next[l].max(1e-30);
sum += trans[[j, l]] * smoothed_probs[[t + 1, l]] / pred_l;
}
smoothed_probs[[t, j]] = xi_filt_t[j] * sum;
}
let row_sum: f64 = smoothed_probs.row(t).sum();
if row_sum > 1e-30 {
for j in 0..k {
smoothed_probs[[t, j]] /= row_sum;
}
}
}
if (log_likelihood - prev_ll).abs() < tol {
break;
}
prev_ll = log_likelihood;
for j in 0..k {
let weights: Vec<f64> = (0..effective_n)
.map(|t| smoothed_probs[[t, j]].max(1e-15))
.collect();
let w_sum: f64 = weights.iter().sum();
if w_sum < 1e-10 {
continue;
}
let n_coefs = 1 + p;
let mut xtwx = Array2::<f64>::zeros((n_coefs, n_coefs));
let mut xtwy = Array1::<f64>::zeros(n_coefs);
for (t, &w) in weights.iter().enumerate().take(effective_n) {
let t_idx = t + p;
let mut x_row = vec![1.0];
for lag in 0..p {
x_row.push(y[t_idx - 1 - lag]);
}
for a in 0..n_coefs {
for b in 0..n_coefs {
xtwx[[a, b]] += w * x_row[a] * x_row[b];
}
xtwy[a] += w * x_row[a] * y[t_idx];
}
}
if let Ok(xtwx_inv) = xtwx.inv() {
let beta = xtwx_inv.dot(&xtwy);
regime_means[j] = beta[0];
for lag in 0..p {
ar_params[[j, lag]] = beta[1 + lag];
}
}
let mut wss = 0.0;
for (t, &w) in weights.iter().enumerate().take(effective_n) {
let t_idx = t + p;
let mut y_hat = regime_means[j];
for lag in 0..p {
y_hat += ar_params[[j, lag]] * y[t_idx - 1 - lag];
}
let resid = y[t_idx] - y_hat;
wss += w * resid * resid;
}
regime_sigmas[j] = (wss / w_sum).max(1e-10).sqrt();
}
for i in 0..k {
let mut row_sum = 0.0;
for j in 0..k {
let mut num = 0.0;
for t in 0..effective_n - 1 {
let xi_filt_t_i = filtered_probs[[t, i]].max(1e-30);
let xi_pred = trans.t().dot(&filtered_probs.row(t).to_owned());
let xi_pred_next_j = xi_pred[j].max(1e-30);
num += trans[[i, j]] * xi_filt_t_i * smoothed_probs[[t + 1, j]]
/ xi_pred_next_j;
}
trans[[i, j]] = num.max(1e-10);
row_sum += trans[[i, j]];
}
if row_sum > 1e-30 {
for j in 0..k {
trans[[i, j]] /= row_sum;
}
}
}
}
let n_free_params = k + k * ar_params_cols + k + k * (k - 1);
let nf = effective_n as f64;
let aic = -2.0 * log_likelihood + 2.0 * n_free_params as f64;
let bic = -2.0 * log_likelihood + n_free_params as f64 * nf.ln();
Ok(MarkovAutoregResult {
regime_means,
ar_params: if p > 0 {
ar_params
} else {
Array2::zeros((k, 0))
},
regime_sigmas,
transition_matrix: trans,
smoothed_probs,
filtered_probs,
log_likelihood,
aic,
bic,
n_obs: effective_n,
k_regimes: k,
ar_order: p,
})
}
}