From 731c3b3b231ab918f8bf3d0e912776bf0e83352d Mon Sep 17 00:00:00 2001 From: rohanbabbar04 Date: Fri, 15 Mar 2024 16:14:02 +0530 Subject: [PATCH 1/2] Add faster Laplace --- docs/api_reference.rst | 3 + preliz/distributions/continuous.py | 74 +------------ preliz/distributions/laplace.py | 171 +++++++++++++++++++++++++++++ preliz/tests/test_scipy.py | 2 + 4 files changed, 177 insertions(+), 73 deletions(-) create mode 100644 preliz/distributions/laplace.py diff --git a/docs/api_reference.rst b/docs/api_reference.rst index ffff19c3..7750672d 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -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: diff --git a/preliz/distributions/continuous.py b/preliz/distributions/continuous.py index f5b8a4ca..9eec2070 100644 --- a/preliz/distributions/continuous.py +++ b/preliz/distributions/continuous.py @@ -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 @@ -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. diff --git a/preliz/distributions/laplace.py b/preliz/distributions/laplace.py new file mode 100644 index 00000000..14d2b3ab --- /dev/null +++ b/preliz/distributions/laplace.py @@ -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 diff --git a/preliz/tests/test_scipy.py b/preliz/tests/test_scipy.py index 34f6e119..2287ad0b 100644 --- a/preliz/tests/test_scipy.py +++ b/preliz/tests/test_scipy.py @@ -8,6 +8,7 @@ Beta, Exponential, HalfNormal, + Laplace, Normal, Weibull, Bernoulli, @@ -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): From 40acba76e3cfa5c30ece46e0c749337561ffc273 Mon Sep 17 00:00:00 2001 From: rohanbabbar04 Date: Fri, 15 Mar 2024 19:57:07 +0530 Subject: [PATCH 2/2] Add nb_logpdf and remove nb_pdf --- preliz/distributions/laplace.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/preliz/distributions/laplace.py b/preliz/distributions/laplace.py index 14d2b3ab..9f905031 100644 --- a/preliz/distributions/laplace.py +++ b/preliz/distributions/laplace.py @@ -69,7 +69,7 @@ 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) + return np.exp(nb_logpdf(x, self.mu, self.b)) def cdf(self, x): """ @@ -89,7 +89,7 @@ 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)) + return nb_logpdf(x, self.mu, self.b) def _neg_logpdf(self, x): """ @@ -131,12 +131,6 @@ def _fit_mle(self, sample, **kwargs): 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 @@ -154,9 +148,15 @@ def nb_ppf(q, mu, b): return q * b + mu +@nb.njit +def nb_logpdf(x, mu, b): + x = (x - mu) / b + return np.log(0.5) - np.abs(x) - np.log(b) + + @nb.njit def nb_neg_logpdf(x, mu, b): - return (-np.log(nb_pdf(x, mu, b))).sum() + return (-nb_logpdf(x, mu, b)).sum() @nb.njit