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
use ndarray::ArrayView1;
use statrs::distribution::{ContinuousCDF, Normal};

/// Theoretical-vs-sample quantile pairs for a normal QQ plot of `residuals`.
///
/// For sorted residuals, observation `i` (1-indexed) is paired with the standard
/// normal quantile at plotting position `(i − 0.5) / n`. The returned pairs are
/// `(theoretical_quantile, sample_quantile)`, sorted ascending by sample value —
/// ready to scatter.
///
/// This crate produces the *data*, not the rendering. The natural pairing is with
/// [`plotters-statistical`](https://crates.io/crates/plotters-statistical)'s
/// scatter/reference-line primitives: a straight diagonal (`y = x`) is the
/// "perfect normality" baseline, conceptually the same role its `RocCurve`
/// diagonal plays. That is a documented cross-crate integration point, not a hard
/// dependency — pass the pairs into any plotting backend you like.
///
/// Typically you feed in standardized or studentized residuals so the diagonal
/// reference line has unit slope; raw residuals produce a line whose slope is the
/// residual standard deviation.
///
/// # Example
///
/// ```
/// use ndarray::array;
/// use regression_diagnostics::residuals::qq_plot_data;
///
/// let r = array![-1.2, 0.3, -0.1, 0.9, 0.05];
/// let pairs = qq_plot_data(r.view());
/// assert_eq!(pairs.len(), 5);
/// // Sample quantiles come out sorted ascending.
/// assert!(pairs.windows(2).all(|w| w[0].1 <= w[1].1));
/// ```
pub fn qq_plot_data(residuals: ArrayView1<f64>) -> Vec<(f64, f64)> {
    let n = residuals.len();
    if n == 0 {
        return Vec::new();
    }
    let mut sorted: Vec<f64> = residuals.iter().copied().collect();
    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));

    let normal = Normal::new(0.0, 1.0).expect("standard normal is valid");
    sorted
        .into_iter()
        .enumerate()
        .map(|(idx, sample)| {
            let pos = (idx as f64 + 0.5) / n as f64;
            (normal.inverse_cdf(pos), sample)
        })
        .collect()
}