Skip to content

Commit fc71f68

Browse files
aloctavodiatwiecki
authored andcommitted
add skew-normal distribution and tests (#1392)
* add skew-normal distribution and tests * replace the order of tau and sd to make it match the calling signature
1 parent 1962645 commit fc71f68

File tree

4 files changed

+89
-5
lines changed

4 files changed

+89
-5
lines changed

pymc3/distributions/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from .continuous import InverseGamma
2424
from .continuous import ExGaussian
2525
from .continuous import VonMises
26+
from .continuous import SkewNormal
2627

2728
from .discrete import Binomial
2829
from .discrete import BetaBinomial
@@ -110,5 +111,6 @@
110111
'LKJCorr',
111112
'AR1',
112113
'GaussianRandomWalk',
113-
'GARCH11'
114+
'GARCH11',
115+
'SkewNormal'
114116
]

pymc3/distributions/continuous.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818

1919
__all__ = ['Uniform', 'Flat', 'Normal', 'Beta', 'Exponential', 'Laplace',
2020
'StudentT', 'Cauchy', 'HalfCauchy', 'Gamma', 'Weibull',
21-
'Bound', 'HalfStudentT', 'StudentTpos', 'Lognormal', 'ChiSquared', 'HalfNormal',
22-
'Wald', 'Pareto', 'InverseGamma', 'ExGaussian', 'VonMises']
21+
'Bound', 'HalfStudentT', 'StudentTpos', 'Lognormal', 'ChiSquared',
22+
'HalfNormal', 'Wald', 'Pareto', 'InverseGamma', 'ExGaussian',
23+
'VonMises', 'SkewNormal']
2324

2425

2526
class PositiveContinuous(Continuous):
@@ -1237,3 +1238,66 @@ def logp(self, value):
12371238
mu = self.mu
12381239
kappa = self.kappa
12391240
return bound(kappa * tt.cos(mu - value) - tt.log(2 * np.pi * i0(kappa)), value >= -np.pi, value <= np.pi, kappa >= 0)
1241+
1242+
1243+
class SkewNormal(Continuous):
1244+
R"""
1245+
Univariate skew-normal log-likelihood.
1246+
.. math::
1247+
f(x \mid \mu, \tau, \alpha) =
1248+
2 \Phi((x-\mu)\sqrt{\tau}\alpha) \phi(x,\mu,\tau)
1249+
======== ==========================================
1250+
Support :math:`x \in \mathbb{R}`
1251+
Mean :math:`\mu + \sigma \sqrt{\frac{2}{\pi}} \frac {\alpha }{{\sqrt {1+\alpha ^{2}}}}`
1252+
Variance :math:`\sigma^2 \left( 1-\frac{2\alpha^2}{(\alpha^2+1) \pi} \right)`
1253+
======== ==========================================
1254+
Skew-normal distribution can be parameterized either in terms of precision
1255+
or standard deviation. The link between the two parametrizations is
1256+
given by
1257+
.. math::
1258+
\tau = \dfrac{1}{\sigma^2}
1259+
Parameters
1260+
----------
1261+
mu : float
1262+
Location parameter.
1263+
sd : float
1264+
Scale parameter (sd > 0).
1265+
tau : float
1266+
Alternative scale parameter (tau > 0).
1267+
alpha : float
1268+
Skewness parameter.
1269+
1270+
Notes
1271+
-----
1272+
When alpha=0 we recover the Normal distribution and mu becomes the mean,
1273+
tau the precision and sd the standard deviation. In the limit of alpha
1274+
approaching plus/minus infinite we get a half-normal distribution.
1275+
1276+
"""
1277+
def __init__(self, mu=0.0, sd=None, tau=None, alpha=1, *args, **kwargs):
1278+
super(SkewNormal, self).__init__(*args, **kwargs)
1279+
self.mu = mu
1280+
self.tau, self.sd = get_tau_sd(tau=tau, sd=sd)
1281+
self.alpha = alpha
1282+
self.mean = mu + self.sd * (2 / np.pi)**0.5 * alpha / (1 + alpha**2)**0.5
1283+
self.variance = self.sd**2 * (1 - (2 * alpha**2) / ((1 + alpha**2) * np.pi))
1284+
1285+
def random(self, point=None, size=None, repeat=None):
1286+
mu, tau, sd, alpha = draw_values(
1287+
[self.mu, self.tau, self.sd, self.alpha], point=point)
1288+
return generate_samples(stats.skewnorm.rvs,
1289+
a=alpha, loc=mu, scale=tau**-0.5,
1290+
dist_shape=self.shape,
1291+
size=size)
1292+
1293+
def logp(self, value):
1294+
tau = self.tau
1295+
sd = self.sd
1296+
mu = self.mu
1297+
alpha = self.alpha
1298+
return bound(
1299+
tt.log(1 +
1300+
tt.erf(((value - mu) * tt.sqrt(tau) * alpha) / tt.sqrt(2)))
1301+
+ (-tau * (value - mu)**2
1302+
+ tt.log(tau / np.pi / 2.)) / 2.,
1303+
tau > 0, sd > 0)

