Skip to content
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

Implement Skellam distribution #260

Merged
merged 3 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Distributions
GeneralizedPoisson
GenExtreme
R2D2M2CP
Skellam
histogram_approximation


Expand Down
2 changes: 1 addition & 1 deletion pymc_experimental/distributions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"""

from pymc_experimental.distributions.continuous import Chi, GenExtreme
from pymc_experimental.distributions.discrete import GeneralizedPoisson
from pymc_experimental.distributions.discrete import GeneralizedPoisson, Skellam
from pymc_experimental.distributions.histogram_utils import histogram_approximation
from pymc_experimental.distributions.multivariate import R2D2M2CP
from pymc_experimental.distributions.timeseries import DiscreteMarkovChain
Expand Down
90 changes: 90 additions & 0 deletions pymc_experimental/distributions/discrete.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,93 @@ def logp(value, mu, lam):
(-mu / 4) <= lam,
msg="0 < mu, max(-1, -mu/4)) <= lam <= 1",
)


class Skellam:
R"""
Skellam distribution.

The Skellam distribution is the distribution of the difference of two
Poisson random variables.

The pmf of this distribution is

.. math::

e^{{-(\mu _{1}\!+\!\mu _{2})}}\left({\frac {\mu _{1}}{\mu _{2}}}\right)^{{k/2}}\!\!I_{{k}}(2{\sqrt {\mu _{1}\mu _{2}}})

where :math:`I_{k}` is the modified Bessel function of the first kind of order :math:`k`.

Read more about the Skellam distribution at https://en.wikipedia.org/wiki/Skellam_distribution

.. plot::
:context: close-figs

import matplotlib.pyplot as plt
import numpy as np
import scipy.stats as st
import arviz as az
plt.style.use('arviz-darkgrid')
x = np.arange(-15, 15)
params = [
(1, 1),
(5, 5),
(5, 1),
]
for mu1, mu2 in params:
pmf = st.skellam.pmf(x, mu1, mu2)
plt.plot(x, pmf, "-o", label=r'$\mu_1$ = {}, $\mu_2$ = {}'.format(mu1, mu2))
plt.xlabel('x', fontsize=12)
plt.ylabel('f(x)', fontsize=12)
plt.legend(loc=1)
plt.show()

======== ======================================
Support :math:`k \in \mathbb{Z}`
Mean :math:`\mu_{1} - \mu_{2}`
Variance :math:`\mu_{1} + \mu_{2}`
======== ======================================

Parameters
----------
mu1 : tensor_like of float
Mean parameter (mu1 >= 0).
mu2 : tensor_like of float
Mean parameter (mu2 >= 0).
"""

@staticmethod
def skellam_dist(mu1, mu2, size):
return pm.Poisson.dist(mu=mu1, size=size) - pm.Poisson.dist(mu=mu2, size=size)

@staticmethod
def skellam_logp(value, mu1, mu2):
res = (
-mu1
- mu2
+ 0.5 * value * (pt.log(mu1) - pt.log(mu2))
+ pt.log(pt.iv(value, 2 * pt.sqrt(mu1 * mu2)))
)
return check_parameters(
res,
mu1 >= 0,
mu2 >= 0,
msg="mu1 >= 0, mu2 >= 0",
)

def __new__(cls, name, mu1, mu2, **kwargs):
return pm.CustomDist(
name,
mu1,
mu2,
dist=cls.skellam_dist,
logp=cls.skellam_logp,
class_name="Skellam",
**kwargs,
)

@classmethod
def dist(cls, mu1, mu2, **kwargs):
return pm.CustomDist.dist(
mu1, mu2, dist=cls.skellam_dist, logp=cls.skellam_logp, class_name="Skellam", **kwargs
wd60622 marked this conversation as resolved.
Show resolved Hide resolved
)
14 changes: 13 additions & 1 deletion pymc_experimental/tests/distributions/test_discrete.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
from pymc.testing import (
BaseTestDistributionRandom,
Domain,
I,
Rplus,
assert_moment_is_expected,
check_logp,
discrete_random_tester,
)
from pytensor import config

from pymc_experimental.distributions import GeneralizedPoisson
from pymc_experimental.distributions import GeneralizedPoisson, Skellam


class TestGeneralizedPoisson:
Expand Down Expand Up @@ -118,3 +120,13 @@ def test_moment(self, mu, lam, size, expected):
with pm.Model() as model:
GeneralizedPoisson("x", mu=mu, lam=lam, size=size)
assert_moment_is_expected(model, expected)


class TestSkellamClass:
wd60622 marked this conversation as resolved.
Show resolved Hide resolved
def test_logp(self):
check_logp(
Skellam,
I,
{"mu1": Rplus, "mu2": Rplus},
lambda value, mu1, mu2: scipy.stats.skellam.logpmf(value, mu1, mu2),
)
Loading