regression-diagnostics 0.2.0

Statistical diagnostics for OLS regression in Rust: VIF, condition number, adjusted R2, F/AIC/BIC, residual tests (Durbin-Watson, Breusch-Pagan, White, Jarque-Bera), influence measures (leverage, Cook's distance, DFFITS), QQ-plot data, and an R/statsmodels-style summary().
Documentation
//! Documented edge-case behavior: each degenerate input has an explicit,
//! asserted outcome rather than "doesn't panic".

use ndarray::{array, Array1, Array2};
use regression_diagnostics::multicollinearity::vif;
use regression_diagnostics::{OlsFit, RegressionError};

#[test]
fn perfectly_collinear_predictors_error() {
    // Column 2 is exactly 2 * column 1, so the design is rank-deficient. The
    // primary fit has no unique solution, so construction errors clearly rather
    // than returning silent NaNs. (VIF's "infinite" case is only reachable via a
    // valid fit; exact collinearity is caught here first.)
    let x = array![
        [1.0, 1.0, 2.0],
        [1.0, 2.0, 4.0],
        [1.0, 3.0, 6.0],
        [1.0, 4.0, 8.0],
    ];
    let y = array![1.0, 2.0, 3.0, 4.0];
    let err = OlsFit::new(x, y).unwrap_err();
    assert_eq!(err, RegressionError::RankDeficient);
}

#[test]
fn single_predictor_vif_is_one() {
    // With one predictor (plus intercept) there are no "other" predictors to
    // regress it on, so R²_aux = 0 and VIF = 1 — the documented trivial value.
    let x = array![[1.0, 1.0], [1.0, 2.0], [1.0, 3.0], [1.0, 4.0], [1.0, 5.0]];
    let y = array![1.1, 1.9, 3.2, 3.9, 5.1];
    let fit = OlsFit::new(x, y).unwrap();
    let v = vif(&fit);
    assert!(v[0].is_nan()); // intercept
    assert!((v[1] - 1.0).abs() < 1e-9, "single-predictor VIF = {}", v[1]);
}

#[test]
fn too_few_observations_error() {
    // n == p leaves zero residual degrees of freedom: no residual-based
    // diagnostic is defined, so construction errors instead of misleading.
    let x = array![[1.0, 1.0], [1.0, 2.0]];
    let y = array![1.0, 2.0];
    let err = OlsFit::new(x, y).unwrap_err();
    assert!(matches!(
        err,
        RegressionError::NoResidualDegreesOfFreedom { n: 2, p: 2, .. }
    ));
}

#[test]
fn shape_mismatch_error() {
    let x = Array2::<f64>::ones((5, 2));
    let y = Array1::<f64>::zeros(4);
    let err = OlsFit::new(x, y).unwrap_err();
    assert!(matches!(err, RegressionError::ShapeMismatch { .. }));
}

#[test]
fn empty_input_error() {
    let x = Array2::<f64>::zeros((0, 0));
    let y = Array1::<f64>::zeros(0);
    let err = OlsFit::new(x, y).unwrap_err();
    assert!(matches!(err, RegressionError::EmptyInput { .. }));
}