Skip to content

Commit 43e66a0

Browse files
lucianopaztwiecki
authored andcommitted
Move math out of distribution random methods (#3509)
* Changed Triangular, TruncatedNormal, Rice and ZeroInflatedNegativeBinomial random method. The math operations between the distribution parameters was moved to _random, and all parameter broadcasting is handled in generate_samples. Added tests. * Added release notes * Fixed domain error in TruncatedNormal test
1 parent 2d506e1 commit 43e66a0

File tree

4 files changed

+238
-17
lines changed

4 files changed

+238
-17
lines changed

RELEASE-NOTES.md

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
### New features
66
- Distinguish between `Data` and `Deterministic` variables when graphing models with graphviz. PR [#3491](https://github.com/pymc-defs/pymc3/pulls/3491).
77

8+
### Maintenance
9+
- Moved math operations out of `Rice`, `TruncatedNormal`, `Triangular` and `ZeroInflatedNegativeBinomial` `random` methods. Math operations on values returned by `draw_values` might not broadcast well, and all the `size` aware broadcasting is left to `generate_samples`. Fixes [#3481](https://github.com/pymc-devs/pymc3/issues/3481) and [#3508](https://github.com/pymc-devs/pymc3/issues/3508)
10+
811
## PyMC3 3.7 (May 29 2019)
912

1013
### New features

pymc3/distributions/continuous.py

+56-14
Original file line numberDiff line numberDiff line change
@@ -664,16 +664,34 @@ def random(self, point=None, size=None):
664664
-------
665665
array
666666
"""
667-
mu_v, std_v, a_v, b_v = draw_values(
668-
[self.mu, self.sigma, self.lower, self.upper], point=point, size=size)
669-
return generate_samples(stats.truncnorm.rvs,
670-
a=(a_v - mu_v)/std_v,
671-
b=(b_v - mu_v) / std_v,
672-
loc=mu_v,
673-
scale=std_v,
674-
dist_shape=self.shape,
675-
size=size,
676-
)
667+
mu, sigma, lower, upper = draw_values(
668+
[self.mu, self.sigma, self.lower, self.upper],
669+
point=point,
670+
size=size
671+
)
672+
return generate_samples(
673+
self._random,
674+
mu=mu,
675+
sigma=sigma,
676+
lower=lower,
677+
upper=upper,
678+
dist_shape=self.shape,
679+
size=size,
680+
)
681+
682+
def _random(self, mu, sigma, lower, upper, size):
683+
""" Wrapper around stats.truncnorm.rvs that converts TruncatedNormal's
684+
parametrization to scipy.truncnorm. All parameter arrays should have
685+
been broadcasted properly by generate_samples at this point and size is
686+
the scipy.rvs representation.
687+
"""
688+
return stats.truncnorm.rvs(
689+
a=(lower - mu) / sigma,
690+
b=(upper - mu) / sigma,
691+
loc=mu,
692+
scale=sigma,
693+
size=size,
694+
)
677695

678696
def logp(self, value):
679697
"""
@@ -3582,11 +3600,23 @@ def random(self, point=None, size=None):
35823600
"""
35833601
c, lower, upper = draw_values([self.c, self.lower, self.upper],
35843602
point=point, size=size)
3585-
scale = upper - lower
3586-
c_ = (c - lower) / scale
3587-
return generate_samples(stats.triang.rvs, c=c_, loc=lower, scale=scale,
3603+
return generate_samples(self._random, c=c, lower=lower, upper=upper,
35883604
size=size, dist_shape=self.shape)
35893605

3606+
def _random(self, c, lower, upper, size):
3607+
""" Wrapper around stats.triang.rvs that converts Triangular's
3608+
parametrization to scipy.triang. All parameter arrays should have
3609+
been broadcasted properly by generate_samples at this point and size is
3610+
the scipy.rvs representation.
3611+
"""
3612+
scale = upper - lower
3613+
return stats.triang.rvs(
3614+
c=(c - lower) / scale,
3615+
loc=lower,
3616+
scale=scale,
3617+
size=size,
3618+
)
3619+
35903620
def logp(self, value):
35913621
"""
35923622
Calculate log-probability of Triangular distribution at specified value.
@@ -3875,9 +3905,21 @@ def random(self, point=None, size=None):
38753905
"""
38763906
nu, sigma = draw_values([self.nu, self.sigma],
38773907
point=point, size=size)
3878-
return generate_samples(stats.rice.rvs, b=nu / sigma, scale=sigma, loc=0,
3908+
return generate_samples(self._random, nu=nu, sigma=sigma,
38793909
dist_shape=self.shape, size=size)
38803910

3911+
def _random(self, nu, sigma, size):
3912+
""" Wrapper around stats.rice.rvs that converts Rice's
3913+
parametrization to scipy.rice. All parameter arrays should have
3914+
been broadcasted properly by generate_samples at this point and size is
3915+
the scipy.rvs representation.
3916+
"""
3917+
return stats.rice.rvs(
3918+
b=nu / sigma,
3919+
scale=sigma,
3920+
size=size,
3921+
)
3922+
38813923
def logp(self, value):
38823924
"""
38833925
Calculate log-probability of Rice distribution at specified value.

pymc3/distributions/discrete.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -1427,13 +1427,29 @@ def random(self, point=None, size=None):
14271427
"""
14281428
mu, alpha, psi = draw_values(
14291429
[self.mu, self.alpha, self.psi], point=point, size=size)
1430-
g = generate_samples(stats.gamma.rvs, alpha, scale=mu / alpha,
1431-
dist_shape=self.shape,
1432-
size=size)
1430+
g = generate_samples(
1431+
self._random,
1432+
mu=mu,
1433+
alpha=alpha,
1434+
dist_shape=self.shape,
1435+
size=size
1436+
)
14331437
g[g == 0] = np.finfo(float).eps # Just in case
14341438
g, psi = broadcast_distribution_samples([g, psi], size=size)
14351439
return stats.poisson.rvs(g) * (np.random.random(g.shape) < psi)
14361440

1441+
def _random(self, mu, alpha, size):
1442+
""" Wrapper around stats.gamma.rvs that converts NegativeBinomial's
1443+
parametrization to scipy.gamma. All parameter arrays should have
1444+
been broadcasted properly by generate_samples at this point and size is
1445+
the scipy.rvs representation.
1446+
"""
1447+
return stats.gamma.rvs(
1448+
a=alpha,
1449+
scale=mu / alpha,
1450+
size=size,
1451+
)
1452+
14371453
def logp(self, value):
14381454
"""
14391455
Calculate log-probability of ZeroInflatedNegativeBinomial distribution at specified value.

pymc3/tests/test_distributions_random.py

+160
Original file line numberDiff line numberDiff line change
@@ -944,3 +944,163 @@ def test_density_dist_without_random_not_sampleable():
944944
samples = 500
945945
with pytest.raises(ValueError):
946946
pm.sample_posterior_predictive(trace, samples=samples, model=model, size=100)
947+
948+
949+
class TestNestedRandom(SeededTest):
950+
def build_model(self, distribution, shape, nested_rvs_info):
951+
with pm.Model() as model:
952+
nested_rvs = {}
953+
for rv_name, info in nested_rvs_info.items():
954+
try:
955+
value, nested_shape = info
956+
loc = 0.
957+
except ValueError:
958+
value, nested_shape, loc = info
959+
if value is None:
960+
nested_rvs[rv_name] = pm.Uniform(
961+
rv_name,
962+
0 + loc,
963+
1 + loc,
964+
shape=nested_shape,
965+
)
966+
else:
967+
nested_rvs[rv_name] = value * np.ones(nested_shape)
968+
rv = distribution(
969+
"target",
970+
shape=shape,
971+
**nested_rvs,
972+
)
973+
return model, rv, nested_rvs
974+
975+
def sample_prior(
976+
self,
977+
distribution,
978+
shape,
979+
nested_rvs_info,
980+
prior_samples
981+
):
982+
model, rv, nested_rvs = self.build_model(
983+
distribution,
984+
shape,
985+
nested_rvs_info,
986+
)
987+
with model:
988+
return pm.sample_prior_predictive(prior_samples)
989+
990+
@pytest.mark.parametrize(
991+
["prior_samples", "shape", "psi", "mu", "alpha"],
992+
[
993+
[10, (3,), (0.5, tuple()), (None, tuple()), (None, (3,))],
994+
[10, (3,), (0.5, (3,)), (None, tuple()), (None, (3,))],
995+
[10, (3,), (0.5, tuple()), (None, (3,)), (None, tuple())],
996+
[10, (3,), (0.5, (3,)), (None, (3,)), (None, tuple())],
997+
[10, (4, 3,), (0.5, (3,)), (None, (3,)), (None, (3,))],
998+
[10, (4, 3,), (0.5, (3,)), (None, (3,)), (None, (4, 3))],
999+
],
1000+
ids=str,
1001+
)
1002+
def test_ZeroInflatedNegativeBinomial(
1003+
self,
1004+
prior_samples,
1005+
shape,
1006+
psi,
1007+
mu,
1008+
alpha,
1009+
):
1010+
prior = self.sample_prior(
1011+
distribution=pm.ZeroInflatedNegativeBinomial,
1012+
shape=shape,
1013+
nested_rvs_info=dict(psi=psi, mu=mu, alpha=alpha),
1014+
prior_samples=prior_samples,
1015+
)
1016+
assert prior["target"].shape == (prior_samples,) + shape
1017+
1018+
@pytest.mark.parametrize(
1019+
["prior_samples", "shape", "nu", "sigma"],
1020+
[
1021+
[10, (3,), (None, tuple()), (None, (3,))],
1022+
[10, (3,), (None, tuple()), (None, (3,))],
1023+
[10, (3,), (None, (3,)), (None, tuple())],
1024+
[10, (3,), (None, (3,)), (None, tuple())],
1025+
[10, (4, 3,), (None, (3,)), (None, (3,))],
1026+
[10, (4, 3,), (None, (3,)), (None, (4, 3))],
1027+
],
1028+
ids=str,
1029+
)
1030+
def test_Rice(
1031+
self,
1032+
prior_samples,
1033+
shape,
1034+
nu,
1035+
sigma,
1036+
):
1037+
prior = self.sample_prior(
1038+
distribution=pm.Rice,
1039+
shape=shape,
1040+
nested_rvs_info=dict(nu=nu, sigma=sigma),
1041+
prior_samples=prior_samples,
1042+
)
1043+
assert prior["target"].shape == (prior_samples,) + shape
1044+
1045+
@pytest.mark.parametrize(
1046+
["prior_samples", "shape", "mu", "sigma", "lower", "upper"],
1047+
[
1048+
[10, (3,), (None, tuple()), (1., tuple()), (None, tuple(), -1), (None, (3,))],
1049+
[10, (3,), (None, tuple()), (1., tuple()), (None, tuple(), -1), (None, (3,))],
1050+
[10, (3,), (None, tuple()), (1., tuple()), (None, (3,), -1), (None, tuple())],
1051+
[10, (3,), (None, tuple()), (1., tuple()), (None, (3,), -1), (None, tuple())],
1052+
[10, (4, 3,), (None, (3,)), (1., tuple()), (None, (3,), -1), (None, (3,))],
1053+
[10, (4, 3,), (None, (3,)), (1., tuple()), (None, (3,), -1), (None, (4, 3))],
1054+
[10, (3,), (0., tuple()), (None, tuple()), (None, tuple(), -1), (None, (3,))],
1055+
[10, (3,), (0., tuple()), (None, tuple()), (None, tuple(), -1), (None, (3,))],
1056+
[10, (3,), (0., tuple()), (None, tuple()), (None, (3,), -1), (None, tuple())],
1057+
[10, (3,), (0., tuple()), (None, tuple()), (None, (3,), -1), (None, tuple())],
1058+
[10, (4, 3,), (0., tuple()), (None, (3,)), (None, (3,), -1), (None, (3,))],
1059+
[10, (4, 3,), (0., tuple()), (None, (3,)), (None, (3,), -1), (None, (4, 3))],
1060+
],
1061+
ids=str,
1062+
)
1063+
def test_TruncatedNormal(
1064+
self,
1065+
prior_samples,
1066+
shape,
1067+
mu,
1068+
sigma,
1069+
lower,
1070+
upper,
1071+
):
1072+
prior = self.sample_prior(
1073+
distribution=pm.TruncatedNormal,
1074+
shape=shape,
1075+
nested_rvs_info=dict(mu=mu, sigma=sigma, lower=lower, upper=upper),
1076+
prior_samples=prior_samples,
1077+
)
1078+
assert prior["target"].shape == (prior_samples,) + shape
1079+
1080+
1081+
@pytest.mark.parametrize(
1082+
["prior_samples", "shape", "c", "lower", "upper"],
1083+
[
1084+
[10, (3,), (None, tuple()), (-1., (3,)), (2, tuple())],
1085+
[10, (3,), (None, tuple()), (-1., tuple()), (None, tuple(), 1)],
1086+
[10, (3,), (None, (3,)), (-1., tuple()), (None, tuple(), 1)],
1087+
[10, (4, 3,), (None, (3,)), (-1., tuple()), (None, (3,), 1)],
1088+
[10, (4, 3,), (None, (3,)), (None, tuple(), -1), (None, (3,), 1)],
1089+
],
1090+
ids=str,
1091+
)
1092+
def test_Triangular(
1093+
self,
1094+
prior_samples,
1095+
shape,
1096+
c,
1097+
lower,
1098+
upper,
1099+
):
1100+
prior = self.sample_prior(
1101+
distribution=pm.Triangular,
1102+
shape=shape,
1103+
nested_rvs_info=dict(c=c, lower=lower, upper=upper),
1104+
prior_samples=prior_samples,
1105+
)
1106+
assert prior["target"].shape == (prior_samples,) + shape

0 commit comments

Comments
 (0)