Repository: sktime/sktime

Issue:
[BUG] "Must supply freq for datetime value" in forecasting pipeline
posted by @anthonygiorgio97 in #2452
---

@fkiraly ok I have tried to reverse pipelines. Now if I use a `make_reduction` model like SVR() as follow, the pipeline work.

```python
from sktime.transformations.series.boxcox import LogTransformer
from sktime.forecasting.compose import ForecastingPipeline, TransformedTargetForecaster
from sktime.transformations.series.compose import OptionalPassthrough
from sktime.forecasting.fbprophet import Prophet
from sktime.transformations.series.difference import Differencer
from sktime.forecasting.model_selection import ExpandingWindowSplitter, temporal_train_test_split, ForecastingGridSearchCV
from sktime.forecasting.compose import make_reduction
from sklearn.svm import SVR
from sktime.datasets import load_longley
y, X = load_longley()

#split train/test both y and X
fh = [1,2]
train_model, test_model = temporal_train_test_split(y, fh = fh)
X_train = X[X.index.isin(train_model.index)]
X_test = X[X.index.isin(test_model.index)]

#pipeline
pipe = TransformedTargetForecaster(steps = [
            ("log", OptionalPassthrough(LogTransformer())),
            ("differencer", Differencer(drop_na = True)),
            ("myforecasterpipe", ForecastingPipeline(steps = [
                ("logX", OptionalPassthrough(LogTransformer())),
                ("differencerX", Differencer(drop_na = True)),
                ("myforecaster", make_reduction(SVR(), window_length = 3))
        ]))  
])               

#cv setup
N_cv_fold = 1
step_cv = 1
cv = ExpandingWindowSplitter(initial_window = len(train_model) - (N_cv_fold - 1) * step_cv - len(fh),
                            start_with_window = True,
                            step_length = step_cv,
                            fh = fh)

param_grid = [{"log__passthrough": [False],
               "myforecasterpipe__logX__passthrough": [False]
              },
              {"log__passthrough": [True],
               "myforecasterpipe__logX__passthrough": [True]
              }
             ]

#grid search
gscv = ForecastingGridSearchCV(
            forecaster = pipe,
            cv=cv,
            param_grid=param_grid,
            verbose = 1
        )

#fit
gscv.fit(train_model, X = X_train)
```

However if I want to use Prophet now I face up with another error. This is the sample code I'm using:

```python
from sktime.transformations.series.boxcox import LogTransformer
from sktime.forecasting.compose import ForecastingPipeline, TransformedTargetForecaster
from sktime.transformations.series.compose import OptionalPassthrough
from sktime.forecasting.fbprophet import Prophet
from sktime.transformations.series.difference import Differencer
from sktime.forecasting.model_selection import ExpandingWindowSplitter, temporal_train_test_split, ForecastingGridSearchCV
from sktime.datasets import load_longley
y, X = load_longley()

#split train/test both y and X
fh = [1,2]
train_model, test_model = temporal_train_test_split(y, fh = fh)
X_train = X[X.index.isin(train_model.index)]
X_test = X[X.index.isin(test_model.index)]

train_model.index = train_model.index.to_timestamp(freq='A')
X_train.index = X_train.index.to_timestamp(freq='A')
X_test.index = X_test.index.to_timestamp(freq='A')

#pipeline
pipe = TransformedTargetForecaster(steps = [
            ("log", OptionalPassthrough(LogTransformer())),
            ("differencer", Differencer(drop_na = True)),
            ("myforecasterpipe", ForecastingPipeline(steps = [
                ("logX", OptionalPassthrough(LogTransformer())),
                ("differencerX", Differencer(drop_na = True)),
                ("myforecaster", Prophet())
        ]))  
])               

#cv setup
N_cv_fold = 1
step_cv = 1
cv = ExpandingWindowSplitter(initial_window = len(train_model) - (N_cv_fold - 1) * step_cv - len(fh),
                            start_with_window = True,
                            step_length = step_cv,
                            fh = fh)

param_grid = [{"log__passthrough": [False],
               "myforecasterpipe__logX__passthrough": [False]
              },
              {"log__passthrough": [True],
               "myforecasterpipe__logX__passthrough": [True]
              }
             ]

#grid search
gscv = ForecastingGridSearchCV(
            forecaster = pipe,
            cv=cv,
            param_grid=param_grid,
            verbose = 1
        )

#fit
gscv.fit(train_model, X = X_train)
```

The error is this:

ValueError: Must supply freq for datetime value

It seems strange because freq is set as 'A' before fitting for train_model and X_train

_Originally posted by @anthonygiorgio97 in https://github.com/alan-turing-institute/sktime/issues/2452#issuecomment-1100713332_


Discussion:
FYI @anthonygiorgio97, moved one of the unresolved bugs to a new issue.
changing the `drop_na=True` to `na_handling="fill_zero"` does not help and indicates that the problem is probably not that too short series are being created

