Repository: sktime/sktime

Issue:
[Bug] Imputer with forecaster not working
**Describe the bug**
I know it's more of a question, but I don't understand how I can use Imputer with a forecaster.

```python
from sklearn.ensemble import GradientBoostingRegressor
from sktime.datasets import load_solar
import pandas as pd
import numpy as np
from sktime.transformations.series.impute import Imputer

y = load_solar()
y = y.groupby(by=pd.Grouper(level=0, freq="D")).sum()
y.iloc[100:110] = np.nan

forecaster = make_reduction(GradientBoostingRegressor(), strategy="multioutput")
impute = Imputer(method="forecaster", forecaster=forecaster)
impute.fit_transform(y)
```

In run the code above and I get 

```python
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[14], line 14
     12 forecaster = make_reduction(GradientBoostingRegressor(), strategy="multioutput")
     13 impute = Imputer(method="forecaster", forecaster=forecaster)
---> 14 impute.fit_transform(y)

File [c:\ProgramData\Miniconda3\envs\DLL_Forecast\Lib\site-packages\sktime\transformations\base.py:619](file:///C:/ProgramData/Miniconda3/envs/DLL_Forecast/Lib/site-packages/sktime/transformations/base.py:619), in BaseTransformer.fit_transform(self, X, y)
    554 """Fit to data, then transform it.
    555 
    556 Fits the transformer to X and y and returns a transformed version of X.
   (...)
    615         Example: i-th instance of the output is the i-th window running over `X`
    616 """
    617 # Non-optimized default implementation; override when a better
    618 # method is possible for a given algorithm.
--> 619 return self.fit(X, y).transform(X, y)

File [c:\ProgramData\Miniconda3\envs\DLL_Forecast\Lib\site-packages\sktime\transformations\base.py:533](file:///C:/ProgramData/Miniconda3/envs/DLL_Forecast/Lib/site-packages/sktime/transformations/base.py:533), in BaseTransformer.transform(self, X, y)
    530 X_inner, y_inner, metadata = self._check_X_y(X=X, y=y, return_metadata=True)
    532 if not isinstance(X_inner, VectorizedDF):
--> 533     Xt = self._transform(X=X_inner, y=y_inner)
    534 else:
    535     # otherwise we call the vectorized version of predict
    536     Xt = self._vectorize("transform", X=X_inner, y=y_inner)

File [c:\ProgramData\Miniconda3\envs\DLL_Forecast\Lib\site-packages\sktime\transformations\series\impute.py:239](file:///C:/ProgramData/Miniconda3/envs/DLL_Forecast/Lib/site-packages/sktime/transformations/series/impute.py:239), in Imputer._transform(self, X, y)
    237     X = self._impute_with_forecaster(X, y)
    238 elif self.method == "forecaster":
--> 239     X = self._impute_with_forecaster(X, y)
    240 elif self.method == "mean":
    241     return X.fillna(value=self._mean)

File [c:\ProgramData\Miniconda3\envs\DLL_Forecast\Lib\site-packages\sktime\transformations\series\impute.py:343](file:///C:/ProgramData/Miniconda3/envs/DLL_Forecast/Lib/site-packages/sktime/transformations/series/impute.py:343), in Imputer._impute_with_forecaster(self, X, y)
    339 fh = ForecastingHorizon(values=na_index, is_relative=False)
    341 # fill NaN before fitting with ffill and backfill (heuristic)
--> 343 self._forecaster.fit(
    344     y=self._X[col].fillna(method="ffill").fillna(method="backfill"),
    345     X=self._y[col].fillna(method="ffill").fillna(method="backfill")
    346     if self._y is not None
    347     else None,
    348 )
    350 # replace missing values with predicted values
    351 X[col][na_index] = self._forecaster.predict(fh=fh, X=y)

File [c:\ProgramData\Miniconda3\envs\DLL_Forecast\Lib\site-packages\sktime\forecasting\base\_base.py:341](file:///C:/ProgramData/Miniconda3/envs/DLL_Forecast/Lib/site-packages/sktime/forecasting/base/_base.py:341), in BaseForecaster.fit(self, y, X, fh)
    338 self._update_y_X(y_inner, X_inner)
    340 # check forecasting horizon and coerce to ForecastingHorizon object
--> 341 fh = self._check_fh(fh)
    343 # checks and conversions complete, pass to inner fit
    344 #####################################################
    345 vectorization_needed = isinstance(y_inner, VectorizedDF)

File [c:\ProgramData\Miniconda3\envs\DLL_Forecast\Lib\site-packages\sktime\forecasting\base\_base.py:1758](file:///C:/ProgramData/Miniconda3/envs/DLL_Forecast/Lib/site-packages/sktime/forecasting/base/_base.py:1758), in BaseForecaster._check_fh(self, fh)
   1751         # in case C. fh is not optional in fit: this is fine
   1752         # any error would have already been caught in fit
   1753 
   1754     # A. strategy not fitted (call of fit)
   1755     elif requires_fh:
   1756         # in case fh is not optional in fit:
   1757         # fh must be passed in fit
-> 1758         raise ValueError(
   1759             "The forecasting horizon `fh` must be passed to "
   1760             f"`fit` of {self.__class__.__name__}, but none was found. " + msg
   1761         )
   1762         # in case C. fh is optional in fit:
   1763         # this is fine, nothing to check/raise
   1764 
   1765 # B. fh is passed
   1766 else:
   1767     # If fh is passed, coerce to ForecastingHorizon and validate (all cases)
   1768     fh = check_fh(fh=fh, freq=self._cutoff)

ValueError: The forecasting horizon `fh` must be passed to `fit` of MultioutputTabularRegressionForecaster, but none was found. This is because fitting of the forecaster MultioutputTabularRegressionForecaster depends on `fh`.
```

