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

Add efficient Moyal #405

Merged
merged 2 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ This reference provides detailed documentation for user functions in the current
.. automodule:: preliz.distributions.lognormal
:members:

.. automodule:: preliz.distributions.moyal
:members:

.. automodule:: preliz.distributions.normal
:members:

Expand Down
81 changes: 1 addition & 80 deletions preliz/distributions/continuous.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from .laplace import Laplace
from .logistic import Logistic
from .lognormal import LogNormal
from .moyal import Moyal
from .normal import Normal
from .pareto import Pareto
from .studentt import StudentT
Expand Down Expand Up @@ -443,86 +444,6 @@ def rvs(
return expit(np.random.normal(self.mu, self.sigma, size))


class Moyal(Continuous):
r"""
Moyal distribution.

The pdf of this distribution is

.. math::

f(x \mid \mu,\sigma) =
\frac{1}{\sqrt{2\pi}\sigma}e^{-\frac{1}{2}\left(z + e^{-z}\right)},

where

.. math::

z = \frac{x-\mu}{\sigma}

.. plot::
:context: close-figs

import arviz as az
from preliz import Moyal
az.style.use('arviz-doc')
mus = [-1., 0., 4.]
sigmas = [2., 1., 4.]
for mu, sigma in zip(mus, sigmas):
Moyal(mu, sigma).plot_pdf(support=(-10,20))

======== ==============================================================
Support :math:`x \in (-\infty, \infty)`
Mean :math:`\mu + \sigma\left(\gamma + \log 2\right)`, where
:math:`\gamma` is the Euler-Mascheroni constant
Variance :math:`\frac{\pi^{2}}{2}\sigma^{2}`
======== ==============================================================

Parameters
----------
mu : float
Location parameter.
sigma : float
Scale parameter (sigma > 0).
"""

def __init__(self, mu=None, sigma=None):
super().__init__()
self.dist = copy(stats.moyal)
self.support = (-np.inf, np.inf)
self._parametrization(mu, sigma)

def _parametrization(self, mu=None, sigma=None):
self.mu = mu
self.sigma = sigma
self.params = (self.mu, self.sigma)
self.param_names = ("mu", "sigma")
self.params_support = ((-np.inf, np.inf), (eps, np.inf))
if all_not_none(mu, sigma):
self._update(self.mu, self.sigma)

def _get_frozen(self):
frozen = None
if all_not_none(self.params):
frozen = self.dist(loc=self.mu, scale=self.sigma)
return frozen

def _update(self, mu, sigma):
self.mu = np.float64(mu)
self.sigma = np.float64(sigma)
self.params = (self.mu, self.sigma)
self._update_rv_frozen()

def _fit_moments(self, mean, sigma):
sigma = sigma / np.pi * 2**0.5
mu = mean - sigma * (np.euler_gamma + np.log(2))
self._update(mu, sigma)

def _fit_mle(self, sample, **kwargs):
mu, sigma = self.dist.fit(sample, **kwargs)
self._update(mu, sigma)


class Rice(Continuous):
r"""
Rice distribution.
Expand Down
170 changes: 170 additions & 0 deletions preliz/distributions/moyal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# pylint: disable=attribute-defined-outside-init
# pylint: disable=arguments-differ
import numba as nb
import numpy as np
from scipy.special import erf, erfinv, zeta # pylint: disable=no-name-in-module

from .distributions import Continuous
from ..internal.distribution_helper import eps, all_not_none
from ..internal.special import erf, erfinv, ppf_bounds_cont
from ..internal.optimization import optimize_ml


class Moyal(Continuous):
r"""
Moyal distribution.

The pdf of this distribution is

.. math::

f(x \mid \mu,\sigma) =
\frac{1}{\sqrt{2\pi}\sigma}e^{-\frac{1}{2}\left(z + e^{-z}\right)},

where

.. math::

z = \frac{x-\mu}{\sigma}

.. plot::
:context: close-figs

import arviz as az
from preliz import Moyal
az.style.use('arviz-doc')
mus = [-1., 0., 4.]
sigmas = [2., 1., 4.]
for mu, sigma in zip(mus, sigmas):
Moyal(mu, sigma).plot_pdf(support=(-10,20))

======== ==============================================================
Support :math:`x \in (-\infty, \infty)`
Mean :math:`\mu + \sigma\left(\gamma + \log 2\right)`, where
:math:`\gamma` is the Euler-Mascheroni constant
Variance :math:`\frac{\pi^{2}}{2}\sigma^{2}`
======== ==============================================================

Parameters
----------
mu : float
Location parameter.
sigma : float
Scale parameter (sigma > 0).
"""

def __init__(self, mu=None, sigma=None):
super().__init__()
self.support = (-np.inf, np.inf)
self._parametrization(mu, sigma)

def _parametrization(self, mu=None, sigma=None):
self.mu = mu
self.sigma = sigma
self.params = (self.mu, self.sigma)
self.param_names = ("mu", "sigma")
self.params_support = ((-np.inf, np.inf), (eps, np.inf))
if all_not_none(mu, sigma):
self._update(self.mu, self.sigma)

def _update(self, mu, sigma):
self.mu = np.float64(mu)
self.sigma = np.float64(sigma)
self.params = (self.mu, self.sigma)
self.is_frozen = True

def pdf(self, x):
"""
Compute the probability density function (PDF) at a given point x.
"""
x = np.asarray(x)
return np.exp(self.logpdf(x))

def cdf(self, x):
"""
Compute the cumulative distribution function (CDF) at a given point x.
"""
x = np.asarray(x)
return nb_cdf(x, self.mu, self.sigma)

def ppf(self, q):
"""
Compute the percent point function (PPF) at a given probability q.
"""
q = np.asarray(q)
return nb_ppf(q, self.mu, self.sigma)

def logpdf(self, x):
"""
Compute the log probability density function (log PDF) at a given point x.
"""
return nb_logpdf(x, self.mu, self.sigma)

def _neg_logpdf(self, x):
"""
Compute the neg log_pdf sum for the array x.
"""
return nb_neg_logpdf(x, self.mu, self.sigma)

def entropy(self):
x_values = self.xvals("restricted")
logpdf = self.logpdf(x_values)
return -np.trapz(np.exp(logpdf) * logpdf, x_values)

def mean(self):
return self.mu + self.sigma * (np.euler_gamma + np.log(2))

def median(self):
return self.ppf(0.5)

def var(self):
return self.sigma**2 * (np.pi**2) / 2

def std(self):
return self.var() ** 0.5

def skewness(self):
return 28 * np.sqrt(2) * zeta(3) / np.pi**3

def kurtosis(self):
return 4

def rvs(self, size=None, random_state=None):
random_state = np.random.default_rng(random_state)
return self.ppf(random_state.random(size))

def _fit_moments(self, mean, sigma):
sigma = sigma / np.pi * 2**0.5
mu = mean - sigma * (np.euler_gamma + np.log(2))
self._update(mu, sigma)

def _fit_mle(self, sample):
optimize_ml(self, sample)


@nb.njit(cache=True)
def nb_cdf(x, mu, sigma):
z_val = (x - mu) / sigma
return 1 - erf(np.exp(-z_val / 2) * (2**-0.5))


@nb.njit(cache=True)
def nb_ppf(q, mu, sigma):
x_val = sigma * -np.log(2.0 * erfinv(1 - q) ** 2) + mu
return ppf_bounds_cont(x_val, q, -np.inf, np.inf)


@nb.njit(cache=True)
def nb_entropy(sigma):
return 0.5 * (np.log(2 * np.pi * np.e * sigma**2))


@nb.njit(cache=True)
def nb_logpdf(x, mu, sigma):
z_val = (x - mu) / sigma
return -(1 / 2) * (z_val + np.exp(-z_val)) - np.log(sigma) - (1 / 2) * np.log(2 * np.pi)


@nb.njit(cache=True)
def nb_neg_logpdf(x, mu, sigma):
return -(nb_logpdf(x, mu, sigma)).sum()
5 changes: 4 additions & 1 deletion preliz/tests/test_scipy.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
Laplace,
Logistic,
LogNormal,
Moyal,
Normal,
Pareto,
StudentT,
Expand Down Expand Up @@ -66,6 +67,7 @@
(Laplace, stats.laplace, {"mu": 2.5, "b": 4}, {"loc": 2.5, "scale": 4}),
(Logistic, stats.logistic, {"mu": 2.5, "s": 4}, {"loc": 2.5, "scale": 4}),
(LogNormal, stats.lognorm, {"mu": 0, "sigma": 2}, {"s": 2, "scale": 1}),
(Moyal, stats.moyal, {"mu": 1, "sigma": 2}, {"loc": 1, "scale": 2}),
(Normal, stats.norm, {"mu": 0, "sigma": 2}, {"loc": 0, "scale": 2}),
(Pareto, stats.pareto, {"m": 1, "alpha": 4.5}, {"b": 4.5}),
(StudentT, stats.t, {"nu": 5, "mu": 0, "sigma": 2}, {"df": 5, "loc": 0, "scale": 2}),
Expand Down Expand Up @@ -122,7 +124,7 @@ def test_match_scipy(p_dist, sp_dist, p_params, sp_params):
expected = scipy_dist.entropy()
if preliz_dist.kind == "discrete":
assert_almost_equal(actual, expected, decimal=1)
elif preliz_name == "HalfStudentT":
elif preliz_name in ["HalfStudentT", "Moyal"]:
assert_almost_equal(actual, expected, decimal=2)
else:
assert_almost_equal(actual, expected, decimal=4)
Expand All @@ -134,6 +136,7 @@ def test_match_scipy(p_dist, sp_dist, p_params, sp_params):
if preliz_name in [
"HalfStudentT",
"Kumaraswamy",
"Moyal",
"StudentT",
"Weibull",
"InverseGamma",
Expand Down
Loading