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 faster Laplace #356

Merged
merged 2 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -33,6 +33,9 @@ This reference provides detailed documentation for user functions in the current
.. automodule:: preliz.distributions.halfnormal
:members:

.. automodule:: preliz.distributions.laplace
:members:

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

Expand Down
74 changes: 1 addition & 73 deletions preliz/distributions/continuous.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .exponential import Exponential # pylint: disable=unused-import
from .normal import Normal # pylint: disable=unused-import
from .halfnormal import HalfNormal # pylint: disable=unused-import
from .laplace import Laplace # pylint: disable=unused-import
from .weibull import Weibull # pylint: disable=unused-import


Expand Down Expand Up @@ -1157,79 +1158,6 @@ def rvs(self, size=1, random_state=None): # pylint: disable=arguments-differ
return self.ppf(q)


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

The pdf of this distribution is

.. math::

f(x \mid \mu, b) =
\frac{1}{2b} \exp \left\{ - \frac{|x - \mu|}{b} \right\}

.. plot::
:context: close-figs

import arviz as az
from preliz import Laplace
az.style.use('arviz-white')
mus = [0., 0., 0., -5.]
bs = [1., 2., 4., 4.]
for mu, b in zip(mus, bs):
Laplace(mu, b).plot_pdf(support=(-10,10))

======== ========================
Support :math:`x \in \mathbb{R}`
Mean :math:`\mu`
Variance :math:`2 b^2`
======== ========================

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

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

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

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

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

def _fit_moments(self, mean, sigma):
mu = mean
b = (sigma / 2) * (2**0.5)
self._update(mu, b)

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


class Logistic(Continuous):
r"""
Logistic distribution.
Expand Down
171 changes: 171 additions & 0 deletions preliz/distributions/laplace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# pylint: disable=attribute-defined-outside-init
# pylint: disable=arguments-differ
import numpy as np
import numba as nb

from ..internal.distribution_helper import all_not_none, eps

from .distributions import Continuous


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

The pdf of this distribution is

.. math::

f(x \mid \mu, b) =
\frac{1}{2b} \exp \left\{ - \frac{|x - \mu|}{b} \right\}

.. plot::
:context: close-figs

import arviz as az
from preliz import Laplace
az.style.use('arviz-white')
mus = [0., 0., 0., -5.]
bs = [1., 2., 4., 4.]
for mu, b in zip(mus, bs):
Laplace(mu, b).plot_pdf(support=(-10,10))

======== ========================
Support :math:`x \in \mathbb{R}`
Mean :math:`\mu`
Variance :math:`2 b^2`
======== ========================

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

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

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

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

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

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.b)

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.b)

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

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

def entropy(self):
return nb_entropy(self.b)

def median(self):
return self.mu

def mean(self):
return self.mu

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

def var(self):
return 2 * self.b**2

def skewness(self):
return 0.0

def kurtosis(self):
return 3.0

def rvs(self, size=1, random_state=None):
random_state = np.random.default_rng(random_state)
return random_state.laplace(self.mu, self.b, size)

def _fit_moments(self, mean, sigma):
b = (sigma / 2) * (2**0.5)
self._update(mean, b)

def _fit_mle(self, sample, **kwargs):
mu, b = nb_fit_mle(sample)
self._update(mu, b)


@nb.njit
def nb_pdf(x, mu, b):
x = (x - mu) / b
return (0.5 * np.exp(-np.abs(x))) / b


@nb.vectorize(nopython=True)
def nb_cdf(x, mu, b):
x = (x - mu) / b
if x > 0:
return 1.0 - 0.5 * np.exp(-x)
return 0.5 * np.exp(x)


@nb.vectorize(nopython=True)
def nb_ppf(q, mu, b):
if q > 0.5:
q = -np.log(2 * (1 - q))
else:
q = np.log(2 * q)
return q * b + mu


@nb.njit
def nb_neg_logpdf(x, mu, b):
return (-np.log(nb_pdf(x, mu, b))).sum()


@nb.njit
def nb_entropy(b):
return np.log(2) + 1 + np.log(b)


@nb.njit
def nb_fit_mle(sample):
median = np.median(sample)
scale = np.sum(np.abs(sample - median)) / len(sample)
return median, scale
2 changes: 2 additions & 0 deletions preliz/tests/test_scipy.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Beta,
Exponential,
HalfNormal,
Laplace,
Normal,
Weibull,
Bernoulli,
Expand Down Expand Up @@ -39,6 +40,7 @@
),
(Poisson, stats.poisson, {"mu": 3.5}, {"mu": 3.5}),
(Exponential, stats.expon, {"beta": 3.7}, {"scale": 3.7}),
(Laplace, stats.laplace, {"mu": 2.5, "b": 4}, {"loc": 2.5, "scale": 4}),
],
)
def test_match_scipy(p_dist, sp_dist, p_params, sp_params):
Expand Down
Loading