Repository: alan-turing-institute/sktime
Pull request #3266 (2022-08-14 10:28:33)

[ENH] constant distance dummy
This adds a constant distance/kernel dummy, useful as a baseline and in composition.


Patch:
diff --git a/sktime/datasets/_data_io.py b/sktime/datasets/_data_io.py
index 4106cf731..dbe764114 100644
--- a/sktime/datasets/_data_io.py
+++ b/sktime/datasets/_data_io.py
@@ -1573,9 +1573,9 @@ def write_ndarray_to_tsfile(
     # write class label line
     if class_label is not None:
         space_separated_class_label = " ".join(str(label) for label in class_label)
-        file.write(f"@classLabel true {space_separated_class_label}\n")
+        file.write(f"@classlabel true {space_separated_class_label}\n")
     else:
-        file.write("@class_label false\n")
+        file.write("@classlabel false\n")
     # begin writing the core data for each case
     # which are the series and the class value list if there is any
     file.write("@data\n")
diff --git a/sktime/dists_kernels/_base.py b/sktime/dists_kernels/_base.py
index 3087f4c05..f1222c9fe 100644
--- a/sktime/dists_kernels/_base.py
+++ b/sktime/dists_kernels/_base.py
@@ -187,7 +187,7 @@ class BasePairwiseTransformerPanel(BaseEstimator):
         "fit_is_empty": True,  # is "fit" empty? Yes, for all pairwise transforms
         "capability:missing_values": True,  # can estimator handle missing data?
         "capability:multivariate": True,  # can estimator handle multivariate data?
-        "capability:unequal_length": False,  # can dist handle unequal length panels?
+        "capability:unequal_length": True,  # can dist handle unequal length panels?
     }
 
     def __init__(self):
