Repository: sktime/sktime
Pull request #8160 (2025-04-05 23:01:24)

[ENH] automatic transformer coercion in explicit transformer pipeline specification syntax
This PR adds automatic transformer coercion in transformer pipelines, and removes the need for manual transformer coercions such as of `sklearn` estimators through `TabularToTransformAdaptor`.

This is achieved through use of the `registry._scitype_coercion` module, which also has minor updates and bugfixes in this PR - the coercion logic is moved from the dunder to the construction of the stpes.

Complements https://github.com/sktime/sktime/pull/8153 for transformation pipelines.


Patch:
diff --git a/docs/source/conf.py b/docs/source/conf.py
index b2e0ff174..3da02c57e 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -350,7 +350,7 @@ def _make_estimator_overview(app):
             "capability:insample",
             "capability:pred_int",
             "capability:pred_int:insample",
-            "handles-missing-data",
+            "capability:missing_values",
             "ignores-exogeneous-X",
             "scitype:y",
             "requires-fh-in-fit",
@@ -364,7 +364,7 @@ def _make_estimator_overview(app):
             "scitype:transform-output",
             "scitype:transform-labels",
             "capability:inverse_transform",
-            "handles-missing-data",
+            "capability:missing_values",
             "capability:missing_values:removes",
             "capability:unequal_length",
             "capability:unequal_length:removes",
diff --git a/extension_templates/forecasting.py b/extension_templates/forecasting.py
index 4686cfdeb..1c1c6d97c 100644
--- a/extension_templates/forecasting.py
+++ b/extension_templates/forecasting.py
@@ -130,7 +130,7 @@ class MyForecaster(BaseForecaster):
         # if not None, raises exception if X.index, y.index level -1 is not of that type
         #
         # handles-missing-data = can estimator handle missing data?
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         # valid values: boolean True (yes), False (no)
         # if False, raises exception if y or X passed contain missing data (nans)
         #
@@ -225,7 +225,7 @@ class MyForecaster(BaseForecaster):
         # if est.foo == 42:
         #   self.set_tags(handles-missing-data=True)
         # example 2: cloning tags from component
-        #   self.clone_tags(est2, ["enforce_index_type", "handles-missing-data"])
+        #   self.clone_tags(est2, ["enforce_index_type", "capability:missing_values"])
 
     # todo: implement this, mandatory
     def _fit(self, y, X, fh):
diff --git a/extension_templates/transformer.py b/extension_templates/transformer.py
index 13ca7dc9a..2ae257c86 100644
--- a/extension_templates/transformer.py
+++ b/extension_templates/transformer.py
@@ -233,7 +233,7 @@ class MyTransformer(BaseTransformer):
         # used for search index and validity checking, does not raise direct exception
         #
         # handles-missing-data = can the transformer handle missing data (np or pd.NA)?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         # valid values: boolean True (yes), False (no)
         # if False, may raise exception when passed time series with missing values
         #
@@ -319,7 +319,7 @@ class MyTransformer(BaseTransformer):
         # if est.foo == 42:
         #   self.set_tags(handles-missing-data=True)
         # example 2: cloning tags from component
-        #   self.clone_tags(est2, ["enforce_index_type", "handles-missing-data"])
+        #   self.clone_tags(est2, ["enforce_index_type", "capability:missing_values"])
 
     # todo: implement this, mandatory (except in special case below)
     def _fit(self, X, y=None):
diff --git a/extension_templates/transformer_simple.py b/extension_templates/transformer_simple.py
index 7074b19dd..511ad8311 100644
--- a/extension_templates/transformer_simple.py
+++ b/extension_templates/transformer_simple.py
@@ -171,7 +171,7 @@ class MyTransformer(BaseTransformer):
         # if False, may raise exception when passed unequal length Panel/Hierarchical
         #
         # handles-missing-data = can the transformer handle missing data (np or pd.NA)?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         # valid values: boolean True (yes), False (no)
         # if False, may raise exception when passed time series with missing values
         #
diff --git a/extension_templates/transformer_supersimple.py b/extension_templates/transformer_supersimple.py
index 400f53f95..eebad2a91 100644
--- a/extension_templates/transformer_supersimple.py
+++ b/extension_templates/transformer_supersimple.py
@@ -95,7 +95,7 @@ class MyTransformer(BaseTransformer):
         "fit_is_empty": False,
         "capability:inverse_transform": False,
         "capability:unequal_length": True,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     # todo: add any hyper-parameters and components to constructor
