__init__.py 43 KB

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