diff --git a/sktime/dists_kernels/algebra.py b/sktime/dists_kernels/algebra.py
new file mode 100644
index 000000000..394aee6ef
--- /dev/null
+++ b/sktime/dists_kernels/algebra.py
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+"""Arithmetics with distances/kernels, e.g., addition, multiplication."""
+
+__author__ = ["fkiraly"]
+
+import numpy as np
+
+from sktime.base import _HeterogenousMetaEstimator
+from sktime.dists_kernels._base import BasePairwiseTransformerPanel
+
+SUPPORTED_MTYPES = ["pd-multiindex", "nested_univ", "df-list", "numpy3D"]
+
+
+class CombinedDistance(BasePairwiseTransformerPanel, _HeterogenousMetaEstimator):
+    """Distances combined via arithmetic operation, e.g., addition, multiplication.
+
+    `CombinedDistance` creates a pairwise trafo from multiple other pairwise trafos,
+    by performing an arithmetic operation (np.ufunc) on the multiple distance matrices.
+
+    For a list of transformers `trafo1`, `trafo2`, ..., `trafoN`, ufunc `operation`,
+    this compositor behaves as follows:
+    `transform(X, X2)` - computes `dist1 = trafo1.transform(X, X2)`,
+        `dist2 = trafo2.transform(X, X2)`, ..., distN =  trafoN.transform(X, X2)`,
+        all of shape `(len(X), len(X2)`, then applies `operation` entry-wise,
+        to obtain a single matrix `dist` of shape `(len(X), len(X2)`
+        Example: if `operation = np.sum`, then `dist` is
+        the entry-wise sum of `dist1`, `dist2`, ..., `distN`
+
+    Parameters
+    ----------
+    pw_trafos : list of sktime pairwise panel distances, or
+        list of tuples (str, transformer) of sktime pairwise panel distances
+        distances combined to a single distance using the operation
+    operation : None, str, function, or numpy ufunc, optional, default = None = mean
+        if str, must be one of "mean", "+" (add), "*" (multiply), "max", "min"
+        if func, must be of signature (1D iterable) -> float
+        operation carried out on the distance matrices distances
+
+    Examples
+    --------
+    >>> from sktime.dists_kernels.algebra import CombinedDistance
+    >>> from sktime.dists_kernels.dtw import DtwDist
+    >>> from sktime.datasets import load_unit_test
+    >>>
+    >>> X, _ = load_unit_test()
+    >>> X = X[0:3]
+    >>> sum_dist = CombinedDistance([DtwDist(), DtwDist(weighted=True)], "+")
+    >>> dist_mat = sum_dist.transform(X)
+    """
+
+    _tags = {
+        "X_inner_mtype": SUPPORTED_MTYPES,
+        "capability:missing_values": True,  # can estimator handle missing data?
+        "capability:multivariate": True,  # can estimator handle multivariate data?
+        "capability:unequal_length": True,  # can dist handle unequal length panels?
+    }
+
+    def __init__(self, pw_trafos, operation=None):
+
+        self.pw_trafos = pw_trafos
+        self.pw_trafos_ = self._check_estimators(
+            self.pw_trafos, cls_type=BasePairwiseTransformerPanel
+        )
+        self.operation = operation
+        self._operation = self._resolve_operation(operation)
+
+        super(CombinedDistance, self).__init__()
+
+        # abbreviate for readability
+        ests = self.pw_trafos_
+
+        # set property tags based on tags of components
+        self._anytagis_then_set("fit_is_empty", False, True, ests)
+        self._anytagis_then_set("capability:missing_values", False, True, ests)
+        self._anytagis_then_set("capability:multivariate", False, True, ests)
+        self._anytagis_then_set("capability:unequal_length", False, True, ests)
+
+    def _resolve_operation(self, operation):
+        """Coerce operation to a numpy.ufunc."""
+        alias_dict = {
+            None: np.mean,
+            "mean": np.mean,
+            "+": np.add,
+            "add": np.add,
+            "*": np.multiply,
+            "mult": np.multiply,
+            "multiply": np.multiply,
+            "min": np.min,
+            "max": np.max,
+        }
+
+        if operation in alias_dict.keys():
+            return alias_dict[operation]
+        else:
+            return operation
+
+    @property
+    def _pw_trafos(self):
+        return self._get_estimator_tuples(self.pw_trafos, clone_ests=False)
+
+    @_pw_trafos.setter
+    def _pw_trafos(self, value):
+        self.pw_trafos = value
+
+    def _transform(self, X, X2=None):
+        """Compute distance/kernel matrix.
+
+        private _transform containing core logic, called from public transform
+
+        Parameters
+        ----------
+        X: sktime Panel data container
+        X2: sktime Panel data container
+
+        Returns
+        -------
+        distmat: np.array of shape [n, m]
+            (i,j)-th entry contains distance/kernel between X.iloc[i] and X2.iloc[j]
+        """
+        ests = self._get_estimator_list(self.pw_trafos_)
+
+        distmats = [est.transform(X=X, X2=X2) for est in ests]
+        distmat_stack = np.stack(distmats)
+
+        operation = self._operation
+        if isinstance(operation, np.ufunc):
+            distmat = operation.reduce(distmat_stack)
+        else:
+            distmat = np.apply_over_axes(operation, distmat_stack, 0)
+            distmat = distmat.squeeze(axis=0)
+
+        return distmat
+
+    def get_params(self, deep=True):
+        """Get parameters of estimator in `steps`.
+
+        Parameters
+        ----------
+        deep : boolean, optional, default=True
+            If True, will return the parameters for this estimator and
+            contained sub-objects that are estimators.
+
+        Returns
+        -------
+        params : mapping of string to any
+            Parameter names mapped to their values.
+        """
+        return self._get_params("_pw_trafos", deep=deep)
+
+    def set_params(self, **kwargs):
+        """Set the parameters of estimator in `steps`.
+
+        Valid parameter keys can be listed with ``get_params()``.
+
+        Returns
+        -------
+        self : returns an instance of self.
+        """
+        self._set_params("_pw_trafos", **kwargs)
+        return self
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+
+        Returns
+        -------
+        params : dict or list of dict, default={}
+            Parameters to create testing instances of the class.
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`.
+        """
+        from sktime.dists_kernels.compose_tab_to_panel import AggrDist
+        from sktime.dists_kernels.dtw import DtwDist
+
+        params1 = {"pw_trafos": [AggrDist.create_test_instance()]}
+        params2 = {
+            "pw_trafos": [AggrDist.create_test_instance(), DtwDist()],
+            "operation": "+",
+        }
+
+        return [params1, params2]
diff --git a/sktime/dists_kernels/dummy.py b/sktime/dists_kernels/dummy.py
new file mode 100644
index 000000000..f004646b0
--- /dev/null
+++ b/sktime/dists_kernels/dummy.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+"""Dummy distances/kernels."""
+
+__author__ = ["fkiraly"]
+
+import numpy as np
+
+from sktime.dists_kernels._base import BasePairwiseTransformerPanel
+
+SUPPORTED_MTYPES = ["df-list", "nested_univ", "numpy3D"]
+
+
+class ConstantPwTrafoPanel(BasePairwiseTransformerPanel):
+    """Pairwise panel transformer that always returns a constant value.
+
+    Parameters
+    ----------
+    constant : float, optional, default = 0
+        the constant value that this transformer returns
+    """
+
+    _tags = {
+        "X_inner_mtype": SUPPORTED_MTYPES,
+        "capability:missing_values": True,  # can estimator handle missing data?
+        "capability:multivariate": True,  # can estimator handle multivariate data?
+        "capability:unequal_length": True,  # can dist handle unequal length panels?
+    }
+
+    def __init__(self, constant=0):
+
+        self.constant = constant
+
+        super(ConstantPwTrafoPanel, self).__init__()
+
+    def _transform(self, X, X2=None):
+        """Compute distance/kernel matrix.
+
+        private _transform containing core logic, called from public transform
+
+        Parameters
+        ----------
+        X: sktime Panel data container
+        X2: sktime Panel data container
+
+        Returns
+        -------
+        distmat: np.array of shape [n, m]
+            (i,j)-th entry contains distance/kernel between X.iloc[i] and X2.iloc[j]
+        """
+        # get metadata from self - this is not always len(X) etc
+        #   but for the mtypes allowed, it is
+        # todo: conversion is wasteful, replace this perhaps by base class functionality
+        #   we check the input type anyway, so we could store n_instances
+        #   (but the problem there is, we should not write to self in transform)
+        n = len(X)
+        if X2 is not None:
+            m = len(X2)
+        else:
+            m = n
+
+        distmat = self.constant * np.ones((n, m))
+
+        return distmat
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+
+        Returns
+        -------
+        params : dict or list of dict, default={}
+            Parameters to create testing instances of the class.
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`.
+        """
+        params1 = {}
+        params2 = {"constant": 42}
+
+        return [params1, params2]
diff --git a/sktime/transformations/base.py b/sktime/transformations/base.py
index eb1bef898..93f7ebd05 100644
--- a/sktime/transformations/base.py
+++ b/sktime/transformations/base.py
@@ -278,6 +278,20 @@ class BaseTransformer(BaseEstimator):
         else:
             return NotImplemented
 
+    def __neg__(self):
+        """Magic unary - (negation) method, return OptionalPassthrough of self.
+
+        Intuition: `OptionalPassthrough` is "not having transformer", as an option.
+
+        Returns
+        -------
+        `OptionalPassthrough` object, containing `self`, with `passthrough=False`.
+            The `passthrough` parameter can be set via `set_params`.
+        """
+        from sktime.transformations.series.compose import OptionalPassthrough
+
+        return OptionalPassthrough(self, passthrough=False)
+
     def __getitem__(self, key):
         """Magic [...] method, return column subsetted transformer.
 
diff --git a/sktime/transformations/compose.py b/sktime/transformations/compose.py
index 93c412dab..102c21842 100644
--- a/sktime/transformations/compose.py
+++ b/sktime/transformations/compose.py
@@ -4,6 +4,7 @@
 
 import pandas as pd
 from sklearn import clone
+from sklearn.utils.metaestimators import if_delegate_has_method
 
 from sktime.base import _HeterogenousMetaEstimator
 from sktime.transformations._delegate import _DelegatedTransformer
@@ -14,13 +15,32 @@ from sktime.utils.sklearn import (
     is_sklearn_regressor,
     is_sklearn_transformer,
 )
+from sktime.utils.validation.series import check_series
 
