robustmodels 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- robustmodels/__init__.py +12 -0
- robustmodels/_validation.py +40 -0
- robustmodels/covariance.py +1191 -0
- robustmodels/location.py +241 -0
- robustmodels/scale.py +758 -0
- robustmodels-0.1.0.dist-info/METADATA +151 -0
- robustmodels-0.1.0.dist-info/RECORD +9 -0
- robustmodels-0.1.0.dist-info/WHEEL +4 -0
- robustmodels-0.1.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,1191 @@
|
|
|
1
|
+
"""Robust covariance estimators and their fitting steps.
|
|
2
|
+
|
|
3
|
+
The estimators follow the scikit-learn API and extend
|
|
4
|
+
:class:`sklearn.covariance.EmpiricalCovariance`. ``fit(X)`` takes observations
|
|
5
|
+
in rows and sets ``location_``, ``covariance_``, ``precision_`` and ``dist_``
|
|
6
|
+
(squared robust distances of the training data). The inherited
|
|
7
|
+
``mahalanobis(X)`` returns squared robust distances of new observations, and
|
|
8
|
+
``score``, ``error_norm`` and ``get_precision`` work as in scikit-learn.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from scipy import linalg, stats
|
|
16
|
+
from scipy.stats import chi2
|
|
17
|
+
from sklearn.covariance import EmpiricalCovariance
|
|
18
|
+
from sklearn.utils.validation import validate_data
|
|
19
|
+
|
|
20
|
+
from ._validation import check_count, check_option, check_range
|
|
21
|
+
from .scale import (
|
|
22
|
+
_bisquare_weights,
|
|
23
|
+
_mscale_bisquare,
|
|
24
|
+
_rho,
|
|
25
|
+
_tau_estimates,
|
|
26
|
+
_tuning_for_breakdown,
|
|
27
|
+
_tuning_for_shape_efficiency,
|
|
28
|
+
mad,
|
|
29
|
+
pn,
|
|
30
|
+
qn,
|
|
31
|
+
tau_scale,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"MCD",
|
|
36
|
+
"MRCD",
|
|
37
|
+
"MVE",
|
|
38
|
+
"OGK",
|
|
39
|
+
"MMEstimator",
|
|
40
|
+
"SEstimator",
|
|
41
|
+
"h_alpha_n",
|
|
42
|
+
"mcd_consistency",
|
|
43
|
+
"mcd_small_sample",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _RobustCovariance(EmpiricalCovariance):
|
|
48
|
+
"""Base class; ``mahalanobis``, ``score`` and ``error_norm`` come from scikit-learn."""
|
|
49
|
+
|
|
50
|
+
store_precision = True # read by the inherited methods
|
|
51
|
+
assume_centered = False
|
|
52
|
+
|
|
53
|
+
def _validate(self, X):
|
|
54
|
+
return validate_data(self, X, dtype=np.float64, ensure_min_samples=2)
|
|
55
|
+
|
|
56
|
+
def _set_estimates(self, location, covariance, X):
|
|
57
|
+
self.location_ = np.asarray(location, dtype=float)
|
|
58
|
+
self.covariance_ = np.asarray(covariance, dtype=float)
|
|
59
|
+
self.precision_ = linalg.pinvh(self.covariance_)
|
|
60
|
+
diff = X - self.location_
|
|
61
|
+
self.dist_ = np.sum((diff @ self.precision_) * diff, axis=1)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class MCD(_RobustCovariance):
|
|
65
|
+
"""Minimum Covariance Determinant estimator (Rousseeuw 1984).
|
|
66
|
+
|
|
67
|
+
Mean and covariance of the ``h`` observations whose covariance matrix has the
|
|
68
|
+
smallest determinant, scaled for consistency at the normal model. The
|
|
69
|
+
reweighting step then recomputes both from the observations whose squared
|
|
70
|
+
robust distance is below the 0.975 chi-square quantile. Breakdown point
|
|
71
|
+
``(n - h + 1) / n``, about 50% for ``alpha = 0.5``. Needs ``n > p``; use
|
|
72
|
+
:class:`MRCD` otherwise. If ``h`` observations lie on a hyperplane, a
|
|
73
|
+
``RuntimeWarning`` reports the exact fit and the raw estimates are kept.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
alpha : float in [0.5, 1], default 0.5
|
|
78
|
+
Coverage. The subset size ``h = h_alpha_n(alpha, n, p)`` goes from
|
|
79
|
+
``(n + p + 1) // 2`` (maximal breakdown) at 0.5 to ``n`` at 1.
|
|
80
|
+
algorithm : {"fast", "deterministic"}, default "fast"
|
|
81
|
+
FAST-MCD with random starts (Rousseeuw & Van Driessen 1999), or DetMCD
|
|
82
|
+
with six deterministic starts (Hubert, Rousseeuw & Verdonck 2012).
|
|
83
|
+
With one variable both use the exact univariate MCD.
|
|
84
|
+
n_trials : int, default 500
|
|
85
|
+
Random starting subsets of FAST-MCD.
|
|
86
|
+
reweighting : bool, default True
|
|
87
|
+
Apply the reweighting step.
|
|
88
|
+
small_sample_correction : bool, default True
|
|
89
|
+
Multiply the covariance by the factors of Pison, Van Aelst & Willems (2002).
|
|
90
|
+
random_state : int, numpy.random.Generator, numpy.random.RandomState or None
|
|
91
|
+
Seed of FAST-MCD.
|
|
92
|
+
|
|
93
|
+
Attributes
|
|
94
|
+
----------
|
|
95
|
+
location_ : ndarray of shape (n_features,)
|
|
96
|
+
Final location.
|
|
97
|
+
covariance_ : ndarray of shape (n_features, n_features)
|
|
98
|
+
Final covariance matrix.
|
|
99
|
+
precision_ : ndarray of shape (n_features, n_features)
|
|
100
|
+
Pseudo-inverse of ``covariance_``.
|
|
101
|
+
support_ : ndarray of bool, shape (n_samples,)
|
|
102
|
+
Observations used for the final estimates.
|
|
103
|
+
dist_ : ndarray of shape (n_samples,)
|
|
104
|
+
Squared robust distances of the training data.
|
|
105
|
+
raw_location_ : ndarray of shape (n_features,)
|
|
106
|
+
Mean of the ``h``-subset.
|
|
107
|
+
raw_covariance_ : ndarray of shape (n_features, n_features)
|
|
108
|
+
Corrected covariance of the ``h``-subset.
|
|
109
|
+
raw_support_ : ndarray of bool, shape (n_samples,)
|
|
110
|
+
The ``h``-subset.
|
|
111
|
+
h_ : int
|
|
112
|
+
Subset size.
|
|
113
|
+
|
|
114
|
+
References
|
|
115
|
+
----------
|
|
116
|
+
Rousseeuw, P. J. (1984). Least median of squares regression. *Journal of the
|
|
117
|
+
American Statistical Association*, 79(388), 871-880.
|
|
118
|
+
|
|
119
|
+
Rousseeuw, P. J. and Van Driessen, K. (1999). A fast algorithm for the
|
|
120
|
+
minimum covariance determinant estimator. *Technometrics*, 41(3), 212-223.
|
|
121
|
+
|
|
122
|
+
Hubert, M., Rousseeuw, P. J. and Verdonck, T. (2012). A deterministic
|
|
123
|
+
algorithm for robust location and scatter. *Journal of Computational and
|
|
124
|
+
Graphical Statistics*, 21(3), 618-637.
|
|
125
|
+
|
|
126
|
+
Croux, C. and Haesbroeck, G. (1999). Influence function and efficiency of the
|
|
127
|
+
minimum covariance determinant scatter matrix estimator. *Journal of
|
|
128
|
+
Multivariate Analysis*, 71(2), 161-190.
|
|
129
|
+
|
|
130
|
+
Pison, G., Van Aelst, S. and Willems, G. (2002). Small sample corrections for
|
|
131
|
+
LTS and MCD. *Metrika*, 55, 111-123.
|
|
132
|
+
|
|
133
|
+
Examples
|
|
134
|
+
--------
|
|
135
|
+
>>> import numpy as np
|
|
136
|
+
>>> from robustmodels.covariance import MCD
|
|
137
|
+
>>> X = np.random.default_rng(0).normal(size=(100, 2))
|
|
138
|
+
>>> X[:10] += 10 # ten outliers
|
|
139
|
+
>>> mcd = MCD(random_state=0).fit(X)
|
|
140
|
+
>>> mcd.location_.round(2).tolist()
|
|
141
|
+
[0.01, 0.1]
|
|
142
|
+
>>> int(mcd.support_[:10].sum()) # no outlier is used
|
|
143
|
+
0
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
*,
|
|
149
|
+
alpha=0.5,
|
|
150
|
+
algorithm="fast",
|
|
151
|
+
n_trials=500,
|
|
152
|
+
reweighting=True,
|
|
153
|
+
small_sample_correction=True,
|
|
154
|
+
random_state=None,
|
|
155
|
+
):
|
|
156
|
+
self.alpha = alpha
|
|
157
|
+
self.algorithm = algorithm
|
|
158
|
+
self.n_trials = n_trials
|
|
159
|
+
self.reweighting = reweighting
|
|
160
|
+
self.small_sample_correction = small_sample_correction
|
|
161
|
+
self.random_state = random_state
|
|
162
|
+
|
|
163
|
+
def fit(self, X, y=None):
|
|
164
|
+
"""Fit to ``X`` of shape (n_samples, n_features); ``y`` is ignored. Returns ``self``."""
|
|
165
|
+
X = self._validate(X)
|
|
166
|
+
n, p = X.shape
|
|
167
|
+
check_option("algorithm", self.algorithm, ("fast", "deterministic"))
|
|
168
|
+
check_count("n_trials", self.n_trials)
|
|
169
|
+
h = h_alpha_n(self.alpha, n, p)
|
|
170
|
+
if n <= p:
|
|
171
|
+
raise ValueError(f"MCD needs n > p (got n={n}, p={p}); use MRCD")
|
|
172
|
+
subset = self._best_subset(X, h)
|
|
173
|
+
self._set_raw_estimates(X, subset, h)
|
|
174
|
+
location, covariance = self._reweighted_estimates(X)
|
|
175
|
+
self._set_estimates(location, covariance, X)
|
|
176
|
+
return self
|
|
177
|
+
|
|
178
|
+
def _set_raw_estimates(self, X, subset, h):
|
|
179
|
+
n, p = X.shape
|
|
180
|
+
location, covariance = _mean_cov(X[subset])
|
|
181
|
+
if h < n:
|
|
182
|
+
covariance *= mcd_consistency(p, h / n) * self._small_sample(p, n, False)
|
|
183
|
+
self.h_ = h
|
|
184
|
+
self.raw_location_ = location
|
|
185
|
+
self.raw_covariance_ = covariance
|
|
186
|
+
self.raw_support_ = np.isin(np.arange(n), subset)
|
|
187
|
+
|
|
188
|
+
def _reweighted_estimates(self, X):
|
|
189
|
+
location, covariance = self.raw_location_, self.raw_covariance_
|
|
190
|
+
self.support_ = self.raw_support_.copy()
|
|
191
|
+
if _logdet(covariance) == -np.inf:
|
|
192
|
+
warnings.warn(
|
|
193
|
+
"exact fit: h observations lie on a hyperplane", RuntimeWarning, stacklevel=3
|
|
194
|
+
)
|
|
195
|
+
return location, covariance
|
|
196
|
+
if self.reweighting:
|
|
197
|
+
location, covariance, self.support_ = _reweight(X, location, covariance)
|
|
198
|
+
if not self.support_.all():
|
|
199
|
+
n, p = X.shape
|
|
200
|
+
covariance = covariance * self._small_sample(p, n, True)
|
|
201
|
+
return location, covariance
|
|
202
|
+
|
|
203
|
+
def _best_subset(self, X, h):
|
|
204
|
+
n, p = X.shape
|
|
205
|
+
if h == n:
|
|
206
|
+
return np.arange(n)
|
|
207
|
+
center, scale = _center_scale(X)
|
|
208
|
+
standardized = (X - center) / scale
|
|
209
|
+
if p == 1:
|
|
210
|
+
return _univariate_mcd(standardized[:, 0], h)
|
|
211
|
+
if self.algorithm == "fast":
|
|
212
|
+
rng = _as_generator(self.random_state)
|
|
213
|
+
return _fast_mcd(standardized, h, self.n_trials, rng)
|
|
214
|
+
return _best_mcd(standardized, _deterministic_starts(standardized, h), h)
|
|
215
|
+
|
|
216
|
+
def _small_sample(self, p, n, reweighted):
|
|
217
|
+
if not self.small_sample_correction:
|
|
218
|
+
return 1.0
|
|
219
|
+
return mcd_small_sample(p, n, self.alpha, reweighted)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _mcd_steps(X, subset, h, max_steps=200):
|
|
223
|
+
"""Concentrate a subset while its covariance determinant decreases."""
|
|
224
|
+
location, covariance = _mean_cov(X[subset])
|
|
225
|
+
determinant = _logdet(covariance)
|
|
226
|
+
for _ in range(max_steps):
|
|
227
|
+
if determinant == -np.inf:
|
|
228
|
+
break
|
|
229
|
+
distances = _mahalanobis_sq(X, location, covariance)
|
|
230
|
+
updated = np.sort(np.argpartition(distances, h - 1)[:h])
|
|
231
|
+
new_location, new_covariance = _mean_cov(X[updated])
|
|
232
|
+
new_determinant = _logdet(new_covariance)
|
|
233
|
+
if len(subset) == h and new_determinant >= determinant:
|
|
234
|
+
break
|
|
235
|
+
subset = updated
|
|
236
|
+
location, covariance = new_location, new_covariance
|
|
237
|
+
determinant = new_determinant
|
|
238
|
+
return determinant, subset
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _best_mcd(X, starts, h, max_steps=200):
|
|
242
|
+
"""Return the best subset after concentrating each starting subset."""
|
|
243
|
+
candidates = [_mcd_steps(X, subset, h, max_steps) for subset in starts]
|
|
244
|
+
return min(candidates, key=lambda candidate: candidate[0])[1]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _fast_mcd(X, h, n_trials, rng):
|
|
248
|
+
"""Take two C-steps per random start, then converge the ten best subsets."""
|
|
249
|
+
starts = [_random_subset(X, rng) for _ in range(n_trials)]
|
|
250
|
+
if starts[0] is None:
|
|
251
|
+
return np.arange(h)
|
|
252
|
+
candidates = [_mcd_steps(X, subset, h, max_steps=2) for subset in starts]
|
|
253
|
+
candidates.sort(key=lambda candidate: candidate[0])
|
|
254
|
+
finalists = [subset for _, subset in candidates[:10]]
|
|
255
|
+
return _best_mcd(X, finalists, h)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _deterministic_starts(Z, h):
|
|
259
|
+
"""The six h-subsets of DetMCD, from robustly standardized data ``Z``."""
|
|
260
|
+
n, p = Z.shape
|
|
261
|
+
ranks = stats.rankdata(Z, axis=0)
|
|
262
|
+
norms = np.linalg.norm(Z, axis=1)
|
|
263
|
+
signs = Z / np.where(norms > 0, norms, 1)[:, None]
|
|
264
|
+
scatters = [
|
|
265
|
+
_corr(np.tanh(Z)),
|
|
266
|
+
_corr(ranks), # Spearman
|
|
267
|
+
_corr(stats.norm.ppf((ranks - 1 / 3) / (n + 1 / 3))), # normal scores
|
|
268
|
+
signs.T @ signs, # spatial sign covariance
|
|
269
|
+
_mean_cov(Z[np.argsort(norms)[: max(2, math.ceil(n / 2))]])[1], # first BACON step
|
|
270
|
+
_gk_covariance(Z, qn), # OGK-type
|
|
271
|
+
]
|
|
272
|
+
return [_start_from_scatter(Z, S, h) for S in scatters]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _corr(Y):
|
|
276
|
+
# A constant column contributes zero correlation to a starting scatter.
|
|
277
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
278
|
+
correlation = np.atleast_2d(np.corrcoef(Y, rowvar=False))
|
|
279
|
+
return np.nan_to_num(correlation)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _start_from_scatter(Z, S, h):
|
|
283
|
+
"""h points closest to a robust center, in the eigenbasis of ``S`` with Qn scales."""
|
|
284
|
+
eigenvectors = np.linalg.eigh(S)[1]
|
|
285
|
+
projected = Z @ eigenvectors
|
|
286
|
+
scales = np.array([qn(column) for column in projected.T])
|
|
287
|
+
scales = np.where(scales > 0, scales, 1e-300)
|
|
288
|
+
whitening = (eigenvectors / scales) @ eigenvectors.T
|
|
289
|
+
inverse_whitening = (eigenvectors * scales) @ eigenvectors.T
|
|
290
|
+
center = np.median(Z @ whitening, axis=0) @ inverse_whitening
|
|
291
|
+
residuals = ((Z - center) @ eigenvectors) / scales
|
|
292
|
+
distances = np.sum(residuals**2, axis=1)
|
|
293
|
+
return np.sort(np.argsort(distances, kind="stable")[:h])
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _univariate_mcd(z, h):
|
|
297
|
+
"""Exact MCD for p = 1: the window of h sorted values with the least variance."""
|
|
298
|
+
order = np.argsort(z, kind="stable")
|
|
299
|
+
sorted_values = z[order]
|
|
300
|
+
sums = np.concatenate([[0], np.cumsum(sorted_values)])
|
|
301
|
+
square_sums = np.concatenate([[0], np.cumsum(sorted_values**2)])
|
|
302
|
+
window_sums = sums[h:] - sums[:-h]
|
|
303
|
+
window_square_sums = square_sums[h:] - square_sums[:-h]
|
|
304
|
+
centered_sums = window_square_sums - window_sums**2 / h
|
|
305
|
+
start = int(np.argmin(centered_sums))
|
|
306
|
+
return np.sort(order[start : start + h])
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def h_alpha_n(alpha, n, p):
|
|
310
|
+
"""Subset size of the MCD for coverage ``alpha``, as R's ``robustbase::h.alpha.n``.
|
|
311
|
+
|
|
312
|
+
``(n + p + 1) // 2`` at ``alpha = 0.5``, ``n`` at ``alpha = 1`` and linear in
|
|
313
|
+
between.
|
|
314
|
+
|
|
315
|
+
Parameters
|
|
316
|
+
----------
|
|
317
|
+
alpha : float in [0.5, 1]
|
|
318
|
+
Coverage.
|
|
319
|
+
n, p : int
|
|
320
|
+
Number of observations and of variables.
|
|
321
|
+
|
|
322
|
+
Returns
|
|
323
|
+
-------
|
|
324
|
+
int
|
|
325
|
+
|
|
326
|
+
Examples
|
|
327
|
+
--------
|
|
328
|
+
>>> from robustmodels.covariance import h_alpha_n
|
|
329
|
+
>>> h_alpha_n(0.5, 100, 3), h_alpha_n(0.75, 100, 3)
|
|
330
|
+
(52, 76)
|
|
331
|
+
"""
|
|
332
|
+
check_range("alpha", alpha, 0.5, 1)
|
|
333
|
+
check_count("n", n)
|
|
334
|
+
check_count("p", p)
|
|
335
|
+
minimum = (n + p + 1) // 2
|
|
336
|
+
return int(2 * minimum - n + 2 * (n - minimum) * alpha)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def mcd_consistency(p, alpha):
|
|
340
|
+
"""Consistency factor of the MCD at the normal model (Croux & Haesbroeck 1999).
|
|
341
|
+
|
|
342
|
+
``alpha / F_{p+2}(chi2_p^-1(alpha))``, with ``F_k`` the chi-square distribution
|
|
343
|
+
function with ``k`` degrees of freedom. It makes the covariance of the
|
|
344
|
+
fraction ``alpha`` of observations closest to the center consistent.
|
|
345
|
+
|
|
346
|
+
Parameters
|
|
347
|
+
----------
|
|
348
|
+
p : int
|
|
349
|
+
Number of variables.
|
|
350
|
+
alpha : float in (0, 1]
|
|
351
|
+
Fraction of observations kept; 1 gives 1.
|
|
352
|
+
|
|
353
|
+
Returns
|
|
354
|
+
-------
|
|
355
|
+
float
|
|
356
|
+
|
|
357
|
+
Examples
|
|
358
|
+
--------
|
|
359
|
+
>>> from robustmodels.covariance import mcd_consistency
|
|
360
|
+
>>> round(mcd_consistency(3, 0.5), 4)
|
|
361
|
+
2.4574
|
|
362
|
+
"""
|
|
363
|
+
check_count("p", p)
|
|
364
|
+
check_range("alpha", alpha, 0, 1, low_open=True)
|
|
365
|
+
if alpha == 1:
|
|
366
|
+
return 1.0
|
|
367
|
+
cutoff = chi2.ppf(alpha, p)
|
|
368
|
+
return float(alpha / chi2.cdf(cutoff, p + 2))
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# Pison, Van Aelst & Willems (2002): curves f(n) = 1 - exp(a) / n**b.
|
|
372
|
+
# (reweighted, alpha) -> [(a, b) for p = 1, (a, b) for p = 2, two (a, b) points for p > 2]
|
|
373
|
+
_PISON = {
|
|
374
|
+
(False, 0.5): [
|
|
375
|
+
(0.262024211897096, 0.604756680630497),
|
|
376
|
+
(0.673292623522027, 0.691365864961895),
|
|
377
|
+
(-1.42764571687802, 1.26263336932151),
|
|
378
|
+
(-1.06141115981725, 1.28907991440387),
|
|
379
|
+
],
|
|
380
|
+
(False, 0.875): [
|
|
381
|
+
(-0.351584646688712, 1.01646567502486),
|
|
382
|
+
(0.446537815635445, 1.06690782995919),
|
|
383
|
+
(-0.455179464070565, 1.11192541278794),
|
|
384
|
+
(-0.294241208320834, 1.09649329149811),
|
|
385
|
+
],
|
|
386
|
+
(True, 0.5): [
|
|
387
|
+
(1.11098143415027, 1.5182890270453),
|
|
388
|
+
(3.11101712909049, 1.91401056721863),
|
|
389
|
+
(-1.02842572724793, 1.67659883081926),
|
|
390
|
+
(-0.26800273450853, 1.35968562893582),
|
|
391
|
+
],
|
|
392
|
+
(True, 0.875): [
|
|
393
|
+
(-0.66046776772861, 0.88939595831888),
|
|
394
|
+
(0.79473550581058, 1.10081930350091),
|
|
395
|
+
(-0.544482443573914, 1.25994483222292),
|
|
396
|
+
(-0.343791072183285, 1.25159004257133),
|
|
397
|
+
],
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _bias_curve(p, n, alpha, reweighted):
|
|
402
|
+
fit = _PISON[reweighted, alpha]
|
|
403
|
+
if p <= 2:
|
|
404
|
+
a, b = fit[p - 1]
|
|
405
|
+
else: # line through log(-a_j / p^b_j) at log(2 p^2) and log(3 p^2)
|
|
406
|
+
(a1, b1), (a2, b2) = fit[2], fit[3]
|
|
407
|
+
y1, y2 = np.log(-a1 / p**b1), np.log(-a2 / p**b2)
|
|
408
|
+
b = (y1 - y2) / np.log(1.5)
|
|
409
|
+
a = y1 + b * np.log(2 * p * p)
|
|
410
|
+
return 1 - np.exp(a) / n**b
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def mcd_small_sample(p, n, alpha, reweighted=False):
|
|
414
|
+
"""Small-sample factor of the MCD (Pison, Van Aelst & Willems 2002).
|
|
415
|
+
|
|
416
|
+
The paper fits the factors at ``alpha = 0.5`` and ``0.875``; other coverages
|
|
417
|
+
are interpolated linearly, reaching 1 at ``alpha = 1``.
|
|
418
|
+
|
|
419
|
+
Parameters
|
|
420
|
+
----------
|
|
421
|
+
p, n : int
|
|
422
|
+
Number of variables and of observations.
|
|
423
|
+
alpha : float in [0.5, 1]
|
|
424
|
+
Coverage.
|
|
425
|
+
reweighted : bool, default False
|
|
426
|
+
Factor for the reweighted instead of the raw MCD.
|
|
427
|
+
|
|
428
|
+
Returns
|
|
429
|
+
-------
|
|
430
|
+
float
|
|
431
|
+
|
|
432
|
+
Examples
|
|
433
|
+
--------
|
|
434
|
+
>>> from robustmodels.covariance import mcd_small_sample
|
|
435
|
+
>>> round(mcd_small_sample(3, 100, 0.5), 4)
|
|
436
|
+
1.0989
|
|
437
|
+
"""
|
|
438
|
+
check_count("p", p)
|
|
439
|
+
check_count("n", n)
|
|
440
|
+
check_range("alpha", alpha, 0.5, 1)
|
|
441
|
+
f50 = _bias_curve(p, n, 0.5, reweighted)
|
|
442
|
+
f875 = _bias_curve(p, n, 0.875, reweighted)
|
|
443
|
+
if alpha <= 0.875:
|
|
444
|
+
f = f50 + (f875 - f50) * (alpha - 0.5) / 0.375
|
|
445
|
+
else:
|
|
446
|
+
f = f875 + (1 - f875) * (alpha - 0.875) / 0.125
|
|
447
|
+
return float(1 / f) if f > 0 else 1.0
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class MRCD(_RobustCovariance):
|
|
451
|
+
"""Minimum Regularized Covariance Determinant estimator (Boudt et al. 2020).
|
|
452
|
+
|
|
453
|
+
The data are standardized with the column medians and Qn scales. The
|
|
454
|
+
``h``-subset ``H`` then minimizes the determinant of
|
|
455
|
+
``K(H) = rho * I + (1 - rho) * c_alpha * cov(H)``, where ``c_alpha`` is the
|
|
456
|
+
MCD consistency factor and ``rho`` the smallest weight that keeps the
|
|
457
|
+
condition number of ``K`` below ``max_condition``. In the original units
|
|
458
|
+
the regularization target is the diagonal matrix of squared Qn scales.
|
|
459
|
+
Works with more variables than observations; when the subset covariance is
|
|
460
|
+
already well-conditioned, ``rho = 0`` and the result is the raw MCD. There is
|
|
461
|
+
no reweighting step.
|
|
462
|
+
|
|
463
|
+
With more variables than observations the starting subsets come from
|
|
464
|
+
eigenvectors that are not unique, so results can differ slightly between
|
|
465
|
+
platforms and linear algebra libraries.
|
|
466
|
+
|
|
467
|
+
Parameters
|
|
468
|
+
----------
|
|
469
|
+
alpha : float in [0.5, 1], default 0.5
|
|
470
|
+
Coverage; the subset size is ``h = ceil(alpha * n)``.
|
|
471
|
+
max_condition : float, default 50
|
|
472
|
+
Largest condition number allowed for the regularized covariance.
|
|
473
|
+
rho : float in [0, 1), optional
|
|
474
|
+
Fixed regularization weight, which replaces ``max_condition``. Needed when a
|
|
475
|
+
starting subset has zero scatter.
|
|
476
|
+
|
|
477
|
+
Attributes
|
|
478
|
+
----------
|
|
479
|
+
location_ : ndarray of shape (n_features,)
|
|
480
|
+
Mean of the ``h``-subset.
|
|
481
|
+
covariance_ : ndarray of shape (n_features, n_features)
|
|
482
|
+
Regularized covariance matrix.
|
|
483
|
+
precision_ : ndarray of shape (n_features, n_features)
|
|
484
|
+
Inverse of ``covariance_``.
|
|
485
|
+
support_ : ndarray of bool, shape (n_samples,)
|
|
486
|
+
The ``h``-subset.
|
|
487
|
+
dist_ : ndarray of shape (n_samples,)
|
|
488
|
+
Squared robust distances of the training data.
|
|
489
|
+
rho_ : float
|
|
490
|
+
Regularization weight used.
|
|
491
|
+
h_ : int
|
|
492
|
+
Subset size.
|
|
493
|
+
|
|
494
|
+
References
|
|
495
|
+
----------
|
|
496
|
+
Boudt, K., Rousseeuw, P. J., Vanduffel, S. and Verdonck, T. (2020). The
|
|
497
|
+
minimum regularized covariance determinant estimator. *Statistics and
|
|
498
|
+
Computing*, 30(1), 113-128.
|
|
499
|
+
|
|
500
|
+
Examples
|
|
501
|
+
--------
|
|
502
|
+
>>> import numpy as np
|
|
503
|
+
>>> from robustmodels.covariance import MRCD
|
|
504
|
+
>>> X = np.random.default_rng(0).normal(size=(20, 50)) # p > n
|
|
505
|
+
>>> mrcd = MRCD().fit(X)
|
|
506
|
+
>>> mrcd.covariance_.shape
|
|
507
|
+
(50, 50)
|
|
508
|
+
>>> 0 < mrcd.rho_ < 1
|
|
509
|
+
True
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
def __init__(self, *, alpha=0.5, max_condition=50.0, rho=None):
|
|
513
|
+
self.alpha = alpha
|
|
514
|
+
self.max_condition = max_condition
|
|
515
|
+
self.rho = rho
|
|
516
|
+
|
|
517
|
+
def fit(self, X, y=None):
|
|
518
|
+
"""Fit to ``X`` of shape (n_samples, n_features); ``y`` is ignored. Returns ``self``."""
|
|
519
|
+
X = self._validate(X)
|
|
520
|
+
check_range("alpha", self.alpha, 0.5, 1)
|
|
521
|
+
check_range("max_condition", self.max_condition, 1, np.inf, low_open=True)
|
|
522
|
+
if self.rho is not None:
|
|
523
|
+
check_range("rho", self.rho, 0, 1, high_open=True)
|
|
524
|
+
n, p = X.shape
|
|
525
|
+
h = math.ceil(self.alpha * n)
|
|
526
|
+
if h < 2:
|
|
527
|
+
raise ValueError("MRCD needs at least two observations in its subset")
|
|
528
|
+
center, scale = _center_scale(X)
|
|
529
|
+
standardized = (X - center) / scale
|
|
530
|
+
correction = mcd_consistency(p, h / n)
|
|
531
|
+
starts, rho = self._starting_subsets(standardized, h, correction)
|
|
532
|
+
try:
|
|
533
|
+
location, covariance, subset = _best_regularized_subset(
|
|
534
|
+
standardized, starts, h, rho, correction
|
|
535
|
+
)
|
|
536
|
+
except np.linalg.LinAlgError:
|
|
537
|
+
raise ValueError("MRCD covariance is singular; pass a larger rho") from None
|
|
538
|
+
self.h_, self.rho_ = h, float(rho)
|
|
539
|
+
self.support_ = np.isin(np.arange(n), subset)
|
|
540
|
+
location = center + scale * location
|
|
541
|
+
covariance = np.outer(scale, scale) * covariance
|
|
542
|
+
self._set_estimates(location, covariance, X)
|
|
543
|
+
return self
|
|
544
|
+
|
|
545
|
+
def _starting_subsets(self, X, h, correction):
|
|
546
|
+
if h == len(X):
|
|
547
|
+
starts = [np.arange(h)]
|
|
548
|
+
else:
|
|
549
|
+
starts = _deterministic_starts(X, h)
|
|
550
|
+
if self.rho is not None:
|
|
551
|
+
return starts, self.rho
|
|
552
|
+
weights = []
|
|
553
|
+
for subset in starts:
|
|
554
|
+
covariance = correction * _mean_cov(X[subset])[1]
|
|
555
|
+
weights.append(_min_rho(covariance, self.max_condition))
|
|
556
|
+
weights = np.array(weights)
|
|
557
|
+
cutoff = max(0.1, np.median(weights))
|
|
558
|
+
selected = [
|
|
559
|
+
subset for subset, weight in zip(starts, weights, strict=True) if weight <= cutoff
|
|
560
|
+
]
|
|
561
|
+
return selected, weights[weights <= cutoff].max()
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _min_rho(covariance, max_condition):
|
|
565
|
+
"""Smallest rho for cond(rho I + (1 - rho) covariance) <= max_condition."""
|
|
566
|
+
eigenvalues = np.linalg.eigvalsh(covariance)
|
|
567
|
+
smallest = max(eigenvalues[0], 0.0)
|
|
568
|
+
largest = eigenvalues[-1]
|
|
569
|
+
if largest <= 0:
|
|
570
|
+
raise ValueError("MRCD subset has zero scatter; pass a positive rho")
|
|
571
|
+
if largest <= max_condition * smallest:
|
|
572
|
+
return 0.0
|
|
573
|
+
numerator = largest - max_condition * smallest
|
|
574
|
+
denominator = max_condition * (1 - smallest) - (1 - largest)
|
|
575
|
+
rho = numerator / denominator
|
|
576
|
+
if rho >= 1:
|
|
577
|
+
raise ValueError("MRCD regularization is too close to 1; rescale the data")
|
|
578
|
+
return rho
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _regularized_estimates(X, subset, rho, correction):
|
|
582
|
+
location, covariance = _mean_cov(X[subset])
|
|
583
|
+
target = np.eye(X.shape[1])
|
|
584
|
+
covariance = rho * target + (1 - rho) * correction * covariance
|
|
585
|
+
return location, covariance
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _best_regularized_subset(X, starts, h, rho, correction):
|
|
589
|
+
candidates = [_mrcd_steps(X, subset, h, rho, correction) for subset in starts]
|
|
590
|
+
_, subset = min(candidates, key=lambda candidate: candidate[0])
|
|
591
|
+
location, covariance = _regularized_estimates(X, subset, rho, correction)
|
|
592
|
+
return location, covariance, subset
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _mrcd_steps(X, subset, h, rho, correction, max_steps=200):
|
|
596
|
+
"""Concentration steps with the same regularization weight at every step."""
|
|
597
|
+
for _ in range(max_steps):
|
|
598
|
+
location, covariance = _regularized_estimates(X, subset, rho, correction)
|
|
599
|
+
distances = _mahalanobis_sq(X, location, covariance)
|
|
600
|
+
updated = np.sort(np.argpartition(distances, h - 1)[:h])
|
|
601
|
+
if np.array_equal(updated, subset):
|
|
602
|
+
break
|
|
603
|
+
subset = updated
|
|
604
|
+
_, covariance = _regularized_estimates(X, subset, rho, correction)
|
|
605
|
+
return np.linalg.slogdet(covariance)[1], subset
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
class MVE(_RobustCovariance):
|
|
609
|
+
"""Minimum Volume Ellipsoid estimator (Rousseeuw 1985).
|
|
610
|
+
|
|
611
|
+
Center and scaled shape of the smallest ellipsoid that covers ``h``
|
|
612
|
+
observations. The search draws random ``(p + 1)``-subsets, then refits the
|
|
613
|
+
10 best ellipsoids to the ``h`` points they cover while the volume
|
|
614
|
+
decreases. The shape is scaled for consistency at the normal model and
|
|
615
|
+
reweighted as in :class:`MCD`. Breakdown point about 50% for
|
|
616
|
+
``alpha = 0.5``. Needs ``n > p + 1``.
|
|
617
|
+
|
|
618
|
+
Parameters
|
|
619
|
+
----------
|
|
620
|
+
alpha : float in [0.5, 1), default 0.5
|
|
621
|
+
Coverage, as for :class:`MCD`. Full coverage has no finite scale correction.
|
|
622
|
+
n_trials : int, default 500
|
|
623
|
+
Random starting subsets.
|
|
624
|
+
reweighting : bool, default True
|
|
625
|
+
Recompute mean and covariance from the observations whose squared robust
|
|
626
|
+
distance is below the 0.975 chi-square quantile.
|
|
627
|
+
random_state : int, numpy.random.Generator, numpy.random.RandomState or None
|
|
628
|
+
Seed of the random subsets.
|
|
629
|
+
|
|
630
|
+
Attributes
|
|
631
|
+
----------
|
|
632
|
+
location_ : ndarray of shape (n_features,)
|
|
633
|
+
Final location.
|
|
634
|
+
covariance_ : ndarray of shape (n_features, n_features)
|
|
635
|
+
Final covariance matrix.
|
|
636
|
+
precision_ : ndarray of shape (n_features, n_features)
|
|
637
|
+
Pseudo-inverse of ``covariance_``.
|
|
638
|
+
support_ : ndarray of bool, shape (n_samples,)
|
|
639
|
+
Observations used for the final estimates.
|
|
640
|
+
dist_ : ndarray of shape (n_samples,)
|
|
641
|
+
Squared robust distances of the training data.
|
|
642
|
+
raw_location_ : ndarray of shape (n_features,)
|
|
643
|
+
Center of the best ellipsoid.
|
|
644
|
+
raw_covariance_ : ndarray of shape (n_features, n_features)
|
|
645
|
+
Scaled shape of the best ellipsoid.
|
|
646
|
+
raw_support_ : ndarray of bool, shape (n_samples,)
|
|
647
|
+
Observations inside the best ellipsoid.
|
|
648
|
+
h_ : int
|
|
649
|
+
Number of observations covered.
|
|
650
|
+
|
|
651
|
+
References
|
|
652
|
+
----------
|
|
653
|
+
Rousseeuw, P. J. (1985). Multivariate estimation with high breakdown point.
|
|
654
|
+
In W. Grossmann et al. (eds.), *Mathematical Statistics and Applications*,
|
|
655
|
+
Vol. B, 283-297. Reidel.
|
|
656
|
+
|
|
657
|
+
Examples
|
|
658
|
+
--------
|
|
659
|
+
>>> import numpy as np
|
|
660
|
+
>>> from robustmodels.covariance import MVE
|
|
661
|
+
>>> X = np.random.default_rng(0).normal(size=(100, 2))
|
|
662
|
+
>>> X[:10] += 10
|
|
663
|
+
>>> mve = MVE(random_state=0).fit(X)
|
|
664
|
+
>>> mve.location_.round(2).tolist()
|
|
665
|
+
[0.01, 0.1]
|
|
666
|
+
"""
|
|
667
|
+
|
|
668
|
+
def __init__(self, *, alpha=0.5, n_trials=500, reweighting=True, random_state=None):
|
|
669
|
+
self.alpha = alpha
|
|
670
|
+
self.n_trials = n_trials
|
|
671
|
+
self.reweighting = reweighting
|
|
672
|
+
self.random_state = random_state
|
|
673
|
+
|
|
674
|
+
def fit(self, X, y=None):
|
|
675
|
+
"""Fit to ``X`` of shape (n_samples, n_features); ``y`` is ignored. Returns ``self``."""
|
|
676
|
+
X = self._validate(X)
|
|
677
|
+
check_range("alpha", self.alpha, 0.5, 1, high_open=True)
|
|
678
|
+
n, p = X.shape
|
|
679
|
+
h = h_alpha_n(self.alpha, n, p)
|
|
680
|
+
if n <= p + 1 or h > n:
|
|
681
|
+
raise ValueError(f"MVE needs n > p + 1 (got n={n}, p={p})")
|
|
682
|
+
check_count("n_trials", self.n_trials)
|
|
683
|
+
_, location, shape, distances = self._best_ellipsoid(X, h)
|
|
684
|
+
radius_squared = np.partition(distances, h - 1)[h - 1]
|
|
685
|
+
self.h_ = h
|
|
686
|
+
self.raw_location_ = location
|
|
687
|
+
self.raw_covariance_ = shape * radius_squared / chi2.ppf(h / n, p)
|
|
688
|
+
self.raw_support_ = distances <= radius_squared
|
|
689
|
+
covariance = self.raw_covariance_
|
|
690
|
+
self.support_ = self.raw_support_
|
|
691
|
+
if self.reweighting:
|
|
692
|
+
location, covariance, self.support_ = _reweight(X, location, covariance)
|
|
693
|
+
self._set_estimates(location, covariance, X)
|
|
694
|
+
return self
|
|
695
|
+
|
|
696
|
+
def _best_ellipsoid(self, X, h):
|
|
697
|
+
rng = _as_generator(self.random_state)
|
|
698
|
+
candidates = []
|
|
699
|
+
for _ in range(self.n_trials):
|
|
700
|
+
subset = _random_subset(X, rng)
|
|
701
|
+
if subset is None:
|
|
702
|
+
raise ValueError("the data lie on a hyperplane")
|
|
703
|
+
location, covariance = _mean_cov(X[subset])
|
|
704
|
+
candidates.append(_ellipsoid(X, location, covariance, h))
|
|
705
|
+
candidates.sort(key=lambda candidate: candidate[0])
|
|
706
|
+
refined = [_refine_ellipsoid(X, candidate, h) for candidate in candidates[:10]]
|
|
707
|
+
return min(refined, key=lambda candidate: candidate[0])
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _ellipsoid(X, location, covariance, h):
|
|
711
|
+
"""Return log-volume, location, shape and distances for an h-point ellipsoid."""
|
|
712
|
+
distances = _mahalanobis_sq(X, location, covariance)
|
|
713
|
+
radius_squared = np.partition(distances, h - 1)[h - 1]
|
|
714
|
+
volume = _logdet(covariance) + len(covariance) * np.log(radius_squared)
|
|
715
|
+
return volume, location, covariance, distances
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _refine_ellipsoid(X, candidate, h):
|
|
719
|
+
"""Refit the covered h points while the ellipsoid volume decreases."""
|
|
720
|
+
for _ in range(100):
|
|
721
|
+
volume, _, _, distances = candidate
|
|
722
|
+
subset = np.argpartition(distances, h - 1)[:h]
|
|
723
|
+
location, covariance = _mean_cov(X[subset])
|
|
724
|
+
if _logdet(covariance) == -np.inf:
|
|
725
|
+
break
|
|
726
|
+
updated = _ellipsoid(X, location, covariance, h)
|
|
727
|
+
if updated[0] >= volume:
|
|
728
|
+
break
|
|
729
|
+
candidate = updated
|
|
730
|
+
return candidate
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
_SCALES = {"tau": tau_scale, "qn": qn, "mad": mad, "pn": pn}
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
class OGK(_RobustCovariance):
|
|
737
|
+
"""Orthogonalized Gnanadesikan-Kettenring estimator (Maronna & Zamar 2002).
|
|
738
|
+
|
|
739
|
+
Robust pairwise covariances ``(s(X + Y)^2 - s(X - Y)^2) / 4``, with ``s`` a
|
|
740
|
+
robust univariate scale, made positive definite by orthogonalization.
|
|
741
|
+
Deterministic and fast for many variables. It is equivariant under shifts
|
|
742
|
+
and rescaling of the variables, but not under rotations.
|
|
743
|
+
|
|
744
|
+
Parameters
|
|
745
|
+
----------
|
|
746
|
+
scale : {"tau", "qn", "mad", "pn"}, default "tau"
|
|
747
|
+
Univariate scale. "tau" is centered at the tau location, the others at
|
|
748
|
+
the median.
|
|
749
|
+
n_iter : int, default 2
|
|
750
|
+
Orthogonalization iterations.
|
|
751
|
+
reweighting : bool, default True
|
|
752
|
+
Recompute mean and covariance from the observations with
|
|
753
|
+
``d^2 <= chi2_p^-1(beta) * median(d^2) / chi2_p^-1(0.5)``. The reweighted
|
|
754
|
+
covariance is corrected for consistency at the normal model, which R's
|
|
755
|
+
``covOGK`` omits.
|
|
756
|
+
beta : float in (0, 1), default 0.9
|
|
757
|
+
Quantile of the reweighting cutoff.
|
|
758
|
+
|
|
759
|
+
Attributes
|
|
760
|
+
----------
|
|
761
|
+
location_ : ndarray of shape (n_features,)
|
|
762
|
+
Final location.
|
|
763
|
+
covariance_ : ndarray of shape (n_features, n_features)
|
|
764
|
+
Final covariance matrix.
|
|
765
|
+
precision_ : ndarray of shape (n_features, n_features)
|
|
766
|
+
Pseudo-inverse of ``covariance_``.
|
|
767
|
+
support_ : ndarray of bool, shape (n_samples,)
|
|
768
|
+
Observations used for the final estimates (all if ``reweighting=False``).
|
|
769
|
+
dist_ : ndarray of shape (n_samples,)
|
|
770
|
+
Squared robust distances of the training data.
|
|
771
|
+
raw_location_ : ndarray of shape (n_features,)
|
|
772
|
+
Location before reweighting.
|
|
773
|
+
raw_covariance_ : ndarray of shape (n_features, n_features)
|
|
774
|
+
Covariance before reweighting.
|
|
775
|
+
|
|
776
|
+
References
|
|
777
|
+
----------
|
|
778
|
+
Maronna, R. A. and Zamar, R. H. (2002). Robust estimates of location and
|
|
779
|
+
dispersion for high-dimensional datasets. *Technometrics*, 44(4), 307-317.
|
|
780
|
+
|
|
781
|
+
Gnanadesikan, R. and Kettenring, J. R. (1972). Robust estimates, residuals,
|
|
782
|
+
and outlier detection with multiresponse data. *Biometrics*, 28(1), 81-124.
|
|
783
|
+
|
|
784
|
+
Examples
|
|
785
|
+
--------
|
|
786
|
+
>>> import numpy as np
|
|
787
|
+
>>> from robustmodels.covariance import OGK
|
|
788
|
+
>>> X = np.random.default_rng(0).normal(size=(100, 2))
|
|
789
|
+
>>> X[:10] += 10
|
|
790
|
+
>>> OGK().fit(X).covariance_.round(1).tolist()
|
|
791
|
+
[[1.1, 0.2], [0.2, 1.1]]
|
|
792
|
+
"""
|
|
793
|
+
|
|
794
|
+
def __init__(self, *, scale="tau", n_iter=2, reweighting=True, beta=0.9):
|
|
795
|
+
self.scale = scale
|
|
796
|
+
self.n_iter = n_iter
|
|
797
|
+
self.reweighting = reweighting
|
|
798
|
+
self.beta = beta
|
|
799
|
+
|
|
800
|
+
def fit(self, X, y=None):
|
|
801
|
+
"""Fit to ``X`` of shape (n_samples, n_features); ``y`` is ignored. Returns ``self``."""
|
|
802
|
+
X = self._validate(X)
|
|
803
|
+
check_option("scale", self.scale, _SCALES)
|
|
804
|
+
check_count("n_iter", self.n_iter)
|
|
805
|
+
check_range("beta", self.beta, 0, 1, low_open=True, high_open=True)
|
|
806
|
+
scale_fn = _SCALES[self.scale]
|
|
807
|
+
transformed, basis = _orthogonalize(X, scale_fn, self.n_iter)
|
|
808
|
+
if self.scale == "tau":
|
|
809
|
+
center = np.array([_tau_estimates(column, 4.5)[0] for column in transformed.T])
|
|
810
|
+
else:
|
|
811
|
+
center = np.median(transformed, axis=0)
|
|
812
|
+
scales = _scales(transformed, scale_fn)
|
|
813
|
+
distances = np.sum(((transformed - center) / scales) ** 2, axis=1)
|
|
814
|
+
self.raw_location_ = basis @ center
|
|
815
|
+
self.raw_covariance_ = (basis * scales**2) @ basis.T
|
|
816
|
+
location, covariance = self._reweighted_estimates(X, distances)
|
|
817
|
+
self._set_estimates(location, covariance, X)
|
|
818
|
+
return self
|
|
819
|
+
|
|
820
|
+
def _reweighted_estimates(self, X, distances):
|
|
821
|
+
self.support_ = np.ones(len(X), dtype=bool)
|
|
822
|
+
if not self.reweighting:
|
|
823
|
+
return self.raw_location_, self.raw_covariance_
|
|
824
|
+
p = X.shape[1]
|
|
825
|
+
cutoff = np.median(distances) * chi2.ppf(self.beta, p) / chi2.ppf(0.5, p)
|
|
826
|
+
self.support_ = distances <= cutoff
|
|
827
|
+
if not self.support_.any():
|
|
828
|
+
raise ValueError("beta is too small: reweighting selects no observations")
|
|
829
|
+
selected = X[self.support_]
|
|
830
|
+
location = selected.mean(axis=0)
|
|
831
|
+
centered = selected - location
|
|
832
|
+
covariance = centered.T @ centered / len(selected)
|
|
833
|
+
covariance *= mcd_consistency(p, self.beta)
|
|
834
|
+
return location, covariance
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def _orthogonalize(X, scale_fn, n_iter):
|
|
838
|
+
"""Rotate standardized columns and record how to return to the original basis."""
|
|
839
|
+
transformed = X
|
|
840
|
+
basis = np.eye(X.shape[1])
|
|
841
|
+
for _ in range(n_iter):
|
|
842
|
+
scales = _scales(transformed, scale_fn)
|
|
843
|
+
standardized = transformed / scales
|
|
844
|
+
eigenvectors = np.linalg.eigh(_gk_covariance(standardized, scale_fn))[1]
|
|
845
|
+
transformed = standardized @ eigenvectors
|
|
846
|
+
basis = basis @ (scales[:, None] * eigenvectors)
|
|
847
|
+
return transformed, basis
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def _scales(X, scale_fn):
|
|
851
|
+
scales = np.array([scale_fn(column) for column in X.T])
|
|
852
|
+
if np.any(scales <= 0):
|
|
853
|
+
raise ValueError("a variable has zero robust scale (more than half of its values tied)")
|
|
854
|
+
return scales
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
class SEstimator(_RobustCovariance):
|
|
858
|
+
"""S-estimator of location and scatter (Davies 1987), with Tukey's bisquare.
|
|
859
|
+
|
|
860
|
+
Finds the location ``mu`` and the shape ``Gamma`` (determinant 1) that
|
|
861
|
+
minimize the M-scale ``s`` of the distances
|
|
862
|
+
``d_i = sqrt((x_i - mu)' Gamma^-1 (x_i - mu))``, where ``s`` solves
|
|
863
|
+
``mean(rho(d_i / s)) = breakdown``. The covariance is ``s^2 * Gamma``,
|
|
864
|
+
consistent at the normal model. Computed with the FAST-S algorithm
|
|
865
|
+
(Salibián-Barrera, Van Aelst & Willems 2006). Affine equivariant; needs
|
|
866
|
+
``n > p``.
|
|
867
|
+
|
|
868
|
+
Parameters
|
|
869
|
+
----------
|
|
870
|
+
breakdown : float in (0, 0.5], default 0.5
|
|
871
|
+
Breakdown point.
|
|
872
|
+
n_trials : int, default 500
|
|
873
|
+
Random ``(p + 1)``-subsets, each refined with ``k_steps`` I-steps.
|
|
874
|
+
n_best : int, default 5
|
|
875
|
+
Best candidates iterated to convergence.
|
|
876
|
+
k_steps : int, default 2
|
|
877
|
+
I-steps applied to each random subset.
|
|
878
|
+
max_iter : int, default 200
|
|
879
|
+
Maximum I-steps for each of the best candidates.
|
|
880
|
+
random_state : int, numpy.random.Generator, numpy.random.RandomState or None
|
|
881
|
+
Seed of the random subsets.
|
|
882
|
+
|
|
883
|
+
Attributes
|
|
884
|
+
----------
|
|
885
|
+
location_ : ndarray of shape (n_features,)
|
|
886
|
+
Location ``mu``.
|
|
887
|
+
covariance_ : ndarray of shape (n_features, n_features)
|
|
888
|
+
Covariance matrix ``s^2 * Gamma``.
|
|
889
|
+
precision_ : ndarray of shape (n_features, n_features)
|
|
890
|
+
Pseudo-inverse of ``covariance_``.
|
|
891
|
+
dist_ : ndarray of shape (n_samples,)
|
|
892
|
+
Squared robust distances of the training data.
|
|
893
|
+
scale_ : float
|
|
894
|
+
The minimized M-scale ``s``.
|
|
895
|
+
|
|
896
|
+
References
|
|
897
|
+
----------
|
|
898
|
+
Davies, P. L. (1987). Asymptotic behaviour of S-estimates of multivariate
|
|
899
|
+
location parameters and dispersion matrices. *Annals of Statistics*, 15(3),
|
|
900
|
+
1269-1292.
|
|
901
|
+
|
|
902
|
+
Salibián-Barrera, M., Van Aelst, S. and Willems, G. (2006). Principal
|
|
903
|
+
components analysis based on multivariate MM estimators with fast and robust
|
|
904
|
+
bootstrap. *Journal of the American Statistical Association*, 101(475),
|
|
905
|
+
1198-1211.
|
|
906
|
+
|
|
907
|
+
Examples
|
|
908
|
+
--------
|
|
909
|
+
>>> import numpy as np
|
|
910
|
+
>>> from robustmodels.covariance import SEstimator
|
|
911
|
+
>>> X = np.random.default_rng(0).normal(size=(100, 2))
|
|
912
|
+
>>> X[:10] += 10
|
|
913
|
+
>>> s = SEstimator(random_state=0).fit(X)
|
|
914
|
+
>>> s.covariance_.round(1).tolist()
|
|
915
|
+
[[1.1, 0.1], [0.1, 1.3]]
|
|
916
|
+
"""
|
|
917
|
+
|
|
918
|
+
def __init__(
|
|
919
|
+
self, *, breakdown=0.5, n_trials=500, n_best=5, k_steps=2, max_iter=200, random_state=None
|
|
920
|
+
):
|
|
921
|
+
self.breakdown = breakdown
|
|
922
|
+
self.n_trials = n_trials
|
|
923
|
+
self.n_best = n_best
|
|
924
|
+
self.k_steps = k_steps
|
|
925
|
+
self.max_iter = max_iter
|
|
926
|
+
self.random_state = random_state
|
|
927
|
+
|
|
928
|
+
def fit(self, X, y=None):
|
|
929
|
+
"""Fit to ``X`` of shape (n_samples, n_features); ``y`` is ignored. Returns ``self``."""
|
|
930
|
+
X = self._validate(X)
|
|
931
|
+
location, shape, self.scale_ = self._fit_s(X)
|
|
932
|
+
self._set_estimates(location, self.scale_**2 * shape, X)
|
|
933
|
+
return self
|
|
934
|
+
|
|
935
|
+
def _fit_s(self, X):
|
|
936
|
+
"""Returns (location, shape with det 1, scale)."""
|
|
937
|
+
n, p = X.shape
|
|
938
|
+
for name in ("n_trials", "n_best", "k_steps", "max_iter"):
|
|
939
|
+
check_count(name, getattr(self, name))
|
|
940
|
+
if n <= p:
|
|
941
|
+
raise ValueError(f"S-estimation needs n > p (got n={n}, p={p})")
|
|
942
|
+
c = _tuning_for_breakdown(self.breakdown, p)
|
|
943
|
+
rng = _as_generator(self.random_state)
|
|
944
|
+
candidates = self._initial_candidates(X, c, rng)
|
|
945
|
+
candidates.sort(key=lambda candidate: candidate[2])
|
|
946
|
+
refined = []
|
|
947
|
+
for location, shape, _ in candidates[: self.n_best]:
|
|
948
|
+
refined.append(_s_steps(X, location, shape, c, self.breakdown, self.max_iter))
|
|
949
|
+
return min(refined, key=lambda candidate: candidate[2])
|
|
950
|
+
|
|
951
|
+
def _initial_candidates(self, X, c, rng):
|
|
952
|
+
candidates = []
|
|
953
|
+
for _ in range(self.n_trials):
|
|
954
|
+
subset = _random_subset(X, rng)
|
|
955
|
+
if subset is None:
|
|
956
|
+
raise ValueError("the data lie on a hyperplane")
|
|
957
|
+
location, covariance = _mean_cov(X[subset])
|
|
958
|
+
shape = _unit_det(covariance)
|
|
959
|
+
candidates.append(_s_steps(X, location, shape, c, self.breakdown, self.k_steps))
|
|
960
|
+
return candidates
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def _unit_det(covariance):
|
|
964
|
+
"""Rescale a positive definite covariance matrix to determinant 1."""
|
|
965
|
+
geometric_mean = np.exp(_logdet(covariance) / len(covariance))
|
|
966
|
+
return covariance / geometric_mean
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _s_steps(X, location, shape, c, breakdown, n_steps, tol=1e-10):
|
|
970
|
+
"""Reweight location and shape while the M-scale decreases."""
|
|
971
|
+
distances = np.sqrt(_mahalanobis_sq(X, location, shape))
|
|
972
|
+
scale = _mscale_bisquare(distances, c, breakdown)
|
|
973
|
+
for _ in range(n_steps):
|
|
974
|
+
if scale == 0:
|
|
975
|
+
break
|
|
976
|
+
weights = _bisquare_weights(distances / scale, c)
|
|
977
|
+
new_location, new_shape = _weighted_shape(X, weights)
|
|
978
|
+
new_distances = np.sqrt(_mahalanobis_sq(X, new_location, new_shape))
|
|
979
|
+
new_scale = _mscale_bisquare(new_distances, c, breakdown, s0=scale)
|
|
980
|
+
if new_scale > scale:
|
|
981
|
+
break
|
|
982
|
+
converged = scale - new_scale <= tol * scale
|
|
983
|
+
location, shape = new_location, new_shape
|
|
984
|
+
distances, scale = new_distances, new_scale
|
|
985
|
+
if converged:
|
|
986
|
+
break
|
|
987
|
+
return location, shape, scale
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def _weighted_shape(X, weights):
|
|
991
|
+
"""Weighted mean and weighted scatter rescaled to determinant 1."""
|
|
992
|
+
total = weights.sum()
|
|
993
|
+
if total == 0:
|
|
994
|
+
raise ValueError("all observations have zero bisquare weight")
|
|
995
|
+
location = weights @ X / total
|
|
996
|
+
centered = X - location
|
|
997
|
+
scatter = (weights[:, None] * centered).T @ centered
|
|
998
|
+
if _logdet(scatter) == -np.inf:
|
|
999
|
+
raise ValueError("bisquare weights produce a singular scatter matrix")
|
|
1000
|
+
return location, _unit_det(scatter)
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
class MMEstimator(_RobustCovariance):
|
|
1004
|
+
"""MM-estimator of location and scatter (Tatsuoka & Tyler 2000).
|
|
1005
|
+
|
|
1006
|
+
Starts from a bisquare :class:`SEstimator` and keeps its scale ``s``. A
|
|
1007
|
+
bisquare M-estimator of location and shape, tuned for the requested Gaussian
|
|
1008
|
+
efficiency of the shape, then refines the estimates. It keeps the breakdown
|
|
1009
|
+
point of the S-estimator and gains efficiency. Affine equivariant; needs
|
|
1010
|
+
``n > p``.
|
|
1011
|
+
|
|
1012
|
+
Parameters
|
|
1013
|
+
----------
|
|
1014
|
+
breakdown : float in (0, 0.5], default 0.5
|
|
1015
|
+
Breakdown point of the initial S-estimator.
|
|
1016
|
+
efficiency : float in (0, 1), default 0.95
|
|
1017
|
+
Gaussian efficiency of the shape.
|
|
1018
|
+
n_trials : int, default 500
|
|
1019
|
+
Random subsets of the initial S-estimator.
|
|
1020
|
+
max_iter : int, default 200
|
|
1021
|
+
Maximum iterations of the M-step.
|
|
1022
|
+
random_state : int, numpy.random.Generator, numpy.random.RandomState or None
|
|
1023
|
+
Seed of the initial S-estimator.
|
|
1024
|
+
|
|
1025
|
+
Attributes
|
|
1026
|
+
----------
|
|
1027
|
+
location_ : ndarray of shape (n_features,)
|
|
1028
|
+
MM location.
|
|
1029
|
+
covariance_ : ndarray of shape (n_features, n_features)
|
|
1030
|
+
MM covariance matrix.
|
|
1031
|
+
precision_ : ndarray of shape (n_features, n_features)
|
|
1032
|
+
Pseudo-inverse of ``covariance_``.
|
|
1033
|
+
dist_ : ndarray of shape (n_samples,)
|
|
1034
|
+
Squared robust distances of the training data.
|
|
1035
|
+
scale_ : float
|
|
1036
|
+
Scale of the S-estimator.
|
|
1037
|
+
s_location_ : ndarray of shape (n_features,)
|
|
1038
|
+
Location of the S-estimator.
|
|
1039
|
+
s_covariance_ : ndarray of shape (n_features, n_features)
|
|
1040
|
+
Covariance of the S-estimator.
|
|
1041
|
+
|
|
1042
|
+
References
|
|
1043
|
+
----------
|
|
1044
|
+
Tatsuoka, K. S. and Tyler, D. E. (2000). On the uniqueness of S-functionals
|
|
1045
|
+
and M-functionals under nonelliptical distributions. *Annals of Statistics*,
|
|
1046
|
+
28(4), 1219-1243.
|
|
1047
|
+
|
|
1048
|
+
Salibián-Barrera, M., Van Aelst, S. and Willems, G. (2006). Principal
|
|
1049
|
+
components analysis based on multivariate MM estimators with fast and robust
|
|
1050
|
+
bootstrap. *Journal of the American Statistical Association*, 101(475),
|
|
1051
|
+
1198-1211.
|
|
1052
|
+
|
|
1053
|
+
Examples
|
|
1054
|
+
--------
|
|
1055
|
+
>>> import numpy as np
|
|
1056
|
+
>>> from robustmodels.covariance import MMEstimator
|
|
1057
|
+
>>> X = np.random.default_rng(0).normal(size=(100, 2))
|
|
1058
|
+
>>> X[:10] += 10
|
|
1059
|
+
>>> mm = MMEstimator(random_state=0).fit(X)
|
|
1060
|
+
>>> mm.covariance_.round(1).tolist()
|
|
1061
|
+
[[1.2, 0.1], [0.1, 1.2]]
|
|
1062
|
+
"""
|
|
1063
|
+
|
|
1064
|
+
def __init__(
|
|
1065
|
+
self, *, breakdown=0.5, efficiency=0.95, n_trials=500, max_iter=200, random_state=None
|
|
1066
|
+
):
|
|
1067
|
+
self.breakdown = breakdown
|
|
1068
|
+
self.efficiency = efficiency
|
|
1069
|
+
self.n_trials = n_trials
|
|
1070
|
+
self.max_iter = max_iter
|
|
1071
|
+
self.random_state = random_state
|
|
1072
|
+
|
|
1073
|
+
def fit(self, X, y=None):
|
|
1074
|
+
"""Fit to ``X`` of shape (n_samples, n_features); ``y`` is ignored. Returns ``self``."""
|
|
1075
|
+
X = self._validate(X)
|
|
1076
|
+
check_count("max_iter", self.max_iter)
|
|
1077
|
+
c = _tuning_for_shape_efficiency(self.efficiency, X.shape[1])
|
|
1078
|
+
initial = SEstimator(
|
|
1079
|
+
breakdown=self.breakdown, n_trials=self.n_trials, random_state=self.random_state
|
|
1080
|
+
)
|
|
1081
|
+
location, shape, scale = initial._fit_s(X)
|
|
1082
|
+
self.scale_ = scale
|
|
1083
|
+
self.s_location_ = location
|
|
1084
|
+
self.s_covariance_ = scale**2 * shape
|
|
1085
|
+
location, shape = _refine_shape(X, location, shape, scale, c, self.max_iter)
|
|
1086
|
+
self._set_estimates(location, scale**2 * shape, X)
|
|
1087
|
+
return self
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def _refine_shape(X, location, shape, scale, c, max_iter):
|
|
1091
|
+
"""Decrease bisquare loss while keeping the initial S-scale fixed."""
|
|
1092
|
+
if scale == 0:
|
|
1093
|
+
return location, shape
|
|
1094
|
+
distances = np.sqrt(_mahalanobis_sq(X, location, shape))
|
|
1095
|
+
loss = np.mean(_rho(distances / scale, c))
|
|
1096
|
+
for _ in range(max_iter):
|
|
1097
|
+
weights = _bisquare_weights(distances / scale, c)
|
|
1098
|
+
new_location, new_shape = _weighted_shape(X, weights)
|
|
1099
|
+
new_distances = np.sqrt(_mahalanobis_sq(X, new_location, new_shape))
|
|
1100
|
+
new_loss = np.mean(_rho(new_distances / scale, c))
|
|
1101
|
+
if new_loss > loss:
|
|
1102
|
+
break
|
|
1103
|
+
converged = loss - new_loss <= 1e-12
|
|
1104
|
+
location, shape, distances, loss = new_location, new_shape, new_distances, new_loss
|
|
1105
|
+
if converged:
|
|
1106
|
+
break
|
|
1107
|
+
return location, shape
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def _mahalanobis_sq(X, location, cov):
|
|
1111
|
+
"""Squared Mahalanobis distances (``cov`` must be positive definite)."""
|
|
1112
|
+
centered = X - location
|
|
1113
|
+
cholesky = np.linalg.cholesky(cov)
|
|
1114
|
+
standardized = np.linalg.solve(cholesky, centered.T)
|
|
1115
|
+
return np.sum(standardized**2, axis=0)
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def _logdet(cov):
|
|
1119
|
+
"""``log det(cov)``, or ``-inf`` if ``cov`` is numerically singular."""
|
|
1120
|
+
eigenvalues = np.linalg.eigvalsh(cov)
|
|
1121
|
+
if eigenvalues[0] <= 1e-12 * eigenvalues[-1]:
|
|
1122
|
+
return -np.inf
|
|
1123
|
+
return float(np.log(eigenvalues).sum())
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def _mean_cov(X):
|
|
1127
|
+
return X.mean(axis=0), np.atleast_2d(np.cov(X, rowvar=False))
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
def _center_scale(X):
|
|
1131
|
+
"""Column medians and Qn scales (MAD, then 1, when a scale is zero)."""
|
|
1132
|
+
scale = np.array([_column_scale(column) for column in X.T])
|
|
1133
|
+
return np.median(X, axis=0), scale
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def _column_scale(column):
|
|
1137
|
+
scale = qn(column)
|
|
1138
|
+
if scale == 0:
|
|
1139
|
+
scale = mad(column)
|
|
1140
|
+
if scale == 0:
|
|
1141
|
+
scale = 1.0
|
|
1142
|
+
return scale
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _random_subset(X, rng):
|
|
1146
|
+
"""Random (p+1)-subset, enlarged until its covariance is non-singular (None if impossible)."""
|
|
1147
|
+
n, p = X.shape
|
|
1148
|
+
order = rng.permutation(n)
|
|
1149
|
+
for k in range(p + 1, n + 1):
|
|
1150
|
+
subset = order[:k]
|
|
1151
|
+
_, covariance = _mean_cov(X[subset])
|
|
1152
|
+
if _logdet(covariance) > -np.inf:
|
|
1153
|
+
return subset
|
|
1154
|
+
return None
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def _reweight(X, location, cov, quantile=0.975):
|
|
1158
|
+
"""Mean and covariance of the points with ``d^2 < chi2_p(quantile)``.
|
|
1159
|
+
|
|
1160
|
+
The covariance is made consistent for that cutoff (not for the fraction
|
|
1161
|
+
actually kept, which outliers would distort). If every point is kept, the
|
|
1162
|
+
sample covariance is returned unchanged, as in robustbase.
|
|
1163
|
+
"""
|
|
1164
|
+
p = X.shape[1]
|
|
1165
|
+
keep = _mahalanobis_sq(X, location, cov) < chi2.ppf(quantile, p)
|
|
1166
|
+
if keep.sum() <= p:
|
|
1167
|
+
return location, cov, keep
|
|
1168
|
+
loc, cov = _mean_cov(X[keep])
|
|
1169
|
+
if not keep.all():
|
|
1170
|
+
cov = cov * mcd_consistency(p, quantile)
|
|
1171
|
+
return loc, cov, keep
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def _gk_covariance(X, scale_fn):
|
|
1175
|
+
"""Pairwise Gnanadesikan-Kettenring scatter with a unit diagonal."""
|
|
1176
|
+
matrix = np.eye(X.shape[1])
|
|
1177
|
+
for i in range(X.shape[1]):
|
|
1178
|
+
for j in range(i):
|
|
1179
|
+
a, b = X[:, i], X[:, j]
|
|
1180
|
+
sum_variance = scale_fn(a + b) ** 2
|
|
1181
|
+
difference_variance = scale_fn(a - b) ** 2
|
|
1182
|
+
covariance = (sum_variance - difference_variance) / 4
|
|
1183
|
+
matrix[i, j] = matrix[j, i] = covariance
|
|
1184
|
+
return matrix
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
def _as_generator(random_state):
|
|
1188
|
+
"""NumPy ``Generator`` from None, an int, a Generator or a legacy RandomState."""
|
|
1189
|
+
if isinstance(random_state, np.random.RandomState):
|
|
1190
|
+
random_state = random_state.randint(2**31)
|
|
1191
|
+
return np.random.default_rng(random_state)
|