diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a7ae2fcf..5bc7b392 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -8,6 +8,7 @@ * Replace the deprecated `friedman_mse` criterion with `squared_error` for the default tree learner and distribution tests (issue #408, PR #409) * Support a distinct base learner for each distribution parameter, including scikit-learn nested parameters and feature importances for all-tree configurations (issue #338, PR #411) * Fix the `CRPScore` metric of `Normal`, `NormalFixedMean` and `LogNormal`, which was the correct metric multiplied by the scale. It was too large where the predicted scale is above 1 and too small where it is below 1. Fits that use `CRPScore` with these distributions change wherever the predicted scale is not 1 (issue #421) +* Add `df` to the `params` of `T`, `TFixedDf`, `TFixedDfFixedVar` and the `Cauchy` classes built on them, so that `scipy.stats.t(**dist.params)` rebuilds the predicted distribution. Before, `params` held only `loc` and `scale` ## Version 0.5.10 diff --git a/ngboost/distns/t.py b/ngboost/distns/t.py index 4657890a..0b704411 100644 --- a/ngboost/distns/t.py +++ b/ngboost/distns/t.py @@ -77,7 +77,7 @@ def __getattr__(self, name): @property def params(self): - return {"loc": self.loc, "scale": self.scale} + return {"loc": self.loc, "scale": self.scale, "df": self.df} class TFixedDfLogScore(LogScore): @@ -141,7 +141,7 @@ def __getattr__(self, name): @property def params(self): - return {"loc": self.loc, "scale": self.scale} + return {"loc": self.loc, "scale": self.scale, "df": self.df} class TFixedDfFixedVarLogScore(LogScore): @@ -199,4 +199,4 @@ def __getattr__(self, name): @property def params(self): - return {"loc": self.loc, "scale": self.scale} + return {"loc": self.loc, "scale": self.scale, "df": self.df} diff --git a/notebooks/example_t.ipynb b/notebooks/example_t.ipynb index 5367108a..b0293899 100644 --- a/notebooks/example_t.ipynb +++ b/notebooks/example_t.ipynb @@ -596,7 +596,7 @@ "ax = axes[1]\n", "x = np.linspace(-5, 12, 300)\n", "for i in [0, 50, 100]:\n", - " pdf = scipy.stats.t.pdf(x, df=dists.df[i], loc=dists.loc[i], scale=dists.scale[i])\n", + " pdf = scipy.stats.t.pdf(x, **dists[i].params)\n", " ax.plot(x, pdf, label=f\"sample {i}\")\n", " ax.axvline(Y_test[i], color=ax.lines[-1].get_color(), linestyle=\":\", alpha=0.5)\n", "ax.set_xlabel(\"y\")\n", diff --git a/tests/test_distns.py b/tests/test_distns.py index 3f7fd9c3..9325e9fa 100644 --- a/tests/test_distns.py +++ b/tests/test_distns.py @@ -2,6 +2,7 @@ import numpy as np import pytest +import scipy.stats from sklearn.datasets import fetch_california_housing, load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeRegressor @@ -25,6 +26,7 @@ Weibull, k_categorical, ) +from ngboost.distns.cauchy import CauchyFixedVar from ngboost.scores import CRPScore, LogScore, Score # test all the dist methods and score implementation methods,# @@ -224,3 +226,34 @@ def test_multivariatenormal(k: int, learner): mean = y_dist.mean sample = y_dist.rv() scipy_list = y_dist.scipy_distribution() + + +@pytest.mark.parametrize( + "dist_class, expected_df", + [ + (T, None), + (TFixedDf, 3.0), + (TFixedDfFixedVar, 3.0), + (Cauchy, 1.0), + (CauchyFixedVar, 1.0), + ], +) +def test_t_family_params_match_scipy(dist_class, expected_df): + rng = np.random.default_rng(0) + n = 20 + raw_params = rng.normal(size=(dist_class.n_params, n)) + dist = dist_class(raw_params) + params = dist.params + + # df is reported next to loc and scale, and loc is the first raw parameter + assert set(params) == {"loc", "scale", "df"} + np.testing.assert_allclose(params["loc"], raw_params[0]) + np.testing.assert_allclose(params["df"], dist.df) + if expected_df is not None: + np.testing.assert_allclose(params["df"], expected_df) + + # the params are enough to rebuild the same distribution in scipy + y = rng.normal(size=n) + frozen = scipy.stats.t(**params) + np.testing.assert_allclose(frozen.logpdf(y), dist.logpdf(y)) + np.testing.assert_allclose(frozen.ppf(0.9), dist.ppf(0.9))