-__author__ = ["fkiraly", "mloning", "miraep8"]
+__author__ = ["fkiraly", "mloning", "miraep8", "aiwalter", "SveaMeyer13"]
 __all__ = [
-    "TransformerPipeline",
+    "ColumnwiseTransformer",
     "FeatureUnion",
     "FitInTransform",
+    "Id",
     "MultiplexTransformer",
+    "OptionalPassthrough",
+    "TransformerPipeline",
+    "YtoX",
+]
+
+
+# mtypes for Series, Panel, Hierarchical,
+# with exception of some ambiguous and discouraged mtypes
+CORE_MTYPES = [
+    "pd.DataFrame",
+    "np.ndarray",
+    "pd.Series",
+    "pd-multiindex",
+    "df-list",
+    "nested_univ",
+    "numpy3D",
+    "pd_multiindex_hier",
 ]
 
 
@@ -128,16 +148,7 @@ class TransformerPipeline(BaseTransformer, _HeterogenousMetaEstimator):
 
     _tags = {
         # we let all X inputs through to be handled by first transformer
-        "X_inner_mtype": [
-            "pd.DataFrame",
-            "np.ndarray",
-            "pd.Series",
-            "pd-multiindex",
-            "df-list",
-            "nested_univ",
-            "numpy3D",
-            "pd_multiindex_hier",
-        ],
+        "X_inner_mtype": CORE_MTYPES,
         "univariate-only": False,
     }
 
@@ -1061,3 +1072,497 @@ class MultiplexTransformer(_DelegatedTransformer, _HeterogenousMetaEstimator):
             attr_name="forecasters",
             concat_order="right",
         )
