Repository: sktime/sktime
Pull request #6144 (2024-03-17 01:55:04)

[MNT] PEP 508 environment markers for estimators
This PR adds functionality for adding PEP 508 environment markers for specifying estimator dependencies.

This is useful to specify operating systems with which the estimator is not compatible, e.g., `platform_system!="Windows"`.
The information is added as a string-valued tag `env_marker`, which must be PEP 508 compliant.

The logic is tied into `_check_estimator_deps`, so estimators marked with operating system excludes will not be tested on the respective operating system, and will raise a user friendly error message pointing to its requirements.

Minor change: moved default values for all current packaging tags related to the env up to the `BaseObject`. Later we could move this into `scikit-base`.

FYI @ianspektor, @achoum, @yarnabrina, this would allow us to release the temporian transformer with a tag that excludes windows operating systems, and we can delay the fix of the windows related issue to after the next `sktime` release. What do you think?

fkiraly: > I like the idea, but not exactly following how will we use it. Can you please share some details?

Situation: suppose an estimator is not compatible with a specific OS, e.g., windows. Like the current temporian one.

Solution: add the tag `env_marker='platform_sys!="Windows"'`. This solves the issue since (a) the estimator is not tested on windows VM, and (b) windows users will receive a message that this estimator is not compatible with windows.
ianspektor: This is awesome @fkiraly, thank you. Would love to get the transformer merged w/o it depending on the windows fix (that we will address asap anyway).
yarnabrina: > > I like the idea, but not exactly following how will we use it. Can you please share some details?
> 
> Situation: suppose an estimator is not compatible with a specific OS, e.g., windows. Like the current temporian one.
> 
> Solution: add the tag `env_marker='platform_sys!="Windows"'`. This solves the issue since (a) the estimator is not tested on windows VM, and (b) windows users will receive a message that this estimator is not compatible with windows.

Understood. Yes it makes sense, but just wondering if it's enough to add these markers in the pyproject.toml or not.

For example, suppose we add a marker for temporian that windows is not supported as of now with the upper bound version that we use. Then with an estimator tag users can not use thia transformer on windows until next release of sktime, even if @ianspektor fixes the issue and makes a new release of temporian.

In a lot cases we have seen that dependabot does not behave as expected or completely fail to detect new versions, plus even if it does users won't get it until one more month for next sktime patch/minor release. But if we add the tag just in pyproject.toml, CI will be happy in the meanwhile and users can start using on temporian if a new version is released with the fix.

(Just to be clear, I'm not opposing the tag, I'm just trying to think of possible string restrictions that may be imposed on users if a check is added to disablean estimator on a OS based on these tags. If it's just for informing users that it's  ot well tested on this OS, then I've no feedbacks.)
fkiraly: I see - perhaps it's better than if we add the `temporian` restriction in `pyproject` for the current release, and discuss how we manage estimator level dependencies, separately?

For `temporian`, using `pyproject` for now makes sense, since there is only a single dependent object/estimator.
ianspektor: We're fine with whatever makes it easy for you guys 👍🏼 I see you release ~once a month so even if it's not ideal I wouldn't worry too much about users having to wait for an sktime release once we've fixed the issue (just my 2 cents, I think the arguments for going with the pyproject make sense).

Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 264884c1d..4d039c995 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -2711,6 +2711,15 @@
         "maintenance"
       ]
     },
