diff --git a/python/prophet/forecaster.py b/python/prophet/forecaster.py index 2c37b5e95..dad01e597 100644 --- a/python/prophet/forecaster.py +++ b/python/prophet/forecaster.py @@ -1083,6 +1083,15 @@ def set_auto_seasonalities(self) -> None: yearly_disable = last - first < pd.Timedelta(days=730) fourier_order = self.parse_seasonality_args( 'yearly', self.yearly_seasonality, yearly_disable, 10) + if fourier_order > 0 and yearly_disable: + logger.warning( + 'Yearly seasonality is enabled with less than 730 days ' + '(approximately 2 years) of history. The model may be ' + 'under-identified, and the trend/seasonality decomposition ' + 'can be unstable and dependent on the Prophet/Stan version. ' + 'Consider disabling yearly seasonality or providing more ' + 'history.' + ) if fourier_order > 0: self.seasonalities['yearly'] = { 'period': 365.25, diff --git a/python/prophet/tests/test_prophet.py b/python/prophet/tests/test_prophet.py index 09918711c..3f633d784 100644 --- a/python/prophet/tests/test_prophet.py +++ b/python/prophet/tests/test_prophet.py @@ -6,6 +6,7 @@ import numpy as np import pandas as pd import pytest +import logging from prophet import Prophet from prophet.utilities import warm_start_params @@ -20,6 +21,15 @@ def train_test_split(ts_data: pd.DataFrame, n_test_rows: int) -> pd.DataFrame: def rmse(predictions, targets) -> float: return np.sqrt(np.mean((predictions - targets) ** 2)) +def _short_monthly_ts() -> pd.DataFrame: + # Mirrors facebook/prophet#2709: a single annual cycle of monthly data. + return pd.DataFrame({ + "ds": pd.date_range("2025-01-01", periods=12, freq="MS"), + "y": [ + 361.06, 33880.23, 29431.62, 17337.68, 208032.5, 515776.5, + 848975.0, 837513.2, 1237904.0, 2246456.0, 1982927.0, 2421611.0, + ], + }) class TestProphetFitPredictDefault: @pytest.mark.parametrize( @@ -517,6 +527,28 @@ def test_auto_yearly_seasonality(self, daily_univariate_ts, backend): "condition_name": None, } + def test_yearly_seasonality_warns_on_short_history(self, caplog): + m = Prophet( + yearly_seasonality=True, + weekly_seasonality=False, + daily_seasonality=False, + ) + with caplog.at_level(logging.WARNING, logger="prophet"): + m.fit(_short_monthly_ts()) + assert "yearly" in m.seasonalities + assert any( + "less than 730 days" in r.getMessage() for r in caplog.records + ), "expected an under-identification warning for short yearly history" + + def test_yearly_seasonality_no_warning_on_auto(self, caplog): + m = Prophet(weekly_seasonality=False, daily_seasonality=False) + with caplog.at_level(logging.WARNING, logger="prophet"): + m.fit(_short_monthly_ts()) + assert "yearly" not in m.seasonalities + assert not any( + "less than 730 days" in r.getMessage() for r in caplog.records + ), "auto-disabled yearly seasonality should not emit the warning" + def test_auto_daily_seasonality(self, daily_univariate_ts, subdaily_univariate_ts, backend): # Should be enabled m = Prophet(stan_backend=backend)