diff --git a/pyproject.toml b/pyproject.toml
index e05dd44d7..205412c51 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -89,7 +89,6 @@ all_extras = [
   'h5py; python_version < "3.12"',
   'hmmlearn>=0.2.7; python_version < "3.11"',
   'holidays; python_version < "3.13"',
-  'keras-self-attention; python_version < "3.11"',
   'matplotlib!=3.9.1,>=3.3.2; python_version < "3.13"',
   'mne; python_version < "3.13"',
   'numba<0.62,>=0.53; python_version < "3.13"',
@@ -135,7 +134,6 @@ all_extras_pandas2 = [
   'h5py; python_version < "3.12"',
   'hmmlearn>=0.2.7; python_version < "3.11"',
   'holidays; python_version < "3.13"',
-  'keras-self-attention; python_version < "3.11"',
   'matplotlib!=3.9.1,>=3.3.2; python_version < "3.13"',
   'mne; python_version < "3.13"',
   'numba<0.62,>=0.53; python_version < "3.13"',
@@ -212,7 +210,6 @@ forecasting = [
   'tbats<1.2,>=1.1; python_version < "3.12"',
 ]
 networks = [
-  'keras-self-attention<0.52,>=0.51; python_version < "3.13"',
   'tensorflow<2.20,>=2; python_version < "3.13"',
 ]
 param_est = [
diff --git a/sktime/base/_base.py b/sktime/base/_base.py
index 20ccdb30e..876ecac53 100644
--- a/sktime/base/_base.py
+++ b/sktime/base/_base.py
@@ -56,11 +56,9 @@ State:
 __author__ = ["mloning", "RNKuhns", "fkiraly"]
 __all__ = ["BaseEstimator", "BaseObject"]
 
-import warnings
-from copy import deepcopy
-
 from skbase.base import BaseEstimator as _BaseEstimator
 from skbase.base import BaseObject as _BaseObject
+from skbase.base._base import TagAliaserMixin as _TagAliaserMixin
 from sklearn import clone
 from sklearn.base import BaseEstimator as _SklearnBaseEstimator
 
@@ -343,16 +341,22 @@ class BaseObject(_HTMLDocumentationLinkMixin, _BaseObject):
             return pickle.loads(file.open("_obj").read())
 
 
-class TagAliaserMixin:
+class TagAliaserMixin(_TagAliaserMixin):
     """Mixin class for tag aliasing and deprecation of old tags.
 
-    To deprecate tags, add the TagAliaserMixin to BaseObject or BaseEstimator.
-    alias_dict contains the deprecated tags, and supports removal and renaming.     For
-    removal, add an entry "old_tag_name": ""     For renaming, add an entry
-    "old_tag_name": "new_tag_name" deprecate_dict contains the version number of
-    renaming or removal.     the keys in deprecate_dict should be the same as in
-    alias_dict.     values in deprecate_dict should be strings, the version of
-    removal/renaming.
+    To deprecate tags, add the ``TagAliaserMixin`` to ``BaseObject``
+    or ``BaseEstimator``.
+
+    ``alias_dict`` contains the deprecated tags, and supports removal and renaming.
+
+    * For removal, add an entry ``"old_tag_name": ""``
+    * For renaming, add an entry ``"old_tag_name": "new_tag_name"``
+
+    ``deprecate_dict`` contains the version number of renaming or removal.
+
+    * The keys in ``deprecate_dict`` should be the same as in alias_dict.
+    * Values in ``deprecate_dict`` should be strings, the version of
+    removal/renaming, in PEP 440 format, e.g., ``"1.0.0"``.
 
     The class will ensure that new tags alias old tags and vice versa, during the
     deprecation period. Informative warnings will be raised whenever the deprecated tags
@@ -360,167 +364,14 @@ class TagAliaserMixin:
 
     When removing tags, ensure to remove the removed tags from this class. If no tags
     are deprecated anymore (e.g., all deprecated tags are removed/renamed), ensure
-    toremove this class as a parent of BaseObject or BaseEstimator.
+    to remove this class as a parent of ``BaseObject`` or ``BaseEstimator``.
     """
 
-    def __init__(self):
-        super().__init__()
-
-    @classmethod
-    def get_class_tags(cls):
-        """Get class tags from estimator class and all its parent classes.
-
-        Returns
-        -------
-        collected_tags : dict
-            Dictionary of tag name : tag value pairs. Collected from _tags
-            class attribute via nested inheritance. NOT overridden by dynamic
-            tags set by set_tags or mirror_tags.
-        """
-        collected_tags = super().get_class_tags()
-        collected_tags = cls._complete_dict(collected_tags)
-        return collected_tags
-
-    @classmethod
-    def get_class_tag(cls, tag_name, tag_value_default=None):
-        """Get tag value from estimator class (only class tags).
-
-        Parameters
-        ----------
-        tag_name : str
-            Name of tag value.
-        tag_value_default : any type
-            Default/fallback value if tag is not found.
-
-        Returns
-        -------
-        tag_value :
-            Value of the ``tag_name`` tag in self. If not found, returns
-            ``tag_value_default``.
-        """
-        cls._deprecate_tag_warn([tag_name])
-        return super().get_class_tag(
-            tag_name=tag_name, tag_value_default=tag_value_default
-        )
-
-    def get_tags(self):
-        """Get tags from estimator class and dynamic tag overrides.
-
-        Returns
-        -------
-        collected_tags : dict
-            Dictionary of tag name : tag value pairs. Collected from _tags
-            class attribute via nested inheritance and then any overrides
-            and new tags from _tags_dynamic object attribute.
-        """
-        collected_tags = super().get_tags()
-        collected_tags = self._complete_dict(collected_tags)
-        return collected_tags
-
-    def get_tag(self, tag_name, tag_value_default=None, raise_error=True):
-        """Get tag value from estimator class and dynamic tag overrides.
+    alias_dict = {"handles-missing-data": "capability:missing_values"}
+    deprecate_dict = {"handles-missing-data": "1.0.0"}
 
-        Parameters
-        ----------
-        tag_name : str
-            Name of tag to be retrieved
-        tag_value_default : any type, optional; default=None
-            Default/fallback value if tag is not found
-        raise_error : bool
-            whether a ValueError is raised when the tag is not found
-
-        Returns
-        -------
-        tag_value :
-            Value of the ``tag_name`` tag in self. If not found, returns an error if
-            raise_error is True, otherwise it returns ``tag_value_default``.
-
-        Raises
-        ------
-        ValueError if raise_error is True i.e. if tag_name is not in self.get_tags(
-        ).keys()
-        """
-        self._deprecate_tag_warn([tag_name])
-        return super().get_tag(
-            tag_name=tag_name,
-            tag_value_default=tag_value_default,
-            raise_error=raise_error,
-        )
-
-    def set_tags(self, **tag_dict):
-        """Set dynamic tags to given values.
-
-        Parameters
-        ----------
-        tag_dict : dict
-            Dictionary of tag name : tag value pairs.
-
-        Returns
-        -------
-        Self :
-            Reference to self.
 
-        Notes
-        -----
-        Changes object state by setting tag values in tag_dict as dynamic tags
-        in self.
-        """
-        self._deprecate_tag_warn(tag_dict.keys())
-
-        tag_dict = self._complete_dict(tag_dict)
-        super().set_tags(**tag_dict)
-        return self
-
-    @classmethod
-    def _complete_dict(cls, tag_dict):
-        """Add all aliased and aliasing tags to the dictionary."""
-        alias_dict = cls.alias_dict
-        deprecated_tags = set(tag_dict.keys()).intersection(alias_dict.keys())
-        new_tags = set(tag_dict.keys()).intersection(alias_dict.values())
-
-        if len(deprecated_tags) > 0 or len(new_tags) > 0:
-            new_tag_dict = deepcopy(tag_dict)
-            # for all tag strings being set, write the value
-            #   to all tags that could *be aliased by* the string
-            #   and all tags that could be *aliasing* the string
-            # this way we ensure upwards and downwards compatibility
-            for old_tag, new_tag in alias_dict.items():
-                for tag in tag_dict:
-                    if tag == old_tag and new_tag != "":
-                        new_tag_dict[new_tag] = tag_dict[tag]
-                    if tag == new_tag:
-                        new_tag_dict[old_tag] = tag_dict[tag]
-            return new_tag_dict
-        else:
-            return tag_dict
-
-    @classmethod
-    def _deprecate_tag_warn(cls, tags):
-        """Print warning message for tag deprecation.
-
-        Parameters
-        ----------
-        tags : list of str
-
-        Raises
-        ------
-        DeprecationWarning for each tag in tags that is aliased by cls.alias_dict
-        """
-        for tag_name in tags:
-            if tag_name in cls.alias_dict.keys():
-                version = cls.deprecate_dict[tag_name]
-                new_tag = cls.alias_dict[tag_name]
-                msg = f"tag {tag_name!r} will be removed in sktime version {version}"
-                if new_tag != "":
-                    msg += (
-                        f" and replaced by {new_tag!r}, please use {new_tag!r} instead"
-                    )
-                else:
-                    msg += ', please remove code that access or sets "{tag_name}"'
-                warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
-
-
-class BaseEstimator(_BaseEstimator, BaseObject):
+class BaseEstimator(TagAliaserMixin, _BaseEstimator, BaseObject):
     """Base class for defining estimators in sktime.
 
     Extends sktime's BaseObject to include basic functionality for fittable estimators.
diff --git a/sktime/classification/compose/_pipeline.py b/sktime/classification/compose/_pipeline.py
index 3ba8422ed..57bf39c21 100644
--- a/sktime/classification/compose/_pipeline.py
+++ b/sktime/classification/compose/_pipeline.py
@@ -126,7 +126,9 @@ class ClassifierPipeline(_HeterogenousMetaEstimator, BaseClassifier):
         # can handle missing values iff: both classifier and all transformers can,
         #   *or* transformer chain removes missing data
         missing = classifier.get_tag("capability:missing_values", False)
-        missing = missing and self.transformers_.get_tag("handles-missing-data", False)
+        missing = missing and self.transformers_.get_tag(
+            "capability:missing_values", False
+        )
         missing = missing or self.transformers_.get_tag(
             "capability:missing_values:removes", False
         )
@@ -457,7 +459,7 @@ class SklearnClassifierPipeline(_HeterogenousMetaEstimator, BaseClassifier):
         # can handle missing values iff transformer chain removes missing data
         # sklearn classifiers might be able to handle missing data (but no tag there)
         # so better set the tag liberally
-        missing = self.transformers_.get_tag("handles-missing-data", False)
+        missing = self.transformers_.get_tag("capability:missing_values", False)
         missing = missing or self.transformers_.get_tag(
             "capability:missing_values:removes", False
         )
diff --git a/sktime/classification/deep_learning/cntc.py b/sktime/classification/deep_learning/cntc.py
index e4404761a..496521f86 100644
--- a/sktime/classification/deep_learning/cntc.py
+++ b/sktime/classification/deep_learning/cntc.py
@@ -79,7 +79,7 @@ class CNTCClassifier(BaseDeepClassifier):
             "AurumnPegasus",
         ],
         "maintainers": ["James-Large", "Withington", "AurumnPegasus"],
-        "python_dependencies": ["tensorflow", "keras-self-attention"],
+        "python_dependencies": ["tensorflow"],
     }
 
     def __init__(
diff --git a/sktime/clustering/compose/_as_transform.py b/sktime/clustering/compose/_as_transform.py
index 03e11dd42..01ff50549 100644
--- a/sktime/clustering/compose/_as_transform.py
+++ b/sktime/clustering/compose/_as_transform.py
@@ -67,7 +67,7 @@ class ClustererAsTransformer(BaseTransformer):
         "skip-inverse-transform": True,  # is inverse-transform skipped when called?
         "capability:unequal_length": True,
         "capability:unequal_length:removes": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "capability:missing_values:removes": True,
     }
 
@@ -91,7 +91,7 @@ class ClustererAsTransformer(BaseTransformer):
         # forward tag information
         tags_to_set = {
             "univariate-only": not multivariate,
-            "handles-missing-data": missing,
+            "capability:missing_values": missing,
             "capability:unequal_length": unequal,
         }
         self.set_tags(**tags_to_set)
diff --git a/sktime/clustering/compose/_pipeline.py b/sktime/clustering/compose/_pipeline.py
index e13d5599b..ca57e5597 100644
--- a/sktime/clustering/compose/_pipeline.py
+++ b/sktime/clustering/compose/_pipeline.py
@@ -125,7 +125,9 @@ class ClustererPipeline(_HeterogenousMetaEstimator, BaseClusterer):
         # can handle missing values iff: both clusterer and all transformers can,
         #   *or* transformer chain removes missing data
         missing = clusterer.get_tag("capability:missing_values", False)
-        missing = missing and self.transformers_.get_tag("handles-missing-data", False)
+        missing = missing and self.transformers_.get_tag(
+            "capability:missing_values", False
+        )
         missing = missing or self.transformers_.get_tag(
             "capability:missing_values:removes", False
         )
@@ -465,7 +467,7 @@ class SklearnClustererPipeline(ClustererPipeline):
         # can handle missing values iff transformer chain removes missing data
         # sklearn clusterers might be able to handle missing data (but no tag there)
         # so better set the tag liberally
-        missing = self.transformers_.get_tag("handles-missing-data", False)
+        missing = self.transformers_.get_tag("capability:missing_values", False)
         missing = missing or self.transformers_.get_tag(
             "capability:missing_values:removes", False
         )
diff --git a/sktime/detection/compose/_as_transform.py b/sktime/detection/compose/_as_transform.py
index 9ca02bc2b..0612b6fe7 100644
--- a/sktime/detection/compose/_as_transform.py
+++ b/sktime/detection/compose/_as_transform.py
@@ -68,7 +68,7 @@ class DetectorAsTransformer(BaseTransformer):
         "skip-inverse-transform": True,  # is inverse-transform skipped when called?
         "capability:unequal_length": True,
         "capability:unequal_length:removes": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "capability:missing_values:removes": True,
     }
 
diff --git a/sktime/detection/stray.py b/sktime/detection/stray.py
index c1ee5753e..04b2011c5 100644
--- a/sktime/detection/stray.py
+++ b/sktime/detection/stray.py
@@ -82,7 +82,7 @@ class STRAY(BaseTransformer):
         "maintainers": "KatieBuc",
         # estimator type
         # --------------
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "X_inner_mtype": "np.ndarray",
         "fit_is_empty": False,
         "skip-inverse-transform": True,
diff --git a/sktime/dists_kernels/compose.py b/sktime/dists_kernels/compose.py
index daf605a66..ccb4b97ca 100644
--- a/sktime/dists_kernels/compose.py
+++ b/sktime/dists_kernels/compose.py
@@ -83,7 +83,9 @@ class PwTrafoPanelPipeline(_HeterogenousMetaEstimator, BasePairwiseTransformerPa
         # can handle missing values iff: both classifier and all transformers can,
         #   *or* transformer chain removes missing data
         missing = pw_trafo.get_tag("capability:missing_values", False)
-        missing = missing and self.transformers_.get_tag("handles-missing-data", False)
+        missing = missing and self.transformers_.get_tag(
+            "capability:missing_values", False
+        )
         missing = missing or self.transformers_.get_tag(
             "capability:missing_values:removes", False
         )
diff --git a/sktime/forecasting/adapters/_hcrystalball.py b/sktime/forecasting/adapters/_hcrystalball.py
index 255db18e3..74581aa18 100644
--- a/sktime/forecasting/adapters/_hcrystalball.py
+++ b/sktime/forecasting/adapters/_hcrystalball.py
@@ -114,7 +114,7 @@ class HCrystalBallAdapter(BaseForecaster):
         # --------------
         "ignores-exogeneous-X": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(self, model):
diff --git a/sktime/forecasting/arch/_uarch.py b/sktime/forecasting/arch/_uarch.py
index e2cf21a83..ed8909a23 100644
--- a/sktime/forecasting/arch/_uarch.py
+++ b/sktime/forecasting/arch/_uarch.py
@@ -163,7 +163,7 @@ class ARCH(BaseForecaster):
         "y_inner_mtype": "pd.Series",
         "X_inner_mtype": "pd.DataFrame",
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:pred_int": True,
         "ignores-exogeneous-X": True,
     }
diff --git a/sktime/forecasting/ardl.py b/sktime/forecasting/ardl.py
index ec71868e4..d80975b07 100644
--- a/sktime/forecasting/ardl.py
+++ b/sktime/forecasting/ardl.py
@@ -204,7 +204,7 @@ class ARDL(_StatsModelsAdapter):
         # --------------
         "scitype:y": "univariate",  # which y are fine? univariate/multivariate/both
         "ignores-exogeneous-X": False,  # does estimator ignore the exogeneous X?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": 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?
         "requires-fh-in-fit": False,  # is forecasting horizon already required in fit?
diff --git a/sktime/forecasting/arima/_pmdarima.py b/sktime/forecasting/arima/_pmdarima.py
index 4428e2662..1f1b62d4d 100644
--- a/sktime/forecasting/arima/_pmdarima.py
+++ b/sktime/forecasting/arima/_pmdarima.py
@@ -284,7 +284,7 @@ class AutoARIMA(_PmdArimaAdapter):
         # python_dependencies: "pmdarima" - inherited from _PmdArimaAdapter
         # estimator type
         # --------------
-        "handles-missing-data": True,
+        "capability:missing_values": True,
     }
 
     SARIMAX_KWARGS_KEYS = [
@@ -694,7 +694,7 @@ class ARIMA(_PmdArimaAdapter):
             "ilkersigirci",
         ],
         "maintainers": ["hyang1996"],
-        "handles-missing-data": True,
+        "capability:missing_values": True,
     }
 
     SARIMAX_KWARGS_KEYS = [
diff --git a/sktime/forecasting/base/_base.py b/sktime/forecasting/base/_base.py
index 4dd25ac36..387675e9e 100644
--- a/sktime/forecasting/base/_base.py
+++ b/sktime/forecasting/base/_base.py
@@ -99,7 +99,7 @@ class BaseForecaster(_PredictProbaMixin, BaseEstimator):
         "capability:insample": True,  # can the estimator make in-sample predictions?
         "capability:pred_int": False,  # can the estimator produce prediction intervals?
         "capability:pred_int:insample": True,  # if yes, also for in-sample horizons?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         "y_inner_mtype": "pd.Series",  # which types do _fit/_predict, support for y?
         "X_inner_mtype": "pd.DataFrame",  # which types do _fit/_predict, support for X?
         "requires-fh-in-fit": True,  # is forecasting horizon already required in fit?
@@ -1532,12 +1532,12 @@ class BaseForecaster(_PredictProbaMixin, BaseEstimator):
 
         def _check_missing(metadata, obj_name):
             """Check input metadata against self's missing capability tag."""
-            if not self.get_tag("handles-missing-data"):
+            if not self.get_tag("capability:missing_values"):
                 msg = (
                     f"{type(self).__name__} cannot handle missing data (nans), "
                     f"but {obj_name} passed contained missing data."
                 )
-                if self.get_class_tag("handles-missing-data"):
+                if self.get_class_tag("capability:missing_values"):
                     msg = msg + (
                         f" Whether instances of {type(self).__name__} can handle "
                         "missing data depends on parameters of the instance, "
@@ -1567,7 +1567,7 @@ class BaseForecaster(_PredictProbaMixin, BaseEstimator):
             y_metadata_required = ["n_features", "feature_names", "feature_kind"]
             if self.get_tag("scitype:y") != "both":
                 y_metadata_required += ["is_univariate"]
-            if not self.get_tag("handles-missing-data"):
+            if not self.get_tag("capability:missing_values"):
                 y_metadata_required += ["has_nans"]
 
             y_valid, y_msg, y_metadata = check_is_scitype(
@@ -1634,7 +1634,7 @@ class BaseForecaster(_PredictProbaMixin, BaseEstimator):
         if X is not None:
             # request only required metadata from checks
             X_metadata_required = ["feature_kind"]
-            if not self.get_tag("handles-missing-data"):
+            if not self.get_tag("capability:missing_values"):
                 X_metadata_required += ["has_nans"]
 
             X_valid, X_msg, X_metadata = check_is_scitype(
diff --git a/sktime/forecasting/base/_delegate.py b/sktime/forecasting/base/_delegate.py
index 333d9b65e..ee83865f5 100644
--- a/sktime/forecasting/base/_delegate.py
+++ b/sktime/forecasting/base/_delegate.py
@@ -67,12 +67,12 @@ class _DelegatedForecaster(BaseForecaster):
 
         TAGS_TO_DELEGATE = [
             "requires-fh-in-fit",
-            "handles-missing-data",
+            "capability:missing_values",
             "ignores-exogeneous-X",
             "capability:insample",
             "capability:pred_int",
             "capability:pred_int:insample",
-            "handles-missing-data",
+            "capability:missing_values",
             "requires-fh-in-fit",
             "X-y-must-have-same-index",
             "enforce_index_type",
diff --git a/sktime/forecasting/base/adapters/_darts.py b/sktime/forecasting/base/adapters/_darts.py
index 73c2153ac..54e4ba084 100644
--- a/sktime/forecasting/base/adapters/_darts.py
+++ b/sktime/forecasting/base/adapters/_darts.py
@@ -120,7 +120,7 @@ class _DartsRegressionAdapter(BaseForecaster):
         "y_inner_mtype": "pd.DataFrame",
         "X_inner_mtype": "pd.DataFrame",
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:insample": False,
         "capability:pred_int": False,
     }
@@ -470,7 +470,7 @@ class _DartsRegressionModelsAdapter(_DartsRegressionAdapter):
         "y_inner_mtype": "pd.DataFrame",
         "X_inner_mtype": "pd.DataFrame",
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:insample": False,
     }
 
diff --git a/sktime/forecasting/base/adapters/_fbprophet.py b/sktime/forecasting/base/adapters/_fbprophet.py
index ae6d7f7c5..1b7b1bb5a 100644
--- a/sktime/forecasting/base/adapters/_fbprophet.py
+++ b/sktime/forecasting/base/adapters/_fbprophet.py
@@ -22,7 +22,7 @@ class _ProphetAdapter(BaseForecaster):
         "capability:pred_int": True,
         "capability:pred_int:insample": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "y_inner_mtype": "pd.DataFrame",
         "python_dependencies": "prophet",
     }
diff --git a/sktime/forecasting/base/adapters/_generalised_statsforecast.py b/sktime/forecasting/base/adapters/_generalised_statsforecast.py
index bf175bc44..47e184c67 100644
--- a/sktime/forecasting/base/adapters/_generalised_statsforecast.py
+++ b/sktime/forecasting/base/adapters/_generalised_statsforecast.py
@@ -31,7 +31,7 @@ class _GeneralisedStatsForecastAdapter(BaseForecaster):
         "requires-fh-in-fit": False,
         # "X-y-must-have-same-index": True,  # TODO: need to check (how?)
         # "enforce_index_type": None,  # TODO: need to check (how?)
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(self):
diff --git a/sktime/forecasting/base/adapters/_neuralforecast.py b/sktime/forecasting/base/adapters/_neuralforecast.py
index 671807461..5d6ad272f 100644
--- a/sktime/forecasting/base/adapters/_neuralforecast.py
+++ b/sktime/forecasting/base/adapters/_neuralforecast.py
@@ -84,7 +84,7 @@ class _NeuralForecastAdapter(_BaseGlobalForecaster):
         "scitype:y": "univariate",
         "requires-fh-in-fit": True,
         "X-y-must-have-same-index": True,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:insample": False,
         "capability:global_forecasting": True,
     }
diff --git a/sktime/forecasting/base/adapters/_pmdarima.py b/sktime/forecasting/base/adapters/_pmdarima.py
index 664da8f62..b9cf2f6af 100644
--- a/sktime/forecasting/base/adapters/_pmdarima.py
+++ b/sktime/forecasting/base/adapters/_pmdarima.py
@@ -22,7 +22,7 @@ class _PmdArimaAdapter(BaseForecaster):
         "capability:pred_int": True,
         "capability:pred_int:insample": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         # TODO 0.38.0: check if numpy 2 incompatiblity can be removed
         "python_dependencies": ["pmdarima", "numpy<2"],
     }
diff --git a/sktime/forecasting/base/adapters/_pytorchforecasting.py b/sktime/forecasting/base/adapters/_pytorchforecasting.py
index 8e88c8349..66b9cb3e8 100644
--- a/sktime/forecasting/base/adapters/_pytorchforecasting.py
+++ b/sktime/forecasting/base/adapters/_pytorchforecasting.py
@@ -77,7 +77,7 @@ class _PytorchForecastingAdapter(_BaseGlobalForecaster):
         "scitype:y": "univariate",
         "requires-fh-in-fit": True,
         "X-y-must-have-same-index": True,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:insample": False,
         "capability:pred_int": False,
         "capability:pred_int:insample": False,
diff --git a/sktime/forecasting/base/adapters/_statsforecast.py b/sktime/forecasting/base/adapters/_statsforecast.py
index 04643e38d..0fbca15b0 100644
--- a/sktime/forecasting/base/adapters/_statsforecast.py
+++ b/sktime/forecasting/base/adapters/_statsforecast.py
@@ -19,7 +19,7 @@ class _StatsForecastAdapter(BaseForecaster):
         "maintainers": ["AzulGarza"],
         "scitype:y": "univariate",  # which y are fine? univariate/multivariate/both
         "ignores-exogeneous-X": False,  # does estimator ignore the exogeneous X?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": 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?
         "requires-fh-in-fit": False,  # is forecasting horizon already required in fit?
diff --git a/sktime/forecasting/base/adapters/_statsmodels.py b/sktime/forecasting/base/adapters/_statsmodels.py
index 8f3abefd3..d6d3e5283 100644
--- a/sktime/forecasting/base/adapters/_statsmodels.py
+++ b/sktime/forecasting/base/adapters/_statsmodels.py
@@ -28,7 +28,7 @@ class _StatsModelsAdapter(BaseForecaster):
         # --------------
         "ignores-exogeneous-X": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(self, random_state=None):
diff --git a/sktime/forecasting/base/adapters/_tbats.py b/sktime/forecasting/base/adapters/_tbats.py
index dc24cf0fa..bedbdbb56 100644
--- a/sktime/forecasting/base/adapters/_tbats.py
+++ b/sktime/forecasting/base/adapters/_tbats.py
@@ -23,7 +23,7 @@ class _TbatsAdapter(BaseForecaster):
         "capability:pred_int": True,
         "capability:pred_int:insample": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "python_dependencies": ["tbats", "numpy<2"],
     }
 
diff --git a/sktime/forecasting/chronos.py b/sktime/forecasting/chronos.py
index ac63a2010..e7985cd2b 100644
--- a/sktime/forecasting/chronos.py
+++ b/sktime/forecasting/chronos.py
@@ -299,7 +299,7 @@ class ChronosForecaster(_BaseGlobalForecaster):
         "requires-fh-in-fit": False,
         "X-y-must-have-same-index": True,
         "enforce_index_type": None,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:pred_int": False,
         "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
         "y_inner_mtype": [
diff --git a/sktime/forecasting/compose/_bagging.py b/sktime/forecasting/compose/_bagging.py
index 32c727f4d..e0dc87c57 100644
--- a/sktime/forecasting/compose/_bagging.py
+++ b/sktime/forecasting/compose/_bagging.py
@@ -85,7 +85,7 @@ class BaggingForecaster(BaseForecaster):
         "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?
+        "capability:missing_values": True,  # can estimator handle missing data?
         "y_inner_mtype": PANDAS_MTYPES,
         # which types do _fit, _predict, assume for y?
         "X_inner_mtype": PANDAS_MTYPES,
diff --git a/sktime/forecasting/compose/_column_ensemble.py b/sktime/forecasting/compose/_column_ensemble.py
index 4bcd94540..bebf7a549 100644
--- a/sktime/forecasting/compose/_column_ensemble.py
+++ b/sktime/forecasting/compose/_column_ensemble.py
@@ -95,7 +95,7 @@ class ColumnEnsembleForecaster(_HeterogenousEnsembleForecaster, _ColumnEstimator
         "y_inner_mtype": PANDAS_MTYPES,
         "X_inner_mtype": PANDAS_MTYPES,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:pred_int": True,
         "capability:pred_int:insample": True,
     }
@@ -122,7 +122,7 @@ class ColumnEnsembleForecaster(_HeterogenousEnsembleForecaster, _ColumnEstimator
                 "requires-fh-in-fit",
                 "capability:pred_int",
                 "ignores-exogeneous-X",
-                "handles-missing-data",
+                "capability:missing_values",
             ]
             self.clone_tags(forecasters, tags_to_clone)
         else:
@@ -130,7 +130,9 @@ class ColumnEnsembleForecaster(_HeterogenousEnsembleForecaster, _ColumnEstimator
             self._anytagis_then_set("requires-fh-in-fit", True, False, l_forecasters)
             self._anytagis_then_set("capability:pred_int", False, True, l_forecasters)
             self._anytagis_then_set("ignores-exogeneous-X", False, True, l_forecasters)
-            self._anytagis_then_set("handles-missing-data", False, True, l_forecasters)
+            self._anytagis_then_set(
+                "capability:missing_values", False, True, l_forecasters
+            )
 
     @property
     def _forecasters(self):
diff --git a/sktime/forecasting/compose/_ensemble.py b/sktime/forecasting/compose/_ensemble.py
index 7b999f59b..7fd65fbcd 100644
--- a/sktime/forecasting/compose/_ensemble.py
+++ b/sktime/forecasting/compose/_ensemble.py
@@ -109,7 +109,7 @@ class AutoEnsembleForecaster(_HeterogenousEnsembleForecaster):
         "authors": ["mloning", "GuzalBulatova", "aiwalter", "RNKuhns", "AnH0ang"],
         "ignores-exogeneous-X": False,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "scitype:y": "univariate",
     }
 
@@ -322,7 +322,7 @@ class EnsembleForecaster(_HeterogenousEnsembleForecaster):
         "authors": ["mloning", "GuzalBulatova", "aiwalter", "RNKuhns", "AnH0ang"],
         "ignores-exogeneous-X": False,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
         "y_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
         "scitype:y": "both",
diff --git a/sktime/forecasting/compose/_fallback.py b/sktime/forecasting/compose/_fallback.py
index ccb87e066..8127e3001 100644
--- a/sktime/forecasting/compose/_fallback.py
+++ b/sktime/forecasting/compose/_fallback.py
@@ -110,7 +110,7 @@ class FallbackForecaster(_HeterogenousMetaEstimator, _DelegatedForecaster):
     _tags = {
         "authors": ["ninedigits", "RikStarmans"],
         "maintainers": ["ninedigits"],
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "scitype:y": "both",
         "y_inner_mtype": ALL_TIME_SERIES_MTYPES,
         "X_inner_mtype": ALL_TIME_SERIES_MTYPES,
diff --git a/sktime/forecasting/compose/_fhplex.py b/sktime/forecasting/compose/_fhplex.py
index ba660a80c..082e486ca 100644
--- a/sktime/forecasting/compose/_fhplex.py
+++ b/sktime/forecasting/compose/_fhplex.py
@@ -85,7 +85,7 @@ class FhPlexForecaster(BaseForecaster):
     _tags = {
         "authors": "fkiraly",
         "requires-fh-in-fit": True,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "scitype:y": "both",
         "y_inner_mtype": PANDAS_TS_MTYPES,
         "X_inner_mtype": PANDAS_TS_MTYPES,
diff --git a/sktime/forecasting/compose/_grouped.py b/sktime/forecasting/compose/_grouped.py
index 4b3f2a8a6..fed6eacbf 100644
--- a/sktime/forecasting/compose/_grouped.py
+++ b/sktime/forecasting/compose/_grouped.py
@@ -64,7 +64,7 @@ class ForecastByLevel(_DelegatedForecaster):
     _tags = {
         "authors": ["fkiraly"],
         "requires-fh-in-fit": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "scitype:y": "both",
         "y_inner_mtype": ALL_TIME_SERIES_MTYPES,
         "X_inner_mtype": ALL_TIME_SERIES_MTYPES,
@@ -270,7 +270,7 @@ class GroupbyCategoryForecaster(BaseForecaster, _HeterogenousMetaEstimator):
             "ignores-exogeneous-X": True,
             "X-y-must-have-same-index": True,
             "enforce_index_type": True,
-            "handles-missing-data": True,
+            "capability:missing_values": True,
             "capability:insample": True,
             "capability:pred_int": True,
             "capability:pred_int:insample": True,
diff --git a/sktime/forecasting/compose/_hierarchy_ensemble.py b/sktime/forecasting/compose/_hierarchy_ensemble.py
index a0efda3f6..77d0cf34e 100644
--- a/sktime/forecasting/compose/_hierarchy_ensemble.py
+++ b/sktime/forecasting/compose/_hierarchy_ensemble.py
@@ -147,7 +147,7 @@ class HierarchyEnsembleForecaster(_HeterogenousEnsembleForecaster):
         "y_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
         "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     BY_LIST = ["level", "node"]
@@ -169,14 +169,16 @@ class HierarchyEnsembleForecaster(_HeterogenousEnsembleForecaster):
             tags_to_clone = [
                 "requires-fh-in-fit",
                 "ignores-exogeneous-X",
-                "handles-missing-data",
+                "capability:missing_values",
             ]
             self.clone_tags(forecasters, tags_to_clone)
         else:
             l_forecasters = [(x[0], x[1]) for x in forecasters]
             self._anytagis_then_set("requires-fh-in-fit", True, False, l_forecasters)
             self._anytagis_then_set("ignores-exogeneous-X", False, True, l_forecasters)
-            self._anytagis_then_set("handles-missing-data", False, True, l_forecasters)
+            self._anytagis_then_set(
+                "capability:missing_values", False, True, l_forecasters
+            )
 
     @property
     def _forecasters(self):
diff --git a/sktime/forecasting/compose/_multiplexer.py b/sktime/forecasting/compose/_multiplexer.py
index 41f02cd73..406b17a3d 100644
--- a/sktime/forecasting/compose/_multiplexer.py
+++ b/sktime/forecasting/compose/_multiplexer.py
@@ -80,7 +80,7 @@ class MultiplexForecaster(_HeterogenousMetaEstimator, _DelegatedForecaster):
     _tags = {
         "authors": ["kkoralturk", "aiwalter", "fkiraly", "miraep8"],
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "scitype:y": "both",
         "y_inner_mtype": ALL_TIME_SERIES_MTYPES,
         "X_inner_mtype": ALL_TIME_SERIES_MTYPES,
diff --git a/sktime/forecasting/compose/_pipeline.py b/sktime/forecasting/compose/_pipeline.py
index bef480ea5..f92816832 100644
--- a/sktime/forecasting/compose/_pipeline.py
+++ b/sktime/forecasting/compose/_pipeline.py
@@ -423,7 +423,7 @@ class ForecastingPipeline(_Pipeline):
         "X_inner_mtype": SUPPORTED_MTYPES,
         "ignores-exogeneous-X": False,
         "requires-fh-in-fit": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "capability:pred_int": True,
         "X-y-must-have-same-index": False,
         "capability:categorical_in_X": True,
@@ -872,7 +872,7 @@ class TransformedTargetForecaster(_Pipeline):
         "X_inner_mtype": SUPPORTED_MTYPES,
         "ignores-exogeneous-X": False,
         "requires-fh-in-fit": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "capability:pred_int": True,
         "X-y-must-have-same-index": False,
     }
@@ -1372,7 +1372,7 @@ class ForecastX(BaseForecaster):
         "ignores-exogeneous-X": False,
         "capability:pred_int": True,
         "capability:pred_int:insample": True,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
     }
 
     def __init__(
@@ -1432,7 +1432,7 @@ class ForecastX(BaseForecaster):
         self.clone_tags(forecaster_y, tags_to_clone_from_forecaster_y)
 
         # tag_translate_dict = {
-        #    "handles-missing-data": forecaster.get_tag("handles-missing-data")
+        #   "capability:missing_values": forecaster.get_tag("capability:missing_values")
         # }
         # self.set_tags(**tag_translate_dict)
 
@@ -1928,7 +1928,7 @@ class Permute(_DelegatedForecaster, BaseForecaster, _HeterogenousMetaEstimator):
         "X_inner_mtype": ALL_TIME_SERIES_MTYPES,
         "ignores-exogeneous-X": False,
         "requires-fh-in-fit": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "capability:pred_int": True,
         "capability:pred_int:insample": True,
         "X-y-must-have-same-index": False,
diff --git a/sktime/forecasting/compose/_reduce.py b/sktime/forecasting/compose/_reduce.py
index 845343d50..99608c27c 100644
--- a/sktime/forecasting/compose/_reduce.py
+++ b/sktime/forecasting/compose/_reduce.py
@@ -231,7 +231,7 @@ class _Reducer(_BaseWindowForecaster):
             "benheid",
         ],
         "ignores-exogeneous-X": False,  # reduction uses X in non-trivial way
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "capability:insample": False,
         "capability:pred_int": True,
         "capability:pred_int:insample": False,
@@ -254,7 +254,9 @@ class _Reducer(_BaseWindowForecaster):
         # it seems that the sklearn tags are not fully reliable
         # see discussion in PR #3405 and issue #3402
         # therefore this is commented out until sktime and sklearn are better aligned
-        # self.set_tags(**{"handles-missing-data": estimator._get_tags()["allow_nan"]})
+        # self.set_tags(
+        #     **{"capability:missing_values": estimator._get_tags()["allow_nan"]}
+        # )
 
         # for dealing with probabilistic regressors:
         # self._est_type encodes information what type of estimator is passed
@@ -1992,7 +1994,9 @@ class DirectReductionForecaster(BaseForecaster, _ReducerMixin):
         # it seems that the sklearn tags are not fully reliable
         # see discussion in PR #3405 and issue #3402
         # therefore this is commented out until sktime and sklearn are better aligned
-        # self.set_tags(**{"handles-missing-data": estimator._get_tags()["allow_nan"]})
+        # self.set_tags(
+        #     **{"capability:missing_values": estimator._get_tags()["allow_nan"]}
+        # )
 
     def _fit(self, y, X, fh):
         """Fit dispatcher based on X_treatment and windows_identical."""
@@ -2762,7 +2766,7 @@ class YfromX(BaseForecaster, _ReducerMixin):
     _tags = {
         "requires-fh-in-fit": False,  # is the forecasting horizon required in fit?
         "ignores-exogeneous-X": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
         "y_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
         "capability:pred_int": True,
diff --git a/sktime/forecasting/compose/_skforecast_reduce.py b/sktime/forecasting/compose/_skforecast_reduce.py
index 46b5f4140..0de7831de 100644
--- a/sktime/forecasting/compose/_skforecast_reduce.py
+++ b/sktime/forecasting/compose/_skforecast_reduce.py
@@ -126,7 +126,7 @@ class SkforecastAutoreg(BaseForecaster):
         "y_inner_mtype": "pd.Series",
         "X_inner_mtype": "pd.DataFrame",
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:insample": False,
         "capability:pred_int": True,
         "capability:pred_int:insample": False,
@@ -590,7 +590,7 @@ class SkforecastRecursive(BaseForecaster):
         "y_inner_mtype": "pd.Series",
         "X_inner_mtype": "pd.DataFrame",
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:insample": False,
         "capability:pred_int": True,
         "capability:pred_int:insample": False,
diff --git a/sktime/forecasting/compose/_stack.py b/sktime/forecasting/compose/_stack.py
index e5cc68a35..6156460e6 100644
--- a/sktime/forecasting/compose/_stack.py
+++ b/sktime/forecasting/compose/_stack.py
@@ -63,7 +63,7 @@ class StackingForecaster(_HeterogenousEnsembleForecaster):
         "authors": ["mloning", "fkiraly", "indinewton"],
         "ignores-exogeneous-X": False,
         "requires-fh-in-fit": True,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "scitype:y": "univariate",
         "X-y-must-have-same-index": True,
     }
@@ -75,7 +75,7 @@ class StackingForecaster(_HeterogenousEnsembleForecaster):
         super().__init__(forecasters=forecasters, n_jobs=n_jobs)
 
         self._anytagis_then_set("ignores-exogeneous-X", False, True, forecasters)
-        self._anytagis_then_set("handles-missing-data", False, True, forecasters)
+        self._anytagis_then_set("capability:missing_values", False, True, forecasters)
         self._anytagis_then_set("fit_is_empty", False, True, forecasters)
 
     def _fit(self, y, X, fh):
diff --git a/sktime/forecasting/compose/_transform_select_forecaster.py b/sktime/forecasting/compose/_transform_select_forecaster.py
index fa3d3629b..e627448bb 100644
--- a/sktime/forecasting/compose/_transform_select_forecaster.py
+++ b/sktime/forecasting/compose/_transform_select_forecaster.py
@@ -144,7 +144,7 @@ class TransformSelectForecaster(BaseForecaster, _HeterogenousMetaEstimator):
             "ignores-exogeneous-X": True,
             "X-y-must-have-same-index": True,
             "enforce_index_type": True,
-            "handles-missing-data": True,
+            "capability:missing_values": True,
             "capability:insample": True,
             "capability:pred_int": True,
             "capability:pred_int:insample": True,
diff --git a/sktime/forecasting/conditional_invertible_neural_network.py b/sktime/forecasting/conditional_invertible_neural_network.py
index 6b5157301..115ac1b6d 100644
--- a/sktime/forecasting/conditional_invertible_neural_network.py
+++ b/sktime/forecasting/conditional_invertible_neural_network.py
@@ -128,7 +128,7 @@ class CINNForecaster(BaseDeepNetworkPyTorch):
         "requires-fh-in-fit": False,
         "X-y-must-have-same-index": True,
         "enforce_index_type": None,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:pred_int": False,
     }
 
diff --git a/sktime/forecasting/conformal.py b/sktime/forecasting/conformal.py
index 5d21e5306..51c600b52 100644
--- a/sktime/forecasting/conformal.py
+++ b/sktime/forecasting/conformal.py
@@ -127,7 +127,7 @@ class ConformalIntervals(BaseForecaster):
         # --------------
         "scitype:y": "univariate",
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "ignores-exogeneous-X": False,
         "capability:pred_int": True,
         "capability:pred_int:insample": False,
@@ -172,7 +172,7 @@ class ConformalIntervals(BaseForecaster):
         tags_to_clone = [
             "requires-fh-in-fit",
             "ignores-exogeneous-X",
-            "handles-missing-data",
+            "capability:missing_values",
             "X-y-must-have-same-index",
             "enforce_index_type",
         ]
diff --git a/sktime/forecasting/dynamic_factor.py b/sktime/forecasting/dynamic_factor.py
index 2d15c8892..559ec8dd2 100644
--- a/sktime/forecasting/dynamic_factor.py
+++ b/sktime/forecasting/dynamic_factor.py
@@ -132,7 +132,7 @@ class DynamicFactor(_StatsModelsAdapter):
         # --------------
         "scitype:y": "multivariate",
         "ignores-exogeneous-X": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "y_inner_mtype": "pd.DataFrame",
         "X_inner_mtype": "pd.DataFrame",
         "requires-fh-in-fit": False,
diff --git a/sktime/forecasting/enbpi.py b/sktime/forecasting/enbpi.py
index 35cc09624..194ac7b97 100644
--- a/sktime/forecasting/enbpi.py
+++ b/sktime/forecasting/enbpi.py
@@ -92,7 +92,7 @@ class EnbPIForecaster(BaseForecaster):
         "python_dependencies": ["tsbootstrap>=0.1.0"],
         "scitype:y": "univariate",  # which y are fine? univariate/multivariate/both
         "ignores-exogeneous-X": False,  # does estimator ignore the exogeneous X?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         "y_inner_mtype": "pd.DataFrame",
         # which types do _fit, _predict, assume for y?
         "X_inner_mtype": "pd.DataFrame",
diff --git a/sktime/forecasting/ets.py b/sktime/forecasting/ets.py
index dfa23bcaf..8e8d7b05c 100644
--- a/sktime/forecasting/ets.py
+++ b/sktime/forecasting/ets.py
@@ -186,7 +186,7 @@ class AutoETS(_StatsModelsAdapter):
         "capability:pred_int": True,
         "capability:pred_int:insample": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
     }
 
     def __init__(
diff --git a/sktime/forecasting/hf_transformers_forecaster.py b/sktime/forecasting/hf_transformers_forecaster.py
index feeed2d42..c7a9b64cc 100644
--- a/sktime/forecasting/hf_transformers_forecaster.py
+++ b/sktime/forecasting/hf_transformers_forecaster.py
@@ -180,7 +180,7 @@ class HFTransformersForecaster(BaseForecaster):
         "requires-fh-in-fit": False,
         "X-y-must-have-same-index": True,
         "enforce_index_type": None,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:pred_int": False,
         "python_dependencies": ["transformers", "torch"],
         "X_inner_mtype": "pd.DataFrame",
diff --git a/sktime/forecasting/model_selection/_tune.py b/sktime/forecasting/model_selection/_tune.py
index f9133ff8f..657d7b5b9 100644
--- a/sktime/forecasting/model_selection/_tune.py
+++ b/sktime/forecasting/model_selection/_tune.py
@@ -32,7 +32,7 @@ class BaseGridSearch(_DelegatedForecaster):
         "authors": ["mloning", "fkiraly", "aiwalter"],
         "scitype:y": "both",
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "ignores-exogeneous-X": True,
         "capability:pred_int": True,
         "capability:pred_int:insample": True,
@@ -1177,7 +1177,7 @@ class ForecastingSkoptSearchCV(BaseGridSearch):
         "maintainers": ["HazrulAkmal"],
         "scitype:y": "both",
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "ignores-exogeneous-X": True,
         "capability:pred_int": True,
         "capability:pred_int:insample": True,
@@ -1760,7 +1760,7 @@ class ForecastingOptunaSearchCV(BaseGridSearch):
         "maintainers": ["gareth-brown-86", "mk406"],
         "scitype:y": "both",
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "ignores-exogeneous-X": True,
         "capability:pred_int": True,
         "capability:pred_int:insample": True,
diff --git a/sktime/forecasting/moirai_forecaster.py b/sktime/forecasting/moirai_forecaster.py
index ba9feac60..a71bd2c5f 100644
--- a/sktime/forecasting/moirai_forecaster.py
+++ b/sktime/forecasting/moirai_forecaster.py
@@ -86,7 +86,7 @@ class MOIRAIForecaster(_BaseGlobalForecaster):
         "requires-fh-in-fit": False,
         "X-y-must-have-same-index": True,
         "enforce_index_type": None,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:pred_int": False,
         "python_dependencies": [
             "gluonts",
diff --git a/sktime/forecasting/naive/_naive.py b/sktime/forecasting/naive/_naive.py
index b5fe6c641..d18be1fd3 100644
--- a/sktime/forecasting/naive/_naive.py
+++ b/sktime/forecasting/naive/_naive.py
@@ -124,7 +124,7 @@ class NaiveForecaster(_BaseWindowForecaster):
         # --------------
         "y_inner_mtype": "pd.Series",
         "requires-fh-in-fit": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "ignores-exogeneous-X": True,
         "scitype:y": "univariate",
         "capability:pred_var": True,
@@ -140,7 +140,7 @@ class NaiveForecaster(_BaseWindowForecaster):
         # Override tag for handling missing data
         # todo: remove if GH1367 is fixed
         if self.strategy in ("last", "mean"):
-            self.set_tags(**{"handles-missing-data": True})
+            self.set_tags(**{"capability:missing_values": True})
 
     def _fit(self, y, X, fh):
         """Fit to training data.
@@ -677,7 +677,7 @@ class NaiveVariance(BaseForecaster):
         # --------------
         "scitype:y": "univariate",
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "ignores-exogeneous-X": False,
         "capability:pred_int": True,
         "capability:pred_var": True,
@@ -692,7 +692,7 @@ class NaiveVariance(BaseForecaster):
         tags_to_clone = [
             "requires-fh-in-fit",
             "ignores-exogeneous-X",
-            "handles-missing-data",
+            "capability:missing_values",
             "y_inner_mtype",
             "X_inner_mtype",
             "X-y-must-have-same-index",
diff --git a/sktime/forecasting/online_learning/_online_ensemble.py b/sktime/forecasting/online_learning/_online_ensemble.py
index 9e8721f3e..2c75f2214 100644
--- a/sktime/forecasting/online_learning/_online_ensemble.py
+++ b/sktime/forecasting/online_learning/_online_ensemble.py
@@ -39,7 +39,7 @@ class OnlineEnsembleForecaster(EnsembleForecaster):
         # --------------
         "ignores-exogeneous-X": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "y_inner_mtype": ["pd.Series"],
         "scitype:y": "univariate",
     }
diff --git a/sktime/forecasting/online_learning/_prediction_weighted_ensembler.py b/sktime/forecasting/online_learning/_prediction_weighted_ensembler.py
index b3ab085e4..408c03590 100644
--- a/sktime/forecasting/online_learning/_prediction_weighted_ensembler.py
+++ b/sktime/forecasting/online_learning/_prediction_weighted_ensembler.py
@@ -29,7 +29,7 @@ class _PredictionWeightedEnsembler:
         # --------------
         "ignores-exogeneous-X": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(self, n_estimators=10, loss_func=None):
@@ -114,7 +114,7 @@ class HedgeExpertEnsemble(_PredictionWeightedEnsembler):
     _tags = {
         "ignores-exogeneous-X": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(self, n_estimators=10, T=10, a=1, loss_func=None):
@@ -147,7 +147,7 @@ class NormalHedgeEnsemble(HedgeExpertEnsemble):
     _tags = {
         "ignores-exogeneous-X": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(self, n_estimators=10, a=1, loss_func=None):
@@ -259,7 +259,7 @@ class NNLSEnsemble(_PredictionWeightedEnsembler):
     _tags = {
         "ignores-exogeneous-X": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(self, n_estimators=10, loss_func=None):
diff --git a/sktime/forecasting/prophetverse.py b/sktime/forecasting/prophetverse.py
index eed16324d..b87765a70 100644
--- a/sktime/forecasting/prophetverse.py
+++ b/sktime/forecasting/prophetverse.py
@@ -327,7 +327,7 @@ class HierarchicalProphet(_DelegatedForecaster):
         # estimator type
         "scitype:y": "univariate",
         "ignores-exogeneous-X": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "y_inner_mtype": [
             "pd.DataFrame",
             "pd-multiindex",
diff --git a/sktime/forecasting/pykan_forecaster.py b/sktime/forecasting/pykan_forecaster.py
index 98de05c36..fe2717e3f 100644
--- a/sktime/forecasting/pykan_forecaster.py
+++ b/sktime/forecasting/pykan_forecaster.py
@@ -69,7 +69,7 @@ class PyKANForecaster(BaseForecaster):
         "requires-fh-in-fit": True,
         "X-y-must-have-same-index": True,
         "enforce_index_type": None,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:pred_int": False,
         "capability:pred_int:insample": False,
         "capability:insample": False,
diff --git a/sktime/forecasting/rbf_forecaster.py b/sktime/forecasting/rbf_forecaster.py
index fa3f2e40e..5fe427df7 100644
--- a/sktime/forecasting/rbf_forecaster.py
+++ b/sktime/forecasting/rbf_forecaster.py
@@ -96,7 +96,7 @@ class RBFForecaster(BaseDeepNetworkPyTorch):
         "y_inner_mtype": "pd.Series",
         "ignores-exogeneous-X": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:pred_int": False,
     }
 
diff --git a/sktime/forecasting/reconcile.py b/sktime/forecasting/reconcile.py
index 050ecc397..ee2695f8c 100644
--- a/sktime/forecasting/reconcile.py
+++ b/sktime/forecasting/reconcile.py
@@ -97,7 +97,7 @@ class ReconcilerForecaster(BaseForecaster):
         # --------------
         "scitype:y": "univariate",  # which y are fine? univariate/multivariate/both
         "ignores-exogeneous-X": False,  # does estimator ignore the exogeneous X?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         "y_inner_mtype": [
             "pd.DataFrame",
             "pd.Series",
diff --git a/sktime/forecasting/squaring_residuals.py b/sktime/forecasting/squaring_residuals.py
index affc8f017..bbbd0e193 100644
--- a/sktime/forecasting/squaring_residuals.py
+++ b/sktime/forecasting/squaring_residuals.py
@@ -90,7 +90,7 @@ class SquaringResiduals(BaseForecaster):
         # --------------
         "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?
+        "capability:missing_values": 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?
         "requires-fh-in-fit": True,  # is forecasting horizon already required in fit?
diff --git a/sktime/forecasting/structural.py b/sktime/forecasting/structural.py
index a430680f6..96e6bdb09 100644
--- a/sktime/forecasting/structural.py
+++ b/sktime/forecasting/structural.py
@@ -211,7 +211,7 @@ class UnobservedComponents(_StatsModelsAdapter):
         # --------------
         "capability:pred_int": True,
         "capability:pred_int:insample": True,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "ignores-exogeneous-X": False,
     }
 
diff --git a/sktime/forecasting/theta.py b/sktime/forecasting/theta.py
index 530d1e0cb..9f7925d07 100644
--- a/sktime/forecasting/theta.py
+++ b/sktime/forecasting/theta.py
@@ -100,7 +100,7 @@ class ThetaForecaster(ExponentialSmoothing):
         "capability:pred_int": True,
         "capability:pred_int:insample": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(self, initial_level=None, deseasonalize=True, sp=1):
@@ -420,10 +420,10 @@ class ThetaModularForecaster(BaseForecaster):
 
     _tags = {
         "authors": ["GuzalBulatova", "fkiraly"],
-        "univariate-only": False,
+        "scitype:y": "univariate",
         "y_inner_mtype": "pd.Series",
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "python_version": ">3.7",
     }
 
diff --git a/sktime/forecasting/timemoe.py b/sktime/forecasting/timemoe.py
index e25204bc2..c88e475e1 100644
--- a/sktime/forecasting/timemoe.py
+++ b/sktime/forecasting/timemoe.py
@@ -115,7 +115,7 @@ class TimeMoEForecaster(_BaseGlobalForecaster):
         "requires-fh-in-fit": False,
         "X-y-must-have-same-index": True,
         "enforce_index_type": None,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:pred_int": False,
         "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
         "y_inner_mtype": [
diff --git a/sktime/forecasting/timesfm_forecaster.py b/sktime/forecasting/timesfm_forecaster.py
index 05e644d8c..1ee6fe172 100644
--- a/sktime/forecasting/timesfm_forecaster.py
+++ b/sktime/forecasting/timesfm_forecaster.py
@@ -177,7 +177,7 @@ class TimesFMForecaster(_BaseGlobalForecaster):
         "requires-fh-in-fit": False,
         "X-y-must-have-same-index": True,
         "enforce_index_type": None,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:insample": False,
         "capability:pred_int": False,
         "capability:pred_int:insample": False,
diff --git a/sktime/forecasting/trend/_polynomial_trend_forecaster.py b/sktime/forecasting/trend/_polynomial_trend_forecaster.py
index b00b7a4f8..658156447 100644
--- a/sktime/forecasting/trend/_polynomial_trend_forecaster.py
+++ b/sktime/forecasting/trend/_polynomial_trend_forecaster.py
@@ -94,7 +94,7 @@ class PolynomialTrendForecaster(BaseForecaster):
         "maintainers": ["tensorflow-as-tf"],
         "ignores-exogeneous-X": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:pred_int": True,
     }
 
diff --git a/sktime/forecasting/trend/_spline_trend_forecaster.py b/sktime/forecasting/trend/_spline_trend_forecaster.py
index cfc9fb78d..55d10fa15 100644
--- a/sktime/forecasting/trend/_spline_trend_forecaster.py
+++ b/sktime/forecasting/trend/_spline_trend_forecaster.py
@@ -90,7 +90,7 @@ class SplineTrendForecaster(_DelegatedForecaster):
         "maintainers": ["jgyasu"],
         "ignores-exogeneous-X": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:pred_int": False,
     }
 
diff --git a/sktime/forecasting/trend/_stl_forecaster.py b/sktime/forecasting/trend/_stl_forecaster.py
index e383cbfd9..a72af6521 100644
--- a/sktime/forecasting/trend/_stl_forecaster.py
+++ b/sktime/forecasting/trend/_stl_forecaster.py
@@ -143,7 +143,7 @@ class STLForecaster(BaseForecaster):
         "maintainers": ["tensorflow-as-tf"],
         "scitype:y": "univariate",  # which y are fine? univariate/multivariate/both
         "ignores-exogeneous-X": False,  # does estimator ignore the exogeneous X?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": 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?
         "requires-fh-in-fit": False,  # is forecasting horizon already required in fit?
diff --git a/sktime/forecasting/trend/_trend_forecaster.py b/sktime/forecasting/trend/_trend_forecaster.py
index 2a9ccdb22..c22199d24 100644
--- a/sktime/forecasting/trend/_trend_forecaster.py
+++ b/sktime/forecasting/trend/_trend_forecaster.py
@@ -63,7 +63,7 @@ class TrendForecaster(BaseForecaster):
         "maintainers": ["tensorflow-as-tf"],
         "ignores-exogeneous-X": True,
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(self, regressor=None):
diff --git a/sktime/forecasting/ttm.py b/sktime/forecasting/ttm.py
index 234a2eb14..294518fa2 100644
--- a/sktime/forecasting/ttm.py
+++ b/sktime/forecasting/ttm.py
@@ -225,7 +225,7 @@ class TinyTimeMixerForecaster(_BaseGlobalForecaster):
         "requires-fh-in-fit": True,
         "X-y-must-have-same-index": True,
         "enforce_index_type": None,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:insample": False,
         "capability:pred_int": False,
         "capability:pred_int:insample": False,
diff --git a/sktime/forecasting/var.py b/sktime/forecasting/var.py
index a50d5fd0a..8e08b2118 100644
--- a/sktime/forecasting/var.py
+++ b/sktime/forecasting/var.py
@@ -93,7 +93,6 @@ class VAR(_StatsModelsAdapter):
         "scitype:y": "multivariate",
         "y_inner_mtype": "pd.DataFrame",
         "requires-fh-in-fit": False,
-        "univariate-only": False,
         "ignores-exogeneous-X": True,
         "capability:pred_int": True,
         "capability:pred_int:insample": False,
diff --git a/sktime/forecasting/varmax.py b/sktime/forecasting/varmax.py
index 9863e1d94..683d8fa59 100644
--- a/sktime/forecasting/varmax.py
+++ b/sktime/forecasting/varmax.py
@@ -218,7 +218,7 @@ class VARMAX(_StatsModelsAdapter):
         # --------------
         "scitype:y": "multivariate",
         "ignores-exogeneous-X": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "y_inner_mtype": "pd.DataFrame",
         "X_inner_mtype": "pd.DataFrame",
         "requires-fh-in-fit": False,
diff --git a/sktime/forecasting/vecm.py b/sktime/forecasting/vecm.py
index 21e54f20a..a3c593f01 100644
--- a/sktime/forecasting/vecm.py
+++ b/sktime/forecasting/vecm.py
@@ -64,16 +64,16 @@ class VECM(_StatsModelsAdapter):
 
     Examples
     --------
-    >>> import numpy as np
-    >>> import pandas as pd
-    >>> from sktime.forecasting.vecm import VECM
-    >>> from sktime.split import temporal_train_test_split
-    >>> from sktime.forecasting.base import ForecastingHorizon
-    >>> index = pd.date_range(start="2005", end="2006-12", freq="ME")
+    >>> import numpy as np  # doctest: +SKIP
+    >>> import pandas as pd  # doctest: +SKIP
+    >>> from sktime.forecasting.vecm import VECM  # doctest: +SKIP
+    >>> from sktime.split import temporal_train_test_split  # doctest: +SKIP
+    >>> from sktime.forecasting.base import ForecastingHorizon  # doctest: +SKIP
+    >>> index = pd.date_range(start="2005", end="2006-12", freq="ME")  # doctest: +SKIP
     >>> df = pd.DataFrame(np.random.randint(0, 100, size=(23, 2)),
-    ... columns=list("AB"),
-    ... index=pd.PeriodIndex(index))
-    >>> train, test = temporal_train_test_split(df)
+    ... columns=list("AB"),  # doctest: +SKIP
+    ... index=pd.PeriodIndex(index))  # doctest: +SKIP
+    >>> train, test = temporal_train_test_split(df)  # doctest: +SKIP
     >>> sktime_model = VECM()  # doctest: +SKIP
     >>> fh = ForecastingHorizon([1, 3, 4, 5, 7, 9])
     >>> _ = sktime_model.fit(train, fh=fh)  # doctest: +SKIP
@@ -98,7 +98,6 @@ class VECM(_StatsModelsAdapter):
         "y_inner_mtype": "pd.DataFrame",
         "X_inner_mtype": "pd.DataFrame",
         "requires-fh-in-fit": False,
-        "univariate-only": False,
         "ignores-exogeneous-X": False,
         "capability:pred_int": True,
         "capability:pred_int:insample": False,
diff --git a/sktime/libs/README.md b/sktime/libs/README.md
index 79c425ede..890dd6abe 100644
--- a/sktime/libs/README.md
+++ b/sktime/libs/README.md
@@ -45,3 +45,5 @@ in folders starting with underscore. These should not be accessed by users of `s
 * `_aws_fortuna-enbpi` - Parts of the `EnbPI` class from aws-fortuna.
   The installation of the original package is not working due to dependency
   mismatches.
+
+* `_keras_self_attention` - fork of some layers from the [abandoned package `keras-self-attention`](https://github.com/CyberZHG/keras-self-attention), archived in March 2024.
diff --git a/sktime/libs/_keras_self_attention/__init__.py b/sktime/libs/_keras_self_attention/__init__.py
new file mode 100644
index 000000000..43edfa4f1
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/__init__.py
@@ -0,0 +1,51 @@
+"""Python implementations of Kalman Filters and Kalman Smoothers.
+
+Unofficial fork of the ``pykalman`` package, maintained in ``sktime``.
+
+sktime migration: 2025, April
+Version 0.51.0 release: 2022, Jan 22 (CyberZHG)
+
+Original authors: CyberZHG
+
+2024 release subject to following license:
+
+Copyright (c) 2018 PoW
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+"""
+
+__authors__ = ["CyberZHG", "EtcEc"]
+
+from sktime.libs._keras_self_attention.real_former import (
+    ResidualScaledDotProductAttention,
+)
+from sktime.libs._keras_self_attention.scaled_dot_attention import (
+    ScaledDotProductAttention,
+)
+from sktime.libs._keras_self_attention.seq_self_attention import SeqSelfAttention
+from sktime.libs._keras_self_attention.seq_weighted_attention import (
+    SeqWeightedAttention,
+)
+
+__all__ = [
+    "SeqSelfAttention",
+    "SeqWeightedAttention",
+    "ScaledDotProductAttention",
+    "ResidualScaledDotProductAttention",
+]
diff --git a/sktime/libs/_keras_self_attention/real_former.py b/sktime/libs/_keras_self_attention/real_former.py
new file mode 100644
index 000000000..7ceffe524
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/real_former.py
@@ -0,0 +1,76 @@
+"""RealFormer layers."""
+
+from sktime.utils.dependencies import _safe_import
+
+keras = _safe_import("tensorflow.keras")
+K = _safe_import("tensorflow.keras.backend")
+
+
+class ResidualScaledDotProductAttention(keras.layers.Layer):
+    r"""Residual scaled dot product attention layer.
+
+    The attention layer that takes three inputs representing queries, keys and values.
+
+    \text{Attention}(Q, K, V, Prev) = \text{softmax}(\frac{Q K^T}{\sqrt{d_k}} + Prev) V
+
+    See: https://arxiv.org/pdf/1706.03762.pdf
+    """
+
+    def __init__(self, return_attention=False, history_only=False, **kwargs):
+        """Initialize the layer.
+
+        :param return_attention: Whether to return attention weights.
+        :param history_only: Whether to only use history data.
+        :param kwargs: Arguments for parent class.
+        """
+        super().__init__(**kwargs)
+        self.supports_masking = True
+        self.return_attention = return_attention
+        self.history_only = history_only
+        self.intensity = self.attention = None
+
+    def get_config(self):
+        """Return the config of the layer."""
+        config = {
+            "return_attention": self.return_attention,
+            "history_only": self.history_only,
+        }
+        base_config = super().get_config()
+        return dict(list(base_config.items()) + list(config.items()))
+
+    def compute_mask(self, inputs, mask=None):
+        """Compute the mask for the layer."""
+        mask = mask[0]
+        if self.return_attention:
+            mask = [mask, mask[-1], None]
+        return [mask, mask[-1]]
+
+    def call(self, inputs, mask=None, **kwargs):
+        """Compute the output of the layer."""
+        if len(inputs) == 4:
+            query, key, value, prev = inputs
+            mask = mask[1]
+        else:
+            query = key = value = inputs[0]
+            prev = inputs[1]
+            mask = mask[0]
+        feature_dim = K.shape(query)[-1]
+        e = K.batch_dot(query, key, axes=2) / K.sqrt(
+            K.cast(feature_dim, dtype=K.floatx())
+        )
+        new_prev = e = e + prev
+        if self.history_only:
+            query_len, key_len = K.shape(query)[1], K.shape(key)[1]
+            indices = K.expand_dims(K.arange(0, key_len), axis=0)
+            upper = K.expand_dims(K.arange(0, query_len), axis=-1)
+            e -= 10000.0 * K.expand_dims(K.cast(indices > upper, K.floatx()), axis=0)
+        if mask is not None:
+            e -= 10000.0 * (1.0 - K.cast(K.expand_dims(mask, axis=-2), K.floatx()))
+        self.intensity = e
+        e = K.exp(e - K.max(e, axis=-1, keepdims=True))
+        self.attention = e / K.sum(e, axis=-1, keepdims=True)
+        v = K.batch_dot(self.attention, value)
+        output = [v, new_prev]
+        if self.return_attention:
+            output.append(self.attention)
+        return output
diff --git a/sktime/libs/_keras_self_attention/scaled_dot_attention.py b/sktime/libs/_keras_self_attention/scaled_dot_attention.py
new file mode 100644
index 000000000..283cde374
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/scaled_dot_attention.py
@@ -0,0 +1,91 @@
+"""Scaled dot product attention layer."""
+
+from sktime.utils.dependencies import _safe_import
+
+keras = _safe_import("tensorflow.keras")
+K = _safe_import("tensorflow.keras.backend")
+
+
+class ScaledDotProductAttention(keras.layers.Layer):
+    r"""Scaled dot product attention layer.
+
+    The attention layer that takes three inputs representing queries, keys and values.
+
+    \text{Attention}(Q, K, V) = \text{softmax}(\frac{Q K^T}{\sqrt{d_k}}) V
+
+    See: https://arxiv.org/pdf/1706.03762.pdf
+
+    Parameters
+    ----------
+    return_attention : bool, optional (default=False)
+        Whether to return attention weights.
+    history_only : bool, optional (default=False)
+        Whether to only use history data. If True, the attention weights are masked
+        to prevent attending to future positions in the sequence.
+    **kwargs : keyword arguments
+        Additional arguments for the parent class.
+    """
+
+    def __init__(self, return_attention=False, history_only=False, **kwargs):
+        """Initialize the layer."""
+        super().__init__(**kwargs)
+        self.supports_masking = True
+        self.return_attention = return_attention
+        self.history_only = history_only
+        self.intensity = self.attention = None
+
+    def get_config(self):
+        """Return the config of the layer."""
+        config = {
+            "return_attention": self.return_attention,
+            "history_only": self.history_only,
+        }
+        base_config = super().get_config()
+        return dict(list(base_config.items()) + list(config.items()))
+
+    def compute_output_shape(self, input_shape):
+        """Compute the output shape of the layer."""
+        if isinstance(input_shape, list):
+            query_shape, key_shape, value_shape = input_shape
+        else:
+            query_shape = key_shape = value_shape = input_shape
+        output_shape = query_shape[:-1] + value_shape[-1:]
+        if self.return_attention:
+            attention_shape = query_shape[:2] + (key_shape[1],)
+            return [output_shape, attention_shape]
+        return output_shape
+
+    def compute_mask(self, inputs, mask=None):
+        """Compute the mask for the layer."""
+        if isinstance(mask, list):
+            mask = mask[0]
+        if self.return_attention:
+            return [mask, None]
+        return mask
+
+    def call(self, inputs, mask=None, **kwargs):
+        """Compute the output of the layer."""
+        if isinstance(inputs, list):
+            query, key, value = inputs
+        else:
+            query = key = value = inputs
+        if isinstance(mask, list):
+            mask = mask[1]
+        feature_dim = K.shape(query)[-1]
+        e = K.batch_dot(query, key, axes=2) / K.sqrt(
+            K.cast(feature_dim, dtype=K.floatx())
+        )
+        if self.history_only:
+            query_len, key_len = K.shape(query)[1], K.shape(key)[1]
+            indices = K.expand_dims(K.arange(0, key_len), axis=0)
+            upper = K.expand_dims(K.arange(0, query_len), axis=-1)
+            e -= 10000.0 * K.expand_dims(K.cast(indices > upper, K.floatx()), axis=0)
+        if mask is not None:
+            e -= 10000.0 * (1.0 - K.cast(K.expand_dims(mask, axis=-2), K.floatx()))
+        self.intensity = e
+        e = K.exp(e - K.max(e, axis=-1, keepdims=True))
+        self.attention = e / K.sum(e, axis=-1, keepdims=True)
+        v = K.batch_dot(self.attention, value)
+        if self.return_attention:
+            return [v, self.attention]
+        return v
diff --git a/sktime/libs/_keras_self_attention/seq_self_attention.py b/sktime/libs/_keras_self_attention/seq_self_attention.py
new file mode 100644
index 000000000..aa1b3d757
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/seq_self_attention.py
@@ -0,0 +1,304 @@
+"""Sequential self-attention layer."""
+
+from sktime.utils.dependencies import _safe_import
+
+keras = _safe_import("tensorflow.keras")
+K = _safe_import("tensorflow.keras.backend")
+
+
+class SeqSelfAttention(keras.layers.Layer):
+    """Sequential self-attention layer.
+
+    For additive attention, see: https://arxiv.org/pdf/1806.01264.pdf
+
+    Parameters
+    ----------
+    units: int, optional (default=32)
+        The dimension of the vectors that used to calculate the attention weights.
+    attention_width: int, optional (default=None)
+        The width of local attention.
+    attention_type: str, optional (default='additive')
+        'additive' or 'multiplicative'.
+    return_attention: bool, optional (default=False)
+        Whether to return the attention weights for visualization.
+    history_only: bool, optional (default=False)
+        Only use historical pieces of data.
+    kernel_initializer: str, optional (default='glorot_normal')
+        The initializer for weight matrices.
+    bias_initializer: str, optional (default='zeros')
+        The initializer for biases.
+    kernel_regularizer: str, optional (default=None)
+        The regularization for weight matrices.
+    bias_regularizer: str, optional (default=None)
+        The regularization for biases.
+    kernel_constraint: str, optional (default=None)
+        The constraint for weight matrices.
+    bias_constraint: str, optional (default=None)
+        The constraint for biases.
+    use_additive_bias: bool, optional (default=True)
+        Whether to use bias while calculating the relevance of inputs features
+        in additive mode.
+    use_attention_bias: bool, optional (default=True)
+        Whether to use bias while calculating the weights of attention.
+    attention_activation: str, optional (default=None)
+        The activation used for calculating the weights of attention.
+    attention_regularizer_weight: float, optional (default=0.0)
+        The weights of attention regularizer.
+    kwargs: dict, optional
+        Parameters for parent class.
+    """
+
+    ATTENTION_TYPE_ADD = "additive"
+    ATTENTION_TYPE_MUL = "multiplicative"
+
+    def __init__(
+        self,
+        units=32,
+        attention_width=None,
+        attention_type=ATTENTION_TYPE_ADD,
+        return_attention=False,
+        history_only=False,
+        kernel_initializer="glorot_normal",
+        bias_initializer="zeros",
+        kernel_regularizer=None,
+        bias_regularizer=None,
+        kernel_constraint=None,
+        bias_constraint=None,
+        use_additive_bias=True,
+        use_attention_bias=True,
+        attention_activation=None,
+        attention_regularizer_weight=0.0,
+        **kwargs,
+    ):
+        """Layer initialization."""
+        super().__init__(**kwargs)
+        self.supports_masking = True
+        self.units = units
+        self.attention_width = attention_width
+        self.attention_type = attention_type
+        self.return_attention = return_attention
+        self.history_only = history_only
+        if history_only and attention_width is None:
+            self.attention_width = int(1e9)
+
+        self.use_additive_bias = use_additive_bias
+        self.use_attention_bias = use_attention_bias
+        self.kernel_initializer = keras.initializers.get(kernel_initializer)
+        self.bias_initializer = keras.initializers.get(bias_initializer)
+        self.kernel_regularizer = keras.regularizers.get(kernel_regularizer)
+        self.bias_regularizer = keras.regularizers.get(bias_regularizer)
+        self.kernel_constraint = keras.constraints.get(kernel_constraint)
+        self.bias_constraint = keras.constraints.get(bias_constraint)
+        self.attention_activation = keras.activations.get(attention_activation)
+        self.attention_regularizer_weight = attention_regularizer_weight
+        self._backend = keras.backend.backend()
+
+        if attention_type == SeqSelfAttention.ATTENTION_TYPE_ADD:
+            self.Wx, self.Wt, self.bh = None, None, None
+            self.Wa, self.ba = None, None
+        elif attention_type == SeqSelfAttention.ATTENTION_TYPE_MUL:
+            self.Wa, self.ba = None, None
+        else:
+            raise NotImplementedError(
+                "No implementation for attention type : " + attention_type
+            )
+
+    def get_config(self):
+        """Return the config of the layer."""
+        config = {
+            "units": self.units,
+            "attention_width": self.attention_width,
+            "attention_type": self.attention_type,
+            "return_attention": self.return_attention,
+            "history_only": self.history_only,
+            "use_additive_bias": self.use_additive_bias,
+            "use_attention_bias": self.use_attention_bias,
+            "kernel_initializer": keras.initializers.serialize(self.kernel_initializer),
+            "bias_initializer": keras.initializers.serialize(self.bias_initializer),
+            "kernel_regularizer": keras.regularizers.serialize(self.kernel_regularizer),
+            "bias_regularizer": keras.regularizers.serialize(self.bias_regularizer),
+            "kernel_constraint": keras.constraints.serialize(self.kernel_constraint),
+            "bias_constraint": keras.constraints.serialize(self.bias_constraint),
+            "attention_activation": keras.activations.serialize(
+                self.attention_activation
+            ),
+            "attention_regularizer_weight": self.attention_regularizer_weight,
+        }
+        base_config = super().get_config()
+        return dict(list(base_config.items()) + list(config.items()))
+
+    def build(self, input_shape):
+        """Build the layer."""
+        if self.attention_type == SeqSelfAttention.ATTENTION_TYPE_ADD:
+            self._build_additive_attention(input_shape)
+        elif self.attention_type == SeqSelfAttention.ATTENTION_TYPE_MUL:
+            self._build_multiplicative_attention(input_shape)
+        super().build(input_shape)
+
+    def _build_additive_attention(self, input_shape):
+        feature_dim = int(input_shape[2])
+
+        self.Wt = self.add_weight(
+            shape=(feature_dim, self.units),
+            name=f"{self.name}_Add_Wt",
+            initializer=self.kernel_initializer,
+            regularizer=self.kernel_regularizer,
+            constraint=self.kernel_constraint,
+        )
+        self.Wx = self.add_weight(
+            shape=(feature_dim, self.units),
+            name=f"{self.name}_Add_Wx",
+            initializer=self.kernel_initializer,
+            regularizer=self.kernel_regularizer,
+            constraint=self.kernel_constraint,
+        )
+        if self.use_additive_bias:
+            self.bh = self.add_weight(
+                shape=(self.units,),
+                name=f"{self.name}_Add_bh",
+                initializer=self.bias_initializer,
+                regularizer=self.bias_regularizer,
+                constraint=self.bias_constraint,
+            )
+
+        self.Wa = self.add_weight(
+            shape=(self.units, 1),
+            name=f"{self.name}_Add_Wa",
+            initializer=self.kernel_initializer,
+            regularizer=self.kernel_regularizer,
+            constraint=self.kernel_constraint,
+        )
+        if self.use_attention_bias:
+            self.ba = self.add_weight(
+                shape=(1,),
+                name=f"{self.name}_Add_ba",
+                initializer=self.bias_initializer,
+                regularizer=self.bias_regularizer,
+                constraint=self.bias_constraint,
+            )
+
+    def _build_multiplicative_attention(self, input_shape):
+        feature_dim = int(input_shape[2])
+
+        self.Wa = self.add_weight(
+            shape=(feature_dim, feature_dim),
+            name=f"{self.name}_Mul_Wa",
+            initializer=self.kernel_initializer,
+            regularizer=self.kernel_regularizer,
+            constraint=self.kernel_constraint,
+        )
+        if self.use_attention_bias:
+            self.ba = self.add_weight(
+                shape=(1,),
+                name=f"{self.name}_Mul_ba",
+                initializer=self.bias_initializer,
+                regularizer=self.bias_regularizer,
+                constraint=self.bias_constraint,
+            )
+
+    def call(self, inputs, mask=None, **kwargs):
+        """Compute the output of the layer."""
+        input_len = K.shape(inputs)[1]
+
+        if self.attention_type == SeqSelfAttention.ATTENTION_TYPE_ADD:
+            e = self._call_additive_emission(inputs)
+        elif self.attention_type == SeqSelfAttention.ATTENTION_TYPE_MUL:
+            e = self._call_multiplicative_emission(inputs)
+
+        if self.attention_activation is not None:
+            e = self.attention_activation(e)
+        if self.attention_width is not None:
+            if self.history_only:
+                lower = K.arange(0, input_len) - (self.attention_width - 1)
+            else:
+                lower = K.arange(0, input_len) - self.attention_width // 2
+            lower = K.expand_dims(lower, axis=-1)
+            upper = lower + self.attention_width
+            indices = K.expand_dims(K.arange(0, input_len), axis=0)
+            e -= 10000.0 * (
+                1.0
+                - K.cast(lower <= indices, K.floatx())
+                * K.cast(indices < upper, K.floatx())
+            )
+        if mask is not None:
+            mask = K.expand_dims(K.cast(mask, K.floatx()), axis=-1)
+            e -= 10000.0 * (
+                (1.0 - mask) * (1.0 - K.permute_dimensions(mask, (0, 2, 1)))
+            )
+
+        # a_{t} = \text{softmax}(e_t)
+        e = K.exp(e - K.max(e, axis=-1, keepdims=True))
+        a = e / K.sum(e, axis=-1, keepdims=True)
+
+        # l_t = \sum_{t'} a_{t, t'} x_{t'}
+        v = K.batch_dot(a, inputs)
+        if self.attention_regularizer_weight > 0.0:
+            self.add_loss(self._attention_regularizer(a))
+
+        if self.return_attention:
+            return [v, a]
+        return v
+
+    def _call_additive_emission(self, inputs):
+        input_shape = K.shape(inputs)
+        batch_size, input_len = input_shape[0], input_shape[1]
+
+        # h_{t, t'} = \tanh(x_t^T W_t + x_{t'}^T W_x + b_h)
+        q = K.expand_dims(K.dot(inputs, self.Wt), 2)
+        k = K.expand_dims(K.dot(inputs, self.Wx), 1)
+        if self.use_additive_bias:
+            h = K.tanh(q + k + self.bh)
+        else:
+            h = K.tanh(q + k)
+
+        # e_{t, t'} = W_a h_{t, t'} + b_a
+        if self.use_attention_bias:
+            e = K.reshape(
+                K.dot(h, self.Wa) + self.ba, (batch_size, input_len, input_len)
+            )
+        else:
+            e = K.reshape(K.dot(h, self.Wa), (batch_size, input_len, input_len))
+        return e
+
+    def _call_multiplicative_emission(self, inputs):
+        # e_{t, t'} = x_t^T W_a x_{t'} + b_a
+        e = K.batch_dot(K.dot(inputs, self.Wa), K.permute_dimensions(inputs, (0, 2, 1)))
+        if self.use_attention_bias:
+            e += self.ba[0]
+        return e
+
+    def compute_output_shape(self, input_shape):
+        """Compute the output shape of the layer."""
+        output_shape = input_shape
+        if self.return_attention:
+            attention_shape = (input_shape[0], output_shape[1], input_shape[1])
+            return [output_shape, attention_shape]
+        return output_shape
+
+    def compute_mask(self, inputs, mask=None):
+        """Compute the mask of the layer."""
+        if self.return_attention:
+            return [mask, None]
+        return mask
+
+    def _attention_regularizer(self, attention):
+        batch_size = K.cast(K.shape(attention)[0], K.floatx())
+        input_len = K.shape(attention)[-1]
+        indices = K.expand_dims(K.arange(0, input_len), axis=0)
+        diagonal = K.expand_dims(K.arange(0, input_len), axis=-1)
+        eye = K.cast(K.equal(indices, diagonal), K.floatx())
+        return (
+            self.attention_regularizer_weight
+            * K.sum(
+                K.square(
+                    K.batch_dot(attention, K.permute_dimensions(attention, (0, 2, 1)))
+                    - eye
+                )
+            )
+            / batch_size
+        )
+
+    @staticmethod
+    def get_custom_objects():
+        """Get custom objects for Keras."""
+        return {"SeqSelfAttention": SeqSelfAttention}
diff --git a/sktime/libs/_keras_self_attention/seq_weighted_attention.py b/sktime/libs/_keras_self_attention/seq_weighted_attention.py
new file mode 100644
index 000000000..7877f9686
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/seq_weighted_attention.py
@@ -0,0 +1,82 @@
+"""Sequential weighted attention layer."""
+
+from sktime.utils.dependencies import _safe_import
+
+keras = _safe_import("tensorflow.keras")
+K = _safe_import("tensorflow.keras.backend")
+
+
+class SeqWeightedAttention(keras.layers.Layer):
+    r"""Sequential weighted attention layer.
+
+    Y = \text{softmax}(XW + b) X
+
+    See: https://arxiv.org/pdf/1708.00524.pdf
+    """
+
+    def __init__(self, use_bias=True, return_attention=False, **kwargs):
+        super().__init__(**kwargs)
+        self.supports_masking = True
+        self.use_bias = use_bias
+        self.return_attention = return_attention
+        self.W, self.b = None, None
+
+    def get_config(self):
+        """Return the config of the layer."""
+        config = {
+            "use_bias": self.use_bias,
+            "return_attention": self.return_attention,
+        }
+        base_config = super().get_config()
+        return dict(list(base_config.items()) + list(config.items()))
+
+    def build(self, input_shape):
+        """Create the weights of the layer."""
+        self.W = self.add_weight(
+            shape=(int(input_shape[2]), 1),
+            name=f"{self.name}_W",
+            initializer=keras.initializers.get("uniform"),
+        )
+        if self.use_bias:
+            self.b = self.add_weight(
+                shape=(1,),
+                name=f"{self.name}_b",
+                initializer=keras.initializers.get("zeros"),
+            )
+        super().build(input_shape)
+
+    def call(self, x, mask=None):
+        """Compute the output of the layer."""
+        logits = K.dot(x, self.W)
+        if self.use_bias:
+            logits += self.b
+        x_shape = K.shape(x)
+        logits = K.reshape(logits, (x_shape[0], x_shape[1]))
+        if mask is not None:
+            mask = K.cast(mask, K.floatx())
+            logits -= 10000.0 * (1.0 - mask)
+        ai = K.exp(logits - K.max(logits, axis=-1, keepdims=True))
+        att_weights = ai / (K.sum(ai, axis=1, keepdims=True) + K.epsilon())
+        weighted_input = x * K.expand_dims(att_weights)
+        result = K.sum(weighted_input, axis=1)
+        if self.return_attention:
+            return [result, att_weights]
+        return result
+
+    def compute_output_shape(self, input_shape):
+        """Compute the output shape of the layer."""
+        output_len = input_shape[2]
+        if self.return_attention:
+            return [(input_shape[0], output_len), (input_shape[0], input_shape[1])]
+        return input_shape[0], output_len
+
+    def compute_mask(self, _, input_mask=None):
+        """Compute the mask for the layer."""
+        if self.return_attention:
+            return [None, None]
+        return None
+
+    @staticmethod
+    def get_custom_objects():
+        """Return the custom objects of the layer."""
+        return {"SeqWeightedAttention": SeqWeightedAttention}
diff --git a/sktime/networks/cntc.py b/sktime/networks/cntc.py
index 8918be55a..f9fde19cf 100644
--- a/sktime/networks/cntc.py
+++ b/sktime/networks/cntc.py
@@ -3,7 +3,7 @@
 __author__ = ["James-Large", "Withington", "TonyBagnall", "AurumnPegasus"]
 
 from sktime.networks.base import BaseDeepNetwork
-from sktime.utils.dependencies import _check_dl_dependencies, _check_soft_dependencies
+from sktime.utils.dependencies import _check_dl_dependencies
 
 
 class CNTCNetwork(BaseDeepNetwork):
@@ -52,7 +52,7 @@ class CNTCNetwork(BaseDeepNetwork):
     _tags = {
         "authors": ["James-Large", "Withington", "TonyBagnall", "AurumnPegasus"],
         "maintainers": ["James-Large", "Withington", "AurumnPegasus"],
-        "python_dependencies": ["tensorflow", "keras-self-attention"],
+        "python_dependencies": ["tensorflow"],
     }
 
     def __init__(
@@ -64,7 +64,6 @@ class CNTCNetwork(BaseDeepNetwork):
         lstm_size=8,
         dense_size=64,
     ):
-        _check_soft_dependencies("keras-self-attention", severity="error")
         _check_dl_dependencies(severity="error")
 
         self.random_state = random_state
@@ -89,9 +88,10 @@ class CNTCNetwork(BaseDeepNetwork):
         input_layer: a keras layer
         output_layer: a keras layer
         """
-        from keras_self_attention import SeqSelfAttention
         from tensorflow import keras
 
+        from sktime.libs._keras_self_attention import SeqSelfAttention
+
         input_layers = []
 
         # CNN Arm
diff --git a/sktime/networks/resnet.py b/sktime/networks/resnet.py
index 53c3f7844..24445a78f 100644
--- a/sktime/networks/resnet.py
+++ b/sktime/networks/resnet.py
@@ -32,7 +32,7 @@ class ResNetNetwork(BaseDeepNetwork):
 
     _tags = {
         "authors": ["hfawaz", "James-Large", "Withington", "nilesh05apr"],
-        "python_dependencies": ["tensorflow", "keras-self-attention"],
+        "python_dependencies": ["tensorflow"],
     }
 
     def __init__(self, random_state=0):
diff --git a/sktime/networks/tapnet.py b/sktime/networks/tapnet.py
index 4804a84a3..2c55595bd 100644
--- a/sktime/networks/tapnet.py
+++ b/sktime/networks/tapnet.py
@@ -9,7 +9,7 @@ import math
 import numpy as np
 
 from sktime.networks.base import BaseDeepNetwork
-from sktime.utils.dependencies import _check_dl_dependencies, _check_soft_dependencies
+from sktime.utils.dependencies import _check_dl_dependencies
 
 
 class TapNetNetwork(BaseDeepNetwork):
@@ -51,7 +51,7 @@ class TapNetNetwork(BaseDeepNetwork):
     34(4), 6845-6852, 2020
     """
 
-    _tags = {"python_dependencies": ["tensorflow", "keras-self-attention"]}
+    _tags = {"python_dependencies": ["tensorflow"]}
 
     def __init__(
         self,
@@ -68,7 +68,6 @@ class TapNetNetwork(BaseDeepNetwork):
         random_state=1,
         padding="same",
     ):
-        _check_soft_dependencies("keras-self-attention", severity="error")
         _check_dl_dependencies(severity="error")
 
         super().__init__()
@@ -156,9 +155,10 @@ class TapNetNetwork(BaseDeepNetwork):
         output_layer : a keras layer
         """
         import tensorflow as tf
-        from keras_self_attention import SeqSelfAttention
         from tensorflow import keras
 
+        from sktime.libs._keras_self_attention import SeqSelfAttention
+
         input_layer = keras.layers.Input(input_shape)
 
         if self.rp_params[0] < 0:
diff --git a/sktime/param_est/compose/_pipeline.py b/sktime/param_est/compose/_pipeline.py
index 364bfb45d..9c1fcf3ae 100644
--- a/sktime/param_est/compose/_pipeline.py
+++ b/sktime/param_est/compose/_pipeline.py
@@ -118,7 +118,9 @@ class ParamFitterPipeline(_HeterogenousMetaEstimator, BaseParamFitter):
         # can handle missing values iff: both estimator and all transformers can,
         #   *or* transformer chain removes missing data
         missing = param_est.get_tag("capability:missing_values", False)
-        missing = missing and self.transformers_.get_tag("handles-missing-data", False)
+        missing = missing and self.transformers_.get_tag(
+            "capability:missing_values", False
+        )
         missing = missing or self.transformers_.get_tag(
             "capability:missing_values:removes", False
         )
diff --git a/sktime/param_est/plugin/_forecaster.py b/sktime/param_est/plugin/_forecaster.py
index ce95bf1ef..a267bd2c4 100644
--- a/sktime/param_est/plugin/_forecaster.py
+++ b/sktime/param_est/plugin/_forecaster.py
@@ -108,7 +108,7 @@ class PluginParamsForecaster(_DelegatedForecaster):
     _tags = {
         "authors": "fkiraly",
         "requires-fh-in-fit": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "scitype:y": "both",
         "y_inner_mtype": ["pd.DataFrame", "pd.Series"],
         "fit_is_empty": False,
diff --git a/sktime/param_est/plugin/_transformer.py b/sktime/param_est/plugin/_transformer.py
index 1f4875aec..0430e35eb 100644
--- a/sktime/param_est/plugin/_transformer.py
+++ b/sktime/param_est/plugin/_transformer.py
@@ -136,7 +136,7 @@ class PluginParamsTransformer(_DelegatedTransformer):
             "skip-inverse-transform",
             "capability:unequal_length",
             "capability:unequal_length:removes",
-            "handles-missing-data",
+            "capability:missing_values",
             "capability:missing_values:removes",
         ]
 
diff --git a/sktime/registry/_lookup.py b/sktime/registry/_lookup.py
index 315df1b86..ddf1b5bad 100644
--- a/sktime/registry/_lookup.py
+++ b/sktime/registry/_lookup.py
@@ -158,7 +158,7 @@ def all_estimators(
     >>> # return all forecasters by filtering for estimator type
     >>> all_estimators("forecaster")  # doctest: +SKIP
     >>> # return all forecasters which handle missing data in the input by tag filtering
-    >>> all_estimators("forecaster", filter_tags={"handles-missing-data": True})  # doctest: +SKIP
+    >>> all_estimators("forecaster", filter_tags={"capability:missing_values": True})  # doctest: +SKIP
 
     References
     ----------
diff --git a/sktime/regression/compose/_pipeline.py b/sktime/regression/compose/_pipeline.py
index 504cacd49..f2bd38c41 100644
--- a/sktime/regression/compose/_pipeline.py
+++ b/sktime/regression/compose/_pipeline.py
@@ -118,7 +118,9 @@ class RegressorPipeline(_HeterogenousMetaEstimator, BaseRegressor):
         # can handle missing values iff: both regressor and all transformers can,
         #   *or* transformer chain removes missing data
         missing = regressor.get_tag("capability:missing_values", False)
-        missing = missing and self.transformers_.get_tag("handles-missing-data", False)
+        missing = missing and self.transformers_.get_tag(
+            "capability:missing_values", False
+        )
         missing = missing or self.transformers_.get_tag(
             "capability:missing_values:removes", False
         )
@@ -417,7 +419,7 @@ class SklearnRegressorPipeline(_HeterogenousMetaEstimator, BaseRegressor):
         # can handle missing values iff transformer chain removes missing data
         # sklearn regressors might be able to handle missing data (but no tag there)
         # so better set the tag liberally
-        missing = self.transformers_.get_tag("handles-missing-data", False)
+        missing = self.transformers_.get_tag("capability:missing_values", False)
         missing = missing or self.transformers_.get_tag(
             "capability:missing_values:removes", False
         )
diff --git a/sktime/regression/deep_learning/cntc.py b/sktime/regression/deep_learning/cntc.py
index 01f41bfd5..41f16a425 100644
--- a/sktime/regression/deep_learning/cntc.py
+++ b/sktime/regression/deep_learning/cntc.py
@@ -69,7 +69,7 @@ class CNTCRegressor(BaseDeepRegressor):
             "AurumnPegasus",
         ],
         "maintainers": ["James-Large", "Withington", "AurumnPegasus", "nilesh05apr"],
-        "python_dependencies": ["tensorflow", "keras-self-attention"],
+        "python_dependencies": ["tensorflow"],
     }
 
     def __init__(
diff --git a/sktime/transformations/base.py b/sktime/transformations/base.py
index 2e0d17335..699b7d417 100644
--- a/sktime/transformations/base.py
+++ b/sktime/transformations/base.py
@@ -131,7 +131,7 @@ class BaseTransformer(BaseEstimator):
         # can the transformer handle unequal length time series (if passed Panel)?
         "capability:unequal_length:removes": False,
         # is transform result always guaranteed to be equal length (and series)?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         # todo: rename to capability:missing_values
         "capability:missing_values:removes": False,
         # is transform result always guaranteed to contain no missing values?
diff --git a/sktime/transformations/bootstrap/_mbb.py b/sktime/transformations/bootstrap/_mbb.py
index b615d217f..357473097 100644
--- a/sktime/transformations/bootstrap/_mbb.py
+++ b/sktime/transformations/bootstrap/_mbb.py
@@ -184,7 +184,7 @@ class STLBootstrapTransformer(BaseTransformer):
         "capability:inverse_transform": False,
         "skip-inverse-transform": True,  # is inverse-transform skipped when called?
         "univariate-only": True,  # can the transformer handle multivariate X?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         "X-y-must-have-same-index": False,  # can estimator handle different X/y index?
         "enforce_index_type": None,  # index type that needs to be enforced in X/y
         "fit_is_empty": False,  # is fit empty and can be skipped? Yes = True
@@ -498,7 +498,7 @@ class MovingBlockBootstrapTransformer(BaseTransformer):
         "capability:inverse_transform": False,
         "skip-inverse-transform": True,  # is inverse-transform skipped when called?
         "univariate-only": True,  # can the transformer handle multivariate X?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         "X-y-must-have-same-index": False,  # can estimator handle different X/y index?
         "enforce_index_type": None,  # index type that needs to be enforced in X/y
         "fit_is_empty": True,  # is fit empty and can be skipped? Yes = True
diff --git a/sktime/transformations/bootstrap/_repeat.py b/sktime/transformations/bootstrap/_repeat.py
index 365ba191f..422bc340b 100644
--- a/sktime/transformations/bootstrap/_repeat.py
+++ b/sktime/transformations/bootstrap/_repeat.py
@@ -34,7 +34,7 @@ class RepeatBootstrapTransformer(BaseTransformer):
         "capability:inverse_transform": False,
         "skip-inverse-transform": True,  # is inverse-transform skipped when called?
         "univariate-only": False,  # can the transformer handle multivariate X?
-        "handles-missing-data": True,  # can estimator handle missing data?
+        "capability:missing_values": True,  # can estimator handle missing data?
         "X-y-must-have-same-index": False,  # can estimator handle different X/y index?
         "enforce_index_type": None,  # index type that needs to be enforced in X/y
         "fit_is_empty": True,  # is fit empty and can be skipped? Yes = True
diff --git a/sktime/transformations/bootstrap/_splitterboot.py b/sktime/transformations/bootstrap/_splitterboot.py
index c2c49e9b1..87f5ff8be 100644
--- a/sktime/transformations/bootstrap/_splitterboot.py
+++ b/sktime/transformations/bootstrap/_splitterboot.py
@@ -104,7 +104,7 @@ class SplitterBootstrapTransformer(BaseTransformer):
         "capability:inverse_transform": False,
         "skip-inverse-transform": True,  # is inverse-transform skipped when called?
         "univariate-only": False,  # can the transformer handle multivariate X?
-        "handles-missing-data": True,  # can estimator handle missing data?
+        "capability:missing_values": True,  # can estimator handle missing data?
         "X-y-must-have-same-index": False,  # can estimator handle different X/y index?
         "enforce_index_type": None,  # index type that needs to be enforced in X/y
         "fit_is_empty": True,  # is fit empty and can be skipped? Yes = True
diff --git a/sktime/transformations/bootstrap/_tsbootstrap.py b/sktime/transformations/bootstrap/_tsbootstrap.py
index 93894043a..d9b2bf000 100644
--- a/sktime/transformations/bootstrap/_tsbootstrap.py
+++ b/sktime/transformations/bootstrap/_tsbootstrap.py
@@ -54,7 +54,7 @@ class TSBootstrapAdapter(BaseTransformer):
         "capability:inverse_transform": False,
         "skip-inverse-transform": True,  # is inverse-transform skipped when called?
         "univariate-only": False,  # can the transformer handle multivariate X?
-        "handles-missing-data": True,  # can estimator handle missing data?
+        "capability:missing_values": True,  # can estimator handle missing data?
         "X-y-must-have-same-index": False,  # can estimator handle different X/y index?
         "enforce_index_type": None,  # index type that needs to be enforced in X/y
         "fit_is_empty": True,  # is fit empty and can be skipped? Yes = True
diff --git a/sktime/transformations/compose/_column.py b/sktime/transformations/compose/_column.py
index f53b7d37f..8aa9cccf3 100644
--- a/sktime/transformations/compose/_column.py
+++ b/sktime/transformations/compose/_column.py
@@ -127,7 +127,7 @@ class ColumnEnsembleTransformer(
         "y_inner_mtype": PANDAS_MTYPES,
         "fit_is_empty": False,
         "capability:unequal_length": True,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "visual_block_kind": "parallel",
     }
 
@@ -167,7 +167,7 @@ class ColumnEnsembleTransformer(
                 "transform-returns-same-time-index",
                 "capability:unequal_length",
                 "capability:unequal_length:removes",
-                "handles-missing-data",
+                "capability:missing_values",
                 "capability:missing_values:removes",
                 "scitype:transform-output",
                 "scitype:transform-labels",
@@ -191,7 +191,9 @@ class ColumnEnsembleTransformer(
             self._anytagis_then_set(
                 "capability:unequal_length:removes", False, True, l_transformers
             )
-            self._anytagis_then_set("handles-missing-data", False, True, l_transformers)
+            self._anytagis_then_set(
+                "capability:missing_values", False, True, l_transformers
+            )
             self._anytagis_then_set(
                 "capability:missing_values:removes", False, True, l_transformers
             )
@@ -439,7 +441,7 @@ class ColumnwiseTransformer(BaseTransformer):
         tags_to_clone = [
             "y_inner_mtype",
             "capability:inverse_transform",
-            "handles-missing-data",
+            "capability:missing_values",
             "X-y-must-have-same-index",
             "transform-returns-same-time-index",
             "skip-inverse-transform",
diff --git a/sktime/transformations/compose/_featureunion.py b/sktime/transformations/compose/_featureunion.py
index 02cf16f93..8e1fc268e 100644
--- a/sktime/transformations/compose/_featureunion.py
+++ b/sktime/transformations/compose/_featureunion.py
@@ -49,7 +49,7 @@ class FeatureUnion(_HeterogenousMetaEstimator, BaseTransformer):
         "scitype:transform-labels": "None",
         "scitype:instancewise": False,  # depends on components
         "univariate-only": False,  # depends on components
-        "handles-missing-data": False,  # depends on components
+        "capability:missing_values": False,  # depends on components
         "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
         "y_inner_mtype": "None",
         "X-y-must-have-same-index": False,
@@ -111,7 +111,7 @@ class FeatureUnion(_HeterogenousMetaEstimator, BaseTransformer):
         self._anytagis_then_set("transform-returns-same-time-index", False, True, ests)
         self._anytagis_then_set("skip-inverse-transform", True, False, ests)
         # self._anytagis_then_set("capability:inverse_transform", False, True, ests)
-        self._anytagis_then_set("handles-missing-data", False, True, ests)
+        self._anytagis_then_set("capability:missing_values", False, True, ests)
         self._anytagis_then_set("univariate-only", True, False, ests)
 
         # if any of the components require_X or require_y, set it for the composite
diff --git a/sktime/transformations/compose/_grouped.py b/sktime/transformations/compose/_grouped.py
index fb5ab7b36..a253bfbf9 100644
--- a/sktime/transformations/compose/_grouped.py
+++ b/sktime/transformations/compose/_grouped.py
@@ -62,7 +62,7 @@ class TransformByLevel(_DelegatedTransformer):
     _tags = {
         "authors": ["fkiraly"],
         "requires-fh-in-fit": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "X_inner_mtype": ALL_TIME_SERIES_MTYPES,
         "y_inner_mtype": ALL_TIME_SERIES_MTYPES,
         "fit_is_empty": False,
diff --git a/sktime/transformations/compose/_id.py b/sktime/transformations/compose/_id.py
index 54cbbf905..9a1366e61 100644
--- a/sktime/transformations/compose/_id.py
+++ b/sktime/transformations/compose/_id.py
@@ -26,7 +26,7 @@ class Id(BaseTransformer):
         "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?
+        "capability:missing_values": True,  # can estimator handle missing data?
     }
 
     def _transform(self, X, y=None):
diff --git a/sktime/transformations/compose/_invert.py b/sktime/transformations/compose/_invert.py
index 28074215e..ff1cec80e 100644
--- a/sktime/transformations/compose/_invert.py
+++ b/sktime/transformations/compose/_invert.py
@@ -67,7 +67,7 @@ class InvertTransform(_DelegatedTransformer):
             "scitype:instancewise",
             "X_inner_mtype",
             "y_inner_mtype",
-            "handles-missing-data",
+            "capability:missing_values",
             "X-y-must-have-same-index",
             "transform-returns-same-time-index",
             "skip-inverse-transform",
diff --git a/sktime/transformations/compose/_logger.py b/sktime/transformations/compose/_logger.py
index 4041d5705..d6667d925 100644
--- a/sktime/transformations/compose/_logger.py
+++ b/sktime/transformations/compose/_logger.py
@@ -80,7 +80,7 @@ class Logger(BaseTransformer):
         "fit_is_empty": False,  # 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?
+        "capability:missing_values": True,  # can estimator handle missing data?
     }
 
     def __init__(
diff --git a/sktime/transformations/compose/_optional.py b/sktime/transformations/compose/_optional.py
index 985cdc63e..e39c8a8f5 100644
--- a/sktime/transformations/compose/_optional.py
+++ b/sktime/transformations/compose/_optional.py
@@ -101,7 +101,7 @@ class OptionalPassthrough(_DelegatedTransformer):
             "scitype:instancewise",
             "y_inner_mtype",
             "capability:inverse_transform",
-            "handles-missing-data",
+            "capability:missing_values",
             "X-y-must-have-same-index",
             "transform-returns-same-time-index",
             "skip-inverse-transform",
diff --git a/sktime/transformations/compose/_pipeline.py b/sktime/transformations/compose/_pipeline.py
index ec50ff0ac..1f33a5e94 100644
--- a/sktime/transformations/compose/_pipeline.py
+++ b/sktime/transformations/compose/_pipeline.py
@@ -6,13 +6,9 @@ __author__ = ["fkiraly"]
 __all__ = ["TransformerPipeline"]
 
 from sktime.base._meta import _HeterogenousMetaEstimator
+from sktime.registry import is_scitype, scitype
 from sktime.transformations.base import BaseTransformer
 from sktime.transformations.compose._common import CORE_MTYPES
-from sktime.utils.sklearn import (
-    is_sklearn_classifier,
-    is_sklearn_clusterer,
-    is_sklearn_regressor,
-)
 
 
 class TransformerPipeline(_HeterogenousMetaEstimator, BaseTransformer):
@@ -147,10 +143,11 @@ class TransformerPipeline(_HeterogenousMetaEstimator, BaseTransformer):
 
     def __init__(self, steps):
         self.steps = steps
-        self.steps_ = self._check_estimators(self.steps, cls_type=BaseTransformer)
 
         super().__init__()
 
+        self.steps_ = self._check_steps(steps)
+
         # abbreviate for readability
         ests = self.steps_
         first_trafo = ests[0][1]
@@ -189,7 +186,7 @@ class TransformerPipeline(_HeterogenousMetaEstimator, BaseTransformer):
         # removes missing data iff can handle missing data,
         #   and there is an estimator in the chain that removes it
         self._tagchain_is_linked_set(
-            "handles-missing-data", "capability:missing_values:removes", ests
+            "capability:missing_values", "capability:missing_values:removes", ests
         )
         # can handle unequal length iff all estimators can handle unequal length
         #   up to a potential estimator which turns the series equal length
@@ -207,6 +204,66 @@ class TransformerPipeline(_HeterogenousMetaEstimator, BaseTransformer):
     def _steps(self, value):
         self.steps = value
 
+    def _check_steps(self, estimators):
+        """Check Steps.
+
+        Parameters
+        ----------
+        estimators : list of estimators, or list of (name, estimator) pairs
+
+        Returns
+        -------
+        step : list of (name, estimator) pairs, estimators are cloned (not references)
+            if estimators was a list of (str, estimator) tuples, then just cloned
+            if was a list of estimators, then str are generated via _get_estimator_names
+
+        Raises
+        ------
+        TypeError if names in ``estimators`` are not unique
+        TypeError if estimators in ``estimators`` are not all transformers
+        """
+        from sktime.registry._scitype_coercion import all_coercible_to, coerce_scitype
+
+        self_name = type(self).__name__
+        if not isinstance(estimators, list):
+            msg = (
+                f"steps in {self_name} must be list of estimators, "
+                f"or (string, estimator) pairs, "
+                f"the two can be mixed; but, found steps of type {type(estimators)}"
+            )
+            raise TypeError(msg)
+
+        estimator_tuples = self._get_estimator_tuples(estimators, clone_ests=True)
+        names, estimators = zip(*estimator_tuples)
+
+        # validate names
+        self._check_names(names)
+
+        ALLOWED_SCITYPES = ["transformer"]
+        COERCIBLE_SCITYPES = all_coercible_to("transformer")
+        COERCIBLE_SCITYPES = set(COERCIBLE_SCITYPES) - set(ALLOWED_SCITYPES)
+        ACCEPTED_SCITYPES = ALLOWED_SCITYPES + list(COERCIBLE_SCITYPES)
+
+        est_scitypes = [scitype(x) for x in estimators]
+
+        if not all([x in ACCEPTED_SCITYPES for x in est_scitypes]):
+            raise TypeError(f"estimators passed to {self_name} must be transformers")
+
+        # coerce to transformer if needed
+
+        def _coerce_to_trafo(x, x_st):
+            if x_st not in ["transformer"]:
+                return coerce_scitype(x, to_scitype="transformer", from_scitype=x_st)
+            return x
+
+        if not all([x in ALLOWED_SCITYPES for x in est_scitypes]):
+            est_plus_scitype = zip(estimators, est_scitypes)
+            coerced_estimators = [_coerce_to_trafo(*x) for x in est_plus_scitype]
+            estimator_tuples = list(zip(names, coerced_estimators))
+
+        # Shallow copy
+        return estimator_tuples
+
     def __mul__(self, other):
         """Magic * method, return (right) concatenated TransformerPipeline.
 
@@ -226,28 +283,28 @@ class TransformerPipeline(_HeterogenousMetaEstimator, BaseTransformer):
         """
         from sktime.classification.compose import SklearnClassifierPipeline
         from sktime.clustering.compose import SklearnClustererPipeline
-        from sktime.registry import coerce_scitype
+        from sktime.registry._scitype_coercion import coerce_scitype
         from sktime.regression.compose import SklearnRegressorPipeline
 
-        # we only coerce transformations on the right, otherwise we would end up
-        # coercing forecasters, clusterers, etc into transformer pipeline
-        # in a case where a forecaster or clusterer pipeline is intended
-        other = coerce_scitype(
-            other, "transformer", from_scitype=["transformer", "transformer_tabular"]
-        )
+        # if sklearn transformer, coerce
+        if is_scitype(other, "transformer_tabular"):
+            other = coerce_scitype(
+                other, to_scitype="transformer", from_scitype="transformer_tabular"
+            )
 
         # if sklearn classifier, use sklearn classifier pipeline
-        if is_sklearn_classifier(other):
+        if is_scitype(other, "classifier_tabular"):
             return SklearnClassifierPipeline(classifier=other, transformers=self.steps)
 
         # if sklearn clusterer, use sklearn clusterer pipeline
-        if is_sklearn_clusterer(other):
+        if is_scitype(other, "clusterer_tabular"):
             return SklearnClustererPipeline(clusterer=other, transformers=self.steps)
 
         # if sklearn regressor, use sklearn regressor pipeline
-        if is_sklearn_regressor(other):
+        if is_scitype(other, "regressor_tabular"):
             return SklearnRegressorPipeline(regressor=other, transformers=self.steps)
 
+        # otherwise, concatenate to a flat TransformerPipeline
         return self._dunder_concat(
             other=other,
             base_class=BaseTransformer,
@@ -301,8 +358,6 @@ class TransformerPipeline(_HeterogenousMetaEstimator, BaseTransformer):
         -------
         self: reference to self
         """
-        self.steps_ = self._check_estimators(self.steps, cls_type=BaseTransformer)
-
         Xt = X
         for _, transformer in self.steps_:
             Xt = transformer.fit_transform(X=Xt, y=y)
@@ -420,7 +475,12 @@ class TransformerPipeline(_HeterogenousMetaEstimator, BaseTransformer):
         # construct with names and provoke multiple naming clashes
         params3 = {"steps": [("foo", t1), ("foo", t2), ("foo_1", t3)]}
 
-        return [params1, params2, params3]
+        # with sklearn transformers
+        from sklearn.preprocessing import StandardScaler
+
+        params4 = {"steps": [("foo", t1), ("bar", StandardScaler())]}
+
+        return [params1, params2, params3, params4]
 
     def _to_dim(self, x):
         """Translate scitype:transform-input or output tag to data dimension.
diff --git a/sktime/transformations/hierarchical/aggregate.py b/sktime/transformations/hierarchical/aggregate.py
index bfa853bba..aa7097075 100644
--- a/sktime/transformations/hierarchical/aggregate.py
+++ b/sktime/transformations/hierarchical/aggregate.py
@@ -70,7 +70,7 @@ class Aggregator(BaseTransformer):
         "capability:inverse_transform": False,  # does transformer have inverse
         "skip-inverse-transform": True,  # is inverse-transform skipped when called?
         "univariate-only": False,  # can the transformer handle multivariate X?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         "X-y-must-have-same-index": False,  # can estimator handle different X/y index?
         "fit_is_empty": True,  # is fit empty and can be skipped? Yes = True
         "transform-returns-same-time-index": False,
diff --git a/sktime/transformations/hierarchical/reconcile.py b/sktime/transformations/hierarchical/reconcile.py
index aabd55457..b4dbf72ad 100644
--- a/sktime/transformations/hierarchical/reconcile.py
+++ b/sktime/transformations/hierarchical/reconcile.py
@@ -97,7 +97,7 @@ class Reconciler(BaseTransformer):
         "capability:inverse_transform": False,
         "skip-inverse-transform": True,  # is inverse-transform skipped when called?
         "univariate-only": True,  # can the transformer handle multivariate X?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         "X-y-must-have-same-index": False,  # can estimator handle different X/y index?
         "fit_is_empty": False,  # is fit empty and can be skipped? Yes = True
         "transform-returns-same-time-index": True,
diff --git a/sktime/transformations/panel/compose_distance.py b/sktime/transformations/panel/compose_distance.py
index 576f82f4f..b9e391d62 100644
--- a/sktime/transformations/panel/compose_distance.py
+++ b/sktime/transformations/panel/compose_distance.py
@@ -62,7 +62,7 @@ class DistanceFeatures(BaseTransformer):
         "skip-inverse-transform": False,
         "capability:unequal_length": True,
         "capability:unequal_length:removes": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "capability:missing_values:removes": False,
         # we leave remember_data as False, since updating self._X in update
         # would increase the number of columns in the transform return
diff --git a/sktime/transformations/series/adi_cv.py b/sktime/transformations/series/adi_cv.py
index 487d37397..311f77de6 100644
--- a/sktime/transformations/series/adi_cv.py
+++ b/sktime/transformations/series/adi_cv.py
@@ -101,7 +101,7 @@ class ADICVTransformer(BaseTransformer):
         "fit_is_empty": True,
         "capability:inverse_transform": False,
         "capability:unequal_length": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(
diff --git a/sktime/transformations/series/augmenter.py b/sktime/transformations/series/augmenter.py
index d18526375..d18b63af2 100755
--- a/sktime/transformations/series/augmenter.py
+++ b/sktime/transformations/series/augmenter.py
@@ -29,7 +29,7 @@ class _AugmenterTags:
         "scitype:transform-output": "Series",
         "scitype:transform-labels": "None",
         "scitype:instancewise": True,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "y_inner_mtype": "pd.Series",
         "X_inner_mtype": "pd.DataFrame",
         "X-y-must-have-same-index": False,
diff --git a/sktime/transformations/series/basisfunction.py b/sktime/transformations/series/basisfunction.py
index d8c71ce06..482a3db2f 100644
--- a/sktime/transformations/series/basisfunction.py
+++ b/sktime/transformations/series/basisfunction.py
@@ -111,7 +111,7 @@ class RBFTransformer(BaseTransformer):
         "fit_is_empty": False,
         "capability:inverse_transform": False,
         "capability:unequal_length": True,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(
diff --git a/sktime/transformations/series/binning.py b/sktime/transformations/series/binning.py
index 995a5ed98..ce9be1baf 100644
--- a/sktime/transformations/series/binning.py
+++ b/sktime/transformations/series/binning.py
@@ -67,7 +67,7 @@ class TimeBinAggregate(BaseTransformer):
         "X_inner_mtype": ["pd.DataFrame"],
         # which mtypes do _fit/_predict support for X?
         "y_inner_mtype": "None",  # and for y?
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "capability:unequal_length": True,
         "capability:unequal_length:removes": True,
         "transform-returns-same-time-index": False,
diff --git a/sktime/transformations/series/bkfilter.py b/sktime/transformations/series/bkfilter.py
index a90c81a0e..22b56703f 100644
--- a/sktime/transformations/series/bkfilter.py
+++ b/sktime/transformations/series/bkfilter.py
@@ -89,7 +89,7 @@ class BKFilter(BaseTransformer):
         # does transform return have the same time index as input X
         "capability:unequal_length": True,
         # can the transformer handle unequal length time series (if passed Panel)?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         "remember_data": False,  # whether all data seen is remembered as self._X
     }
 
diff --git a/sktime/transformations/series/cffilter.py b/sktime/transformations/series/cffilter.py
index 195da31d1..39966b998 100644
--- a/sktime/transformations/series/cffilter.py
+++ b/sktime/transformations/series/cffilter.py
@@ -77,7 +77,7 @@ class CFFilter(BaseTransformer):
         # does transform return have the same time index as input X
         "capability:unequal_length": True,
         # can the transformer handle unequal length time series (if passed Panel)?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         "remember_data": False,  # whether all data seen is remembered as self._X
     }
 
diff --git a/sktime/transformations/series/clear_sky.py b/sktime/transformations/series/clear_sky.py
index 1cdb09bb0..a47f0b220 100644
--- a/sktime/transformations/series/clear_sky.py
+++ b/sktime/transformations/series/clear_sky.py
@@ -100,7 +100,7 @@ class ClearSky(BaseTransformer):
         "skip-inverse-transform": False,  # is inverse-transform skipped when called?
         "capability:unequal_length": False,
         "capability:unequal_length:removes": True,  # ?
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:missing_values:removes": True,
     }
 
diff --git a/sktime/transformations/series/dilation_mapping.py b/sktime/transformations/series/dilation_mapping.py
index 4ba62c894..d52e05734 100644
--- a/sktime/transformations/series/dilation_mapping.py
+++ b/sktime/transformations/series/dilation_mapping.py
@@ -74,7 +74,7 @@ class DilationMappingTransformer(BaseTransformer):
         "fit_is_empty": True,
         "capability:inverse_transform": False,
         "capability:unequal_length": True,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
     }
 
     def __init__(self, dilation=2):
diff --git a/sktime/transformations/series/dropna.py b/sktime/transformations/series/dropna.py
index 237218ab7..5b05b7b18 100644
--- a/sktime/transformations/series/dropna.py
+++ b/sktime/transformations/series/dropna.py
@@ -67,7 +67,7 @@ class DropNA(BaseTransformer):
         "fit_is_empty": False,
         "capability:inverse_transform": False,
         "capability:unequal_length": True,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     VALID_AXIS_VALUES = [0, "index", 1, "columns"]
diff --git a/sktime/transformations/series/dummies.py b/sktime/transformations/series/dummies.py
index b30917cf2..95b0545ef 100644
--- a/sktime/transformations/series/dummies.py
+++ b/sktime/transformations/series/dummies.py
@@ -94,7 +94,7 @@ class SeasonalDummiesOneHot(BaseTransformer):
         "capability:unequal_length:removes": False,
         # is transform result always guaranteed to be equal length (and series)?
         #   not relevant for transformers that return Primitives in transform-output
-        "handles-missing-data": True,  # can estimator handle missing data?
+        "capability:missing_values": True,  # can estimator handle missing data?
         # todo: rename to capability:missing_values
         "capability:missing_values:removes": True,
         # is transform result always guaranteed to contain no missing values?
diff --git a/sktime/transformations/series/fourier.py b/sktime/transformations/series/fourier.py
index 5b9274d6b..1b1c9aba8 100644
--- a/sktime/transformations/series/fourier.py
+++ b/sktime/transformations/series/fourier.py
@@ -129,7 +129,7 @@ class FourierFeatures(BaseTransformer):
         "capability:unequal_length:removes": False,
         # is transform result always guaranteed to be equal length (and series)?
         #   not relevant for transformers that return Primitives in transform-output
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         # todo: rename to capability:missing_values
         "capability:missing_values:removes": False,
         # is transform result always guaranteed to contain no missing values?
@@ -385,7 +385,7 @@ class FourierTransform(BaseTransformer):
         "fit_is_empty": True,
         "capability:inverse_transform": False,
         "capability:unequal_length": True,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(self):
diff --git a/sktime/transformations/series/func_transform.py b/sktime/transformations/series/func_transform.py
index fd5a84df0..a5e96cee5 100644
--- a/sktime/transformations/series/func_transform.py
+++ b/sktime/transformations/series/func_transform.py
@@ -84,7 +84,7 @@ class FunctionTransformer(BaseTransformer):
         # which mtypes do _fit/_predict support for X?
         "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for y?
         "fit_is_empty": True,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "capability:inverse_transform": True,
     }
 
diff --git a/sktime/transformations/series/holiday/_holidayfeats.py b/sktime/transformations/series/holiday/_holidayfeats.py
index 8ad2abcde..b68dec0b9 100644
--- a/sktime/transformations/series/holiday/_holidayfeats.py
+++ b/sktime/transformations/series/holiday/_holidayfeats.py
@@ -102,7 +102,7 @@ class HolidayFeatures(BaseTransformer):
         "scitype:transform-labels": "None",
         "scitype:instancewise": True,
         "univariate-only": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "X_inner_mtype": "pd.DataFrame",
         "y_inner_mtype": "None",
         "X-y-must-have-same-index": False,
diff --git a/sktime/transformations/series/holiday/country_holidays.py b/sktime/transformations/series/holiday/country_holidays.py
index 290fdeec8..a1d7192b6 100644
--- a/sktime/transformations/series/holiday/country_holidays.py
+++ b/sktime/transformations/series/holiday/country_holidays.py
@@ -87,7 +87,7 @@ class CountryHolidaysTransformer(BaseTransformer):
         "enforce_index_type": [pandas.DatetimeIndex, pandas.PeriodIndex],
         "capability:inverse_transform": False,
         "capability:unequal_length": True,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
     }
 
     def __init__(
diff --git a/sktime/transformations/series/holiday/financial_holidays.py b/sktime/transformations/series/holiday/financial_holidays.py
index 2f1718dc6..dfe449376 100644
--- a/sktime/transformations/series/holiday/financial_holidays.py
+++ b/sktime/transformations/series/holiday/financial_holidays.py
@@ -87,7 +87,7 @@ class FinancialHolidaysTransformer(BaseTransformer):
         "enforce_index_type": [pandas.DatetimeIndex, pandas.PeriodIndex],
         "capability:inverse_transform": False,
         "capability:unequal_length": True,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
     }
 
     def __init__(self, market, years=None, expand=True, observed=True, name=None):
diff --git a/sktime/transformations/series/hpfilter.py b/sktime/transformations/series/hpfilter.py
index a07ffe51e..36bb50901 100644
--- a/sktime/transformations/series/hpfilter.py
+++ b/sktime/transformations/series/hpfilter.py
@@ -75,7 +75,7 @@ class HPFilter(BaseTransformer):
         # does transform return have the same time index as input X
         "capability:unequal_length": True,
         # can the transformer handle unequal length time series (if passed Panel)?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         "remember_data": False,  # whether all data seen is remembered as self._X
         "python_dependencies": "statsmodels",
     }
diff --git a/sktime/transformations/series/hurst.py b/sktime/transformations/series/hurst.py
index 66fed787f..a5d0b8792 100644
--- a/sktime/transformations/series/hurst.py
+++ b/sktime/transformations/series/hurst.py
@@ -62,7 +62,7 @@ class HurstExponentTransformer(BaseTransformer):
         "fit_is_empty": False,
         "capability:inverse_transform": False,
         "capability:unequal_length": True,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "authors": ["phoeenniixx"],
     }
 
diff --git a/sktime/transformations/series/impute.py b/sktime/transformations/series/impute.py
index 0bcc559ad..31305c3e9 100644
--- a/sktime/transformations/series/impute.py
+++ b/sktime/transformations/series/impute.py
@@ -105,7 +105,7 @@ class Imputer(BaseTransformer):
         # which mtypes do _fit/_predict support for X?
         "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for y?
         "fit_is_empty": False,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "skip-inverse-transform": True,
         "capability:inverse_transform": True,
         "univariate-only": False,
diff --git a/sktime/transformations/series/kalman_filter/_kalman_filter.py b/sktime/transformations/series/kalman_filter/_kalman_filter.py
index 3f3b95f80..0638d2cbb 100644
--- a/sktime/transformations/series/kalman_filter/_kalman_filter.py
+++ b/sktime/transformations/series/kalman_filter/_kalman_filter.py
@@ -295,7 +295,7 @@ class KalmanFilterTransformerPK(BaseKalmanFilter, BaseTransformer):
         "fit_is_empty": False,  # is fit empty and can be skipped? Yes = True
         "capability:unequal_length": False,
         # can the transformer handle unequal length time series (if passed Panel)?
-        "handles-missing-data": True,  # can estimator handle missing data?
+        "capability:missing_values": True,  # can estimator handle missing data?
         "capability:missing_values:removes": False,
         # is transform result always guaranteed to contain no missing values?
         "scitype:instancewise": True,  # is this an instance-wise transform?
@@ -786,7 +786,7 @@ class KalmanFilterTransformerFP(BaseKalmanFilter, BaseTransformer):
         "fit_is_empty": False,  # is fit empty and can be skipped? Yes = True
         "capability:unequal_length": False,
         # can the transformer handle unequal length time series (if passed Panel)?
-        "handles-missing-data": True,  # can estimator handle missing data?
+        "capability:missing_values": True,  # can estimator handle missing data?
         "capability:missing_values:removes": False,
         # is transform result always guaranteed to contain no missing values?
         "scitype:instancewise": True,  # is this an instance-wise transform?
diff --git a/sktime/transformations/series/kalman_filter/_simdkalman.py b/sktime/transformations/series/kalman_filter/_simdkalman.py
index f460660d9..3d161ce29 100644
--- a/sktime/transformations/series/kalman_filter/_simdkalman.py
+++ b/sktime/transformations/series/kalman_filter/_simdkalman.py
@@ -149,7 +149,7 @@ class KalmanFilterTransformerSIMD(BaseKalmanFilter, BaseTransformer):
         "fit_is_empty": False,  # is fit empty and can be skipped? Yes = True
         "capability:unequal_length": False,
         # can the transformer handle unequal length time series (if passed Panel)?
-        "handles-missing-data": True,  # can estimator handle missing data?
+        "capability:missing_values": True,  # can estimator handle missing data?
         "capability:missing_values:removes": False,
         # is transform result always guaranteed to contain no missing values?
         "scitype:instancewise": True,  # is this an instance-wise transform?
diff --git a/sktime/transformations/series/kinematic.py b/sktime/transformations/series/kinematic.py
index e26a4271a..2f0929ba5 100644
--- a/sktime/transformations/series/kinematic.py
+++ b/sktime/transformations/series/kinematic.py
@@ -77,7 +77,7 @@ class KinematicFeatures(BaseTransformer):
         "fit_is_empty": True,
         "capability:inverse_transform": False,
         "capability:unequal_length": True,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     # todo: add any hyper-parameters and components to constructor
diff --git a/sktime/transformations/series/lag.py b/sktime/transformations/series/lag.py
index 215a696c3..fb95d32db 100644
--- a/sktime/transformations/series/lag.py
+++ b/sktime/transformations/series/lag.py
@@ -136,7 +136,7 @@ class Lag(BaseTransformer):
         "skip-inverse-transform": True,  # is inverse-transform skipped when called?
         "capability:unequal_length": True,
         "capability:unequal_length:removes": False,
-        "handles-missing-data": True,  # can estimator handle missing data?
+        "capability:missing_values": True,  # can estimator handle missing data?
         "capability:missing_values:removes": False,
         "remember_data": True,  # remember all data seen as _X
     }
@@ -458,7 +458,7 @@ class ReducerTransform(BaseTransformer):
         "skip-inverse-transform": True,  # is inverse-transform skipped when called?
         "capability:unequal_length": True,
         "capability:unequal_length:removes": False,
-        "handles-missing-data": True,  # can estimator handle missing data?
+        "capability:missing_values": True,  # can estimator handle missing data?
         "capability:missing_values:removes": False,
     }
 