+    {
+      "login": "SamruddhiNavale",
+      "name": "Samruddhi Navale",
+      "avatar_url": "https://avatars.githubusercontent.com/u/86359115?v=4",
+      "profile": "https://github.com/SamruddhiNavale",
+      "contributions": [
+        "doc"
+      ]
+    },
     {
       "login": "vandit98",
       "name": "Vandit Tyagi",
diff --git a/docs/source/api_reference/classification.rst b/docs/source/api_reference/classification.rst
index 36872704b..f9abe7b2a 100644
--- a/docs/source/api_reference/classification.rst
+++ b/docs/source/api_reference/classification.rst
@@ -77,6 +77,7 @@ Dictionary-based
     :toctree: auto_generated/
     :template: class.rst
 
+    BOSSVSClassifierPyts
     BOSSEnsemble
     ContractableBOSS
     IndividualBOSS
@@ -194,6 +195,7 @@ Shapelet-based
     :template: class.rst
 
     ShapeletTransformClassifier
+    ShapeletLearningClassifierPyts
     ShapeletLearningClassifierTslearn
     MrSEQL
     MrSQM
diff --git a/docs/source/api_reference/tags.rst b/docs/source/api_reference/tags.rst
index 37de75a25..9cfb42fcb 100644
--- a/docs/source/api_reference/tags.rst
+++ b/docs/source/api_reference/tags.rst
@@ -64,6 +64,7 @@ These tags are typically used for typing, packaging and documentation purposes.
     python_version
     python_dependencies
     python_dependencies_alias
+    env_marker
     requires_cython
 
 
diff --git a/docs/source/api_reference/transformations.rst b/docs/source/api_reference/transformations.rst
index b584c4d25..d07f454ae 100644
--- a/docs/source/api_reference/transformations.rst
+++ b/docs/source/api_reference/transformations.rst
@@ -224,6 +224,14 @@ These transformers extract larger collections of features.
 
     Catch22
 
+.. currentmodule:: sktime.transformations.panel.catch22wrapper
+
+.. autosummary::
+    :toctree: auto_generated/
+    :template: class.rst
+
+    Catch22Wrapper
+
 Series-to-series transformers
 -----------------------------
 
diff --git a/sktime/base/_base.py b/sktime/base/_base.py
index 83c4b54e2..cf4e9339d 100644
--- a/sktime/base/_base.py
+++ b/sktime/base/_base.py
@@ -78,6 +78,14 @@ class BaseObject(_BaseObject):
     Extends skbase BaseObject with additional features.
     """
 
+    # global default tags for dependency management
+    _tags = {
+        "python_version": None,  # PEP 440 version specifier, e.g., ">=3.7"
+        "python_dependencies": None,  # PEP 440 dependency strs, e.g., "pandas>=1.0"
+        "python_dependencies_alias": {"scikit-learn": "sklearn"},
+        "env_marker": None,  # PEP 508 environment marker, e.g., "os_name=='posix'"
+    }
+
     _config = {
         "warnings": "on",
         "backend:parallel": None,  # parallelization backend for broadcasting
@@ -502,9 +510,6 @@ class BaseEstimator(BaseObject):
     Extends sktime's BaseObject to include basic functionality for fittable estimators.
     """
 
-    # global dependency alias tag for sklearn dependency management
-    _tags = {"python_dependencies_alias": {"scikit-learn": "sklearn"}}
-
     def __init__(self):
         self._is_fitted = False
         super().__init__()
diff --git a/sktime/base/adapters/_pyts.py b/sktime/base/adapters/_pyts.py
index 629674422..208744bee 100644
--- a/sktime/base/adapters/_pyts.py
+++ b/sktime/base/adapters/_pyts.py
@@ -47,6 +47,26 @@ class _PytsAdapter:
         setattr(self, self._estimator_attr, cls)
         return getattr(self, self._estimator_attr)
 
+    def _call_with_y_optional(self, method, X, y=None):
+        """Call method with or without y parameter.
+
+        The y parameter is passed only if the method has it.
+
+        Parameters
+        ----------
+        method: callable
+            Method to call
+        X, y: any objects
+        """
+        # check if method has y parameter
+        # if yes, call with y, otherwise without
+        method_has_y = "y" in signature(method).parameters
+
+        if method_has_y:
+            return method(X, y)
+        else:
+            return method(X)
+
     def _fit(self, X, y=None):
         """Fit estimator training data.
 
@@ -64,14 +84,7 @@ class _PytsAdapter:
         """
         pyts_est = self._init_pyts_object()
 
-        # check if pyts_est fit has y parameter
-        # if yes, call with y, otherwise without
-        pyts_has_y = "y" in signature(pyts_est.fit).parameters
-
-        if pyts_has_y:
-            pyts_est.fit(X, y)
-        else:
-            pyts_est.fit(X)
+        self._call_with_y_optional(pyts_est.fit, X, y)
 
         # write fitted params to self
         pyts_fitted_params = self._get_fitted_params_default(pyts_est)
@@ -96,14 +109,7 @@ class _PytsAdapter:
         """
         pyts_est = getattr(self, self._estimator_attr)
 
-        # check if pyts_est fit has y parameter
-        # if yes, call with y, otherwise without
-        pyts_has_y = "y" in signature(pyts_est.transform).parameters
-
-        if pyts_has_y:
-            return pyts_est.transform(X, y)
-        else:
-            return pyts_est.transform(X)
+        return self._call_with_y_optional(pyts_est.transform, X, y)
 
     def _predict(self, X, y=None):
         """Predict method adapter.
@@ -121,14 +127,7 @@ class _PytsAdapter:
         """
         pyts_est = getattr(self, self._estimator_attr)
 
-        # check if pyts_est fit has y parameter
-        # if yes, call with y, otherwise without
-        pyts_has_y = "y" in signature(pyts_est.predict).parameters
-
-        if pyts_has_y:
-            return pyts_est.predict(X, y)
-        else:
-            return pyts_est.predict(X)
+        return self._call_with_y_optional(pyts_est.predict, X, y)
 
     def _predict_proba(self, X, y=None):
         """Predict_proba method adapter.
@@ -147,11 +146,12 @@ class _PytsAdapter:
         """
         pyts_est = getattr(self, self._estimator_attr)
 
-        # check if pyts_est fit has y parameter
-        # if yes, call with y, otherwise without
-        pyts_has_y = "y" in signature(pyts_est.predict_proba).parameters
-
-        if pyts_has_y:
-            return pyts_est.predict_proba(X, y)
+        # check if pyts_est has predict_proba
+        # if not, call super's _predict_proba
+        pyts_has_predict_proba = hasattr(pyts_est, "predict_proba")
+        if not pyts_has_predict_proba:
+            method = super()._predict_proba
         else:
-            return pyts_est.predict_proba(X)
+            method = pyts_est.predict_proba
+
+        return self._call_with_y_optional(method, X, y)
diff --git a/sktime/classification/deep_learning/inceptiontime.py b/sktime/classification/deep_learning/inceptiontime.py
index 4a475a125..354009a8f 100644
--- a/sktime/classification/deep_learning/inceptiontime.py
+++ b/sktime/classification/deep_learning/inceptiontime.py
@@ -45,9 +45,9 @@ class InceptionTimeClassifier(BaseDeepClassifier):
     Examples
     --------
     >>> from sktime.classification.deep_learning import InceptionTimeClassifier
-    >>> from sktime.datasets import load_unit_test
-    >>> X_train, y_train = load_unit_test(split="train")
-    >>> X_test, y_test = load_unit_test(split="test")
+    >>> from sktime.datasets import load_unit_test  # doctest: +SKIP
+    >>> X_train, y_train = load_unit_test(split="train")  # doctest: +SKIP
+    >>> X_test, y_test = load_unit_test(split="test")  # doctest: +SKIP
     >>> clf = InceptionTimeClassifier()  # doctest: +SKIP
     >>> clf.fit(X_train, y_train)  # doctest: +SKIP
     InceptionTimeClassifier(...)
diff --git a/sktime/classification/deep_learning/lstmfcn.py b/sktime/classification/deep_learning/lstmfcn.py
index 3f6bcad88..e288a80d2 100644
--- a/sktime/classification/deep_learning/lstmfcn.py
+++ b/sktime/classification/deep_learning/lstmfcn.py
@@ -52,6 +52,17 @@ class LSTMFCNClassifier(BaseDeepClassifier):
     ----------
     .. [1] Karim et al. Multivariate LSTM-FCNs for Time Series Classification, 2019
     https://arxiv.org/pdf/1801.04503.pdf
+
+    Examples
+    --------
+    >>> import sktime.classification.deep_learning as dl_clf  # doctest: +SKIP
+    >>> from dl_clf.lstmfcn import LSTMFCNClassifier  # doctest: +SKIP
+    >>> from sktime.datasets import load_unit_test
+    >>> X_train, y_train = load_unit_test(split="train", return_X_y=True)
+    >>> X_test, y_test = load_unit_test(split="test", return_X_y=True)
+    >>> lstmfcn = FCNClassifier(n_epochs=20,batch_size=4)  # doctest: +SKIP
+    >>> lstmfcn.fit(X_train, y_train)  # doctest: +SKIP
+    FCNClassifier(...)
     """
 
     _tags = {
diff --git a/sktime/classification/dictionary_based/__init__.py b/sktime/classification/dictionary_based/__init__.py
index ed1605259..d0be17514 100644
--- a/sktime/classification/dictionary_based/__init__.py
+++ b/sktime/classification/dictionary_based/__init__.py
@@ -2,6 +2,7 @@
 __all__ = [
     "IndividualBOSS",
     "BOSSEnsemble",
+    "BOSSVSClassifierPyts",
     "ContractableBOSS",
     "TemporalDictionaryEnsemble",
     "IndividualTDE",
@@ -10,6 +11,7 @@ __all__ = [
 ]
 
 from sktime.classification.dictionary_based._boss import BOSSEnsemble, IndividualBOSS
+from sktime.classification.dictionary_based._boss_pyts import BOSSVSClassifierPyts
 from sktime.classification.dictionary_based._cboss import ContractableBOSS
 from sktime.classification.dictionary_based._muse import MUSE
 from sktime.classification.dictionary_based._tde import (
diff --git a/sktime/classification/dictionary_based/_boss.py b/sktime/classification/dictionary_based/_boss.py
index 5e742767f..0bcb4def2 100644
--- a/sktime/classification/dictionary_based/_boss.py
+++ b/sktime/classification/dictionary_based/_boss.py
@@ -116,7 +116,6 @@ class BOSSEnsemble(BaseClassifier):
     - `TSML <https://github.com/uea-machine-learning/tsml/blob/master/src/main/java/
     tsml/classifiers/dictionary_based/BOSS.java>`_.
 
-
     References
     ----------
     .. [1] Patrick Schäfer, "The BOSS is concerned with time series classification
diff --git a/sktime/classification/dictionary_based/_boss_pyts.py b/sktime/classification/dictionary_based/_boss_pyts.py
new file mode 100644
index 000000000..74df932b5
--- /dev/null
+++ b/sktime/classification/dictionary_based/_boss_pyts.py
@@ -0,0 +1,194 @@
+"""BOSS classifier, from pyts."""
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+
+__author__ = ["fkiraly"]
+__all__ = ["BOSSVSClassifierPyts"]
+
+from sktime.base.adapters._pyts import _PytsAdapter
+from sktime.classification.base import BaseClassifier
+
+
+class BOSSVSClassifierPyts(_PytsAdapter, BaseClassifier):
+    """Bag-of-SFA Symbols in Vector Space, from pyts.
+
+    Direct interface to ``pyts.classification.BOSSVS``,
+    author of the interfaced class is ``johannfaouzi``.
+
+    Each time series is transformed into an histogram using the
+    Bag-of-SFA Symbols (BOSS) algorithm. Then, for each class, the histograms
+    are added up and a tf-idf vector is computed. The predicted class for
+    a new sample is the class giving the highest cosine similarity between
+    its tf vector and the tf-idf vectors of each class.
+
+    Parameters
+    ----------
+    word_size : int (default = 4)
+        Size of each word.
+
+    n_bins : int (default = 4)
+        The number of bins to produce. It must be between 2 and 26.
+
+    window_size : int or float (default = 10)
+        Size of the sliding window. If float, it represents the percentage of
+        the size of each time series and must be between 0 and 1. The window
+        size will be computed as ``ceil(window_size * n_timestamps)``.
+
+    window_step : int or float (default = 1)
+        Step of the sliding window. If float, it represents the percentage of
+        the size of each time series and must be between 0 and 1. The window
+        size will be computed as ``ceil(window_step * n_timestamps)``.
+
+    anova : bool (default = False)
+        If True, the Fourier coefficient selection is done via a one-way
+        ANOVA test. If False, the first Fourier coefficients are selected.
+
+    drop_sum : bool (default = False)
+        If True, the first Fourier coefficient (i.e. the sum of the subseries)
+        is dropped. Otherwise, it is kept.
+
+    norm_mean : bool (default = False)
+        If True, center each subseries before scaling.
+
+    norm_std : bool (default = False)
+        If True, scale each subseries to unit variance.
+
+    strategy : str (default = 'quantile')
+        Strategy used to define the widths of the bins:
+
+        - 'uniform': All bins in each sample have identical widths
+        - 'quantile': All bins in each sample have the same number of points
+        - 'normal': Bin edges are quantiles from a standard normal distribution
+        - 'entropy': Bin edges are computed using information gain
+
+    alphabet : None, 'ordinal' or array-like, shape = (n_bins,)
+        Alphabet to use. If None, the first `n_bins` letters of the Latin
+        alphabet are used.
+
+    numerosity_reduction : bool (default = True)
+        If True, delete sample-wise all but one occurence of back to back
+        identical occurences of the same words.
+
+    use_idf : bool (default = True)
+        Enable inverse-document-frequency reweighting.
+
+    smooth_idf : bool (default = False)
+        Smooth idf weights by adding one to document frequencies, as if an
+        extra document was seen containing every term in the collection
+        exactly once. Prevents zero divisions.
+
+    sublinear_tf : bool (default = True)
+        Apply sublinear tf scaling, i.e. replace tf with 1 + log(tf).
+
+    Attributes
+    ----------
+    idf_ : array, shape = (n_features,) , or None
+        The learned idf vector (global term weights) when ``use_idf=True``,
+        None otherwise.
+
+    tfidf_ : array, shape = (n_classes, n_words)
+        Term-document matrix.
+
+    vocabulary_ : dict
+        A mapping of feature indices to terms.
+
+    References
+    ----------
+    .. [1] P. Schäfer, "Scalable Time Series Classification". Data Mining and
+           Knowledge Discovery, 30(5), 1273-1298 (2016).
+    """
+
+    _tags = {
+        # packaging info
+        # --------------
+        "authors": ["johannfaouzi", "fkiraly"],  # johannfaouzi is author of upstream
+        "python_dependencies": "pyts",
+        # estimator type
+        # --------------
+        "capability:multivariate": False,
+        "capability:unequal_length": False,
+        "capability:missing_values": True,
+        "capability:predict_proba": True,
+        "classifier_type": "dictionary",
+    }
+
+    # defines the name of the attribute containing the pyts estimator
+    _estimator_attr = "_pyts_boss"
+
+    def _get_pyts_class(self):
+        """Get pyts class.
+
+        should import and return pyts class
+        """
+        from pyts.classification import BOSSVS
+
+        return BOSSVS
+
+    def __init__(
+        self,
+        word_size=4,
+        n_bins=4,
+        window_size=10,
+        window_step=1,
+        anova=False,
+        drop_sum=False,
+        norm_mean=False,
+        norm_std=False,
+        strategy="quantile",
+        alphabet=None,
+        numerosity_reduction=True,
+        use_idf=True,
+        smooth_idf=False,
+        sublinear_tf=True,
+    ):
+        self.word_size = word_size
+        self.n_bins = n_bins
+        self.window_size = window_size
+        self.window_step = window_step
+        self.anova = anova
+        self.drop_sum = drop_sum
+        self.norm_mean = norm_mean
+        self.norm_std = norm_std
+        self.strategy = strategy
+        self.alphabet = alphabet
+        self.numerosity_reduction = numerosity_reduction
+        self.use_idf = use_idf
+        self.smooth_idf = smooth_idf
+        self.sublinear_tf = sublinear_tf
+
+        super().__init__()
+
+    @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 = {
+            "word_size": 3,
+            "n_bins": 3,
+            "window_size": 5,
+            "window_step": 2,
+            "anova": True,
+            "drop_sum": True,
+            "norm_mean": True,
+            "norm_std": True,
+            "strategy": "uniform",
+            "smooth_idf": True,
+            "sublinear_tf": False,
+        }
+        return [params1, params2]
diff --git a/sktime/classification/distance_based/_shape_dtw.py b/sktime/classification/distance_based/_shape_dtw.py
index 3d9960f3b..4728df3e2 100644
--- a/sktime/classification/distance_based/_shape_dtw.py
+++ b/sktime/classification/distance_based/_shape_dtw.py
@@ -108,6 +108,22 @@ class ShapeDTW(BaseClassifier):
     .. [1] Jiaping Zhao and Laurent Itti, "shapeDTW: Shape Dynamic Time Warping",
         Pattern Recognition, 74, pp 171-184, 2018
         http://www.sciencedirect.com/science/article/pii/S0031320317303710,
+
+    Examples
+    --------
+    >>> from sktime.classification.distance_based import ShapeDTW
+    >>> from sktime.datasets import load_unit_test  # doctest: +SKIP
+    >>> X_train, y_train = load_unit_test(split="train")  # doctest: +SKIP
+    >>> X_test, y_test = load_unit_test(split="test")  # doctest: +SKIP
+    >>> clf = ShapeDTW(n_neighbors=1,
+    ...     subsequence_length=30,
+    ...     shape_descriptor_function="raw",
+    ...     shape_descriptor_functions=None,
+    ...     metric_params=None,
+    ... )  # doctest: +SKIP
+    >>> clf.fit(X_train, y_train)  # doctest: +SKIP
+    ShapeDTW(...)
+    >>> y_pred = clf.predict(X_test)  # doctest: +SKIP
     """
 
     _tags = {
diff --git a/sktime/classification/distance_based/_time_series_neighbors_pyts.py b/sktime/classification/distance_based/_time_series_neighbors_pyts.py
index c2550aec5..68aa73a20 100644
--- a/sktime/classification/distance_based/_time_series_neighbors_pyts.py
+++ b/sktime/classification/distance_based/_time_series_neighbors_pyts.py
@@ -71,6 +71,26 @@ class KNeighborsTimeSeriesClassifierPyts(_PytsAdapter, BaseClassifier):
     ----------
     classes_ : array, shape = (n_classes,)
         An array of class labels known to the classifier.
+
+    Examples
+    --------
+    >>> import sktime.classification.distance_based as clf_db  # doctest: +SKIP
+    >>> from clf_db import KNeighborsTimeSeriesClassifierPyts  # doctest: +SKIP
+    >>> from sktime.datasets import load_unit_test  # doctest: +SKIP
+    >>> X_train, y_train = load_unit_test(split="train")  # doctest: +SKIP
+    >>> X_test, y_test = load_unit_test(split="test")  # doctest: +SKIP
+    >>> clf = KNeighborsTimeSeriesClassifierPyts(n_neighbors=1,
+    ...     weights="uniform",
+    ...     algorithm="auto",
+    ...     leaf_size=30,
+    ...     p=2,
+    ...     metric="minkowski",
+    ...     metric_params=None,
+    ...     n_jobs=1,
+    ... )  # doctest: +SKIP
+    >>> clf.fit(X_train, y_train)  # doctest: +SKIP
+    KNeighborsTimeSeriesClassifierPyts(...)
+    >>> y_pred = clf.predict(X_test)  # doctest: +SKIP
     """
 
     _tags = {
diff --git a/sktime/classification/distance_based/_time_series_neighbors_tslearn.py b/sktime/classification/distance_based/_time_series_neighbors_tslearn.py
index 050ca304e..d9e0a6fd9 100644
--- a/sktime/classification/distance_based/_time_series_neighbors_tslearn.py
+++ b/sktime/classification/distance_based/_time_series_neighbors_tslearn.py
@@ -55,6 +55,25 @@ class KNeighborsTimeSeriesClassifierTslearn(_TslearnAdapter, BaseClassifier):
         Above 50, the output is sent to stdout.
         The frequency of the messages increases with the verbosity level.
         If it more than 10, all iterations are reported.
+
+    Examples
+    --------
+    >>> import sktime.classification.distance_based as db_clf  # doctest: +SKIP
+    >>> from db_clf import KNeighborsTimeSeriesClassifierTslearn  # doctest: +SKIP
+    >>> from sktime.datasets import load_unit_test  # doctest: +SKIP
+    >>> X_train, y_train = load_unit_test(split="train")  # doctest: +SKIP
+    >>> X_test, y_test = load_unit_test(split="test")  # doctest: +SKIP
+    >>> clf = KNeighborsTimeSeriesClassifierTslearn(
+    ...     n_neighbors=5,
+    ...     weights="uniform",
+    ...     metric="dtw",
+    ...     metric_params=None,
+    ...     n_jobs=None,
+    ...     verbose=0,
+    ... )  # doctest: +SKIP
+    >>> clf.fit(X_train, y_train)  # doctest: +SKIP
+    KNeighborsTimeSeriesClassifierTslearn(...)
+    >>> y_pred = clf.predict(X_test)  # doctest: +SKIP
     """
 
     _tags = {
diff --git a/sktime/classification/ensemble/_ctsf.py b/sktime/classification/ensemble/_ctsf.py
index 782262982..8617cdfb8 100644
--- a/sktime/classification/ensemble/_ctsf.py
+++ b/sktime/classification/ensemble/_ctsf.py
@@ -171,6 +171,21 @@ class ComposableTimeSeriesForestClassifier(BaseTimeSeriesForest, BaseClassifier)
     ----------
     .. [1] Deng et. al, A time series forest for classification and feature extraction,
     Information Sciences, 239:2013.
+
+    Examples
+    --------
+    >>> from sktime.classification.ensemble import ComposableTimeSeriesForestClassifier
+    >>> from sktime.classification.kernel_based import RocketClassifier
+    >>> from sktime.datasets import load_unit_test
+    >>> X_train, y_train = load_unit_test(split="train") # doctest: +SKIP
+    >>> X_test, y_test = load_unit_test(split="test") # doctest: +SKIP
+    >>> clf = ComposableTimeSeriesForestClassifier(
+    ...     RocketClassifier(num_kernels=100),
+    ...     n_estimators=10,
+    ... )  # doctest: +SKIP
+    >>> clf.fit(X_train, y_train)  # doctest: +SKIP
+    ComposableTimeSeriesForestClassifier(...)
+    >>> y_pred = clf.predict(X_test)  # doctest: +SKIP
     """
 
     _tags = {
diff --git a/sktime/classification/feature_based/_fresh_prince.py b/sktime/classification/feature_based/_fresh_prince.py
index 25559f871..704117df7 100644
--- a/sktime/classification/feature_based/_fresh_prince.py
+++ b/sktime/classification/feature_based/_fresh_prince.py
@@ -57,6 +57,23 @@ class FreshPRINCE(BaseClassifier):
         scalable hypothesis tests (tsfresh–a python package)." Neurocomputing 307
         (2018): 72-77.
         https://www.sciencedirect.com/science/article/pii/S0925231218304843
+
+    Examples
+    --------
+    >>> from sktime.classification.feature_based import FreshPRINCE
+    >>> from sktime.datasets import load_unit_test
+    >>> X_train, y_train = load_unit_test(split="train", return_X_y=True)
+    >>> X_test, y_test = load_unit_test(split="test", return_X_y=True) # doctest: +SKIP
+    >>> clf = FreshPRINCE(
+    ...     default_fc_parameters="comprehensive",
+    ...     n_estimators=200,
+    ...     save_transformed_data=False,
+    ...     verbose=0,
+    ...     n_jobs=1,
+    ... ) # doctest: +SKIP
+    >>> clf.fit(X_train, y_train)  # doctest: +SKIP
+    FreshPRINCE(...)
+    >>> y_pred = clf.predict(X_test)  # doctest: +SKIP
     """
 
     _tags = {
diff --git a/sktime/classification/feature_based/_random_interval_classifier.py b/sktime/classification/feature_based/_random_interval_classifier.py
index a9d47553b..149f6dfc6 100644
--- a/sktime/classification/feature_based/_random_interval_classifier.py
+++ b/sktime/classification/feature_based/_random_interval_classifier.py
@@ -49,6 +49,20 @@ class RandomIntervalClassifier(BaseClassifier):
     See Also
     --------
     RandomIntervals
+
+    Examples
+    --------
+    >>> from sktime.classification.feature_based import RandomIntervalClassifier
+    >>> from sklearn.ensemble import RandomForestClassifier
+    >>> from sktime.datasets import load_unit_test
+    >>> X_train, y_train = load_unit_test(split="train", return_X_y=True)
+    >>> X_test, y_test = load_unit_test(split="test", return_X_y=True) # doctest: +SKIP
+    >>> clf = RandomIntervalClassifier(
+    ...     estimator=RandomForestClassifier(n_estimators=5)
+    ... )  # doctest: +SKIP
+    >>> clf.fit(X_train, y_train)  # doctest: +SKIP
+    RandomIntervalClassifier(...)
+    >>> y_pred = clf.predict(X_test)  # doctest: +SKIP
     """
 
     _tags = {
diff --git a/sktime/classification/feature_based/_tsfresh_classifier.py b/sktime/classification/feature_based/_tsfresh_classifier.py
index 259a487c0..526ada2ae 100644
--- a/sktime/classification/feature_based/_tsfresh_classifier.py
+++ b/sktime/classification/feature_based/_tsfresh_classifier.py
@@ -62,6 +62,21 @@ class TSFreshClassifier(BaseClassifier):
         scalable hypothesis tests (tsfresh–a python package)." Neurocomputing 307
         (2018): 72-77.
         https://www.sciencedirect.com/science/article/pii/S0925231218304843
+
+    Examples
+    --------
+    >>> from sktime.classification.feature_based import TSFreshClassifier
+    >>> from sklearn.ensemble import RandomForestClassifier
+    >>> from sktime.datasets import load_unit_test
+    >>> X_train, y_train = load_unit_test(split="train", return_X_y=True)
+    >>> X_test, y_test = load_unit_test(split="test", return_X_y=True) # doctest: +SKIP
+    >>> clf = TSFreshClassifier(
+    ...     estimator=RandomForestClassifier(n_estimators=5),
+    ...     default_fc_parameters="efficient",
+    ... ) # doctest: +SKIP
+    >>> clf.fit(X_train, y_train)  # doctest: +SKIP
+    TSFreshClassifier(...)
+    >>> y_pred = clf.predict(X_test)  # doctest: +SKIP
     """
 
     _tags = {
diff --git a/sktime/classification/shapelet_based/__init__.py b/sktime/classification/shapelet_based/__init__.py
index 7eb51eff3..f309cdd63 100644
--- a/sktime/classification/shapelet_based/__init__.py
+++ b/sktime/classification/shapelet_based/__init__.py
@@ -2,10 +2,14 @@
 __all__ = [
     "MrSEQL",
     "MrSQM",
+    "ShapeletLearningClassifierPyts",
     "ShapeletLearningClassifierTslearn",
     "ShapeletTransformClassifier",
 ]
 
+from sktime.classification.shapelet_based._learning_pyts import (
+    ShapeletLearningClassifierPyts,
+)
 from sktime.classification.shapelet_based._learning_tslearn import (
     ShapeletLearningClassifierTslearn,
 )
diff --git a/sktime/classification/shapelet_based/_learning_pyts.py b/sktime/classification/shapelet_based/_learning_pyts.py
new file mode 100644
index 000000000..4930b762f
--- /dev/null
+++ b/sktime/classification/shapelet_based/_learning_pyts.py
@@ -0,0 +1,293 @@
+"""Learning Shapelets classifier, from pyts."""
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+
+__author__ = ["fkiraly"]
+__all__ = ["ShapeletLearningClassifierPyts"]
+
+from sktime.base.adapters._pyts import _PytsAdapter
+from sktime.classification.base import BaseClassifier
+
+
+class ShapeletLearningClassifierPyts(_PytsAdapter, BaseClassifier):
+    """Learning Shapelets algorithm, from pyts.
+
+    Direct interface to ``pyts.classification.LearningShapelets`` or
+    ``pyts.classification.LearningShapeletsCrossEntropy``,
+    author of the interfaced classes is ``johannfaouzi``.
+
+    Dispatches to ``LearningShapelets`` or ``LearningShapeletsCrossEntropy``,
+    depending on the value of the ``loss`` parameter.
+
+    This estimator consists of two steps: computing the distances between the
+    shapelets and the time series, then carrying out empirical risk minimization
+    using these distances as linear features.
+    The risk is minimized using gradient descent; for ``loss="softmax"``,
+    this is mathematically equivalent to multinomial logistic regression
+    on shapelet features.
+    This algorithm learns the shapelets as well as
+    the coefficients of the classification.
+
+    Parameters
+    ----------
+    loss : str (default = 'softmax'), "softmax" or "crossentropy"
+        Loss function to use.
+        If "softmax", the loss function is the softmax function (logistic loss).
+        Dispatches to ``LearningShapelets``.
+        If "crossentropy", the loss function is the cross-entropy loss.
+        Dispatches to ``LearningShapeletsCrossEntropy``.
+
+    n_shapelets_per_size : int or float (default = 0.2)
+        Number of shapelets per size. If float, it represents
+        a fraction of the number of timestamps and the number
+        of shapelets per size is equal to
+        ``ceil(n_shapelets_per_size * n_timestamps)``.
+
+    min_shapelet_length : int or float (default = 0.1)
+        Minimum length of the shapelets. If float, it represents
+        a fraction of the number of timestamps and the minimum
+        length of the shapelets per size is equal to
+        ``ceil(min_shapelet_length * n_timestamps)``.
+
+    shapelet_scale : int (default = 3)
+        The different scales for the lengths of the shapelets.
+        The lengths of the shapelets are equal to
+        ``min_shapelet_length * np.arange(1, shapelet_scale + 1)``.
+        The total number of shapelets (and features)
+        is equal to ``n_shapelets_per_size * shapelet_scale``.
+
+    penalty : 'l1' or 'l2' (default = 'l2')
+        Used to specify the norm used in the penalization.
+
+    tol : float (default = 1e-3)
+        Tolerance for stopping criterion.
+
+    C : float (default = 1000)
+        Inverse of regularization strength. It must be a positive float.
+        Smaller values specify stronger regularization.
+
+    learning_rate : float (default = 1.)
+        Learning rate for gradient descent optimization. It must be a positive
+        float. Note that the learning rate will be automatically decreased
+        if the loss function is not decreasing.
+
+    max_iter : int (default = 1000)
+        Maximum number of iterations for gradient descent algorithm.
+
+    multi_class : {'multinomial', 'ovr', 'ovo'} (default = 'multinomial')
+        Strategy for multiclass classification.
+        Only used if ``loss="softmax"``. The options are as follows:
+        'multinomial' stands for multinomial cross-entropy loss.
+        'ovr' stands for one-vs-rest strategy.
+        'ovo' stands for one-vs-one strategy.
+        Ignored if the classification task is binary.
+
+    alpha : float (default = -100)
+        Scaling term in the softmin function. The lower, the more precised
+        the soft minimum will be. Default value should be good for
+        standardized time series.
+
+    fit_intercept : bool (default = True)
+        Specifies if a constant (a.k.a. bias or intercept) should be
+        added to the decision function.
+
+    intercept_scaling : float (default = 1.)
+        Scaling of the intercept. Only used if ``fit_intercept=True``.
+
+    class_weight : dict, None or 'balanced' (default = None)
+        Weights associated with classes in the form ``{class_label: weight}``.
+        If not given, all classes are supposed to have unit weight.
+        The "balanced" mode uses the values of y to automatically adjust
+        weights inversely proportional to class frequencies in the input data
+        as ``n_samples / (n_classes * np.bincount(y))``.
+
+    n_jobs : None or int (default = None)
+        The number of jobs to use for the computation.
+        Only used if ``loss="softmax"`` and ``multi_class`` is "ovr" or "ovo".
+
+    verbose : int (default = 0)
+        Controls the verbosity. It must be a non-negative integer.
+        If positive, loss at each iteration is printed.
+
+    random_state : None, int or RandomState instance (default = None)
+        The seed of the pseudo random number generator to use when shuffling
+        the data. If int, random_state is the seed used by the random number
+        generator. If RandomState instance, random_state is the random number
+        generator. If None, the random number generator is the RandomState
+        instance used by `np.random`.
+
+    Attributes
+    ----------
+    shapelets_ : array shape = (n_tasks, n_shapelets)
+        Learned shapelets. Each element of this array is a learned
+        shapelet.
+
+    coef_ : array, shape = (n_tasks, n_shapelets) or (n_classes, n_shapelets)
+        Coefficients for each shapelet in the decision function.
+
+    intercept_ : array, shape = (n_tasks,) or (n_classes,)
+        Intercepts (a.k.a. biases) added to the decision function.
+        If ``fit_intercept=False``, the intercepts are set to zero.
+
+    n_iter_ : array, shape = (n_tasks,)
+        Actual number of iterations.
+
+    Notes
+    -----
+    The number of tasks (n_tasks) depends on the value of ``multi_class``
+    and the number of classes. If there are two classes, the number of
+    tasks is equal to 1. If there are more than two classes, the number
+    of tasks is equal to:
+
+        - 1 if ``multi_class='multinomial'``
+        - n_classes if ``multi_class='ovr'``
+        - n_classes * (n_classes - 1) / 2 if ``multi_class='ovo'``
+
+    References
+    ----------
+    .. [1] J. Grabocka, N. Schilling, M. Wistuba and L. Schmidt-Thieme,
+           "Learning Time-Series Shapelets". International Conference on Data
+           Mining, 14, 392-401 (2014).
+    """
+
+    _tags = {
+        # packaging info
+        # --------------
+        "authors": ["johannfaouzi", "fkiraly"],  # johannfaouzi is author of upstream
+        "python_dependencies": "pyts",
+        # estimator type
+        # --------------
+        "capability:multioutput": False,
+        "capability:multivariate": False,
+        "capability:unequal_length": False,
+        "capability:missing_values": True,
+        "capability:predict_proba": True,
+        "classifier_type": "shapelet",
+    }
+
+    # defines the name of the attribute containing the pyts estimator
+    _estimator_attr = "_pyts_ls"
+
+    def _get_pyts_class(self):
+        """Get pyts class.
+
+        should import and return pyts class
+        """
+        if self.loss == "crossentropy":
+            from pyts.classification.learning_shapelets import (
+                CrossEntropyLearningShapelets,
+            )
+
+            return CrossEntropyLearningShapelets
+        elif self.loss == "softmax":
+            from pyts.classification import LearningShapelets
+
+            return LearningShapelets
+        else:
+            raise ValueError(f"Unknown loss function: {self.loss}")
+
+    def _get_pyts_object(self):
+        """Abstract method to initialize pyts object.
+
+        The default initializes result of _get_pyts_class
+        with self.get_params.
+        """
+        cls = self._get_pyts_class()
+        params = self.get_params()
+        params_inner = params.copy()
+        params_inner.pop("loss")
+
+        if self.loss == "crossentropy":
+            params_inner.pop("multi_class")
+            params_inner.pop("n_jobs")
+
+        return cls(**params_inner)
+
+    def __init__(
+        self,
+        loss="softmax",  # "softmax" or "crossentropy
+        n_shapelets_per_size=0.2,
+        min_shapelet_length=0.1,
+        shapelet_scale=3,
+        penalty="l2",
+        tol=0.001,
+        C=1000,
+        learning_rate=1.0,
+        max_iter=1000,
+        multi_class="multinomial",
+        alpha=-100,
+        fit_intercept=True,
+        intercept_scaling=1.0,
+        class_weight=None,
+        n_jobs=None,
+        verbose=0,
+        random_state=None,
+    ):
+        self.loss = loss
+        self.n_shapelets_per_size = n_shapelets_per_size
+        self.min_shapelet_length = min_shapelet_length
+        self.shapelet_scale = shapelet_scale
+        self.penalty = penalty
+        self.tol = tol
+        self.C = C
+        self.learning_rate = learning_rate
+        self.max_iter = max_iter
+        self.multi_class = multi_class
+        self.alpha = alpha
+        self.fit_intercept = fit_intercept
+        self.intercept_scaling = intercept_scaling
+        self.class_weight = class_weight
+        self.verbose = verbose
+        self.random_state = random_state
+        self.n_jobs = n_jobs
+
+        super().__init__()
+
+    @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 = {
+            "loss": "softmax",
+            "n_shapelets_per_size": 0.3,
+            "min_shapelet_length": 0.11,
+            "shapelet_scale": 4,
+            "penalty": "l1",
+            "tol": 0.01,
+            "C": 100,
+            "learning_rate": 0.9,
+            "max_iter": 500,
+            "multi_class": "ovr",
+            "alpha": -99,
+            "fit_intercept": False,
+        }
+        params3 = {
+            "loss": "crossentropy",
+            "n_shapelets_per_size": 0.3,
+            "min_shapelet_length": 0.11,
+            "shapelet_scale": 4,
+            "penalty": "l1",
+            "tol": 0.01,
+            "C": 100,
+            "learning_rate": 0.9,
+            "max_iter": 500,
+            "alpha": -99,
+            "fit_intercept": False,
+        }
+        return [params1, params2, params3]
diff --git a/sktime/forecasting/compose/_bagging.py b/sktime/forecasting/compose/_bagging.py
index bc9678268..c842f6cfa 100644
--- a/sktime/forecasting/compose/_bagging.py
+++ b/sktime/forecasting/compose/_bagging.py
@@ -2,7 +2,7 @@
 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file).
 """Implements Bagging Forecaster."""
 
-__author__ = ["ltsaprounis"]
+__author__ = ["fkiraly", "ltsaprounis"]
 
 from typing import List, Union
 
@@ -12,9 +12,10 @@ from sklearn.utils import check_random_state
 
 from sktime.datatypes._utilities import update_data
 from sktime.forecasting.base import BaseForecaster
-from sktime.forecasting.ets import AutoETS
+from sktime.proba.empirical import Empirical
 from sktime.transformations.base import BaseTransformer
-from sktime.transformations.bootstrap import STLBootstrapTransformer
+
+PANDAS_MTYPES = ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"]
 
 
 class BaggingForecaster(BaseForecaster):
@@ -82,13 +83,15 @@ class BaggingForecaster(BaseForecaster):
     """
 
     _tags = {
-        "authors": ["ltsaprounis"],
-        "scitype:y": "univariate",  # which y are fine? univariate/multivariate/both
-        "ignores-exogeneous-X": True,  # does estimator ignore the exogeneous X?
-        "handles-missing-data": False,  # can estimator handle missing data?
-        "y_inner_mtype": "pd.Series",  # which types do _fit, _predict, assume for y?
-        "X_inner_mtype": "pd.DataFrame",  # which types do _fit, _predict, assume for X?
-        "X-y-must-have-same-index": True,  # can estimator handle different X/y index?
+        "authors": ["fkiraly", "ltsaprounis"],
+        "scitype:y": "both",  # which y are fine? univariate/multivariate/both
+        "ignores-exogeneous-X": False,  # does estimator ignore the exogeneous X?
+        "handles-missing-data": True,  # can estimator handle missing data?
+        "y_inner_mtype": PANDAS_MTYPES,
+        # which types do _fit, _predict, assume for y?
+        "X_inner_mtype": PANDAS_MTYPES,
+        # which types do _fit, _predict, assume for X?
+        "X-y-must-have-same-index": False,  # can estimator handle different X/y index?
         "requires-fh-in-fit": False,  # like AutoETS overwritten if forecaster not None
         "enforce_index_type": None,  # like AutoETS overwritten if forecaster not None
         "capability:insample": True,  # can the estimator make in-sample predictions?
@@ -125,6 +128,80 @@ class BaggingForecaster(BaseForecaster):
         if forecaster is not None:
             self.clone_tags(self.forecaster, tags_to_clone)
 
+        self.bootstrap_transformer_ = self._check_transformer(bootstrap_transformer)
+        self.forecaster_ = self._check_forecaster(forecaster)
+
+    def _check_transformer(self, transformer):
+        """Check if the transformer is a valid transformer for BaggingForecaster.
+
+        Also replaces with default if transformer is None
+
+        Parameters
+        ----------
+        transformer : BaseTransformer
+            The transformer to check
+
+        Returns
+        -------
+        fresh clone of the transformer to set to self.bootstrap_transformer_
+        """
+        from sktime.registry import scitype
+
+        if transformer is None:
+            from sktime.transformations.bootstrap import STLBootstrapTransformer
+
+            return STLBootstrapTransformer(sp=self.sp, random_state=self.random_state)
+
+        msg = (
+            "Error in BaggingForecaster: "
+            "bootstrap_transformer in BaggingForecaster should be an sktime transformer"
+            " that takes as input a Series and output a Panel."
+        )
+
+        t_inp = transformer.get_tag("scitype:transform-input", raise_error=False)
+        t_out = transformer.get_tag("scitype:transform-output", raise_error=False)
+
+        if t_inp != "Series" or t_out != "Panel":
+            raise TypeError(msg)
+        if scitype(transformer) != "transformer":
+            raise TypeError(msg)
+
+        if hasattr(transformer, "clone"):
+            return transformer.clone()
+        else:
+            from sklearn import clone
+
+            return clone(transformer)
+
+    def _check_forecaster(self, forecaster):
+        """Check if the forecaster is a valid transformer for BaggingForecaster.
+
+        Also replaces with default if forecaster is None
+
+        Parameters
+        ----------
+        forecaster : BaseForecaster
+            The forecaster to check
+
+        Returns
+        -------
+        fresh clone of the forecaster to set to self.forecaster_
+        """
+        from sktime.registry import scitype
+
+        if forecaster is None:
+            from sktime.forecasting.ets import AutoETS
+
+            return AutoETS(sp=self.sp, random_state=self.random_state)
+
+        if not scitype(forecaster) == "forecaster":
+            raise TypeError(
+                "Error in BaggingForecaster: "
+                "forecaster in BaggingForecaster should be an sktime forecaster"
+            )
+
+        return forecaster.clone()
+
     def _fit(self, y, X, fh):
         """Fit forecaster to training data.
 
@@ -135,13 +212,8 @@ class BaggingForecaster(BaseForecaster):
 
         Parameters
         ----------
-        y : guaranteed to be of a type in self.get_tag("y_inner_mtype")
+        y : pd.DataFrame
             Time series to which to fit the forecaster.
-            if self.get_tag("scitype:y")=="univariate":
-                guaranteed to have a single column/variable
-            if self.get_tag("scitype:y")=="multivariate":
-                guaranteed to have 2 or more columns
-            if self.get_tag("scitype:y")=="both": no restrictions apply
         fh : guaranteed to be ForecastingHorizon or None, optional (default=None)
             The forecasting horizon with the steps ahead to to predict.
             Required (non-optional) here if self.get_tag("requires-fh-in-fit")==True
@@ -154,49 +226,49 @@ class BaggingForecaster(BaseForecaster):
         -------
         self : reference to self
         """
-        if self.bootstrap_transformer is None:
-            self.bootstrap_transformer_ = STLBootstrapTransformer(sp=self.sp)
-        elif hasattr(self.bootstrap_transformer, "clone"):
-            self.bootstrap_transformer_ = self.bootstrap_transformer.clone()
-        else:
-            from sklearn import clone
+        self._y_ix_names = y.index.names
 
-            self.bootstrap_transformer_ = clone(self.bootstrap_transformer)
-
-        if self.forecaster is None:
-            self.forecaster_ = AutoETS(sp=self.sp)
-        else:
-            self.forecaster_ = self.forecaster.clone()
+        # random state handling passed into input estimators
+        self.random_state_ = check_random_state(self.random_state)
 
-        if (
-            self.bootstrap_transformer_.get_tag(
-                "scitype:transform-input", raise_error=False
-            )
-            != "Series"
-            and self.bootstrap_transformer_.get_tag(
-                "scitype:transform-output", raise_error=False
-            )
-            != "Panel"
-            and not isinstance(self.bootstrap_transformer_, BaseTransformer)
-        ):
-            raise TypeError(
-                "bootstrap_transformer in BaggingForecaster should be a Transformer "
-                "that takes as input a Series and output a Panel."
-            )
+        # fit/transform the transformer to obtain bootstrap samples
+        y_bootstraps = self.bootstrap_transformer_.fit_transform(X=y)
+        self._y_bs_ix = y_bootstraps.index
 
-        if not isinstance(self.forecaster_, BaseForecaster):
-            raise TypeError(
-                "forecaster in BaggingForecaster should be an sktime Forecaster"
-            )
+        # generate replicates of exogenous data for bootstrap
+        X_inner = self._gen_X_bootstraps(X)
 
-        # random state handling passed into input estimators
-        self.random_state_ = check_random_state(self.random_state)
-        self.bootstrap_transformer_.fit(X=y)
-        y_bootstraps = self.bootstrap_transformer_.transform(X=y)
-        self.forecaster_.fit(y=y_bootstraps, fh=fh, X=None)
+        # fit the forecaster to the bootstrapped samples
+        self.forecaster_.fit(y=y_bootstraps, fh=fh, X=X_inner)
 
         return self
 
+    def _gen_X_bootstraps(self, X):
+        """Generate replicates of exogenous data for bootstrap.
+
+        Accesses self._y_bs_ix to obtain the index of the bootstrapped time series.
+
+        Parameters
+        ----------
+        X : pd.DataFrame
+            Exogenous time series, non-hierarchical
+
+        Returns
+        -------
+        X_bootstraps : pd.DataFrame
+            Bootstrapped exogenous data
+        """
+        if X is None:
+            return None
+
+        y_bs_ix = self._y_bs_ix
+
+        # bootstrap instance index ends up at level -2
+        inst_ix = y_bs_ix.get_level_values(-2).unique()
+        X_repl = [X] * len(inst_ix)
+        X_bootstraps = pd.concat(X_repl, keys=inst_ix)
+        return X_bootstraps
+
     def _predict(self, fh, X):
         """Forecast time series at future horizon.
 
@@ -219,48 +291,66 @@ class BaggingForecaster(BaseForecaster):
 
         Returns
         -------
-        y_pred : pd.Series
+        y_pred : pd.DataFrame
             Point predictions
         """
-        y_bootstraps_pred = self.forecaster_.predict(fh=fh, X=None)
-        y_pred = y_bootstraps_pred.groupby(level=-1).mean().iloc[:, 0]
-        y_pred.name = self._y.name
-        return y_pred
+        # generate replicates of exogenous data for bootstrap
+        X_inner = self._gen_X_bootstraps(X)
 
-    def _predict_quantiles(self, fh, X, alpha):
-        """Compute/return prediction quantiles for a forecast.
+        # compute bootstrapped forecasts
+        y_bootstraps_pred = self.forecaster_.predict(fh=fh, X=X_inner)
 
-        private _predict_quantiles containing the core logic,
-            called from predict_quantiles and possibly predict_interval
+        # aggregate bootstrapped forecasts
+        # the bootstrap index ends up at level -2, so we have to groupby the rest
+        n_ist_lv = y_bootstraps_pred.index.nlevels - 2
+        gb_lvls = [-1]
+        if n_ist_lv > 0:
+            gb_lvls = list(range(n_ist_lv)) + gb_lvls
 
-        State required:
-            Requires state to be "fitted".
+        y_pred = y_bootstraps_pred.groupby(level=gb_lvls).mean()
+        y_pred.index.names = self._y_ix_names
+        return y_pred
 
-        Accesses in self:
-            Fitted model attributes ending in "_"
-            self.cutoff
+    def _predict_proba(self, fh, X, marginal=True):
+        """Compute/return fully probabilistic forecasts.
+
+        private _predict_proba containing the core logic, called from predict_proba
 
         Parameters
         ----------
-        fh : int, list, np.array or ForecastingHorizon
-            Forecasting horizon
-        X : pd.DataFrame, optional (default=None)
-            Exogenous time series
-        alpha : list of float (guaranteed not None and floats in [0,1] interval)
-            A list of probabilities at which quantile forecasts are computed.
+        fh : guaranteed to be ForecastingHorizon
+            The forecasting horizon with the steps ahead to to predict.
+        X : optional (default=None)
+            guaranteed to be of a type in self.get_tag("X_inner_mtype")
+            Exogeneous time series to predict from.
+        marginal : bool, optional (default=True)
+            whether returned distribution is marginal by time index
 
         Returns
         -------
-        pred_quantiles : pd.DataFrame
-            Column has multi-index: first level is variable name from y in fit,
-                second level being the quantile forecasts for each alpha.
-                Quantile forecasts are calculated for each a in alpha.
-            Row index is fh. Entries are quantile forecasts, for var in col index,
-                at quantile probability in second-level col index, for each row index.
+        pred_dist : sktime BaseDistribution
+            predictive distribution
+            if marginal=True, will be marginal distribution by time point
+            if marginal=False and implemented by method, will be joint
         """
-        # X is ignored
-        y_pred = self.forecaster_.predict(fh=fh, X=None)
-        return self._calculate_data_quantiles(y_pred, alpha)
+        # generate replicates of exogenous data for bootstrap
+        X_inner = self._gen_X_bootstraps(X)
+
+        # compute bootstrapped forecasts
+        y_bootstraps_pred = self.forecaster_.predict(fh=fh, X=X_inner)
+
+        # aggregate bootstrapped forecasts
+        # the bootstrap index ends up at level -2,
+        # while Empirical assumes bootstrap index as level
+        # so we have to reorder if -2 is not the same as 0
+        n_ist_lv = y_bootstraps_pred.index.nlevels - 2
+        if n_ist_lv > 0:
+            y_bootstraps_pred = y_bootstraps_pred.reorder_levels(
+                [-2] + list(range(n_ist_lv)) + [-1], axis=0
+            )
+
+        pred_dist = Empirical(y_bootstraps_pred, time_indep=marginal)
+        return pred_dist
 
     def _update(self, y, X=None, update_params=True):
         """Update cutoff value and, optionally, fitted parameters.
@@ -278,14 +368,16 @@ class BaggingForecaster(BaseForecaster):
         -------
         self : reference to self
         """
-        # Need to construct a completely new y out of ol self._y and y and then
+        # Need to construct a completely new y out of old self._y and y and then
         # fit_treansform the transformer and re-fit the forecaster.
         _y = update_data(self._y, y)
 
-        self.bootstrap_transformer_.fit(X=_y)
-        y_bootstraps = self.bootstrap_transformer_.transform(X=_y)
-        self.forecaster_.fit(y=y_bootstraps, fh=self.fh, X=None)
+        y_bootstraps = self.bootstrap_transformer_.fit_transform(X=_y)
 
+        # generate replicates of exogenous data for bootstrap
+        X_inner = self._gen_X_bootstraps(X)
+
+        self.forecaster_.update(y=y_bootstraps, X=X_inner, update_params=update_params)
         return self
 
     @classmethod
@@ -301,16 +393,13 @@ class BaggingForecaster(BaseForecaster):
             instance.
             ``create_test_instance`` uses the first (or only) dictionary in ``params``
         """
+        from sktime.forecasting.compose import YfromX
         from sktime.transformations.bootstrap import MovingBlockBootstrapTransformer
-        from sktime.utils.estimators import MockForecaster
         from sktime.utils.validation._dependencies import _check_soft_dependencies
 
-        params = [
-            {
-                "bootstrap_transformer": MovingBlockBootstrapTransformer(),
-                "forecaster": MockForecaster(),
-            },
-        ]
+        mbb = MovingBlockBootstrapTransformer(block_length=6)
+        fcst = YfromX.create_test_instance()
+        params = [{"bootstrap_transformer": mbb, "forecaster": fcst}]
 
         # the default param set causes a statsmodels based estimator
         # to be created as bootstrap_transformer
diff --git a/sktime/proba/empirical.py b/sktime/proba/empirical.py
index 5f3d0131a..8c25bee0c 100644
--- a/sktime/proba/empirical.py
+++ b/sktime/proba/empirical.py
@@ -63,7 +63,7 @@ class Empirical(BaseDistribution):
         self._N = len(_spl_instances)
 
         if index is None:
-            index = pd.Index(_timestamps)
+            index = _timestamps
 
         if columns is None:
             columns = spl.columns
@@ -127,6 +127,7 @@ class Empirical(BaseDistribution):
                 if x is None:
                     x_t = None
                 elif hasattr(x, "loc"):
+                    # x_t = x.at[ix, col]
                     x_t = x.loc[ix, col]
                 else:
                     x_t = x
diff --git a/sktime/registry/_tags.py b/sktime/registry/_tags.py
index cdbee7d27..835489919 100644
--- a/sktime/registry/_tags.py
+++ b/sktime/registry/_tags.py
@@ -336,6 +336,51 @@ class python_dependencies_alias(_BaseTag):
     }
 
 
+class env_marker(_BaseTag):
+    """Environment marker requirement for the object (PEP 508).
+
+    Part of packaging metadata for the object.
+
+    - String name: ``"env_marker"``
+    - Private tag, developer and framework facing
+    - Values: str, PEP 508 compliant environment marker
+    - Example: ``"platform_system == 'Linux'"``
+    - Default: no environment marker (``None``)
+
+    ``sktime`` manages objects and estimators like mini-packages,
+    with their own dependencies and compatibility requirements.
+    Dependencies are specified in the tags:
+
+    - ``"python_version"``: Python version specifier (PEP 440) for the object,
+    - ``"python_dependencies"``: list of required Python packages (PEP 440)
+    - ``"python_dependencies_alias"``: alias for package names,
+      if different from import names
+    - ``"env_marker"``: environment marker for the object (PEP 508)
+    - ``"requires_cython"``: whether the object requires a C compiler present
+
+    The ``env_marker`` tag of an object is a string,
+    specifying a PEP 508 compliant environment marker for the object.
+
+        The tag is used in packaging metadata for the object,
+    and is used internally to check compatibility of the object with
+    the build environment, to raise informative error messages.
+
+    Developers can use ``_check_env_marker`` from ``skbase.utils.dependencies``
+    to check compatibility of the python constraint of the object
+    with the current build environment, or
+    ``_check_estimator_deps`` to check compatibility of the object
+    (including further checks) with the current build environment.
+    """
+
+    _tags = {
+        "tag_name": "env_marker",
+        "parent_type": "object",
+        "tag_type": "str",
+        "short_descr": "environment marker (PEP 508) requirement for estimator, or None = no marker",  # noqa: E501
+        "user_facing": False,
+    }
+
+
 class requires_cython(_BaseTag):
     """Whether the object requires a C compiler present, such as libomp, gcc.
 
diff --git a/sktime/transformations/panel/catch22wrapper.py b/sktime/transformations/panel/catch22wrapper.py
index c71a03721..27c14bcd6 100644
--- a/sktime/transformations/panel/catch22wrapper.py
+++ b/sktime/transformations/panel/catch22wrapper.py
@@ -14,9 +14,10 @@ from sktime.transformations.panel import catch22
 
 
 class Catch22Wrapper(BaseTransformer):
-    """Canonical Time-series Characteristics (Catch22) C Wrapper.
+    """Canonical Time-series Characteristics (Catch22 and 24), using pycatch22 package.
 
-    Wraps the pycatch22 implementation for sktime
+    Direct interface to the ``pycatch22`` implementation of Catch-22 and Catch-24
+    feature sets
     (https://github.com/DynamicsAndNeuralSystems/pycatch22).
 
     Overview: Input n series with d dimensions of length m
@@ -59,7 +60,8 @@ class Catch22Wrapper(BaseTransformer):
 
     See Also
     --------
-    Catch22 Catch22Classifier
+    Catch22
+    Catch22Classifier
 
     References
     ----------
@@ -74,8 +76,8 @@ class Catch22Wrapper(BaseTransformer):
     _tags = {
         # packaging info
         # --------------
-        "authors": ["MatthewMiddlehurst", "fkiraly"],
-        "maintainers": "benfulcher",
+        "authors": ["benfulcher", "jmoo2880", "MatthewMiddlehurst", "fkiraly"],
+        "maintainers": ["benfulcher", "jmoo2880"],
         "python_dependencies": "pycatch22",
         # estimator type
         # --------------
diff --git a/sktime/transformations/series/adapt.py b/sktime/transformations/series/adapt.py
index c7759e86a..5b7526912 100644
--- a/sktime/transformations/series/adapt.py
+++ b/sktime/transformations/series/adapt.py
@@ -21,69 +21,88 @@ class TabularToSeriesAdaptor(BaseTransformer):
     do not require multiple :term:`instances <instance>` for fitting.
 
     The adaptor behaves as follows.
-    If fit_in_transform = False and X is a series (pd.DataFrame, pd.Series, np.ndarray):
-        ``fit(X)`` fits a clone of ``transformer`` to X (considered as a table)
-        ``transform(X)`` applies transformer.transform to X and returns the result
-        ``inverse_transform(X)`` applies transformer.inverse_transform to X
-    If fit_in_transform = True and X is a series (pd.DataFrame, pd.Series, np.ndarray):
-        ``fit`` is empty
-        ``transform(X)`` applies transformer.fit(X).transform(X) to X,
-            considered as a table, and returns the result
-        ``inverse_transform(X)`` applies transformer.fit(X).inverse_transform(X) to X
-
-    If fit_in_transform = False, and X is of a panel/hierarchical type:
-        ``fit(X)`` fits a clone of ``transformer`` for each individual series x in X
-        ``transform(X)`` applies transform(x) of the clone belonging to x,
-                (where the index of x in transform equals the index of x in fit)
-            for each individual series x in X, and returns the result
-        ``inverse_transform(X)`` applies transform(x) of the clone belonging to x,
-                (where the index of x in transform equals the index of x in fit)
-            for each individual series x in X, and returns the result
-        Note: instances indices in transform/inverse_transform
-            must be equal to those seen in fit
-    If fit_in_transform = True, and X is of a panel/hierarchical type:
-        ``fit`` is empty
-        ``transform(X)`` applies transformer.fit(x).transform(x)
-            to all individual series x in X and returns the result
-        ``inverse_transform(X)`` applies transformer.fit(x).inverse_transform(x)
-            to all individual series x in X and returns the result
-
-    WARNING: if fit_in_transform is set to False,
+
+    If ``fit_in_transform = False`` and ``X`` is a series
+    (``pd.DataFrame``, ``pd.Series``, ``np.ndarray``):
+
+        * ``fit(X)`` fits a clone of ``transformer`` to X (considered as a table)
+        * ``transform(X)`` applies transformer.transform to X and returns the result
+        * ``inverse_transform(X)`` applies ``transformer.inverse_transform`` to ``X``
+
+    If ``fit_in_transform = True`` and ``X`` is a series
+    (``pd.DataFrame``, ``pd.Series``, ``np.ndarray``):
+
+        * ``fit`` is empty
+        * ``transform(X)`` applies ``transformer.fit(X).transform(X)`` to ``X``,
+          considered as a table, and returns the result
+        * ``inverse_transform(X)`` applies ``transformer.fit(X).inverse_transform(X)``
+          to ``X``
+
+    If ``fit_in_transform = False``, and ``X`` is of a panel/hierarchical type:
+        * ``fit(X)`` fits a clone of ``transformer`` for each individual
+          series ``x`` in ``X``
+        * ``transform(X)`` applies ``transform(x)`` of the clone belonging to ``x``
+          (where the index of x in transform equals the index of x in fit),
+          for each individual series ``x`` in ``X``, and returns the result
+        * ``inverse_transform(X)`` applies ``transform(x)`` of the clone belonging to
+          ``x`` (where the index of x in transform equals the index of ``x`` in fit),
+          for each individual series ``x`` in ``X``, and returns the result
+        * Note: instances indices in ``transform/inverse_transform``
+          must be equal to those seen in ``fit``
+
+    If ``fit_in_transform = True``, and ``X`` is of a panel/hierarchical type:
+        * ``fit`` is empty
+        * ``transform(X)`` applies ``transformer.fit(x).transform(x)``
+          to all individual series ``x`` in ``X`` and returns the result
+        * ``inverse_transform(X)`` applies ``transformer.fit(x).inverse_transform(x)``
+          to all individual series ``x`` in ``X`` and returns the result
+
+    WARNING: if ``fit_in_transform`` is set to ``False``,
         when applied to Panel or Hierarchical data,
         the resulting transformer will identify individual series in test set
         with series indices in training set, on which instances were fit
         in particular, transform will not work if number of instances
-            and indices of instances in transform are different from those in fit
-    WARNING: if fit_in_transform is set to True,
+        and indices of instances in transform are different from those in fit
+
+    WARNING: if ``fit_in_transform`` is set to ``True``,
         then each series in the test set will be transformed as batch by fit-predict,
         this may cause information leakage in a forecasting setting
-            (but not in a time series classification/regression/clustering setting,
-            because in these settings the independent samples are the individual series)
+        (but not in a time series classification/regression/clustering setting,
+        because in these settings the independent samples are the individual series)
 
     Whether ``y`` is passed to transformer methods is controlled by ``pass_y``.
     If the inner transformer has non-defaulting ``y`` args, the default behaviour is
     to pass ``y`` to ``fit``, ``fit_transform``, or ``transform``.
     If no ``y`` arg is present, or if it has a default value, ``y`` is not passed.
 
+    If the passed transformer accepts only ``y`` in ``fit`` and ``transform``,
+    then ``pass_y`` is ignored, and ``X`` is plugged into the ``y`` argument.
+
     Parameters
     ----------
-    transformer : Estimator
+    transformer : ``sklearn`` transformer, ``BaseEstimator`` descendant instance
         scikit-learn-like transformer to fit and apply to series.
         This is used as a "blueprint" and not fitted or otherwise mutated.
+
     fit_in_transform: bool, optional, default=False
-        whether transformer_ should be fitted in transform (True), or in fit (False)
-            recommended setting in forecasting (single series or hierarchical): False
-            recommended setting in ts classification, regression, clustering: True
+        whether transformer_ should be fitted in transform (True), or in fit (False).
+
+        * recommended setting in forecasting (single series or hierarchical): ``False``
+        * recommended setting in ts classification, regression, clustering: ``True``
+
     pass_y : str, optional, one of "auto" (default), "fit", "always", "never"
         Whether to pass y to transformer methods of the ``transformer`` clone.
-        "auto": passes y to methods fit, transform, fit_transform, inverse_transform,
-            if and only if y is a named arg of either method without default.
-            Note: passes y even if it is None
-        "fit": passes y to method fit, but not to transform.
-            Note: passes y even if it is None, or if not a named arg
-        "always": passes y to all methods, fit, transform, inverse_transform.
-            Note: passes y even if it is None, or if not a named arg
-        "never": never passes y to any method.
+
+        * "auto": passes ``y`` to methods ``fit``, ``transform``, ``fit_transform``,
+          ``inverse_transform``,
+          if and only if ``y`` is a named arg of either method without default.
+          Note: passes ``y`` even if it is ``None``
+        * "fit": passes ``y`` to method ``fit``, but not to ``transform``.
+          Note: passes ``y`` even if it is ``None``, or if not a named arg
+        * "always": passes ``y`` to all methods, ``fit``, ``transform``,
+          ``inverse_transform``.
+          Note: passes ``y`` even if it is ``None``, or if not a named arg
+        * "never": never passes ``y`` to any method.
 
     Attributes
     ----------
@@ -140,33 +159,50 @@ class TabularToSeriesAdaptor(BaseTransformer):
         if self._skip_fit:
             self.set_tags(**{"fit_is_empty": True})
 
-        trafo_has_y, trafo_has_y_default = self._trafo_has_y_and_default("fit")
+        trafo_has_y, trafo_has_y_default = self._trafo_has_param_and_default("fit", "y")
         need_y = trafo_has_y and not trafo_has_y_default
         if need_y or pass_y not in ["auto", "no"]:
             self.set_tags(**{"y_inner_mtype": "numpy1D"})
 
-    def _trafo_has_y_and_default(self, method="fit"):
-        """Return if transformer.method has a y, and whether y has a default."""
+    def _trafo_has_param_and_default(self, method="fit", arg="y"):
+        """Return if transformer.method has a parameter, and whether it has a default.
+
+        Parameters
+        ----------
+        method : str, optional, default="fit"
+            method name to check
+        arg : str, optional, default="y"
+            parameter name to check
+
+        Returns
+        -------
+        has_param : bool
+            whether the method ``method`` has a parameter with name ``arg``
+        has_default : bool
+            whether the parameter ``arg`` of method ``method`` has a default value
+        """
         method_fun = getattr(self.transformer, method)
         method_params = list(signature(method_fun).parameters.keys())
-        if "y" in method_params:
-            y_param = signature(self.transformer.fit).parameters["y"]
-            y_default = y_param.default
-            y_has_default = y_default is not y_param.empty
-            return True, y_has_default
+        if arg in method_params:
+            param = signature(self.transformer.fit).parameters[arg]
+            default = param.default
+            has_default = default is not param.empty
+            return True, has_default
         else:
             return False, False
 
-    def _get_y_args(self, y, method="fit"):
-        """Get empty dict or dict with y, depending on pass_y and method.
+    def _get_args(self, X, y, method="fit"):
+        """Get kwargs for method, depending on pass_y and method.
 
-        The return is a dict which is passed to the method of name method,
-        according to the pass_y setting.
+        The return is a dict which is passed to the method of name method.
         """
+        if not self._trafo_has_param_and_default(method, "X"):
+            return {"y": X}
+
         pass_y = self.pass_y
 
         if pass_y == "auto":
-            has_y, has_y_default = self._trafo_has_y_and_default(method)
+            has_y, has_y_default = self._trafo_has_param_and_default(method, "y")
             need_y = has_y and not has_y_default
             return_y = need_y
         elif pass_y == "fit":
@@ -182,9 +218,9 @@ class TabularToSeriesAdaptor(BaseTransformer):
             )
 
         if return_y:
-            return {"y": y}
+            return {"X": X, "y": y}
         else:
-            return {}
+            return {"X": X}
 
     def _fit(self, X, y=None):
         """Fit transformer to X and y.
@@ -202,10 +238,10 @@ class TabularToSeriesAdaptor(BaseTransformer):
         -------
         self: a fitted instance of the estimator
         """
-        y_args = self._get_y_args(y, method="fit")
+        fit_args = self._get_args(X, y, method="fit")
 
         if not self._skip_fit:
-            self.transformer_.fit(X, **y_args)
+            self.transformer_.fit(**fit_args)
 
         return self
 
@@ -226,13 +262,13 @@ class TabularToSeriesAdaptor(BaseTransformer):
         Xt : 2D np.ndarray
             transformed version of X
         """
-        y_fit_args = self._get_y_args(y, method="fit")
-        y_trafo_args = self._get_y_args(y, method="transform")
+        fit_args = self._get_args(X, y, method="fit")
+        trafo_args = self._get_args(X, y, method="transform")
 
         if self._skip_fit:
-            Xt = self.transformer_.fit(X, **y_fit_args).transform(X, **y_trafo_args)
+            Xt = self.transformer_.fit(**fit_args).transform(**trafo_args)
         else:
-            Xt = self.transformer_.transform(X)
+            Xt = self.transformer_.transform(**trafo_args)
 
         # coerce sensibly to 2D np.ndarray
         if isinstance(Xt, (int, float, str)):
@@ -261,13 +297,13 @@ class TabularToSeriesAdaptor(BaseTransformer):
         Xt : 2D np.ndarray
             inverse transformed version of X
         """
-        y_fit_args = self._get_y_args(y, method="fit")
-        y_i_args = self._get_y_args(y, method="inverse_transform")
+        fit_args = self._get_args(X, y, method="fit")
+        it_args = self._get_args(X, y, method="inverse_transform")
 
         if self.fit_in_transform:
-            Xt = self.transformer_.fit(X, **y_fit_args).inverse_transform(X, **y_i_args)
+            Xt = self.transformer_.fit(X, **fit_args).inverse_transform(X, **it_args)
         else:
-            Xt = self.transformer_.inverse_transform(X, **y_i_args)
+            Xt = self.transformer_.inverse_transform(**it_args)
         return Xt
 
     @classmethod
diff --git a/sktime/transformations/series/augmenter.py b/sktime/transformations/series/augmenter.py
index 2bbfeec0b..b2da6e50c 100755
--- a/sktime/transformations/series/augmenter.py
+++ b/sktime/transformations/series/augmenter.py
@@ -58,6 +58,16 @@ class WhiteNoiseAugmenter(_AugmenterTags, BaseTransformer):
             If None, rely on ``self.random_state``.
             Default is None." [3]
 
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from sktime.transformations.series.augmenter import WhiteNoiseAugmenter
+    >>> X = np.array([1, 2, 3, 4, 5])
+    >>> augmenter = WhiteNoiseAugmenter(scale=0.5, random_state=42)
+    >>> augmenter.fit(X)
+    WhiteNoiseAugmenter(...)
+    >>> X_augmented = augmenter.transform(X)
+
     References and Footnotes
     ----------
 
diff --git a/sktime/transformations/series/detrend/_deseasonalize.py b/sktime/transformations/series/detrend/_deseasonalize.py
index 0e2897aa1..8f740789d 100644
--- a/sktime/transformations/series/detrend/_deseasonalize.py
+++ b/sktime/transformations/series/detrend/_deseasonalize.py
@@ -593,7 +593,7 @@ class STLTransformer(BaseTransformer):
         # for inverse transform, we sum up the columns
         # this will be close if return_components=True
         row_sums = X.sum(axis=1)
-        row_sums.columns = self.fit_column_names
+        row_sums.columns = self._X.columns
         return row_sums
 
     def _make_return_object(self, X, stl):
diff --git a/sktime/utils/validation/_dependencies.py b/sktime/utils/validation/_dependencies.py
index ce9740491..28609adaf 100644
--- a/sktime/utils/validation/_dependencies.py
+++ b/sktime/utils/validation/_dependencies.py
@@ -8,6 +8,7 @@ import warnings
 from importlib import import_module
 from inspect import isclass
 
+from packaging.markers import InvalidMarker, Marker
 from packaging.requirements import InvalidRequirement, Requirement
 from packaging.specifiers import InvalidSpecifier, SpecifierSet
 
@@ -374,6 +375,81 @@ def _check_python_version(obj, package=None, msg=None, severity="error"):
     return True
 
 
+def _check_env_marker(obj, package=None, msg=None, severity="error"):
+    """Check if packaging marker tag is with requirements of obj.
+
+    Parameters
+    ----------
+    obj : sktime estimator, BaseObject descendant
+        used to check python version
+    package : str, default = None
+        if given, will be used in error message as package name
+    msg : str, optional, default = default message (msg below)
+        error message to be returned in the `ModuleNotFoundError`, overrides default
+    severity : str, "error" (default), "warning", or "none"
+        whether the check should raise an error, a warning, or nothing
+
+    Returns
+    -------
+    compatible : bool, whether obj is compatible with system python version
+        check is using the python_version tag of obj
+
+    Raises
+    ------
+    InvalidMarker
+        User friendly error if obj has env_marker tag that is not a
+        packaging compatible marker string
+    ModuleNotFoundError
+        User friendly error if obj has an env_marker tag that is
+        incompatible with the python environment. If package is given,
+        error message gives package as the reason for incompatibility.
+    """
+    est_marker_tag = obj.get_class_tag("env_marker", tag_value_default="None")
+    if est_marker_tag in ["None", None]:
+        return True
+
+    try:
+        est_marker = Marker(est_marker_tag)
+    except InvalidMarker:
+        msg_version = (
+            f"wrong format for env_marker tag, "
+            f"must be PEP 508 compatible specifier string, e.g., "
+            f'platform_system!="windows", but found "{est_marker_tag}"'
+        )
+        raise InvalidMarker(msg_version)
+
+    if est_marker.evaluate():
+        return True
+    # now we know that est_marker is not compatible with the environment
+
+    if isclass(obj):
+        class_name = obj.__name__
+    else:
+        class_name = type(obj).__name__
+
+    if not isinstance(msg, str):
+        msg = (
+            f"{class_name} requires an environment to satisfy "
+            f"packaging marker spec {est_marker}, but enviroment does not satisfy it."
+        )
+
+        if package is not None:
+            msg += f" This is due to requirements of the {package} package."
+
+    if severity == "error":
+        raise ModuleNotFoundError(msg)
+    elif severity == "warning":
+        warnings.warn(msg, stacklevel=2)
+    elif severity == "none":
+        return False
+    else:
+        raise RuntimeError(
+            "Error in calling _check_env_marker, severity "
+            f'argument must be "error", "warning", or "none", found "{severity}".'
+        )
+    return True
+
+
 def _check_estimator_deps(obj, msg=None, severity="error"):
     """Check if object/estimator's package & python requirements are met by python env.
 
@@ -424,6 +500,7 @@ def _check_estimator_deps(obj, msg=None, severity="error"):
         return compatible
 
     compatible = compatible and _check_python_version(obj, severity=severity)
+    compatible = compatible and _check_env_marker(obj, severity=severity)
 
     pkg_deps = obj.get_class_tag("python_dependencies", None)
     pck_alias = obj.get_class_tag("python_dependencies_alias", None)


Test patch:
diff --git a/sktime/base/tests/test_base.py b/sktime/base/tests/test_base.py
index ca27a9232..074263d71 100644
--- a/sktime/base/tests/test_base.py
+++ b/sktime/base/tests/test_base.py
@@ -63,6 +63,9 @@ FIXTURE_OBJECT._tags_dynamic = {"A": 42424241, "B": 3}
 
 FIXTURE_OBJECT_TAGS = {"A": 42424241, "B": 3, "C": 1234, 3: "E"}
 
+# default tags in BaseObject
+DEFAULT_TAGS = BaseObject._tags
+
 
 def test_get_class_tags():
     """Tests get_class_tags class method of BaseObject for correctness.
@@ -75,7 +78,9 @@ def test_get_class_tags():
 
     msg = "Inheritance logic in BaseObject.get_class_tags is incorrect"
 
-    assert child_tags == FIXTURE_CLASSCHILD_TAGS, msg
+    expected_tags = FIXTURE_CLASSCHILD_TAGS.copy()
+    expected_tags.update(DEFAULT_TAGS)
+    assert child_tags == expected_tags, msg
 
 
 def test_get_class_tag():
@@ -117,7 +122,9 @@ def test_get_tags():
 
     msg = "Inheritance logic in BaseObject.get_tags is incorrect"
 
-    assert object_tags == FIXTURE_OBJECT_TAGS, msg
+    expected_tags = FIXTURE_OBJECT_TAGS.copy()
+    expected_tags.update(DEFAULT_TAGS)
+    assert object_tags == expected_tags, msg
 
 
 def test_get_tag():
@@ -175,7 +182,10 @@ def test_set_tags():
     msg = "Setter/override logic in BaseObject.set_tags is incorrect"
 
     assert FIXTURE_OBJECT_SET._tags_dynamic == FIXTURE_OBJECT_SET_DYN, msg
-    assert FIXTURE_OBJECT_SET.get_tags() == FIXTURE_OBJECT_SET_TAGS, msg
+
+    expected_tags = FIXTURE_OBJECT_SET_TAGS.copy()
+    expected_tags.update(DEFAULT_TAGS)
+    assert FIXTURE_OBJECT_SET.get_tags() == expected_tags, msg
 
 
 class CompositionDummy(BaseObject):
diff --git a/sktime/transformations/bootstrap/tests/__init__.py b/sktime/transformations/bootstrap/tests/__init__.py
index 6c46bafe0..4bd3f4f44 100644
--- a/sktime/transformations/bootstrap/tests/__init__.py
+++ b/sktime/transformations/bootstrap/tests/__init__.py
@@ -1,1 +1,1 @@
-"""Tests bor bootstrap Transformers."""
+"""Tests bootstrap Transformers."""
diff --git a/sktime/transformations/series/detrend/tests/test_deseasonalise.py b/sktime/transformations/series/detrend/tests/test_deseasonalise.py
index b39c860e3..0c32a650e 100644
--- a/sktime/transformations/series/detrend/tests/test_deseasonalise.py
+++ b/sktime/transformations/series/detrend/tests/test_deseasonalise.py
@@ -15,9 +15,6 @@ from sktime.utils.validation._dependencies import _check_soft_dependencies
 
 MODELS = ["additive", "multiplicative"]
 
-y = make_forecasting_problem()
-y_train, y_test = temporal_train_test_split(y, train_size=0.75)
-
 
 @pytest.mark.skipif(
     not _check_soft_dependencies("statsmodels", severity="none"),
@@ -27,6 +24,9 @@ y_train, y_test = temporal_train_test_split(y, train_size=0.75)
 def test_deseasonalised_values(sp):
     from statsmodels.tsa.seasonal import seasonal_decompose
 
+    y = make_forecasting_problem()
+    y_train, _ = temporal_train_test_split(y, train_size=0.75)
+
     transformer = Deseasonalizer(sp=sp)
     transformer.fit(y_train)
     actual = transformer.transform(y_train)
@@ -43,6 +43,9 @@ def test_deseasonalised_values(sp):
 @pytest.mark.parametrize("sp", TEST_SPS)
 @pytest.mark.parametrize("model", MODELS)
 def test_transform_time_index(sp, model):
+    y = make_forecasting_problem()
+    y_train, y_test = temporal_train_test_split(y, train_size=0.75)
+
     transformer = Deseasonalizer(sp=sp, model=model)
     transformer.fit(y_train)
     yt = transformer.transform(y_test)
@@ -56,6 +59,9 @@ def test_transform_time_index(sp, model):
 @pytest.mark.parametrize("sp", TEST_SPS)
 @pytest.mark.parametrize("model", MODELS)
 def test_inverse_transform_time_index(sp, model):
+    y = make_forecasting_problem()
+    y_train, y_test = temporal_train_test_split(y, train_size=0.75)
+
     transformer = Deseasonalizer(sp=sp, model=model)
     transformer.fit(y_train)
     yit = transformer.inverse_transform(y_test)
@@ -69,6 +75,9 @@ def test_inverse_transform_time_index(sp, model):
 @pytest.mark.parametrize("sp", TEST_SPS)
 @pytest.mark.parametrize("model", MODELS)
 def test_transform_inverse_transform_equivalence(sp, model):
+    y = make_forecasting_problem()
+    y_train, y_test = temporal_train_test_split(y, train_size=0.75)
+
     transformer = Deseasonalizer(sp=sp, model=model)
     transformer.fit(y_train)
     yit = transformer.inverse_transform(transformer.transform(y_train))
@@ -98,3 +107,24 @@ def test_deseasonalizer_in_pipeline():
     train_df = all_df["1949":"1950"]
     model.fit(train_df)
     model.update(y=all_df["1951":"1951"])
+
+
+@pytest.mark.skipif(
+    not _check_soft_dependencies("statsmodels", severity="none"),
+    reason="skip test if required soft dependency not available",
+)
+@pytest.mark.parametrize("return_components", [True, False])
+def test_stl_inverse(return_components):
+    """Test STLtransformer inverse_transform."""
+    from sktime.datasets import load_airline
+    from sktime.transformations.series.detrend import STLTransformer
+
+    X = load_airline()
+    transformer = STLTransformer(sp=12, return_components=return_components)
+    transformer.fit_transform(X)
+    Xit = transformer.inverse_transform(X)
+    # this currently fails, bug #6337
+    # Xit = transformer.inverse_transform(Xt)
+
+    # check that the index is preserved
+    assert X.index.equals(Xit.index)