Skip to main content

robust_rs/
lib.rs

1//! Robust statistics for Rust, built around a single abstraction: the M-estimator
2//! loss `ρ`, its score `ψ = ρ'` and the IRLS weight `w(r) = ψ(r)/r`. Almost every
3//! robust method (a robust mean, a median, a Huber or bisquare regression, a
4//! high-breakdown S/MM fit, a robust covariance) is an M-estimator differing only
5//! in that loss, so implementing the [`RhoFunction`](crate::rho::RhoFunction) trait
6//! is enough to add an estimator. On that spine the crate layers location,
7//! regression and multivariate estimators, plus a result surface that reports the
8//! sampling theory derived from each loss: influence function, asymptotic variance,
9//! Gaussian efficiency, coefficient covariance and breakdown point.
10//!
11//! The workspace is split at the linear-algebra boundary. [`robust_rs_core`] owns
12//! the losses, robust scale, solver and theory and depends on nothing heavier than
13//! `libm` (so it builds for `wasm32`); this crate adds the estimators, which need
14//! `ndarray` + `faer`. The core is re-exported here, so `use robust_rs::prelude::*;`
15//! is all most callers need.
16//!
17//! # Choosing an estimator
18//!
19//! If you know the shape of your problem but not the name of the method:
20//!
21//! | Your problem | Reach for | Notes |
22//! |---|---|---|
23//! | A robust average of one variable | [`m_location`](crate::location::m_location) | or [`trimmed_mean`](crate::location::trimmed_mean) / [`hodges_lehmann`](crate::location::hodges_lehmann) |
24//! | Regression, outliers only in `y` | [`MEstimator`](crate::regression::MEstimator) + [`Huber`](crate::rho::Huber) | fast, convex, unique; **0 breakdown** against leverage |
25//! | Regression, outliers also in `X` (leverage) | [`MMEstimator`](crate::regression::MMEstimator) **(start here)** | R's `lmrob`; 50% breakdown **and** ≈95% efficiency |
26//! | Regression, want a bare high-breakdown fit | [`SEstimator`](crate::regression::SEstimator) / [`Lts`](crate::regression::Lts) | 50% breakdown, lower efficiency (what MM builds on) |
27//! | Simple one-predictor regression, no tuning | [`theil_sen`](crate::regression::theil_sen) | median of pairwise slopes |
28//! | Robust covariance / multivariate outliers | [`Mcd`](crate::multivariate::Mcd) **(start here)** / [`Ogk`](crate::multivariate::Ogk) | MCD is affine-equivariant; OGK is deterministic |
29//! | Just a robust spread of some numbers | [`Mad`](crate::scale::Mad) | or [`Qn`](crate::scale::Qn) / [`Sn`](crate::scale::Sn); Gaussian-consistent |
30//!
31//! Rule of thumb: for regression reach for `MMEstimator::default()` unless you
32//! know the contamination is purely vertical (then `MEstimator` is cheaper); for
33//! covariance and multivariate outlier flagging reach for `Mcd::new()`.
34//!
35//! # Robust location
36//!
37//! A robust location estimate is unmoved by a gross outlier that wrecks the mean:
38//!
39//! ```
40//! use robust_rs::prelude::*;
41//!
42//! let data = [2.1, 2.3, 1.9, 2.0, 2.2, 47.0]; // one gross outlier (mean ≈ 9.58)
43//! let fit = m_location(&data, &Huber::default(), &Mad::default(), &Control::default())?;
44//! assert!(fit.estimate < 3.0); // sits with the bulk near ≈ 2.16, not the mean
45//! # Ok::<(), robust_rs::error::RobustError>(())
46//! ```
47//!
48//! # Robust regression
49//!
50//! An outlier in the response drags ordinary least squares off; a Huber
51//! M-estimator resists it and drives the offending observation's weight toward 0:
52//!
53//! ```
54//! use ndarray::array;
55//! use robust_rs::prelude::*;
56//!
57//! // y ≈ 1 + 2x, with one gross outlier at x = 5.
58//! let x = array![
59//!     [1.0, 1.0], [1.0, 2.0], [1.0, 3.0], [1.0, 4.0], [1.0, 5.0],
60//!     [1.0, 6.0], [1.0, 7.0], [1.0, 8.0], [1.0, 9.0], [1.0, 10.0],
61//! ];
62//! let y = array![3.2, 4.8, 7.3, 9.1, 40.0, 12.7, 15.2, 17.1, 18.8, 21.3];
63//!
64//! let fit = MEstimator::new(Huber::default(), Mad::default()).fit(&x, &y)?;
65//! assert!((fit.coefficients()[1] - 2.0).abs() < 0.5); // slope ≈ 2, not dragged
66//! assert!(fit.weights[4] < 0.1);                        // the outlier is down-weighted
67//! # Ok::<(), robust_rs::error::RobustError>(())
68//! ```
69//!
70//! # Sampling theory
71//!
72//! Every [`RegressionFit`](crate::estimator::RegressionFit) implements
73//! [`RobustEstimator`](crate::estimator::RobustEstimator), reporting the sampling
74//! theory derived from its loss:
75//!
76//! ```
77//! # use ndarray::array;
78//! # use robust_rs::prelude::*;
79//! # let x = array![[1.0, 1.0], [1.0, 2.0], [1.0, 3.0], [1.0, 4.0], [1.0, 5.0]];
80//! # let y = array![1.1, 1.9, 3.2, 3.9, 5.1];
81//! let fit = MEstimator::new(Huber::default(), Mad::default()).fit(&x, &y)?;
82//!
83//! let _eff = fit.gaussian_efficiency();   // ≈ 0.95, asymptotic efficiency at the Gaussian
84//! let _var = fit.asymptotic_variance();   // V(ψ) = E[ψ²] / (E[ψ'])²
85//! let _cov = fit.coef_covariance(&x);     // ŝ²·V·(XᵀX)⁻¹, coefficient covariance
86//! let inf = fit.influence_function();     // x ↦ ψ(x) / E[ψ'], a bounded closure
87//! assert!(inf(1e6).is_finite());          // bounded influence: an extreme residual can't blow up
88//! # Ok::<(), robust_rs::error::RobustError>(())
89//! ```
90//!
91//! # High-breakdown regression
92//!
93//! A plain M-estimator bounds the influence of a large *residual* but has ~0
94//! breakdown against *leverage*. The high-breakdown estimators escape that; MM
95//! (R's `lmrob` default) pairs 50% breakdown with ≈ 95% efficiency. On the
96//! `starsCYG` H–R diagram four giant stars flip the OLS slope negative; MM recovers
97//! the physical positive slope and rejects them:
98//!
99//! ```
100//! use ndarray::Array2;
101//! use robust_rs::prelude::*;
102//!
103//! let (x_raw, y) = robust_rs::datasets::stars_cyg();
104//! let mut x = Array2::ones((x_raw.nrows(), 2)); // prepend an intercept column
105//! x.column_mut(1).assign(&x_raw.column(0));
106//!
107//! let mm = MMEstimator::default().fit(&x, &y)?;    // reproducible (fixed default seed)
108//! assert!(mm.coefficients()[1] > 0.0);             // main-sequence slope recovered
109//! assert!((mm.breakdown_point() - 0.5).abs() < 1e-9);
110//! # Ok::<(), robust_rs::error::RobustError>(())
111//! ```
112//!
113//! Randomized estimators ([`SEstimator`](crate::regression::SEstimator),
114//! [`MMEstimator`](crate::regression::MMEstimator), [`Lts`](crate::regression::Lts),
115//! [`Mcd`](crate::multivariate::Mcd)) are reproducible by default and expose a
116//! `.seed(u64)` builder and a `fit_with_rng` escape hatch; their per-subsample RNG
117//! sub-streams make results thread-count invariant.
118//!
119//! # Multivariate location and scatter
120//!
121//! The multivariate analogue of a robust residual is a robust **Mahalanobis
122//! distance** built from a robust location–scatter pair `(μ̂, Σ̂)`. Feeding a
123//! robust pair into the distance/outlier map defeats the *masking* that lets
124//! outliers hide from the classical covariance:
125//!
126//! ```
127//! use robust_rs::prelude::*;
128//!
129//! let (x, _y) = robust_rs::datasets::stackloss();      // 21 × 3 operating variables
130//! let mcd = Mcd::new().seed(1).fit(&x)?;                // FAST-MCD, affine equivariant
131//! let flags = mcd.outliers(0.975);                     // χ²_{p,0.975} cutoff
132//! assert_eq!(flags.len(), x.nrows());
133//! # Ok::<(), robust_rs::error::RobustError>(())
134//! ```
135//!
136//! # What is implemented
137//!
138//! ## Losses ([`rho`]): implement [`RhoFunction`](crate::rho::RhoFunction)
139//!
140//! | Loss | Default tuning | ψ | Gaussian efficiency | Bounded ρ | Redescending |
141//! |---|---|---|---|---|---|
142//! | [`LeastSquares`](crate::rho::LeastSquares) | – | monotone | 1.000 | no | no |
143//! | [`L1`](crate::rho::L1) | – | monotone | (median) | no | no |
144//! | [`Huber`](crate::rho::Huber) | `k = 1.345` | monotone (clipped) | ≈ 0.95 | no | no |
145//! | [`TukeyBiweight`](crate::rho::TukeyBiweight) | `c = 4.685` | redescends to 0 | ≈ 0.95 | `c²/6` | yes |
146//! | [`Cauchy`](crate::rho::Cauchy) | `c = 2.3849` | soft redescend | ≈ 0.95 | **no** (soft) | yes |
147//! | [`Welsch`](crate::rho::Welsch) | `c = 2.9846` | redescends | ≈ 0.95 | `c²/2` | yes |
148//! | [`Andrews`](crate::rho::Andrews) | `c = 1.339` | sine, hard cutoff | ≈ 0.95 | `2c²` | yes |
149//! | [`Hampel`](crate::rho::Hampel) | `(2, 4, 8)` | 3-part linear | ≈ 0.99 | `(a/2)(b+c−a)` | yes |
150//!
151//! The trait exposes `rho`, `psi`, `weight`, `psi_prime`, `tuning`,
152//! `is_redescending` and `rho_sup`. See [`docs/conventions.md`] for the
153//! `ψ'(0) = 1 ⇒ weight(0) = 1` convention every loss follows.
154//!
155//! ## Robust scale ([`scale`]): implement [`ScaleEstimator`](crate::scale::ScaleEstimator)
156//!
157//! | Estimator | Formula | Consistency | Efficiency | Breakdown |
158//! |---|---|---|---|---|
159//! | [`Mad`](crate::scale::Mad) | `1.4826 · med\|rᵢ − med r\|` | `1/Φ⁻¹(¾)` | ≈ 0.37 | 0.5 |
160//! | [`HuberProposal2`](crate::scale::HuberProposal2) | joint `(μ, s)`, `Σψ² = β` | closed-form `β` | – | – |
161//! | [`SScale`](crate::scale::SScale) | `s` solving `(1/n)Σρ(rᵢ/s) = δ` | `δ = E_Φ[ρ]` | (loss) | up to 0.5 |
162//! | [`Qn`](crate::scale::Qn) | `¼`-quantile of pairwise `\|rᵢ − rⱼ\|` | `2.2219` | ≈ 0.82 | 0.5 |
163//! | [`Sn`](crate::scale::Sn) | `med_i med_j \|rᵢ − rⱼ\|` | `1.1926` | ≈ 0.58 | 0.5 |
164//!
165//! ## Location ([`location`])
166//!
167//! - [`m_location`](crate::location::m_location): M-estimate of location by IRLS
168//!   (returns [`LocationFit`](crate::location::LocationFit)).
169//! - [`trimmed_mean`](crate::location::trimmed_mean) /
170//!   [`winsorized_mean`](crate::location::winsorized_mean): `α`-trimmed / Winsorized means.
171//! - [`hodges_lehmann`](crate::location::hodges_lehmann): median of the Walsh
172//!   averages (returns [`HodgesLehmannFit`](crate::location::HodgesLehmannFit)).
173//!
174//! ## Regression ([`regression`])
175//!
176//! - [`MEstimator`](crate::regression::MEstimator): M-regression by IRLS
177//!   (convex/unique for monotone losses; **0 breakdown** against leverage).
178//! - [`SEstimator`](crate::regression::SEstimator): FAST-S, 50% breakdown by
179//!   minimizing an S-scale of the residuals.
180//! - [`MMEstimator`](crate::regression::MMEstimator): S-init + fixed-scale
181//!   redescending M-step ⇒ 50% breakdown **and** ≈ 95% efficiency.
182//! - [`Lts`](crate::regression::Lts): FAST-LTS with a coverage knob (returns
183//!   [`LtsFit`](crate::regression::LtsFit)).
184//! - [`theil_sen`](crate::regression::theil_sen): median of pairwise slopes for
185//!   simple regression (returns [`TheilSenFit`](crate::regression::TheilSenFit)).
186//! - [`weighted_least_squares`](crate::wls::weighted_least_squares): the WLS core
187//!   (rank-revealing QR on `√W·X`, never the normal equations).
188//! - M/S/MM fits are [`RegressionFit`](crate::estimator::RegressionFit)s
189//!   implementing [`RobustEstimator`](crate::estimator::RobustEstimator).
190//!
191//! ## Multivariate ([`multivariate`])
192//!
193//! [`Mcd`](crate::multivariate::Mcd), [`Ogk`](crate::multivariate::Ogk) and
194//! [`MScatter`](crate::multivariate::MScatter) produce a Gaussian-calibrated
195//! location–scatter pair and implement [`RobustScatter`](crate::multivariate::RobustScatter)
196//! (the χ²-calibrated distance/outlier map); [`Tyler`](crate::multivariate::Tyler)
197//! identifies *shape* only, so it returns a bespoke
198//! [`TylerFit`](crate::multivariate::TylerFit) that does **not** implement that
199//! trait (its distances aren't χ²-calibrated).
200//!
201//! - [`Mcd`](crate::multivariate::Mcd): FAST-MCD, affine equivariant, 50%
202//!   breakdown, reweighted (RMCD) by default (returns
203//!   [`McdFit`](crate::multivariate::McdFit)).
204//! - [`Ogk`](crate::multivariate::Ogk): deterministic, positive-definite
205//!   orthogonalized Gnanadesikan–Kettenring estimator.
206//! - [`MScatter`](crate::multivariate::MScatter): monotone M-estimator of
207//!   location/scatter (low breakdown; the multivariate analogue of M-regression).
208//! - [`Tyler`](crate::multivariate::Tyler): distribution-free M-estimator of
209//!   *shape* (unit determinant); returns [`TylerFit`](crate::multivariate::TylerFit).
210//! - [`mahalanobis`](crate::multivariate::mahalanobis): the robust distance /
211//!   outlier map over any Gaussian-calibrated `(μ̂, Σ̂)` pair, plus the classical
212//!   baseline.
213//!
214//! ## Theory ([`theory`])
215//!
216//! - [`influence_function`](crate::theory::influence_function),
217//!   [`asymptotic_variance`](crate::theory::asymptotic_variance),
218//!   [`gaussian_efficiency`](crate::theory::gaussian_efficiency),
219//!   [`breakdown_point`](crate::theory::breakdown_point).
220//! - [`gauss_hermite`](crate::theory::gauss_hermite) quadrature (Golub–Welsch) and
221//!   the expectation helpers [`expect_psi_squared`](crate::theory::expect_psi_squared) /
222//!   [`expect_psi_prime`](crate::theory::expect_psi_prime) (the latter via Stein's
223//!   identity, so kinked scores and L1 are exact).
224//!
225//! ## Types and errors
226//!
227//! Newtypes make illegal states unrepresentable:
228//! [`Scale`](crate::types::Scale), [`TuningConstant`](crate::types::TuningConstant),
229//! and the [`RawResidual`](crate::types::RawResidual) /
230//! [`ScaledResidual`](crate::types::ScaledResidual) pair; all fallible operations
231//! return [`RobustError`](crate::error::RobustError).
232//!
233//! # Cargo features
234//!
235//! - `rayon`: parallelize the FAST-MCD random starts (thread-count invariant).
236//!
237//! # Datasets
238//!
239//! [`datasets::stackloss`] and [`datasets::stars_cyg`] vendor the classic
240//! `robustbase` reference data (predictors only; prepend your own intercept
241//! column).
242//!
243//! [`docs/conventions.md`]: https://github.com/coes300/robust-rs/blob/master/docs/conventions.md
244#![deny(missing_docs)]
245
246pub mod datasets;
247pub mod estimator;
248pub mod location;
249pub mod multivariate;
250pub mod regression;
251pub mod wls;
252
253mod util; // small internal numeric helpers (shared median)
254
255pub mod prelude;
256
257#[doc(inline)]
258pub use robust_rs_core::{error, rho, scale, theory, types};