diff --git a/sktime/transformations/series/outlier_detection.py b/sktime/transformations/series/outlier_detection.py
index e51ec4fbb..d7b5cddf6 100644
--- a/sktime/transformations/series/outlier_detection.py
+++ b/sktime/transformations/series/outlier_detection.py
@@ -63,7 +63,7 @@ class HampelFilter(BaseTransformer):
         # which mtypes do _fit/_predict support for X?
         "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for y?
         "fit_is_empty": True,
-        "handles-missing-data": True,
+        "capability:missing_values": True,
         "skip-inverse-transform": True,
         "univariate-only": False,
     }
diff --git a/sktime/transformations/series/paa.py b/sktime/transformations/series/paa.py
index e125f36ea..46171e436 100644
--- a/sktime/transformations/series/paa.py
+++ b/sktime/transformations/series/paa.py
@@ -60,7 +60,7 @@ class PAA(BaseTransformer):
         "requires_y": False,
         "fit_is_empty": True,
         "capability:inverse_transform": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(self, frames=8, frame_size=0):
diff --git a/sktime/transformations/series/sax.py b/sktime/transformations/series/sax.py
index c9518d922..d97b514fc 100644
--- a/sktime/transformations/series/sax.py
+++ b/sktime/transformations/series/sax.py
@@ -70,7 +70,7 @@ class SAX(BaseTransformer):
         "requires_y": False,
         "fit_is_empty": True,
         "capability:inverse_transform": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(self, word_size=8, alphabet_size=5, frame_size=0):
