use ndarray::{Array1, Array2};
use solow_core::error::{Error, Result};
use solow_distributions::norm_cdf;
use solow_regression::LinearModel;
pub fn variance_inflation_factor(exog: &Array2<f64>, exog_idx: usize) -> Result<f64> {
let (n, k) = exog.dim();
if exog_idx >= k {
return Err(Error::Value("exog_idx out of range".into()));
}
if k < 2 {
return Err(Error::Value("need at least two columns for a VIF".into()));
}
let mut x_noti = Array2::<f64>::zeros((n, k - 1));
let mut cc = 0usize;
for j in 0..k {
if j == exog_idx {
continue;
}
for i in 0..n {
x_noti[[i, cc]] = exog[[i, j]];
}
cc += 1;
}
let x_i = exog.column(exog_idx).to_owned();
let res = LinearModel::ols(x_i, x_noti)?.fit()?;
Ok(1.0 / (1.0 - res.rsquared))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LillieforsDist {
Norm,
}
fn ks_stat_normal(z: &mut [f64]) -> f64 {
z.sort_by(|a, b| a.total_cmp(b));
let n = z.len() as f64;
let mut d_plus = f64::NEG_INFINITY;
let mut d_min = f64::NEG_INFINITY;
for (idx, &zi) in z.iter().enumerate() {
let cdf = norm_cdf(zi);
let i = idx as f64; let dp = (i + 1.0) / n - cdf; let dm = cdf - i / n; if dp > d_plus {
d_plus = dp;
}
if dm > d_min {
d_min = dm;
}
}
d_plus.max(d_min)
}
fn pval_lf(d_max: f64, n: usize) -> f64 {
let mut d = d_max;
let mut nn = n as f64;
if n > 100 {
d *= (nn / 100.0).powf(0.49);
nn = 100.0;
}
(-7.01256 * d * d * (nn + 2.78019) + 2.99587 * d * (nn + 2.78019).sqrt() - 0.122119
+ 0.974598 / nn.sqrt()
+ 1.67997 / nn)
.exp()
}
pub fn lilliefors(x: &Array1<f64>, dist: LillieforsDist) -> Result<(f64, f64)> {
let LillieforsDist::Norm = dist;
let n = x.len();
if n < 4 {
return Err(Error::Value(
"Lilliefors test requires at least 4 observations".into(),
));
}
let nf = n as f64;
let mean = x.sum() / nf;
let var = x.iter().map(|&v| (v - mean) * (v - mean)).sum::<f64>() / (nf - 1.0);
let sd = var.sqrt();
let mut z: Vec<f64> = x.iter().map(|&v| (v - mean) / sd).collect();
let d_ks = ks_stat_normal(&mut z);
let pval = pval_lf(d_ks, n);
Ok((d_ks, pval))
}
pub fn kstest_normal(x: &Array1<f64>) -> Result<(f64, f64)> {
lilliefors(x, LillieforsDist::Norm)
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
#[test]
fn vif_orthogonal_columns_is_one() {
let x = array![
[1.0, 1.0, 1.0],
[1.0, 1.0, -1.0],
[1.0, -1.0, 1.0],
[1.0, -1.0, -1.0],
];
let v = variance_inflation_factor(&x, 1).unwrap();
assert!((v - 1.0).abs() < 1e-9);
}
#[test]
fn lilliefors_statistic_in_unit_range() {
let x = array![0.1, -0.5, 0.3, 1.2, -0.7, 0.4, -0.2, 0.9, -1.1, 0.05];
let (stat, p) = lilliefors(&x, LillieforsDist::Norm).unwrap();
assert!((0.0..=1.0).contains(&stat));
assert!(p > 0.0);
let (stat2, _) = kstest_normal(&x).unwrap();
assert!((stat - stat2).abs() < 1e-15);
}
}