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>;