diff --git a/sktime/transformations/series/subsequence_extraction.py b/sktime/transformations/series/subsequence_extraction.py
index 7ee16aa88..33a985551 100644
--- a/sktime/transformations/series/subsequence_extraction.py
+++ b/sktime/transformations/series/subsequence_extraction.py
@@ -85,7 +85,7 @@ class SubsequenceExtractionTransformer(BaseTransformer):
         "fit_is_empty": False,
         "capability:inverse_transform": False,
         "capability:unequal_length:removes": True,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(self, aggregate_fn, subseq_len, kwargs=None, selector="max"):
diff --git a/sktime/transformations/series/summarize.py b/sktime/transformations/series/summarize.py
index 15d808f7a..314467ab9 100644
--- a/sktime/transformations/series/summarize.py
+++ b/sktime/transformations/series/summarize.py
@@ -209,7 +209,7 @@ class WindowSummarizer(BaseTransformer):
         ],  # which mtypes do _fit/_predict support for X?
         "skip-inverse-transform": True,  # is inverse-transform skipped when called?
         "univariate-only": False,  # can the transformer handle multivariate X?
-        "handles-missing-data": True,  # can estimator handle missing data?
+        "capability:missing_values": True,  # can estimator handle missing data?
         "X-y-must-have-same-index": False,  # can estimator handle different X/y index?
         "enforce_index_type": None,  # index type that needs to be enforced in X/y
         "fit_is_empty": False,  # is fit empty and can be skipped? Yes = True