Resolving patch:
diff --git a/sktime/transformations/series/difference.py b/sktime/transformations/series/difference.py
--- a/sktime/transformations/series/difference.py
+++ b/sktime/transformations/series/difference.py
@@ -11,6 +11,7 @@
 import pandas as pd
 from sklearn.utils import check_array
 
+from sktime.datatypes._utilities import get_cutoff
 from sktime.forecasting.base import ForecastingHorizon
 from sktime.transformations.base import BaseTransformer
 from sktime.utils.validation import is_int
@@ -168,7 +169,9 @@ def _check_inverse_transform_index(self, Z):
         pad_z_inv = self.na_handling == "drop_na" or is_future
 
         cutoff = Z.index[0] if pad_z_inv else Z.index[self._cumulative_lags[-1]]
-        fh = ForecastingHorizon(np.arange(-1, -(self._cumulative_lags[-1] + 1), -1))
+        fh = ForecastingHorizon(
+            np.arange(-1, -(self._cumulative_lags[-1] + 1), -1), freq=self._freq
+        )
         index = fh.to_absolute(cutoff).to_pandas()
         index_diff = index.difference(self._Z.index)
 
@@ -206,6 +209,8 @@ def _fit(self, X, y=None):
         self._prior_cum_lags = np.zeros_like(self._cumulative_lags)
         self._prior_cum_lags[1:] = self._cumulative_lags[:-1]
         self._Z = X.copy()
+
+        self._freq = get_cutoff(X, return_index=True)
         return self
 
     def _transform(self, X, y=None):
@@ -280,7 +285,7 @@ def _inverse_transform(self, X, y=None):
                     cutoff = Z_inv.index[0]
                 else:
                     cutoff = Z_inv.index[prior_cum_lag + lag]
-                fh = ForecastingHorizon(np.arange(-1, -(lag + 1), -1))
+                fh = ForecastingHorizon(np.arange(-1, -(lag + 1), -1), freq=self._freq)
                 index = fh.to_absolute(cutoff).to_pandas()
 
                 if is_df:


Test patch:
diff --git a/sktime/transformations/series/tests/test_differencer.py b/sktime/transformations/series/tests/test_differencer.py
--- a/sktime/transformations/series/tests/test_differencer.py
+++ b/sktime/transformations/series/tests/test_differencer.py
@@ -13,6 +13,7 @@
 from sktime.datasets import load_airline
 from sktime.transformations.series.difference import Differencer
 from sktime.utils._testing.estimator_checks import _assert_array_almost_equal
+from sktime.utils.validation._dependencies import _check_soft_dependencies
 
 y_airline = load_airline()
 y_airline_df = pd.concat([y_airline, y_airline], axis=1)
@@ -88,3 +89,65 @@ def test_differencer_prediction(y, lags):
     y_pred_inv = transformer.inverse_transform(y_pred)
 
     _assert_array_almost_equal(y_true, y_pred_inv)
+
+
+@pytest.mark.skipif(
+    _check_soft_dependencies("prophet", severity="none"),
+    reason="requires Prophet forecaster in the example",
+)
+def test_differencer_cutoff():
+    """Tests a special case that triggers freq inference.
+
+    Failure mode:
+    raises ValueError "Must supply freq for datetime value"
+    on line "fh = ForecastingHorizon(etc" in Differencer._check_inverse_transform_index
+    """
+    from sktime.datasets import load_longley
+    from sktime.forecasting.compose import TransformedTargetForecaster
+    from sktime.forecasting.fbprophet import Prophet
+    from sktime.forecasting.model_selection import (
+        ExpandingWindowSplitter,
+        ForecastingGridSearchCV,
+        temporal_train_test_split,
+    )
+    from sktime.transformations.series.difference import Differencer
+
+    y, X = load_longley()
+
+    # split train/test both y and X
+    fh = [1, 2]
+    train_model, _ = temporal_train_test_split(y, fh=fh)
+    train_model.index = train_model.index.to_timestamp(freq="A")
+    X_train = X[X.index.isin(train_model.index)]
+    X_train.index = X_train.index.to_timestamp(freq="A")
+
+    # pipeline
+    pipe = TransformedTargetForecaster(
+        steps=[
+            ("differencer", Differencer(na_handling="fill_zero")),
+            ("myforecaster", Prophet()),
+        ]
+    )
+
+    # cv setup
+    N_cv_fold = 1
+    step_cv = 1
+    cv = ExpandingWindowSplitter(
+        initial_window=len(train_model) - (N_cv_fold - 1) * step_cv - len(fh),
+        start_with_window=True,
+        step_length=step_cv,
+        fh=fh,
+    )
+
+    param_grid = [{"differencer__na_handling": ["fill_zero"]}]
+
+    # grid search
+    gscv = ForecastingGridSearchCV(
+        forecaster=pipe,
+        cv=cv,
+        param_grid=param_grid,
+        verbose=1,
+    )
+
+    # fit
+    gscv.fit(train_model, X=X_train)