Skip to content

Add icdf functions for Lognormal, Half Cauchy and Half Normal distributions #6766

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions pymc/distributions/continuous.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,15 @@ def logcdf(value, loc, sigma):
msg="sigma > 0",
)

def icdf(value, loc, sigma):
res = Normal.icdf((value + 1.0) / 2.0, loc, sigma)
Copy link
Member

@ricardoV94 ricardoV94 Jun 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit annoying because we don't allow users to create a HalfNormal with loc != 0 but in theory they could call icdf or reach this function with a HalfNormal that has a custom non-zero loc.

My question is, will this expression work for non-zero loc?

This is also a question for the HalfCauchy.

I wonder if the best solution is to reimplement these RandomVariables directly in PyMC in a way that they don't accept a loc argument (just like the HalfStudentTRV in this file). This way we don't have to worry about loc in the logp/logcdf/icdf/moment functions.

If we go down that path, we can remove the current HalfNormal and HalfCauchy RandomVariables from PyTensor as they aren't really needed there.

Copy link
Member Author

@amyoshino amyoshino Jun 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implemented formula work well even when loc != 0 and is consistent with SciPy results:

Testing HalfNormal:
image

Testing HalfCauchy:
image

Allow me some more time to bring some screenshots calling the implemented functions with pm.HalfNormal.icdf() and pm.HalfCauchy.icdf()

If you think the best solution is still to reimplement RandomVariables directly in PyMC in a way that they don't accept a loc argument, let me know.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to do it in this PR, but the fact that it isn't being tested in our suite (and not just the new icdf) is already a good argument to drop it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good news that it works though!

res = check_icdf_value(res, value)
return check_icdf_parameters(
res,
sigma > 0,
msg="sigma > 0",
)


class WaldRV(RandomVariable):
name = "wald"
Expand Down Expand Up @@ -1714,12 +1723,22 @@ def logcdf(value, mu, sigma):
-np.inf,
normal_lcdf(mu, sigma, pt.log(value)),
)

return check_parameters(
res,
sigma > 0,
msg="sigma > 0",
)

def icdf(value, mu, sigma):
res = pt.exp(Normal.icdf(value, mu, sigma))
res = check_icdf_value(res, value)
return check_icdf_parameters(
res,
sigma > 0,
msg="sigma > 0",
)


Lognormal = LogNormal

Expand Down Expand Up @@ -2121,6 +2140,15 @@ def logcdf(value, loc, beta):
msg="beta > 0",
)

def icdf(value, loc, beta):
res = loc + beta * pt.tan(np.pi * (value) / 2.0)
res = check_icdf_value(res, value)
return check_parameters(
res,
beta > 0,
msg="beta > 0",
)


class Gamma(PositiveContinuous):
r"""
Expand Down
23 changes: 23 additions & 0 deletions tests/distributions/test_continuous.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ def test_half_normal(self):
{"sigma": Rplus},
lambda value, sigma: st.halfnorm.logcdf(value, scale=sigma),
)
check_icdf(
pm.HalfNormal,
{"sigma": Rplus},
lambda q, sigma: st.halfnorm.ppf(q, scale=sigma),
)

def test_chisquared_logp(self):
check_logp(
Expand Down Expand Up @@ -502,6 +507,21 @@ def test_lognormal(self):
{"mu": R, "sigma": Rplusbig},
lambda value, mu, sigma: st.lognorm.logcdf(value, sigma, 0, np.exp(mu)),
)
check_icdf(
pm.LogNormal,
{"mu": R, "tau": Rplusbig},
lambda q, mu, tau: floatX(st.lognorm.ppf(q, tau**-0.5, 0, np.exp(mu))),
)
# Because we exponentiate the normal quantile function, setting sigma >= 9.5
# return extreme values that results in relative errors above 4 digits
# we circumvent it by keeping it below or equal to 9.
custom_rplusbig = Domain([0, 0.5, 0.9, 0.99, 1, 1.5, 2, 9, np.inf])
check_icdf(
pm.LogNormal,
{"mu": R, "sigma": custom_rplusbig},
lambda q, mu, sigma: floatX(st.lognorm.ppf(q, sigma, 0, np.exp(mu))),
decimal=select_by_precision(float64=4, float32=3),
)

def test_studentt_logp(self):
check_logp(
Expand Down Expand Up @@ -567,6 +587,9 @@ def test_half_cauchy(self):
{"beta": Rplusbig},
lambda value, beta: st.halfcauchy.logcdf(value, scale=beta),
)
check_icdf(
pm.HalfCauchy, {"beta": Rplusbig}, lambda q, beta: st.halfcauchy.ppf(q, scale=beta)
)

def test_gamma_logp(self):
check_logp(
Expand Down