diff --git a/sktime/transformations/series/temporian.py b/sktime/transformations/series/temporian.py
index 01ddf472a..163b73c0c 100644
--- a/sktime/transformations/series/temporian.py
+++ b/sktime/transformations/series/temporian.py
@@ -64,7 +64,7 @@ class TemporianTransformer(BaseTransformer):
         "fit_is_empty": True,
         "capability:inverse_transform": False,
         "capability:unequal_length": True,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
     }
 
     def __init__(self, function, compile=False):
diff --git a/sktime/transformations/series/time_since.py b/sktime/transformations/series/time_since.py
index 7d89e5a19..67f522d55 100644
--- a/sktime/transformations/series/time_since.py
+++ b/sktime/transformations/series/time_since.py
@@ -100,7 +100,7 @@ class TimeSince(BaseTransformer):
         "skip-inverse-transform": True,  # is inverse-transform skipped when called?
         "capability:unequal_length": True,
         "capability:unequal_length:removes": False,
-        "handles-missing-data": True,  # can estimator handle missing data?
+        "capability:missing_values": True,  # can estimator handle missing data?
         "capability:missing_values:removes": False,
     }
 
diff --git a/sktime/transformations/series/vmd.py b/sktime/transformations/series/vmd.py
index ed95a66c8..07dba35aa 100644
--- a/sktime/transformations/series/vmd.py
+++ b/sktime/transformations/series/vmd.py
@@ -131,7 +131,7 @@ class VmdTransformer(BaseTransformer):
         "skip-inverse-transform": False,
         "capability:unequal_length": False,
         "capability:unequal_length:removes": False,
