Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/scipy/stats/_levy_stable/__init__.py: 16%
406 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-12 06:31 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-12 06:31 +0000
1# -*- coding: utf-8 -*-
2#
4import warnings
5from functools import partial
7import numpy as np
9from scipy import optimize
10from scipy import integrate
11from scipy.integrate._quadrature import _builtincoeffs
12from scipy import interpolate
13from scipy.interpolate import RectBivariateSpline
14import scipy.special as sc
15from scipy._lib._util import _lazywhere
16from .._distn_infrastructure import rv_continuous, _ShapeInfo
17from .._continuous_distns import uniform, expon, _norm_pdf, _norm_cdf
18from .levyst import Nolan
19from scipy._lib.doccer import inherit_docstring_from
22__all__ = ["levy_stable", "levy_stable_gen", "pdf_from_cf_with_fft"]
24# Stable distributions are known for various parameterisations
25# some being advantageous for numerical considerations and others
26# useful due to their location/scale awareness.
27#
28# Here we follow [NO] convention (see the references in the docstring
29# for levy_stable_gen below).
30#
31# S0 / Z0 / x0 (aka Zoleterav's M)
32# S1 / Z1 / x1
33#
34# Where S* denotes parameterisation, Z* denotes standardized
35# version where gamma = 1, delta = 0 and x* denotes variable.
36#
37# Scipy's original Stable was a random variate generator. It
38# uses S1 and unfortunately is not a location/scale aware.
41# default numerical integration tolerance
42# used for epsrel in piecewise and both epsrel and epsabs in dni
43# (epsabs needed in dni since weighted quad requires epsabs > 0)
44_QUAD_EPS = 1.2e-14
47def _Phi_Z0(alpha, t):
48 return (
49 -np.tan(np.pi * alpha / 2) * (np.abs(t) ** (1 - alpha) - 1)
50 if alpha != 1
51 else -2.0 * np.log(np.abs(t)) / np.pi
52 )
55def _Phi_Z1(alpha, t):
56 return (
57 np.tan(np.pi * alpha / 2)
58 if alpha != 1
59 else -2.0 * np.log(np.abs(t)) / np.pi
60 )
63def _cf(Phi, t, alpha, beta):
64 """Characteristic function."""
65 return np.exp(
66 -(np.abs(t) ** alpha) * (1 - 1j * beta * np.sign(t) * Phi(alpha, t))
67 )
70_cf_Z0 = partial(_cf, _Phi_Z0)
71_cf_Z1 = partial(_cf, _Phi_Z1)
74def _pdf_single_value_cf_integrate(Phi, x, alpha, beta, **kwds):
75 """To improve DNI accuracy convert characteristic function in to real
76 valued integral using Euler's formula, then exploit cosine symmetry to
77 change limits to [0, inf). Finally use cosine addition formula to split
78 into two parts that can be handled by weighted quad pack.
79 """
80 quad_eps = kwds.get("quad_eps", _QUAD_EPS)
82 def integrand1(t):
83 if t == 0:
84 return 0
85 return np.exp(-(t ** alpha)) * (
86 np.cos(beta * (t ** alpha) * Phi(alpha, t))
87 )
89 def integrand2(t):
90 if t == 0:
91 return 0
92 return np.exp(-(t ** alpha)) * (
93 np.sin(beta * (t ** alpha) * Phi(alpha, t))
94 )
96 with np.errstate(invalid="ignore"):
97 int1, *ret1 = integrate.quad(
98 integrand1,
99 0,
100 np.inf,
101 weight="cos",
102 wvar=x,
103 limit=1000,
104 epsabs=quad_eps,
105 epsrel=quad_eps,
106 full_output=1,
107 )
109 int2, *ret2 = integrate.quad(
110 integrand2,
111 0,
112 np.inf,
113 weight="sin",
114 wvar=x,
115 limit=1000,
116 epsabs=quad_eps,
117 epsrel=quad_eps,
118 full_output=1,
119 )
121 return (int1 + int2) / np.pi
124_pdf_single_value_cf_integrate_Z0 = partial(
125 _pdf_single_value_cf_integrate, _Phi_Z0
126)
127_pdf_single_value_cf_integrate_Z1 = partial(
128 _pdf_single_value_cf_integrate, _Phi_Z1
129)
132def _nolan_round_difficult_input(
133 x0, alpha, beta, zeta, x_tol_near_zeta, alpha_tol_near_one
134):
135 """Round difficult input values for Nolan's method in [NO]."""
137 # following Nolan's STABLE,
138 # "1. When 0 < |alpha-1| < 0.005, the program has numerical problems
139 # evaluating the pdf and cdf. The current version of the program sets
140 # alpha=1 in these cases. This approximation is not bad in the S0
141 # parameterization."
142 if np.abs(alpha - 1) < alpha_tol_near_one:
143 alpha = 1.0
145 # "2. When alpha=1 and |beta| < 0.005, the program has numerical
146 # problems. The current version sets beta=0."
147 # We seem to have addressed this through re-expression of g(theta) here
149 # "8. When |x0-beta*tan(pi*alpha/2)| is small, the
150 # computations of the density and cumulative have numerical problems.
151 # The program works around this by setting
152 # z = beta*tan(pi*alpha/2) when
153 # |z-beta*tan(pi*alpha/2)| < tol(5)*alpha**(1/alpha).
154 # (The bound on the right is ad hoc, to get reasonable behavior
155 # when alpha is small)."
156 # where tol(5) = 0.5e-2 by default.
157 #
158 # We seem to have partially addressed this through re-expression of
159 # g(theta) here, but it still needs to be used in some extreme cases.
160 # Perhaps tol(5) = 0.5e-2 could be reduced for our implementation.
161 if np.abs(x0 - zeta) < x_tol_near_zeta * alpha ** (1 / alpha):
162 x0 = zeta
164 return x0, alpha, beta
167def _pdf_single_value_piecewise_Z1(x, alpha, beta, **kwds):
168 # convert from Nolan's S_1 (aka S) to S_0 (aka Zolaterev M)
169 # parameterization
171 zeta = -beta * np.tan(np.pi * alpha / 2.0)
172 x0 = x + zeta if alpha != 1 else x
174 return _pdf_single_value_piecewise_Z0(x0, alpha, beta, **kwds)
177def _pdf_single_value_piecewise_Z0(x0, alpha, beta, **kwds):
179 quad_eps = kwds.get("quad_eps", _QUAD_EPS)
180 x_tol_near_zeta = kwds.get("piecewise_x_tol_near_zeta", 0.005)
181 alpha_tol_near_one = kwds.get("piecewise_alpha_tol_near_one", 0.005)
183 zeta = -beta * np.tan(np.pi * alpha / 2.0)
184 x0, alpha, beta = _nolan_round_difficult_input(
185 x0, alpha, beta, zeta, x_tol_near_zeta, alpha_tol_near_one
186 )
188 # some other known distribution pdfs / analytical cases
189 # TODO: add more where possible with test coverage,
190 # eg https://en.wikipedia.org/wiki/Stable_distribution#Other_analytic_cases
191 if alpha == 2.0:
192 # normal
193 return _norm_pdf(x0 / np.sqrt(2)) / np.sqrt(2)
194 elif alpha == 0.5 and beta == 1.0:
195 # levy
196 # since S(1/2, 1, gamma, delta; <x>) ==
197 # S(1/2, 1, gamma, gamma + delta; <x0>).
198 _x = x0 + 1
199 if _x <= 0:
200 return 0
202 return 1 / np.sqrt(2 * np.pi * _x) / _x * np.exp(-1 / (2 * _x))
203 elif alpha == 0.5 and beta == 0.0 and x0 != 0:
204 # analytical solution [HO]
205 S, C = sc.fresnel([1 / np.sqrt(2 * np.pi * np.abs(x0))])
206 arg = 1 / (4 * np.abs(x0))
207 return (
208 np.sin(arg) * (0.5 - S[0]) + np.cos(arg) * (0.5 - C[0])
209 ) / np.sqrt(2 * np.pi * np.abs(x0) ** 3)
210 elif alpha == 1.0 and beta == 0.0:
211 # cauchy
212 return 1 / (1 + x0 ** 2) / np.pi
214 return _pdf_single_value_piecewise_post_rounding_Z0(
215 x0, alpha, beta, quad_eps
216 )
219def _pdf_single_value_piecewise_post_rounding_Z0(x0, alpha, beta, quad_eps):
220 """Calculate pdf using Nolan's methods as detailed in [NO].
221 """
223 _nolan = Nolan(alpha, beta, x0)
224 zeta = _nolan.zeta
225 xi = _nolan.xi
226 c2 = _nolan.c2
227 g = _nolan.g
229 # handle Nolan's initial case logic
230 if x0 == zeta:
231 return (
232 sc.gamma(1 + 1 / alpha)
233 * np.cos(xi)
234 / np.pi
235 / ((1 + zeta ** 2) ** (1 / alpha / 2))
236 )
237 elif x0 < zeta:
238 return _pdf_single_value_piecewise_post_rounding_Z0(
239 -x0, alpha, -beta, quad_eps
240 )
242 # following Nolan, we may now assume
243 # x0 > zeta when alpha != 1
244 # beta != 0 when alpha == 1
246 # spare calculating integral on null set
247 # use isclose as macos has fp differences
248 if np.isclose(-xi, np.pi / 2, rtol=1e-014, atol=1e-014):
249 return 0.0
251 def integrand(theta):
252 # limit any numerical issues leading to g_1 < 0 near theta limits
253 g_1 = g(theta)
254 if not np.isfinite(g_1) or g_1 < 0:
255 g_1 = 0
256 return g_1 * np.exp(-g_1)
258 with np.errstate(all="ignore"):
259 peak = optimize.bisect(
260 lambda t: g(t) - 1, -xi, np.pi / 2, xtol=quad_eps
261 )
263 # this integrand can be very peaked, so we need to force
264 # QUADPACK to evaluate the function inside its support
265 #
267 # lastly, we add additional samples at
268 # ~exp(-100), ~exp(-10), ~exp(-5), ~exp(-1)
269 # to improve QUADPACK's detection of rapidly descending tail behavior
270 # (this choice is fairly ad hoc)
271 tail_points = [
272 optimize.bisect(lambda t: g(t) - exp_height, -xi, np.pi / 2)
273 for exp_height in [100, 10, 5]
274 # exp_height = 1 is handled by peak
275 ]
276 intg_points = [0, peak] + tail_points
277 intg, *ret = integrate.quad(
278 integrand,
279 -xi,
280 np.pi / 2,
281 points=intg_points,
282 limit=100,
283 epsrel=quad_eps,
284 epsabs=0,
285 full_output=1,
286 )
288 return c2 * intg
291def _cdf_single_value_piecewise_Z1(x, alpha, beta, **kwds):
292 # convert from Nolan's S_1 (aka S) to S_0 (aka Zolaterev M)
293 # parameterization
295 zeta = -beta * np.tan(np.pi * alpha / 2.0)
296 x0 = x + zeta if alpha != 1 else x
298 return _cdf_single_value_piecewise_Z0(x0, alpha, beta, **kwds)
301def _cdf_single_value_piecewise_Z0(x0, alpha, beta, **kwds):
303 quad_eps = kwds.get("quad_eps", _QUAD_EPS)
304 x_tol_near_zeta = kwds.get("piecewise_x_tol_near_zeta", 0.005)
305 alpha_tol_near_one = kwds.get("piecewise_alpha_tol_near_one", 0.005)
307 zeta = -beta * np.tan(np.pi * alpha / 2.0)
308 x0, alpha, beta = _nolan_round_difficult_input(
309 x0, alpha, beta, zeta, x_tol_near_zeta, alpha_tol_near_one
310 )
312 # some other known distribution cdfs / analytical cases
313 # TODO: add more where possible with test coverage,
314 # eg https://en.wikipedia.org/wiki/Stable_distribution#Other_analytic_cases
315 if alpha == 2.0:
316 # normal
317 return _norm_cdf(x0 / np.sqrt(2))
318 elif alpha == 0.5 and beta == 1.0:
319 # levy
320 # since S(1/2, 1, gamma, delta; <x>) ==
321 # S(1/2, 1, gamma, gamma + delta; <x0>).
322 _x = x0 + 1
323 if _x <= 0:
324 return 0
326 return sc.erfc(np.sqrt(0.5 / _x))
327 elif alpha == 1.0 and beta == 0.0:
328 # cauchy
329 return 0.5 + np.arctan(x0) / np.pi
331 return _cdf_single_value_piecewise_post_rounding_Z0(
332 x0, alpha, beta, quad_eps
333 )
336def _cdf_single_value_piecewise_post_rounding_Z0(x0, alpha, beta, quad_eps):
337 """Calculate cdf using Nolan's methods as detailed in [NO].
338 """
339 _nolan = Nolan(alpha, beta, x0)
340 zeta = _nolan.zeta
341 xi = _nolan.xi
342 c1 = _nolan.c1
343 # c2 = _nolan.c2
344 c3 = _nolan.c3
345 g = _nolan.g
347 # handle Nolan's initial case logic
348 if (alpha == 1 and beta < 0) or x0 < zeta:
349 # NOTE: Nolan's paper has a typo here!
350 # He states F(x) = 1 - F(x, alpha, -beta), but this is clearly
351 # incorrect since F(-infty) would be 1.0 in this case
352 # Indeed, the alpha != 1, x0 < zeta case is correct here.
353 return 1 - _cdf_single_value_piecewise_post_rounding_Z0(
354 -x0, alpha, -beta, quad_eps
355 )
356 elif x0 == zeta:
357 return 0.5 - xi / np.pi
359 # following Nolan, we may now assume
360 # x0 > zeta when alpha != 1
361 # beta > 0 when alpha == 1
363 # spare calculating integral on null set
364 # use isclose as macos has fp differences
365 if np.isclose(-xi, np.pi / 2, rtol=1e-014, atol=1e-014):
366 return c1
368 def integrand(theta):
369 g_1 = g(theta)
370 return np.exp(-g_1)
372 with np.errstate(all="ignore"):
373 # shrink supports where required
374 left_support = -xi
375 right_support = np.pi / 2
376 if alpha > 1:
377 # integrand(t) monotonic 0 to 1
378 if integrand(-xi) != 0.0:
379 res = optimize.minimize(
380 integrand,
381 (-xi,),
382 method="L-BFGS-B",
383 bounds=[(-xi, np.pi / 2)],
384 )
385 left_support = res.x[0]
386 else:
387 # integrand(t) monotonic 1 to 0
388 if integrand(np.pi / 2) != 0.0:
389 res = optimize.minimize(
390 integrand,
391 (np.pi / 2,),
392 method="L-BFGS-B",
393 bounds=[(-xi, np.pi / 2)],
394 )
395 right_support = res.x[0]
397 intg, *ret = integrate.quad(
398 integrand,
399 left_support,
400 right_support,
401 points=[left_support, right_support],
402 limit=100,
403 epsrel=quad_eps,
404 epsabs=0,
405 full_output=1,
406 )
408 return c1 + c3 * intg
411def _rvs_Z1(alpha, beta, size=None, random_state=None):
412 """Simulate random variables using Nolan's methods as detailed in [NO].
413 """
415 def alpha1func(alpha, beta, TH, aTH, bTH, cosTH, tanTH, W):
416 return (
417 2
418 / np.pi
419 * (
420 (np.pi / 2 + bTH) * tanTH
421 - beta * np.log((np.pi / 2 * W * cosTH) / (np.pi / 2 + bTH))
422 )
423 )
425 def beta0func(alpha, beta, TH, aTH, bTH, cosTH, tanTH, W):
426 return (
427 W
428 / (cosTH / np.tan(aTH) + np.sin(TH))
429 * ((np.cos(aTH) + np.sin(aTH) * tanTH) / W) ** (1.0 / alpha)
430 )
432 def otherwise(alpha, beta, TH, aTH, bTH, cosTH, tanTH, W):
433 # alpha is not 1 and beta is not 0
434 val0 = beta * np.tan(np.pi * alpha / 2)
435 th0 = np.arctan(val0) / alpha
436 val3 = W / (cosTH / np.tan(alpha * (th0 + TH)) + np.sin(TH))
437 res3 = val3 * (
438 (
439 np.cos(aTH)
440 + np.sin(aTH) * tanTH
441 - val0 * (np.sin(aTH) - np.cos(aTH) * tanTH)
442 )
443 / W
444 ) ** (1.0 / alpha)
445 return res3
447 def alphanot1func(alpha, beta, TH, aTH, bTH, cosTH, tanTH, W):
448 res = _lazywhere(
449 beta == 0,
450 (alpha, beta, TH, aTH, bTH, cosTH, tanTH, W),
451 beta0func,
452 f2=otherwise,
453 )
454 return res
456 alpha = np.broadcast_to(alpha, size)
457 beta = np.broadcast_to(beta, size)
458 TH = uniform.rvs(
459 loc=-np.pi / 2.0, scale=np.pi, size=size, random_state=random_state
460 )
461 W = expon.rvs(size=size, random_state=random_state)
462 aTH = alpha * TH
463 bTH = beta * TH
464 cosTH = np.cos(TH)
465 tanTH = np.tan(TH)
466 res = _lazywhere(
467 alpha == 1,
468 (alpha, beta, TH, aTH, bTH, cosTH, tanTH, W),
469 alpha1func,
470 f2=alphanot1func,
471 )
472 return res
475def _fitstart_S0(data):
476 alpha, beta, delta1, gamma = _fitstart_S1(data)
478 # Formulas for mapping parameters in S1 parameterization to
479 # those in S0 parameterization can be found in [NO]. Note that
480 # only delta changes.
481 if alpha != 1:
482 delta0 = delta1 + beta * gamma * np.tan(np.pi * alpha / 2.0)
483 else:
484 delta0 = delta1 + 2 * beta * gamma * np.log(gamma) / np.pi
486 return alpha, beta, delta0, gamma
489def _fitstart_S1(data):
490 # We follow McCullock 1986 method - Simple Consistent Estimators
491 # of Stable Distribution Parameters
493 # fmt: off
494 # Table III and IV
495 nu_alpha_range = [2.439, 2.5, 2.6, 2.7, 2.8, 3, 3.2, 3.5, 4,
496 5, 6, 8, 10, 15, 25]
497 nu_beta_range = [0, 0.1, 0.2, 0.3, 0.5, 0.7, 1]
499 # table III - alpha = psi_1(nu_alpha, nu_beta)
500 alpha_table = np.array([
501 [2.000, 2.000, 2.000, 2.000, 2.000, 2.000, 2.000],
502 [1.916, 1.924, 1.924, 1.924, 1.924, 1.924, 1.924],
503 [1.808, 1.813, 1.829, 1.829, 1.829, 1.829, 1.829],
504 [1.729, 1.730, 1.737, 1.745, 1.745, 1.745, 1.745],
505 [1.664, 1.663, 1.663, 1.668, 1.676, 1.676, 1.676],
506 [1.563, 1.560, 1.553, 1.548, 1.547, 1.547, 1.547],
507 [1.484, 1.480, 1.471, 1.460, 1.448, 1.438, 1.438],
508 [1.391, 1.386, 1.378, 1.364, 1.337, 1.318, 1.318],
509 [1.279, 1.273, 1.266, 1.250, 1.210, 1.184, 1.150],
510 [1.128, 1.121, 1.114, 1.101, 1.067, 1.027, 0.973],
511 [1.029, 1.021, 1.014, 1.004, 0.974, 0.935, 0.874],
512 [0.896, 0.892, 0.884, 0.883, 0.855, 0.823, 0.769],
513 [0.818, 0.812, 0.806, 0.801, 0.780, 0.756, 0.691],
514 [0.698, 0.695, 0.692, 0.689, 0.676, 0.656, 0.597],
515 [0.593, 0.590, 0.588, 0.586, 0.579, 0.563, 0.513]]).T
516 # transpose because interpolation with `RectBivariateSpline` is with
517 # `nu_beta` as `x` and `nu_alpha` as `y`
519 # table IV - beta = psi_2(nu_alpha, nu_beta)
520 beta_table = np.array([
521 [0, 2.160, 1.000, 1.000, 1.000, 1.000, 1.000],
522 [0, 1.592, 3.390, 1.000, 1.000, 1.000, 1.000],
523 [0, 0.759, 1.800, 1.000, 1.000, 1.000, 1.000],
524 [0, 0.482, 1.048, 1.694, 1.000, 1.000, 1.000],
525 [0, 0.360, 0.760, 1.232, 2.229, 1.000, 1.000],
526 [0, 0.253, 0.518, 0.823, 1.575, 1.000, 1.000],
527 [0, 0.203, 0.410, 0.632, 1.244, 1.906, 1.000],
528 [0, 0.165, 0.332, 0.499, 0.943, 1.560, 1.000],
529 [0, 0.136, 0.271, 0.404, 0.689, 1.230, 2.195],
530 [0, 0.109, 0.216, 0.323, 0.539, 0.827, 1.917],
531 [0, 0.096, 0.190, 0.284, 0.472, 0.693, 1.759],
532 [0, 0.082, 0.163, 0.243, 0.412, 0.601, 1.596],
533 [0, 0.074, 0.147, 0.220, 0.377, 0.546, 1.482],
534 [0, 0.064, 0.128, 0.191, 0.330, 0.478, 1.362],
535 [0, 0.056, 0.112, 0.167, 0.285, 0.428, 1.274]]).T
537 # Table V and VII
538 # These are ordered with decreasing `alpha_range`; so we will need to
539 # reverse them as required by RectBivariateSpline.
540 alpha_range = [2, 1.9, 1.8, 1.7, 1.6, 1.5, 1.4, 1.3, 1.2, 1.1,
541 1, 0.9, 0.8, 0.7, 0.6, 0.5][::-1]
542 beta_range = [0, 0.25, 0.5, 0.75, 1]
544 # Table V - nu_c = psi_3(alpha, beta)
545 nu_c_table = np.array([
546 [1.908, 1.908, 1.908, 1.908, 1.908],
547 [1.914, 1.915, 1.916, 1.918, 1.921],
548 [1.921, 1.922, 1.927, 1.936, 1.947],
549 [1.927, 1.930, 1.943, 1.961, 1.987],
550 [1.933, 1.940, 1.962, 1.997, 2.043],
551 [1.939, 1.952, 1.988, 2.045, 2.116],
552 [1.946, 1.967, 2.022, 2.106, 2.211],
553 [1.955, 1.984, 2.067, 2.188, 2.333],
554 [1.965, 2.007, 2.125, 2.294, 2.491],
555 [1.980, 2.040, 2.205, 2.435, 2.696],
556 [2.000, 2.085, 2.311, 2.624, 2.973],
557 [2.040, 2.149, 2.461, 2.886, 3.356],
558 [2.098, 2.244, 2.676, 3.265, 3.912],
559 [2.189, 2.392, 3.004, 3.844, 4.775],
560 [2.337, 2.634, 3.542, 4.808, 6.247],
561 [2.588, 3.073, 4.534, 6.636, 9.144]])[::-1].T
562 # transpose because interpolation with `RectBivariateSpline` is with
563 # `beta` as `x` and `alpha` as `y`
565 # Table VII - nu_zeta = psi_5(alpha, beta)
566 nu_zeta_table = np.array([
567 [0, 0.000, 0.000, 0.000, 0.000],
568 [0, -0.017, -0.032, -0.049, -0.064],
569 [0, -0.030, -0.061, -0.092, -0.123],
570 [0, -0.043, -0.088, -0.132, -0.179],
571 [0, -0.056, -0.111, -0.170, -0.232],
572 [0, -0.066, -0.134, -0.206, -0.283],
573 [0, -0.075, -0.154, -0.241, -0.335],
574 [0, -0.084, -0.173, -0.276, -0.390],
575 [0, -0.090, -0.192, -0.310, -0.447],
576 [0, -0.095, -0.208, -0.346, -0.508],
577 [0, -0.098, -0.223, -0.380, -0.576],
578 [0, -0.099, -0.237, -0.424, -0.652],
579 [0, -0.096, -0.250, -0.469, -0.742],
580 [0, -0.089, -0.262, -0.520, -0.853],
581 [0, -0.078, -0.272, -0.581, -0.997],
582 [0, -0.061, -0.279, -0.659, -1.198]])[::-1].T
583 # fmt: on
585 psi_1 = RectBivariateSpline(nu_beta_range, nu_alpha_range,
586 alpha_table, kx=1, ky=1, s=0)
588 def psi_1_1(nu_beta, nu_alpha):
589 return psi_1(nu_beta, nu_alpha) \
590 if nu_beta > 0 else psi_1(-nu_beta, nu_alpha)
592 psi_2 = RectBivariateSpline(nu_beta_range, nu_alpha_range,
593 beta_table, kx=1, ky=1, s=0)
595 def psi_2_1(nu_beta, nu_alpha):
596 return psi_2(nu_beta, nu_alpha) \
597 if nu_beta > 0 else -psi_2(-nu_beta, nu_alpha)
599 phi_3 = RectBivariateSpline(beta_range, alpha_range, nu_c_table,
600 kx=1, ky=1, s=0)
602 def phi_3_1(beta, alpha):
603 return phi_3(beta, alpha) if beta > 0 else phi_3(-beta, alpha)
605 phi_5 = RectBivariateSpline(beta_range, alpha_range, nu_zeta_table,
606 kx=1, ky=1, s=0)
608 def phi_5_1(beta, alpha):
609 return phi_5(beta, alpha) if beta > 0 else -phi_5(-beta, alpha)
611 # quantiles
612 p05 = np.percentile(data, 5)
613 p50 = np.percentile(data, 50)
614 p95 = np.percentile(data, 95)
615 p25 = np.percentile(data, 25)
616 p75 = np.percentile(data, 75)
618 nu_alpha = (p95 - p05) / (p75 - p25)
619 nu_beta = (p95 + p05 - 2 * p50) / (p95 - p05)
621 if nu_alpha >= 2.439:
622 eps = np.finfo(float).eps
623 alpha = np.clip(psi_1_1(nu_beta, nu_alpha)[0, 0], eps, 2.)
624 beta = np.clip(psi_2_1(nu_beta, nu_alpha)[0, 0], -1.0, 1.0)
625 else:
626 alpha = 2.0
627 beta = np.sign(nu_beta)
628 c = (p75 - p25) / phi_3_1(beta, alpha)[0, 0]
629 zeta = p50 + c * phi_5_1(beta, alpha)[0, 0]
630 delta = zeta-beta*c*np.tan(np.pi*alpha/2.) if alpha != 1. else zeta
632 return (alpha, beta, delta, c)
635class levy_stable_gen(rv_continuous):
636 r"""A Levy-stable continuous random variable.
638 %(before_notes)s
640 See Also
641 --------
642 levy, levy_l, cauchy, norm
644 Notes
645 -----
646 The distribution for `levy_stable` has characteristic function:
648 .. math::
650 \varphi(t, \alpha, \beta, c, \mu) =
651 e^{it\mu -|ct|^{\alpha}(1-i\beta\operatorname{sign}(t)\Phi(\alpha, t))}
653 where two different parameterizations are supported. The first :math:`S_1`:
655 .. math::
657 \Phi = \begin{cases}
658 \tan \left({\frac {\pi \alpha }{2}}\right)&\alpha \neq 1\\
659 -{\frac {2}{\pi }}\log |t|&\alpha =1
660 \end{cases}
662 The second :math:`S_0`:
664 .. math::
666 \Phi = \begin{cases}
667 -\tan \left({\frac {\pi \alpha }{2}}\right)(|ct|^{1-\alpha}-1)
668 &\alpha \neq 1\\
669 -{\frac {2}{\pi }}\log |ct|&\alpha =1
670 \end{cases}
673 The probability density function for `levy_stable` is:
675 .. math::
677 f(x) = \frac{1}{2\pi}\int_{-\infty}^\infty \varphi(t)e^{-ixt}\,dt
679 where :math:`-\infty < t < \infty`. This integral does not have a known
680 closed form.
682 `levy_stable` generalizes several distributions. Where possible, they
683 should be used instead. Specifically, when the shape parameters
684 assume the values in the table below, the corresponding equivalent
685 distribution should be used.
687 ========= ======== ===========
688 ``alpha`` ``beta`` Equivalent
689 ========= ======== ===========
690 1/2 -1 `levy_l`
691 1/2 1 `levy`
692 1 0 `cauchy`
693 2 any `norm` (with ``scale=sqrt(2)``)
694 ========= ======== ===========
696 Evaluation of the pdf uses Nolan's piecewise integration approach with the
697 Zolotarev :math:`M` parameterization by default. There is also the option
698 to use direct numerical integration of the standard parameterization of the
699 characteristic function or to evaluate by taking the FFT of the
700 characteristic function.
702 The default method can changed by setting the class variable
703 ``levy_stable.pdf_default_method`` to one of 'piecewise' for Nolan's
704 approach, 'dni' for direct numerical integration, or 'fft-simpson' for the
705 FFT based approach. For the sake of backwards compatibility, the methods
706 'best' and 'zolotarev' are equivalent to 'piecewise' and the method
707 'quadrature' is equivalent to 'dni'.
709 The parameterization can be changed by setting the class variable
710 ``levy_stable.parameterization`` to either 'S0' or 'S1'.
711 The default is 'S1'.
713 To improve performance of piecewise and direct numerical integration one
714 can specify ``levy_stable.quad_eps`` (defaults to 1.2e-14). This is used
715 as both the absolute and relative quadrature tolerance for direct numerical
716 integration and as the relative quadrature tolerance for the piecewise
717 method. One can also specify ``levy_stable.piecewise_x_tol_near_zeta``
718 (defaults to 0.005) for how close x is to zeta before it is considered the
719 same as x [NO]. The exact check is
720 ``abs(x0 - zeta) < piecewise_x_tol_near_zeta*alpha**(1/alpha)``. One can
721 also specify ``levy_stable.piecewise_alpha_tol_near_one`` (defaults to
722 0.005) for how close alpha is to 1 before being considered equal to 1.
724 To increase accuracy of FFT calculation one can specify
725 ``levy_stable.pdf_fft_grid_spacing`` (defaults to 0.001) and
726 ``pdf_fft_n_points_two_power`` (defaults to None which means a value is
727 calculated that sufficiently covers the input range).
729 Further control over FFT calculation is available by setting
730 ``pdf_fft_interpolation_degree`` (defaults to 3) for spline order and
731 ``pdf_fft_interpolation_level`` for determining the number of points to use
732 in the Newton-Cotes formula when approximating the characteristic function
733 (considered experimental).
735 Evaluation of the cdf uses Nolan's piecewise integration approach with the
736 Zolatarev :math:`S_0` parameterization by default. There is also the option
737 to evaluate through integration of an interpolated spline of the pdf
738 calculated by means of the FFT method. The settings affecting FFT
739 calculation are the same as for pdf calculation. The default cdf method can
740 be changed by setting ``levy_stable.cdf_default_method`` to either
741 'piecewise' or 'fft-simpson'. For cdf calculations the Zolatarev method is
742 superior in accuracy, so FFT is disabled by default.
744 Fitting estimate uses quantile estimation method in [MC]. MLE estimation of
745 parameters in fit method uses this quantile estimate initially. Note that
746 MLE doesn't always converge if using FFT for pdf calculations; this will be
747 the case if alpha <= 1 where the FFT approach doesn't give good
748 approximations.
750 Any non-missing value for the attribute
751 ``levy_stable.pdf_fft_min_points_threshold`` will set
752 ``levy_stable.pdf_default_method`` to 'fft-simpson' if a valid
753 default method is not otherwise set.
757 .. warning::
759 For pdf calculations FFT calculation is considered experimental.
761 For cdf calculations FFT calculation is considered experimental. Use
762 Zolatarev's method instead (default).
764 %(after_notes)s
766 References
767 ----------
768 .. [MC] McCulloch, J., 1986. Simple consistent estimators of stable
769 distribution parameters. Communications in Statistics - Simulation and
770 Computation 15, 11091136.
771 .. [WZ] Wang, Li and Zhang, Ji-Hong, 2008. Simpson's rule based FFT method
772 to compute densities of stable distribution.
773 .. [NO] Nolan, J., 1997. Numerical Calculation of Stable Densities and
774 distributions Functions.
775 .. [HO] Hopcraft, K. I., Jakeman, E., Tanner, R. M. J., 1999. Lévy random
776 walks with fluctuating step number and multiscale behavior.
778 %(example)s
780 """
781 # Configurable options as class variables
782 # (accesible from self by attribute lookup).
783 parameterization = "S1"
784 pdf_default_method = "piecewise"
785 cdf_default_method = "piecewise"
786 quad_eps = _QUAD_EPS
787 piecewise_x_tol_near_zeta = 0.005
788 piecewise_alpha_tol_near_one = 0.005
789 pdf_fft_min_points_threshold = None
790 pdf_fft_grid_spacing = 0.001
791 pdf_fft_n_points_two_power = None
792 pdf_fft_interpolation_level = 3
793 pdf_fft_interpolation_degree = 3
795 def _argcheck(self, alpha, beta):
796 return (alpha > 0) & (alpha <= 2) & (beta <= 1) & (beta >= -1)
798 def _shape_info(self):
799 ialpha = _ShapeInfo("alpha", False, (0, 2), (False, True))
800 ibeta = _ShapeInfo("beta", False, (-1, 1), (True, True))
801 return [ialpha, ibeta]
803 def _parameterization(self):
804 allowed = ("S0", "S1")
805 pz = self.parameterization
806 if pz not in allowed:
807 raise RuntimeError(
808 f"Parameterization '{pz}' in supported list: {allowed}"
809 )
810 return pz
812 @inherit_docstring_from(rv_continuous)
813 def rvs(self, *args, **kwds):
814 X1 = super().rvs(*args, **kwds)
816 discrete = kwds.pop("discrete", None) # noqa
817 rndm = kwds.pop("random_state", None) # noqa
818 (alpha, beta), delta, gamma, size = self._parse_args_rvs(*args, **kwds)
820 # shift location for this parameterisation (S1)
821 X1 = np.where(
822 alpha == 1.0, X1 + 2 * beta * gamma * np.log(gamma) / np.pi, X1
823 )
825 if self._parameterization() == "S0":
826 return np.where(
827 alpha == 1.0,
828 X1 - (beta * 2 * gamma * np.log(gamma) / np.pi),
829 X1 - gamma * beta * np.tan(np.pi * alpha / 2.0),
830 )
831 elif self._parameterization() == "S1":
832 return X1
834 def _rvs(self, alpha, beta, size=None, random_state=None):
835 return _rvs_Z1(alpha, beta, size, random_state)
837 @inherit_docstring_from(rv_continuous)
838 def pdf(self, x, *args, **kwds):
839 # override base class version to correct
840 # location for S1 parameterization
841 if self._parameterization() == "S0":
842 return super().pdf(x, *args, **kwds)
843 elif self._parameterization() == "S1":
844 (alpha, beta), delta, gamma = self._parse_args(*args, **kwds)
845 if np.all(np.reshape(alpha, (1, -1))[0, :] != 1):
846 return super().pdf(x, *args, **kwds)
847 else:
848 # correct location for this parameterisation
849 x = np.reshape(x, (1, -1))[0, :]
850 x, alpha, beta = np.broadcast_arrays(x, alpha, beta)
852 data_in = np.dstack((x, alpha, beta))[0]
853 data_out = np.empty(shape=(len(data_in), 1))
854 # group data in unique arrays of alpha, beta pairs
855 uniq_param_pairs = np.unique(data_in[:, 1:], axis=0)
856 for pair in uniq_param_pairs:
857 _alpha, _beta = pair
858 _delta = (
859 delta + 2 * _beta * gamma * np.log(gamma) / np.pi
860 if _alpha == 1.0
861 else delta
862 )
863 data_mask = np.all(data_in[:, 1:] == pair, axis=-1)
864 _x = data_in[data_mask, 0]
865 data_out[data_mask] = (
866 super()
867 .pdf(_x, _alpha, _beta, loc=_delta, scale=gamma)
868 .reshape(len(_x), 1)
869 )
870 output = data_out.T[0]
871 if output.shape == (1,):
872 return output[0]
873 return output
875 def _pdf(self, x, alpha, beta):
876 if self._parameterization() == "S0":
877 _pdf_single_value_piecewise = _pdf_single_value_piecewise_Z0
878 _pdf_single_value_cf_integrate = _pdf_single_value_cf_integrate_Z0
879 _cf = _cf_Z0
880 elif self._parameterization() == "S1":
881 _pdf_single_value_piecewise = _pdf_single_value_piecewise_Z1
882 _pdf_single_value_cf_integrate = _pdf_single_value_cf_integrate_Z1
883 _cf = _cf_Z1
885 x = np.asarray(x).reshape(1, -1)[0, :]
887 x, alpha, beta = np.broadcast_arrays(x, alpha, beta)
889 data_in = np.dstack((x, alpha, beta))[0]
890 data_out = np.empty(shape=(len(data_in), 1))
892 pdf_default_method_name = levy_stable_gen.pdf_default_method
893 if pdf_default_method_name in ("piecewise", "best", "zolotarev"):
894 pdf_single_value_method = _pdf_single_value_piecewise
895 elif pdf_default_method_name in ("dni", "quadrature"):
896 pdf_single_value_method = _pdf_single_value_cf_integrate
897 elif (
898 pdf_default_method_name == "fft-simpson"
899 or self.pdf_fft_min_points_threshold is not None
900 ):
901 pdf_single_value_method = None
903 pdf_single_value_kwds = {
904 "quad_eps": self.quad_eps,
905 "piecewise_x_tol_near_zeta": self.piecewise_x_tol_near_zeta,
906 "piecewise_alpha_tol_near_one": self.piecewise_alpha_tol_near_one,
907 }
909 fft_grid_spacing = self.pdf_fft_grid_spacing
910 fft_n_points_two_power = self.pdf_fft_n_points_two_power
911 fft_interpolation_level = self.pdf_fft_interpolation_level
912 fft_interpolation_degree = self.pdf_fft_interpolation_degree
914 # group data in unique arrays of alpha, beta pairs
915 uniq_param_pairs = np.unique(data_in[:, 1:], axis=0)
916 for pair in uniq_param_pairs:
917 data_mask = np.all(data_in[:, 1:] == pair, axis=-1)
918 data_subset = data_in[data_mask]
919 if pdf_single_value_method is not None:
920 data_out[data_mask] = np.array(
921 [
922 pdf_single_value_method(
923 _x, _alpha, _beta, **pdf_single_value_kwds
924 )
925 for _x, _alpha, _beta in data_subset
926 ]
927 ).reshape(len(data_subset), 1)
928 else:
929 warnings.warn(
930 "Density calculations experimental for FFT method."
931 + " Use combination of piecewise and dni methods instead.",
932 RuntimeWarning,
933 )
934 _alpha, _beta = pair
935 _x = data_subset[:, (0,)]
937 if _alpha < 1.0:
938 raise RuntimeError(
939 "FFT method does not work well for alpha less than 1."
940 )
942 # need enough points to "cover" _x for interpolation
943 if fft_grid_spacing is None and fft_n_points_two_power is None:
944 raise ValueError(
945 "One of fft_grid_spacing or fft_n_points_two_power "
946 + "needs to be set."
947 )
948 max_abs_x = np.max(np.abs(_x))
949 h = (
950 2 ** (3 - fft_n_points_two_power) * max_abs_x
951 if fft_grid_spacing is None
952 else fft_grid_spacing
953 )
954 q = (
955 np.ceil(np.log(2 * max_abs_x / h) / np.log(2)) + 2
956 if fft_n_points_two_power is None
957 else int(fft_n_points_two_power)
958 )
960 # for some parameters, the range of x can be quite
961 # large, let's choose an arbitrary cut off (8GB) to save on
962 # computer memory.
963 MAX_Q = 30
964 if q > MAX_Q:
965 raise RuntimeError(
966 "fft_n_points_two_power has a maximum "
967 + f"value of {MAX_Q}"
968 )
970 density_x, density = pdf_from_cf_with_fft(
971 lambda t: _cf(t, _alpha, _beta),
972 h=h,
973 q=q,
974 level=fft_interpolation_level,
975 )
976 f = interpolate.InterpolatedUnivariateSpline(
977 density_x, np.real(density), k=fft_interpolation_degree
978 ) # patch FFT to use cubic
979 data_out[data_mask] = f(_x)
981 return data_out.T[0]
983 @inherit_docstring_from(rv_continuous)
984 def cdf(self, x, *args, **kwds):
985 # override base class version to correct
986 # location for S1 parameterization
987 # NOTE: this is near identical to pdf() above
988 if self._parameterization() == "S0":
989 return super().cdf(x, *args, **kwds)
990 elif self._parameterization() == "S1":
991 (alpha, beta), delta, gamma = self._parse_args(*args, **kwds)
992 if np.all(np.reshape(alpha, (1, -1))[0, :] != 1):
993 return super().cdf(x, *args, **kwds)
994 else:
995 # correct location for this parameterisation
996 x = np.reshape(x, (1, -1))[0, :]
997 x, alpha, beta = np.broadcast_arrays(x, alpha, beta)
999 data_in = np.dstack((x, alpha, beta))[0]
1000 data_out = np.empty(shape=(len(data_in), 1))
1001 # group data in unique arrays of alpha, beta pairs
1002 uniq_param_pairs = np.unique(data_in[:, 1:], axis=0)
1003 for pair in uniq_param_pairs:
1004 _alpha, _beta = pair
1005 _delta = (
1006 delta + 2 * _beta * gamma * np.log(gamma) / np.pi
1007 if _alpha == 1.0
1008 else delta
1009 )
1010 data_mask = np.all(data_in[:, 1:] == pair, axis=-1)
1011 _x = data_in[data_mask, 0]
1012 data_out[data_mask] = (
1013 super()
1014 .cdf(_x, _alpha, _beta, loc=_delta, scale=gamma)
1015 .reshape(len(_x), 1)
1016 )
1017 output = data_out.T[0]
1018 if output.shape == (1,):
1019 return output[0]
1020 return output
1022 def _cdf(self, x, alpha, beta):
1023 if self._parameterization() == "S0":
1024 _cdf_single_value_piecewise = _cdf_single_value_piecewise_Z0
1025 _cf = _cf_Z0
1026 elif self._parameterization() == "S1":
1027 _cdf_single_value_piecewise = _cdf_single_value_piecewise_Z1
1028 _cf = _cf_Z1
1030 x = np.asarray(x).reshape(1, -1)[0, :]
1032 x, alpha, beta = np.broadcast_arrays(x, alpha, beta)
1034 data_in = np.dstack((x, alpha, beta))[0]
1035 data_out = np.empty(shape=(len(data_in), 1))
1037 cdf_default_method_name = self.cdf_default_method
1038 if cdf_default_method_name == "piecewise":
1039 cdf_single_value_method = _cdf_single_value_piecewise
1040 elif cdf_default_method_name == "fft-simpson":
1041 cdf_single_value_method = None
1043 cdf_single_value_kwds = {
1044 "quad_eps": self.quad_eps,
1045 "piecewise_x_tol_near_zeta": self.piecewise_x_tol_near_zeta,
1046 "piecewise_alpha_tol_near_one": self.piecewise_alpha_tol_near_one,
1047 }
1049 fft_grid_spacing = self.pdf_fft_grid_spacing
1050 fft_n_points_two_power = self.pdf_fft_n_points_two_power
1051 fft_interpolation_level = self.pdf_fft_interpolation_level
1052 fft_interpolation_degree = self.pdf_fft_interpolation_degree
1054 # group data in unique arrays of alpha, beta pairs
1055 uniq_param_pairs = np.unique(data_in[:, 1:], axis=0)
1056 for pair in uniq_param_pairs:
1057 data_mask = np.all(data_in[:, 1:] == pair, axis=-1)
1058 data_subset = data_in[data_mask]
1059 if cdf_single_value_method is not None:
1060 data_out[data_mask] = np.array(
1061 [
1062 cdf_single_value_method(
1063 _x, _alpha, _beta, **cdf_single_value_kwds
1064 )
1065 for _x, _alpha, _beta in data_subset
1066 ]
1067 ).reshape(len(data_subset), 1)
1068 else:
1069 warnings.warn(
1070 "Cumulative density calculations experimental for FFT"
1071 + " method. Use piecewise method instead.",
1072 RuntimeWarning,
1073 )
1074 _alpha, _beta = pair
1075 _x = data_subset[:, (0,)]
1077 # need enough points to "cover" _x for interpolation
1078 if fft_grid_spacing is None and fft_n_points_two_power is None:
1079 raise ValueError(
1080 "One of fft_grid_spacing or fft_n_points_two_power "
1081 + "needs to be set."
1082 )
1083 max_abs_x = np.max(np.abs(_x))
1084 h = (
1085 2 ** (3 - fft_n_points_two_power) * max_abs_x
1086 if fft_grid_spacing is None
1087 else fft_grid_spacing
1088 )
1089 q = (
1090 np.ceil(np.log(2 * max_abs_x / h) / np.log(2)) + 2
1091 if fft_n_points_two_power is None
1092 else int(fft_n_points_two_power)
1093 )
1095 density_x, density = pdf_from_cf_with_fft(
1096 lambda t: _cf(t, _alpha, _beta),
1097 h=h,
1098 q=q,
1099 level=fft_interpolation_level,
1100 )
1101 f = interpolate.InterpolatedUnivariateSpline(
1102 density_x, np.real(density), k=fft_interpolation_degree
1103 )
1104 data_out[data_mask] = np.array(
1105 [f.integral(self.a, x_1) for x_1 in _x]
1106 ).reshape(data_out[data_mask].shape)
1108 return data_out.T[0]
1110 def _fitstart(self, data):
1111 if self._parameterization() == "S0":
1112 _fitstart = _fitstart_S0
1113 elif self._parameterization() == "S1":
1114 _fitstart = _fitstart_S1
1115 return _fitstart(data)
1117 def _stats(self, alpha, beta):
1118 mu = 0 if alpha > 1 else np.nan
1119 mu2 = 2 if alpha == 2 else np.inf
1120 g1 = 0.0 if alpha == 2.0 else np.NaN
1121 g2 = 0.0 if alpha == 2.0 else np.NaN
1122 return mu, mu2, g1, g2
1125# cotes numbers - see sequence from http://oeis.org/A100642
1126Cotes_table = np.array(
1127 [[], [1]] + [v[2] for v in _builtincoeffs.values()], dtype=object
1128)
1129Cotes = np.array(
1130 [
1131 np.pad(r, (0, len(Cotes_table) - 1 - len(r)), mode='constant')
1132 for r in Cotes_table
1133 ]
1134)
1137def pdf_from_cf_with_fft(cf, h=0.01, q=9, level=3):
1138 """Calculates pdf from characteristic function.
1140 Uses fast Fourier transform with Newton-Cotes integration following [WZ].
1141 Defaults to using Simpson's method (3-point Newton-Cotes integration).
1143 Parameters
1144 ----------
1145 cf : callable
1146 Single argument function from float -> complex expressing a
1147 characteristic function for some distribution.
1148 h : Optional[float]
1149 Step size for Newton-Cotes integration. Default: 0.01
1150 q : Optional[int]
1151 Use 2**q steps when peforming Newton-Cotes integration.
1152 The infinite integral in the inverse Fourier transform will then
1153 be restricted to the interval [-2**q * h / 2, 2**q * h / 2]. Setting
1154 the number of steps equal to a power of 2 allows the fft to be
1155 calculated in O(n*log(n)) time rather than O(n**2).
1156 Default: 9
1157 level : Optional[int]
1158 Calculate integral using n-point Newton-Cotes integration for
1159 n = level. The 3-point Newton-Cotes formula corresponds to Simpson's
1160 rule. Default: 3
1162 Returns
1163 -------
1164 x_l : ndarray
1165 Array of points x at which pdf is estimated. 2**q equally spaced
1166 points from -pi/h up to but not including pi/h.
1167 density : ndarray
1168 Estimated values of pdf corresponding to cf at points in x_l.
1170 References
1171 ----------
1172 .. [WZ] Wang, Li and Zhang, Ji-Hong, 2008. Simpson's rule based FFT method
1173 to compute densities of stable distribution.
1174 """
1175 n = level
1176 N = 2**q
1177 steps = np.arange(0, N)
1178 L = N * h / 2
1179 x_l = np.pi * (steps - N / 2) / L
1180 if level > 1:
1181 indices = np.arange(n).reshape(n, 1)
1182 s1 = np.sum(
1183 (-1) ** steps * Cotes[n, indices] * np.fft.fft(
1184 (-1)**steps * cf(-L + h * steps + h * indices / (n - 1))
1185 ) * np.exp(
1186 1j * np.pi * indices / (n - 1)
1187 - 2 * 1j * np.pi * indices * steps /
1188 (N * (n - 1))
1189 ),
1190 axis=0
1191 )
1192 else:
1193 s1 = (-1) ** steps * Cotes[n, 0] * np.fft.fft(
1194 (-1) ** steps * cf(-L + h * steps)
1195 )
1196 density = h * s1 / (2 * np.pi * np.sum(Cotes[n]))
1197 return (x_l, density)
1200levy_stable = levy_stable_gen(name="levy_stable")