From 95a9d6d636388edfae9efd61c6ae63ad1c13f552 Mon Sep 17 00:00:00 2001 From: Lucca Zenobio Date: Sat, 15 May 2021 23:02:07 -0300 Subject: [PATCH 1/6] initial --- pymc3/distributions/continuous.py | 56 ++++++++++-------------- pymc3/tests/test_distributions.py | 2 +- pymc3/tests/test_distributions_random.py | 23 +++++++--- 3 files changed, 40 insertions(+), 41 deletions(-) diff --git a/pymc3/distributions/continuous.py b/pymc3/distributions/continuous.py index d2d12fc1e5..293412d8bc 100644 --- a/pymc3/distributions/continuous.py +++ b/pymc3/distributions/continuous.py @@ -3726,6 +3726,18 @@ def logcdf(value, mu, s): 0 < s, ) +class LogitNormalRV(RandomVariable): + name = "logit_normal" + ndim_supp = 0 + ndims_params = [0, 0] + dtype = "floatX" + _print_name = ("logitNormal", "\\operatorname{logitNormal}") + + @classmethod + def rng_fn(cls, rng, mu, sigma, size=None): + return invlogit(stats.norm.rvs(loc=mu, scale=sigma, size=size, random_state=rng)) + +logit_normal = LogitNormalRV() class LogitNormal(UnitContinuous): r""" @@ -3771,44 +3783,22 @@ class LogitNormal(UnitContinuous): tau: float Scale parameter (tau > 0). """ + rv_op = logit_normal - def __init__(self, mu=0, sigma=None, tau=None, sd=None, **kwargs): + @classmethod + def dist(cls, mu=0, sigma=None, tau=None, sd=None, **kwargs): if sd is not None: sigma = sd - self.mu = mu = at.as_tensor_variable(floatX(mu)) + mu = at.as_tensor_variable(floatX(mu)) tau, sigma = get_tau_sigma(tau=tau, sigma=sigma) - self.sigma = self.sd = at.as_tensor_variable(sigma) - self.tau = tau = at.as_tensor_variable(tau) - - self.median = invlogit(mu) + sigma = sd = at.as_tensor_variable(sigma) + tau = at.as_tensor_variable(tau) assert_negative_support(sigma, "sigma", "LogitNormal") assert_negative_support(tau, "tau", "LogitNormal") - super().__init__(**kwargs) - - def random(self, point=None, size=None): - """ - Draw random values from LogitNormal distribution. - - Parameters - ---------- - point: dict, optional - Dict of variable values on which random values are to be - conditioned (uses default point if not specified). - size: int, optional - Desired size of random sample (returns one sample if not - specified). - - Returns - ------- - array - """ - # mu, _, sigma = draw_values([self.mu, self.tau, self.sigma], point=point, size=size) - # return expit( - # generate_samples(stats.norm.rvs, loc=mu, scale=sigma, dist_shape=self.shape, size=size) - # ) + return super().dist([mu, sigma], **kwargs) - def logp(self, value): + def logp(value, mu, sigma): """ Calculate log-probability of LogitNormal distribution at specified value. @@ -3822,8 +3812,7 @@ def logp(self, value): ------- TensorVariable """ - mu = self.mu - tau = self.tau + tau, sigma = get_tau_sigma(sigma=sigma) return bound( -0.5 * tau * (logit(value) - mu) ** 2 + 0.5 * at.log(tau / (2.0 * np.pi)) @@ -3833,9 +3822,8 @@ def logp(self, value): tau > 0, ) - def _distr_parameters_for_repr(self): - return ["mu", "sigma"] + class Interpolated(BoundedContinuous): r""" diff --git a/pymc3/tests/test_distributions.py b/pymc3/tests/test_distributions.py index a756281fdd..66379d35de 100644 --- a/pymc3/tests/test_distributions.py +++ b/pymc3/tests/test_distributions.py @@ -2515,7 +2515,7 @@ def test_logistic(self): decimal=select_by_precision(float64=6, float32=1), ) - @pytest.mark.xfail(reason="Distribution not refactored yet") + def test_logitnormal(self): self.check_logp( LogitNormal, diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 2759f2d3b5..4dcc7df1f2 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -15,6 +15,7 @@ import itertools from contextlib import ExitStack as does_not_raise +from pymc3.math import invlogit from typing import Callable, List, Optional import aesara @@ -301,11 +302,6 @@ class TestExGaussian(BaseTestCases.BaseTestCase): params = {"mu": 0.0, "sigma": 1.0, "nu": 1.0} -@pytest.mark.xfail(reason="This distribution has not been refactored for v4") -class TestLogitNormal(BaseTestCases.BaseTestCase): - distribution = pm.LogitNormal - params = {"mu": 0.0, "sigma": 1.0} - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") class TestZeroInflatedNegativeBinomial(BaseTestCases.BaseTestCase): @@ -513,6 +509,22 @@ class TestNormal(BaseTestDistribution): "check_pymc_draws_match_reference", "check_rv_size", ] +class TestLogitNormal(BaseTestDistribution): + def logit_normal_rng_fn(self, rng, size, loc, scale): + return expit(st.norm.rvs(loc=loc, scale=scale, size=size, random_state=rng)) + + pymc_dist = pm.LogitNormal + pymc_dist_params = {"mu": 5.0, "sigma": 10.0} + expected_rv_op_params = {"mu": 5.0, "sigma": 10.0} + reference_dist_params = {"loc": 5.0, "scale": 10.0} + reference_dist = lambda self: functools.partial( + self.logit_normal_rng_fn, rng=self.get_random_state() + ) + tests_to_run = [ + "check_pymc_params_match_rv_op", + "check_pymc_draws_match_reference", + "check_rv_size", + ] class TestNormalTau(BaseTestDistribution): @@ -1384,7 +1396,6 @@ def test_dirichlet_multinomial_dist_ShapeError(self, n, a, shape, expectation): with expectation: m.random() - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") def test_logitnormal(self): def ref_rand(size, mu, sigma): return expit(st.norm.rvs(loc=mu, scale=sigma, size=size)) From e71c6d38ffbeab1eecd7e302c58d2e0ce21c753e Mon Sep 17 00:00:00 2001 From: Lucca Zenobio Date: Sat, 15 May 2021 23:08:43 -0300 Subject: [PATCH 2/6] fix linte --- pymc3/tests/test_distributions_random.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 4dcc7df1f2..d226886247 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -509,6 +509,7 @@ class TestNormal(BaseTestDistribution): "check_pymc_draws_match_reference", "check_rv_size", ] + class TestLogitNormal(BaseTestDistribution): def logit_normal_rng_fn(self, rng, size, loc, scale): return expit(st.norm.rvs(loc=loc, scale=scale, size=size, random_state=rng)) @@ -1037,6 +1038,7 @@ def seeded_zero_inflated_binomial_rng_fn(self): ] + class TestZeroInflatedNegativeBinomial(BaseTestDistribution): def zero_inflated_negbinomial_rng_fn( self, size, psi, n, p, negbinomial_rng_fct, random_rng_fct From 5782426d20bba3347f90f4a0bbe1ca95af5f1f80 Mon Sep 17 00:00:00 2001 From: Lucca Zenobio Date: Sat, 15 May 2021 23:13:08 -0300 Subject: [PATCH 3/6] fix: remvove invlogit --- pymc3/tests/test_distributions_random.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index d226886247..6764f88cc3 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -15,7 +15,6 @@ import itertools from contextlib import ExitStack as does_not_raise -from pymc3.math import invlogit from typing import Callable, List, Optional import aesara @@ -1038,7 +1037,6 @@ def seeded_zero_inflated_binomial_rng_fn(self): ] - class TestZeroInflatedNegativeBinomial(BaseTestDistribution): def zero_inflated_negbinomial_rng_fn( self, size, psi, n, p, negbinomial_rng_fct, random_rng_fct From 7fb1d0424f3561272117e4905a6d951ab92faebe Mon Sep 17 00:00:00 2001 From: Lucca Zenobio Date: Sun, 16 May 2021 09:55:38 -0300 Subject: [PATCH 4/6] fixes for logitnormal --- pymc3/distributions/continuous.py | 10 ++++++---- pymc3/tests/test_distributions.py | 1 - pymc3/tests/test_distributions_random.py | 10 +++++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pymc3/distributions/continuous.py b/pymc3/distributions/continuous.py index 293412d8bc..5c407110ed 100644 --- a/pymc3/distributions/continuous.py +++ b/pymc3/distributions/continuous.py @@ -46,6 +46,7 @@ from aesara.tensor.random.op import RandomVariable from aesara.tensor.var import TensorVariable from scipy import stats +from scipy.special import expit from scipy.interpolate import InterpolatedUnivariateSpline from pymc3.aesaraf import floatX @@ -66,7 +67,7 @@ ) from pymc3.distributions.distribution import Continuous from pymc3.distributions.special import log_i0 -from pymc3.math import invlogit, log1mexp, log1pexp, logdiffexp, logit +from pymc3.math import log1mexp, log1pexp, logdiffexp, logit from pymc3.util import UNSET __all__ = [ @@ -3726,6 +3727,7 @@ def logcdf(value, mu, s): 0 < s, ) + class LogitNormalRV(RandomVariable): name = "logit_normal" ndim_supp = 0 @@ -3735,10 +3737,12 @@ class LogitNormalRV(RandomVariable): @classmethod def rng_fn(cls, rng, mu, sigma, size=None): - return invlogit(stats.norm.rvs(loc=mu, scale=sigma, size=size, random_state=rng)) + return expit(stats.norm.rvs(loc=mu, scale=sigma, size=size, random_state=rng)) + logit_normal = LogitNormalRV() + class LogitNormal(UnitContinuous): r""" Logit-Normal log-likelihood. @@ -3823,8 +3827,6 @@ def logp(value, mu, sigma): ) - - class Interpolated(BoundedContinuous): r""" Univariate probability distribution defined as a linear interpolation diff --git a/pymc3/tests/test_distributions.py b/pymc3/tests/test_distributions.py index 66379d35de..6da39b279c 100644 --- a/pymc3/tests/test_distributions.py +++ b/pymc3/tests/test_distributions.py @@ -2515,7 +2515,6 @@ def test_logistic(self): decimal=select_by_precision(float64=6, float32=1), ) - def test_logitnormal(self): self.check_logp( LogitNormal, diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 6764f88cc3..6c63ea3014 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -158,7 +158,7 @@ def setup_method(self, *args, **kwargs): self.model = pm.Model() def get_random_variable(self, shape, with_vector_params=False, name=None): - """ Creates a RandomVariable of the parametrized distribution. """ + """Creates a RandomVariable of the parametrized distribution.""" if with_vector_params: params = { key: value * np.ones(self.shape, dtype=np.dtype(type(value))) @@ -187,7 +187,7 @@ def get_random_variable(self, shape, with_vector_params=False, name=None): @staticmethod def sample_random_variable(random_variable, size): - """ Draws samples from a RandomVariable using its .random() method. """ + """Draws samples from a RandomVariable using its .random() method.""" if size is None: return random_variable.eval() else: @@ -196,7 +196,7 @@ def sample_random_variable(random_variable, size): @pytest.mark.parametrize("size", [None, (), 1, (1,), 5, (4, 5)], ids=str) @pytest.mark.parametrize("shape", [None, ()], ids=str) def test_scalar_distribution_shape(self, shape, size): - """ Draws samples of different [size] from a scalar [shape] RV. """ + """Draws samples of different [size] from a scalar [shape] RV.""" rv = self.get_random_variable(shape) exp_shape = self.default_shape if shape is None else tuple(np.atleast_1d(shape)) exp_size = self.default_size if size is None else tuple(np.atleast_1d(size)) @@ -216,7 +216,7 @@ def test_scalar_distribution_shape(self, shape, size): "shape", [None, (), (1,), (1, 1), (1, 2), (10, 11, 1), (9, 10, 2)], ids=str ) def test_scalar_sample_shape(self, shape, size): - """ Draws samples of scalar [size] from a [shape] RV. """ + """Draws samples of scalar [size] from a [shape] RV.""" rv = self.get_random_variable(shape) exp_shape = self.default_shape if shape is None else tuple(np.atleast_1d(shape)) exp_size = self.default_size if size is None else tuple(np.atleast_1d(size)) @@ -301,7 +301,6 @@ class TestExGaussian(BaseTestCases.BaseTestCase): params = {"mu": 0.0, "sigma": 1.0, "nu": 1.0} - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") class TestZeroInflatedNegativeBinomial(BaseTestCases.BaseTestCase): distribution = pm.ZeroInflatedNegativeBinomial @@ -509,6 +508,7 @@ class TestNormal(BaseTestDistribution): "check_rv_size", ] + class TestLogitNormal(BaseTestDistribution): def logit_normal_rng_fn(self, rng, size, loc, scale): return expit(st.norm.rvs(loc=loc, scale=scale, size=size, random_state=rng)) From c891f663bf5708bdd482d812dd0fb5764d2a28ff Mon Sep 17 00:00:00 2001 From: Lucca Zenobio Date: Sun, 16 May 2021 19:35:54 -0300 Subject: [PATCH 5/6] fixes for logitnormal --- pymc3/distributions/continuous.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymc3/distributions/continuous.py b/pymc3/distributions/continuous.py index 5c407110ed..4f2b3c255d 100644 --- a/pymc3/distributions/continuous.py +++ b/pymc3/distributions/continuous.py @@ -46,8 +46,8 @@ from aesara.tensor.random.op import RandomVariable from aesara.tensor.var import TensorVariable from scipy import stats -from scipy.special import expit from scipy.interpolate import InterpolatedUnivariateSpline +from scipy.special import expit from pymc3.aesaraf import floatX from pymc3.distributions import logp_transform, transforms From 15d42b97bbda2e2947f90da9debafaad33ba8992 Mon Sep 17 00:00:00 2001 From: Lucca Zenobio Date: Mon, 17 May 2021 09:39:05 -0300 Subject: [PATCH 6/6] tau test for logitnormal --- pymc3/tests/test_distributions_random.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 6c63ea3014..9a289535e9 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -527,6 +527,14 @@ def logit_normal_rng_fn(self, rng, size, loc, scale): ] +class TestLogitNormalTau(BaseTestDistribution): + pymc_dist = pm.LogitNormal + tau, sigma = get_tau_sigma(tau=25.0) + pymc_dist_params = {"mu": 1.0, "tau": tau} + expected_rv_op_params = {"mu": 1.0, "sigma": sigma} + tests_to_run = ["check_pymc_params_match_rv_op"] + + class TestNormalTau(BaseTestDistribution): pymc_dist = pm.Normal tau, sigma = get_tau_sigma(tau=25.0)