robust_rs/multivariate.rs
1//! Multivariate robust statistics: robust estimates of multivariate location
2//! and scatter and the outlier detection built on them.
3//!
4//! The organizing object here is a **location–scatter pair** `(μ̂, Σ̂)` (a
5//! robust centre and a robust covariance) from which a robust Mahalanobis
6//! distance `dᵢ = √((xᵢ − μ̂)ᵀ Σ̂⁻¹ (xᵢ − μ̂))` flags multivariate outliers, just
7//! as robust residuals flag them in regression. Four estimators produce such a
8//! pair, trading efficiency against breakdown and equivariance:
9//!
10//! - [`Mcd`]: the Minimum Covariance Determinant (Rousseeuw 1985) via FAST-MCD
11//! (Rousseeuw & Van Driessen 1999): 50%-breakdown, affine-equivariant, the
12//! multivariate analogue of LTS.
13//! - [`Ogk`]: the Orthogonalized Gnanadesikan–Kettenring estimator
14//! (Maronna & Zamar 2002): a fast, deterministic, positive-definite pairwise
15//! estimator (orthogonally, not fully affine, equivariant).
16//! - [`MScatter`]: a monotone M-estimator of location and scatter
17//! (Maronna 1976): the direct multivariate analogue of the regression
18//! M-estimator, reusing a [`robust_rs_core::rho::RhoFunction`] weight.
19//! - [`Tyler`]: Tyler's (1987) distribution-free M-estimator of *shape*,
20//! normalized to unit determinant.
21//!
22//! [`mahalanobis`] exposes the distance/outlier map over *any* `(μ̂, Σ̂)` pair,
23//! together with the classical (non-robust) mean/covariance baseline.
24
25mod chi2;
26mod correction;
27mod linalg;
28
29mod m_scatter;
30pub mod mahalanobis;
31mod mcd;
32mod ogk;
33mod tyler;
34
35pub use self::m_scatter::MScatter;
36pub use self::mcd::{Mcd, McdFit};
37pub use self::ogk::Ogk;
38pub use self::tyler::{Tyler, TylerFit};
39
40use ndarray::{Array1, Array2};
41use robust_rs_core::error::RobustError;
42
43use self::chi2::chi2_quantile;
44use self::linalg::{mahalanobis_sq, spd_inverse_logdet};
45
46/// A fitted robust location–scatter estimate.
47///
48/// The shared result type of [`MScatter`] and [`Ogk`]. FAST-MCD carries extra
49/// structure (a raw estimate and the retained subset), so it returns its own
50/// [`McdFit`], but that type implements [`RobustScatter`] too, so all three are
51/// interchangeable behind the trait and the Mahalanobis/outlier map is written
52/// once against `(μ̂, Σ̂)`. [`Tyler`] does *not* use this type: it returns a
53/// bespoke [`TylerFit`] implementing no shared trait, because its shape-only
54/// distances are not `χ²`-calibrated, so this type's Gaussian outlier map would
55/// misapply to them.
56#[derive(Debug, Clone)]
57pub struct ScatterFit {
58 /// Robust location estimate `μ̂` (a `p`-vector).
59 pub location: Array1<f64>,
60 /// Robust scatter (covariance) estimate `Σ̂` (`p × p`, symmetric positive
61 /// definite).
62 pub scatter: Array2<f64>,
63 /// Robust Mahalanobis distances `dᵢ` of the rows the fit was computed on.
64 pub distances: Array1<f64>,
65 /// Final per-observation weights (their meaning is estimator-specific: a
66 /// hard `0/1` acceptance for a reweighting estimator, a smooth `w(dᵢ)` for
67 /// an M-estimator).
68 pub weights: Array1<f64>,
69}
70
71/// Quantities every fitted robust covariance estimator can report, the
72/// multivariate counterpart of [`crate::estimator::RobustEstimator`].
73pub trait RobustScatter {
74 /// Robust location estimate `μ̂`.
75 fn location(&self) -> &Array1<f64>;
76 /// Robust scatter estimate `Σ̂`.
77 fn scatter(&self) -> &Array2<f64>;
78 /// Robust Mahalanobis distances of the fitted rows.
79 fn distances(&self) -> &Array1<f64>;
80
81 /// The distance cutoff `√χ²_{p, quantile}` (e.g. `quantile = 0.975`) against
82 /// which robust Mahalanobis distances are compared to flag outliers.
83 fn distance_cutoff(&self, quantile: f64) -> f64 {
84 let p = self.scatter().nrows() as f64;
85 chi2_quantile(quantile, p).sqrt()
86 }
87
88 /// Flag each fitted observation as an outlier when its robust distance
89 /// exceeds `distance_cutoff(quantile)`. Under a `p`-variate Gaussian the
90 /// squared distances are `χ²_p`, so `quantile = 0.975` flags ≈ 2.5% of clean
91 /// data.
92 fn outliers(&self, quantile: f64) -> Vec<bool> {
93 let cut = self.distance_cutoff(quantile);
94 self.distances().iter().map(|&d| d > cut).collect()
95 }
96}
97
98impl RobustScatter for ScatterFit {
99 fn location(&self) -> &Array1<f64> {
100 &self.location
101 }
102 fn scatter(&self) -> &Array2<f64> {
103 &self.scatter
104 }
105 fn distances(&self) -> &Array1<f64> {
106 &self.distances
107 }
108}
109
110/// Robust Mahalanobis distances `dᵢ = √((xᵢ − loc)ᵀ scatter⁻¹ (xᵢ − loc))` for
111/// every row of `x`. Shared by the estimators to populate [`ScatterFit`] and
112/// re-exported publicly as [`mahalanobis::mahalanobis_distances`]. Errors if the
113/// scatter is not positive definite.
114pub(crate) fn distances_from(
115 x: &Array2<f64>,
116 loc: &Array1<f64>,
117 scatter: &Array2<f64>,
118) -> Result<Array1<f64>, RobustError> {
119 let (inv, _logdet) = spd_inverse_logdet(scatter)?;
120 Ok(mahalanobis_sq(x, loc, &inv).mapv(f64::sqrt))
121}