From 70597b1b8987e89acb90fe4a30327dc130b1d472 Mon Sep 17 00:00:00 2001 From: malikrafsan Date: Mon, 29 Apr 2024 14:12:35 +0700 Subject: [PATCH 01/11] feat: add scipy adapter and fisk dist based on adapter --- skpro/distributions/__init__.py | 2 + .../distributions/adapters/scipy/__init__.py | 3 +- .../adapters/scipy/_distribution.py | 60 +++++++++++++++++++ skpro/distributions/fisk_scipy.py | 27 +++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 skpro/distributions/adapters/scipy/_distribution.py create mode 100644 skpro/distributions/fisk_scipy.py diff --git a/skpro/distributions/__init__.py b/skpro/distributions/__init__.py index 75e803606..b59d49507 100644 --- a/skpro/distributions/__init__.py +++ b/skpro/distributions/__init__.py @@ -19,6 +19,7 @@ "TDistribution", "Uniform", "Weibull", + "FiskScipy", ] from skpro.distributions.chi_squared import ChiSquared @@ -35,3 +36,4 @@ from skpro.distributions.t import TDistribution from skpro.distributions.uniform import Uniform from skpro.distributions.weibull import Weibull +from skpro.distributions.fisk_scipy import FiskScipy diff --git a/skpro/distributions/adapters/scipy/__init__.py b/skpro/distributions/adapters/scipy/__init__.py index 3904e31d4..ac01944e4 100644 --- a/skpro/distributions/adapters/scipy/__init__.py +++ b/skpro/distributions/adapters/scipy/__init__.py @@ -2,5 +2,6 @@ # copyright: skpro developers, BSD-3-Clause License (see LICENSE file) from skpro.distributions.adapters.scipy._empirical import empirical_from_discrete +from skpro.distributions.adapters.scipy._distribution import _ScipyAdapter, _ScipyContinuousAdapter -__all__ = ["empirical_from_discrete"] +__all__ = ["empirical_from_discrete", "_ScipyAdapter", "_ScipyContinuousAdapter"] diff --git a/skpro/distributions/adapters/scipy/_distribution.py b/skpro/distributions/adapters/scipy/_distribution.py new file mode 100644 index 000000000..d77547610 --- /dev/null +++ b/skpro/distributions/adapters/scipy/_distribution.py @@ -0,0 +1,60 @@ +from skpro.distributions.base import BaseDistribution + +import numpy as np +import pandas as pd +from scipy.stats._distn_infrastructure import rv_generic +from scipy.stats import rv_continuous + +__all__ = ["_ScipyAdapter", "_ScipyContinuousAdapter"] + +class _ScipyAdapter(BaseDistribution): + _distribution_attr = "_dist" + + def __init__(self, index=None, columns=None): + obj = self._get_scipy_object() + setattr(self, self._distribution_attr, obj) + super().__init__(index, columns) + + def _get_scipy_object(self) -> rv_generic: + raise NotImplementedError("abstract method") + + def _get_scipy_param(self) -> dict: + raise NotImplementedError("abstract method") + + def _mean(self, *args, **kwds): + obj: rv_generic = getattr(self, self._distribution_attr) + params = self._get_scipy_param() + return obj.mean(*args, **kwds, **params) + + def _var(self, *args, **kwds): + obj: rv_generic = getattr(self, self._distribution_attr) + params = self._get_scipy_param() + return obj.var(*args, **kwds, **params) + + +class _ScipyContinuousAdapter(_ScipyAdapter): + def __init__(self, index=None, columns=None): + super().__init__(index, columns) + + def _get_scipy_object(self) -> rv_continuous: + raise NotImplementedError("abstract method") + + def _pdf(self, x: pd.DataFrame, *args, **kwds): + obj: rv_continuous = getattr(self, self._distribution_attr) + params = self._get_scipy_param() + return obj.pdf(x.values, *args, **kwds, **params) + + def _log_pdf(self, x: pd.DataFrame, *args, **kwds): + obj: rv_continuous = getattr(self, self._distribution_attr) + params = self._get_scipy_param() + return obj.logpdf(x.values, *args, **kwds, **params) + + def _cdf(self, x: pd.DataFrame, *args, **kwds): + obj: rv_continuous = getattr(self, self._distribution_attr) + params = self._get_scipy_param() + return obj.cdf(x.values, *args, **kwds, **params) + + def _ppf(self, q: pd.DataFrame, *args, **kwds): + obj: rv_continuous = getattr(self, self._distribution_attr) + params = self._get_scipy_param() + return obj.ppf(q.values, *args, **kwds, **params) diff --git a/skpro/distributions/fisk_scipy.py b/skpro/distributions/fisk_scipy.py new file mode 100644 index 000000000..286e14a2a --- /dev/null +++ b/skpro/distributions/fisk_scipy.py @@ -0,0 +1,27 @@ +from scipy.stats import rv_continuous, fisk +from skpro.distributions.adapters.scipy import _ScipyContinuousAdapter + +__all__ = ["FiskScipy"] + +class FiskScipy(_ScipyContinuousAdapter): + _tags = { + "capabilities:approx": ["energy", "pdfnorm"], + "capabilities:exact": ["mean", "var", "pdf", "log_pdf", "cdf", "ppf"], + "distr:measuretype": "continuous", + "broadcast_init": "on", + } + + def __init__(self, alpha=1, beta=1, index=None, columns=None): + self.alpha = alpha + self.beta = beta + + super().__init__(index=index, columns=columns) + + def _get_scipy_object(self) -> rv_continuous: + return fisk + + def _get_scipy_param(self) -> dict: + alpha = self._bc_params["alpha"] + beta = self._bc_params["beta"] + + return {"c": beta, "scale": alpha} From 25ee8373f0bd6a2599a44cdf8cbe2e72b83d05fc Mon Sep 17 00:00:00 2001 From: malikrafsan Date: Mon, 29 Apr 2024 14:22:16 +0700 Subject: [PATCH 02/11] chore: refactor rv_generic into rv_continuous and rv_discrete on scipy adapter --- .../distributions/adapters/scipy/__init__.py | 4 +-- .../adapters/scipy/_distribution.py | 29 +++++++------------ skpro/distributions/fisk_scipy.py | 4 +-- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/skpro/distributions/adapters/scipy/__init__.py b/skpro/distributions/adapters/scipy/__init__.py index ac01944e4..5a0078a34 100644 --- a/skpro/distributions/adapters/scipy/__init__.py +++ b/skpro/distributions/adapters/scipy/__init__.py @@ -2,6 +2,6 @@ # copyright: skpro developers, BSD-3-Clause License (see LICENSE file) from skpro.distributions.adapters.scipy._empirical import empirical_from_discrete -from skpro.distributions.adapters.scipy._distribution import _ScipyAdapter, _ScipyContinuousAdapter +from skpro.distributions.adapters.scipy._distribution import _ScipyAdapter -__all__ = ["empirical_from_discrete", "_ScipyAdapter", "_ScipyContinuousAdapter"] +__all__ = ["empirical_from_discrete", "_ScipyAdapter"] diff --git a/skpro/distributions/adapters/scipy/_distribution.py b/skpro/distributions/adapters/scipy/_distribution.py index d77547610..24dec32b4 100644 --- a/skpro/distributions/adapters/scipy/_distribution.py +++ b/skpro/distributions/adapters/scipy/_distribution.py @@ -1,11 +1,12 @@ +from typing import Union + from skpro.distributions.base import BaseDistribution -import numpy as np import pandas as pd -from scipy.stats._distn_infrastructure import rv_generic +from scipy.stats import rv_continuous, rv_discrete from scipy.stats import rv_continuous -__all__ = ["_ScipyAdapter", "_ScipyContinuousAdapter"] +__all__ = ["_ScipyAdapter"] class _ScipyAdapter(BaseDistribution): _distribution_attr = "_dist" @@ -15,46 +16,38 @@ def __init__(self, index=None, columns=None): setattr(self, self._distribution_attr, obj) super().__init__(index, columns) - def _get_scipy_object(self) -> rv_generic: + def _get_scipy_object(self) -> Union[rv_continuous, rv_discrete]: raise NotImplementedError("abstract method") def _get_scipy_param(self) -> dict: raise NotImplementedError("abstract method") def _mean(self, *args, **kwds): - obj: rv_generic = getattr(self, self._distribution_attr) + obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) params = self._get_scipy_param() return obj.mean(*args, **kwds, **params) def _var(self, *args, **kwds): - obj: rv_generic = getattr(self, self._distribution_attr) + obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) params = self._get_scipy_param() return obj.var(*args, **kwds, **params) - -class _ScipyContinuousAdapter(_ScipyAdapter): - def __init__(self, index=None, columns=None): - super().__init__(index, columns) - - def _get_scipy_object(self) -> rv_continuous: - raise NotImplementedError("abstract method") - def _pdf(self, x: pd.DataFrame, *args, **kwds): - obj: rv_continuous = getattr(self, self._distribution_attr) + obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) params = self._get_scipy_param() return obj.pdf(x.values, *args, **kwds, **params) def _log_pdf(self, x: pd.DataFrame, *args, **kwds): - obj: rv_continuous = getattr(self, self._distribution_attr) + obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) params = self._get_scipy_param() return obj.logpdf(x.values, *args, **kwds, **params) def _cdf(self, x: pd.DataFrame, *args, **kwds): - obj: rv_continuous = getattr(self, self._distribution_attr) + obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) params = self._get_scipy_param() return obj.cdf(x.values, *args, **kwds, **params) def _ppf(self, q: pd.DataFrame, *args, **kwds): - obj: rv_continuous = getattr(self, self._distribution_attr) + obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) params = self._get_scipy_param() return obj.ppf(q.values, *args, **kwds, **params) diff --git a/skpro/distributions/fisk_scipy.py b/skpro/distributions/fisk_scipy.py index 286e14a2a..ba09cccfa 100644 --- a/skpro/distributions/fisk_scipy.py +++ b/skpro/distributions/fisk_scipy.py @@ -1,9 +1,9 @@ from scipy.stats import rv_continuous, fisk -from skpro.distributions.adapters.scipy import _ScipyContinuousAdapter +from skpro.distributions.adapters.scipy import _ScipyAdapter __all__ = ["FiskScipy"] -class FiskScipy(_ScipyContinuousAdapter): +class FiskScipy(_ScipyAdapter): _tags = { "capabilities:approx": ["energy", "pdfnorm"], "capabilities:exact": ["mean", "var", "pdf", "log_pdf", "cdf", "ppf"], From 1897320fb62dfa67c4a663dc504bd257aa20cd2a Mon Sep 17 00:00:00 2001 From: malikrafsan Date: Mon, 29 Apr 2024 14:39:40 +0700 Subject: [PATCH 03/11] feat: refactor and add poisson dist based on scipy adapter --- .../adapters/scipy/_distribution.py | 38 +++++++++---------- skpro/distributions/fisk_scipy.py | 4 +- skpro/distributions/poisson_scipy.py | 25 ++++++++++++ 3 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 skpro/distributions/poisson_scipy.py diff --git a/skpro/distributions/adapters/scipy/_distribution.py b/skpro/distributions/adapters/scipy/_distribution.py index 24dec32b4..e72446007 100644 --- a/skpro/distributions/adapters/scipy/_distribution.py +++ b/skpro/distributions/adapters/scipy/_distribution.py @@ -19,35 +19,35 @@ def __init__(self, index=None, columns=None): def _get_scipy_object(self) -> Union[rv_continuous, rv_discrete]: raise NotImplementedError("abstract method") - def _get_scipy_param(self) -> dict: + def _get_scipy_param(self) -> tuple[list, dict]: raise NotImplementedError("abstract method") - def _mean(self, *args, **kwds): + def _mean(self): obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) - params = self._get_scipy_param() - return obj.mean(*args, **kwds, **params) + args, kwds = self._get_scipy_param() + return obj.mean(*args, **kwds) - def _var(self, *args, **kwds): + def _var(self): obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) - params = self._get_scipy_param() - return obj.var(*args, **kwds, **params) + args, kwds = self._get_scipy_param() + return obj.var(*args, **kwds) - def _pdf(self, x: pd.DataFrame, *args, **kwds): + def _pdf(self, x: pd.DataFrame): obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) - params = self._get_scipy_param() - return obj.pdf(x.values, *args, **kwds, **params) + args, kwds = self._get_scipy_param() + return obj.pdf(x, *args, **kwds) - def _log_pdf(self, x: pd.DataFrame, *args, **kwds): + def _log_pdf(self, x: pd.DataFrame): obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) - params = self._get_scipy_param() - return obj.logpdf(x.values, *args, **kwds, **params) + args, kwds = self._get_scipy_param() + return obj.logpdf(x, *args, **kwds) - def _cdf(self, x: pd.DataFrame, *args, **kwds): + def _cdf(self, x: pd.DataFrame): obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) - params = self._get_scipy_param() - return obj.cdf(x.values, *args, **kwds, **params) + args, kwds = self._get_scipy_param() + return obj.cdf(x, *args, **kwds) - def _ppf(self, q: pd.DataFrame, *args, **kwds): + def _ppf(self, q: pd.DataFrame): obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) - params = self._get_scipy_param() - return obj.ppf(q.values, *args, **kwds, **params) + args, kwds = self._get_scipy_param() + return obj.ppf(q, *args, **kwds) diff --git a/skpro/distributions/fisk_scipy.py b/skpro/distributions/fisk_scipy.py index ba09cccfa..22f179419 100644 --- a/skpro/distributions/fisk_scipy.py +++ b/skpro/distributions/fisk_scipy.py @@ -20,8 +20,8 @@ def __init__(self, alpha=1, beta=1, index=None, columns=None): def _get_scipy_object(self) -> rv_continuous: return fisk - def _get_scipy_param(self) -> dict: + def _get_scipy_param(self) -> tuple[list, dict]: alpha = self._bc_params["alpha"] beta = self._bc_params["beta"] - return {"c": beta, "scale": alpha} + return [], {"c": beta, "scale": alpha} diff --git a/skpro/distributions/poisson_scipy.py b/skpro/distributions/poisson_scipy.py new file mode 100644 index 000000000..5c5f37b3c --- /dev/null +++ b/skpro/distributions/poisson_scipy.py @@ -0,0 +1,25 @@ +from scipy.stats import rv_discrete, poisson +from skpro.distributions.adapters.scipy import _ScipyAdapter + +__all__ = ["PoissonScipy"] + +class PoissonScipy(_ScipyAdapter): + _tags = { + "capabilities:approx": ["ppf", "energy"], + "capabilities:exact": ["mean", "var", "pmf", "log_pmf", "cdf"], + "distr:measuretype": "discrete", + "broadcast_init": "on", + } + + def __init__(self, mu, index=None, columns=None): + self.mu = mu + + super().__init__(index=index, columns=columns) + + def _get_scipy_object(self) -> rv_discrete: + return poisson + + def _get_scipy_param(self) -> dict: + mu = self._bc_params["mu"] + + return [mu], {} From 936f0d1243163e71ae257ff8ba2ac7cccbf9b127 Mon Sep 17 00:00:00 2001 From: malikrafsan Date: Mon, 29 Apr 2024 14:52:47 +0700 Subject: [PATCH 04/11] docs: add docstring on scipy adapters --- .../distributions/adapters/scipy/__init__.py | 4 +- .../adapters/scipy/_distribution.py | 49 ++++++++++++++++++- skpro/distributions/fisk.py | 2 +- skpro/distributions/fisk_scipy.py | 24 +++++++++ skpro/distributions/poisson_scipy.py | 20 +++++++- 5 files changed, 93 insertions(+), 6 deletions(-) diff --git a/skpro/distributions/adapters/scipy/__init__.py b/skpro/distributions/adapters/scipy/__init__.py index 5a0078a34..875c61deb 100644 --- a/skpro/distributions/adapters/scipy/__init__.py +++ b/skpro/distributions/adapters/scipy/__init__.py @@ -2,6 +2,6 @@ # copyright: skpro developers, BSD-3-Clause License (see LICENSE file) from skpro.distributions.adapters.scipy._empirical import empirical_from_discrete -from skpro.distributions.adapters.scipy._distribution import _ScipyAdapter +from skpro.distributions.adapters.scipy._distribution import _ScipyAdapter, _ScipyDiscreteAdapter -__all__ = ["empirical_from_discrete", "_ScipyAdapter"] +__all__ = ["empirical_from_discrete", "_ScipyAdapter", "_ScipyDiscreteAdapter"] diff --git a/skpro/distributions/adapters/scipy/_distribution.py b/skpro/distributions/adapters/scipy/_distribution.py index e72446007..c46a6e20b 100644 --- a/skpro/distributions/adapters/scipy/_distribution.py +++ b/skpro/distributions/adapters/scipy/_distribution.py @@ -6,9 +6,16 @@ from scipy.stats import rv_continuous, rv_discrete from scipy.stats import rv_continuous -__all__ = ["_ScipyAdapter"] +__all__ = ["_ScipyAdapter", "_ScipyDiscreteAdapter"] class _ScipyAdapter(BaseDistribution): + """Adapter for scipy distributions. + + This class is an adapter for scipy distributions. It provides a common + interface for all scipy distributions. The class is abstract + and should not be instantiated directly. + """ + _distribution_attr = "_dist" def __init__(self, index=None, columns=None): @@ -17,9 +24,18 @@ def __init__(self, index=None, columns=None): super().__init__(index, columns) def _get_scipy_object(self) -> Union[rv_continuous, rv_discrete]: + """Abstract method to get the scipy distribution object. + + Should import the scipy distribution object and return it. + """ raise NotImplementedError("abstract method") def _get_scipy_param(self) -> tuple[list, dict]: + """Abstract method to get the scipy distribution parameters. + + Should return a tuple with two elements: a list of positional arguments (args) + and a dictionary of keyword arguments (kwds). + """ raise NotImplementedError("abstract method") def _mean(self): @@ -51,3 +67,34 @@ def _ppf(self, q: pd.DataFrame): obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) args, kwds = self._get_scipy_param() return obj.ppf(q, *args, **kwds) + +class _ScipyDiscreteAdapter(_ScipyAdapter): + """Adapter for scipy discrete distributions. + + This class is an adapter for scipy discrete distributions. It provides a common + interface for all scipy discrete distributions. The class is abstract + and should not be instantiated directly. + """ + + def _get_scipy_object(self) -> rv_discrete: + raise NotImplementedError("abstract method") + + def _pmf(self, x: pd.DataFrame): + """Return the probability mass function evaluated at x.""" + obj: rv_discrete = getattr(self, self._distribution_attr) + args, kwds = self._get_scipy_param() + return obj.pmf(x, *args, **kwds) + + def pmf(self, x: pd.DataFrame): + """Return the probability mass function evaluated at x.""" + return self._boilerplate("_pmf", x=x) + + def _log_pmf(self, x: pd.DataFrame): + """Return the log of the probability mass function evaluated at x.""" + obj: rv_discrete = getattr(self, self._distribution_attr) + args, kwds = self._get_scipy_param() + return obj.logpmf(x, *args, **kwds) + + def log_pmf(self, x: pd.DataFrame): + """Return the log of the probability mass function evaluated at x.""" + return self._boilerplate("_log_pmf", x=x) diff --git a/skpro/distributions/fisk.py b/skpro/distributions/fisk.py index c48e3c664..0002deaf7 100644 --- a/skpro/distributions/fisk.py +++ b/skpro/distributions/fisk.py @@ -29,7 +29,7 @@ class Fisk(BaseDistribution): Example ------- - >>> from skpro.distributions.fisk import Fisk + >>> from skpro.distributions.fisk import FiskScipy as Fisk >>> d = Fisk(beta=[[1, 1], [2, 3], [4, 5]], alpha=2) """ diff --git a/skpro/distributions/fisk_scipy.py b/skpro/distributions/fisk_scipy.py index 22f179419..926293820 100644 --- a/skpro/distributions/fisk_scipy.py +++ b/skpro/distributions/fisk_scipy.py @@ -4,6 +4,30 @@ __all__ = ["FiskScipy"] class FiskScipy(_ScipyAdapter): + r"""Fisk distribution, aka log-logistic distribution. + + The Fisk distribution is parametrized by a scale parameter :math:`\alpha` + and a shape parameter :math:`\beta`, such that the cumulative distribution + function (CDF) is given by: + + .. math:: F(x) = 1 - \left(1 + \frac{x}{\alpha}\right)^{-\beta}\right)^{-1} + + Parameters + ---------- + alpha : float or array of float (1D or 2D), must be positive + scale parameter of the distribution + beta : float or array of float (1D or 2D), must be positive + shape parameter of the distribution + index : pd.Index, optional, default = RangeIndex + columns : pd.Index, optional, default = RangeIndex + + Example + ------- + >>> from skpro.distributions.fisk import Fisk + + >>> d = Fisk(beta=[[1, 1], [2, 3], [4, 5]], alpha=2) + """ + _tags = { "capabilities:approx": ["energy", "pdfnorm"], "capabilities:exact": ["mean", "var", "pdf", "log_pdf", "cdf", "ppf"], diff --git a/skpro/distributions/poisson_scipy.py b/skpro/distributions/poisson_scipy.py index 5c5f37b3c..186ef4e5e 100644 --- a/skpro/distributions/poisson_scipy.py +++ b/skpro/distributions/poisson_scipy.py @@ -1,9 +1,25 @@ from scipy.stats import rv_discrete, poisson -from skpro.distributions.adapters.scipy import _ScipyAdapter +from skpro.distributions.adapters.scipy import _ScipyDiscreteAdapter __all__ = ["PoissonScipy"] -class PoissonScipy(_ScipyAdapter): +class PoissonScipy(_ScipyDiscreteAdapter): + """Poisson distribution. + + Parameters + ---------- + mu : float or array of float (1D or 2D) + mean of the distribution + index : pd.Index, optional, default = RangeIndex + columns : pd.Index, optional, default = RangeIndex + + Example + ------- + >>> from skpro.distributions import PoissonScipy as Poisson + + >>> distr = Poisson(mu=[[1, 1], [2, 3], [4, 5]]) + """ + _tags = { "capabilities:approx": ["ppf", "energy"], "capabilities:exact": ["mean", "var", "pmf", "log_pmf", "cdf"], From 97a6960807347c557fb4b707696eff6ce3ac8b51 Mon Sep 17 00:00:00 2001 From: malikrafsan Date: Wed, 1 May 2024 17:10:19 +0700 Subject: [PATCH 05/11] chore: replace existing fisk and poisson dist and add authors --- skpro/distributions/__init__.py | 2 - .../adapters/scipy/_distribution.py | 24 ++-- skpro/distributions/fisk.py | 113 ++---------------- skpro/distributions/fisk_scipy.py | 51 -------- skpro/distributions/poisson.py | 78 ++---------- skpro/distributions/poisson_scipy.py | 41 ------- 6 files changed, 34 insertions(+), 275 deletions(-) delete mode 100644 skpro/distributions/fisk_scipy.py delete mode 100644 skpro/distributions/poisson_scipy.py diff --git a/skpro/distributions/__init__.py b/skpro/distributions/__init__.py index b59d49507..75e803606 100644 --- a/skpro/distributions/__init__.py +++ b/skpro/distributions/__init__.py @@ -19,7 +19,6 @@ "TDistribution", "Uniform", "Weibull", - "FiskScipy", ] from skpro.distributions.chi_squared import ChiSquared @@ -36,4 +35,3 @@ from skpro.distributions.t import TDistribution from skpro.distributions.uniform import Uniform from skpro.distributions.weibull import Weibull -from skpro.distributions.fisk_scipy import FiskScipy diff --git a/skpro/distributions/adapters/scipy/_distribution.py b/skpro/distributions/adapters/scipy/_distribution.py index c46a6e20b..f59498d9e 100644 --- a/skpro/distributions/adapters/scipy/_distribution.py +++ b/skpro/distributions/adapters/scipy/_distribution.py @@ -1,16 +1,21 @@ -from typing import Union +# copyright: skpro developers, BSD-3-Clause License (see LICENSE file) +"""Adapter for Scipy Distributions.""" -from skpro.distributions.base import BaseDistribution +__author__ = ["malikrafsan"] + +from typing import Union import pandas as pd from scipy.stats import rv_continuous, rv_discrete -from scipy.stats import rv_continuous + +from skpro.distributions.base import BaseDistribution __all__ = ["_ScipyAdapter", "_ScipyDiscreteAdapter"] + class _ScipyAdapter(BaseDistribution): """Adapter for scipy distributions. - + This class is an adapter for scipy distributions. It provides a common interface for all scipy distributions. The class is abstract and should not be instantiated directly. @@ -25,7 +30,7 @@ def __init__(self, index=None, columns=None): def _get_scipy_object(self) -> Union[rv_continuous, rv_discrete]: """Abstract method to get the scipy distribution object. - + Should import the scipy distribution object and return it. """ raise NotImplementedError("abstract method") @@ -34,7 +39,7 @@ def _get_scipy_param(self) -> tuple[list, dict]: """Abstract method to get the scipy distribution parameters. Should return a tuple with two elements: a list of positional arguments (args) - and a dictionary of keyword arguments (kwds). + and a dictionary of keyword arguments (kwds). """ raise NotImplementedError("abstract method") @@ -68,6 +73,7 @@ def _ppf(self, q: pd.DataFrame): args, kwds = self._get_scipy_param() return obj.ppf(q, *args, **kwds) + class _ScipyDiscreteAdapter(_ScipyAdapter): """Adapter for scipy discrete distributions. @@ -78,17 +84,17 @@ class _ScipyDiscreteAdapter(_ScipyAdapter): def _get_scipy_object(self) -> rv_discrete: raise NotImplementedError("abstract method") - + def _pmf(self, x: pd.DataFrame): """Return the probability mass function evaluated at x.""" obj: rv_discrete = getattr(self, self._distribution_attr) args, kwds = self._get_scipy_param() return obj.pmf(x, *args, **kwds) - + def pmf(self, x: pd.DataFrame): """Return the probability mass function evaluated at x.""" return self._boilerplate("_pmf", x=x) - + def _log_pmf(self, x: pd.DataFrame): """Return the log of the probability mass function evaluated at x.""" obj: rv_discrete = getattr(self, self._distribution_attr) diff --git a/skpro/distributions/fisk.py b/skpro/distributions/fisk.py index 0002deaf7..a0baf51a7 100644 --- a/skpro/distributions/fisk.py +++ b/skpro/distributions/fisk.py @@ -1,15 +1,15 @@ # copyright: skpro developers, BSD-3-Clause License (see LICENSE file) """Log-logistic aka Fisk probability distribution.""" -__author__ = ["fkiraly"] +__author__ = ["fkiraly", "malikrafsan"] import pandas as pd -from scipy.stats import fisk +from scipy.stats import fisk, rv_continuous -from skpro.distributions.base import BaseDistribution +from skpro.distributions.adapters.scipy import _ScipyAdapter -class Fisk(BaseDistribution): +class Fisk(_ScipyAdapter): r"""Fisk distribution, aka log-logistic distribution. The Fisk distribution is parametrized by a scale parameter :math:`\alpha` @@ -29,7 +29,7 @@ class Fisk(BaseDistribution): Example ------- - >>> from skpro.distributions.fisk import FiskScipy as Fisk + >>> from skpro.distributions.fisk import Fisk >>> d = Fisk(beta=[[1, 1], [2, 3], [4, 5]], alpha=2) """ @@ -47,109 +47,14 @@ def __init__(self, alpha=1, beta=1, index=None, columns=None): super().__init__(index=index, columns=columns) - def _mean(self): - """Return expected value of the distribution. + def _get_scipy_object(self) -> rv_continuous: + return fisk - Returns - ------- - 2D np.ndarray, same shape as ``self`` - expected value of distribution (entry-wise) - """ + def _get_scipy_param(self) -> tuple[list, dict]: alpha = self._bc_params["alpha"] beta = self._bc_params["beta"] - mean_arr = fisk.mean(scale=alpha, c=beta) - return mean_arr - - def _var(self): - r"""Return element/entry-wise variance of the distribution. - - Returns - ------- - 2D np.ndarray, same shape as ``self`` - variance of the distribution (entry-wise) - """ - alpha = self._bc_params["alpha"] - beta = self._bc_params["beta"] - - var_arr = fisk.var(scale=alpha, c=beta) - return var_arr - - def _pdf(self, x): - """Probability density function. - - Parameters - ---------- - x : 2D np.ndarray, same shape as ``self`` - values to evaluate the pdf at - - Returns - ------- - 2D np.ndarray, same shape as ``self`` - pdf values at the given points - """ - alpha = self._bc_params["alpha"] - beta = self._bc_params["beta"] - - pdf_arr = fisk.pdf(x, scale=alpha, c=beta) - return pdf_arr - - def _log_pdf(self, x): - """Logarithmic probability density function. - - Parameters - ---------- - x : 2D np.ndarray, same shape as ``self`` - values to evaluate the pdf at - - Returns - ------- - 2D np.ndarray, same shape as ``self`` - log pdf values at the given points - """ - alpha = self._bc_params["alpha"] - beta = self._bc_params["beta"] - - lpdf_arr = fisk.logpdf(x, scale=alpha, c=beta) - return lpdf_arr - - def _cdf(self, x): - """Cumulative distribution function. - - Parameters - ---------- - x : 2D np.ndarray, same shape as ``self`` - values to evaluate the cdf at - - Returns - ------- - 2D np.ndarray, same shape as ``self`` - cdf values at the given points - """ - alpha = self._bc_params["alpha"] - beta = self._bc_params["beta"] - - cdf_arr = fisk.cdf(x, scale=alpha, c=beta) - return cdf_arr - - def _ppf(self, p): - """Quantile function = percent point function = inverse cdf. - - Parameters - ---------- - p : 2D np.ndarray, same shape as ``self`` - values to evaluate the ppf at - - Returns - ------- - 2D np.ndarray, same shape as ``self`` - ppf values at the given points - """ - alpha = self._bc_params["alpha"] - beta = self._bc_params["beta"] - - icdf_arr = fisk.ppf(p, scale=alpha, c=beta) - return icdf_arr + return [], {"c": beta, "scale": alpha} @classmethod def get_test_params(cls, parameter_set="default"): diff --git a/skpro/distributions/fisk_scipy.py b/skpro/distributions/fisk_scipy.py deleted file mode 100644 index 926293820..000000000 --- a/skpro/distributions/fisk_scipy.py +++ /dev/null @@ -1,51 +0,0 @@ -from scipy.stats import rv_continuous, fisk -from skpro.distributions.adapters.scipy import _ScipyAdapter - -__all__ = ["FiskScipy"] - -class FiskScipy(_ScipyAdapter): - r"""Fisk distribution, aka log-logistic distribution. - - The Fisk distribution is parametrized by a scale parameter :math:`\alpha` - and a shape parameter :math:`\beta`, such that the cumulative distribution - function (CDF) is given by: - - .. math:: F(x) = 1 - \left(1 + \frac{x}{\alpha}\right)^{-\beta}\right)^{-1} - - Parameters - ---------- - alpha : float or array of float (1D or 2D), must be positive - scale parameter of the distribution - beta : float or array of float (1D or 2D), must be positive - shape parameter of the distribution - index : pd.Index, optional, default = RangeIndex - columns : pd.Index, optional, default = RangeIndex - - Example - ------- - >>> from skpro.distributions.fisk import Fisk - - >>> d = Fisk(beta=[[1, 1], [2, 3], [4, 5]], alpha=2) - """ - - _tags = { - "capabilities:approx": ["energy", "pdfnorm"], - "capabilities:exact": ["mean", "var", "pdf", "log_pdf", "cdf", "ppf"], - "distr:measuretype": "continuous", - "broadcast_init": "on", - } - - def __init__(self, alpha=1, beta=1, index=None, columns=None): - self.alpha = alpha - self.beta = beta - - super().__init__(index=index, columns=columns) - - def _get_scipy_object(self) -> rv_continuous: - return fisk - - def _get_scipy_param(self) -> tuple[list, dict]: - alpha = self._bc_params["alpha"] - beta = self._bc_params["beta"] - - return [], {"c": beta, "scale": alpha} diff --git a/skpro/distributions/poisson.py b/skpro/distributions/poisson.py index f4d7b5a56..64c43b679 100644 --- a/skpro/distributions/poisson.py +++ b/skpro/distributions/poisson.py @@ -1,15 +1,15 @@ # copyright: skpro developers, BSD-3-Clause License (see LICENSE file) """Poisson probability distribution.""" -__author__ = ["fkiraly"] +__author__ = ["fkiraly", "malikrafsan"] import pandas as pd -from scipy.stats import poisson +from scipy.stats import poisson, rv_discrete -from skpro.distributions.base import BaseDistribution +from skpro.distributions.adapters.scipy import _ScipyDiscreteAdapter -class Poisson(BaseDistribution): +class Poisson(_ScipyDiscreteAdapter): """Poisson distribution. Parameters @@ -30,79 +30,21 @@ class Poisson(BaseDistribution): "capabilities:approx": ["ppf", "energy"], "capabilities:exact": ["mean", "var", "pmf", "log_pmf", "cdf"], "distr:measuretype": "discrete", + "broadcast_init": "on", } def __init__(self, mu, index=None, columns=None): self.mu = mu - self.index = index - self.columns = columns - - # todo: untangle index handling - # and broadcast of parameters. - # move this functionality to the base class - # important: if only one argument, it is a lenght-1-tuple, deal with this - self._mu = self._get_bc_params(self.mu)[0] - shape = self._mu.shape - - if index is None: - index = pd.RangeIndex(shape[0]) - - if columns is None: - columns = pd.RangeIndex(shape[1]) super().__init__(index=index, columns=columns) - def mean(self): - r"""Return expected value of the distribution. - - Let :math:`X` be a random variable with the distribution of `self`. - Returns the expectation :math:`\mathbb{E}[X]` - - Returns - ------- - pd.DataFrame with same rows, columns as `self` - expected value of distribution (entry-wise) - """ - mean_arr = self._mu - return pd.DataFrame(mean_arr, index=self.index, columns=self.columns) - - def var(self): - r"""Return element/entry-wise variance of the distribution. - - Let :math:`X` be a random variable with the distribution of `self`. - Returns :math:`\mathbb{V}[X] = \mathbb{E}\left(X - \mathbb{E}[X]\right)^2` - - Returns - ------- - pd.DataFrame with same rows, columns as `self` - variance of distribution (entry-wise) - """ - mean_arr = self._mu - return pd.DataFrame(mean_arr, index=self.index, columns=self.columns) - - def pmf(self, x): - """Probability mass function.""" - d = self.loc[x.index, x.columns] - pdf_arr = poisson.pmf(x.values, d.mu) - return pd.DataFrame(pdf_arr, index=x.index, columns=x.columns) - - def log_pmf(self, x): - """Logarithmic probability mass function.""" - d = self.loc[x.index, x.columns] - lpdf_arr = poisson.logpmf(x.values, d.mu) - return pd.DataFrame(lpdf_arr, index=x.index, columns=x.columns) + def _get_scipy_object(self) -> rv_discrete: + return poisson - def cdf(self, x): - """Cumulative distribution function.""" - d = self.loc[x.index, x.columns] - cdf_arr = poisson.cdf(x.values, d.mu) - return pd.DataFrame(cdf_arr, index=x.index, columns=x.columns) + def _get_scipy_param(self) -> dict: + mu = self._bc_params["mu"] - def ppf(self, p): - """Quantile function = percent point function = inverse cdf.""" - d = self.loc[p.index, p.columns] - icdf_arr = poisson.ppf(p.values, d.mu) - return pd.DataFrame(icdf_arr, index=p.index, columns=p.columns) + return [mu], {} @classmethod def get_test_params(cls, parameter_set="default"): diff --git a/skpro/distributions/poisson_scipy.py b/skpro/distributions/poisson_scipy.py deleted file mode 100644 index 186ef4e5e..000000000 --- a/skpro/distributions/poisson_scipy.py +++ /dev/null @@ -1,41 +0,0 @@ -from scipy.stats import rv_discrete, poisson -from skpro.distributions.adapters.scipy import _ScipyDiscreteAdapter - -__all__ = ["PoissonScipy"] - -class PoissonScipy(_ScipyDiscreteAdapter): - """Poisson distribution. - - Parameters - ---------- - mu : float or array of float (1D or 2D) - mean of the distribution - index : pd.Index, optional, default = RangeIndex - columns : pd.Index, optional, default = RangeIndex - - Example - ------- - >>> from skpro.distributions import PoissonScipy as Poisson - - >>> distr = Poisson(mu=[[1, 1], [2, 3], [4, 5]]) - """ - - _tags = { - "capabilities:approx": ["ppf", "energy"], - "capabilities:exact": ["mean", "var", "pmf", "log_pmf", "cdf"], - "distr:measuretype": "discrete", - "broadcast_init": "on", - } - - def __init__(self, mu, index=None, columns=None): - self.mu = mu - - super().__init__(index=index, columns=columns) - - def _get_scipy_object(self) -> rv_discrete: - return poisson - - def _get_scipy_param(self) -> dict: - mu = self._bc_params["mu"] - - return [mu], {} From 8eb606537d3c45c2300fcd3301e622c453d9ab49 Mon Sep 17 00:00:00 2001 From: malikrafsan Date: Wed, 1 May 2024 17:38:23 +0700 Subject: [PATCH 06/11] chore: fix codes on formatting pre-commit --- skpro/distributions/adapters/scipy/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skpro/distributions/adapters/scipy/__init__.py b/skpro/distributions/adapters/scipy/__init__.py index 875c61deb..3b6feb2fe 100644 --- a/skpro/distributions/adapters/scipy/__init__.py +++ b/skpro/distributions/adapters/scipy/__init__.py @@ -1,7 +1,10 @@ """Adapters for probability distribution objects, scipy facing.""" # copyright: skpro developers, BSD-3-Clause License (see LICENSE file) +from skpro.distributions.adapters.scipy._distribution import ( + _ScipyAdapter, + _ScipyDiscreteAdapter, +) from skpro.distributions.adapters.scipy._empirical import empirical_from_discrete -from skpro.distributions.adapters.scipy._distribution import _ScipyAdapter, _ScipyDiscreteAdapter __all__ = ["empirical_from_discrete", "_ScipyAdapter", "_ScipyDiscreteAdapter"] From fc849fdec6cd28d9ed3749211d40f79062207f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20Kir=C3=A1ly?= Date: Wed, 1 May 2024 18:42:57 +0100 Subject: [PATCH 07/11] p, not q --- skpro/distributions/adapters/scipy/_distribution.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skpro/distributions/adapters/scipy/_distribution.py b/skpro/distributions/adapters/scipy/_distribution.py index f59498d9e..b35b3ae64 100644 --- a/skpro/distributions/adapters/scipy/_distribution.py +++ b/skpro/distributions/adapters/scipy/_distribution.py @@ -68,10 +68,10 @@ def _cdf(self, x: pd.DataFrame): args, kwds = self._get_scipy_param() return obj.cdf(x, *args, **kwds) - def _ppf(self, q: pd.DataFrame): + def _ppf(self, p: pd.DataFrame): obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) args, kwds = self._get_scipy_param() - return obj.ppf(q, *args, **kwds) + return obj.ppf(p, *args, **kwds) class _ScipyDiscreteAdapter(_ScipyAdapter): From 9f1ec3b9b83f30ae091babcadf9860e29438b2b2 Mon Sep 17 00:00:00 2001 From: malikrafsan Date: Fri, 3 May 2024 13:29:57 +0700 Subject: [PATCH 08/11] fix: remove tuple square bracket for python3.8 support --- skpro/distributions/adapters/scipy/_distribution.py | 2 +- skpro/distributions/fisk.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skpro/distributions/adapters/scipy/_distribution.py b/skpro/distributions/adapters/scipy/_distribution.py index b35b3ae64..e337dd8a0 100644 --- a/skpro/distributions/adapters/scipy/_distribution.py +++ b/skpro/distributions/adapters/scipy/_distribution.py @@ -35,7 +35,7 @@ def _get_scipy_object(self) -> Union[rv_continuous, rv_discrete]: """ raise NotImplementedError("abstract method") - def _get_scipy_param(self) -> tuple[list, dict]: + def _get_scipy_param(self): """Abstract method to get the scipy distribution parameters. Should return a tuple with two elements: a list of positional arguments (args) diff --git a/skpro/distributions/fisk.py b/skpro/distributions/fisk.py index a0baf51a7..39b928ed9 100644 --- a/skpro/distributions/fisk.py +++ b/skpro/distributions/fisk.py @@ -50,7 +50,7 @@ def __init__(self, alpha=1, beta=1, index=None, columns=None): def _get_scipy_object(self) -> rv_continuous: return fisk - def _get_scipy_param(self) -> tuple[list, dict]: + def _get_scipy_param(self): alpha = self._bc_params["alpha"] beta = self._bc_params["beta"] From 8da17d30f8ea976a2656570dd33c543140f01533 Mon Sep 17 00:00:00 2001 From: malikrafsan Date: Fri, 3 May 2024 14:51:58 +0700 Subject: [PATCH 09/11] feat: adjust and add pytest for scipy adapter --- .../distributions/adapters/scipy/__init__.py | 7 +- .../adapters/scipy/_distribution.py | 33 ++++---- skpro/distributions/poisson.py | 4 +- .../distributions/tests/test_scipy_adapter.py | 84 +++++++++++++++++++ 4 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 skpro/distributions/tests/test_scipy_adapter.py diff --git a/skpro/distributions/adapters/scipy/__init__.py b/skpro/distributions/adapters/scipy/__init__.py index 3b6feb2fe..0dd0b152a 100644 --- a/skpro/distributions/adapters/scipy/__init__.py +++ b/skpro/distributions/adapters/scipy/__init__.py @@ -1,10 +1,7 @@ """Adapters for probability distribution objects, scipy facing.""" # copyright: skpro developers, BSD-3-Clause License (see LICENSE file) -from skpro.distributions.adapters.scipy._distribution import ( - _ScipyAdapter, - _ScipyDiscreteAdapter, -) +from skpro.distributions.adapters.scipy._distribution import _ScipyAdapter from skpro.distributions.adapters.scipy._empirical import empirical_from_discrete -__all__ = ["empirical_from_discrete", "_ScipyAdapter", "_ScipyDiscreteAdapter"] +__all__ = ["empirical_from_discrete", "_ScipyAdapter"] diff --git a/skpro/distributions/adapters/scipy/_distribution.py b/skpro/distributions/adapters/scipy/_distribution.py index e337dd8a0..a05eed94f 100644 --- a/skpro/distributions/adapters/scipy/_distribution.py +++ b/skpro/distributions/adapters/scipy/_distribution.py @@ -10,7 +10,7 @@ from skpro.distributions.base import BaseDistribution -__all__ = ["_ScipyAdapter", "_ScipyDiscreteAdapter"] +__all__ = ["_ScipyAdapter"] class _ScipyAdapter(BaseDistribution): @@ -22,6 +22,9 @@ class _ScipyAdapter(BaseDistribution): """ _distribution_attr = "_dist" + _tags = { + "object_type": ["distribution", "scipy_distribution_adapter"], + } def __init__(self, index=None, columns=None): obj = self._get_scipy_object() @@ -55,11 +58,17 @@ def _var(self): def _pdf(self, x: pd.DataFrame): obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) + if isinstance(obj, rv_discrete): + return 0 + args, kwds = self._get_scipy_param() return obj.pdf(x, *args, **kwds) def _log_pdf(self, x: pd.DataFrame): obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) + if isinstance(obj, rv_discrete): + return 0 + args, kwds = self._get_scipy_param() return obj.logpdf(x, *args, **kwds) @@ -73,21 +82,12 @@ def _ppf(self, p: pd.DataFrame): args, kwds = self._get_scipy_param() return obj.ppf(p, *args, **kwds) - -class _ScipyDiscreteAdapter(_ScipyAdapter): - """Adapter for scipy discrete distributions. - - This class is an adapter for scipy discrete distributions. It provides a common - interface for all scipy discrete distributions. The class is abstract - and should not be instantiated directly. - """ - - def _get_scipy_object(self) -> rv_discrete: - raise NotImplementedError("abstract method") - def _pmf(self, x: pd.DataFrame): """Return the probability mass function evaluated at x.""" - obj: rv_discrete = getattr(self, self._distribution_attr) + obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) + if isinstance(obj, rv_continuous): + return 0 + args, kwds = self._get_scipy_param() return obj.pmf(x, *args, **kwds) @@ -97,7 +97,10 @@ def pmf(self, x: pd.DataFrame): def _log_pmf(self, x: pd.DataFrame): """Return the log of the probability mass function evaluated at x.""" - obj: rv_discrete = getattr(self, self._distribution_attr) + obj: Union[rv_continuous, rv_discrete] = getattr(self, self._distribution_attr) + if isinstance(obj, rv_continuous): + return 0 + args, kwds = self._get_scipy_param() return obj.logpmf(x, *args, **kwds) diff --git a/skpro/distributions/poisson.py b/skpro/distributions/poisson.py index 64c43b679..df7450a6a 100644 --- a/skpro/distributions/poisson.py +++ b/skpro/distributions/poisson.py @@ -6,10 +6,10 @@ import pandas as pd from scipy.stats import poisson, rv_discrete -from skpro.distributions.adapters.scipy import _ScipyDiscreteAdapter +from skpro.distributions.adapters.scipy import _ScipyAdapter -class Poisson(_ScipyDiscreteAdapter): +class Poisson(_ScipyAdapter): """Poisson distribution. Parameters diff --git a/skpro/distributions/tests/test_scipy_adapter.py b/skpro/distributions/tests/test_scipy_adapter.py new file mode 100644 index 000000000..fbc7ac59f --- /dev/null +++ b/skpro/distributions/tests/test_scipy_adapter.py @@ -0,0 +1,84 @@ +import numpy as np +import pytest +from skbase.testing import QuickTester + +from skpro.tests.test_all_estimators import BaseFixtureGenerator, PackageConfig + + +class ScipyDistributionFixtureGenerator(BaseFixtureGenerator): + """Fixture generator for scipy distributions adapter. + + Fixtures parameterized + ---------------------- + object_class: object inheriting from BaseObject + ranges over object classes not excluded by EXCLUDE_OBJECTS, EXCLUDED_TESTS + object_instance: instance of object inheriting from BaseObject + ranges over object classes not excluded by EXCLUDE_OBJECTS, EXCLUDED_TESTS + instances are generated by create_test_instance class method + """ + + object_type_filter = "scipy_distribution_adapter" + + +class TestScipyAdapter(PackageConfig, ScipyDistributionFixtureGenerator, QuickTester): + """Test the scipy adapter.""" + + METHOD_TESTS = { + "NO_PARAMS": [("mean", "mean"), ("var", "var")], + "X_PARAMS": [("cdf", "cdf"), ("ppf", "ppf")], + "CONTINUOUS": [("pdf", "pdf"), ("log_pdf", "logpdf")], + "DISCRETE": [("pmf", "pmf"), ("log_pmf", "logpmf")], + } + + @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["NO_PARAMS"]) + def test_method_no_params(self, object_instance, method, scipy_method): + """Test method that doesn't need additional parameters.""" + res = getattr(object_instance, method)() + params = object_instance._get_scipy_param() + scipy_obj = object_instance._get_scipy_object() + + scipy_res = getattr(scipy_obj, scipy_method)(*params[0], **params[1]) + + assert np.allclose(res, scipy_res) + + @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["X_PARAMS"]) + def test_method_with_x_params(self, object_instance, method, scipy_method): + """Test method that needs x as parameter.""" + x = 0.5 + res = getattr(object_instance, method)(x) + params = object_instance._get_scipy_param() + scipy_obj = object_instance._get_scipy_object() + + scipy_res = getattr(scipy_obj, scipy_method)(x, *params[0], **params[1]) + + assert np.allclose(res, scipy_res) + + @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["CONTINUOUS"]) + def test_method_continuous_dist(self, object_instance, method, scipy_method): + """Test continuous distribution method.""" + x = 0.5 + + res = getattr(object_instance, method)(x) + if object_instance._tags["distr:measuretype"] != "continuous": + scipy_res = 0 + else: + params = object_instance._get_scipy_param() + scipy_obj = object_instance._get_scipy_object() + scipy_res = getattr(scipy_obj, scipy_method)(x, *params[0], **params[1]) + + assert np.allclose(res, scipy_res) + + @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["DISCRETE"]) + def test_method_discrete_dist(self, object_instance, method, scipy_method): + """Test discrete distribution method.""" + x = 0.5 + + res = getattr(object_instance, method)(x) + if object_instance._tags["distr:measuretype"] != "discrete": + scipy_res = 0 + else: + params = object_instance._get_scipy_param() + scipy_obj = object_instance._get_scipy_object() + scipy_res = getattr(scipy_obj, scipy_method)(x, *params[0], **params[1]) + + assert np.allclose(res, scipy_res) From 01380b0e6b0ed7ce3fad12660e532267e8abbb89 Mon Sep 17 00:00:00 2001 From: malikrafsan Date: Fri, 3 May 2024 15:00:51 +0700 Subject: [PATCH 10/11] chore: make x values parameterized in scipy adapter pytest --- .../distributions/tests/test_scipy_adapter.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/skpro/distributions/tests/test_scipy_adapter.py b/skpro/distributions/tests/test_scipy_adapter.py index fbc7ac59f..5a296db30 100644 --- a/skpro/distributions/tests/test_scipy_adapter.py +++ b/skpro/distributions/tests/test_scipy_adapter.py @@ -4,6 +4,8 @@ from skpro.tests.test_all_estimators import BaseFixtureGenerator, PackageConfig +__author__ = ["malikrafsan"] + class ScipyDistributionFixtureGenerator(BaseFixtureGenerator): """Fixture generator for scipy distributions adapter. @@ -30,6 +32,8 @@ class TestScipyAdapter(PackageConfig, ScipyDistributionFixtureGenerator, QuickTe "DISCRETE": [("pmf", "pmf"), ("log_pmf", "logpmf")], } + X_VALUES = [0.1, 0.5, 0.99] + @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["NO_PARAMS"]) def test_method_no_params(self, object_instance, method, scipy_method): """Test method that doesn't need additional parameters.""" @@ -42,9 +46,9 @@ def test_method_no_params(self, object_instance, method, scipy_method): assert np.allclose(res, scipy_res) @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["X_PARAMS"]) - def test_method_with_x_params(self, object_instance, method, scipy_method): + @pytest.mark.parametrize("x", X_VALUES) + def test_method_with_x_params(self, object_instance, method, scipy_method, x): """Test method that needs x as parameter.""" - x = 0.5 res = getattr(object_instance, method)(x) params = object_instance._get_scipy_param() scipy_obj = object_instance._get_scipy_object() @@ -54,10 +58,9 @@ def test_method_with_x_params(self, object_instance, method, scipy_method): assert np.allclose(res, scipy_res) @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["CONTINUOUS"]) - def test_method_continuous_dist(self, object_instance, method, scipy_method): + @pytest.mark.parametrize("x", X_VALUES) + def test_method_continuous_dist(self, object_instance, method, scipy_method, x): """Test continuous distribution method.""" - x = 0.5 - res = getattr(object_instance, method)(x) if object_instance._tags["distr:measuretype"] != "continuous": scipy_res = 0 @@ -69,10 +72,9 @@ def test_method_continuous_dist(self, object_instance, method, scipy_method): assert np.allclose(res, scipy_res) @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["DISCRETE"]) - def test_method_discrete_dist(self, object_instance, method, scipy_method): + @pytest.mark.parametrize("x", X_VALUES) + def test_method_discrete_dist(self, object_instance, method, scipy_method, x): """Test discrete distribution method.""" - x = 0.5 - res = getattr(object_instance, method)(x) if object_instance._tags["distr:measuretype"] != "discrete": scipy_res = 0 From 898c26bf563ffcf6c5f54e0d9a66bd6c5bb66457 Mon Sep 17 00:00:00 2001 From: malikrafsan Date: Fri, 3 May 2024 15:12:46 +0700 Subject: [PATCH 11/11] chore: move the scipy adapter test to appropriate dir location --- .../scipy/tests/test_scipy_adapters.py | 85 ++++++++++++++++++ .../distributions/tests/test_scipy_adapter.py | 86 ------------------- 2 files changed, 85 insertions(+), 86 deletions(-) delete mode 100644 skpro/distributions/tests/test_scipy_adapter.py diff --git a/skpro/distributions/adapters/scipy/tests/test_scipy_adapters.py b/skpro/distributions/adapters/scipy/tests/test_scipy_adapters.py index 907ae0317..75cf4cd38 100644 --- a/skpro/distributions/adapters/scipy/tests/test_scipy_adapters.py +++ b/skpro/distributions/adapters/scipy/tests/test_scipy_adapters.py @@ -2,6 +2,12 @@ import numpy as np import pandas as pd +import pytest +from skbase.testing import QuickTester + +from skpro.tests.test_all_estimators import BaseFixtureGenerator, PackageConfig + +__author__ = ["fkiraly", "malikrafsan"] def test_empirical_from_discrete(): @@ -40,3 +46,82 @@ def test_empirical_from_discrete(): ) assert np.all(emp2.spl.index == expected_idx) assert np.all(emp2.spl.columns == ["abc"]) + + +class ScipyDistributionFixtureGenerator(BaseFixtureGenerator): + """Fixture generator for scipy distributions adapter. + + Fixtures parameterized + ---------------------- + object_class: object inheriting from BaseObject + ranges over object classes not excluded by EXCLUDE_OBJECTS, EXCLUDED_TESTS + object_instance: instance of object inheriting from BaseObject + ranges over object classes not excluded by EXCLUDE_OBJECTS, EXCLUDED_TESTS + instances are generated by create_test_instance class method + """ + + object_type_filter = "scipy_distribution_adapter" + + +class TestScipyAdapter(PackageConfig, ScipyDistributionFixtureGenerator, QuickTester): + """Test the scipy adapter.""" + + METHOD_TESTS = { + "NO_PARAMS": [("mean", "mean"), ("var", "var")], + "X_PARAMS": [("cdf", "cdf"), ("ppf", "ppf")], + "CONTINUOUS": [("pdf", "pdf"), ("log_pdf", "logpdf")], + "DISCRETE": [("pmf", "pmf"), ("log_pmf", "logpmf")], + } + + X_VALUES = [0.1, 0.5, 0.99] + + @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["NO_PARAMS"]) + def test_method_no_params(self, object_instance, method, scipy_method): + """Test method that doesn't need additional parameters.""" + res = getattr(object_instance, method)() + params = object_instance._get_scipy_param() + scipy_obj = object_instance._get_scipy_object() + + scipy_res = getattr(scipy_obj, scipy_method)(*params[0], **params[1]) + + assert np.allclose(res, scipy_res) + + @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["X_PARAMS"]) + @pytest.mark.parametrize("x", X_VALUES) + def test_method_with_x_params(self, object_instance, method, scipy_method, x): + """Test method that needs x as parameter.""" + res = getattr(object_instance, method)(x) + params = object_instance._get_scipy_param() + scipy_obj = object_instance._get_scipy_object() + + scipy_res = getattr(scipy_obj, scipy_method)(x, *params[0], **params[1]) + + assert np.allclose(res, scipy_res) + + @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["CONTINUOUS"]) + @pytest.mark.parametrize("x", X_VALUES) + def test_method_continuous_dist(self, object_instance, method, scipy_method, x): + """Test continuous distribution method.""" + res = getattr(object_instance, method)(x) + if object_instance._tags["distr:measuretype"] != "continuous": + scipy_res = 0 + else: + params = object_instance._get_scipy_param() + scipy_obj = object_instance._get_scipy_object() + scipy_res = getattr(scipy_obj, scipy_method)(x, *params[0], **params[1]) + + assert np.allclose(res, scipy_res) + + @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["DISCRETE"]) + @pytest.mark.parametrize("x", X_VALUES) + def test_method_discrete_dist(self, object_instance, method, scipy_method, x): + """Test discrete distribution method.""" + res = getattr(object_instance, method)(x) + if object_instance._tags["distr:measuretype"] != "discrete": + scipy_res = 0 + else: + params = object_instance._get_scipy_param() + scipy_obj = object_instance._get_scipy_object() + scipy_res = getattr(scipy_obj, scipy_method)(x, *params[0], **params[1]) + + assert np.allclose(res, scipy_res) diff --git a/skpro/distributions/tests/test_scipy_adapter.py b/skpro/distributions/tests/test_scipy_adapter.py deleted file mode 100644 index 5a296db30..000000000 --- a/skpro/distributions/tests/test_scipy_adapter.py +++ /dev/null @@ -1,86 +0,0 @@ -import numpy as np -import pytest -from skbase.testing import QuickTester - -from skpro.tests.test_all_estimators import BaseFixtureGenerator, PackageConfig - -__author__ = ["malikrafsan"] - - -class ScipyDistributionFixtureGenerator(BaseFixtureGenerator): - """Fixture generator for scipy distributions adapter. - - Fixtures parameterized - ---------------------- - object_class: object inheriting from BaseObject - ranges over object classes not excluded by EXCLUDE_OBJECTS, EXCLUDED_TESTS - object_instance: instance of object inheriting from BaseObject - ranges over object classes not excluded by EXCLUDE_OBJECTS, EXCLUDED_TESTS - instances are generated by create_test_instance class method - """ - - object_type_filter = "scipy_distribution_adapter" - - -class TestScipyAdapter(PackageConfig, ScipyDistributionFixtureGenerator, QuickTester): - """Test the scipy adapter.""" - - METHOD_TESTS = { - "NO_PARAMS": [("mean", "mean"), ("var", "var")], - "X_PARAMS": [("cdf", "cdf"), ("ppf", "ppf")], - "CONTINUOUS": [("pdf", "pdf"), ("log_pdf", "logpdf")], - "DISCRETE": [("pmf", "pmf"), ("log_pmf", "logpmf")], - } - - X_VALUES = [0.1, 0.5, 0.99] - - @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["NO_PARAMS"]) - def test_method_no_params(self, object_instance, method, scipy_method): - """Test method that doesn't need additional parameters.""" - res = getattr(object_instance, method)() - params = object_instance._get_scipy_param() - scipy_obj = object_instance._get_scipy_object() - - scipy_res = getattr(scipy_obj, scipy_method)(*params[0], **params[1]) - - assert np.allclose(res, scipy_res) - - @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["X_PARAMS"]) - @pytest.mark.parametrize("x", X_VALUES) - def test_method_with_x_params(self, object_instance, method, scipy_method, x): - """Test method that needs x as parameter.""" - res = getattr(object_instance, method)(x) - params = object_instance._get_scipy_param() - scipy_obj = object_instance._get_scipy_object() - - scipy_res = getattr(scipy_obj, scipy_method)(x, *params[0], **params[1]) - - assert np.allclose(res, scipy_res) - - @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["CONTINUOUS"]) - @pytest.mark.parametrize("x", X_VALUES) - def test_method_continuous_dist(self, object_instance, method, scipy_method, x): - """Test continuous distribution method.""" - res = getattr(object_instance, method)(x) - if object_instance._tags["distr:measuretype"] != "continuous": - scipy_res = 0 - else: - params = object_instance._get_scipy_param() - scipy_obj = object_instance._get_scipy_object() - scipy_res = getattr(scipy_obj, scipy_method)(x, *params[0], **params[1]) - - assert np.allclose(res, scipy_res) - - @pytest.mark.parametrize("method,scipy_method", METHOD_TESTS["DISCRETE"]) - @pytest.mark.parametrize("x", X_VALUES) - def test_method_discrete_dist(self, object_instance, method, scipy_method, x): - """Test discrete distribution method.""" - res = getattr(object_instance, method)(x) - if object_instance._tags["distr:measuretype"] != "discrete": - scipy_res = 0 - else: - params = object_instance._get_scipy_param() - scipy_obj = object_instance._get_scipy_object() - scipy_res = getattr(scipy_obj, scipy_method)(x, *params[0], **params[1]) - - assert np.allclose(res, scipy_res)