Repository: sktime/sktime

Issue:
Make transformer for detrending support multivariate series
**Is your feature request related to a problem? Please describe.**
For the EEG applications we need a transformer that detrends each channel. Currently the detrending transformer only supports univariate time series.

**Describe the solution you'd like**
Detrend each univariate series separately 


Discussion:

Resolving patch:
diff --git a/sktime/transformations/series/detrend/_detrend.py b/sktime/transformations/series/detrend/_detrend.py
--- a/sktime/transformations/series/detrend/_detrend.py
+++ b/sktime/transformations/series/detrend/_detrend.py
@@ -6,6 +6,7 @@
 __author__ = ["Markus Löning"]
 
 from sklearn.base import clone
+import pandas as pd
 
 from sktime.forecasting.base._fh import ForecastingHorizon
 from sktime.transformations.base import _SeriesToSeriesTransformer
@@ -59,7 +60,7 @@ class Detrender(_SeriesToSeriesTransformer):
     """
 
     _required_parameters = ["forecaster"]
-    _tags = {"transform-returns-same-time-index": True, "univariate-only": True}
+    _tags = {"transform-returns-same-time-index": True}
 
     def __init__(self, forecaster=None):
         self.forecaster = forecaster
@@ -82,11 +83,20 @@ def fit(self, Z, X=None):
         self : an instance of self
         """
         self._is_fitted = False
-        z = check_series(Z, enforce_univariate=True)
+        z = check_series(Z)
         if self.forecaster is None:
             self.forecaster = PolynomialTrendForecaster(degree=1)
-        forecaster = clone(self.forecaster)
-        self.forecaster_ = forecaster.fit(z, X)
+
+        # multivariate
+        if isinstance(z, pd.DataFrame):
+            self.forecaster_ = {}
+            for colname in z.columns:
+                forecaster = clone(self.forecaster)
+                self.forecaster_[colname] = forecaster.fit(z[colname], X)
+        # univariate
+        else:
+            forecaster = clone(self.forecaster)
+            self.forecaster_ = forecaster.fit(z, X)
         self._is_fitted = True
         return self
 
@@ -107,10 +117,28 @@ def transform(self, Z, X=None):
             De-trended series
         """
         self.check_is_fitted()
-        z = check_series(Z, enforce_univariate=True)
+        z = check_series(Z)
         fh = ForecastingHorizon(z.index, is_relative=False)
-        z_pred = self.forecaster_.predict(fh, X)
-        return z - z_pred
+
+        # multivariate
+        if isinstance(z, pd.DataFrame):
+            # check if all columns are known
+            Z_fit_keys = set(self.forecaster_.keys())
+            Z_new_keys = set(z.columns)
+            difference = Z_new_keys.difference(Z_fit_keys)
+            if len(difference) != 0:
+                raise ValueError(
+                    "Z contains columns that have not been "
+                    "seen in fit: " + str(difference)
+                )
+            for colname in z.columns:
+                z_pred = self.forecaster_[colname].predict(fh, X)
+                z[colname] = z[colname] - z_pred
+            return z
+        # univariate
+        else:
+            z_pred = self.forecaster_.predict(fh, X)
+            return z - z_pred
 
     def inverse_transform(self, Z, X=None):
         """
@@ -129,10 +157,28 @@ def inverse_transform(self, Z, X=None):
             Series with the trend
         """
         self.check_is_fitted()
-        z = check_series(Z, enforce_univariate=True)
+        z = check_series(Z)
         fh = ForecastingHorizon(z.index, is_relative=False)
-        z_pred = self.forecaster_.predict(fh, X)
-        return z + z_pred
+
+        # multivariate
+        if isinstance(z, pd.DataFrame):
+            # check if all columns are known
+            Z_fit_keys = set(self.forecaster_.keys())
+            Z_new_keys = set(z.columns)
+            difference = Z_new_keys.difference(Z_fit_keys)
+            if len(difference) != 0:
+                raise ValueError(
+                    "Z contains columns that have not been "
+                    "seen in fit: " + difference
+                )
+            for colname in z.columns:
+                z_pred = self.forecaster_[colname].predict(fh, X)
+                z[colname] = z[colname] + z_pred
+            return z
+        # univariate
+        else:
+            z_pred = self.forecaster_.predict(fh, X)
+            return z + z_pred
 
     def update(self, Z, X=None, update_params=True):
         """
@@ -149,6 +195,23 @@ def update(self, Z, X=None, update_params=True):
         -------
         self : an instance of self
         """
-        z = check_series(Z, enforce_univariate=True, allow_empty=True)
-        self.forecaster_.update(z, X, update_params=update_params)
+        z = check_series(Z, allow_empty=True)
+        # multivariate
+        if isinstance(z, pd.DataFrame):
+            # check if all columns are known
+            Z_fit_keys = set(self.forecaster_.keys())
+            Z_new_keys = set(z.columns)
+            difference = Z_new_keys.difference(Z_fit_keys)
+            if len(difference) != 0:
+                raise ValueError(
+                    "Z contains columns that have not been "
+                    "seen in fit: " + str(difference)
+                )
+            for colname in z.columns:
+                self.forecaster_[colname].update(
+                    z[colname], X, update_params=update_params
+                )
+        # univariate
+        else:
+            self.forecaster_.update(z, X, update_params=update_params)
         return self


Test patch: