use ndarray::{Array1, Array2};
use solow_core::error::{Error, Result};
use solow_distributions::{chi2_sf, f_sf};
use solow_linalg::inv;
use solow_regression::{LinearModel, LinearResults};
fn lagmat_both(x: &[f64], k: usize) -> Array2<f64> {
let n = x.len();
let rows = n - k;
let mut out = Array2::<f64>::zeros((rows, k));
for t in 0..rows {
let base = k + t;
for j in 0..k {
out[[t, j]] = x[base - 1 - j];
}
}
out
}
fn with_const(m: &Array2<f64>) -> Array2<f64> {
let (r, c) = m.dim();
let mut out = Array2::<f64>::zeros((r, c + 1));
for i in 0..r {
out[[i, 0]] = 1.0;
for j in 0..c {
out[[i, j + 1]] = m[[i, j]];
}
}
out
}
fn default_nlags(nobs: usize) -> usize {
(nobs / 5).min(10)
}
fn wald_test(res: &LinearResults, r_mat: &Array2<f64>, use_f: bool) -> Result<(f64, f64)> {
let j = r_mat.nrows();
let rb = r_mat.dot(&res.params); let rv = r_mat.dot(&res.cov_params); let rvr = rv.dot(&r_mat.t()); let rvr_inv = inv(&rvr)?;
let mid = rvr_inv.dot(&rb); let quad = rb.dot(&mid);
if use_f {
let stat = quad / j as f64;
Ok((stat, f_sf(stat, j as f64, res.df_resid)))
} else {
Ok((quad, chi2_sf(quad, j as f64)))
}
}
pub fn acorr_lm(
resid: &Array1<f64>,
nlags: Option<usize>,
ddof: usize,
) -> Result<(f64, f64, f64, f64)> {
let n = resid.len();
let k = nlags.unwrap_or_else(|| default_nlags(n));
if k == 0 {
return Err(Error::Value("nlags must be >= 1".into()));
}
if k >= n {
return Err(Error::Value("nlags too large for series length".into()));
}
let r: Vec<f64> = resid.to_vec();
let lags = lagmat_both(&r, k);
let nobs = lags.nrows();
let design = with_const(&lags);
let yshort = Array1::from_iter(r[n - nobs..].iter().copied());
let res = LinearModel::ols(yshort, design)?.fit()?;
let lm = (nobs as f64 - ddof as f64) * res.rsquared;
let lm_pvalue = chi2_sf(lm, k as f64);
Ok((lm, lm_pvalue, res.fvalue, res.f_pvalue))
}
pub fn het_arch(
resid: &Array1<f64>,
nlags: Option<usize>,
ddof: usize,
) -> Result<(f64, f64, f64, f64)> {
let sq = resid.mapv(|v| v * v);
acorr_lm(&sq, nlags, ddof)
}
pub fn acorr_breusch_godfrey(
resid: &Array1<f64>,
exog: &Array2<f64>,
nlags: Option<usize>,
) -> Result<(f64, f64, f64, f64)> {
let n = resid.len();
if exog.nrows() != n {
return Err(Error::Shape("exog rows must equal residual length".into()));
}
let k = nlags.unwrap_or_else(|| default_nlags(n));
if k == 0 {
return Err(Error::Value("nlags must be >= 1".into()));
}
let mut padded = vec![0.0; k];
padded.extend(resid.iter().copied());
let lags = lagmat_both(&padded, k); let nobs = lags.nrows();
debug_assert_eq!(nobs, n);
let lag_const = with_const(&lags);
let k_old = exog.ncols();
let k_vars = k_old + lag_const.ncols();
let mut design = Array2::<f64>::zeros((nobs, k_vars));
for i in 0..nobs {
for j in 0..k_old {
design[[i, j]] = exog[[i, j]];
}
for j in 0..lag_const.ncols() {
design[[i, k_old + j]] = lag_const[[i, j]];
}
}
let yshort = resid.clone();
let res = LinearModel::ols(yshort, design)?.fit()?;
let lm = nobs as f64 * res.rsquared;
let lm_pvalue = chi2_sf(lm, k as f64);
let mut r_mat = Array2::<f64>::zeros((k, k_vars));
for i in 0..k {
r_mat[[i, k_vars - k + i]] = 1.0;
}
let (fvalue, f_pvalue) = wald_test(&res, &r_mat, true)?;
Ok((lm, lm_pvalue, fvalue, f_pvalue))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResetAug {
Fitted,
Exog,
}
pub fn linear_reset(
endog: &Array1<f64>,
exog: &Array2<f64>,
power: usize,
test_type: ResetAug,
use_f: bool,
) -> Result<(f64, f64)> {
if power < 2 {
return Err(Error::Value("power must be >= 2".into()));
}
let (n, k) = exog.dim();
if endog.len() != n {
return Err(Error::Shape("endog length must equal exog rows".into()));
}
let aug_base: Array2<f64> = match test_type {
ResetAug::Fitted => {
let res = LinearModel::ols(endog.clone(), exog.clone())?.fit()?;
let mut a = Array2::<f64>::zeros((n, 1));
for i in 0..n {
a[[i, 0]] = res.fittedvalues[i];
}
a
}
ResetAug::Exog => {
let mut keep: Vec<usize> = Vec::new();
for j in 0..k {
let col = exog.column(j);
let mut mx = f64::NEG_INFINITY;
let mut mn = f64::INFINITY;
for &v in col.iter() {
mx = mx.max(v);
mn = mn.min(v);
}
let binary = col.iter().all(|&v| v == mx || v == mn);
if !binary {
keep.push(j);
}
}
if keep.is_empty() {
return Err(Error::Value(
"model contains only constant or binary data".into(),
));
}
let mut a = Array2::<f64>::zeros((n, keep.len()));
for (cc, &j) in keep.iter().enumerate() {
for i in 0..n {
a[[i, cc]] = exog[[i, j]];
}
}
a
}
};
let base_cols = aug_base.ncols();
let powers: Vec<usize> = (2..=power).collect();
let nrestr = base_cols * powers.len();
let k_full = k + nrestr;
let mut design = Array2::<f64>::zeros((n, k_full));
for i in 0..n {
for j in 0..k {
design[[i, j]] = exog[[i, j]];
}
}
let mut col = k;
for &p in &powers {
for bc in 0..base_cols {
for i in 0..n {
design[[i, col]] = aug_base[[i, bc]].powi(p as i32);
}
col += 1;
}
}
let res = LinearModel::ols(endog.clone(), design)?.fit()?;
let mut r_mat = Array2::<f64>::zeros((nrestr, k_full));
for i in 0..nrestr {
r_mat[[i, k_full - nrestr + i]] = 1.0;
}
wald_test(&res, &r_mat, use_f)
}
pub fn compare_lr_test(full: &LinearResults, restricted: &LinearResults) -> (f64, f64, f64) {
let lrdf = restricted.df_resid - full.df_resid;
let lrstat = -2.0 * (restricted.llf - full.llf);
let lr_pvalue = chi2_sf(lrstat, lrdf);
(lrstat, lr_pvalue, lrdf)
}
pub fn compare_f_test(full: &LinearResults, restricted: &LinearResults) -> (f64, f64, f64) {
let df_full = full.df_resid;
let df_diff = restricted.df_resid - df_full;
let f_value = (restricted.ssr - full.ssr) / df_diff / full.ssr * df_full;
let p_value = f_sf(f_value, df_diff, df_full);
(f_value, p_value, df_diff)
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
fn small_ols() -> (Array1<f64>, Array2<f64>) {
let x = array![
[1.0, 0.2, -0.5],
[1.0, -0.1, 0.3],
[1.0, 0.4, 0.1],
[1.0, -0.3, -0.2],
[1.0, 0.5, 0.6],
[1.0, -0.2, -0.4],
[1.0, 0.1, 0.2],
[1.0, 0.3, -0.1],
[1.0, -0.4, 0.5],
[1.0, 0.0, -0.3],
[1.0, 0.25, 0.15],
[1.0, -0.35, 0.05],
];
let y = array![0.9, 1.1, 1.4, 0.7, 1.8, 0.6, 1.2, 1.0, 0.8, 1.05, 1.3, 0.95];
(y, x)
}
#[test]
fn lagmat_both_shape_and_values() {
let x = [1.0, 2.0, 3.0, 4.0, 5.0];
let m = lagmat_both(&x, 2);
assert_eq!(m.dim(), (3, 2));
assert_eq!(m[[0, 0]], 2.0);
assert_eq!(m[[0, 1]], 1.0);
assert_eq!(m[[2, 0]], 4.0);
assert_eq!(m[[2, 1]], 3.0);
}
#[test]
fn acorr_lm_runs_and_bounds() {
let (y, x) = small_ols();
let res = LinearModel::ols(y, x).unwrap().fit().unwrap();
let (lm, p, f, fp) = acorr_lm(&res.resid, Some(2), 0).unwrap();
assert!(lm >= 0.0);
assert!((0.0..=1.0).contains(&p));
assert!(f >= 0.0);
assert!((0.0..=1.0).contains(&fp));
}
#[test]
fn het_arch_equals_acorr_lm_of_squares() {
let (y, x) = small_ols();
let res = LinearModel::ols(y, x).unwrap().fit().unwrap();
let a = het_arch(&res.resid, Some(2), 0).unwrap();
let b = acorr_lm(&res.resid.mapv(|v| v * v), Some(2), 0).unwrap();
assert!((a.0 - b.0).abs() < 1e-12);
assert!((a.2 - b.2).abs() < 1e-12);
}
#[test]
fn bg_and_reset_run() {
let (y, x) = small_ols();
let res = LinearModel::ols(y.clone(), x.clone())
.unwrap()
.fit()
.unwrap();
let (lm, p, _f, fp) = acorr_breusch_godfrey(&res.resid, &x, Some(2)).unwrap();
assert!(lm >= 0.0 && (0.0..=1.0).contains(&p) && (0.0..=1.0).contains(&fp));
let (stat, pv) = linear_reset(&y, &x, 3, ResetAug::Fitted, false).unwrap();
assert!(stat >= 0.0 && (0.0..=1.0).contains(&pv));
}
#[test]
fn compare_tests_nested() {
let (y, x) = small_ols();
let full = LinearModel::ols(y.clone(), x.clone())
.unwrap()
.fit()
.unwrap();
let xr = x.slice(ndarray::s![.., 0..2]).to_owned();
let restr = LinearModel::ols(y, xr).unwrap().fit().unwrap();
let (lr, lp, ldf) = compare_lr_test(&full, &restr);
let (f, fp, fdf) = compare_f_test(&full, &restr);
assert_eq!(ldf, 1.0);
assert_eq!(fdf, 1.0);
assert!(lr >= 0.0 && (0.0..=1.0).contains(&lp));
assert!(f >= 0.0 && (0.0..=1.0).contains(&fp));
}
}