1use ndarray::{Array1, Array2};
14use solow_core::error::{Error, Result};
15use solow_distributions::{chi2_sf, f_sf};
16use solow_linalg::inv;
17use solow_regression::{LinearModel, LinearResults};
18
19fn lagmat_both(x: &[f64], k: usize) -> Array2<f64> {
25 let n = x.len();
26 let rows = n - k;
27 let mut out = Array2::<f64>::zeros((rows, k));
28 for t in 0..rows {
29 let base = k + t;
31 for j in 0..k {
32 out[[t, j]] = x[base - 1 - j];
33 }
34 }
35 out
36}
37
38fn with_const(m: &Array2<f64>) -> Array2<f64> {
40 let (r, c) = m.dim();
41 let mut out = Array2::<f64>::zeros((r, c + 1));
42 for i in 0..r {
43 out[[i, 0]] = 1.0;
44 for j in 0..c {
45 out[[i, j + 1]] = m[[i, j]];
46 }
47 }
48 out
49}
50
51fn default_nlags(nobs: usize) -> usize {
53 (nobs / 5).min(10)
54}
55
56fn wald_test(res: &LinearResults, r_mat: &Array2<f64>, use_f: bool) -> Result<(f64, f64)> {
64 let j = r_mat.nrows();
65 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)?;
69 let mid = rvr_inv.dot(&rb); let quad = rb.dot(&mid);
71 if use_f {
72 let stat = quad / j as f64;
73 Ok((stat, f_sf(stat, j as f64, res.df_resid)))
74 } else {
75 Ok((quad, chi2_sf(quad, j as f64)))
76 }
77}
78
79pub fn acorr_lm(
88 resid: &Array1<f64>,
89 nlags: Option<usize>,
90 ddof: usize,
91) -> Result<(f64, f64, f64, f64)> {
92 let n = resid.len();
93 let k = nlags.unwrap_or_else(|| default_nlags(n));
94 if k == 0 {
95 return Err(Error::Value("nlags must be >= 1".into()));
96 }
97 if k >= n {
98 return Err(Error::Value("nlags too large for series length".into()));
99 }
100 let r: Vec<f64> = resid.to_vec();
101 let lags = lagmat_both(&r, k);
102 let nobs = lags.nrows();
103 let design = with_const(&lags);
104 let yshort = Array1::from_iter(r[n - nobs..].iter().copied());
105 let res = LinearModel::ols(yshort, design)?.fit()?;
106 let lm = (nobs as f64 - ddof as f64) * res.rsquared;
107 let lm_pvalue = chi2_sf(lm, k as f64);
108 Ok((lm, lm_pvalue, res.fvalue, res.f_pvalue))
109}
110
111pub fn het_arch(
117 resid: &Array1<f64>,
118 nlags: Option<usize>,
119 ddof: usize,
120) -> Result<(f64, f64, f64, f64)> {
121 let sq = resid.mapv(|v| v * v);
122 acorr_lm(&sq, nlags, ddof)
123}
124
125pub fn acorr_breusch_godfrey(
137 resid: &Array1<f64>,
138 exog: &Array2<f64>,
139 nlags: Option<usize>,
140) -> Result<(f64, f64, f64, f64)> {
141 let n = resid.len();
142 if exog.nrows() != n {
143 return Err(Error::Shape("exog rows must equal residual length".into()));
144 }
145 let k = nlags.unwrap_or_else(|| default_nlags(n));
146 if k == 0 {
147 return Err(Error::Value("nlags must be >= 1".into()));
148 }
149 let mut padded = vec![0.0; k];
152 padded.extend(resid.iter().copied());
153 let lags = lagmat_both(&padded, k); let nobs = lags.nrows();
155 debug_assert_eq!(nobs, n);
156 let lag_const = with_const(&lags); let k_old = exog.ncols();
160 let k_vars = k_old + lag_const.ncols();
161 let mut design = Array2::<f64>::zeros((nobs, k_vars));
162 for i in 0..nobs {
163 for j in 0..k_old {
164 design[[i, j]] = exog[[i, j]];
165 }
166 for j in 0..lag_const.ncols() {
167 design[[i, k_old + j]] = lag_const[[i, j]];
168 }
169 }
170 let yshort = resid.clone();
172 let res = LinearModel::ols(yshort, design)?.fit()?;
173 let lm = nobs as f64 * res.rsquared;
174 let lm_pvalue = chi2_sf(lm, k as f64);
175
176 let mut r_mat = Array2::<f64>::zeros((k, k_vars));
178 for i in 0..k {
179 r_mat[[i, k_vars - k + i]] = 1.0;
180 }
181 let (fvalue, f_pvalue) = wald_test(&res, &r_mat, true)?;
182 Ok((lm, lm_pvalue, fvalue, f_pvalue))
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum ResetAug {
188 Fitted,
190 Exog,
192}
193
194pub fn linear_reset(
204 endog: &Array1<f64>,
205 exog: &Array2<f64>,
206 power: usize,
207 test_type: ResetAug,
208 use_f: bool,
209) -> Result<(f64, f64)> {
210 if power < 2 {
211 return Err(Error::Value("power must be >= 2".into()));
212 }
213 let (n, k) = exog.dim();
214 if endog.len() != n {
215 return Err(Error::Shape("endog length must equal exog rows".into()));
216 }
217
218 let aug_base: Array2<f64> = match test_type {
220 ResetAug::Fitted => {
221 let res = LinearModel::ols(endog.clone(), exog.clone())?.fit()?;
222 let mut a = Array2::<f64>::zeros((n, 1));
224 for i in 0..n {
225 a[[i, 0]] = res.fittedvalues[i];
226 }
227 a
228 }
229 ResetAug::Exog => {
230 let mut keep: Vec<usize> = Vec::new();
233 for j in 0..k {
234 let col = exog.column(j);
235 let mut mx = f64::NEG_INFINITY;
236 let mut mn = f64::INFINITY;
237 for &v in col.iter() {
238 mx = mx.max(v);
239 mn = mn.min(v);
240 }
241 let binary = col.iter().all(|&v| v == mx || v == mn);
242 if !binary {
243 keep.push(j);
244 }
245 }
246 if keep.is_empty() {
247 return Err(Error::Value(
248 "model contains only constant or binary data".into(),
249 ));
250 }
251 let mut a = Array2::<f64>::zeros((n, keep.len()));
252 for (cc, &j) in keep.iter().enumerate() {
253 for i in 0..n {
254 a[[i, cc]] = exog[[i, j]];
255 }
256 }
257 a
258 }
259 };
260 let base_cols = aug_base.ncols();
261 let powers: Vec<usize> = (2..=power).collect();
262 let nrestr = base_cols * powers.len();
263
264 let k_full = k + nrestr;
266 let mut design = Array2::<f64>::zeros((n, k_full));
267 for i in 0..n {
268 for j in 0..k {
269 design[[i, j]] = exog[[i, j]];
270 }
271 }
272 let mut col = k;
273 for &p in &powers {
274 for bc in 0..base_cols {
275 for i in 0..n {
276 design[[i, col]] = aug_base[[i, bc]].powi(p as i32);
277 }
278 col += 1;
279 }
280 }
281
282 let res = LinearModel::ols(endog.clone(), design)?.fit()?;
283 let mut r_mat = Array2::<f64>::zeros((nrestr, k_full));
285 for i in 0..nrestr {
286 r_mat[[i, k_full - nrestr + i]] = 1.0;
287 }
288 wald_test(&res, &r_mat, use_f)
289}
290
291pub fn compare_lr_test(full: &LinearResults, restricted: &LinearResults) -> (f64, f64, f64) {
300 let lrdf = restricted.df_resid - full.df_resid;
301 let lrstat = -2.0 * (restricted.llf - full.llf);
302 let lr_pvalue = chi2_sf(lrstat, lrdf);
303 (lrstat, lr_pvalue, lrdf)
304}
305
306pub fn compare_f_test(full: &LinearResults, restricted: &LinearResults) -> (f64, f64, f64) {
314 let df_full = full.df_resid;
315 let df_diff = restricted.df_resid - df_full;
316 let f_value = (restricted.ssr - full.ssr) / df_diff / full.ssr * df_full;
317 let p_value = f_sf(f_value, df_diff, df_full);
318 (f_value, p_value, df_diff)
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use ndarray::array;
325
326 fn small_ols() -> (Array1<f64>, Array2<f64>) {
327 let x = array![
328 [1.0, 0.2, -0.5],
329 [1.0, -0.1, 0.3],
330 [1.0, 0.4, 0.1],
331 [1.0, -0.3, -0.2],
332 [1.0, 0.5, 0.6],
333 [1.0, -0.2, -0.4],
334 [1.0, 0.1, 0.2],
335 [1.0, 0.3, -0.1],
336 [1.0, -0.4, 0.5],
337 [1.0, 0.0, -0.3],
338 [1.0, 0.25, 0.15],
339 [1.0, -0.35, 0.05],
340 ];
341 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];
342 (y, x)
343 }
344
345 #[test]
346 fn lagmat_both_shape_and_values() {
347 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
348 let m = lagmat_both(&x, 2);
349 assert_eq!(m.dim(), (3, 2));
350 assert_eq!(m[[0, 0]], 2.0);
352 assert_eq!(m[[0, 1]], 1.0);
353 assert_eq!(m[[2, 0]], 4.0);
354 assert_eq!(m[[2, 1]], 3.0);
355 }
356
357 #[test]
358 fn acorr_lm_runs_and_bounds() {
359 let (y, x) = small_ols();
360 let res = LinearModel::ols(y, x).unwrap().fit().unwrap();
361 let (lm, p, f, fp) = acorr_lm(&res.resid, Some(2), 0).unwrap();
362 assert!(lm >= 0.0);
363 assert!((0.0..=1.0).contains(&p));
364 assert!(f >= 0.0);
365 assert!((0.0..=1.0).contains(&fp));
366 }
367
368 #[test]
369 fn het_arch_equals_acorr_lm_of_squares() {
370 let (y, x) = small_ols();
371 let res = LinearModel::ols(y, x).unwrap().fit().unwrap();
372 let a = het_arch(&res.resid, Some(2), 0).unwrap();
373 let b = acorr_lm(&res.resid.mapv(|v| v * v), Some(2), 0).unwrap();
374 assert!((a.0 - b.0).abs() < 1e-12);
375 assert!((a.2 - b.2).abs() < 1e-12);
376 }
377
378 #[test]
379 fn bg_and_reset_run() {
380 let (y, x) = small_ols();
381 let res = LinearModel::ols(y.clone(), x.clone())
382 .unwrap()
383 .fit()
384 .unwrap();
385 let (lm, p, _f, fp) = acorr_breusch_godfrey(&res.resid, &x, Some(2)).unwrap();
386 assert!(lm >= 0.0 && (0.0..=1.0).contains(&p) && (0.0..=1.0).contains(&fp));
387 let (stat, pv) = linear_reset(&y, &x, 3, ResetAug::Fitted, false).unwrap();
388 assert!(stat >= 0.0 && (0.0..=1.0).contains(&pv));
389 }
390
391 #[test]
392 fn compare_tests_nested() {
393 let (y, x) = small_ols();
394 let full = LinearModel::ols(y.clone(), x.clone())
395 .unwrap()
396 .fit()
397 .unwrap();
398 let xr = x.slice(ndarray::s![.., 0..2]).to_owned();
400 let restr = LinearModel::ols(y, xr).unwrap().fit().unwrap();
401 let (lr, lp, ldf) = compare_lr_test(&full, &restr);
402 let (f, fp, fdf) = compare_f_test(&full, &restr);
403 assert_eq!(ldf, 1.0);
404 assert_eq!(fdf, 1.0);
405 assert!(lr >= 0.0 && (0.0..=1.0).contains(&lp));
406 assert!(f >= 0.0 && (0.0..=1.0).contains(&fp));
407 }
408}