regression-diagnostics
Statistical regression diagnostics for Rust — the kind of surface R's
car/lmtest and Python's statsmodels expose, packaged as a dependency-light
crate. It fills a specific ecosystem gap: no maintained Rust crate offers Variance
Inflation Factor, adjusted R², or residual diagnostics (autocorrelation,
heteroskedasticity, normality, influence).
The core is ordinary least squares, built on one fitted-model type,
[OlsFit], computed once via a single QR factorization and reused by every
diagnostic. Two further model families each get their own module with the
diagnostics that are actually well-defined for them:
- Regularized regression ([
regularized]) — [RidgeFit] and [LassoFit], with effective degrees of freedom, ridge leverage/GCV, and a ridge-VIF that reduces to the OLS VIF atλ = 0(the "VIF before/after regularization" comparison). - Binary logistic regression ([
logistic]) — [LogisticFit] via IRLS, with deviance/Pearson residuals, McFadden pseudo-R², the Hosmer–Lemeshow test, and logistic leverage / Cook's distance.
use array;
use OlsFit;
// Caller supplies the intercept column (leading ones).
let x = array!;
let y = array!;
let fit = new.unwrap;
println!; // full statsmodels-style report
fit.summary() renders like this (from cargo run --example full_diagnostics):
============================== OLS Diagnostics ===============================
No. Observations: 12 Df Residuals: 8 Df Model: 3
R-squared: 0.9797 Adj. R-squared: 0.9720 Resid. SE: 17.1435
F-statistic: 128.4729 Prob(F): 0.0000 Log-Lik: -48.69
AIC: 105.39 BIC: 107.33 Cond. No.: 2.012e2
------------------------------------------------------------------------------
coef std err t P>|t| beta VIF
------------------------------------------------------------------------------
const 133.3251 28.5980 4.662 0.002 - -
x1 17.7810 2.7081 6.566 0.000 1.471 19.74
x2 -6.8850 0.5978 -11.517 0.000 -0.793 1.86
x3 -23.1377 19.6447 -1.178 0.273 -0.242 16.63
------------------------------------------------------------------------------
Durbin-Watson: 2.2030 (residual autocorrelation; ~2 is ideal)
Jarque-Bera: 0.4109 Prob: 0.8143 (skew 0.217, kurt 2.204)
Breusch-Pagan: 1.2231 Prob: 0.7475 (heteroskedasticity, LM)
White: 7.1759 Prob: 0.6188 (heteroskedasticity, general)
==============================================================================
Summary is structured data first — every number above is a field you can read
programmatically; the table is just a Display convenience on top.
What's covered
OLS
| Area | This crate | statsmodels OLSResults equivalent |
|---|---|---|
| Coefficients, std errors, t, p | OlsFit + summary() |
.params, .bse, .tvalues, .pvalues |
| R² / adjusted R² | fit_statistics::r_squared / adjusted_r_squared |
.rsquared / .rsquared_adj |
| F-statistic (overall) | fit_statistics::f_statistic |
.fvalue, .f_pvalue |
| Log-likelihood, AIC, BIC | fit_statistics::{log_likelihood, aic, bic} |
.llf, .aic, .bic |
| VIF (per predictor) | multicollinearity::vif |
statsmodels.stats.outliers_influence.variance_inflation_factor |
| Condition number | multicollinearity::condition_number |
.condition_number (see scaling note) |
| Durbin-Watson | residuals::durbin_watson |
statsmodels.stats.stattools.durbin_watson |
| Breusch-Pagan | residuals::breusch_pagan |
het_breuschpagan |
| White's test | residuals::white_test |
het_white |
| Jarque-Bera | residuals::jarque_bera |
jarque_bera |
| Standardized / studentized residuals | residuals::{standardized, internally_studentized, externally_studentized}_residuals |
OLSInfluence.resid_studentized_{internal,external} |
| Leverage | influence::leverage |
OLSInfluence.hat_matrix_diag |
| Cook's distance | influence::cooks_distance |
OLSInfluence.cooks_distance |
| DFFITS | influence::dffits |
OLSInfluence.dffits |
| QQ-plot data | residuals::qq_plot_data |
qqplot (data only; rendering is left to a plotting crate) |
| Standardized (beta) coefficients | coefficients::standardized_coefficients |
(not built in) |
| One-call report | OlsFit::summary() |
.summary() |
Validated against statsmodels' published Longley-dataset output — coefficients,
R²/adjusted R², F, log-likelihood, AIC, BIC, Durbin-Watson, and the six
per-predictor VIFs all match (see tests/reference_datasets/longley.rs).
Regularized regression
| Area | This crate |
|---|---|
| Ridge fit (closed form via SVD) | regularized::RidgeFit |
Effective degrees of freedom Σ dⱼ²/(dⱼ²+λ) |
RidgeFit::effective_df |
Ridge leverage (diag H_λ) |
RidgeFit::leverage |
| GCV score / GCV λ-selection | RidgeFit::gcv, regularized::select_lambda_gcv |
| Effective AIC/BIC | RidgeFit::{aic, bic} |
| Ridge VIF (→ OLS VIF at λ=0) | RidgeFit::ridge_vif |
| Lasso fit (coordinate descent) | regularized::LassoFit |
| Active set / non-zero count (df estimate) | LassoFit::{active_set, n_nonzero} |
Anchored on exact identities: RidgeFit/LassoFit at λ = 0 reproduce the OLS
coefficients, ridge leverage sums to the effective df, and ridge VIF equals the
OLS VIF at λ = 0 (see tests/regularized.rs).
Binary logistic regression
| Area | This crate | statsmodels equivalent |
|---|---|---|
| Logistic fit (IRLS) | logistic::LogisticFit |
Logit / GLM(Binomial) |
| Coefficients, std errors, z, p | LogisticFit::{coefficients, coefficient_standard_errors, z_values, p_values} |
.params, .bse, .tvalues, .pvalues |
| Deviance / Pearson residuals | logistic::{deviance_residuals, pearson_residuals} |
.resid_deviance, .resid_pearson |
| Null/residual deviance, McFadden R², AIC/BIC | LogisticFit::goodness_of_fit |
.null_deviance, .deviance, .prsquared, .aic |
| Hosmer–Lemeshow test | LogisticFit::hosmer_lemeshow |
(statsmodels contrib) |
| Logistic leverage / Cook's distance | logistic::{leverage, cooks_distance} |
GLMInfluence.hat_matrix_diag, .cooks_distance |
Anchored on exact identities too: the fit satisfies the score equations
Xᵀ(y − p) = 0, a single binary predictor recovers the log-odds and
log-odds-ratio in closed form, and deviance residuals square to the residual
deviance (see tests/logistic.rs).
Linear algebra dependency
The internals use nalgebra (pure-Rust
QR/SVD) while the public API speaks ndarray.
That split is deliberate:
ndarray-linalgwould be faster but pulls in a system-level BLAS/LAPACK, which cuts against the portability goal (this series is Docker-first and multi-arch).nalgebrais pure Rust — no external BLAS — so the crate stays portable, at the cost of a small conversion layer at the API boundary, since the rest of the guide's code isndarray-based.
We take that trade in favour of portability: nalgebra is used purely internally
and never appears in the public API, so callers only ever see ndarray types.
Two numerical choices worth flagging:
- Coefficients come from a QR solve of
X, never from invertingXᵀX. FormingXᵀXsquares the condition number — exactly the quantity the multicollinearity diagnostics measure — which would make VIF and the condition number untrustworthy on the ill-conditioned designs where they matter most. - Leverage (
diag(H)) is read from the thinQfactor as row norms, so the fulln × nhat matrix is never materialized (O(n·p)memory, notO(n²)).
Conventions worth knowing
- Intercept: the caller owns the design matrix.
OlsFit::newfits the columns as given (auto-detecting a constant column as the intercept);OlsFit::with_interceptprepends a ones column for you. Getting this wrong silently biases every downstream statistic, so it's explicit at the call site. - AIC/BIC count the regression parameters (intercept included) as
k, notσ²— matchingstatsmodels' convention so values line up. - Condition number is computed on the design as fitted (unscaled columns). Tools that normalize columns first report a different number; the convention is documented so the value is reproducible.
- Thresholds (VIF > 5/10, condition number > 30, Cook's D > 4/n, …) are documented as convention, not mathematical fact — they vary by source.
- Undefined-per-column results (VIF and beta of the intercept) are returned as
NaNin the column's slot, aligned to the design columns, rather than shifting indices.
Regularization and logistic conventions
Because ridge, lasso and logistic have different structure from OLS, a few choices are worth stating (each is documented on the type):
- Ridge/lasso intercept is an unpenalized, mean-centered constant (the
scikit-learn convention). Lasso standardizes predictors internally, so its
λ(on the(1/2n)‖y−Xβ‖² + λ‖β‖₁objective) is not comparable to ridge's. Neither estimator is scale-invariant — standardize predictors if that matters. - Effective degrees of freedom replace the raw parameter count under
shrinkage:
Σ dⱼ²/(dⱼ²+λ)for ridge, the active-set size for lasso. The information criteria use these, so a shrunk model is charged for the freedom it actually spends. - Ridge VIF is the diagonal of
(R+λI)⁻¹R(R+λI)⁻¹on standardized predictors; it can fall below 1, because shrinkage reduces coefficient variance below the orthogonal-OLS baseline — that is the point of the before/after comparison. - Logistic requires a
0/1response with both classes present. Perfect or quasi-complete separation makes the MLE diverge; IRLS then returnsNotConvergedrather than reporting meaningless coefficients.
Out of scope for v0.1
Still noted rather than silently omitted:
- Other GLM families — Poisson/negative-binomial counts, multinomial/ordinal logistic, gamma. Each needs its own link/variance functions and residual scales; only binary logistic is implemented.
- Penalized-GLM and elastic-net diagnostics, survival models, and mixed / hierarchical models — separate frameworks, deferred to possible future crates.
Examples
qq_plot_integration shows the intended cross-crate pairing: this crate produces
QQ-plot data, and a plotting crate (plotters / plotters-statistical) renders
it against a y = x normality reference line.
License
MIT.