-        "handles-missing-data": False,
+        "capability:missing_values": False,
         "capability:missing_values:removes": False,
     }
 
diff --git a/sktime/utils/estimators/_forecasters.py b/sktime/utils/estimators/_forecasters.py
index 8f331311a..58983b5fa 100644
--- a/sktime/utils/estimators/_forecasters.py
+++ b/sktime/utils/estimators/_forecasters.py
@@ -30,7 +30,7 @@ class MockUnivariateForecasterLogger(BaseForecaster, _MockEstimatorMixin):
     _tags = {
         "scitype:y": "univariate",  # which y are fine? univariate/multivariate/both
         "ignores-exogeneous-X": False,  # does estimator ignore the exogeneous X?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": 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?
         "requires-fh-in-fit": False,  # is forecasting horizon already required in fit?
@@ -212,7 +212,7 @@ class MockForecaster(BaseForecaster):
     _tags = {
         "scitype:y": "both",  # which y are fine? univariate/multivariate/both
         "ignores-exogeneous-X": False,  # does estimator ignore the exogeneous X?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         "y_inner_mtype": "pd.DataFrame",  # which types do _fit, _predict, assume for y?
         "X_inner_mtype": "pd.DataFrame",  # which types do _fit, _predict, assume for X?
         "requires-fh-in-fit": False,  # is forecasting horizon already required in fit?


Test patch:
diff --git a/sktime/forecasting/compose/tests/test_pipeline.py b/sktime/forecasting/compose/tests/test_pipeline.py
index d9db79eb8..28f1e4223 100644
--- a/sktime/forecasting/compose/tests/test_pipeline.py
+++ b/sktime/forecasting/compose/tests/test_pipeline.py
@@ -416,7 +416,7 @@ def test_tag_handles_missing_data():
     """
     forecaster = MockForecaster()
     # make sure that test forecaster can't handle missing data
-    forecaster.set_tags(**{"handles-missing-data": False})
+    forecaster.set_tags(**{"capability:missing_values": False})
 
     y = _make_series()
     y.iloc[10] = np.nan
diff --git a/sktime/forecasting/tests/test_naive.py b/sktime/forecasting/tests/test_naive.py
index 243820037..498491dbd 100644
--- a/sktime/forecasting/tests/test_naive.py
+++ b/sktime/forecasting/tests/test_naive.py
@@ -282,7 +282,7 @@ def test_strategy_mean_and_last_seasonal_additional_combinations(
     forecast_data = model.predict(fh)
 
     # Make sure that the model (object) reports that it handles missing data
-    assert model.get_tag("handles-missing-data")
+    assert model.get_tag("capability:missing_values")
 
     if sp < window_length:
         # We expect a perfect forecast given our perfectly cyclic data
diff --git a/sktime/libs/_keras_self_attention/tests/__init__.py b/sktime/libs/_keras_self_attention/tests/__init__.py
new file mode 100644
index 000000000..fb695d298
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/__init__.py
@@ -0,0 +1,1 @@
+"""Tests for keras_self_attention."""
diff --git a/sktime/libs/_keras_self_attention/tests/scaled_dot_attention/__init__.py b/sktime/libs/_keras_self_attention/tests/scaled_dot_attention/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/sktime/libs/_keras_self_attention/tests/scaled_dot_attention/test_history.py b/sktime/libs/_keras_self_attention/tests/scaled_dot_attention/test_history.py
new file mode 100644
index 000000000..62a429615
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/scaled_dot_attention/test_history.py
@@ -0,0 +1,59 @@
+import numpy as np
+import pytest
+
+from sktime.libs._keras_self_attention import ScaledDotProductAttention
+from sktime.tests.test_switch import run_test_module_changed
+from sktime.utils.dependencies import _check_soft_dependencies, _safe_import
+
+keras = _safe_import("tensorflow.keras")
+
+
+@pytest.mark.skipif(
+    not run_test_module_changed("sktime.libs._keras_self_attention")
+    or not _check_soft_dependencies("tensorflow", severity="none"),
+    reason="Execute tests for iff anything in the module has changed",
+)
+class TestHistory:
+    def test_history(self):
+        input_layer = keras.layers.Input(
+            shape=(5,),
+            name="Input",
+        )
+        embed_layer = keras.layers.Embedding(
+            input_dim=4,
+            output_dim=5,
+            mask_zero=True,
+            weights=[
+                np.asarray(
+                    [
+                        [0.1, 0.2, 0.3, 0.4, 0.5],
+                        [0.2, 0.3, 0.4, 0.6, 0.5],
+                        [0.4, 0.7, 0.2, 0.6, 0.9],
+                        [0.3, 0.5, 0.8, 0.9, 0.1],
+                    ]
+                ),
+            ],
+            name="Embedding",
+        )(input_layer)
+        att_layer, att_weights = ScaledDotProductAttention(
+            history_only=True,
+            return_attention=True,
+            name="Attention",
+        )([embed_layer, embed_layer, embed_layer])
+        model = keras.models.Model(inputs=input_layer, outputs=[att_layer, att_weights])
+        model.compile(optimizer="adam", loss="mse")
+        model.summary()
+        inputs = np.array([[1, 2, 3, 1, 0]])
+        predicts = model.predict(inputs)
+        results, weights = predicts[0][0], predicts[1][0]
+        assert not np.allclose(results[0], results[3])
+        assert np.allclose(
+            np.asarray([0.2, 0.3, 0.4, 0.6, 0.5]),
+            results[0],
+        ), results[0]
+        for i in range(4):
+            for j in range(5):
+                if j > i:
+                    assert weights[i][j] == 0.0
+                else:
+                    assert weights[i][j] > 0.0
diff --git a/sktime/libs/_keras_self_attention/tests/scaled_dot_attention/test_real_former.py b/sktime/libs/_keras_self_attention/tests/scaled_dot_attention/test_real_former.py
new file mode 100644
index 000000000..37c68d3ef
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/scaled_dot_attention/test_real_former.py
@@ -0,0 +1,118 @@
+import os
+import tempfile
+
+import numpy as np
+import pytest
+
+from sktime.libs._keras_self_attention import ResidualScaledDotProductAttention
+from sktime.tests.test_switch import run_test_module_changed
+from sktime.utils.dependencies import _check_soft_dependencies, _safe_import
+
+keras = _safe_import("tensorflow.keras")
+
+
+@pytest.mark.skipif(
+    not run_test_module_changed("sktime.libs._keras_self_attention")
+    or not _check_soft_dependencies("tensorflow", severity="none"),
+    reason="Execute tests for iff anything in the module has changed",
+)
+class TestResidualScaledDotProductAttention:
+    @pytest.mark.xfail(reason="Unknown failure reason - TODO investigate")
+    def test_history(self):
+        input_layer = keras.layers.Input(
+            shape=(5,),
+            name="Input",
+        )
+        prev_layer = keras.layers.Input(
+            shape=(5, 5),
+            name="Prev",
+        )
+        embed_layer = keras.layers.Embedding(
+            input_dim=4,
+            output_dim=5,
+            mask_zero=True,
+            weights=[
+                np.asarray(
+                    [
+                        [0.1, 0.2, 0.3, 0.4, 0.5],
+                        [0.2, 0.3, 0.4, 0.6, 0.5],
+                        [0.4, 0.7, 0.2, 0.6, 0.9],
+                        [0.3, 0.5, 0.8, 0.9, 0.1],
+                    ]
+                ),
+            ],
+            name="Embedding",
+        )(input_layer)
+        att_layer, _, att_weights = ResidualScaledDotProductAttention(
+            history_only=True,
+            return_attention=True,
+            name="Attention",
+        )([embed_layer, embed_layer, embed_layer, prev_layer])
+        model = keras.models.Model(
+            inputs=[input_layer, prev_layer], outputs=[att_layer, att_weights]
+        )
+        model.compile(optimizer="adam", loss="mse")
+        model_path = os.path.join(
+            tempfile.gettempdir(), "keras_self_att_test_sl_%f.h5" % np.random.random()
+        )
+        model.save(model_path)
+        model = keras.models.load_model(
+            model_path,
+            custom_objects={
+                "ResidualScaledDotProductAttention": ResidualScaledDotProductAttention,
+            },
+        )
+        inputs = np.array([[1, 2, 3, 1, 0]])
+        prev = np.zeros((1, 5, 5))
+        predicts = model.predict([inputs, prev])
+        results, weights = predicts[0][0], predicts[1][0]
+        assert not np.allclose(results[0], results[3])
+        assert not np.allclose(
+            np.asarray([0.2, 0.3, 0.4, 0.6, 0.5]),
+            results[0],
+        ), results[0]
+        for i in range(4):
+            for j in range(5):
+                if j > i:
+                    assert weights[i][j] == 0.0
+                else:
+                    assert 0.0 < weights[i][j] < 1.0
+
+    def test_sample(self):
+        input_layer = keras.layers.Input(
+            shape=(5,),
+            name="Input",
+        )
+        prev_layer = keras.layers.Input(
+            shape=(5, 5),
+            name="Prev",
+        )
+        embed_layer = keras.layers.Embedding(
+            input_dim=4,
+            output_dim=5,
+            mask_zero=True,
+            weights=[
+                np.array(
+                    [
+                        [0.1, 0.2, 0.3, 0.4, 0.5],
+                        [0.2, 0.3, 0.4, 0.6, 0.5],
+                        [0.4, 0.7, 0.2, 0.6, 0.9],
+                        [0.3, 0.5, 0.8, 0.9, 0.1],
+                    ]
+                ),
+            ],
+            name="Embedding",
+        )(input_layer)
+        att_layer, _ = ResidualScaledDotProductAttention(name="Attention")(
+            [embed_layer, prev_layer]
+        )
+        model = keras.models.Model(inputs=[input_layer, prev_layer], outputs=att_layer)
+        model.compile(optimizer="adam", loss="mse")
+        inputs = np.array([[1, 2, 3, 1, 0]])
+        prev = np.zeros((1, 5, 5))
+        predict = model.predict([inputs, prev])[0]
+        assert np.allclose(predict[0], predict[3])
+        assert np.allclose(
+            np.asarray([0.27883747, 0.45767492, 0.47448885, 0.69199574, 0.47368336]),
+            predict[2],
+        ), predict[2]
diff --git a/sktime/libs/_keras_self_attention/tests/scaled_dot_attention/test_sample.py b/sktime/libs/_keras_self_attention/tests/scaled_dot_attention/test_sample.py
new file mode 100644
index 000000000..031ec1022
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/scaled_dot_attention/test_sample.py
@@ -0,0 +1,48 @@
+import numpy as np
+import pytest
+
+from sktime.libs._keras_self_attention import ScaledDotProductAttention
+from sktime.tests.test_switch import run_test_module_changed
+from sktime.utils.dependencies import _check_soft_dependencies, _safe_import
+
+keras = _safe_import("tensorflow.keras")
+
+
+@pytest.mark.skipif(
+    not run_test_module_changed("sktime.libs._keras_self_attention")
+    or not _check_soft_dependencies("tensorflow", severity="none"),
+    reason="Execute tests for iff anything in the module has changed",
+)
+class TestAttention:
+    def test_sample(self):
+        input_layer = keras.layers.Input(
+            shape=(5,),
+            name="Input",
+        )
+        embed_layer = keras.layers.Embedding(
+            input_dim=4,
+            output_dim=5,
+            mask_zero=True,
+            weights=[
+                np.array(
+                    [
+                        [0.1, 0.2, 0.3, 0.4, 0.5],
+                        [0.2, 0.3, 0.4, 0.6, 0.5],
+                        [0.4, 0.7, 0.2, 0.6, 0.9],
+                        [0.3, 0.5, 0.8, 0.9, 0.1],
+                    ]
+                ),
+            ],
+            name="Embedding",
+        )(input_layer)
+        att_layer = ScaledDotProductAttention(name="Attention")(embed_layer)
+        model = keras.models.Model(inputs=input_layer, outputs=att_layer)
+        model.compile(optimizer="adam", loss="mse")
+        model.summary()
+        inputs = np.array([[1, 2, 3, 1, 0]])
+        predict = model.predict(inputs)[0]
+        assert np.allclose(predict[0], predict[3])
+        assert np.allclose(
+            np.asarray([0.27883747, 0.45767492, 0.47448885, 0.69199574, 0.47368336]),
+            predict[2],
+        ), predict[2]
diff --git a/sktime/libs/_keras_self_attention/tests/scaled_dot_attention/test_save_load.py b/sktime/libs/_keras_self_attention/tests/scaled_dot_attention/test_save_load.py
new file mode 100644
index 000000000..0a30ad962
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/scaled_dot_attention/test_save_load.py
@@ -0,0 +1,45 @@
+import os
+import tempfile
+
+import numpy as np
+import pytest
+
+from sktime.libs._keras_self_attention import ScaledDotProductAttention
+from sktime.tests.test_switch import run_test_module_changed
+from sktime.utils.dependencies import _check_soft_dependencies, _safe_import
+
+keras = _safe_import("tensorflow.keras")
+
+
+@pytest.mark.xfail(reason="Unknown failure reason - TODO investigate")
+@pytest.mark.skipif(
+    not run_test_module_changed("sktime.libs._keras_self_attention")
+    or not _check_soft_dependencies("tensorflow", severity="none"),
+    reason="Execute tests for iff anything in the module has changed",
+)
+class TestSaveLoad:
+    def test_save_load(self):
+        input_q = keras.layers.Input(shape=(5, 3), name="Input-Q")
+        input_k = keras.layers.Input(shape=(4, 3), name="Input-K")
+        input_v = keras.layers.Input(shape=(4, 6), name="Input-V")
+        attention, weights = ScaledDotProductAttention(
+            return_attention=True,
+            history_only=True,
+            name="Attention",
+        )([input_q, input_k, input_v])
+        model = keras.models.Model(
+            inputs=[input_q, input_k, input_v], outputs=[attention, weights]
+        )
+        model.compile(optimizer="adam", loss="mse")
+        model_path = os.path.join(
+            tempfile.gettempdir(), "keras_self_att_test_sl_%f.h5" % np.random.random()
+        )
+        model.save(model_path)
+        model = keras.models.load_model(
+            model_path,
+            custom_objects={
+                "ScaledDotProductAttention": ScaledDotProductAttention,
+            },
+        )
+        model.summary(line_length=120)
+        assert model is not None
diff --git a/sktime/libs/_keras_self_attention/tests/seq_self_attention/__init__.py b/sktime/libs/_keras_self_attention/tests/seq_self_attention/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_activation.py b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_activation.py
new file mode 100644
index 000000000..ba5aa311a
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_activation.py
@@ -0,0 +1,28 @@
+import pytest
+
+from sktime.libs._keras_self_attention import SeqSelfAttention
+from sktime.libs._keras_self_attention.tests.seq_self_attention.util import (
+    TestMaskShape,
+)
+from sktime.tests.test_switch import run_test_module_changed
+from sktime.utils.dependencies import _check_soft_dependencies, _safe_import
+
+keras = _safe_import("tensorflow.keras")
+
+
+@pytest.mark.skipif(
+    not run_test_module_changed("sktime.libs._keras_self_attention")
+    or not _check_soft_dependencies("tensorflow", severity="none"),
+    reason="Execute tests for iff anything in the module has changed",
+)
+class TestActivation(TestMaskShape):
+    def test_attention_activation(self):
+        attention = SeqSelfAttention(
+            return_attention=True,
+            attention_width=3,
+            kernel_regularizer=keras.regularizers.l2(1e-4),
+            bias_regularizer=keras.regularizers.l1(1e-4),
+            attention_activation="sigmoid",
+            name="Attention",
+        )
+        self.check_mask_shape(attention)
diff --git a/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_bias.py b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_bias.py
new file mode 100644
index 000000000..74bdefaea
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_bias.py
@@ -0,0 +1,30 @@
+import pytest
+
+from sktime.libs._keras_self_attention import SeqSelfAttention
+from sktime.libs._keras_self_attention.tests.seq_self_attention.util import (
+    TestMaskShape,
+)
+from sktime.tests.test_switch import run_test_module_changed
+from sktime.utils.dependencies import _check_soft_dependencies, _safe_import
+
+keras = _safe_import("tensorflow.keras")
+
+
+@pytest.mark.skipif(
+    not run_test_module_changed("sktime.libs._keras_self_attention")
+    or not _check_soft_dependencies("tensorflow", severity="none"),
+    reason="Execute tests for iff anything in the module has changed",
+)
+class TestBias(TestMaskShape):
+    def test_no_bias(self):
+        attention = SeqSelfAttention(
+            return_attention=True,
+            attention_width=3,
+            kernel_regularizer=keras.regularizers.l2(1e-4),
+            bias_regularizer=keras.regularizers.l1(1e-4),
+            use_additive_bias=False,
+            use_attention_bias=False,
+            attention_activation="relu",
+            name="Attention",
+        )
+        self.check_mask_shape(attention)
diff --git a/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_history.py b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_history.py
new file mode 100644
index 000000000..4d4d97919
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_history.py
@@ -0,0 +1,30 @@
+import pytest
+
+from sktime.libs._keras_self_attention import SeqSelfAttention
+from sktime.libs._keras_self_attention.tests.seq_self_attention.util import (
+    TestMaskShape,
+)
+from sktime.tests.test_switch import run_test_module_changed
+from sktime.utils.dependencies import _check_soft_dependencies
+
+
+@pytest.mark.skipif(
+    not run_test_module_changed("sktime.libs._keras_self_attention")
+    or not _check_soft_dependencies("tensorflow", severity="none"),
+    reason="Execute tests for iff anything in the module has changed",
+)
+class TestHistory(TestMaskShape):
+    def test_history(self):
+        attention = SeqSelfAttention(
+            return_attention=True,
+            attention_width=3,
+            history_only=True,
+            name="Attention",
+        )
+        self.check_mask_shape(attention)
+
+    def test_infinite_history(self):
+        attention = SeqSelfAttention(
+            return_attention=True, history_only=True, name="Attention"
+        )
+        self.check_mask_shape(attention)
diff --git a/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_local.py b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_local.py
new file mode 100644
index 000000000..50a553655
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_local.py
@@ -0,0 +1,34 @@
+import pytest
+
+from sktime.libs._keras_self_attention import SeqSelfAttention
+from sktime.libs._keras_self_attention.tests.seq_self_attention.util import (
+    TestMaskShape,
+)
+from sktime.tests.test_switch import run_test_module_changed
+from sktime.utils.dependencies import _check_soft_dependencies, _safe_import
+
+keras = _safe_import("tensorflow.keras")
+
+
+@pytest.mark.skipif(
+    not run_test_module_changed("sktime.libs._keras_self_attention")
+    or not _check_soft_dependencies("tensorflow", severity="none"),
+    reason="Execute tests for iff anything in the module has changed",
+)
+class TestLocal(TestMaskShape):
+    def check_local_range(self, attention_type):
+        attention = SeqSelfAttention(
+            return_attention=True,
+            attention_width=5,
+            attention_type=attention_type,
+            kernel_regularizer=keras.regularizers.l2(1e-4),
+            bias_regularizer=keras.regularizers.l1(1e-4),
+            name="Attention",
+        )
+        self.check_mask_shape(attention)
+
+    def test_add(self):
+        self.check_local_range(attention_type=SeqSelfAttention.ATTENTION_TYPE_ADD)
+
+    def test_mul(self):
+        self.check_local_range(attention_type=SeqSelfAttention.ATTENTION_TYPE_MUL)
diff --git a/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_loss.py b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_loss.py
new file mode 100644
index 000000000..d8e0fb98c
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_loss.py
@@ -0,0 +1,37 @@
+import numpy as np
+import pytest
+
+from sktime.libs._keras_self_attention import SeqSelfAttention
+from sktime.libs._keras_self_attention.tests.seq_self_attention.util import (
+    TestMaskShape,
+)
+from sktime.tests.test_switch import run_test_module_changed
+from sktime.utils.dependencies import _check_soft_dependencies, _safe_import
+
+keras = _safe_import("tensorflow.keras")
+
+
+@pytest.mark.skipif(
+    not run_test_module_changed("sktime.libs._keras_self_attention")
+    or not _check_soft_dependencies("tensorflow", severity="none"),
+    reason="Execute tests for iff anything in the module has changed",
+)
+class TestLoss(TestMaskShape):
+    def test_loss(self):
+        attention = SeqSelfAttention(
+            return_attention=False,
+            attention_type=SeqSelfAttention.ATTENTION_TYPE_MUL,
+            kernel_regularizer=keras.regularizers.l2(1e-6),
+            bias_regularizer=keras.regularizers.l1(1e-6),
+            attention_regularizer_weight=1e-4,
+            name="Attention",
+        )
+        sentences, input_data, token_dict = self.get_input_data()
+        model = self.get_model(attention, token_dict)
+        sentence_len = input_data.shape[1]
+        model.fit(
+            x=input_data,
+            y=np.zeros((len(sentences), sentence_len, 1)),
+            epochs=10,
+        )
+        assert model is not None
diff --git a/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_mask.py b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_mask.py
new file mode 100644
index 000000000..ce03d252e
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_mask.py
@@ -0,0 +1,26 @@
+import pytest
+
+from sktime.libs._keras_self_attention import SeqSelfAttention
+from sktime.libs._keras_self_attention.tests.seq_self_attention.util import (
+    TestMaskShape,
+)
+from sktime.tests.test_switch import run_test_module_changed
+from sktime.utils.dependencies import _check_soft_dependencies, _safe_import
+
+keras = _safe_import("tensorflow.keras")
+
+
+@pytest.mark.skipif(
+    not run_test_module_changed("sktime.libs._keras_self_attention")
+    or not _check_soft_dependencies("tensorflow", severity="none"),
+    reason="Execute tests for iff anything in the module has changed",
+)
+class TestMask(TestMaskShape):
+    def test_return_attention(self):
+        attention = SeqSelfAttention(
+            return_attention=True,
+            kernel_regularizer=keras.regularizers.l2(1e-4),
+            bias_regularizer=keras.regularizers.l1(1e-4),
+            name="Attention",
+        )
+        self.check_mask_shape(attention)
diff --git a/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_mul.py b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_mul.py
new file mode 100644
index 000000000..0cba4c828
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_mul.py
@@ -0,0 +1,32 @@
+import pytest
+
+from sktime.libs._keras_self_attention import SeqSelfAttention
+from sktime.libs._keras_self_attention.tests.seq_self_attention.util import (
+    TestMaskShape,
+)
+from sktime.tests.test_switch import run_test_module_changed
+from sktime.utils.dependencies import _check_soft_dependencies, _safe_import
+
+keras = _safe_import("tensorflow.keras")
+
+
+@pytest.mark.skipif(
+    not run_test_module_changed("sktime.libs._keras_self_attention")
+    or not _check_soft_dependencies("tensorflow", severity="none"),
+    reason="Execute tests for iff anything in the module has changed",
+)
+class TestMul(TestMaskShape):
+    def test_multiplicative(self):
+        attention = SeqSelfAttention(
+            return_attention=True,
+            attention_width=15,
+            attention_type=SeqSelfAttention.ATTENTION_TYPE_MUL,
+            kernel_regularizer=keras.regularizers.l2(1e-4),
+            bias_regularizer=keras.regularizers.l1(1e-4),
+            name="Attention",
+        )
+        self.check_mask_shape(attention)
+
+    def test_not_implemented(self):
+        with pytest.raises(NotImplementedError):
+            SeqSelfAttention(return_attention=True, attention_type="random")
diff --git a/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_save_load.py b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_save_load.py
new file mode 100644
index 000000000..d80c26e57
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/seq_self_attention/test_save_load.py
@@ -0,0 +1,59 @@
+import os
+import tempfile
+
+import numpy as np
+import pytest
+
+from sktime.libs._keras_self_attention import SeqSelfAttention
+from sktime.libs._keras_self_attention.tests.seq_self_attention.util import (
+    TestMaskShape,
+)
+from sktime.tests.test_switch import run_test_module_changed
+from sktime.utils.dependencies import _check_soft_dependencies, _safe_import
+
+keras = _safe_import("tensorflow.keras")
+
+
+@pytest.mark.xfail(reason="Unknown failure reason - TODO investigate")
+@pytest.mark.skipif(
+    not run_test_module_changed("sktime.libs._keras_self_attention")
+    or not _check_soft_dependencies("tensorflow", severity="none"),
+    reason="Execute tests for iff anything in the module has changed",
+)
+class TestSaveLoad(TestMaskShape):
+    def test_save_load(self):
+        _, _, token_dict = self.get_input_data()
+        model = self.get_model(SeqSelfAttention(name="Attention"), token_dict)
+        model_path = os.path.join(
+            tempfile.gettempdir(),
+            "keras_self_att_test_save_load_%f.h5" % np.random.random(),
+        )
+        model.save(model_path)
+        model = keras.models.load_model(
+            model_path, custom_objects={"SeqSelfAttention": SeqSelfAttention}
+        )
+        model.summary()
+        assert model is not None
+
+    def test_save_load_with_loss(self):
+        attention = SeqSelfAttention(
+            return_attention=True,
+            attention_width=7,
+            attention_type=SeqSelfAttention.ATTENTION_TYPE_MUL,
+            kernel_regularizer=keras.regularizers.l2(1e-4),
+            bias_regularizer=keras.regularizers.l1(1e-4),
+            attention_regularizer_weight=1e-3,
+            name="Attention",
+        )
+        _, _, token_dict = self.get_input_data()
+        model = self.get_model(attention, token_dict)
+        model_path = os.path.join(
+            tempfile.gettempdir(),
+            "keras_self_att_test_sl_with_loss_%f.h5" % np.random.random(),
+        )
+        model.save(model_path)
+        model = keras.models.load_model(
+            model_path, custom_objects=SeqSelfAttention.get_custom_objects()
+        )
+        model.summary()
+        assert model is not None
diff --git a/sktime/libs/_keras_self_attention/tests/seq_self_attention/util.py b/sktime/libs/_keras_self_attention/tests/seq_self_attention/util.py
new file mode 100644
index 000000000..f6f945d45
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/seq_self_attention/util.py
@@ -0,0 +1,83 @@
+import numpy as np
+import pytest
+
+from sktime.tests.test_switch import run_test_module_changed
+from sktime.utils.dependencies import _check_soft_dependencies, _safe_import
+
+keras = _safe_import("tensorflow.keras")
+
+
+@pytest.mark.skipif(
+    not run_test_module_changed("sktime.libs._keras_self_attention")
+    or not _check_soft_dependencies("tensorflow", severity="none"),
+    reason="Execute tests for iff anything in the module has changed",
+)
+class TestMaskShape:
+    @staticmethod
+    def get_input_data():
+        sentences = [
+            ["All", "work", "and", "no", "play"],
+            ["makes", "Jack", "a", "dull", "boy", "."],
+            ["From", "that", "day", "forth", "my", "arm", "changed"],
+        ]
+        token_dict = {
+            "": 0,
+            "<UNK>": 1,
+        }
+        sentence_len = max(map(len, sentences))
+        input_data = [[0] * sentence_len for _ in range(len(sentences))]
+        for i, sentence in enumerate(sentences):
+            for j, token in enumerate(sentence):
+                if token in token_dict:
+                    input_data[i][j] = token_dict[token]
+                elif np.random.randint(0, 5) == 0:
+                    input_data[i][j] = token_dict["<UNK>"]
+                else:
+                    input_data[i][j] = len(token_dict)
+                    token_dict[token] = len(token_dict)
+        return sentences, np.asarray(input_data), token_dict
+
+    @staticmethod
+    def get_model(attention, token_dict):
+        inputs = keras.layers.Input(shape=(None,), name="Input")
+        embd = keras.layers.Embedding(
+            input_dim=len(token_dict), output_dim=16, mask_zero=True, name="Embedding"
+        )(inputs)
+        lstm = keras.layers.Bidirectional(
+            keras.layers.LSTM(units=16, return_sequences=True), name="Bi-LSTM"
+        )(embd)
+        if attention.return_attention:
+            att, weights = attention(lstm)
+        else:
+            att = attention(lstm)
+        dense = keras.layers.Dense(units=5, name="Dense")(att)
+        loss = {"Dense": "sparse_categorical_crossentropy"}
+        if attention.return_attention:
+            model = keras.models.Model(inputs=inputs, outputs=[dense, weights])
+            loss[attention.name] = "mse"
+        else:
+            model = keras.models.Model(inputs=inputs, outputs=dense)
+        model.compile(optimizer="adam", loss=loss)
+        model.summary(line_length=100)
+        return model
+
+    def check_mask_shape(self, attention):
+        sentences, input_data, token_dict = self.get_input_data()
+        model = self.get_model(attention, token_dict)
+        outputs = model.predict(input_data)
+        if attention.attention_width is None:
+            attention_width = 1e9
+        else:
+            attention_width = attention.attention_width
+        history_only = attention.history_only
+        attention_output = outputs[1]
+        for i, sentence in enumerate(sentences):
+            for j in range(len(sentence)):
+                for k in range(len(sentence)):
+                    if history_only and 0 <= j - k < attention_width:
+                        assert attention_output[i][j][k] > 0.0
+                    elif not history_only and abs(j - k) <= attention_width // 2:
+                        assert attention_output[i][j][k] > 0.0
+                    else:
+                        assert attention_output[i][j][k] == 0.0
+                assert abs(np.sum(attention_output[i][j]) - 1.0) < 1e-6
diff --git a/sktime/libs/_keras_self_attention/tests/seq_weighted_attention/__init__.py b/sktime/libs/_keras_self_attention/tests/seq_weighted_attention/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/sktime/libs/_keras_self_attention/tests/seq_weighted_attention/test_save_load.py b/sktime/libs/_keras_self_attention/tests/seq_weighted_attention/test_save_load.py
new file mode 100644
index 000000000..9f2a68aa6
--- /dev/null
+++ b/sktime/libs/_keras_self_attention/tests/seq_weighted_attention/test_save_load.py
@@ -0,0 +1,62 @@
+import os
+import tempfile
+
+import numpy as np
+import pytest
+
+from sktime.libs._keras_self_attention import SeqWeightedAttention as Attention
+from sktime.tests.test_switch import run_test_module_changed
+from sktime.utils.dependencies import _check_soft_dependencies, _safe_import
+
+keras = _safe_import("tensorflow.keras")
+
+
+@pytest.mark.xfail(reason="Unknown failure reason - TODO investigate")
+@pytest.mark.skipif(
+    not run_test_module_changed("sktime.libs._keras_self_attention")
+    or not _check_soft_dependencies("tensorflow", severity="none"),
+    reason="Execute tests for iff anything in the module has changed",
+)
+class TestSaveLoad:
+    def _test_save_load(self, attention):
+        inputs = keras.layers.Input(shape=(None,), name="Input")
+        embd = keras.layers.Embedding(
+            input_dim=3, output_dim=5, mask_zero=True, name="Embedding"
+        )(inputs)
+        lstm = keras.layers.Bidirectional(
+            keras.layers.LSTM(units=7, return_sequences=True), name="Bi-LSTM"
+        )(embd)
+        if attention.return_attention:
+            layer, weights = attention(lstm)
+        else:
+            layer = attention(lstm)
+        dense = keras.layers.Dense(units=2, activation="softmax", name="Softmax")(layer)
+        loss = {"Softmax": "sparse_categorical_crossentropy"}
+        if attention.return_attention:
+            outputs = [dense, weights]
+            loss[attention.name] = "mse"
+        else:
+            outputs = dense
+        model = keras.models.Model(inputs=inputs, outputs=outputs)
+        model.compile(optimizer="adam", loss=loss)
+        model_path = os.path.join(
+            tempfile.gettempdir(),
+            "keras_weighted_att_test_sl_%f.h5" % np.random.random(),
+        )
+        model.save(model_path)
+        model = keras.models.load_model(
+            model_path, custom_objects=Attention.get_custom_objects()
+        )
+        model.summary(line_length=100)
+        if attention.return_attention:
+            assert len(model.outputs) == 2
+        else:
+            assert len(model.outputs) == 1
+
+    def test_default(self):
+        self._test_save_load(Attention(name="Attention"))
+
+    def test_return_attention(self):
+        self._test_save_load(
+            Attention(return_attention=True, use_bias=False, name="Attention")
+        )
diff --git a/sktime/registry/tests/test_lookup.py b/sktime/registry/tests/test_lookup.py
index 9b20de9a9..7e8c07606 100644
--- a/sktime/registry/tests/test_lookup.py
+++ b/sktime/registry/tests/test_lookup.py
@@ -216,7 +216,9 @@ def test_all_estimators_return_tags_bad_arg(return_tags):
         _ = all_estimators(return_tags=return_tags)
 
 
-@pytest.mark.parametrize("tag_name", ["capability:pred_int", "handles-missing-data"])
+@pytest.mark.parametrize(
+    "tag_name", ["capability:pred_int", "capability:missing_values"]
+)
 @pytest.mark.parametrize("tag_value", [True, False])
 def test_all_estimators_tag_filter(tag_value, tag_name):
     """Test that tag filtering returns estimators as expected."""
diff --git a/sktime/transformations/tests/test_check_estimator.py b/sktime/transformations/tests/test_check_estimator.py
index 9e6c61bb6..750197a7c 100644
--- a/sktime/transformations/tests/test_check_estimator.py
+++ b/sktime/transformations/tests/test_check_estimator.py
@@ -41,7 +41,7 @@ class _TransformChangeNInstances(BaseTransformer):
         # can the transformer handle unequal length time series (if passed Panel)?
         "capability:unequal_length:removes": False,
         # is transform result always guaranteed to be equal length (and series)?
-        "handles-missing-data": False,  # can estimator handle missing data?
+        "capability:missing_values": False,  # can estimator handle missing data?
         # todo: rename to capability:missing_values
         "capability:missing_values:removes": False,
         # is transform result always guaranteed to contain no missing values?
diff --git a/sktime/transformations/tests/test_compose.py b/sktime/transformations/tests/test_compose.py
index 17530c23e..8eb392b77 100644
--- a/sktime/transformations/tests/test_compose.py
+++ b/sktime/transformations/tests/test_compose.py
@@ -157,9 +157,9 @@ def test_missing_unequal_tag_inference():
     assert t1.get_tag("capability:unequal_length")
     assert t1.get_tag("capability:unequal_length:removes")
     assert not t2.get_tag("capability:unequal_length:removes")
-    assert t3.get_tag("handles-missing-data")
+    assert t3.get_tag("capability:missing_values")
     assert t3.get_tag("capability:missing_values:removes")
-    assert not t4.get_tag("handles-missing-data")
+    assert not t4.get_tag("capability:missing_values")
     assert not t4.get_tag("capability:missing_values:removes")