We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ENH] Imputer for multivariate timeseries
**Is your feature request related to a problem? Please describe.**

Imputer Transformation (sktime.transformations.series.impute.Imputer) works only with univariate time series. So that one does not have to manipulate the data laboriously before, a multivariate version of Imputer would help. sktime.transformations.series.compose -> ColumnwiseTransformer could work with the Imputer. Is it planned to provide a multivariate imputer or should the ColumnwiseTransformer always be applied?

**Describe the solution you'd like**
A query of the dimension of the input data could be prefixed so that only one Imputer version is needed.

```
from sktime.transformations.base import _SeriesToSeriesTransformer
from sktime.transformations.series.compose import ColumnwiseTransformer
from sktime.transformations.series.impute import Imputer

__author__ = ["Martin Walter"]
__all__ = ["ImputerMultivariate"]

class ImputerMultivariate(_SeriesToSeriesTransformer):
    """Missing value imputation of multivariate timeseries.

        The Imputer transforms input series by replacing missing values according
        to an imputation strategy specified by `method`.

        Parameters
        ----------
        method : str, default="drift"
            Method to fill the missing values values.

            * "drift" : drift/trend values by sktime.PolynomialTrendForecaster()
            * "linear" : linear interpolation, by pd.Series.interpolate()
            * "nearest" : use nearest value, by pd.Series.interpolate()
            * "constant" : same constant value (given in arg value) for all NaN
            * "mean" : pd.Series.mean()
            * "median" : pd.Series.median()
            * "backfill" ot "bfill" : adapted from pd.Series.fillna()
            * "pad" or "ffill" : adapted from pd.Series.fillna()
            * "random" : random values between pd.Series.min() and .max()
            * "forecaster" : use an sktime Forecaster, given in arg forecaster

        missing_values : int/float/str, default=None
            The placeholder for the missing values. All occurrences of
            missing_values will be imputed. If None then np.nan is used.
        value : int/float, default=None
            Value to use to fill missing values when method="constant".
        forecaster : Any Forecaster based on sktime.BaseForecaster, default=None
            Use a given Forecaster to impute by insample predictions when
            method="forecaster". Before fitting, missing data is imputed with
            method="ffill" or "bfill" as heuristic.
        random_state : int/float/str, optional
            Value to set random.seed() if method="random", default None

        Examples
        --------
        >>> from sktime.transformations.series.impute import Imputer
        >>> from sktime.datasets import load_airline
        >>> y = load_airline()
        >>> transformer = Imputer(method="drift")
        >>> y_hat = transformer.fit_transform(y)
    """
    _tags = {
            "fit-in-transform": True,
            "handles-missing-data": True,
            "skip-inverse-transform": True,
        }
    def __init__(
        self, 
        method="drift", 
        random_state=None, 
        value=None,
        forecaster=None,
        missing_values=None):

        self.transformer = ColumnwiseTransformer(
            Imputer(
            method=method,
            random_state=random_state,
            value=value,
            forecaster=forecaster,
            missing_values=missing_values,
        )
        )
        super(ImputerMultivariate, self).__init__()
    
    def fit(self, X, y=None):
        self._is_fitted = True
        self.transformer.fit(X, y)
        return self

    def transform(self, X, y=None):
        X = self.transformer.transform(X, y)
        return X
```


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `sktime/transformations/series/impute.py`
Content:
```
1 #!/usr/bin/env python3 -u
2 # -*- coding: utf-8 -*-
3 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
4 """Utilities to impute series with missing values."""
5 
6 __author__ = ["Martin Walter"]
7 __all__ = ["Imputer"]
8 
9 from sktime.transformations.base import _SeriesToSeriesTransformer
10 from sktime.utils.validation.series import check_series
11 from sktime.forecasting.trend import PolynomialTrendForecaster
12 from sklearn.utils import check_random_state
13 from sktime.forecasting.base import ForecastingHorizon
14 from sklearn.base import clone
15 
16 import numpy as np
17 import pandas as pd
18 
19 
20 class Imputer(_SeriesToSeriesTransformer):
21     """Missing value imputation.
22 
23     The Imputer transforms input series by replacing missing values according
24     to an imputation strategy specified by `method`.
25 
26     Parameters
27     ----------
28     method : str, default="drift"
29         Method to fill the missing values values.
30 
31         * "drift" : drift/trend values by sktime.PolynomialTrendForecaster()
32         * "linear" : linear interpolation, by pd.Series.interpolate()
33         * "nearest" : use nearest value, by pd.Series.interpolate()
34         * "constant" : same constant value (given in arg value) for all NaN
35         * "mean" : pd.Series.mean()
36         * "median" : pd.Series.median()
37         * "backfill" ot "bfill" : adapted from pd.Series.fillna()
38         * "pad" or "ffill" : adapted from pd.Series.fillna()
39         * "random" : random values between pd.Series.min() and .max()
40         * "forecaster" : use an sktime Forecaster, given in arg forecaster
41 
42     missing_values : int/float/str, default=None
43         The placeholder for the missing values. All occurrences of
44         missing_values will be imputed. If None then np.nan is used.
45     value : int/float, default=None
46         Value to use to fill missing values when method="constant".
47     forecaster : Any Forecaster based on sktime.BaseForecaster, default=None
48         Use a given Forecaster to impute by insample predictions when
49         method="forecaster". Before fitting, missing data is imputed with
50         method="ffill" or "bfill" as heuristic.
51     random_state : int/float/str, optional
52         Value to set random.seed() if method="random", default None
53 
54     Examples
55     --------
56     >>> from sktime.transformations.series.impute import Imputer
57     >>> from sktime.datasets import load_airline
58     >>> y = load_airline()
59     >>> transformer = Imputer(method="drift")
60     >>> y_hat = transformer.fit_transform(y)
61     """
62 
63     _tags = {
64         "fit-in-transform": True,
65         "handles-missing-data": True,
66         "skip-inverse-transform": True,
67     }
68 
69     def __init__(
70         self,
71         method="drift",
72         random_state=None,
73         value=None,
74         forecaster=None,
75         missing_values=None,
76     ):
77 
78         self.method = method
79         self.missing_values = missing_values
80         self.value = value
81         self.forecaster = forecaster
82         self.random_state = random_state
83         super(Imputer, self).__init__()
84 
85     def transform(self, Z, X=None):
86         """Transform data.
87 
88         Returns a transformed version of Z.
89 
90         Parameters
91         ----------
92         Z : pd.Series, pd.DataFrame
93 
94         Returns
95         -------
96         Z : pd.Series, pd.DataFrame
97             Transformed time series(es).
98         """
99         self.check_is_fitted()
100         self._check_method()
101         Z = check_series(Z)
102         Z = Z.copy()
103 
104         # replace missing_values with np.nan
105         if self.missing_values:
106             Z = Z.replace(to_replace=self.missing_values, value=np.nan)
107 
108         if not _has_missing_values(Z):
109             return Z
110 
111         elif self.method == "random":
112             if isinstance(Z, pd.DataFrame):
113                 for col in Z:
114                     Z[col] = Z[col].apply(
115                         lambda i: self._get_random(Z[col]) if np.isnan(i) else i
116                     )
117             else:
118                 Z = Z.apply(lambda i: self._get_random(Z) if np.isnan(i) else i)
119         elif self.method == "constant":
120             Z = Z.fillna(value=self.value)
121         elif self.method in ["backfill", "bfill", "pad", "ffill"]:
122             Z = Z.fillna(method=self.method)
123         elif self.method == "drift":
124             forecaster = PolynomialTrendForecaster(degree=1)
125             Z = _impute_with_forecaster(forecaster, Z)
126         elif self.method == "forecaster":
127             forecaster = clone(self.forecaster)
128             Z = _impute_with_forecaster(forecaster, Z)
129         elif self.method == "mean":
130             Z = Z.fillna(value=Z.mean())
131         elif self.method == "median":
132             Z = Z.fillna(value=Z.median())
133         elif self.method in ["nearest", "linear"]:
134             Z = Z.interpolate(method=self.method)
135         else:
136             raise ValueError(f"`method`: {self.method} not available.")
137         # fill first/last elements of series,
138         # as some methods (e.g. "linear") cant impute those
139         Z = Z.fillna(method="ffill").fillna(method="backfill")
140         return Z
141 
142     def _check_method(self):
143         if (
144             self.value is not None
145             and self.method != "constant"
146             or self.method == "constant"
147             and self.value is None
148         ):
149             raise ValueError(
150                 """Imputing with a value can only be
151                 used if method="constant" and if parameter "value" is not None"""
152             )
153         elif (
154             self.forecaster is not None
155             and self.method != "forecaster"
156             or self.method == "forecaster"
157             and self.forecaster is None
158         ):
159             raise ValueError(
160                 """Imputing with a forecaster can only be used if
161                 method=\"forecaster\" and if arg forecaster is not None"""
162             )
163         else:
164             pass
165 
166     def _get_random(self, Z):
167         """Create a random int or float value.
168 
169         :param Z: Series
170         :type Z: pd.Series
171         :return: Random int or float between min and max of Z
172         :rtype: int/float
173         """
174         rng = check_random_state(self.random_state)
175         # check if series contains only int or int-like values (e.g. 3.0)
176         if (Z.dropna() % 1 == 0).all():
177             return rng.randint(Z.min(), Z.max())
178         else:
179             return rng.uniform(Z.min(), Z.max())
180 
181 
182 def _impute_with_forecaster(forecaster, Z):
183     """Use a given forecaster for imputation by in-sample predictions.
184 
185     Parameters
186     ----------
187     forecaster: Forecaster
188         Forecaster to use for imputation
189     Z : pd.Series or pd.DataFrame
190         Series to impute.
191 
192     Returns
193     -------
194     zt : pd.Series or pd.DataFrame
195         Series with imputed values.
196     """
197     if isinstance(Z, pd.Series):
198         series = [Z]
199     elif isinstance(Z, pd.DataFrame):
200         series = [Z[column] for column in Z]
201 
202     for z in series:
203         # define fh based on index of missing values
204         na_index = z.index[z.isna()]
205         fh = ForecastingHorizon(values=na_index, is_relative=False)
206 
207         # fill NaN before fitting with ffill and backfill (heuristic)
208         forecaster.fit(y=z.fillna(method="ffill").fillna(method="backfill"), fh=fh)
209 
210         # replace missing values with predicted values
211         z[na_index] = forecaster.predict()
212     return Z
213 
214 
215 def _has_missing_values(Z):
216     return Z.isnull().to_numpy().any()
217
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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
@@ -6,15 +6,17 @@
 __author__ = ["Martin Walter"]
 __all__ = ["Imputer"]
 
+
+import numpy as np
+import pandas as pd
+
+from sklearn.base import clone
+from sklearn.utils import check_random_state
+
 from sktime.transformations.base import _SeriesToSeriesTransformer
 from sktime.utils.validation.series import check_series
 from sktime.forecasting.trend import PolynomialTrendForecaster
-from sklearn.utils import check_random_state
 from sktime.forecasting.base import ForecastingHorizon
-from sklearn.base import clone
-
-import numpy as np
-import pandas as pd
 
 
 class Imputer(_SeriesToSeriesTransformer):
@@ -64,6 +66,7 @@
         "fit-in-transform": True,
         "handles-missing-data": True,
         "skip-inverse-transform": True,
+        "univariate-only": False,
     }
 
     def __init__(

{"golden_diff": "diff --git a/sktime/transformations/series/impute.py b/sktime/transformations/series/impute.py\n--- a/sktime/transformations/series/impute.py\n+++ b/sktime/transformations/series/impute.py\n@@ -6,15 +6,17 @@\n __author__ = [\"Martin Walter\"]\n __all__ = [\"Imputer\"]\n \n+\n+import numpy as np\n+import pandas as pd\n+\n+from sklearn.base import clone\n+from sklearn.utils import check_random_state\n+\n from sktime.transformations.base import _SeriesToSeriesTransformer\n from sktime.utils.validation.series import check_series\n from sktime.forecasting.trend import PolynomialTrendForecaster\n-from sklearn.utils import check_random_state\n from sktime.forecasting.base import ForecastingHorizon\n-from sklearn.base import clone\n-\n-import numpy as np\n-import pandas as pd\n \n \n class Imputer(_SeriesToSeriesTransformer):\n@@ -64,6 +66,7 @@\n         \"fit-in-transform\": True,\n         \"handles-missing-data\": True,\n         \"skip-inverse-transform\": True,\n+        \"univariate-only\": False,\n     }\n \n     def __init__(\n", "issue": "[ENH] Imputer for multivariate timeseries\n**Is your feature request related to a problem? Please describe.**\r\n\r\nImputer Transformation (sktime.transformations.series.impute.Imputer) works only with univariate time series. So that one does not have to manipulate the data laboriously before, a multivariate version of Imputer would help. sktime.transformations.series.compose -> ColumnwiseTransformer could work with the Imputer. Is it planned to provide a multivariate imputer or should the ColumnwiseTransformer always be applied?\r\n\r\n**Describe the solution you'd like**\r\nA query of the dimension of the input data could be prefixed so that only one Imputer version is needed.\r\n\r\n```\r\nfrom sktime.transformations.base import _SeriesToSeriesTransformer\r\nfrom sktime.transformations.series.compose import ColumnwiseTransformer\r\nfrom sktime.transformations.series.impute import Imputer\r\n\r\n__author__ = [\"Martin Walter\"]\r\n__all__ = [\"ImputerMultivariate\"]\r\n\r\nclass ImputerMultivariate(_SeriesToSeriesTransformer):\r\n    \"\"\"Missing value imputation of multivariate timeseries.\r\n\r\n        The Imputer transforms input series by replacing missing values according\r\n        to an imputation strategy specified by `method`.\r\n\r\n        Parameters\r\n        ----------\r\n        method : str, default=\"drift\"\r\n            Method to fill the missing values values.\r\n\r\n            * \"drift\" : drift/trend values by sktime.PolynomialTrendForecaster()\r\n            * \"linear\" : linear interpolation, by pd.Series.interpolate()\r\n            * \"nearest\" : use nearest value, by pd.Series.interpolate()\r\n            * \"constant\" : same constant value (given in arg value) for all NaN\r\n            * \"mean\" : pd.Series.mean()\r\n            * \"median\" : pd.Series.median()\r\n            * \"backfill\" ot \"bfill\" : adapted from pd.Series.fillna()\r\n            * \"pad\" or \"ffill\" : adapted from pd.Series.fillna()\r\n            * \"random\" : random values between pd.Series.min() and .max()\r\n            * \"forecaster\" : use an sktime Forecaster, given in arg forecaster\r\n\r\n        missing_values : int/float/str, default=None\r\n            The placeholder for the missing values. All occurrences of\r\n            missing_values will be imputed. If None then np.nan is used.\r\n        value : int/float, default=None\r\n            Value to use to fill missing values when method=\"constant\".\r\n        forecaster : Any Forecaster based on sktime.BaseForecaster, default=None\r\n            Use a given Forecaster to impute by insample predictions when\r\n            method=\"forecaster\". Before fitting, missing data is imputed with\r\n            method=\"ffill\" or \"bfill\" as heuristic.\r\n        random_state : int/float/str, optional\r\n            Value to set random.seed() if method=\"random\", default None\r\n\r\n        Examples\r\n        --------\r\n        >>> from sktime.transformations.series.impute import Imputer\r\n        >>> from sktime.datasets import load_airline\r\n        >>> y = load_airline()\r\n        >>> transformer = Imputer(method=\"drift\")\r\n        >>> y_hat = transformer.fit_transform(y)\r\n    \"\"\"\r\n    _tags = {\r\n            \"fit-in-transform\": True,\r\n            \"handles-missing-data\": True,\r\n            \"skip-inverse-transform\": True,\r\n        }\r\n    def __init__(\r\n        self, \r\n        method=\"drift\", \r\n        random_state=None, \r\n        value=None,\r\n        forecaster=None,\r\n        missing_values=None):\r\n\r\n        self.transformer = ColumnwiseTransformer(\r\n            Imputer(\r\n            method=method,\r\n            random_state=random_state,\r\n            value=value,\r\n            forecaster=forecaster,\r\n            missing_values=missing_values,\r\n        )\r\n        )\r\n        super(ImputerMultivariate, self).__init__()\r\n    \r\n    def fit(self, X, y=None):\r\n        self._is_fitted = True\r\n        self.transformer.fit(X, y)\r\n        return self\r\n\r\n    def transform(self, X, y=None):\r\n        X = self.transformer.transform(X, y)\r\n        return X\r\n```\r\n\n", "before_files": [{"content": "#!/usr/bin/env python3 -u\n# -*- coding: utf-8 -*-\n# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)\n\"\"\"Utilities to impute series with missing values.\"\"\"\n\n__author__ = [\"Martin Walter\"]\n__all__ = [\"Imputer\"]\n\nfrom sktime.transformations.base import _SeriesToSeriesTransformer\nfrom sktime.utils.validation.series import check_series\nfrom sktime.forecasting.trend import PolynomialTrendForecaster\nfrom sklearn.utils import check_random_state\nfrom sktime.forecasting.base import ForecastingHorizon\nfrom sklearn.base import clone\n\nimport numpy as np\nimport pandas as pd\n\n\nclass Imputer(_SeriesToSeriesTransformer):\n    \"\"\"Missing value imputation.\n\n    The Imputer transforms input series by replacing missing values according\n    to an imputation strategy specified by `method`.\n\n    Parameters\n    ----------\n    method : str, default=\"drift\"\n        Method to fill the missing values values.\n\n        * \"drift\" : drift/trend values by sktime.PolynomialTrendForecaster()\n        * \"linear\" : linear interpolation, by pd.Series.interpolate()\n        * \"nearest\" : use nearest value, by pd.Series.interpolate()\n        * \"constant\" : same constant value (given in arg value) for all NaN\n        * \"mean\" : pd.Series.mean()\n        * \"median\" : pd.Series.median()\n        * \"backfill\" ot \"bfill\" : adapted from pd.Series.fillna()\n        * \"pad\" or \"ffill\" : adapted from pd.Series.fillna()\n        * \"random\" : random values between pd.Series.min() and .max()\n        * \"forecaster\" : use an sktime Forecaster, given in arg forecaster\n\n    missing_values : int/float/str, default=None\n        The placeholder for the missing values. All occurrences of\n        missing_values will be imputed. If None then np.nan is used.\n    value : int/float, default=None\n        Value to use to fill missing values when method=\"constant\".\n    forecaster : Any Forecaster based on sktime.BaseForecaster, default=None\n        Use a given Forecaster to impute by insample predictions when\n        method=\"forecaster\". Before fitting, missing data is imputed with\n        method=\"ffill\" or \"bfill\" as heuristic.\n    random_state : int/float/str, optional\n        Value to set random.seed() if method=\"random\", default None\n\n    Examples\n    --------\n    >>> from sktime.transformations.series.impute import Imputer\n    >>> from sktime.datasets import load_airline\n    >>> y = load_airline()\n    >>> transformer = Imputer(method=\"drift\")\n    >>> y_hat = transformer.fit_transform(y)\n    \"\"\"\n\n    _tags = {\n        \"fit-in-transform\": True,\n        \"handles-missing-data\": True,\n        \"skip-inverse-transform\": True,\n    }\n\n    def __init__(\n        self,\n        method=\"drift\",\n        random_state=None,\n        value=None,\n        forecaster=None,\n        missing_values=None,\n    ):\n\n        self.method = method\n        self.missing_values = missing_values\n        self.value = value\n        self.forecaster = forecaster\n        self.random_state = random_state\n        super(Imputer, self).__init__()\n\n    def transform(self, Z, X=None):\n        \"\"\"Transform data.\n\n        Returns a transformed version of Z.\n\n        Parameters\n        ----------\n        Z : pd.Series, pd.DataFrame\n\n        Returns\n        -------\n        Z : pd.Series, pd.DataFrame\n            Transformed time series(es).\n        \"\"\"\n        self.check_is_fitted()\n        self._check_method()\n        Z = check_series(Z)\n        Z = Z.copy()\n\n        # replace missing_values with np.nan\n        if self.missing_values:\n            Z = Z.replace(to_replace=self.missing_values, value=np.nan)\n\n        if not _has_missing_values(Z):\n            return Z\n\n        elif self.method == \"random\":\n            if isinstance(Z, pd.DataFrame):\n                for col in Z:\n                    Z[col] = Z[col].apply(\n                        lambda i: self._get_random(Z[col]) if np.isnan(i) else i\n                    )\n            else:\n                Z = Z.apply(lambda i: self._get_random(Z) if np.isnan(i) else i)\n        elif self.method == \"constant\":\n            Z = Z.fillna(value=self.value)\n        elif self.method in [\"backfill\", \"bfill\", \"pad\", \"ffill\"]:\n            Z = Z.fillna(method=self.method)\n        elif self.method == \"drift\":\n            forecaster = PolynomialTrendForecaster(degree=1)\n            Z = _impute_with_forecaster(forecaster, Z)\n        elif self.method == \"forecaster\":\n            forecaster = clone(self.forecaster)\n            Z = _impute_with_forecaster(forecaster, Z)\n        elif self.method == \"mean\":\n            Z = Z.fillna(value=Z.mean())\n        elif self.method == \"median\":\n            Z = Z.fillna(value=Z.median())\n        elif self.method in [\"nearest\", \"linear\"]:\n            Z = Z.interpolate(method=self.method)\n        else:\n            raise ValueError(f\"`method`: {self.method} not available.\")\n        # fill first/last elements of series,\n        # as some methods (e.g. \"linear\") cant impute those\n        Z = Z.fillna(method=\"ffill\").fillna(method=\"backfill\")\n        return Z\n\n    def _check_method(self):\n        if (\n            self.value is not None\n            and self.method != \"constant\"\n            or self.method == \"constant\"\n            and self.value is None\n        ):\n            raise ValueError(\n                \"\"\"Imputing with a value can only be\n                used if method=\"constant\" and if parameter \"value\" is not None\"\"\"\n            )\n        elif (\n            self.forecaster is not None\n            and self.method != \"forecaster\"\n            or self.method == \"forecaster\"\n            and self.forecaster is None\n        ):\n            raise ValueError(\n                \"\"\"Imputing with a forecaster can only be used if\n                method=\\\"forecaster\\\" and if arg forecaster is not None\"\"\"\n            )\n        else:\n            pass\n\n    def _get_random(self, Z):\n        \"\"\"Create a random int or float value.\n\n        :param Z: Series\n        :type Z: pd.Series\n        :return: Random int or float between min and max of Z\n        :rtype: int/float\n        \"\"\"\n        rng = check_random_state(self.random_state)\n        # check if series contains only int or int-like values (e.g. 3.0)\n        if (Z.dropna() % 1 == 0).all():\n            return rng.randint(Z.min(), Z.max())\n        else:\n            return rng.uniform(Z.min(), Z.max())\n\n\ndef _impute_with_forecaster(forecaster, Z):\n    \"\"\"Use a given forecaster for imputation by in-sample predictions.\n\n    Parameters\n    ----------\n    forecaster: Forecaster\n        Forecaster to use for imputation\n    Z : pd.Series or pd.DataFrame\n        Series to impute.\n\n    Returns\n    -------\n    zt : pd.Series or pd.DataFrame\n        Series with imputed values.\n    \"\"\"\n    if isinstance(Z, pd.Series):\n        series = [Z]\n    elif isinstance(Z, pd.DataFrame):\n        series = [Z[column] for column in Z]\n\n    for z in series:\n        # define fh based on index of missing values\n        na_index = z.index[z.isna()]\n        fh = ForecastingHorizon(values=na_index, is_relative=False)\n\n        # fill NaN before fitting with ffill and backfill (heuristic)\n        forecaster.fit(y=z.fillna(method=\"ffill\").fillna(method=\"backfill\"), fh=fh)\n\n        # replace missing values with predicted values\n        z[na_index] = forecaster.predict()\n    return Z\n\n\ndef _has_missing_values(Z):\n    return Z.isnull().to_numpy().any()\n", "path": "sktime/transformations/series/impute.py"}], "after_files": [{"content": "#!/usr/bin/env python3 -u\n# -*- coding: utf-8 -*-\n# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)\n\"\"\"Utilities to impute series with missing values.\"\"\"\n\n__author__ = [\"Martin Walter\"]\n__all__ = [\"Imputer\"]\n\n\nimport numpy as np\nimport pandas as pd\n\nfrom sklearn.base import clone\nfrom sklearn.utils import check_random_state\n\nfrom sktime.transformations.base import _SeriesToSeriesTransformer\nfrom sktime.utils.validation.series import check_series\nfrom sktime.forecasting.trend import PolynomialTrendForecaster\nfrom sktime.forecasting.base import ForecastingHorizon\n\n\nclass Imputer(_SeriesToSeriesTransformer):\n    \"\"\"Missing value imputation.\n\n    The Imputer transforms input series by replacing missing values according\n    to an imputation strategy specified by `method`.\n\n    Parameters\n    ----------\n    method : str, default=\"drift\"\n        Method to fill the missing values values.\n\n        * \"drift\" : drift/trend values by sktime.PolynomialTrendForecaster()\n        * \"linear\" : linear interpolation, by pd.Series.interpolate()\n        * \"nearest\" : use nearest value, by pd.Series.interpolate()\n        * \"constant\" : same constant value (given in arg value) for all NaN\n        * \"mean\" : pd.Series.mean()\n        * \"median\" : pd.Series.median()\n        * \"backfill\" ot \"bfill\" : adapted from pd.Series.fillna()\n        * \"pad\" or \"ffill\" : adapted from pd.Series.fillna()\n        * \"random\" : random values between pd.Series.min() and .max()\n        * \"forecaster\" : use an sktime Forecaster, given in arg forecaster\n\n    missing_values : int/float/str, default=None\n        The placeholder for the missing values. All occurrences of\n        missing_values will be imputed. If None then np.nan is used.\n    value : int/float, default=None\n        Value to use to fill missing values when method=\"constant\".\n    forecaster : Any Forecaster based on sktime.BaseForecaster, default=None\n        Use a given Forecaster to impute by insample predictions when\n        method=\"forecaster\". Before fitting, missing data is imputed with\n        method=\"ffill\" or \"bfill\" as heuristic.\n    random_state : int/float/str, optional\n        Value to set random.seed() if method=\"random\", default None\n\n    Examples\n    --------\n    >>> from sktime.transformations.series.impute import Imputer\n    >>> from sktime.datasets import load_airline\n    >>> y = load_airline()\n    >>> transformer = Imputer(method=\"drift\")\n    >>> y_hat = transformer.fit_transform(y)\n    \"\"\"\n\n    _tags = {\n        \"fit-in-transform\": True,\n        \"handles-missing-data\": True,\n        \"skip-inverse-transform\": True,\n        \"univariate-only\": False,\n    }\n\n    def __init__(\n        self,\n        method=\"drift\",\n        random_state=None,\n        value=None,\n        forecaster=None,\n        missing_values=None,\n    ):\n\n        self.method = method\n        self.missing_values = missing_values\n        self.value = value\n        self.forecaster = forecaster\n        self.random_state = random_state\n        super(Imputer, self).__init__()\n\n    def transform(self, Z, X=None):\n        \"\"\"Transform data.\n\n        Returns a transformed version of Z.\n\n        Parameters\n        ----------\n        Z : pd.Series, pd.DataFrame\n\n        Returns\n        -------\n        Z : pd.Series, pd.DataFrame\n            Transformed time series(es).\n        \"\"\"\n        self.check_is_fitted()\n        self._check_method()\n        Z = check_series(Z)\n        Z = Z.copy()\n\n        # replace missing_values with np.nan\n        if self.missing_values:\n            Z = Z.replace(to_replace=self.missing_values, value=np.nan)\n\n        if not _has_missing_values(Z):\n            return Z\n\n        elif self.method == \"random\":\n            if isinstance(Z, pd.DataFrame):\n                for col in Z:\n                    Z[col] = Z[col].apply(\n                        lambda i: self._get_random(Z[col]) if np.isnan(i) else i\n                    )\n            else:\n                Z = Z.apply(lambda i: self._get_random(Z) if np.isnan(i) else i)\n        elif self.method == \"constant\":\n            Z = Z.fillna(value=self.value)\n        elif self.method in [\"backfill\", \"bfill\", \"pad\", \"ffill\"]:\n            Z = Z.fillna(method=self.method)\n        elif self.method == \"drift\":\n            forecaster = PolynomialTrendForecaster(degree=1)\n            Z = _impute_with_forecaster(forecaster, Z)\n        elif self.method == \"forecaster\":\n            forecaster = clone(self.forecaster)\n            Z = _impute_with_forecaster(forecaster, Z)\n        elif self.method == \"mean\":\n            Z = Z.fillna(value=Z.mean())\n        elif self.method == \"median\":\n            Z = Z.fillna(value=Z.median())\n        elif self.method in [\"nearest\", \"linear\"]:\n            Z = Z.interpolate(method=self.method)\n        else:\n            raise ValueError(f\"`method`: {self.method} not available.\")\n        # fill first/last elements of series,\n        # as some methods (e.g. \"linear\") cant impute those\n        Z = Z.fillna(method=\"ffill\").fillna(method=\"backfill\")\n        return Z\n\n    def _check_method(self):\n        if (\n            self.value is not None\n            and self.method != \"constant\"\n            or self.method == \"constant\"\n            and self.value is None\n        ):\n            raise ValueError(\n                \"\"\"Imputing with a value can only be\n                used if method=\"constant\" and if parameter \"value\" is not None\"\"\"\n            )\n        elif (\n            self.forecaster is not None\n            and self.method != \"forecaster\"\n            or self.method == \"forecaster\"\n            and self.forecaster is None\n        ):\n            raise ValueError(\n                \"\"\"Imputing with a forecaster can only be used if\n                method=\\\"forecaster\\\" and if arg forecaster is not None\"\"\"\n            )\n        else:\n            pass\n\n    def _get_random(self, Z):\n        \"\"\"Create a random int or float value.\n\n        :param Z: Series\n        :type Z: pd.Series\n        :return: Random int or float between min and max of Z\n        :rtype: int/float\n        \"\"\"\n        rng = check_random_state(self.random_state)\n        # check if series contains only int or int-like values (e.g. 3.0)\n        if (Z.dropna() % 1 == 0).all():\n            return rng.randint(Z.min(), Z.max())\n        else:\n            return rng.uniform(Z.min(), Z.max())\n\n\ndef _impute_with_forecaster(forecaster, Z):\n    \"\"\"Use a given forecaster for imputation by in-sample predictions.\n\n    Parameters\n    ----------\n    forecaster: Forecaster\n        Forecaster to use for imputation\n    Z : pd.Series or pd.DataFrame\n        Series to impute.\n\n    Returns\n    -------\n    zt : pd.Series or pd.DataFrame\n        Series with imputed values.\n    \"\"\"\n    if isinstance(Z, pd.Series):\n        series = [Z]\n    elif isinstance(Z, pd.DataFrame):\n        series = [Z[column] for column in Z]\n\n    for z in series:\n        # define fh based on index of missing values\n        na_index = z.index[z.isna()]\n        fh = ForecastingHorizon(values=na_index, is_relative=False)\n\n        # fill NaN before fitting with ffill and backfill (heuristic)\n        forecaster.fit(y=z.fillna(method=\"ffill\").fillna(method=\"backfill\"), fh=fh)\n\n        # replace missing values with predicted values\n        z[na_index] = forecaster.predict()\n    return Z\n\n\ndef _has_missing_values(Z):\n    return Z.isnull().to_numpy().any()\n", "path": "sktime/transformations/series/impute.py"}]}