+
+
+# mtypes for Series, Panel, Hierarchical,
+# with exception of some ambiguous and discouraged mtypes
+CORE_MTYPES = [
+    "pd.DataFrame",
+    "np.ndarray",
+    "pd.Series",
+    "pd-multiindex",
+    "df-list",
+    "nested_univ",
+    "numpy3D",
+    "pd_multiindex_hier",
+]
+
+
+class Id(_DelegatedTransformer):
+    """Identity transformer, returns data unchanged in transform/inverse_transform."""
+
+    _tags = {
+        "capability:inverse_transform": True,  # can the transformer inverse transform?
+        "univariate-only": False,  # can the transformer handle multivariate X?
+        "X_inner_mtype": CORE_MTYPES,  # which mtypes do _fit/_predict support for X?
+        # this can be a Panel mtype even if transform-input is Series, vectorized
+        "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for y?
+        "fit_is_empty": True,  # is fit empty and can be skipped? Yes = True
+        "transform-returns-same-time-index": True,
+        # does transform return have the same time index as input X
+        "handles-missing-data": True,  # can estimator handle missing data?
+    }
+
+    def _transform(self, X, y=None):
+        """Transform X and return a transformed version.
+
+        private _transform containing the core logic, called from transform
+
+        Parameters
+        ----------
+        X : any sktime compatible data, Series, Panel, or Hierarchical
+        y : optional, default=None
+            ignored, argument present for interface conformance
+
+        Returns
+        -------
+        X, identical to input
+        """
+        return X
+
+    def _inverse_transform(self, X, y=None):
+        """Inverse transform X and return an inverse transformed version.
+
+        private _inverse_transform containing core logic, called from inverse_transform
+
+        Parameters
+        ----------
+        X : any sktime compatible data, Series, Panel, or Hierarchical
+        y : optional, default=None
+            ignored, argument present for interface conformance
+
+        Returns
+        -------
+        X, identical to input
+        """
+        return X
+
+
+class OptionalPassthrough(_DelegatedTransformer):
+    """Wrap an existing transformer to tune whether to include it in a pipeline.
+
+    Allows tuning the implicit hyperparameter whether or not to use a
+    particular transformer inside a pipeline (e.g. TransformedTargetForecaster)
+    or not. This is achieved by the hyperparameter `passthrough`
+    which can be added to a tuning grid then (see example).
+
+    Parameters
+    ----------
+    transformer : Estimator
+        scikit-learn-like or sktime-like transformer to fit and apply to series.
+        this is a "blueprint" transformer, state does not change when `fit` is called
+    passthrough : bool, default=False
+       Whether to apply the given transformer or to just
+        passthrough the data (identity transformation). If, True the transformer
+        is not applied and the OptionalPassthrough uses the identity
+        transformation.
+
+    Attributes
+    ----------
+    transformer_: transformer,
+        this clone is fitted when `fit` is called and provides `transform` and inverse
+        if passthrough = False, a clone of `transformer`passed
+        if passthrough = True, the identity transformer `Id`
+
+    Examples
+    --------
+    >>> from sktime.datasets import load_airline
+    >>> from sktime.forecasting.naive import NaiveForecaster
+    >>> from sktime.transformations.series.compose import OptionalPassthrough
+    >>> from sktime.transformations.series.detrend import Deseasonalizer
+    >>> from sktime.transformations.series.adapt import TabularToSeriesAdaptor
+    >>> from sktime.forecasting.compose import TransformedTargetForecaster
+    >>> from sktime.forecasting.model_selection import (
+    ...     ForecastingGridSearchCV,
+    ...     SlidingWindowSplitter)
+    >>> from sklearn.preprocessing import StandardScaler
+    >>> # create pipeline
+    >>> pipe = TransformedTargetForecaster(steps=[
+    ...     ("deseasonalizer", OptionalPassthrough(Deseasonalizer())),
+    ...     ("scaler", OptionalPassthrough(TabularToSeriesAdaptor(StandardScaler()))),
+    ...     ("forecaster", NaiveForecaster())])
+    >>> # putting it all together in a grid search
+    >>> cv = SlidingWindowSplitter(
+    ...     initial_window=60,
+    ...     window_length=24,
+    ...     start_with_window=True,
+    ...     step_length=48)
+    >>> param_grid = {
+    ...     "deseasonalizer__passthrough" : [True, False],
+    ...     "scaler__transformer__transformer__with_mean": [True, False],
+    ...     "scaler__passthrough" : [True, False],
+    ...     "forecaster__strategy": ["drift", "mean", "last"]}
+    >>> gscv = ForecastingGridSearchCV(
+    ...     forecaster=pipe,
+    ...     param_grid=param_grid,
+    ...     cv=cv,
+    ...     n_jobs=-1)
+    >>> gscv_fitted = gscv.fit(load_airline())
+    """
+
+    _tags = {
+        "scitype:transform-input": "Series",
+        # what is the scitype of X: Series, or Panel
+        "scitype:transform-output": "Series",
+        # what scitype is returned: Primitives, Series, Panel
+        "scitype:instancewise": True,  # is this an instance-wise transform?
+        "X_inner_mtype": CORE_MTYPES,
+        # which mtypes do _fit/_predict support for X?
+        "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for y?
+        "univariate-only": False,
+        "fit_is_empty": False,
+        "capability:inverse_transform": True,
+    }
+
+    def __init__(self, transformer, passthrough=False):
+        self.transformer = transformer
+        self.passthrough = passthrough
+
+        super(OptionalPassthrough, self).__init__()
+
+        # should be all tags, but not fit_is_empty
+        #   (_fit should not be skipped)
+        tags_to_clone = [
+            "scitype:transform-input",
+            "scitype:transform-output",
+            "scitype:instancewise",
+            "X_inner_mtype",
+            "y_inner_mtype",
+            "capability:inverse_transform",
+            "handles-missing-data",
+            "X-y-must-have-same-index",
+            "transform-returns-same-time-index",
+            "skip-inverse-transform",
+        ]
+        self.clone_tags(transformer, tag_names=tags_to_clone)
+
+        if passthrough:
+            self.transformer_ = Id()
+        else:
+            self.transformer_ = transformer.clone()
+
+    _delegate_name = "transformer_"
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+
+
+        Returns
+        -------
+        params : dict or list of dict, default = {}
+            Parameters to create testing instances of the class
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`
+        """
+        from sktime.transformations.series.boxcox import BoxCoxTransformer
+
+        return {"transformer": BoxCoxTransformer(), "passthrough": False}
+
+
+class ColumnwiseTransformer(BaseTransformer):
+    """Apply a transformer columnwise to multivariate series.
+
+    Overview: input multivariate time series and the transformer passed
+    in `transformer` parameter is applied to specified `columns`, each
+    column is handled as a univariate series. The resulting transformed
+    data has the same shape as input data.
+
+    Parameters
+    ----------
+    transformer : Estimator
+        scikit-learn-like or sktime-like transformer to fit and apply to series.
+    columns : list of str or None
+            Names of columns that are supposed to be transformed.
+            If None, all columns are transformed.
+
+    Attributes
+    ----------
+    transformers_ : dict of {str : transformer}
+        Maps columns to transformers.
+    columns_ : list of str
+        Names of columns that are supposed to be transformed.
+
+    See Also
+    --------
+    OptionalPassthrough
+
+    Examples
+    --------
+    >>> from sktime.datasets import load_longley
+    >>> from sktime.transformations.series.detrend import Detrender
+    >>> from sktime.transformations.series.compose import ColumnwiseTransformer
+    >>> _, X = load_longley()
+    >>> transformer = ColumnwiseTransformer(Detrender())
+    >>> Xt = transformer.fit_transform(X)
+    """
+
+    _tags = {
+        "scitype:transform-input": "Series",
+        # what is the scitype of X: Series, or Panel
+        "scitype:transform-output": "Series",
+        # what scitype is returned: Primitives, Series, Panel
+        "scitype:instancewise": True,  # is this an instance-wise transform?
+        "X_inner_mtype": "pd.DataFrame",
+        # which mtypes do _fit/_predict support for X?
+        "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for y?
+        "univariate-only": False,
+        "fit_is_empty": False,
+    }
+
+    def __init__(self, transformer, columns=None):
+        self.transformer = transformer
+        self.columns = columns
+        super(ColumnwiseTransformer, self).__init__()
+
+        tags_to_clone = [
+            "y_inner_mtype",
+            "capability:inverse_transform",
+            "handles-missing-data",
+            "X-y-must-have-same-index",
+            "transform-returns-same-time-index",
+            "skip-inverse-transform",
+        ]
+        self.clone_tags(transformer, tag_names=tags_to_clone)
+
+    def _fit(self, X, y=None):
+        """Fit transformer to X and y.
+
+        private _fit containing the core logic, called from fit
+
+        Parameters
+        ----------
+        X : pd.DataFrame
+            Data to fit transform to
+        y : Series or Panel, default=None
+            Additional data, e.g., labels for transformation
+
+        Returns
+        -------
+        self: a fitted instance of the estimator
+        """
+        # check that columns are None or list of strings
+        if self.columns is not None:
+            if not isinstance(self.columns, list) and all(
+                isinstance(s, str) for s in self.columns
+            ):
+                raise ValueError("Columns need to be a list of strings or None.")
+
+        # set self.columns_ to columns that are going to be transformed
+        # (all if self.columns is None)
+        self.columns_ = self.columns
+        if self.columns_ is None:
+            self.columns_ = X.columns
+
+        # make sure z contains all columns that the user wants to transform
+        _check_columns(X, selected_columns=self.columns_)
+
+        # fit by iterating over columns
+        self.transformers_ = {}
+        for colname in self.columns_:
+            transformer = self.transformer.clone()
+            self.transformers_[colname] = transformer
+            self.transformers_[colname].fit(X[colname], y)
+        return self
+
+    def _transform(self, X, y=None):
+        """Transform X and return a transformed version.
+
+        private _transform containing the core logic, called from transform
+
+        Returns a transformed version of X by iterating over specified
+        columns and applying the wrapped transformer to them.
+
+        Parameters
+        ----------
+        X : pd.DataFrame
+            Data to be transformed
+        y : Series or Panel, default=None
+            Additional data, e.g., labels for transformation
+
+        Returns
+        -------
+        Xt : pd.DataFrame
+            transformed version of X
+        """
+        # make copy of z
+        X = X.copy()
+
+        # make sure z contains all columns that the user wants to transform
+        _check_columns(X, selected_columns=self.columns_)
+        for colname in self.columns_:
+            X[colname] = self.transformers_[colname].transform(X[colname], y)
+        return X
+
+    def _inverse_transform(self, X, y=None):
+        """Logic used by `inverse_transform` to reverse transformation on `X`.
+
+        Returns an inverse-transformed version of X by iterating over specified
+        columns and applying the univariate series transformer to them.
+        Only works if `self.transformer` has an `inverse_transform` method.
+
+        Parameters
+        ----------
+        X : pd.DataFrame
+            Data to be inverse transformed
+        y : Series or Panel, default=None
+            Additional data, e.g., labels for transformation
+
+        Returns
+        -------
+        Xt : pd.DataFrame
+            inverse transformed version of X
+        """
+        # make copy of z
+        X = X.copy()
+
+        # make sure z contains all columns that the user wants to transform
+        _check_columns(X, selected_columns=self.columns_)
+
+        # iterate over columns that are supposed to be inverse_transformed
+        for colname in self.columns_:
+            X[colname] = self.transformers_[colname].inverse_transform(X[colname], y)
+
+        return X
+
+    @if_delegate_has_method(delegate="transformer")
+    def update(self, X, y=None, update_params=True):
+        """Update parameters.
+
+        Update the parameters of the estimator with new data
+        by iterating over specified columns.
+        Only works if `self.transformer` has an `update` method.
+
+        Parameters
+        ----------
+        X : pd.Series
+            New time series.
+        update_params : bool, optional, default=True
+
+        Returns
+        -------
+        self : an instance of self
+        """
+        z = check_series(X)
+
+        # make z a pd.DataFrame in univariate case
+        if isinstance(z, pd.Series):
+            z = z.to_frame()
+
+        # make sure z contains all columns that the user wants to transform
+        _check_columns(z, selected_columns=self.columns_)
+        for colname in self.columns_:
+            self.transformers_[colname].update(z[colname], X)
+        return self
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+
+
+        Returns
+        -------
+        params : dict or list of dict, default = {}
+            Parameters to create testing instances of the class
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`
+        """
+        from sktime.transformations.series.detrend import Detrender
+
+        return {"transformer": Detrender()}
+
+
+def _check_columns(z, selected_columns):
+    # make sure z contains all columns that the user wants to transform
+    z_wanted_keys = set(selected_columns)
+    z_new_keys = set(z.columns)
+    difference = z_wanted_keys.difference(z_new_keys)
+    if len(difference) != 0:
+        raise ValueError("Missing columns" + str(difference) + "in Z.")
+
+
+def _check_is_pdseries(z):
+    # make z a pd.Dataframe in univariate case
+    is_series = False
+    if isinstance(z, pd.Series):
+        z = z.to_frame()
+        is_series = True
+    return z, is_series
+
+
+class YtoX(BaseTransformer):
+    """Create exogeneous features which are a copy of the endogenous data.
+
+    Replaces exogeneous features (`X`) by endogeneous data (`y`).
+
+    To *add* instead of *replace*, use `FeatureUnion`.
+
+    Parameters
+    ----------
+    no parameters
+    """
+
+    _tags = {
+        "transform-returns-same-time-index": True,
+        "skip-inverse-transform": False,
+        "univariate-only": False,
+        "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
+        "y_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
+        "scitype:y": "both",
+        "fit_is_empty": True,
+        "requires_y": True,
+    }
+
+    def __init__(self):
+        super(YtoX, self).__init__()
+
+    def _transform(self, X, y=None):
+        """Transform X and return a transformed version.
+
+        private _transform containing core logic, called from transform
+
+        Parameters
+        ----------
+        X : time series or panel in one of the pd.DataFrame formats
+            Data to be transformed
+        y : time series or panel in one of the pd.DataFrame formats
+            Additional data, e.g., labels for transformation
+
+        Returns
+        -------
+        y, as a transformed version of X
+        """
+        return y
+
+    def _inverse_transform(self, X, y=None):
+        """Inverse transform, inverse operation to transform.
+
+        Drops featurized column that was added in transform().
+
+        Parameters
+        ----------
+        X : Series or Panel of mtype X_inner_mtype
+            if X_inner_mtype is list, _inverse_transform must support all types in it
+            Data to be inverse transformed
+        y : Series or Panel of mtype y_inner_mtype, optional (default=None)
+            Additional data, e.g., labels for transformation
+
+        Returns
+        -------
+        inverse transformed version of X
+        """
+        return y
diff --git a/sktime/transformations/series/compose.py b/sktime/transformations/series/compose.py
index 58fc0fe71..8238efc48 100644
--- a/sktime/transformations/series/compose.py
+++ b/sktime/transformations/series/compose.py
@@ -6,488 +6,17 @@
 __author__ = ["aiwalter", "SveaMeyer13", "fkiraly"]
 __all__ = ["OptionalPassthrough", "ColumnwiseTransformer", "YtoX"]
 
-import pandas as pd
-from sklearn.utils.metaestimators import if_delegate_has_method
-
-from sktime.transformations.base import BaseTransformer
-from sktime.utils.validation.series import check_series
-
-
-class OptionalPassthrough(BaseTransformer):
-    """Wrap an existing transformer to tune whether to include it in a pipeline.
-
-    Allows tuning the implicit hyperparameter whether or not to use a
-    particular transformer inside a pipeline (e.g. TranformedTargetForecaster)
-    or not. This is achieved by the hyperparameter `passthrough`
-    which can be added to a tuning grid then (see example).
-
-    Parameters
-    ----------
-    transformer : Estimator
-        scikit-learn-like or sktime-like transformer to fit and apply to series.
-    passthrough : bool, default=False
-       Whether to apply the given transformer or to just
-        passthrough the data (identity transformation). If, True the transformer
-        is not applied and the OptionalPassthrough uses the identity
-        transformation.
-
-    Examples
-    --------
-    >>> from sktime.datasets import load_airline
-    >>> from sktime.forecasting.naive import NaiveForecaster
-    >>> from sktime.transformations.series.compose import OptionalPassthrough
-    >>> from sktime.transformations.series.detrend import Deseasonalizer
-    >>> from sktime.transformations.series.adapt import TabularToSeriesAdaptor
-    >>> from sktime.forecasting.compose import TransformedTargetForecaster
-    >>> from sktime.forecasting.model_selection import (
-    ...     ForecastingGridSearchCV,
-    ...     SlidingWindowSplitter)
-    >>> from sklearn.preprocessing import StandardScaler
-    >>> # create pipeline
-    >>> pipe = TransformedTargetForecaster(steps=[
-    ...     ("deseasonalizer", OptionalPassthrough(Deseasonalizer())),
-    ...     ("scaler", OptionalPassthrough(TabularToSeriesAdaptor(StandardScaler()))),
-    ...     ("forecaster", NaiveForecaster())])
-    >>> # putting it all together in a grid search
-    >>> cv = SlidingWindowSplitter(
-    ...     initial_window=60,
-    ...     window_length=24,
-    ...     start_with_window=True,
-    ...     step_length=48)
-    >>> param_grid = {
-    ...     "deseasonalizer__passthrough" : [True, False],
-    ...     "scaler__transformer__transformer__with_mean": [True, False],
-    ...     "scaler__passthrough" : [True, False],
-    ...     "forecaster__strategy": ["drift", "mean", "last"]}
-    >>> gscv = ForecastingGridSearchCV(
-    ...     forecaster=pipe,
-    ...     param_grid=param_grid,
-    ...     cv=cv,
-    ...     n_jobs=-1)
-    >>> gscv_fitted = gscv.fit(load_airline())
-    """
-
-    _tags = {
-        "scitype:transform-input": "Series",
-        # what is the scitype of X: Series, or Panel
-        "scitype:transform-output": "Series",
-        # what scitype is returned: Primitives, Series, Panel
-        "scitype:instancewise": True,  # is this an instance-wise transform?
-        "X_inner_mtype": ["pd.DataFrame", "pd.Series"],
-        # which mtypes do _fit/_predict support for X?
-        "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for y?
-        "univariate-only": False,
-        "fit_is_empty": False,
-        "capability:inverse_transform": True,
-    }
-
-    def __init__(self, transformer, passthrough=False):
-        self.transformer = transformer
-        self.transformer_ = None
-        self.passthrough = passthrough
-        self._is_fitted = False
-        super(OptionalPassthrough, self).__init__()
-
-        # should be all tags, but not fit_is_empty
-        #   (_fit should not be skipped)
-        tags_to_clone = [
-            "scitype:transform-input",
-            "scitype:transform-output",
-            "scitype:instancewise",
-            "X_inner_mtype",
-            "y_inner_mtype",
-            "capability:inverse_transform",
-            "handles-missing-data",
-            "X-y-must-have-same-index",
-            "transform-returns-same-time-index",
-            "skip-inverse-transform",
-        ]
-        self.clone_tags(transformer, tag_names=tags_to_clone)
-
-    def _fit(self, X, y=None):
-        """Fit transformer to X and y.
-
-        private _fit containing the core logic, called from fit
-
-        Parameters
-        ----------
-        X : Series or Panel of mtype X_inner_mtype
-            if X_inner_mtype is list, _fit must support all types in it
-            Data to fit transform to
-        y : Series or Panel of mtype y_inner_mtype, default=None
-            Additional data, e.g., labels for tarnsformation
-
-        Returns
-        -------
-        self: a fitted instance of the estimator
-        """
-        if not self.passthrough:
-            self.transformer_ = self.transformer.clone()
-            self.transformer_._fit(X, y)
-        return self
-
-    def _transform(self, X, y=None):
-        """Transform X and return a transformed version.
-
-        private _transform containing the core logic, called from transform
-
-        Parameters
-        ----------
-        X : Series or Panel of mtype X_inner_mtype
-            if X_inner_mtype is list, _transform must support all types in it
-            Data to be transformed
-        y : Series or Panel of mtype y_inner_mtype, default=None
-            Additional data, e.g., labels for transformation
-
-        Returns
-        -------
-        transformed version of X
-        """
-        if not self.passthrough:
-            X = self.transformer_._transform(X, y)
-        return X
-
-    def _inverse_transform(self, X, y=None):
-        """Inverse transform, inverse operation to transform.
-
-        core logic
-
-        Parameters
-        ----------
-        X : Series or Panel of mtype X_inner_mtype
-            if X_inner_mtype is list, _inverse_transform must support all types in it
-            Data to be inverse transformed
-        y : Series or Panel of mtype y_inner_mtype, optional (default=None)
-            Additional data, e.g., labels for transformation
-
-        Returns
-        -------
-        inverse transformed version of X
-        """
-        if not self.passthrough:
-            X = self.transformer_._inverse_transform(X, y)
-        return X
-
-    @classmethod
-    def get_test_params(cls, parameter_set="default"):
-        """Return testing parameter settings for the estimator.
-
-        Parameters
-        ----------
-        parameter_set : str, default="default"
-            Name of the set of test parameters to return, for use in tests. If no
-            special parameters are defined for a value, will return `"default"` set.
-
-
-        Returns
-        -------
-        params : dict or list of dict, default = {}
-            Parameters to create testing instances of the class
-            Each dict are parameters to construct an "interesting" test instance, i.e.,
-            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
-            `create_test_instance` uses the first (or only) dictionary in `params`
-        """
-        from sktime.transformations.series.boxcox import BoxCoxTransformer
-
-        return {"transformer": BoxCoxTransformer(), "passthrough": False}
-
-
-class ColumnwiseTransformer(BaseTransformer):
-    """Apply a transformer columnwise to multivariate series.
-
-    Overview: input multivariate time series and the transformer passed
-    in `transformer` parameter is applied to specified `columns`, each
-    column is handled as a univariate series. The resulting transformed
-    data has the same shape as input data.
-
-    Parameters
-    ----------
-    transformer : Estimator
-        scikit-learn-like or sktime-like transformer to fit and apply to series.
-    columns : list of str or None
-            Names of columns that are supposed to be transformed.
-            If None, all columns are transformed.
-
-    Attributes
-    ----------
-    transformers_ : dict of {str : transformer}
-        Maps columns to transformers.
-    columns_ : list of str
-        Names of columns that are supposed to be transformed.
-
-    See Also
-    --------
-    OptionalPassthrough
-
-    Examples
-    --------
-    >>> from sktime.datasets import load_longley
-    >>> from sktime.transformations.series.detrend import Detrender
-    >>> from sktime.transformations.series.compose import ColumnwiseTransformer
-    >>> _, X = load_longley()
-    >>> transformer = ColumnwiseTransformer(Detrender())
-    >>> Xt = transformer.fit_transform(X)
-    """
-
-    _tags = {
-        "scitype:transform-input": "Series",
-        # what is the scitype of X: Series, or Panel
-        "scitype:transform-output": "Series",
-        # what scitype is returned: Primitives, Series, Panel
-        "scitype:instancewise": True,  # is this an instance-wise transform?
-        "X_inner_mtype": "pd.DataFrame",
-        # which mtypes do _fit/_predict support for X?
-        "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for y?
-        "univariate-only": False,
-        "fit_is_empty": False,
-    }
-
-    def __init__(self, transformer, columns=None):
-        self.transformer = transformer
-        self.columns = columns
-        super(ColumnwiseTransformer, self).__init__()
-
-        tags_to_clone = [
-            "y_inner_mtype",
-            "capability:inverse_transform",
-            "handles-missing-data",
-            "X-y-must-have-same-index",
-            "transform-returns-same-time-index",
-            "skip-inverse-transform",
-        ]
-        self.clone_tags(transformer, tag_names=tags_to_clone)
-
-    def _fit(self, X, y=None):
-        """Fit transformer to X and y.
-
-        private _fit containing the core logic, called from fit
-
-        Parameters
-        ----------
-        X : pd.DataFrame
-            Data to fit transform to
-        y : Series or Panel, default=None
-            Additional data, e.g., labels for transformation
-
-        Returns
-        -------
-        self: a fitted instance of the estimator
-        """
-        # check that columns are None or list of strings
-        if self.columns is not None:
-            if not isinstance(self.columns, list) and all(
-                isinstance(s, str) for s in self.columns
-            ):
-                raise ValueError("Columns need to be a list of strings or None.")
-
-        # set self.columns_ to columns that are going to be transformed
-        # (all if self.columns is None)
-        self.columns_ = self.columns
-        if self.columns_ is None:
-            self.columns_ = X.columns
-
-        # make sure z contains all columns that the user wants to transform
-        _check_columns(X, selected_columns=self.columns_)
-
-        # fit by iterating over columns
-        self.transformers_ = {}
-        for colname in self.columns_:
-            transformer = self.transformer.clone()
-            self.transformers_[colname] = transformer
-            self.transformers_[colname].fit(X[colname], y)
-        return self
-
-    def _transform(self, X, y=None):
-        """Transform X and return a transformed version.
-
-        private _transform containing the core logic, called from transform
-
-        Returns a transformed version of X by iterating over specified
-        columns and applying the wrapped transformer to them.
-
-        Parameters
-        ----------
-        X : pd.DataFrame
-            Data to be transformed
-        y : Series or Panel, default=None
-            Additional data, e.g., labels for transformation
-
-        Returns
-        -------
-        Xt : pd.DataFrame
-            transformed version of X
-        """
-        # make copy of z
-        X = X.copy()
-
-        # make sure z contains all columns that the user wants to transform
-        _check_columns(X, selected_columns=self.columns_)
-        for colname in self.columns_:
-            X[colname] = self.transformers_[colname].transform(X[colname], y)
-        return X
-
-    def _inverse_transform(self, X, y=None):
-        """Logic used by `inverse_transform` to reverse transformation on `X`.
-
-        Returns an inverse-transformed version of X by iterating over specified
-        columns and applying the univariate series transformer to them.
-        Only works if `self.transformer` has an `inverse_transform` method.
-
-        Parameters
-        ----------
-        X : pd.DataFrame
-            Data to be inverse transformed
-        y : Series or Panel, default=None
-            Additional data, e.g., labels for transformation
-
-        Returns
-        -------
-        Xt : pd.DataFrame
-            inverse transformed version of X
-        """
-        # make copy of z
-        X = X.copy()
-
-        # make sure z contains all columns that the user wants to transform
-        _check_columns(X, selected_columns=self.columns_)
-
-        # iterate over columns that are supposed to be inverse_transformed
-        for colname in self.columns_:
-            X[colname] = self.transformers_[colname].inverse_transform(X[colname], y)
-
-        return X
-
-    @if_delegate_has_method(delegate="transformer")
-    def update(self, X, y=None, update_params=True):
-        """Update parameters.
-
-        Update the parameters of the estimator with new data
-        by iterating over specified columns.
-        Only works if `self.transformer` has an `update` method.
-
-        Parameters
-        ----------
-        X : pd.Series
-            New time series.
-        update_params : bool, optional, default=True
-
-        Returns
-        -------
-        self : an instance of self
-        """
-        z = check_series(X)
-
-        # make z a pd.DataFrame in univariate case
-        if isinstance(z, pd.Series):
-            z = z.to_frame()
-
-        # make sure z contains all columns that the user wants to transform
-        _check_columns(z, selected_columns=self.columns_)
-        for colname in self.columns_:
-            self.transformers_[colname].update(z[colname], X)
-        return self
-
-    @classmethod
-    def get_test_params(cls, parameter_set="default"):
-        """Return testing parameter settings for the estimator.
-
-        Parameters
-        ----------
-        parameter_set : str, default="default"
-            Name of the set of test parameters to return, for use in tests. If no
-            special parameters are defined for a value, will return `"default"` set.
-
-
-        Returns
-        -------
-        params : dict or list of dict, default = {}
-            Parameters to create testing instances of the class
-            Each dict are parameters to construct an "interesting" test instance, i.e.,
-            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
-            `create_test_instance` uses the first (or only) dictionary in `params`
-        """
-        from sktime.transformations.series.detrend import Detrender
-
-        return {"transformer": Detrender()}
-
-
-def _check_columns(z, selected_columns):
-    # make sure z contains all columns that the user wants to transform
-    z_wanted_keys = set(selected_columns)
-    z_new_keys = set(z.columns)
-    difference = z_wanted_keys.difference(z_new_keys)
-    if len(difference) != 0:
-        raise ValueError("Missing columns" + str(difference) + "in Z.")
-
-
-def _check_is_pdseries(z):
-    # make z a pd.Dataframe in univariate case
-    is_series = False
-    if isinstance(z, pd.Series):
-        z = z.to_frame()
-        is_series = True
-    return z, is_series
-
-
-class YtoX(BaseTransformer):
-    """Create exogeneous features which are a copy of the endogenous data.
-
-    Replaces exogeneous features (`X`) by endogeneous data (`y`).
-
-    To *add* instead of *replace*, use `FeatureUnion`.
-
-    Parameters
-    ----------
-    no parameters
-    """
-
-    _tags = {
-        "transform-returns-same-time-index": True,
-        "skip-inverse-transform": False,
-        "univariate-only": False,
-        "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
-        "y_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
-        "scitype:y": "both",
-        "fit_is_empty": True,
-        "requires_y": True,
-    }
-
-    def __init__(self):
-        super(YtoX, self).__init__()
-
-    def _transform(self, X, y=None):
-        """Transform X and return a transformed version.
-
-        private _transform containing core logic, called from transform
-
-        Parameters
-        ----------
-        X : time series or panel in one of the pd.DataFrame formats
-            Data to be transformed
-        y : time series or panel in one of the pd.DataFrame formats
-            Additional data, e.g., labels for transformation
-
-        Returns
-        -------
-        y, as a transformed version of X
-        """
-        return y
-
-    def _inverse_transform(self, X, y=None):
-        """Inverse transform, inverse operation to transform.
-
-        Drops featurized column that was added in transform().
-
-        Parameters
-        ----------
-        X : Series or Panel of mtype X_inner_mtype
-            if X_inner_mtype is list, _inverse_transform must support all types in it
-            Data to be inverse transformed
-        y : Series or Panel of mtype y_inner_mtype, optional (default=None)
-            Additional data, e.g., labels for transformation
-
-        Returns
-        -------
-        inverse transformed version of X
-        """
-        return y
+from warnings import warn
+
+from sktime.transformations.compose import (
+    ColumnwiseTransformer,
+    OptionalPassthrough,
+    YtoX,
+)
+
+warn(
+    "transformations.series.compose is deprecated and will be removed in"
+    " version 0.15.0. All estimators in it will be moved to transformations.compose. "
+    "Please change imports to transformations.compose to avoid breakage.",
+    DeprecationWarning,
+)
diff --git a/sktime/transformations/series/detrend/_deseasonalize.py b/sktime/transformations/series/detrend/_deseasonalize.py
index 906e8c7ce..bf3e2e190 100644
--- a/sktime/transformations/series/detrend/_deseasonalize.py
+++ b/sktime/transformations/series/detrend/_deseasonalize.py
@@ -204,7 +204,9 @@ class Deseasonalizer(BaseTransformer):
         """
         X_full = X.combine_first(self._X)
         self._X = X_full
