|
103 | 103 | 'rankdata', 'combine_pvalues', 'quantile_test',
|
104 | 104 | 'wasserstein_distance', 'wasserstein_distance_nd', 'energy_distance',
|
105 | 105 | 'brunnermunzel', 'alexandergovern',
|
106 |
| - 'expectile'] |
| 106 | + 'expectile', 'lmoment'] |
107 | 107 |
|
108 | 108 |
|
109 | 109 | def _chk_asarray(a, axis, *, xp=None):
|
@@ -961,11 +961,11 @@ def tsem(a, limits=None, inclusive=(True, True), axis=0, ddof=1):
|
961 | 961 | #####################################
|
962 | 962 |
|
963 | 963 |
|
964 |
| -def _moment_outputs(kwds): |
965 |
| - order = np.atleast_1d(kwds.get('order', 1)) |
966 |
| - if order.size == 0: |
967 |
| - raise ValueError("'order' must be a scalar or a non-empty 1D " |
968 |
| - "list/array.") |
| 964 | +def _moment_outputs(kwds, default_order=1): |
| 965 | + order = np.atleast_1d(kwds.get('order', default_order)) |
| 966 | + message = "`order` must be a scalar or a non-empty 1D array." |
| 967 | + if order.size == 0 or order.ndim > 1: |
| 968 | + raise ValueError(message) |
969 | 969 | return len(order)
|
970 | 970 |
|
971 | 971 |
|
@@ -1094,7 +1094,7 @@ def moment(a, order=1, axis=0, nan_policy='propagate', *, center=None):
|
1094 | 1094 | # decorator. Currently, the `_axis_nan_policy` decorator is skipped when `a`
|
1095 | 1095 | # is a non-NumPy array, so we need to check again. When the decorator is
|
1096 | 1096 | # updated for array API compatibility, we can remove this second check.
|
1097 |
| - raise ValueError("'order' must be a scalar or a non-empty 1D list/array.") |
| 1097 | + raise ValueError("`order` must be a scalar or a non-empty 1D array.") |
1098 | 1098 | if xp.any(order != xp.round(order)):
|
1099 | 1099 | raise ValueError("All elements of `order` must be integral.")
|
1100 | 1100 | order = order[()] if order.ndim == 0 else order
|
@@ -10225,6 +10225,146 @@ def first_order(t):
|
10225 | 10225 | return res.root
|
10226 | 10226 |
|
10227 | 10227 |
|
| 10228 | +def _lmoment_iv(sample, order, axis, sorted, standardize): |
| 10229 | + # input validation/standardization for `lmoment` |
| 10230 | + sample = np.asarray(sample) |
| 10231 | + message = "`sample` must be an array of real numbers." |
| 10232 | + if np.issubdtype(sample.dtype, np.integer): |
| 10233 | + sample = sample.astype(np.float64) |
| 10234 | + if not np.issubdtype(sample.dtype, np.floating): |
| 10235 | + raise ValueError(message) |
| 10236 | + |
| 10237 | + message = "`order` must be a scalar or a non-empty array of positive integers." |
| 10238 | + order = np.arange(1, 5) if order is None else np.asarray(order) |
| 10239 | + if not np.issubdtype(order.dtype, np.integer) or np.any(order <= 0): |
| 10240 | + raise ValueError(message) |
| 10241 | + |
| 10242 | + axis = np.asarray(axis)[()] |
| 10243 | + message = "`axis` must be an integer." |
| 10244 | + if not np.issubdtype(axis.dtype, np.integer) or axis.ndim != 0: |
| 10245 | + raise ValueError(message) |
| 10246 | + |
| 10247 | + sorted = np.asarray(sorted)[()] |
| 10248 | + message = "`sorted` must be True or False." |
| 10249 | + if not np.issubdtype(sorted.dtype, np.bool_) or sorted.ndim != 0: |
| 10250 | + raise ValueError(message) |
| 10251 | + |
| 10252 | + standardize = np.asarray(standardize)[()] |
| 10253 | + message = "`standardize` must be True or False." |
| 10254 | + if not np.issubdtype(standardize.dtype, np.bool_) or standardize.ndim != 0: |
| 10255 | + raise ValueError(message) |
| 10256 | + |
| 10257 | + sample = np.moveaxis(sample, axis, -1) |
| 10258 | + sample = np.sort(sample, axis=-1) if not sorted else sample |
| 10259 | + |
| 10260 | + return sample, order, axis, sorted, standardize |
| 10261 | + |
| 10262 | + |
| 10263 | +def _br(x, *, r=0): |
| 10264 | + n = x.shape[-1] |
| 10265 | + x = np.expand_dims(x, axis=-2) |
| 10266 | + x = np.broadcast_to(x, x.shape[:-2] + (len(r), n)) |
| 10267 | + x = np.triu(x) |
| 10268 | + j = np.arange(n, dtype=x.dtype) |
| 10269 | + n = np.asarray(n, dtype=x.dtype)[()] |
| 10270 | + return (np.sum(special.binom(j, r[:, np.newaxis])*x, axis=-1) |
| 10271 | + / special.binom(n-1, r) / n) |
| 10272 | + |
| 10273 | + |
| 10274 | +def _prk(r, k): |
| 10275 | + # Writen to match [1] Equation 27 closely to facilitate review. |
| 10276 | + # This does not protect against overflow, so improvements to |
| 10277 | + # robustness would be a welcome follow-up. |
| 10278 | + return (-1)**(r-k)*special.binom(r, k)*special.binom(r+k, k) |
| 10279 | + |
| 10280 | + |
| 10281 | +@_axis_nan_policy_factory( # noqa: E302 |
| 10282 | + _moment_result_object, n_samples=1, result_to_tuple=lambda x: (x,), |
| 10283 | + n_outputs=lambda kwds: _moment_outputs(kwds, [1, 2, 3, 4]) |
| 10284 | +) |
| 10285 | +def lmoment(sample, order=None, *, axis=0, sorted=False, standardize=True): |
| 10286 | + r"""Compute L-moments of a sample from a continuous distribution |
| 10287 | +
|
| 10288 | + The L-moments of a probability distribution are summary statistics with |
| 10289 | + uses similar to those of conventional moments, but they are defined in |
| 10290 | + terms of the expected values of order statistics. |
| 10291 | + Sample L-moments are defined analogously to population L-moments, and |
| 10292 | + they can serve as estimators of population L-moments. They tend to be less |
| 10293 | + sensitive to extreme observations than conventional moments. |
| 10294 | +
|
| 10295 | + Parameters |
| 10296 | + ---------- |
| 10297 | + sample : array_like |
| 10298 | + The real-valued sample whose L-moments are desired. |
| 10299 | + order : array_like, optional |
| 10300 | + The (positive integer) orders of the desired L-moments. |
| 10301 | + Must be a scalar or non-empty 1D array. Default is [1, 2, 3, 4]. |
| 10302 | + axis : int or None, default=0 |
| 10303 | + If an int, the axis of the input along which to compute the statistic. |
| 10304 | + The statistic of each axis-slice (e.g. row) of the input will appear |
| 10305 | + in a corresponding element of the output. If None, the input will be |
| 10306 | + raveled before computing the statistic. |
| 10307 | + sorted : bool, default=False |
| 10308 | + Whether `sample` is already sorted in increasing order along `axis`. |
| 10309 | + If False (default), `sample` will be sorted. |
| 10310 | + standardize : bool, default=True |
| 10311 | + Whether to return L-moment ratios for orders 3 and higher. |
| 10312 | + L-moment ratios are analogous to standardized conventional |
| 10313 | + moments: they are the non-standardized L-moments divided |
| 10314 | + by the L-moment of order 2. |
| 10315 | +
|
| 10316 | + Returns |
| 10317 | + ------- |
| 10318 | + lmoments : ndarray |
| 10319 | + The sample L-moments of order `order`. |
| 10320 | +
|
| 10321 | + See Also |
| 10322 | + -------- |
| 10323 | + moment |
| 10324 | +
|
| 10325 | + References |
| 10326 | + ---------- |
| 10327 | + .. [1] D. Bilkova. "L-Moments and TL-Moments as an Alternative Tool of |
| 10328 | + Statistical Data Analysis". Journal of Applied Mathematics and |
| 10329 | + Physics. 2014. :doi:`10.4236/jamp.2014.210104` |
| 10330 | + .. [2] J. R. M. Hosking. "L-Moments: Analysis and Estimation of Distributions |
| 10331 | + Using Linear Combinations of Order Statistics". Journal of the Royal |
| 10332 | + Statistical Society. 1990. :doi:`10.1111/j.2517-6161.1990.tb01775.x` |
| 10333 | + .. [3] "L-moment". *Wikipedia*. https://en.wikipedia.org/wiki/L-moment. |
| 10334 | +
|
| 10335 | + Examples |
| 10336 | + -------- |
| 10337 | + >>> import numpy as np |
| 10338 | + >>> from scipy import stats |
| 10339 | + >>> rng = np.random.default_rng(328458568356392) |
| 10340 | + >>> sample = rng.exponential(size=100000) |
| 10341 | + >>> stats.lmoment(sample) |
| 10342 | + array([1.00124272, 0.50111437, 0.3340092 , 0.16755338]) |
| 10343 | +
|
| 10344 | + Note that the first four standardized population L-moments of the standard |
| 10345 | + exponential distribution are 1, 1/2, 1/3, and 1/6; the sample L-moments |
| 10346 | + provide reasonable estimates. |
| 10347 | +
|
| 10348 | + """ |
| 10349 | + args = _lmoment_iv(sample, order, axis, sorted, standardize) |
| 10350 | + sample, order, axis, sorted, standardize = args |
| 10351 | + |
| 10352 | + n_moments = np.max(order) |
| 10353 | + k = np.arange(n_moments, dtype=sample.dtype) |
| 10354 | + prk = _prk(np.expand_dims(k, tuple(range(1, sample.ndim+1))), k) |
| 10355 | + bk = _br(sample, r=k) |
| 10356 | + |
| 10357 | + n = sample.shape[-1] |
| 10358 | + bk[..., n:] = 0 # remove NaNs due to n_moments > n |
| 10359 | + |
| 10360 | + lmoms = np.sum(prk * bk, axis=-1) |
| 10361 | + if standardize and n_moments > 2: |
| 10362 | + lmoms[2:] /= lmoms[1] |
| 10363 | + |
| 10364 | + lmoms[n:] = np.nan # add NaNs where appropriate |
| 10365 | + return lmoms[order-1] |
| 10366 | + |
| 10367 | + |
10228 | 10368 | LinregressResult = _make_tuple_bunch('LinregressResult',
|
10229 | 10369 | ['slope', 'intercept', 'rvalue',
|
10230 | 10370 | 'pvalue', 'stderr'],
|
|
0 commit comments