Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] NeuralForecastRNN should auto-detect freq #6039

Merged
merged 8 commits into from Mar 20, 2024
9 changes: 9 additions & 0 deletions .all-contributorsrc
Expand Up @@ -2604,6 +2604,15 @@
"bug",
"code"
]
},
{
"login": "geetu040",
"name": "Armaghan",
"avatar_url": "https://avatars.githubusercontent.com/u/90601662?s=96&v=4",
"profile": "https://github.com/geetu040",
"contributions": [
"code"
]
}
]
}
30 changes: 25 additions & 5 deletions sktime/forecasting/base/adapters/_neuralforecast.py
Expand Up @@ -17,8 +17,11 @@ class _NeuralForecastAdapter(BaseForecaster):

Parameters
----------
freq : str
freq : str (default="auto")
frequency of the data, see available frequencies [1]_ from ``pandas``

default ("auto") interprets freq from ForecastingHorizon in ``fit``
raises ValueError if freq cannot be interpreted
yarnabrina marked this conversation as resolved.
Show resolved Hide resolved
local_scaler_type : str (default=None)
scaler to apply per-series to all features before fitting, which is inverted
after predicting
Expand Down Expand Up @@ -66,7 +69,7 @@ class _NeuralForecastAdapter(BaseForecaster):

def __init__(
self: "_NeuralForecastAdapter",
freq: str,
freq: str = "auto",
local_scaler_type: typing.Optional[
typing.Literal["standard", "robust", "robust-iqr", "minmax", "boxcox"]
] = None,
Expand All @@ -84,6 +87,9 @@ def __init__(

super().__init__()

# initiate internal variables to avoid AttributeError in future
self._freq = None

self.id_col = "unique_id"
self.time_col = "ds"
self.target_col = "y"
Expand Down Expand Up @@ -143,7 +149,7 @@ def _instantiate_model(self: "_NeuralForecastAdapter", fh: ForecastingHorizon):
from neuralforecast import NeuralForecast

model = NeuralForecast(
[algorithm_instance], self.freq, local_scaler_type=self.local_scaler_type
[algorithm_instance], self._freq, local_scaler_type=self.local_scaler_type
)

return model
Expand Down Expand Up @@ -179,9 +185,23 @@ def _fit(
if not fh.is_all_out_of_sample(cutoff=self.cutoff):
raise NotImplementedError("in-sample prediction is currently not supported")

if self.freq == "auto" and fh.freq is None:
# when freq cannot be interpreted from ForecastingHorizon
raise ValueError(
f"Error in {self.__class__.__name__}, "
f"could not interpret freq, "
f"try passing freq in model initialization"
)

if self.freq == "auto":
yarnabrina marked this conversation as resolved.
Show resolved Hide resolved
# interpret freq from ForecastingHorizon
self._freq = fh.freq
else:
self._freq = self.freq
yarnabrina marked this conversation as resolved.
Show resolved Hide resolved

train_indices = y.index
if isinstance(train_indices, pandas.PeriodIndex):
train_indices = train_indices.to_timestamp(freq=self.freq)
train_indices = train_indices.to_timestamp(freq=self._freq)

train_data = {
self.id_col: 1,
Expand Down Expand Up @@ -252,7 +272,7 @@ def _predict(
if self.futr_exog_list:
predict_indices = X.index
if isinstance(predict_indices, pandas.PeriodIndex):
predict_indices = predict_indices.to_timestamp(freq=self.freq)
predict_indices = predict_indices.to_timestamp(freq=self._freq)

predict_data = {self.id_col: 1, self.time_col: predict_indices.to_numpy()}

Expand Down
7 changes: 5 additions & 2 deletions sktime/forecasting/neuralforecast.py
Expand Up @@ -22,8 +22,11 @@ class NeuralForecastRNN(_NeuralForecastAdapter):

Parameters
----------
freq : str
freq : str (default="auto")
frequency of the data, see available frequencies [4]_ from ``pandas``

default ("auto") interprets freq from ForecastingHorizon in ``fit``
raises ValueError if freq cannot be interpreted
local_scaler_type : str (default=None)
scaler to apply per-series to all features before fitting, which is inverted
after predicting
Expand Down Expand Up @@ -156,7 +159,7 @@ class NeuralForecastRNN(_NeuralForecastAdapter):

def __init__(
self: "NeuralForecastRNN",
freq: str,
freq: str = "auto",
local_scaler_type: typing.Optional[
typing.Literal["standard", "robust", "robust-iqr", "minmax", "boxcox"]
] = None,
Expand Down
104 changes: 104 additions & 0 deletions sktime/forecasting/tests/test_neuralforecast.py
yarnabrina marked this conversation as resolved.
Show resolved Hide resolved
Expand Up @@ -148,3 +148,107 @@ def test_neural_forecast_rnn_fail_with_multiple_predictions() -> None:
NotImplementedError, match="Multiple prediction columns are not supported."
):
model.predict()


@pytest.mark.skipif(
not run_test_for_class(NeuralForecastRNN),
reason="run test only if softdeps are present and incrementally (if requested)",
)
def test_neural_forecast_rnn_with_auto_freq() -> None:
"""Test NeuralForecastRNN with freq set to 'auto'."""
# define model
model = NeuralForecastRNN("auto", max_steps=5, trainer_kwargs={"logger": False})
yarnabrina marked this conversation as resolved.
Show resolved Hide resolved

# train model
model.fit(y_train, fh=[1, 2, 3, 4])

# predict with trained model
y_pred = model.predict()

# check interpreted freq
assert y_pred.index.freq == "A-DEC", "The interpreted frequency was incorrect"
yarnabrina marked this conversation as resolved.
Show resolved Hide resolved

# check prediction index
pandas.testing.assert_index_equal(y_pred.index, y_test.index, check_names=False)
yarnabrina marked this conversation as resolved.
Show resolved Hide resolved


@pytest.mark.skipif(
not run_test_for_class(NeuralForecastRNN),
reason="run test only if softdeps are present and incrementally (if requested)",
)
def test_neural_forecast_rnn_with_auto_freq_on_all_freq() -> None:
"""Test NeuralForecastRNN with freq set to 'auto' on all freqs."""
# define all supported frequencies
freqs = [
"B",
"C",
"D",
"M",
"Q",
"W",
"W-FRI",
"W-MON",
"W-SAT",
"W-SUN",
"W-THU",
"W-TUE",
"W-WED",
"Y",
"h",
"min",
"ms",
"ns",
"s",
"us",
]

for freq in freqs:
yarnabrina marked this conversation as resolved.
Show resolved Hide resolved
# prepare data
y = pandas.Series(
data=range(10),
index=pandas.date_range(start="2024-01-01", periods=10, freq=freq),
)

# define model
model = NeuralForecastRNN(freq, max_steps=1, trainer_kwargs={"logger": False})
model_auto = NeuralForecastRNN(
"auto", max_steps=1, trainer_kwargs={"logger": False}
)

# attempt train
model.fit(y, fh=[1, 2, 3, 4])
model_auto.fit(y, fh=[1, 2, 3, 4])

yarnabrina marked this conversation as resolved.
Show resolved Hide resolved
# predict with trained model
pred = model.predict()
pred_auto = model_auto.predict()

# check prediction
pandas.testing.assert_series_equal(pred, pred_auto)


@pytest.mark.skipif(
not run_test_for_class(NeuralForecastRNN),
reason="run test only if softdeps are present and incrementally (if requested)",
)
def test_neural_forecast_rnn_with_auto_freq_on_range_index() -> None:
"""Test NeuralForecastRNN with freq set to 'auto' on pd.RangeIndex."""
# prepare data
y = pandas.Series(data=range(10), index=pandas.RangeIndex(start=0, stop=10))

# should fail to interpret auto freq
with pytest.raises(
ValueError,
match="could not interpret freq, try passing freq in model initialization",
):
# define model
model = NeuralForecastRNN("auto", max_steps=5, trainer_kwargs={"logger": False})

# attempt train
model.fit(y, fh=[1, 2, 3, 4])

# should work with freq passed as param
model = NeuralForecastRNN("W", max_steps=5, trainer_kwargs={"logger": False})

# attempt train
model.fit(y, fh=[1, 2, 3, 4])