-        return self._fit(X_full, update_params=update_params)
+        if update_params:
+            self._fit(X_full, update_params=update_params)
+        return self
 
 
 class ConditionalDeseasonalizer(Deseasonalizer):


Test patch:
diff --git a/sktime/transformations/series/detrend/tests/test_deseasonalise.py b/sktime/transformations/series/detrend/tests/test_deseasonalise.py
index 0509326c4..1017e5e01 100644
--- a/sktime/transformations/series/detrend/tests/test_deseasonalise.py
+++ b/sktime/transformations/series/detrend/tests/test_deseasonalise.py
@@ -1,17 +1,18 @@
-#!/usr/bin/env python3 -u
 # -*- coding: utf-8 -*-
+"""Tests for Deseasonalizer."""
 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
 
-__author__ = ["Markus Löning"]
+__author__ = ["mloning"]
 __all__ = []
 
 import numpy as np
 import pytest
+from statsmodels.tsa.seasonal import seasonal_decompose
+
 from sktime.forecasting.model_selection import temporal_train_test_split
 from sktime.forecasting.tests._config import TEST_SPS
 from sktime.transformations.series.detrend import Deseasonalizer
 from sktime.utils._testing.forecasting import make_forecasting_problem
-from statsmodels.tsa.seasonal import seasonal_decompose
 
 MODELS = ["additive", "multiplicative"]
 
