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
//! Error type shared across every diagnostic in the crate.

use thiserror::Error;

/// Errors returned when constructing an [`OlsFit`](crate::OlsFit) or computing a
/// diagnostic on it.
///
/// The policy on degenerate inputs is deliberate and documented per-variant:
/// the crate **hard-errors** on inputs where no meaningful statistic exists
/// (empty data, a shape mismatch, non-positive residual degrees of freedom, a
/// rank-deficient design matrix) rather than silently returning `NaN`. The one
/// place `NaN` is used *intentionally* is a per-column result slot that is
/// genuinely undefined for that column (e.g. the VIF of the intercept column);
/// that is a documented sentinel, not an error condition.
#[derive(Debug, Error, Clone, PartialEq)]
pub enum RegressionError {
    /// The design matrix or target was empty.
    #[error("empty input: {what}")]
    EmptyInput {
        /// Which input was empty.
        what: &'static str,
    },

    /// Two inputs that had to agree on a dimension did not (e.g. `X` has a
    /// different number of rows than `y` has entries).
    #[error("shape mismatch: {what} expected {expected}, got {got}")]
    ShapeMismatch {
        /// Human-readable description of the mismatched quantity.
        what: &'static str,
        /// The value that was expected.
        expected: usize,
        /// The value that was supplied.
        got: usize,
    },

    /// Residual degrees of freedom (`n - p`) are not strictly positive, so the
    /// residual variance, and every statistic derived from it, is undefined.
    ///
    /// This is exactly the "`n` close to the number of parameters" edge case:
    /// with `n <= p` the model has no residual freedom left and reporting any
    /// residual-based diagnostic would be misleading, so construction fails
    /// clearly instead.
    #[error(
        "non-positive residual degrees of freedom: {n} observations with {p} \
         parameters leaves n - p = {df}, which must be >= 1"
    )]
    NoResidualDegreesOfFreedom {
        /// Number of observations supplied.
        n: usize,
        /// Number of model parameters (design-matrix columns).
        p: usize,
        /// The offending `n - p` value.
        df: isize,
    },

    /// The design matrix is not full column rank, so the OLS solution is not
    /// unique and QR cannot recover the coefficients.
    ///
    /// Perfectly collinear predictors are the usual cause. VIF has its own,
    /// softer handling of near/exact collinearity (it reports a very large or
    /// infinite value rather than erroring); this variant is for the *primary*
    /// model fit, where a non-unique solution has no sensible fallback.
    #[error("rank-deficient design matrix: columns are linearly dependent (perfect collinearity)")]
    RankDeficient,

    /// A caller-supplied hyperparameter was outside its valid range (e.g. a
    /// negative ridge/lasso penalty `λ`).
    #[error("invalid parameter: {msg}")]
    InvalidParameter {
        /// Human-readable explanation of what was wrong.
        msg: String,
    },

    /// The response passed to a logistic fit was not a valid binary outcome —
    /// either it contained values other than `0` and `1`, or it was entirely one
    /// class (so the maximum-likelihood fit is degenerate / non-identifiable).
    #[error("invalid binary response: {msg}")]
    InvalidResponse {
        /// Human-readable explanation of what was wrong.
        msg: String,
    },

    /// An iterative fit (IRLS for logistic regression, coordinate descent for
    /// lasso) did not converge within its iteration budget.
    ///
    /// For logistic regression the usual cause is **perfect** or
    /// **quasi-complete separation**, where the maximum-likelihood coefficients
    /// diverge to ±∞ and no finite fit exists — a real modeling problem the
    /// caller needs to know about, not a solver detail to paper over.
    #[error("iterative fit did not converge within {iterations} iterations: {msg}")]
    NotConverged {
        /// Number of iterations attempted before giving up.
        iterations: usize,
        /// Human-readable note on the likely cause.
        msg: String,
    },
}

/// Convenience alias for results returned throughout this crate.
pub type Result<T> = std::result::Result<T, RegressionError>;