**Expected behavior**
As far as I understand or what I would expect : 

- The Imputer only forecasts the values missing within the training data, so fh is kind-of a duplicate or unneeded information.
- I can't even pass a fh as it's not allowed in any of the methods.

Am I doing something wrong or missing something?
I couldn't find any example here or online, so that's why I am asking.

**Versions**
<details>

System:
    python: 3.11.4 | packaged by conda-forge | (main, Jun 10 2023, 17:59:51) [MSC v.1935 64 bit (AMD64)]
executable: [c:\ProgramData\Miniconda3\envs\DLL_Forecast\python.exe](file:///C:/ProgramData/Miniconda3/envs/DLL_Forecast/python.exe)
   machine: Windows-10-10.0.19042-SP0

Python dependencies:
          pip: 23.2.1
       sktime: 0.21.0
      sklearn: 1.3.0
       skbase: 0.4.6
        numpy: 1.24.4
        scipy: 1.11.1
       pandas: 2.0.3
   matplotlib: 3.7.2
       joblib: 1.3.0
  statsmodels: 0.14.0
        numba: 0.57.1
     pmdarima: None
      tsfresh: None
   tensorflow: None
tensorflow_probability: None

</details>


Discussion:
It looks like you found a bug!

The `fh` is not passed to `fit` of the forecaster, even though it's right there.
That is, the `fh` computed on line 339 should be passed to `fit` on line 343.

This does not cause a failure in the tests, since it *is* passed in `predict`, and the foreceaster we test with (`TrendForecaster`, see `get_test_params`) does require it only in `predict`, not in `fit`.
So, we should add another test setting with a forecaster that requires `fh` in `fit` - optimally a fast one so the tests do not run too long.

Do you want to make a PR with the fix?
I.e., passing `fit`, andn addig a test case in `get_test_params`?

We can also do it, but since you spotted it, and you may like to get some contributor credits?
Sure. Never had to make a PR on an outside project.
I'll see if I can make it 👍 
> I'll see if I can make it

Sure you can! Your profile says you make 100 PR a month, so what can go wrong 😁 

Resolving patch:
diff --git a/sktime/transformations/series/impute.py b/sktime/transformations/series/impute.py
--- a/sktime/transformations/series/impute.py
+++ b/sktime/transformations/series/impute.py
@@ -345,6 +345,7 @@ def _impute_with_forecaster(self, X, y):
                     X=self._y[col].fillna(method="ffill").fillna(method="backfill")
                     if self._y is not None
                     else None,
+                    fh=fh,
                 )
 
                 # replace missing values with predicted values
@@ -370,8 +371,13 @@ def get_test_params(cls, parameter_set="default"):
             `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
             `create_test_instance` uses the first (or only) dictionary in `params`
         """
+        from sklearn.linear_model import LinearRegression
+
+        from sktime.forecasting.compose import make_reduction
         from sktime.forecasting.trend import TrendForecaster
 
+        linear_forecaster = make_reduction(LinearRegression(), strategy="multioutput")
+
         return [
             {"method": "drift"},
             {"method": "linear"},
@@ -383,6 +389,7 @@ def get_test_params(cls, parameter_set="default"):
             {"method": "pad"},
             {"method": "random"},
             {"method": "forecaster", "forecaster": TrendForecaster()},
+            {"method": "forecaster", "forecaster": linear_forecaster},
         ]
 
 


Test patch: