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
robustmodels/scale.py
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
"""Robust scale, skewness and their numerical calculations.
|
|
2
|
+
|
|
3
|
+
Every public function takes a one-dimensional sample of finite values and
|
|
4
|
+
returns a float. An empty sample gives ``nan`` and a single observation has
|
|
5
|
+
zero scale.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from scipy import optimize, special
|
|
10
|
+
from scipy.stats import chi2, norm
|
|
11
|
+
|
|
12
|
+
from ._validation import as_1d, check_range
|
|
13
|
+
|
|
14
|
+
__all__ = ["lmc", "m_scale", "mad", "medcouple", "pn", "qn", "rmc", "sn", "tau_scale"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
MAD_CONSTANT = 1 / norm.ppf(0.75)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# MAD factors for n = 2, ..., 9; Croux & Rousseeuw (1992).
|
|
21
|
+
_MAD_N = (1.196, 1.495, 1.363, 1.206, 1.200, 1.140, 1.129, 1.107)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def mad(x, *, center=None, consistency=True, finite_correction=False):
|
|
25
|
+
"""Median absolute deviation, ``1.4826 * med|x_i - center|``.
|
|
26
|
+
|
|
27
|
+
Breakdown point 50%, Gaussian efficiency 37%.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
x : array_like
|
|
32
|
+
One-dimensional sample of finite values.
|
|
33
|
+
center : float, optional
|
|
34
|
+
Center of the deviations. Defaults to the sample median.
|
|
35
|
+
consistency : bool, default True
|
|
36
|
+
Multiply by ``1 / Phi^-1(3/4) = 1.4826``, so that the MAD estimates the
|
|
37
|
+
standard deviation at the normal model.
|
|
38
|
+
finite_correction : bool, default False
|
|
39
|
+
Multiply by the small-sample factors of Croux & Rousseeuw (1992). Without
|
|
40
|
+
them the MAD underestimates the normal standard deviation by 17% at n = 5.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
float
|
|
45
|
+
``nan`` for an empty sample, 0 for a single observation.
|
|
46
|
+
|
|
47
|
+
References
|
|
48
|
+
----------
|
|
49
|
+
Hampel, F. R. (1974). The influence curve and its role in robust estimation.
|
|
50
|
+
*Journal of the American Statistical Association*, 69(346), 383-393.
|
|
51
|
+
|
|
52
|
+
Croux, C. and Rousseeuw, P. J. (1992). Time-efficient algorithms for two
|
|
53
|
+
highly robust estimators of scale. *Computational Statistics*, 1, 411-428.
|
|
54
|
+
|
|
55
|
+
Examples
|
|
56
|
+
--------
|
|
57
|
+
>>> from robustmodels.scale import mad
|
|
58
|
+
>>> round(mad([1, 2, 2, 3, 4, 100]), 4)
|
|
59
|
+
1.4826
|
|
60
|
+
>>> mad([1, 2, 2, 3, 4, 100], consistency=False)
|
|
61
|
+
1.0
|
|
62
|
+
"""
|
|
63
|
+
x = as_1d(x)
|
|
64
|
+
if center is not None:
|
|
65
|
+
check_range("center", center, -np.inf, np.inf)
|
|
66
|
+
if x.size < 2:
|
|
67
|
+
return _trivial_scale(x)
|
|
68
|
+
if center is None:
|
|
69
|
+
center = np.median(x)
|
|
70
|
+
value = np.median(np.abs(x - center))
|
|
71
|
+
if consistency:
|
|
72
|
+
value *= MAD_CONSTANT
|
|
73
|
+
if finite_correction:
|
|
74
|
+
value *= _mad_factor(x.size)
|
|
75
|
+
return float(value)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _mad_factor(n):
|
|
79
|
+
return _MAD_N[n - 2] if n <= 9 else n / (n - 0.8)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
QN_CONSTANT = 1 / (np.sqrt(2) * norm.ppf(5 / 8))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_QN_N = (
|
|
86
|
+
0.399356,
|
|
87
|
+
0.99365,
|
|
88
|
+
0.51321,
|
|
89
|
+
0.84401,
|
|
90
|
+
0.6122,
|
|
91
|
+
0.85877,
|
|
92
|
+
0.66993,
|
|
93
|
+
0.87344,
|
|
94
|
+
0.72014,
|
|
95
|
+
0.88906,
|
|
96
|
+
0.75743,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def qn(x, *, consistency=True, finite_correction=True):
|
|
101
|
+
"""Qn scale estimator of Rousseeuw & Croux (1993).
|
|
102
|
+
|
|
103
|
+
The ``k``-th smallest of the distances ``|x_i - x_j|``, ``i < j``, with
|
|
104
|
+
``k = h(h - 1) / 2`` and ``h = n // 2 + 1``. Breakdown point 50%, Gaussian
|
|
105
|
+
efficiency 82%. Computed exactly in O(n log n) time, without forming all pairs.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
x : array_like
|
|
110
|
+
One-dimensional sample of finite values.
|
|
111
|
+
consistency : bool, default True
|
|
112
|
+
Multiply by ``1 / (sqrt(2) * Phi^-1(5/8)) = 2.2191``, so that Qn estimates
|
|
113
|
+
the standard deviation at the normal model.
|
|
114
|
+
finite_correction : bool, default True
|
|
115
|
+
Multiply by the small-sample factors used by R's robustbase, which make Qn
|
|
116
|
+
unbiased at the normal model for small ``n``.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
float
|
|
121
|
+
``nan`` for an empty sample, 0 for a single observation.
|
|
122
|
+
|
|
123
|
+
References
|
|
124
|
+
----------
|
|
125
|
+
Rousseeuw, P. J. and Croux, C. (1993). Alternatives to the median absolute
|
|
126
|
+
deviation. *Journal of the American Statistical Association*, 88(424), 1273-1283.
|
|
127
|
+
|
|
128
|
+
Examples
|
|
129
|
+
--------
|
|
130
|
+
>>> from robustmodels.scale import qn
|
|
131
|
+
>>> round(qn([1, 2, 2, 3, 4, 100]), 4)
|
|
132
|
+
1.3586
|
|
133
|
+
"""
|
|
134
|
+
x = np.sort(as_1d(x))
|
|
135
|
+
if x.size < 2:
|
|
136
|
+
return _trivial_scale(x)
|
|
137
|
+
h = x.size // 2 + 1
|
|
138
|
+
rank = h * (h - 1) // 2
|
|
139
|
+
first, last = _pairwise_rows(x.size)
|
|
140
|
+
value = _kth_smallest(lambda i, j: x[j] - x[i], first, last, rank)
|
|
141
|
+
if consistency:
|
|
142
|
+
value *= QN_CONSTANT
|
|
143
|
+
if finite_correction:
|
|
144
|
+
value *= _qn_factor(x.size)
|
|
145
|
+
return float(value)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _qn_factor(n):
|
|
149
|
+
if n <= 12:
|
|
150
|
+
return _QN_N[n - 2]
|
|
151
|
+
if n % 2:
|
|
152
|
+
correction = -2.1284 - 5.172 / n
|
|
153
|
+
correction = 1.60188 + correction / n
|
|
154
|
+
else:
|
|
155
|
+
correction = 6.987 - 77 / n
|
|
156
|
+
correction = 1.9654 + correction / n
|
|
157
|
+
correction = 3.67561 + correction / n
|
|
158
|
+
return 1 / (1 + correction / n)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
SN_CONSTANT = 1.1925985531232086
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# Sn factors for n = 2, ..., 9; Croux & Rousseeuw (1992).
|
|
165
|
+
_SN_N = (0.743, 1.851, 0.954, 1.351, 0.993, 1.198, 1.005, 1.131)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def sn(x, *, consistency=True, finite_correction=True):
|
|
169
|
+
"""Sn scale estimator of Rousseeuw & Croux (1993).
|
|
170
|
+
|
|
171
|
+
``lomed_i himed_j |x_i - x_j|``: for each observation, the high median of its
|
|
172
|
+
distances to all observations; then the low median of these values.
|
|
173
|
+
Breakdown point 50%, Gaussian efficiency 58%. Computed exactly in
|
|
174
|
+
O(n log n) time.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
x : array_like
|
|
179
|
+
One-dimensional sample of finite values.
|
|
180
|
+
consistency : bool, default True
|
|
181
|
+
Multiply by 1.1926, so that Sn estimates the standard deviation at the
|
|
182
|
+
normal model.
|
|
183
|
+
finite_correction : bool, default True
|
|
184
|
+
Multiply by the small-sample factors of Croux & Rousseeuw (1992).
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
float
|
|
189
|
+
``nan`` for an empty sample, 0 for a single observation.
|
|
190
|
+
|
|
191
|
+
References
|
|
192
|
+
----------
|
|
193
|
+
Rousseeuw, P. J. and Croux, C. (1993). Alternatives to the median absolute
|
|
194
|
+
deviation. *Journal of the American Statistical Association*, 88(424), 1273-1283.
|
|
195
|
+
|
|
196
|
+
Croux, C. and Rousseeuw, P. J. (1992). Time-efficient algorithms for two
|
|
197
|
+
highly robust estimators of scale. *Computational Statistics*, 1, 411-428.
|
|
198
|
+
|
|
199
|
+
Examples
|
|
200
|
+
--------
|
|
201
|
+
>>> from robustmodels.scale import sn
|
|
202
|
+
>>> round(sn([1, 2, 2, 3, 4, 100]), 4)
|
|
203
|
+
1.1843
|
|
204
|
+
"""
|
|
205
|
+
x = np.sort(as_1d(x))
|
|
206
|
+
if x.size < 2:
|
|
207
|
+
return _trivial_scale(x)
|
|
208
|
+
distances = np.sort(_himed_distances(x))
|
|
209
|
+
lower_middle = (x.size + 1) // 2 - 1
|
|
210
|
+
value = distances[lower_middle]
|
|
211
|
+
if consistency:
|
|
212
|
+
value *= SN_CONSTANT
|
|
213
|
+
if finite_correction:
|
|
214
|
+
value *= _sn_factor(x.size)
|
|
215
|
+
return float(value)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _sn_factor(n):
|
|
219
|
+
if n <= 9:
|
|
220
|
+
return _SN_N[n - 2]
|
|
221
|
+
return n / (n - 0.9) if n % 2 else 1.0
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _himed_distances(x):
|
|
225
|
+
"""For sorted x and every i, the ``(n//2 + 1)``-th smallest ``|x_i - x_j|`` over j.
|
|
226
|
+
|
|
227
|
+
The zero self-distance comes first, so this is the ``n//2``-th smallest of the
|
|
228
|
+
merged sorted lists ``left[a] = x_i - x_(i-1-a)`` and ``right[b] = x_(i+1+b) - x_i``;
|
|
229
|
+
a vectorized binary search finds how many come from the left list.
|
|
230
|
+
"""
|
|
231
|
+
n, k = x.size, x.size // 2
|
|
232
|
+
i = np.arange(n)
|
|
233
|
+
|
|
234
|
+
def left(a): # +inf past the end
|
|
235
|
+
indices = np.clip(i - 1 - a, 0, n - 1)
|
|
236
|
+
distances = x[i] - x[indices]
|
|
237
|
+
return np.where(a < i, distances, np.inf)
|
|
238
|
+
|
|
239
|
+
def right(b): # -inf before the start
|
|
240
|
+
indices = np.clip(i + 1 + b, 0, n - 1)
|
|
241
|
+
distances = x[indices] - x[i]
|
|
242
|
+
return np.where(b >= 0, distances, -np.inf)
|
|
243
|
+
|
|
244
|
+
lo, hi = np.maximum(0, k - (n - 1 - i)), np.minimum(k, i)
|
|
245
|
+
while np.any(lo < hi):
|
|
246
|
+
active = lo < hi
|
|
247
|
+
mid = (lo + hi) // 2
|
|
248
|
+
more = left(mid) < right(k - mid - 1)
|
|
249
|
+
lo[active & more] = mid[active & more] + 1
|
|
250
|
+
hi[active & ~more] = mid[active & ~more]
|
|
251
|
+
last_left = np.where(lo > 0, left(lo - 1), -np.inf)
|
|
252
|
+
last_right = right(k - lo - 1)
|
|
253
|
+
return np.maximum(last_left, last_right)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
PN_CONSTANT = 1 / (np.sqrt(2) * norm.ppf(0.75))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# Tarr (2014), Appendix B.3, p. 148: factors for n = 3, ..., 40.
|
|
260
|
+
# https://www.researchgate.net/publication/267927753
|
|
261
|
+
_PN_N = (
|
|
262
|
+
1.128,
|
|
263
|
+
1.303,
|
|
264
|
+
1.109,
|
|
265
|
+
1.064,
|
|
266
|
+
1.166,
|
|
267
|
+
1.103,
|
|
268
|
+
1.087,
|
|
269
|
+
1.105,
|
|
270
|
+
1.047,
|
|
271
|
+
1.063,
|
|
272
|
+
1.057,
|
|
273
|
+
1.040,
|
|
274
|
+
1.061,
|
|
275
|
+
1.047,
|
|
276
|
+
1.043,
|
|
277
|
+
1.048,
|
|
278
|
+
1.031,
|
|
279
|
+
1.037,
|
|
280
|
+
1.035,
|
|
281
|
+
1.028,
|
|
282
|
+
1.036,
|
|
283
|
+
1.030,
|
|
284
|
+
1.029,
|
|
285
|
+
1.032,
|
|
286
|
+
1.023,
|
|
287
|
+
1.025,
|
|
288
|
+
1.024,
|
|
289
|
+
1.021,
|
|
290
|
+
1.026,
|
|
291
|
+
1.022,
|
|
292
|
+
1.021,
|
|
293
|
+
1.023,
|
|
294
|
+
1.018,
|
|
295
|
+
1.020,
|
|
296
|
+
1.019,
|
|
297
|
+
1.017,
|
|
298
|
+
1.020,
|
|
299
|
+
1.018,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def pn(x, *, consistency=True, finite_correction=True):
|
|
304
|
+
"""Pn scale estimator of Tarr, Müller & Weber (2012).
|
|
305
|
+
|
|
306
|
+
Interquartile range of the pairwise means: ``H^-1(3/4) - H^-1(1/4)`` with
|
|
307
|
+
``H`` the empirical distribution of ``(x_i + x_j) / 2``, ``i < j``.
|
|
308
|
+
Breakdown point 13.4%, Gaussian efficiency 86%. Computed exactly in
|
|
309
|
+
O(n log n) time.
|
|
310
|
+
|
|
311
|
+
Parameters
|
|
312
|
+
----------
|
|
313
|
+
x : array_like
|
|
314
|
+
One-dimensional sample of finite values.
|
|
315
|
+
consistency : bool, default True
|
|
316
|
+
Multiply by ``1 / (sqrt(2) * Phi^-1(3/4)) = 1.0484``, so that Pn estimates
|
|
317
|
+
the standard deviation at the normal model.
|
|
318
|
+
finite_correction : bool, default True
|
|
319
|
+
Multiply by the factors of Tarr (2014), Appendix B.3, for ``n <= 40`` and
|
|
320
|
+
by ``n / (n - 0.7)`` for larger samples.
|
|
321
|
+
|
|
322
|
+
Returns
|
|
323
|
+
-------
|
|
324
|
+
float
|
|
325
|
+
``nan`` for an empty sample, 0 for one or two observations.
|
|
326
|
+
|
|
327
|
+
References
|
|
328
|
+
----------
|
|
329
|
+
Tarr, G., Müller, S. and Weber, N. C. (2012). A robust scale estimator based
|
|
330
|
+
on pairwise means. *Journal of Nonparametric Statistics*, 24(1), 187-199.
|
|
331
|
+
|
|
332
|
+
Tarr, G. (2014). *Quantile Based Estimation of Scale and Dependence*.
|
|
333
|
+
PhD thesis, University of Sydney.
|
|
334
|
+
|
|
335
|
+
Examples
|
|
336
|
+
--------
|
|
337
|
+
One outlier in six exceeds the breakdown point of Pn:
|
|
338
|
+
|
|
339
|
+
>>> from robustmodels.scale import pn
|
|
340
|
+
>>> round(pn([1, 2, 2, 3, 4, 100]), 4)
|
|
341
|
+
54.6572
|
|
342
|
+
"""
|
|
343
|
+
x = np.sort(as_1d(x))
|
|
344
|
+
if x.size < 3:
|
|
345
|
+
return _trivial_scale(x)
|
|
346
|
+
q1 = _pairwise_quantile(x, 0.25)
|
|
347
|
+
q3 = _pairwise_quantile(x, 0.75)
|
|
348
|
+
value = q3 - q1
|
|
349
|
+
if consistency:
|
|
350
|
+
value *= PN_CONSTANT
|
|
351
|
+
if finite_correction:
|
|
352
|
+
value *= _pn_factor(x.size)
|
|
353
|
+
return float(value)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _pn_factor(n):
|
|
357
|
+
if n <= 40:
|
|
358
|
+
return _PN_N[n - 3]
|
|
359
|
+
return n / (n - 0.7)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def tau_scale(x, *, c1=4.5, c2=3.0, consistency=True):
|
|
363
|
+
"""Tau scale of Yohai & Zamar (1988), in the form of Maronna & Zamar (2002).
|
|
364
|
+
|
|
365
|
+
``s0 * sqrt(mean(min(((x - mu) / s0)^2, c2^2)) / E)``, with ``s0`` the raw
|
|
366
|
+
(unscaled) MAD and ``mu`` the tau location
|
|
367
|
+
(:func:`robustmodels.location.tau_location`).
|
|
368
|
+
|
|
369
|
+
Parameters
|
|
370
|
+
----------
|
|
371
|
+
x : array_like
|
|
372
|
+
One-dimensional sample of finite values.
|
|
373
|
+
c1 : float, default 4.5
|
|
374
|
+
Bisquare tuning constant of the location weights.
|
|
375
|
+
c2 : float, default 3.0
|
|
376
|
+
Truncation point of the standardized residuals.
|
|
377
|
+
consistency : bool, default True
|
|
378
|
+
Divide by ``E = E[min(Z^2, (c2 * Phi^-1(3/4))^2)]``, so that the result
|
|
379
|
+
estimates the standard deviation at the normal model.
|
|
380
|
+
|
|
381
|
+
Returns
|
|
382
|
+
-------
|
|
383
|
+
float
|
|
384
|
+
``nan`` for an empty sample, 0 for a single observation or a zero MAD.
|
|
385
|
+
|
|
386
|
+
References
|
|
387
|
+
----------
|
|
388
|
+
Yohai, V. J. and Zamar, R. H. (1988). High breakdown-point estimates of
|
|
389
|
+
regression by means of the minimization of an efficient scale. *Journal of
|
|
390
|
+
the American Statistical Association*, 83(402), 406-413.
|
|
391
|
+
|
|
392
|
+
Maronna, R. A. and Zamar, R. H. (2002). Robust estimates of location and
|
|
393
|
+
dispersion for high-dimensional datasets. *Technometrics*, 44(4), 307-317.
|
|
394
|
+
|
|
395
|
+
Examples
|
|
396
|
+
--------
|
|
397
|
+
>>> from robustmodels.scale import tau_scale
|
|
398
|
+
>>> round(tau_scale([1, 2, 2, 3, 4, 100]), 4)
|
|
399
|
+
1.5998
|
|
400
|
+
"""
|
|
401
|
+
x = as_1d(x)
|
|
402
|
+
check_range("c1", c1, 0, np.inf, low_open=True)
|
|
403
|
+
check_range("c2", c2, 0, np.inf, low_open=True)
|
|
404
|
+
if x.size < 2:
|
|
405
|
+
return _trivial_scale(x)
|
|
406
|
+
location, s0 = _tau_estimates(x, c1)
|
|
407
|
+
if s0 == 0:
|
|
408
|
+
return 0.0
|
|
409
|
+
residuals = (x - location) / s0
|
|
410
|
+
clipped_squares = np.minimum(residuals**2, c2**2)
|
|
411
|
+
mean_square = clipped_squares.mean()
|
|
412
|
+
if consistency:
|
|
413
|
+
mean_square /= _e_trunc_square(c2 * norm.ppf(0.75))
|
|
414
|
+
return float(s0 * np.sqrt(mean_square))
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _tau_estimates(x, c):
|
|
418
|
+
"""Return the tau location and raw MAD of a nonempty, finite sample."""
|
|
419
|
+
center = np.median(x)
|
|
420
|
+
raw_mad = np.median(np.abs(x - center))
|
|
421
|
+
if raw_mad == 0:
|
|
422
|
+
return float(center), 0.0
|
|
423
|
+
residuals = (x - center) / raw_mad
|
|
424
|
+
weights = _bisquare_weights(residuals, c)
|
|
425
|
+
total = weights.sum()
|
|
426
|
+
if total == 0:
|
|
427
|
+
return float(center), float(raw_mad)
|
|
428
|
+
location = np.dot(weights, x) / total
|
|
429
|
+
return float(location), float(raw_mad)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _e_trunc_square(b):
|
|
433
|
+
"""E[min(Z^2, b^2)] for Z ~ N(0, 1)."""
|
|
434
|
+
return 2 * norm.cdf(b) - 1 - 2 * b * norm.pdf(b) + 2 * b * b * norm.sf(b)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def m_scale(x, *, center=None, breakdown=0.5):
|
|
438
|
+
"""Bisquare M-estimator of scale.
|
|
439
|
+
|
|
440
|
+
Solves ``mean(rho((x_i - center) / s)) = breakdown`` for ``s``, with Tukey's
|
|
441
|
+
bisquare ``rho`` scaled to a maximum of 1. Its tuning constant makes ``s``
|
|
442
|
+
estimate the standard deviation at the normal model. This is the scale that
|
|
443
|
+
S-estimators minimize.
|
|
444
|
+
|
|
445
|
+
Parameters
|
|
446
|
+
----------
|
|
447
|
+
x : array_like
|
|
448
|
+
One-dimensional sample of finite values.
|
|
449
|
+
center : float, optional
|
|
450
|
+
Center of the residuals. Defaults to the sample median; pass 0 for
|
|
451
|
+
residuals that are already centered.
|
|
452
|
+
breakdown : float in (0, 0.5], default 0.5
|
|
453
|
+
Breakdown point, the right-hand side of the equation.
|
|
454
|
+
|
|
455
|
+
Returns
|
|
456
|
+
-------
|
|
457
|
+
float
|
|
458
|
+
``nan`` for an empty sample. 0 for a single observation, or when at most a
|
|
459
|
+
fraction ``breakdown`` of the residuals is nonzero.
|
|
460
|
+
|
|
461
|
+
References
|
|
462
|
+
----------
|
|
463
|
+
Maronna, R. A., Martin, R. D., Yohai, V. J. and Salibián-Barrera, M. (2019).
|
|
464
|
+
*Robust Statistics: Theory and Methods (with R)*, 2nd ed. Wiley.
|
|
465
|
+
|
|
466
|
+
Examples
|
|
467
|
+
--------
|
|
468
|
+
>>> from robustmodels.scale import m_scale
|
|
469
|
+
>>> round(m_scale([1, 2, 2, 3, 4, 100]), 4)
|
|
470
|
+
1.5019
|
|
471
|
+
"""
|
|
472
|
+
x = as_1d(x)
|
|
473
|
+
check_range("breakdown", breakdown, 0, 0.5, low_open=True)
|
|
474
|
+
if center is not None:
|
|
475
|
+
check_range("center", center, -np.inf, np.inf)
|
|
476
|
+
if x.size < 2:
|
|
477
|
+
return _trivial_scale(x)
|
|
478
|
+
center = np.median(x) if center is None else center
|
|
479
|
+
residuals = x - center
|
|
480
|
+
tuning = _tuning_for_breakdown(breakdown)
|
|
481
|
+
return _mscale_bisquare(residuals, tuning, breakdown)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _mscale_bisquare(residuals, c, breakdown, s0=None, tol=1e-10, max_iter=200):
|
|
485
|
+
"""Solve mean(rho(residuals / scale)) = breakdown by fixed-point iteration."""
|
|
486
|
+
residuals = np.abs(residuals)
|
|
487
|
+
positive = residuals[residuals > 0]
|
|
488
|
+
if positive.size <= breakdown * residuals.size:
|
|
489
|
+
return 0.0
|
|
490
|
+
scale = np.median(residuals) / 0.6745 if s0 is None else s0
|
|
491
|
+
if scale <= 0:
|
|
492
|
+
scale = np.median(positive) / 0.6745
|
|
493
|
+
for _ in range(max_iter):
|
|
494
|
+
if scale <= 0:
|
|
495
|
+
return 0.0
|
|
496
|
+
average_loss = np.mean(_rho(residuals / scale, c))
|
|
497
|
+
updated = scale * np.sqrt(average_loss / breakdown)
|
|
498
|
+
if abs(updated - scale) <= tol * scale:
|
|
499
|
+
return float(updated)
|
|
500
|
+
scale = updated
|
|
501
|
+
return float(scale)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _rho(t, c):
|
|
505
|
+
"""Tukey's bisquare loss, normalized to rho(infinity) = 1."""
|
|
506
|
+
squared = (t / c) ** 2
|
|
507
|
+
clipped = np.minimum(squared, 1)
|
|
508
|
+
return 1 - (1 - clipped) ** 3
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _bisquare_weights(t, c):
|
|
512
|
+
"""``psi(t) / t`` up to a constant."""
|
|
513
|
+
u = (t / c) ** 2
|
|
514
|
+
return np.where(u < 1, (1 - u) ** 2, 0.0)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _expected_rho(c, p=1):
|
|
518
|
+
"""``E[rho_c(||Z||)]`` for ``Z ~ N_p(0, I)``."""
|
|
519
|
+
t = c * c
|
|
520
|
+
m1 = p * chi2.cdf(t, p + 2)
|
|
521
|
+
m2 = p * (p + 2) * chi2.cdf(t, p + 4)
|
|
522
|
+
m3 = p * (p + 2) * (p + 4) * chi2.cdf(t, p + 6)
|
|
523
|
+
return 3 * m1 / t - 3 * m2 / t**2 + m3 / t**3 + chi2.sf(t, p)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _tuning_for_breakdown(breakdown=0.5, p=1):
|
|
527
|
+
"""``c`` with ``E[rho_c] = breakdown``: consistent scale with that breakdown point."""
|
|
528
|
+
check_range("breakdown", breakdown, 0, 0.5, low_open=True)
|
|
529
|
+
upper = 10 * np.sqrt((p + 4) / breakdown) # E[rho_c] < breakdown there
|
|
530
|
+
return optimize.brentq(lambda c: _expected_rho(c, p) - breakdown, 1e-3, upper, xtol=1e-14)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _shape_efficiency(c, p):
|
|
534
|
+
"""Gaussian efficiency of the bisquare M-estimator of shape (Tyler 1982).
|
|
535
|
+
|
|
536
|
+
``1 / sigma1``, ``sigma1 = p(p+2) E[psi^2 d^2] / E[psi' d^2 + (p+1) psi d]^2``
|
|
537
|
+
with ``psi(d) = d (1 - (d/c)^2)^2`` and ``d = ||Z||``.
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
def m(a): # E[d^a; d <= c]
|
|
541
|
+
g = np.exp(special.gammaln((p + a) / 2) - special.gammaln(p / 2))
|
|
542
|
+
return g * 2 ** (a / 2) * chi2.cdf(c * c, p + a)
|
|
543
|
+
|
|
544
|
+
psi2_d2 = m(4) - 4 * m(6) / c**2 + 6 * m(8) / c**4 - 4 * m(10) / c**6 + m(12) / c**8
|
|
545
|
+
dpsi_d2 = m(2) - 6 * m(4) / c**2 + 5 * m(6) / c**4
|
|
546
|
+
psi_d = m(2) - 2 * m(4) / c**2 + m(6) / c**4
|
|
547
|
+
return (dpsi_d2 + (p + 1) * psi_d) ** 2 / (p * (p + 2) * psi2_d2)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _tuning_for_shape_efficiency(efficiency, p):
|
|
551
|
+
check_range("efficiency", efficiency, 0, 1, low_open=True, high_open=True)
|
|
552
|
+
return optimize.brentq(lambda c: _shape_efficiency(c, p) - efficiency, 0.5, 1e3, xtol=1e-12)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def medcouple(x):
|
|
556
|
+
"""Medcouple, a robust measure of skewness (Brys, Hubert & Struyf 2004).
|
|
557
|
+
|
|
558
|
+
Median of ``((x_i - m) - (m - x_j)) / (x_i - x_j)`` over ``x_i >= m >= x_j``,
|
|
559
|
+
with ``m`` the sample median and the paper's kernel for ties at ``m``.
|
|
560
|
+
Lies in [-1, 1]: positive for right skew, 0 for symmetric or constant
|
|
561
|
+
samples, and ``medcouple(-x) == -medcouple(x)``. Breakdown point 25%.
|
|
562
|
+
Computed exactly in O(n log n) time.
|
|
563
|
+
|
|
564
|
+
Parameters
|
|
565
|
+
----------
|
|
566
|
+
x : array_like
|
|
567
|
+
One-dimensional sample of finite values.
|
|
568
|
+
|
|
569
|
+
Returns
|
|
570
|
+
-------
|
|
571
|
+
float
|
|
572
|
+
``nan`` for an empty sample.
|
|
573
|
+
|
|
574
|
+
References
|
|
575
|
+
----------
|
|
576
|
+
Brys, G., Hubert, M. and Struyf, A. (2004). A robust measure of skewness.
|
|
577
|
+
*Journal of Computational and Graphical Statistics*, 13(4), 996-1017.
|
|
578
|
+
|
|
579
|
+
Examples
|
|
580
|
+
--------
|
|
581
|
+
>>> from robustmodels.scale import medcouple
|
|
582
|
+
>>> medcouple([1, 2, 2, 3, 4, 100])
|
|
583
|
+
0.5
|
|
584
|
+
"""
|
|
585
|
+
x = np.sort(as_1d(x))
|
|
586
|
+
if x.size == 0:
|
|
587
|
+
return np.nan
|
|
588
|
+
center = np.median(x)
|
|
589
|
+
upper = x[x >= center] - center
|
|
590
|
+
lower = x[x <= center] - center
|
|
591
|
+
|
|
592
|
+
def kernel(i, j):
|
|
593
|
+
tie = (upper[i] == 0) & (lower[j] == 0)
|
|
594
|
+
denominator = np.where(tie, 1, upper[i] - lower[j])
|
|
595
|
+
ratio = (upper[i] + lower[j]) / denominator
|
|
596
|
+
tie_value = np.sign(i + j - lower.size + 1)
|
|
597
|
+
return np.where(tie, tie_value, ratio)
|
|
598
|
+
|
|
599
|
+
first = np.zeros(upper.size, dtype=int)
|
|
600
|
+
last = np.full(upper.size, lower.size - 1)
|
|
601
|
+
return _matrix_median(kernel, first, last)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def lmc(x):
|
|
605
|
+
"""Left medcouple, a robust measure of left tail weight (Brys, Hubert & Struyf 2006).
|
|
606
|
+
|
|
607
|
+
``LMC = -medcouple(x[x <= median(x)])``; larger values mean a heavier left tail.
|
|
608
|
+
|
|
609
|
+
Parameters
|
|
610
|
+
----------
|
|
611
|
+
x : array_like
|
|
612
|
+
One-dimensional sample of finite values.
|
|
613
|
+
|
|
614
|
+
Returns
|
|
615
|
+
-------
|
|
616
|
+
float
|
|
617
|
+
``nan`` for an empty sample.
|
|
618
|
+
|
|
619
|
+
References
|
|
620
|
+
----------
|
|
621
|
+
Brys, G., Hubert, M. and Struyf, A. (2006). Robust measures of tail weight.
|
|
622
|
+
*Computational Statistics & Data Analysis*, 50(3), 733-759.
|
|
623
|
+
|
|
624
|
+
Examples
|
|
625
|
+
--------
|
|
626
|
+
>>> from robustmodels.scale import lmc
|
|
627
|
+
>>> round(lmc([0.2, 0.5, 0.9, 1.1, 1.6, 2.4, 3.8, 7.5]), 4)
|
|
628
|
+
0.0556
|
|
629
|
+
"""
|
|
630
|
+
x = as_1d(x)
|
|
631
|
+
if x.size == 0:
|
|
632
|
+
return np.nan
|
|
633
|
+
return 0.0 - medcouple(x[x <= np.median(x)])
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def rmc(x):
|
|
637
|
+
"""Right medcouple, a robust measure of right tail weight (Brys, Hubert & Struyf 2006).
|
|
638
|
+
|
|
639
|
+
``RMC = medcouple(x[x >= median(x)])``; larger values mean a heavier right tail.
|
|
640
|
+
|
|
641
|
+
Parameters
|
|
642
|
+
----------
|
|
643
|
+
x : array_like
|
|
644
|
+
One-dimensional sample of finite values.
|
|
645
|
+
|
|
646
|
+
Returns
|
|
647
|
+
-------
|
|
648
|
+
float
|
|
649
|
+
``nan`` for an empty sample.
|
|
650
|
+
|
|
651
|
+
References
|
|
652
|
+
----------
|
|
653
|
+
Brys, G., Hubert, M. and Struyf, A. (2006). Robust measures of tail weight.
|
|
654
|
+
*Computational Statistics & Data Analysis*, 50(3), 733-759.
|
|
655
|
+
|
|
656
|
+
Examples
|
|
657
|
+
--------
|
|
658
|
+
>>> from robustmodels.scale import rmc
|
|
659
|
+
>>> round(rmc([0.2, 0.5, 0.9, 1.1, 1.6, 2.4, 3.8, 7.5]), 4)
|
|
660
|
+
0.2458
|
|
661
|
+
"""
|
|
662
|
+
x = as_1d(x)
|
|
663
|
+
if x.size == 0:
|
|
664
|
+
return np.nan
|
|
665
|
+
return medcouple(x[x >= np.median(x)])
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _trivial_scale(x):
|
|
669
|
+
"""Scale of fewer than two values: NaN if empty, else 0."""
|
|
670
|
+
return np.nan if x.size == 0 else 0.0
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
# Select pairwise values without allocating the full n-by-n matrix.
|
|
674
|
+
_DIRECT_LIMIT = 50_000
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _pairwise_rows(n, diagonal=False):
|
|
678
|
+
"""Inclusive column bounds for pairs i < j, or i <= j with the diagonal."""
|
|
679
|
+
row_count = n if diagonal else n - 1
|
|
680
|
+
first = np.arange(row_count)
|
|
681
|
+
if not diagonal:
|
|
682
|
+
first += 1
|
|
683
|
+
last = np.full(row_count, n - 1)
|
|
684
|
+
return first, last
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _pairwise_quantile(x, probability, diagonal=False):
|
|
688
|
+
"""Inverse empirical CDF of (x[i] + x[j]) / 2 for sorted x."""
|
|
689
|
+
first, last = _pairwise_rows(x.size, diagonal)
|
|
690
|
+
count = int(np.sum(last - first + 1))
|
|
691
|
+
rank = int(np.ceil(probability * count))
|
|
692
|
+
return _kth_smallest(lambda i, j: (x[i] + x[j]) / 2, first, last, rank)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _matrix_median(value, first, last):
|
|
696
|
+
"""Median of an implicit sorted-row matrix, averaging the two middle values."""
|
|
697
|
+
count = int(np.sum(last - first + 1))
|
|
698
|
+
rank = (count + 1) // 2
|
|
699
|
+
lower = _kth_smallest(value, first, last, rank)
|
|
700
|
+
if count % 2:
|
|
701
|
+
return lower
|
|
702
|
+
upper = _kth_smallest(value, first, last, rank + 1)
|
|
703
|
+
return float((lower + upper) / 2)
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _kth_smallest(value, lo, hi, k):
|
|
707
|
+
"""Select rank k (1-based) from value(i, j), with lo[i] <= j <= hi[i]."""
|
|
708
|
+
lo = np.array(lo, copy=True)
|
|
709
|
+
hi = np.array(hi, copy=True)
|
|
710
|
+
while True:
|
|
711
|
+
sizes = np.maximum(hi - lo + 1, 0)
|
|
712
|
+
if sizes.sum() <= max(_DIRECT_LIMIT, 4 * lo.size):
|
|
713
|
+
return _direct(value, lo, sizes, k)
|
|
714
|
+
rows = np.flatnonzero(sizes)
|
|
715
|
+
midpoints = (lo[rows] + hi[rows]) // 2
|
|
716
|
+
trial = _weighted_median(value(rows, midpoints), sizes[rows])
|
|
717
|
+
below = _count(value, rows, lo[rows], hi[rows], trial, strict=True)
|
|
718
|
+
through = _count(value, rows, lo[rows], hi[rows], trial, strict=False)
|
|
719
|
+
if k <= below.sum():
|
|
720
|
+
hi[rows] = lo[rows] + below - 1
|
|
721
|
+
elif k > through.sum():
|
|
722
|
+
k -= through.sum()
|
|
723
|
+
lo[rows] += through
|
|
724
|
+
else:
|
|
725
|
+
return float(trial)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _direct(value, first, sizes, rank):
|
|
729
|
+
"""Expand only the remaining candidates and select their order statistic."""
|
|
730
|
+
rows = np.repeat(np.arange(first.size), sizes)
|
|
731
|
+
row_offsets = np.cumsum(sizes) - sizes
|
|
732
|
+
offsets = np.arange(rows.size) - np.repeat(row_offsets, sizes)
|
|
733
|
+
columns = np.repeat(first, sizes) + offsets
|
|
734
|
+
candidates = value(rows, columns)
|
|
735
|
+
return float(np.partition(candidates, rank - 1)[rank - 1])
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _weighted_median(values, weights):
|
|
739
|
+
order = np.argsort(values)
|
|
740
|
+
cumulative = np.cumsum(weights[order])
|
|
741
|
+
middle = np.searchsorted(cumulative, cumulative[-1] / 2)
|
|
742
|
+
return values[order[middle]]
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _count(value, rows, lo, hi, trial, strict):
|
|
746
|
+
"""Count entries below trial in each row, including ties if strict is False."""
|
|
747
|
+
left = lo.copy()
|
|
748
|
+
right = hi + 1
|
|
749
|
+
while np.any(left < right):
|
|
750
|
+
active = left < right
|
|
751
|
+
middle = np.minimum((left + right) // 2, hi)
|
|
752
|
+
values = value(rows, middle)
|
|
753
|
+
below = values < trial if strict else values <= trial
|
|
754
|
+
move_left = active & below
|
|
755
|
+
move_right = active & ~below
|
|
756
|
+
left[move_left] = middle[move_left] + 1
|
|
757
|
+
right[move_right] = middle[move_right]
|
|
758
|
+
return left - lo
|