|
20 | 20 |
|
21 | 21 | from typing import List, Optional, Tuple, Union
|
22 | 22 |
|
| 23 | +import aesara |
23 | 24 | import aesara.tensor as at
|
24 | 25 | import numpy as np
|
25 | 26 |
|
26 | 27 | from aesara.assert_op import Assert
|
| 28 | +from aesara.graph.basic import Apply |
| 29 | +from aesara.graph.op import Op |
27 | 30 | from aesara.tensor import gammaln
|
| 31 | +from aesara.tensor.extra_ops import broadcast_shape |
28 | 32 | from aesara.tensor.random.basic import (
|
29 | 33 | BetaRV,
|
30 | 34 | WeibullRV,
|
|
47 | 51 | )
|
48 | 52 | from aesara.tensor.random.op import RandomVariable
|
49 | 53 | from aesara.tensor.var import TensorConstant, TensorVariable
|
| 54 | + |
| 55 | +try: |
| 56 | + from polyagamma import polyagamma_cdf, polyagamma_pdf, random_polyagamma |
| 57 | +except ImportError: # pragma: no cover |
| 58 | + |
| 59 | + def random_polyagamma(*args, **kwargs): |
| 60 | + raise RuntimeError("polyagamma package is not installed!") |
| 61 | + |
| 62 | + def polyagamma_pdf(*args, **kwargs): |
| 63 | + raise RuntimeError("polyagamma package is not installed!") |
| 64 | + |
| 65 | + def polyagamma_cdf(*args, **kwargs): |
| 66 | + raise RuntimeError("polyagamma package is not installed!") |
| 67 | + |
| 68 | + |
50 | 69 | from scipy import stats
|
51 | 70 | from scipy.interpolate import InterpolatedUnivariateSpline
|
52 | 71 | from scipy.special import expit
|
|
103 | 122 | "Rice",
|
104 | 123 | "Moyal",
|
105 | 124 | "AsymmetricLaplace",
|
| 125 | + "PolyaGamma", |
106 | 126 | ]
|
107 | 127 |
|
108 | 128 |
|
@@ -4007,3 +4027,201 @@ def logcdf(value, mu, sigma):
|
4007 | 4027 | at.log(at.erfc(at.exp(-scaled / 2) * (2 ** -0.5))),
|
4008 | 4028 | 0 < sigma,
|
4009 | 4029 | )
|
| 4030 | + |
| 4031 | + |
| 4032 | +class PolyaGammaRV(RandomVariable): |
| 4033 | + """Polya-Gamma random variable.""" |
| 4034 | + |
| 4035 | + name = "polyagamma" |
| 4036 | + ndim_supp = 0 |
| 4037 | + ndims_params = [0, 0] |
| 4038 | + dtype = "floatX" |
| 4039 | + _print_name = ("PG", "\\operatorname{PG}") |
| 4040 | + |
| 4041 | + def __call__(self, h=1.0, z=0.0, size=None, **kwargs): |
| 4042 | + return super().__call__(h, z, size=size, **kwargs) |
| 4043 | + |
| 4044 | + @classmethod |
| 4045 | + def rng_fn(cls, rng, h, z, size=None): |
| 4046 | + """ |
| 4047 | + Generate a random sample from the distribution with the given parameters |
| 4048 | +
|
| 4049 | + Parameters |
| 4050 | + ---------- |
| 4051 | + rng : {None, int, array_like[ints], SeedSequence, BitGenerator, Generator} |
| 4052 | + A seed to initialize the random number generator. If None, then fresh, |
| 4053 | + unpredictable entropy will be pulled from the OS. If an ``int`` or |
| 4054 | + ``array_like[ints]`` is passed, then it will be passed to |
| 4055 | + `SeedSequence` to derive the initial `BitGenerator` state. One may also |
| 4056 | + pass in a `SeedSequence` instance. |
| 4057 | + Additionally, when passed a `BitGenerator`, it will be wrapped by |
| 4058 | + `Generator`. If passed a `Generator`, it will be returned unaltered. |
| 4059 | + h : scalar or sequence |
| 4060 | + The shape parameter of the distribution. |
| 4061 | + z : scalar or sequence |
| 4062 | + The exponential tilting parameter. |
| 4063 | + size : int or tuple of ints, optional |
| 4064 | + The number of elements to draw from the distribution. If size is |
| 4065 | + ``None`` (default) then a single value is returned. If a tuple of |
| 4066 | + integers is passed, the returned array will have the same shape. |
| 4067 | + If the element(s) of size is not an integer type, it will be truncated |
| 4068 | + to the largest integer smaller than its value (e.g (2.1, 1) -> (2, 1)). |
| 4069 | + This parameter only applies if `h` and `z` are scalars. |
| 4070 | + """ |
| 4071 | + # handle the kind of rng passed to the sampler |
| 4072 | + bg = rng._bit_generator if isinstance(rng, np.random.RandomState) else rng |
| 4073 | + return random_polyagamma(h, z, size=size, random_state=bg).astype(aesara.config.floatX) |
| 4074 | + |
| 4075 | + |
| 4076 | +polyagamma = PolyaGammaRV() |
| 4077 | + |
| 4078 | + |
| 4079 | +class _PolyaGammaLogDistFunc(Op): |
| 4080 | + __props__ = ("get_pdf",) |
| 4081 | + |
| 4082 | + def __init__(self, get_pdf=False): |
| 4083 | + self.get_pdf = get_pdf |
| 4084 | + |
| 4085 | + def make_node(self, x, h, z): |
| 4086 | + x = at.as_tensor_variable(floatX(x)) |
| 4087 | + h = at.as_tensor_variable(floatX(h)) |
| 4088 | + z = at.as_tensor_variable(floatX(z)) |
| 4089 | + shape = broadcast_shape(x, h, z) |
| 4090 | + broadcastable = [] if not shape else [False] * len(shape) |
| 4091 | + return Apply(self, [x, h, z], [at.TensorType(aesara.config.floatX, broadcastable)()]) |
| 4092 | + |
| 4093 | + def perform(self, node, ins, outs): |
| 4094 | + x, h, z = ins[0], ins[1], ins[2] |
| 4095 | + outs[0][0] = ( |
| 4096 | + polyagamma_pdf(x, h, z, return_log=True) |
| 4097 | + if self.get_pdf |
| 4098 | + else polyagamma_cdf(x, h, z, return_log=True) |
| 4099 | + ).astype(aesara.config.floatX) |
| 4100 | + |
| 4101 | + |
| 4102 | +class PolyaGamma(PositiveContinuous): |
| 4103 | + r""" |
| 4104 | + The Polya-Gamma distribution. |
| 4105 | +
|
| 4106 | + The distribution is parametrized by ``h`` (shape parameter) and ``z`` |
| 4107 | + (exponential tilting parameter). The pdf of this distribution is |
| 4108 | +
|
| 4109 | + .. math:: |
| 4110 | +
|
| 4111 | + f(x \mid h, z) = cosh^h(\frac{z}{2})e^{-\frac{1}{2}xz^2}f(x \mid h, 0), |
| 4112 | + where :math:`f(x \mid h, 0)` is the pdf of a :math:`PG(h, 0)` variable. |
| 4113 | + Notice that the pdf of this distribution is expressed as an alternating-sign |
| 4114 | + sum of inverse-Gaussian densities. |
| 4115 | +
|
| 4116 | + .. math:: |
| 4117 | +
|
| 4118 | + X = \Sigma_{k=1}^{\infty}\frac{Ga(h, 1)}{d_k}, |
| 4119 | +
|
| 4120 | + where :math:`d_k = 2(k - 0.5)^2\pi^2 + z^2/2`, :math:`Ga(h, 1)` is a gamma |
| 4121 | + random variable with shape parameter ``h`` and scale parameter ``1``. |
| 4122 | +
|
| 4123 | + .. plot:: |
| 4124 | +
|
| 4125 | + import matplotlib.pyplot as plt |
| 4126 | + import numpy as np |
| 4127 | + from polyagamma import polyagamma_pdf |
| 4128 | + plt.style.use('seaborn-darkgrid') |
| 4129 | + x = np.linspace(0.01, 5, 500);x.sort() |
| 4130 | + hs = [1., 5., 10., 15.] |
| 4131 | + zs = [0.] * 4 |
| 4132 | + for h, z in zip(hs, zs): |
| 4133 | + pdf = polyagamma_pdf(x, h=h, z=z) |
| 4134 | + plt.plot(x, pdf, label=r'$h$ = {}, $z$ = {}'.format(h, z)) |
| 4135 | + plt.xlabel('x', fontsize=12) |
| 4136 | + plt.ylabel('f(x)', fontsize=12) |
| 4137 | + plt.legend(loc=1) |
| 4138 | + plt.show() |
| 4139 | +
|
| 4140 | + ======== ============================= |
| 4141 | + Support :math:`x \in (0, \infty)` |
| 4142 | + Mean :math:`dfrac{h}{4} if :math:`z=0`, :math:`\dfrac{tanh(z/2)h}{2z}` otherwise. |
| 4143 | + Variance :math:`0.041666688h` if :math:`z=0`, :math:`\dfrac{h(sinh(z) - z)(1 - tanh^2(z/2))}{4z^3}` otherwise. |
| 4144 | + ======== ============================= |
| 4145 | +
|
| 4146 | + Parameters |
| 4147 | + ---------- |
| 4148 | + h: float, optional |
| 4149 | + The shape parameter of the distribution (h > 0). |
| 4150 | + z: float, optional |
| 4151 | + The exponential tilting parameter of the distribution. |
| 4152 | +
|
| 4153 | + Examples |
| 4154 | + -------- |
| 4155 | + .. code-block:: python |
| 4156 | +
|
| 4157 | + rng = np.random.default_rng() |
| 4158 | + with pm.Model(): |
| 4159 | + x = pm.PolyaGamma('x', h=1, z=5.5) |
| 4160 | + with pm.Model(): |
| 4161 | + x = pm.PolyaGamma('x', h=25, z=-2.3, rng=rng, size=(100, 5)) |
| 4162 | +
|
| 4163 | + References |
| 4164 | + ---------- |
| 4165 | + .. [1] Polson, Nicholas G., James G. Scott, and Jesse Windle. |
| 4166 | + "Bayesian inference for logistic models using Pólya–Gamma latent |
| 4167 | + variables." Journal of the American statistical Association |
| 4168 | + 108.504 (2013): 1339-1349. |
| 4169 | + .. [2] Windle, Jesse, Nicholas G. Polson, and James G. Scott. |
| 4170 | + "Sampling Polya-Gamma random variates: alternate and approximate |
| 4171 | + techniques." arXiv preprint arXiv:1405.0506 (2014) |
| 4172 | + .. [3] Luc Devroye. "On exact simulation algorithms for some distributions |
| 4173 | + related to Jacobi theta functions." Statistics & Probability Letters, |
| 4174 | + Volume 79, Issue 21, (2009): 2251-2259. |
| 4175 | + .. [4] Windle, J. (2013). Forecasting high-dimensional, time-varying |
| 4176 | + variance-covariance matrices with high-frequency data and sampling |
| 4177 | + Pólya-Gamma random variates for posterior distributions derived |
| 4178 | + from logistic likelihoods.(PhD thesis). Retrieved from |
| 4179 | + http://hdl.handle.net/2152/21842 |
| 4180 | + """ |
| 4181 | + rv_op = polyagamma |
| 4182 | + |
| 4183 | + @classmethod |
| 4184 | + def dist(cls, h=1.0, z=0.0, **kwargs): |
| 4185 | + h = at.as_tensor_variable(floatX(h)) |
| 4186 | + z = at.as_tensor_variable(floatX(z)) |
| 4187 | + |
| 4188 | + msg = f"The variable {h} specified for PolyaGamma has non-positive " |
| 4189 | + msg += "values, making it unsuitable for this parameter." |
| 4190 | + Assert(msg)(h, at.all(at.gt(h, 0.0))) |
| 4191 | + |
| 4192 | + return super().dist([h, z], **kwargs) |
| 4193 | + |
| 4194 | + def logp(value, h, z): |
| 4195 | + """ |
| 4196 | + Calculate log-probability of Polya-Gamma distribution at specified value. |
| 4197 | +
|
| 4198 | + Parameters |
| 4199 | + ---------- |
| 4200 | + value: numeric |
| 4201 | + Value(s) for which log-probability is calculated. If the log |
| 4202 | + probabilities for multiple values are desired the values must be |
| 4203 | + provided in a numpy array. |
| 4204 | +
|
| 4205 | + Returns |
| 4206 | + ------- |
| 4207 | + TensorVariable |
| 4208 | + """ |
| 4209 | + |
| 4210 | + return bound(_PolyaGammaLogDistFunc(True)(value, h, z), h > 0, value > 0) |
| 4211 | + |
| 4212 | + def logcdf(value, h, z): |
| 4213 | + """ |
| 4214 | + Compute the log of the cumulative distribution function for the |
| 4215 | + Polya-Gamma distribution at the specified value. |
| 4216 | +
|
| 4217 | + Parameters |
| 4218 | + ---------- |
| 4219 | + value: numeric or np.ndarray or `TensorVariable` |
| 4220 | + Value(s) for which log CDF is calculated. If the log CDF for multiple |
| 4221 | + values are desired the values must be provided in a numpy array. |
| 4222 | +
|
| 4223 | + Returns |
| 4224 | + ------- |
| 4225 | + TensorVariable |
| 4226 | + """ |
| 4227 | + return bound(_PolyaGammaLogDistFunc(False)(value, h, z), h > 0, value > 0) |
0 commit comments