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.
@@ -0,0 +1,241 @@
1
+ """Robust univariate location estimators.
2
+
3
+ Every function takes a one-dimensional sample of finite values and returns a
4
+ float; an empty sample gives ``nan``.
5
+ """
6
+
7
+ import numpy as np
8
+
9
+ from ._validation import as_1d, check_count, check_option, check_range
10
+ from .scale import _bisquare_weights, _matrix_median, _pairwise_rows, _tau_estimates, mad
11
+
12
+ __all__ = ["hodges_lehmann", "m_location", "tau_location", "trimmed_mean", "winsorized_mean"]
13
+
14
+
15
+ def hodges_lehmann(x):
16
+ """Hodges-Lehmann estimator: the median of the Walsh averages.
17
+
18
+ Median of ``(x_i + x_j) / 2`` over ``i <= j``. Breakdown point 29.3%,
19
+ Gaussian efficiency 95.5%. Computed exactly in O(n log n) time.
20
+
21
+ Parameters
22
+ ----------
23
+ x : array_like
24
+ One-dimensional sample of finite values.
25
+
26
+ Returns
27
+ -------
28
+ float
29
+ ``nan`` for an empty sample.
30
+
31
+ References
32
+ ----------
33
+ Hodges, J. L. and Lehmann, E. L. (1963). Estimates of location based on rank
34
+ tests. *Annals of Mathematical Statistics*, 34(2), 598-611.
35
+
36
+ Examples
37
+ --------
38
+ >>> from robustmodels.location import hodges_lehmann
39
+ >>> hodges_lehmann([1, 2, 2, 3, 4, 100])
40
+ 3.0
41
+ """
42
+ x = np.sort(as_1d(x))
43
+ if x.size == 0:
44
+ return np.nan
45
+ first, last = _pairwise_rows(x.size, diagonal=True)
46
+ return _matrix_median(lambda i, j: (x[i] + x[j]) / 2, first, last)
47
+
48
+
49
+ def trimmed_mean(x, proportion=0.1):
50
+ """Trimmed mean, as ``scipy.stats.trim_mean``.
51
+
52
+ Mean after removing ``floor(proportion * n)`` observations from each end of
53
+ the sorted sample. Breakdown point close to ``proportion``.
54
+
55
+ Parameters
56
+ ----------
57
+ x : array_like
58
+ One-dimensional sample of finite values.
59
+ proportion : float in [0, 0.5), default 0.1
60
+ Fraction removed from each end.
61
+
62
+ Returns
63
+ -------
64
+ float
65
+ ``nan`` for an empty sample.
66
+
67
+ Examples
68
+ --------
69
+ >>> from robustmodels.location import trimmed_mean
70
+ >>> trimmed_mean([1, 2, 2, 3, 4, 100], proportion=0.2)
71
+ 2.75
72
+ """
73
+ x = np.sort(as_1d(x))
74
+ count = _trim_count(x.size, proportion)
75
+ if x.size == 0:
76
+ return np.nan
77
+ return float(x[count : x.size - count].mean())
78
+
79
+
80
+ def winsorized_mean(x, proportion=0.1):
81
+ """Winsorized mean.
82
+
83
+ Mean after replacing the ``k = floor(proportion * n)`` smallest observations
84
+ by the ``(k + 1)``-th smallest, and the ``k`` largest by the ``(k + 1)``-th
85
+ largest. Breakdown point close to ``proportion``.
86
+
87
+ Parameters
88
+ ----------
89
+ x : array_like
90
+ One-dimensional sample of finite values.
91
+ proportion : float in [0, 0.5), default 0.1
92
+ Fraction clipped at each end.
93
+
94
+ Returns
95
+ -------
96
+ float
97
+ ``nan`` for an empty sample.
98
+
99
+ Examples
100
+ --------
101
+ >>> from robustmodels.location import winsorized_mean
102
+ >>> round(winsorized_mean([1, 2, 2, 3, 4, 100], proportion=0.2), 4)
103
+ 2.8333
104
+ """
105
+ x = np.sort(as_1d(x))
106
+ count = _trim_count(x.size, proportion)
107
+ if x.size == 0:
108
+ return np.nan
109
+ clipped = np.clip(x, x[count], x[-1 - count])
110
+ return float(clipped.mean())
111
+
112
+
113
+ def _trim_count(n, proportion):
114
+ check_range("proportion", proportion, 0, 0.5, high_open=True)
115
+ return int(proportion * n)
116
+
117
+
118
+ def tau_location(x, *, c1=4.5):
119
+ """Tau location of Maronna & Zamar (2002): one bisquare-weighted mean.
120
+
121
+ Weighted mean with weights ``(1 - (r_i / c1)^2)^2`` for ``|r_i| < c1`` and 0
122
+ otherwise, where ``r_i = (x_i - median) / MAD0`` and ``MAD0`` is the raw
123
+ (unscaled) MAD. This is the location used by
124
+ :func:`robustmodels.scale.tau_scale`. Returns the median if ``MAD0`` is zero
125
+ or every weight is zero.
126
+
127
+ Parameters
128
+ ----------
129
+ x : array_like
130
+ One-dimensional sample of finite values.
131
+ c1 : float, default 4.5
132
+ Bisquare tuning constant.
133
+
134
+ Returns
135
+ -------
136
+ float
137
+ ``nan`` for an empty sample.
138
+
139
+ References
140
+ ----------
141
+ Maronna, R. A. and Zamar, R. H. (2002). Robust estimates of location and
142
+ dispersion for high-dimensional datasets. *Technometrics*, 44(4), 307-317.
143
+
144
+ Examples
145
+ --------
146
+ >>> from robustmodels.location import tau_location
147
+ >>> round(tau_location([1, 2, 2, 3, 4, 100]), 4)
148
+ 2.3918
149
+ """
150
+ x = as_1d(x)
151
+ check_range("c1", c1, 0, np.inf, low_open=True)
152
+ if x.size == 0:
153
+ return np.nan
154
+ location, _ = _tau_estimates(x, c1)
155
+ return location
156
+
157
+
158
+ PSI_C = {"huber": 1.345, "bisquare": 4.685}
159
+
160
+
161
+ def m_location(x, *, psi="huber", c=None, scale=None, tol=1e-9, max_iter=500):
162
+ """M-estimator of location with Huber's or Tukey's bisquare psi function.
163
+
164
+ Solves ``sum(psi((x_i - mu) / s)) = 0`` for ``mu`` by iteration from the
165
+ median, with the scale ``s`` held fixed. With the MAD as scale the breakdown
166
+ point is 50%; the default tuning constants give 95% Gaussian efficiency.
167
+
168
+ Parameters
169
+ ----------
170
+ x : array_like
171
+ One-dimensional sample of finite values.
172
+ psi : {"huber", "bisquare"}, default "huber"
173
+ Huber's monotone psi or Tukey's redescending bisquare psi.
174
+ c : float, optional
175
+ Tuning constant. Defaults to 1.345 for "huber" and 4.685 for "bisquare".
176
+ scale : float, optional
177
+ Fixed scale ``s``. Defaults to the MAD about the median. A zero scale
178
+ returns the median.
179
+ tol : float, default 1e-9
180
+ Stop when the location changes by less than ``tol * s``.
181
+ max_iter : int, default 500
182
+ Maximum number of iterations.
183
+
184
+ Returns
185
+ -------
186
+ float
187
+ ``nan`` for an empty sample. If the bisquare gives every observation zero
188
+ weight, the current location is returned.
189
+
190
+ References
191
+ ----------
192
+ Huber, P. J. (1964). Robust estimation of a location parameter. *Annals of
193
+ Mathematical Statistics*, 35(1), 73-101.
194
+
195
+ Beaton, A. E. and Tukey, J. W. (1974). The fitting of power series, meaning
196
+ polynomials, illustrated on band-spectroscopic data. *Technometrics*, 16(2),
197
+ 147-185.
198
+
199
+ Examples
200
+ --------
201
+ >>> from robustmodels.location import m_location
202
+ >>> round(m_location([1, 2, 2, 3, 4, 100]), 4)
203
+ 2.7988
204
+ >>> round(m_location([1, 2, 2, 3, 4, 100], psi="bisquare"), 4)
205
+ 2.3869
206
+ """
207
+ x = as_1d(x)
208
+ check_option("psi", psi, PSI_C)
209
+ c = PSI_C[psi] if c is None else c
210
+ _check_parameters(c, scale, tol, max_iter)
211
+ if x.size == 0:
212
+ return np.nan
213
+ location = float(np.median(x))
214
+ scale = mad(x, center=location) if scale is None else scale
215
+ if scale == 0:
216
+ return location
217
+ for _ in range(max_iter):
218
+ updated = _location_step(x, location, scale, psi, c)
219
+ if abs(updated - location) < tol * scale:
220
+ return float(updated)
221
+ location = updated
222
+ return float(location)
223
+
224
+
225
+ def _check_parameters(c, scale, tol, max_iter):
226
+ check_range("c", c, 0, np.inf, low_open=True)
227
+ check_range("tol", tol, 0, np.inf, low_open=True)
228
+ check_count("max_iter", max_iter)
229
+ if scale is not None:
230
+ check_range("scale", scale, 0, np.inf)
231
+
232
+
233
+ def _location_step(x, location, scale, psi, c):
234
+ if psi == "huber":
235
+ clipped = np.clip(x, location - c * scale, location + c * scale)
236
+ return float(clipped.mean())
237
+ weights = _bisquare_weights((x - location) / scale, c)
238
+ total = weights.sum()
239
+ if total == 0:
240
+ return location
241
+ return float(np.dot(weights, x) / total)