Skip to main content

regression_diagnostics/
error.rs

1//! Error type shared across every diagnostic in the crate.
2
3use thiserror::Error;
4
5/// Errors returned when constructing an [`OlsFit`](crate::OlsFit) or computing a
6/// diagnostic on it.
7///
8/// The policy on degenerate inputs is deliberate and documented per-variant:
9/// the crate **hard-errors** on inputs where no meaningful statistic exists
10/// (empty data, a shape mismatch, non-positive residual degrees of freedom, a
11/// rank-deficient design matrix) rather than silently returning `NaN`. The one
12/// place `NaN` is used *intentionally* is a per-column result slot that is
13/// genuinely undefined for that column (e.g. the VIF of the intercept column);
14/// that is a documented sentinel, not an error condition.
15#[derive(Debug, Error, Clone, PartialEq)]
16pub enum RegressionError {
17    /// The design matrix or target was empty.
18    #[error("empty input: {what}")]
19    EmptyInput {
20        /// Which input was empty.
21        what: &'static str,
22    },
23
24    /// Two inputs that had to agree on a dimension did not (e.g. `X` has a
25    /// different number of rows than `y` has entries).
26    #[error("shape mismatch: {what} expected {expected}, got {got}")]
27    ShapeMismatch {
28        /// Human-readable description of the mismatched quantity.
29        what: &'static str,
30        /// The value that was expected.
31        expected: usize,
32        /// The value that was supplied.
33        got: usize,
34    },
35
36    /// Residual degrees of freedom (`n - p`) are not strictly positive, so the
37    /// residual variance, and every statistic derived from it, is undefined.
38    ///
39    /// This is exactly the "`n` close to the number of parameters" edge case:
40    /// with `n <= p` the model has no residual freedom left and reporting any
41    /// residual-based diagnostic would be misleading, so construction fails
42    /// clearly instead.
43    #[error(
44        "non-positive residual degrees of freedom: {n} observations with {p} \
45         parameters leaves n - p = {df}, which must be >= 1"
46    )]
47    NoResidualDegreesOfFreedom {
48        /// Number of observations supplied.
49        n: usize,
50        /// Number of model parameters (design-matrix columns).
51        p: usize,
52        /// The offending `n - p` value.
53        df: isize,
54    },
55
56    /// The design matrix is not full column rank, so the OLS solution is not
57    /// unique and QR cannot recover the coefficients.
58    ///
59    /// Perfectly collinear predictors are the usual cause. VIF has its own,
60    /// softer handling of near/exact collinearity (it reports a very large or
61    /// infinite value rather than erroring); this variant is for the *primary*
62    /// model fit, where a non-unique solution has no sensible fallback.
63    #[error("rank-deficient design matrix: columns are linearly dependent (perfect collinearity)")]
64    RankDeficient,
65
66    /// A caller-supplied hyperparameter was outside its valid range (e.g. a
67    /// negative ridge/lasso penalty `λ`).
68    #[error("invalid parameter: {msg}")]
69    InvalidParameter {
70        /// Human-readable explanation of what was wrong.
71        msg: String,
72    },
73
74    /// The response passed to a logistic fit was not a valid binary outcome —
75    /// either it contained values other than `0` and `1`, or it was entirely one
76    /// class (so the maximum-likelihood fit is degenerate / non-identifiable).
77    #[error("invalid binary response: {msg}")]
78    InvalidResponse {
79        /// Human-readable explanation of what was wrong.
80        msg: String,
81    },
82
83    /// An iterative fit (IRLS for logistic regression, coordinate descent for
84    /// lasso) did not converge within its iteration budget.
85    ///
86    /// For logistic regression the usual cause is **perfect** or
87    /// **quasi-complete separation**, where the maximum-likelihood coefficients
88    /// diverge to ±∞ and no finite fit exists — a real modeling problem the
89    /// caller needs to know about, not a solver detail to paper over.
90    #[error("iterative fit did not converge within {iterations} iterations: {msg}")]
91    NotConverged {
92        /// Number of iterations attempted before giving up.
93        iterations: usize,
94        /// Human-readable note on the likely cause.
95        msg: String,
96    },
97}
98
99/// Convenience alias for results returned throughout this crate.
100pub type Result<T> = std::result::Result<T, RegressionError>;