use crate::linalg::LinalgInverse as _;
use crate::GreenersError;
use ndarray::{Array1, Array2};
use statrs::distribution::{ContinuousCDF, Normal};
use std::fmt;
#[derive(Debug)]
pub struct FmolsResult {
pub alpha: f64,
pub beta: Array1<f64>,
pub alpha_se: f64,
pub beta_se: Array1<f64>,
pub beta_t: Array1<f64>,
pub beta_p: Array1<f64>,
pub omega: Array2<f64>,
pub r_squared: f64,
pub n_obs: usize,
pub n_regressors: usize,
pub bandwidth: usize,
pub variable_names: Vec<String>,
}
impl fmt::Display for FmolsResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n{:=^78}", " Fully Modified OLS (FMOLS) ")?;
writeln!(f, "Phillips & Hansen (1990) nonparametric correction")?;
writeln!(f, "{:<20} {:>12}", "Observations:", self.n_obs)?;
writeln!(f, "{:<20} {:>12}", "Regressors:", self.n_regressors)?;
writeln!(f, "{:<20} {:>12}", "Bandwidth:", self.bandwidth)?;
writeln!(f, "{:<20} {:>12.6}", "R-squared:", self.r_squared)?;
writeln!(f, "\n{:-^78}", "")?;
writeln!(
f,
"{:<12} {:>12} {:>12} {:>10} {:>10}",
"Variable", "Coef.", "Std.Err.", "t", "P>|t|"
)?;
writeln!(f, "{:-^78}", "")?;
writeln!(
f,
"{:<12} {:>12.6} {:>12.6} {:>10.3} {:>10.4}",
"alpha",
self.alpha,
self.alpha_se,
self.alpha / self.alpha_se.max(1e-10),
0.0
)?;
for i in 0..self.beta.len() {
let name = self
.variable_names
.get(i)
.cloned()
.unwrap_or_else(|| format!("x{}", i));
writeln!(
f,
"{:<12} {:>12.6} {:>12.6} {:>10.3} {:>10.4}",
name, self.beta[i], self.beta_se[i], self.beta_t[i], self.beta_p[i]
)?;
}
write!(f, "{:=^78}", "")
}
}
pub struct FMOLS;
impl FMOLS {
pub fn fit(
y: &Array1<f64>,
x: &Array2<f64>,
variable_names: Option<Vec<String>>,
) -> Result<FmolsResult, GreenersError> {
let t = y.len();
let k = x.ncols();
if x.nrows() != t {
return Err(GreenersError::ShapeMismatch(
"FMOLS: y and x must have same length".into(),
));
}
if t < k + 5 {
return Err(GreenersError::InvalidOperation(
"FMOLS: too few observations".into(),
));
}
let names = variable_names.unwrap_or_else(|| (0..k).map(|i| format!("x{}", i)).collect());
let mut z = Array2::zeros((t, k + 1));
for i in 0..t {
z[(i, 0)] = 1.0;
for j in 0..k {
z[(i, j + 1)] = x[(i, j)];
}
}
let zt = z.t();
let ztz = zt.dot(&z);
let ztz_reg = &ztz + Array2::<f64>::eye(k + 1) * 1e-8;
let ztz_inv = ztz_reg.inv()?;
let zty = zt.dot(y);
let ols_beta: Array1<f64> = ztz_inv.dot(&zty);
let residuals = y - z.dot(&ols_beta);
let bw = (4.0 * (t as f64 / 100.0).powf(2.0 / 9.0)) as usize;
let bandwidth = bw.max(1);
let mut combined = Array2::zeros((t - 1, k + 1));
for i in 0..t - 1 {
combined[(i, 0)] = residuals[i + 1]; for j in 0..k {
combined[(i, j + 1)] = x[(i + 1, j)] - x[(i, j)]; }
}
let omega = Self::long_run_covariance(&combined, bandwidth);
let omega_uu = omega[(0, 0)];
let omega_ux: Array1<f64> = (0..k).map(|j| omega[(0, j + 1)]).collect();
let _omega_xu: Array1<f64> = (0..k).map(|j| omega[(j + 1, 0)]).collect();
let omega_xx: Array2<f64> = omega.slice(ndarray::s![1..k + 1, 1..k + 1]).to_owned();
let omega_xx_inv = (&omega_xx + Array2::<f64>::eye(k) * 1e-10).inv()?;
let correction_coef = omega_ux.insert_axis(ndarray::Axis(0)).dot(&omega_xx_inv);
let mut y_plus = y.clone();
for i in 1..t {
let delta_x = x.row(i).to_owned() - x.row(i - 1);
let corr = correction_coef.dot(&delta_x);
y_plus[i] -= corr[0];
}
let zty_plus = zt.dot(&y_plus);
let fmols_beta: Array1<f64> = ztz_inv.dot(&zty_plus);
let alpha = fmols_beta[0];
let beta = fmols_beta.slice(ndarray::s![1..k + 1]).to_owned();
let sigma_uu = omega_uu;
let cov = &ztz_inv * sigma_uu;
let std_errors = cov.diag().mapv(|v| v.sqrt());
let alpha_se = std_errors[0];
let beta_se = std_errors.slice(ndarray::s![1..k + 1]).to_owned();
let t_values = &beta / &beta_se;
let normal =
Normal::new(0.0, 1.0).map_err(|e| GreenersError::InvalidOperation(e.to_string()))?;
let p_values = t_values.mapv(|t| 2.0 * (1.0 - normal.cdf(t.abs())));
let y_mean = y.mean().unwrap_or(0.0);
let tss = y.mapv(|v| (v - y_mean).powi(2)).sum();
let rss = residuals.dot(&residuals);
let r_squared = if tss > 1e-15 { 1.0 - rss / tss } else { 0.0 };
Ok(FmolsResult {
alpha,
beta,
alpha_se,
beta_se,
beta_t: t_values,
beta_p: p_values,
omega,
r_squared,
n_obs: t,
n_regressors: k,
bandwidth,
variable_names: names,
})
}
fn long_run_covariance(data: &Array2<f64>, bandwidth: usize) -> Array2<f64> {
let n = data.nrows();
let k = data.ncols();
let mut omega = Array2::zeros((k, k));
for i in 0..n {
let row = data.row(i);
for a in 0..k {
for b in 0..k {
omega[(a, b)] += row[a] * row[b];
}
}
}
omega /= n as f64;
for lag in 1..=bandwidth {
if lag >= n {
break;
}
let weight = 1.0 - lag as f64 / (bandwidth + 1) as f64;
let mut gamma = Array2::zeros((k, k));
let n_lag = n - lag;
for i in 0..n_lag {
let row1 = data.row(i);
let row2 = data.row(i + lag);
for a in 0..k {
for b in 0..k {
gamma[(a, b)] += row1[a] * row2[b];
}
}
}
gamma /= n as f64;
omega = &omega + &gamma * weight;
let gamma_t = gamma.t();
omega = &omega + &gamma_t * weight;
}
omega
}
}