pymc3/tests/test_distributions.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
Gamma, Cauchy, HalfCauchy, Lognormal, Laplace, NegativeBinomial,
1313
Geometric, Exponential, ExGaussian, Normal, Flat, LKJCorr, Wald,
1414
ChiSquared, HalfNormal, DiscreteUniform, Bound, Uniform,
15-
Binomial, Wishart)
15+
Binomial, Wishart, SkewNormal)
1616
from ..distributions import continuous
1717
from numpy import array, inf, log, exp
1818
from numpy.testing import assert_almost_equal
@@ -457,6 +457,10 @@ def test_student_tpos(self):
457457
self.pymc3_matches_scipy(HalfStudentT, Rplus, {'nu': Rplus, 'mu': R, 'lam': Rplus},
458458
lambda value, nu, mu, lam: sp.t.logpdf(value, nu, mu, lam**-.5))
459459

460+
def test_skew_normal(self):
461+
self.pymc3_matches_scipy(SkewNormal, R, {'mu': R, 'sd': Rplusbig, 'alpha': R},
462+
lambda value, alpha, mu, sd: sp.skewnorm.logpdf(value, alpha, mu, sd))
463+
460464
def test_binomial(self):
461465
self.pymc3_matches_scipy(Binomial, Nat, {'n': NatSmall, 'p': Unit},
462466
lambda value, n, p: sp.binom.logpmf(value, n, p))

pymc3/tests/test_distributions_random.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
ZeroInflatedNegativeBinomial, ConstantDist, Poisson, Bernoulli, Beta,
1313
BetaBinomial, StudentT, Weibull, Pareto, InverseGamma, Gamma, Cauchy,
1414
HalfCauchy, Lognormal, Laplace, NegativeBinomial, Geometric,
15-
Exponential, ExGaussian, Normal, Flat, Wald, ChiSquared,
15+
Exponential, ExGaussian, Normal, SkewNormal, Flat, Wald, ChiSquared,
1616
HalfNormal, DiscreteUniform, Bound, Uniform, Binomial, draw_values)
1717
from ..model import Model, Point
1818

@@ -257,6 +257,9 @@ def check(self, dist, **kwargs):
257257

258258
def test_normal(self):
259259
self.check(Normal, mu=0., tau=1.)
260+
261+
def test_skew_normal(self):
262+
self.check(SkewNormal, mu=0., sd=1., alpha=5.)
260263

261264
def test_uniform(self):
262265
self.check(Uniform, lower=0., upper=1.)
@@ -359,6 +362,9 @@ def check(self, dist, **kwargs):
359362

360363
def test_normal(self):
361364
self.check(Normal, mu=self.zeros, tau=self.ones)
365+
366+
def test_skew_normal(self):
367+
self.check(SkewNormal, mu=self.zeros, sd=self.ones, alpha=self.ones * 5)
362368

363369
def test_uniform(self):
364370
self.check(Uniform, lower=self.zeros, upper=self.ones)
@@ -475,6 +481,9 @@ def check(self, dist, **kwargs):
475481

476482
def test_normal(self):
477483
self.check(Normal, mu=self.zeros, tau=self.ones)
484+
485+
def test_skew_normal(self):
486+
self.check(SkewNormal, mu=self.zeros, sd=self.ones, alpha=self.ones * 5)
478487

479488
def test_uniform(self):
480489
self.check(Uniform, lower=self.zeros, upper=self.ones)
@@ -593,6 +602,11 @@ def ref_rand(size, mu, sd):
593602
return st.norm.rvs(size=size, loc=mu, scale=sd)
594603
pymc3_random(Normal, {'mu': R, 'sd': Rplus}, ref_rand=ref_rand)
595604

605+
def test_skew_normal(self):
606+
def ref_rand(size, alpha, mu, sd):
607+
return st.skewnorm.rvs(size=size, a=alpha, loc=mu, scale=sd)
608+
pymc3_random(SkewNormal, {'mu': R, 'sd': Rplus, 'alpha': R}, ref_rand=ref_rand)
609+
596610
def test_half_normal(self):
597611
def ref_rand(size, tau):
598612
return st.halfnorm.rvs(size=size, loc=0, scale=tau ** -0.5)

0 commit comments

Comments
 (0)