Skip to content

Commit 044c407

Browse files
authored
Asymmetric Laplace distribution added (#4392)
* asymmetric laplace distribution added * unit test for random function added * continuous.py updated * suggested changes added * updated * RELEASE-NOTES.md and continuous.rst updated
1 parent 34b05d8 commit 044c407

File tree

6 files changed

+138
-0
lines changed

6 files changed

+138
-0
lines changed

RELEASE-NOTES.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ It also brings some dreadfully awaited fixes, so be sure to go through the chang
2121
- `plot_posterior_predictive_glm` now works with `arviz.InferenceData` as well (see [#4234](https://github.com/pymc-devs/pymc3/pull/4234))
2222
- Add `logcdf` method to all univariate discrete distributions (see [#4387](https://github.com/pymc-devs/pymc3/pull/4387)).
2323
- Add `random` method to `MvGaussianRandomWalk` (see [#4388](https://github.com/pymc-devs/pymc3/pull/4388))
24+
- `AsymmetricLaplace` distribution added (see [#4392](https://github.com/pymc-devs/pymc3/pull/4392)).
2425

2526
### Maintenance
2627
- Fixed bug whereby partial traces returns after keyboard interrupt during parallel sampling had fewer draws than would've been available [#4318](https://github.com/pymc-devs/pymc3/pull/4318)

docs/source/api/distributions/continuous.rst

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Continuous
1616
Kumaraswamy
1717
Exponential
1818
Laplace
19+
AsymmetricLaplace
1920
StudentT
2021
HalfStudentT
2122
Cauchy

pymc3/distributions/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pymc3.distributions.bart import BART
1717
from pymc3.distributions.bound import Bound
1818
from pymc3.distributions.continuous import (
19+
AsymmetricLaplace,
1920
Beta,
2021
Cauchy,
2122
ChiSquared,
@@ -160,6 +161,7 @@
160161
"LKJCorr",
161162
"AR1",
162163
"AR",
164+
"AsymmetricLaplace",
163165
"GaussianRandomWalk",
164166
"MvGaussianRandomWalk",
165167
"MvStudentTRandomWalk",

pymc3/distributions/continuous.py

+101
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"Interpolated",
8181
"Rice",
8282
"Moyal",
83+
"AsymmetricLaplace",
8384
]
8485

8586

@@ -1661,6 +1662,106 @@ def logcdf(self, value):
16611662
)
16621663

16631664

1665+
class AsymmetricLaplace(Continuous):
1666+
r"""
1667+
Asymmetric-Laplace log-likelihood.
1668+
1669+
The pdf of this distribution is
1670+
1671+
.. math::
1672+
{f(x|\\b,\kappa,\mu) =
1673+
\left({\frac{\\b}{\kappa + 1/\kappa}}\right)\,e^{-(x-\mu)\\b\,s\kappa ^{s}}}
1674+
1675+
where
1676+
1677+
.. math::
1678+
1679+
s = sgn(x-\mu)
1680+
1681+
======== ========================
1682+
Support :math:`x \in \mathbb{R}`
1683+
Mean :math:`\mu-\frac{\\\kappa-1/\kappa}b`
1684+
Variance :math:`\frac{1+\kappa^{4}}{b^2\kappa^2 }`
1685+
======== ========================
1686+
1687+
Parameters
1688+
----------
1689+
b: float
1690+
Scale parameter (b > 0)
1691+
kappa: float
1692+
Symmetry parameter (kappa > 0)
1693+
mu: float
1694+
Location parameter
1695+
1696+
See Also:
1697+
--------
1698+
`Reference <https://en.wikipedia.org/wiki/Asymmetric_Laplace_distribution>`_
1699+
"""
1700+
1701+
def __init__(self, b, kappa, mu=0, *args, **kwargs):
1702+
self.b = tt.as_tensor_variable(floatX(b))
1703+
self.kappa = tt.as_tensor_variable(floatX(kappa))
1704+
self.mu = mu = tt.as_tensor_variable(floatX(mu))
1705+
1706+
self.mean = self.mu - (self.kappa - 1 / self.kappa) / b
1707+
self.variance = (1 + self.kappa ** 4) / (self.kappa ** 2 * self.b ** 2)
1708+
1709+
assert_negative_support(kappa, "kappa", "AsymmetricLaplace")
1710+
assert_negative_support(b, "b", "AsymmetricLaplace")
1711+
1712+
super().__init__(*args, **kwargs)
1713+
1714+
def _random(self, b, kappa, mu, size=None):
1715+
u = np.random.uniform(size=size)
1716+
switch = kappa ** 2 / (1 + kappa ** 2)
1717+
non_positive_x = mu + kappa * np.log(u * (1 / switch)) / b
1718+
positive_x = mu - np.log((1 - u) * (1 + kappa ** 2)) / (kappa * b)
1719+
draws = non_positive_x * (u <= switch) + positive_x * (u > switch)
1720+
return draws
1721+
1722+
def random(self, point=None, size=None):
1723+
"""
1724+
Draw random samples from this distribution, using the inverse CDF method.
1725+
1726+
Parameters
1727+
----------
1728+
point: dict, optional
1729+
Dict of variable values on which random values are to be
1730+
conditioned (uses default point if not specified).
1731+
size:int, optional
1732+
Desired size of random sample (returns one sample if not
1733+
specified).
1734+
1735+
Returns
1736+
-------
1737+
array
1738+
"""
1739+
b, kappa, mu = draw_values([self.b, self.kappa, self.mu], point=point, size=size)
1740+
return generate_samples(self._random, b, kappa, mu, dist_shape=self.shape, size=size)
1741+
1742+
def logp(self, value):
1743+
"""
1744+
Calculate log-probability of Asymmetric-Laplace distribution at specified value.
1745+
1746+
Parameters
1747+
----------
1748+
value: numeric
1749+
Value(s) for which log-probability is calculated. If the log probabilities for multiple
1750+
values are desired the values must be provided in a numpy array or theano tensor
1751+
1752+
Returns
1753+
-------
1754+
TensorVariable
1755+
"""
1756+
value = value - self.mu
1757+
return bound(
1758+
tt.log(self.b / (self.kappa + (self.kappa ** -1)))
1759+
+ (-value * self.b * tt.sgn(value) * (self.kappa ** tt.sgn(value))),
1760+
0 < self.b,
1761+
0 < self.kappa,
1762+
)
1763+
1764+
16641765
class Lognormal(PositiveContinuous):
16651766
r"""
16661767
Log-normal log-likelihood.

pymc3/tests/test_distributions.py

+17
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from pymc3.blocking import DictToVarBijection
3636
from pymc3.distributions import (
3737
AR1,
38+
AsymmetricLaplace,
3839
Bernoulli,
3940
Beta,
4041
BetaBinomial,
@@ -223,6 +224,14 @@ def build_model(distfam, valuedomain, vardomains, extra_args=None):
223224
return m
224225

225226

227+
def laplace_asymmetric_logpdf(value, kappa, b, mu):
228+
kapinv = 1 / kappa
229+
value = value - mu
230+
lPx = value * b * np.where(value >= 0, -kappa, kapinv)
231+
lPx += np.log(b / (kappa + kapinv))
232+
return lPx
233+
234+
226235
def integrate_nd(f, domain, shape, dtype):
227236
if shape == () or shape == (1,):
228237
if dtype in continuous_types:
@@ -991,6 +1000,14 @@ def test_laplace(self):
9911000
lambda value, mu, b: sp.laplace.logcdf(value, mu, b),
9921001
)
9931002

1003+
def test_laplace_asymmetric(self):
1004+
self.pymc3_matches_scipy(
1005+
AsymmetricLaplace,
1006+
R,
1007+
{"b": Rplus, "kappa": Rplus, "mu": R},
1008+
laplace_asymmetric_logpdf,
1009+
)
1010+
9941011
def test_lognormal(self):
9951012
self.pymc3_matches_scipy(
9961013
Lognormal,

pymc3/tests/test_distributions_random.py

+16
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,11 @@ class TestLaplace(BaseTestCases.BaseTestCase):
375375
params = {"mu": 1.0, "b": 1.0}
376376

377377

378+
class TestAsymmetricLaplace(BaseTestCases.BaseTestCase):
379+
distribution = pm.AsymmetricLaplace
380+
params = {"kappa": 1.0, "b": 1.0, "mu": 0.0}
381+
382+
378383
class TestLognormal(BaseTestCases.BaseTestCase):
379384
distribution = pm.Lognormal
380385
params = {"mu": 1.0, "tau": 1.0}
@@ -626,6 +631,17 @@ def ref_rand(size, mu, b):
626631

627632
pymc3_random(pm.Laplace, {"mu": R, "b": Rplus}, ref_rand=ref_rand)
628633

634+
def test_laplace_asymmetric(self):
635+
def ref_rand(size, kappa, b, mu):
636+
u = np.random.uniform(size=size)
637+
switch = kappa ** 2 / (1 + kappa ** 2)
638+
non_positive_x = mu + kappa * np.log(u * (1 / switch)) / b
639+
positive_x = mu - np.log((1 - u) * (1 + kappa ** 2)) / (kappa * b)
640+
draws = non_positive_x * (u <= switch) + positive_x * (u > switch)
641+
return draws
642+
643+
pymc3_random(pm.AsymmetricLaplace, {"b": Rplus, "kappa": Rplus, "mu": R}, ref_rand=ref_rand)
644+
629645
def test_lognormal(self):
630646
def ref_rand(size, mu, tau):
631647
return np.exp(mu + (tau ** -0.5) * st.norm.rvs(loc=0.0, scale=1.0, size=size))

0 commit comments

Comments
 (0)