1use ndarray::{Array1, Array2};
11use solow_core::error::{Error, Result};
12use solow_distributions::norm_cdf;
13use solow_regression::LinearModel;
14
15pub fn variance_inflation_factor(exog: &Array2<f64>, exog_idx: usize) -> Result<f64> {
22 let (n, k) = exog.dim();
23 if exog_idx >= k {
24 return Err(Error::Value("exog_idx out of range".into()));
25 }
26 if k < 2 {
27 return Err(Error::Value("need at least two columns for a VIF".into()));
28 }
29 let mut x_noti = Array2::<f64>::zeros((n, k - 1));
31 let mut cc = 0usize;
32 for j in 0..k {
33 if j == exog_idx {
34 continue;
35 }
36 for i in 0..n {
37 x_noti[[i, cc]] = exog[[i, j]];
38 }
39 cc += 1;
40 }
41 let x_i = exog.column(exog_idx).to_owned();
42 let res = LinearModel::ols(x_i, x_noti)?.fit()?;
43 Ok(1.0 / (1.0 - res.rsquared))
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum LillieforsDist {
49 Norm,
51}
52
53fn ks_stat_normal(z: &mut [f64]) -> f64 {
60 z.sort_by(|a, b| a.total_cmp(b));
61 let n = z.len() as f64;
62 let mut d_plus = f64::NEG_INFINITY;
63 let mut d_min = f64::NEG_INFINITY;
64 for (idx, &zi) in z.iter().enumerate() {
65 let cdf = norm_cdf(zi);
66 let i = idx as f64; let dp = (i + 1.0) / n - cdf; let dm = cdf - i / n; if dp > d_plus {
70 d_plus = dp;
71 }
72 if dm > d_min {
73 d_min = dm;
74 }
75 }
76 d_plus.max(d_min)
77}
78
79fn pval_lf(d_max: f64, n: usize) -> f64 {
85 let mut d = d_max;
86 let mut nn = n as f64;
87 if n > 100 {
88 d *= (nn / 100.0).powf(0.49);
89 nn = 100.0;
90 }
91 (-7.01256 * d * d * (nn + 2.78019) + 2.99587 * d * (nn + 2.78019).sqrt() - 0.122119
92 + 0.974598 / nn.sqrt()
93 + 1.67997 / nn)
94 .exp()
95}
96
97pub fn lilliefors(x: &Array1<f64>, dist: LillieforsDist) -> Result<(f64, f64)> {
107 let LillieforsDist::Norm = dist;
108 let n = x.len();
109 if n < 4 {
110 return Err(Error::Value(
111 "Lilliefors test requires at least 4 observations".into(),
112 ));
113 }
114 let nf = n as f64;
115 let mean = x.sum() / nf;
116 let var = x.iter().map(|&v| (v - mean) * (v - mean)).sum::<f64>() / (nf - 1.0);
118 let sd = var.sqrt();
119 let mut z: Vec<f64> = x.iter().map(|&v| (v - mean) / sd).collect();
120 let d_ks = ks_stat_normal(&mut z);
121 let pval = pval_lf(d_ks, n);
122 Ok((d_ks, pval))
123}
124
125pub fn kstest_normal(x: &Array1<f64>) -> Result<(f64, f64)> {
128 lilliefors(x, LillieforsDist::Norm)
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use ndarray::array;
135
136 #[test]
137 fn vif_orthogonal_columns_is_one() {
138 let x = array![
140 [1.0, 1.0, 1.0],
141 [1.0, 1.0, -1.0],
142 [1.0, -1.0, 1.0],
143 [1.0, -1.0, -1.0],
144 ];
145 let v = variance_inflation_factor(&x, 1).unwrap();
146 assert!((v - 1.0).abs() < 1e-9);
147 }
148
149 #[test]
150 fn lilliefors_statistic_in_unit_range() {
151 let x = array![0.1, -0.5, 0.3, 1.2, -0.7, 0.4, -0.2, 0.9, -1.1, 0.05];
152 let (stat, p) = lilliefors(&x, LillieforsDist::Norm).unwrap();
153 assert!((0.0..=1.0).contains(&stat));
154 assert!(p > 0.0);
155 let (stat2, _) = kstest_normal(&x).unwrap();
156 assert!((stat - stat2).abs() < 1e-15);
157 }
158}