diff --git a/preliz/distributions/continuous.py b/preliz/distributions/continuous.py index 56f06646..0c0836df 100644 --- a/preliz/distributions/continuous.py +++ b/preliz/distributions/continuous.py @@ -1,6 +1,7 @@ # pylint: disable=too-many-lines # pylint: disable=too-many-instance-attributes # pylint: disable=invalid-name +# pylint: disable=attribute-defined-outside-init """ Continuous probability distributions. """ @@ -87,31 +88,33 @@ def __init__(self, alpha=None, beta=None, mu=None, sigma=None, kappa=None): self.name = "beta" self.dist = stats.beta self.support = (0, 1) - self.params_support = ((eps, np.inf), (eps, np.inf)) - self.alpha, self.beta, self.param_names = self._parametrization( - alpha, beta, mu, sigma, kappa - ) - if self.alpha is not None and self.beta is not None: - self._update(self.alpha, self.beta) + self._parametrization(alpha, beta, mu, sigma, kappa) - def _parametrization(self, alpha, beta, mu, sigma, kappa): + def _parametrization(self, alpha=None, beta=None, mu=None, sigma=None, kappa=None): if mu is None and sigma is None: names = ("alpha", "beta") + self.params_support = ((eps, np.inf), (eps, np.inf)) elif mu is not None and sigma is not None: alpha, beta = self._from_mu_sigma(mu, sigma) names = ("mu", "sigma") + self.params_support = ((eps, 1 - eps), (eps, (mu * (1 - mu)) ** 0.5)) elif mu is not None and kappa is not None: alpha, beta = self._from_mu_kappa(mu, kappa) names = ("mu", "kappa") + self.params_support = ((eps, 1 - eps), (eps, np.inf)) else: raise ValueError( "Incompatible parametrization. Either use alpha " "and beta, or mu and sigma." ) - return alpha, beta, names + self.alpha = alpha + self.beta = beta + self.param_names = names + if self.alpha is not None and self.beta is not None: + self._update(self.alpha, self.beta) def _from_mu_sigma(self, mu, sigma): kappa = mu * (1 - mu) / sigma**2 - 1 @@ -1526,11 +1529,9 @@ def __init__(self, mu=None, sigma=None, tau=None): self.dist = stats.norm self.support = (-np.inf, np.inf) self.params_support = ((-np.inf, np.inf), (eps, np.inf)) - self.mu, self.sigma, self.param_names = self._parametrization(mu, sigma, tau) - if self.mu is not None and self.sigma is not None: - self._update(self.mu, self.sigma) + self._parametrization(mu, sigma, tau) - def _parametrization(self, mu, sigma, tau): + def _parametrization(self, mu=None, sigma=None, tau=None): if sigma is not None and tau is not None: raise ValueError( "Incompatible parametrization. Either use mu and sigma, or mu and tau." @@ -1543,7 +1544,11 @@ def _parametrization(self, mu, sigma, tau): sigma = from_precision(tau) names = ("mu", "tau") - return mu, sigma, names + self.mu = mu + self.sigma = sigma + self.param_names = names + if mu is not None and sigma is not None: + self._update(mu, sigma) def _get_frozen(self): frozen = None diff --git a/preliz/distributions/distributions.py b/preliz/distributions/distributions.py index 49634d2a..9e827101 100644 --- a/preliz/distributions/distributions.py +++ b/preliz/distributions/distributions.py @@ -4,6 +4,8 @@ # pylint: disable=no-member from collections import namedtuple +from ipywidgets import interact +import ipywidgets as ipyw import numpy as np from ..utils.plot_utils import plot_pdfpmf, plot_cdf, plot_ppf @@ -327,6 +329,84 @@ def plot_ppf( "you need to first define its parameters or use one of the fit methods" ) + def interactive(self, kind="pdf", fixed_lim="both", pointinterval=True, quantiles=None): + """ + Interactive exploration of distributions parameters + + Parameters + ---------- + kind : str: + Type of plot. Available options are `pdf`, `cdf` and `ppf`. + fixed_lim : str or tuple + Set the limits of the x-axis and/or y-axis. + Defaults to `"both"`, the limits of both axis are fixed. + Use `"auto"` for automatic rescaling of x-axis and y-axis. + Or set them manually by passing a tuple of 4 elements, + the first two fox x-axis, the last two for x-axis. The tuple can have `None`. + pointinterval : bool + Whether to include a plot of the quantiles. Defaults to False. If True the default is to + plot the median and two interquantiles ranges. + quantiles : list + Values of the five quantiles to use when ``pointinterval=True`` if None (default) + the values ``[0.05, 0.25, 0.5, 0.75, 0.95]`` will be used. The number of elements + should be 5, 3, 1 or 0 (in this last case nothing will be plotted). + """ + + # temporary patch until we migrate all distributions to use + # self.params_report and self.params + try: + params_value = self.params_report + except AttributeError: + params_value = self.params + + args = dict(zip(self.param_names, params_value)) + + if fixed_lim == "both": + self.__init__(**args) + xlim = self._finite_endpoints("full") + xvals = self.xvals("restricted") + ylim = (0, np.max(self.pdf(xvals) * 1.5)) + elif isinstance(fixed_lim, tuple): + xlim = fixed_lim[:2] + ylim = fixed_lim[2:] + + sliders = {} + for name, value, support in zip(self.param_names, params_value, self.params_support): + lower, upper = support + if np.isfinite(lower): + min_v = lower + else: + min_v = value - 10 + if np.isfinite(upper): + max_v = upper + else: + max_v = value + 10 + + step = (max_v - min_v) / 100 + + sliders[name] = ipyw.FloatSlider( + min=min_v, + max=max_v, + step=step, + description=f"{name} ({lower:.0f}, {upper:.0f})", + value=value, + ) + + def plot(**args): + self.__init__(**args) + if kind == "pdf": + ax = self.plot_pdf(legend=False, pointinterval=pointinterval, quantiles=quantiles) + elif kind == "cdf": + ax = self.plot_cdf(legend=False, pointinterval=pointinterval, quantiles=quantiles) + elif kind == "ppf": + ax = self.plot_ppf(legend=False, pointinterval=pointinterval, quantiles=quantiles) + if fixed_lim != "auto" and kind != "ppf": + ax.set_xlim(*xlim) + if fixed_lim != "auto" and kind != "cdf": + ax.set_ylim(*ylim) + + interact(plot, **sliders) + class Continuous(Distribution): """Base class for continuous distributions."""