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