@@ -56,3 +57,23 @@ def test_transform_inverse_transform_equivalence(sp, model):
     yit = transformer.inverse_transform(transformer.transform(y_train))
     np.testing.assert_array_equal(y_train.index, yit.index)
     np.testing.assert_array_almost_equal(y_train, yit)
+
+
+def test_deseasonalizer_in_pipeline():
+    """Test deseasonalizer in pipeline, see issue #3267."""
+    from sktime.datasets import load_airline
+    from sktime.forecasting.compose import TransformedTargetForecaster
+    from sktime.forecasting.theta import ThetaForecaster
+    from sktime.transformations.series.detrend import Deseasonalizer
+
+    all_df = load_airline().to_frame()
+
+    model = TransformedTargetForecaster(
+        [
+            ("deseasonalize", Deseasonalizer(model="additive", sp=12)),
+            ("forecast", ThetaForecaster()),
+        ]
+    )
+    train_df = all_df["1949":"1950"]
+    model.fit(train_df)
+    model.update(y=all_df["1951"])
diff --git a/sktime/transformations/tests/test_compose.py b/sktime/transformations/tests/test_compose.py
index 24be02488..7acd7e8c0 100644
--- a/sktime/transformations/tests/test_compose.py
+++ b/sktime/transformations/tests/test_compose.py
@@ -13,6 +13,7 @@ from sktime.datatypes import get_examples
 from sktime.transformations.compose import FeatureUnion, TransformerPipeline
 from sktime.transformations.panel.padder import PaddingTransformer
 from sktime.transformations.series.boxcox import LogTransformer
+from sktime.transformations.series.compose import OptionalPassthrough
 from sktime.transformations.series.exponent import ExponentTransformer
 from sktime.transformations.series.impute import Imputer
 from sktime.transformations.series.subset import ColumnSelect
@@ -224,3 +225,18 @@ def test_subset_getitem():
     )
     _assert_array_almost_equal(t_both.fit_transform(X), X_theta[["b__0", "b__2"]])
     _assert_array_almost_equal(t_none.fit_transform(X), X_theta)
+
+
+def test_dunder_neg():
+    """Test the neg dunder method, for wrapping in OptionalPassthrough."""
+    X = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
+
+    t = ExponentTransformer(power=2)
+
+    tp = -t
+
+    assert isinstance(tp, OptionalPassthrough)
+    assert not tp.get_params()["passthrough"]
+    assert isinstance(tp.get_params()["transformer"], ExponentTransformer)
+
+    _assert_array_almost_equal(tp.fit_transform(X), X)