Skip to content

Commit d848b9c

Browse files
refactor kumaraswamy (#4706)
* refactor kumaraswamy * add logcdf method Co-authored-by: Farhan Reynaldo <[email protected]> Co-authored-by: Ricardo <[email protected]>
1 parent 1a2da3d commit d848b9c

File tree

4 files changed

+80
-47
lines changed

4 files changed

+80
-47
lines changed

RELEASE-NOTES.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
### New Features
1111
- The `CAR` distribution has been added to allow for use of conditional autoregressions which often are used in spatial and network models.
12+
- Add `logcdf` method to Kumaraswamy distribution (see [#4706](https://github.com/pymc-devs/pymc3/pull/4706)).
1213
- ...
1314

1415
### Maintenance

pymc3/distributions/continuous.py

+41-38
Original file line numberDiff line numberDiff line change
@@ -1271,6 +1271,22 @@ def logcdf(value, alpha, beta):
12711271
)
12721272

12731273

1274+
class KumaraswamyRV(RandomVariable):
1275+
name = "kumaraswamy"
1276+
ndim_supp = 0
1277+
ndims_params = [0, 0]
1278+
dtype = "floatX"
1279+
_print_name = ("Kumaraswamy", "\\operatorname{Kumaraswamy}")
1280+
1281+
@classmethod
1282+
def rng_fn(cls, rng, a, b, size):
1283+
u = rng.uniform(size=size)
1284+
return (1 - (1 - u) ** (1 / b)) ** (1 / a)
1285+
1286+
1287+
kumaraswamy = KumaraswamyRV()
1288+
1289+
12741290
class Kumaraswamy(UnitContinuous):
12751291
r"""
12761292
Kumaraswamy log-likelihood.
@@ -1313,67 +1329,54 @@ class Kumaraswamy(UnitContinuous):
13131329
b: float
13141330
b > 0.
13151331
"""
1332+
rv_op = kumaraswamy
13161333

1317-
def __init__(self, a, b, *args, **kwargs):
1318-
super().__init__(*args, **kwargs)
1319-
1320-
self.a = a = at.as_tensor_variable(floatX(a))
1321-
self.b = b = at.as_tensor_variable(floatX(b))
1322-
1323-
ln_mean = at.log(b) + at.gammaln(1 + 1 / a) + at.gammaln(b) - at.gammaln(1 + 1 / a + b)
1324-
self.mean = at.exp(ln_mean)
1325-
ln_2nd_raw_moment = (
1326-
at.log(b) + at.gammaln(1 + 2 / a) + at.gammaln(b) - at.gammaln(1 + 2 / a + b)
1327-
)
1328-
self.variance = at.exp(ln_2nd_raw_moment) - self.mean ** 2
1334+
@classmethod
1335+
def dist(cls, a, b, *args, **kwargs):
1336+
a = at.as_tensor_variable(floatX(a))
1337+
b = at.as_tensor_variable(floatX(b))
13291338

13301339
assert_negative_support(a, "a", "Kumaraswamy")
13311340
assert_negative_support(b, "b", "Kumaraswamy")
13321341

1333-
def _random(self, a, b, size=None):
1334-
u = np.random.uniform(size=size)
1335-
return (1 - (1 - u) ** (1 / b)) ** (1 / a)
1342+
return super().dist([a, b], *args, **kwargs)
13361343

1337-
def random(self, point=None, size=None):
1344+
def logp(value, a, b):
13381345
"""
1339-
Draw random values from Kumaraswamy distribution.
1346+
Calculate log-probability of Kumaraswamy distribution at specified value.
13401347
13411348
Parameters
13421349
----------
1343-
point: dict, optional
1344-
Dict of variable values on which random values are to be
1345-
conditioned (uses default point if not specified).
1346-
size: int, optional
1347-
Desired size of random sample (returns one sample if not
1348-
specified).
1350+
value: numeric
1351+
Value(s) for which log-probability is calculated. If the log probabilities for multiple
1352+
values are desired the values must be provided in a numpy array or Aesara tensor
13491353
13501354
Returns
13511355
-------
1352-
array
1356+
TensorVariable
13531357
"""
1354-
# a, b = draw_values([self.a, self.b], point=point, size=size)
1355-
# return generate_samples(self._random, a, b, dist_shape=self.shape, size=size)
1358+
logp = at.log(a) + at.log(b) + (a - 1) * at.log(value) + (b - 1) * at.log(1 - value ** a)
13561359

1357-
def logp(self, value):
1358-
"""
1359-
Calculate log-probability of Kumaraswamy distribution at specified value.
1360+
return bound(logp, value >= 0, value <= 1, a > 0, b > 0)
1361+
1362+
def logcdf(value, a, b):
1363+
r"""
1364+
Compute the log of cumulative distribution function for the Kumaraswamy distribution
1365+
at the specified value.
13601366
13611367
Parameters
13621368
----------
1363-
value: numeric
1364-
Value(s) for which log-probability is calculated. If the log probabilities for multiple
1365-
values are desired the values must be provided in a numpy array or Aesara tensor
1369+
value: numeric or np.ndarray or aesara.tensor
1370+
Value(s) for which log CDF is calculated. If the log CDF for
1371+
multiple values are desired the values must be provided in a numpy
1372+
array or Aesara tensor.
13661373
13671374
Returns
13681375
-------
13691376
TensorVariable
13701377
"""
1371-
a = self.a
1372-
b = self.b
1373-
1374-
logp = at.log(a) + at.log(b) + (a - 1) * at.log(value) + (b - 1) * at.log(1 - value ** a)
1375-
1376-
return bound(logp, value >= 0, value <= 1, a > 0, b > 0)
1378+
logcdf = log1mexp(-(b * at.log1p(-(value ** a))))
1379+
return bound(at.switch(value < 1, logcdf, 0), value >= 0, a > 0, b > 0)
13771380

13781381

13791382
class Exponential(PositiveContinuous):

pymc3/tests/test_distributions.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -1110,15 +1110,28 @@ def test_beta(self):
11101110
decimal=select_by_precision(float64=5, float32=3),
11111111
)
11121112

1113-
@pytest.mark.xfail(reason="Distribution not refactored yet")
11141113
def test_kumaraswamy(self):
1115-
# Scipy does not have a built-in Kumaraswamy pdf
1114+
# Scipy does not have a built-in Kumaraswamy
11161115
def scipy_log_pdf(value, a, b):
11171116
return (
11181117
np.log(a) + np.log(b) + (a - 1) * np.log(value) + (b - 1) * np.log(1 - value ** a)
11191118
)
11201119

1121-
self.check_logp(Kumaraswamy, Unit, {"a": Rplus, "b": Rplus}, scipy_log_pdf)
1120+
def scipy_log_cdf(value, a, b):
1121+
return pm.math.log1mexp_numpy(-(b * np.log1p(-(value ** a))))
1122+
1123+
self.check_logp(
1124+
Kumaraswamy,
1125+
Unit,
1126+
{"a": Rplus, "b": Rplus},
1127+
scipy_log_pdf,
1128+
)
1129+
self.check_logcdf(
1130+
Kumaraswamy,
1131+
Unit,
1132+
{"a": Rplus, "b": Rplus},
1133+
scipy_log_cdf,
1134+
)
11221135

11231136
def test_exponential(self):
11241137
self.check_logp(

pymc3/tests/test_distributions_random.py

+22-6
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,6 @@ class TestWald(BaseTestCases.BaseTestCase):
277277
params = {"mu": 1.0, "lam": 1.0, "alpha": 0.0}
278278

279279

280-
@pytest.mark.xfail(reason="This distribution has not been refactored for v4")
281-
class TestKumaraswamy(BaseTestCases.BaseTestCase):
282-
distribution = pm.Kumaraswamy
283-
params = {"a": 1.0, "b": 1.0}
284-
285-
286280
@pytest.mark.xfail(reason="This distribution has not been refactored for v4")
287281
class TestAsymmetricLaplace(BaseTestCases.BaseTestCase):
288282
distribution = pm.AsymmetricLaplace
@@ -498,6 +492,28 @@ class TestMoyal(BaseTestDistribution):
498492
]
499493

500494

495+
class TestKumaraswamy(BaseTestDistribution):
496+
def kumaraswamy_rng_fn(self, a, b, size, uniform_rng_fct):
497+
return (1 - (1 - uniform_rng_fct(size=size)) ** (1 / b)) ** (1 / a)
498+
499+
def seeded_kumaraswamy_rng_fn(self):
500+
uniform_rng_fct = functools.partial(
501+
getattr(np.random.RandomState, "uniform"), self.get_random_state()
502+
)
503+
return functools.partial(self.kumaraswamy_rng_fn, uniform_rng_fct=uniform_rng_fct)
504+
505+
pymc_dist = pm.Kumaraswamy
506+
pymc_dist_params = {"a": 1.0, "b": 1.0}
507+
expected_rv_op_params = {"a": 1.0, "b": 1.0}
508+
reference_dist_params = {"a": 1.0, "b": 1.0}
509+
reference_dist = seeded_kumaraswamy_rng_fn
510+
tests_to_run = [
511+
"check_pymc_params_match_rv_op",
512+
"check_pymc_draws_match_reference",
513+
"check_rv_size",
514+
]
515+
516+
501517
class TestStudentTLam(BaseTestDistribution):
502518
pymc_dist = pm.StudentT
503519
lam, sigma = get_tau_sigma(tau=2.0)

0 commit comments

Comments
 (0)