From f8824ab2933bc66a75784f5ba80a036e58e6da32 Mon Sep 17 00:00:00 2001 From: David Wurtz Date: Mon, 25 Nov 2024 10:17:22 -0800 Subject: [PATCH] add tests --- python/prophet/tests/test_diagnostics.py | 47 ++++++++++++++++++++++++ python/prophet/tests/test_prophet.py | 44 ++++++++++++++++++++++ python/prophet/tests/test_serialize.py | 20 ++++++++++ python/prophet/tests/test_utilities.py | 35 ++++++++++++++++++ 4 files changed, 146 insertions(+) diff --git a/python/prophet/tests/test_diagnostics.py b/python/prophet/tests/test_diagnostics.py index 4eba826b6..27c8300e1 100644 --- a/python/prophet/tests/test_diagnostics.py +++ b/python/prophet/tests/test_diagnostics.py @@ -12,6 +12,7 @@ from prophet import Prophet, diagnostics +import logging @pytest.fixture(scope="module") def ts_short(daily_univariate_ts): @@ -401,3 +402,49 @@ def test_prophet_copy_custom(self, data, backend): assert (changepoints == m2.changepoints).all() assert "custom" in m2.seasonalities assert "binary_feature" in m2.extra_regressors + + + def test_cross_validation_dask_import_error(self, ts_short, backend, monkeypatch): + m = Prophet(stan_backend=backend) + m.fit(ts_short) + + # Simulate dask not being installed + import sys + if 'dask' in sys.modules: + del sys.modules['dask'] + + with pytest.raises(ImportError, match="parallel='dask' requires the optional dependency dask."): + diagnostics.cross_validation(m, horizon="4 days", parallel="dask") + + + def test_cross_validation_min_cutoff_value_error(self, ts_short, backend): + m = Prophet(stan_backend=backend) + m.fit(ts_short) + cutoffs = [ts_short['ds'].min(), pd.Timestamp("2012-07-01")] + with pytest.raises(ValueError, match="Minimum cutoff value is not strictly greater than min date in history"): + diagnostics.cross_validation(m, horizon="4 days", cutoffs=cutoffs) + + + def test_cross_validation_initial_window_warning(self, ts_short, backend, caplog): + m = Prophet(stan_backend=backend) + m.add_seasonality(name="monthly", period=30.5, fourier_order=5) + m.fit(ts_short) + with caplog.at_level(logging.WARNING): + diagnostics.cross_validation( + m, horizon="4 days", period="10 days", initial="20 days" + ) + assert any("Seasonality has period of" in message for message in caplog.messages) + + + def test_cross_validation_max_cutoff_value_error(self, ts_short, backend): + m = Prophet(stan_backend=backend) + m.fit(ts_short) + cutoffs = [pd.Timestamp("2012-12-31")] + with pytest.raises(ValueError, match="Maximum cutoff value is greater than end date minus horizon"): + diagnostics.cross_validation(m, horizon="4 days", cutoffs=cutoffs) + + + def test_cross_validation_unfit_model_exception(self): + m = Prophet() + with pytest.raises(Exception, match="Model has not been fit"): + diagnostics.cross_validation(m, horizon="4 days") diff --git a/python/prophet/tests/test_prophet.py b/python/prophet/tests/test_prophet.py index 1313006f8..ac05d52b4 100644 --- a/python/prophet/tests/test_prophet.py +++ b/python/prophet/tests/test_prophet.py @@ -982,3 +982,47 @@ def test_sampling_warm_start(self, daily_univariate_ts, backend): daily_univariate_ts.iloc[:510], init=warm_start_params(m), show_progress=False ) assert m2.params["delta"].shape == (200, 25) + + + def test_invalid_scaling_parameter(self, daily_univariate_ts, backend): + with pytest.raises(ValueError, match="scaling must be one of 'absmax' or 'minmax'"): + Prophet(scaling="invalid", stan_backend=backend).fit(daily_univariate_ts) + + + def test_holidays_missing_ds_column(self, daily_univariate_ts, backend): + holidays = pd.DataFrame({ + "holiday": ["xmas"] + }) + with pytest.raises(ValueError, match='holidays must be a DataFrame with "ds" and "holiday" columns.'): + Prophet(holidays=holidays, stan_backend=backend).fit(daily_univariate_ts) + + + def test_holidays_upper_window_less_than_zero(self, daily_univariate_ts, backend): + holidays = pd.DataFrame({ + "ds": pd.to_datetime(["2016-12-25"]), + "holiday": ["xmas"], + "lower_window": [-1], + "upper_window": [-2] + }) + with pytest.raises(ValueError, match="Holiday upper_window should be >= 0"): + Prophet(holidays=holidays, stan_backend=backend).fit(daily_univariate_ts) + + + def test_holidays_lower_window_greater_than_zero(self, daily_univariate_ts, backend): + holidays = pd.DataFrame({ + "ds": pd.to_datetime(["2016-12-25"]), + "holiday": ["xmas"], + "lower_window": [1], + "upper_window": [0] + }) + with pytest.raises(ValueError, match="Holiday lower_window should be <= 0"): + Prophet(holidays=holidays, stan_backend=backend).fit(daily_univariate_ts) + + + def test_holidays_with_nan(self, daily_univariate_ts, backend): + holidays = pd.DataFrame({ + "ds": pd.to_datetime(["2016-12-25", None]), + "holiday": ["xmas", "xmas"] + }) + with pytest.raises(ValueError, match="Found a NaN in holidays dataframe."): + Prophet(holidays=holidays, stan_backend=backend).fit(daily_univariate_ts) diff --git a/python/prophet/tests/test_serialize.py b/python/prophet/tests/test_serialize.py index 6e42bde21..f4fb95c6c 100644 --- a/python/prophet/tests/test_serialize.py +++ b/python/prophet/tests/test_serialize.py @@ -140,3 +140,23 @@ def test_backwards_compatibility(self): future = m.make_future_dataframe(10) fcst = m.predict(future) assert fcst["yhat"].values[-1] == pytest.approx(pred_val) + + + def test_fit_kwargs_conversion(self, daily_univariate_ts, backend): + from prophet.serialize import model_to_dict + m = Prophet(stan_backend=backend) + df = daily_univariate_ts.head(daily_univariate_ts.shape[0] - 30) + m.fit(df) + m.fit_kwargs['init'] = {'param1': np.array([1.0, 2.0]), 'param2': np.float64(3.0)} + + model_dict = model_to_dict(m) + + assert model_dict['fit_kwargs']['init']['param1'] == [1.0, 2.0] + assert model_dict['fit_kwargs']['init']['param2'] == 3.0 + + + def test_model_to_dict_unfitted_model(self): + from prophet.serialize import model_to_dict + m = Prophet() + with pytest.raises(ValueError, match="This can only be used to serialize models that have already been fit."): + model_to_dict(m) diff --git a/python/prophet/tests/test_utilities.py b/python/prophet/tests/test_utilities.py index 646df3b78..737501554 100644 --- a/python/prophet/tests/test_utilities.py +++ b/python/prophet/tests/test_utilities.py @@ -8,6 +8,7 @@ from prophet import Prophet from prophet.utilities import regressor_coefficients +from prophet.utilities import warm_start_params class TestUtilities: def test_regressor_coefficients(self, daily_univariate_ts, backend): @@ -25,3 +26,37 @@ def test_regressor_coefficients(self, daily_univariate_ts, backend): # No MCMC sampling, so lower and upper should be the same as mean assert np.array_equal(coefs["coef_lower"].values, coefs["coef"].values) assert np.array_equal(coefs["coef_upper"].values, coefs["coef"].values) + + def test_warm_start_params_with_mcmc(self, daily_univariate_ts, backend): + m = Prophet(stan_backend=backend, mcmc_samples=10) + df = daily_univariate_ts.copy() + np.random.seed(123) + df["regr1"] = np.random.normal(size=df.shape[0]) + m.add_regressor("regr1", mode="additive") + m.fit(df) + + params = warm_start_params(m) + assert isinstance(params, dict) + assert "k" in params + assert "m" in params + assert "sigma_obs" in params + assert "delta" in params + assert "beta" in params + + + def test_warm_start_params_no_mcmc(self, daily_univariate_ts, backend): + m = Prophet(stan_backend=backend, mcmc_samples=0) + df = daily_univariate_ts.copy() + np.random.seed(123) + df["regr1"] = np.random.normal(size=df.shape[0]) + m.add_regressor("regr1", mode="additive") + m.fit(df) + + params = warm_start_params(m) + assert isinstance(params, dict) + assert "k" in params + assert "m" in params + assert "sigma_obs" in params + assert "delta" in params + assert "beta" in params +