use crate::error::GreenersError;
use crate::linalg::LinalgInverse as _;
use ndarray::{Array1, Array2};
use statrs::distribution::{ContinuousCDF, Normal};
use std::fmt;
#[derive(Debug)]
pub struct SetarResult {
pub beta_low: Array1<f64>,
pub beta_high: Array1<f64>,
pub se_low: Array1<f64>,
pub se_high: Array1<f64>,
pub t_low: Array1<f64>,
pub t_high: Array1<f64>,
pub p_low: Array1<f64>,
pub p_high: Array1<f64>,
pub threshold: f64,
pub delay: usize,
pub ar_order: usize,
pub n_low: usize,
pub n_high: usize,
pub r_squared: f64,
pub log_likelihood: f64,
pub sigma: f64,
pub n_obs: usize,
}
impl fmt::Display for SetarResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n{:=^78}", " SETAR (Threshold AR) ")?;
writeln!(f, "Model: y_t = a_i + sum phi_ij y_{{t-j}} + eps_t")?;
writeln!(
f,
" Regime 1 (low): y_{{t-{d}}} <= {thr:.6}",
d = self.delay,
thr = self.threshold
)?;
writeln!(
f,
" Regime 2 (high): y_{{t-{d}}} > {thr:.6}",
d = self.delay,
thr = self.threshold
)?;
writeln!(f, "{:<20} {:>12}", "AR order:", self.ar_order)?;
writeln!(f, "{:<20} {:>12}", "Delay:", self.delay)?;
writeln!(
f,
"{:<20} {:>12}",
"Obs (low/high):",
format!("{}/{}", self.n_low, self.n_high)
)?;
writeln!(f, "{:<20} {:>12.6}", "Threshold:", self.threshold)?;
writeln!(f, "{:<20} {:>12.6}", "R-squared:", self.r_squared)?;
writeln!(f, "{:<20} {:>12.6}", "Sigma:", self.sigma)?;
writeln!(f, "{:<20} {:>12.6}", "Log-likelihood:", self.log_likelihood)?;
writeln!(f, "\n{:-^78}", "")?;
writeln!(f, " Regime 1 (low): {n} observations", n = self.n_low)?;
writeln!(f, "{:-^78}", "")?;
writeln!(
f,
"{:<12} {:>12} {:>12} {:>10} {:>10}",
"Variable", "Coef.", "Std.Err.", "t", "P>|t|"
)?;
writeln!(f, "{:-^78}", "")?;
for i in 0..self.beta_low.len() {
let name = if i == 0 {
"const".to_string()
} else {
format!("y_{{t-{i}}}")
};
writeln!(
f,
"{:<12} {:>12.6} {:>12.6} {:>10.3} {:>10.4}",
name, self.beta_low[i], self.se_low[i], self.t_low[i], self.p_low[i]
)?;
}
writeln!(f, "\n{:-^78}", "")?;
writeln!(f, " Regime 2 (high): {n} observations", n = self.n_high)?;
writeln!(f, "{:-^78}", "")?;
writeln!(
f,
"{:<12} {:>12} {:>12} {:>10} {:>10}",
"Variable", "Coef.", "Std.Err.", "t", "P>|t|"
)?;
writeln!(f, "{:-^78}", "")?;
for i in 0..self.beta_high.len() {
let name = if i == 0 {
"const".to_string()
} else {
format!("y_{{t-{i}}}")
};
writeln!(
f,
"{:<12} {:>12.6} {:>12.6} {:>10.3} {:>10.4}",
name, self.beta_high[i], self.se_high[i], self.t_high[i], self.p_high[i]
)?;
}
write!(f, "{:=^78}", "")
}
}
pub struct SETAR;
impl SETAR {
pub fn fit(
y: &Array1<f64>,
ar_order: usize,
delay: usize,
) -> Result<SetarResult, GreenersError> {
let t = y.len();
if t < (ar_order + delay) * 3 {
return Err(GreenersError::InvalidOperation(
"SETAR: too few observations for given AR order and delay".into(),
));
}
let start = ar_order.max(delay);
let n_eff = t - start;
let mut x = Array2::zeros((n_eff, ar_order + 1));
let mut y_dep = Array1::zeros(n_eff);
let mut thresh_var = Array1::zeros(n_eff);
for i in 0..n_eff {
let t_i = start + i;
y_dep[i] = y[t_i];
thresh_var[i] = y[t_i - delay];
x[(i, 0)] = 1.0; for j in 1..=ar_order {
x[(i, j)] = y[t_i - j];
}
}
let mut sorted_thresh: Vec<f64> = thresh_var.iter().copied().collect();
sorted_thresh.sort_by(|a, b| a.total_cmp(b));
let trim = (n_eff * 15 / 100).max(ar_order + 2);
let n_candidates = n_eff - 2 * trim;
let mut best_sse = f64::INFINITY;
let mut best_threshold = 0.0_f64;
let mut best_beta_low = Array1::zeros(ar_order + 1);
let mut best_beta_high = Array1::zeros(ar_order + 1);
let mut best_n_low = 0;
let mut best_n_high = 0;
let mut best_se_low = Array1::zeros(ar_order + 1);
let mut best_se_high = Array1::zeros(ar_order + 1);
for i in 0..n_candidates {
let idx = trim + i;
let thr = sorted_thresh[idx];
let low_mask: Vec<bool> = thresh_var.iter().map(|&v| v <= thr).collect();
let high_mask: Vec<bool> = thresh_var.iter().map(|&v| v > thr).collect();
let n_low = low_mask.iter().filter(|&&b| b).count();
let n_high = high_mask.iter().filter(|&&b| b).count();
if n_low < ar_order + 2 || n_high < ar_order + 2 {
continue;
}
let (beta_low, sse_low, se_low) =
Self::ols_subset(&y_dep, &x, &low_mask, ar_order + 1)?;
let (beta_high, sse_high, se_high) =
Self::ols_subset(&y_dep, &x, &high_mask, ar_order + 1)?;
let sse = sse_low + sse_high;
if sse < best_sse {
best_sse = sse;
best_threshold = thr;
best_beta_low = beta_low;
best_beta_high = beta_high;
best_n_low = n_low;
best_n_high = n_high;
best_se_low = se_low;
best_se_high = se_high;
}
}
if best_sse == f64::INFINITY {
return Err(GreenersError::InvalidOperation(
"SETAR: no valid threshold found (insufficient obs in regimes)".into(),
));
}
let normal =
Normal::new(0.0, 1.0).map_err(|e| GreenersError::InvalidOperation(e.to_string()))?;
let t_low = &best_beta_low / &best_se_low;
let t_high = &best_beta_high / &best_se_high;
let p_low = t_low.mapv(|t| 2.0 * (1.0 - normal.cdf(t.abs())));
let p_high = t_high.mapv(|t| 2.0 * (1.0 - normal.cdf(t.abs())));
let y_mean = y_dep.mean().unwrap_or(0.0);
let tss = y_dep.mapv(|v| (v - y_mean).powi(2)).sum();
let r_squared = if tss > 1e-15 {
1.0 - best_sse / tss
} else {
0.0
};
let sigma2 = best_sse / (n_eff - 2 * (ar_order + 1)) as f64;
let sigma = sigma2.sqrt();
let log_likelihood = -(n_eff as f64) / 2.0 * (2.0 * std::f64::consts::PI * sigma2).ln()
- best_sse / (2.0 * sigma2);
Ok(SetarResult {
beta_low: best_beta_low,
beta_high: best_beta_high,
se_low: best_se_low,
se_high: best_se_high,
t_low,
t_high,
p_low,
p_high,
threshold: best_threshold,
delay,
ar_order,
n_low: best_n_low,
n_high: best_n_high,
r_squared,
log_likelihood,
sigma,
n_obs: n_eff,
})
}
fn ols_subset(
y: &Array1<f64>,
x: &Array2<f64>,
mask: &[bool],
k: usize,
) -> Result<(Array1<f64>, f64, Array1<f64>), GreenersError> {
let n_sub = mask.iter().filter(|&&b| b).count();
let mut x_sub = Array2::zeros((n_sub, k));
let mut y_sub = Array1::zeros(n_sub);
let mut idx = 0;
for i in 0..y.len() {
if mask[i] {
for j in 0..k {
x_sub[(idx, j)] = x[(i, j)];
}
y_sub[idx] = y[i];
idx += 1;
}
}
let xt = x_sub.t();
let xtx = xt.dot(&x_sub);
let xtx_reg = &xtx + Array2::eye(k) * 1e-10;
let xtx_inv = xtx_reg.inv()?;
let xty = xt.dot(&y_sub);
let beta: Array1<f64> = xtx_inv.dot(&xty);
let residuals = &y_sub - x_sub.dot(&beta);
let sse = residuals.dot(&residuals);
let sigma2 = sse / (n_sub - k) as f64;
let se = (xtx_inv * sigma2).diag().mapv(|v| v.sqrt());
Ok((beta, sse, se))
}
}