Repository: aeon-toolkit/aeon
Pull request #2150 (2024-10-05 23:11:04)

[ENH] More `BaseEstimator` method and attribute changes
- replaces `_is_fitted` and the `is_fitted` property with just an `is_fitted` attribute
- requires `_tags_dynamic` be present (I do not know why it would be gone, but there are checks for some reason)
- requires a `parameter_set` argument for `get_test_params` method
- removes `create_test_instances_and_names` method (no usages, cant see why it would be useful for users)
- removes `is_composite` method. I don't know why you would want this, but it should be a tag if it stays
- adds some overloads for `sklearn` private methods to make them more compatible with our framework (or raise a better error where there is no compatibility)

aeon-actions-bot[bot]: 
## Thank you for contributing to `aeon`


This PR changes too many different packages (>3) for automatic addition of labels, please manually add package labels if relevant.

The [Checks](https://github.com/aeon-toolkit/aeon/pull/2150/checks) tab will show the status of our automated tests. You can click on individual test runs in the tab or "Details" in the panel below to see more information if there is a failure.

If our `pre-commit` code quality check fails, any trivial fixes will automatically be pushed to your PR unless it is a draft.

Don't hesitate to ask questions on the `aeon` [Slack](https://join.slack.com/t/aeon-toolkit/shared_invite/zt-22vwvut29-HDpCu~7VBUozyfL_8j3dLA) channel if you have any.

### PR CI actions

These checkboxes will add labels to enable/disable CI functionality for this PR. This may not take effect immediately, and a new commit may be required to run the new configuration.

- [ ] Run `pre-commit` checks for all files
- [ ] Run `mypy` typecheck tests
- [ ] Run all `pytest` tests and configurations
- [ ] Run all notebook example tests
- [ ] Run numba-disabled `codecov` tests
- [ ] Stop automatic `pre-commit` fixes (always disabled for drafts)
- [ ] Disable numba cache loading
- [ ] Push an empty commit to re-run CI checks
    

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 265e81c2..4a647e75 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.6.0
+    rev: v5.0.0
     hooks:
       - id: check-ast
       - id: check-added-large-files
@@ -29,7 +29,7 @@ repos:
         args: [ "--create", "--python-folders", "aeon" ]
 
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.6.8
+    rev: v0.6.9
     hooks:
       - id: ruff
         args: [ "--fix"]
diff --git a/aeon/anomaly_detection/_left_stampi.py b/aeon/anomaly_detection/_left_stampi.py
index 38c08f2b..a6980dc9 100644
--- a/aeon/anomaly_detection/_left_stampi.py
+++ b/aeon/anomaly_detection/_left_stampi.py
@@ -77,7 +77,7 @@ class LeftSTAMPi(BaseAnomalyDetector):
         "capability:multivariate": False,
         "capability:missing_values": False,
         "fit_is_empty": False,
-        "cant-pickle": True,
+        "cant_pickle": True,
         "python_dependencies": ["stumpy"],
     }
 
diff --git a/aeon/anomaly_detection/base.py b/aeon/anomaly_detection/base.py
index 9d6896c5..b5b0acb0 100644
--- a/aeon/anomaly_detection/base.py
+++ b/aeon/anomaly_detection/base.py
@@ -82,8 +82,6 @@ class BaseAnomalyDetector(BaseSeriesEstimator, ABC):
     }
 
     def __init__(self, axis):
-        self._is_fitted = False
-
         super().__init__(axis=axis)
 
     @final
@@ -118,7 +116,7 @@ class BaseAnomalyDetector(BaseSeriesEstimator, ABC):
             The fitted estimator, reference to self.
         """
         if self.get_class_tag("fit_is_empty"):
-            self._is_fitted = True
+            self.is_fitted = True
             return self
 
         if self.get_class_tag("requires_y"):
@@ -135,7 +133,7 @@ class BaseAnomalyDetector(BaseSeriesEstimator, ABC):
         self._fit(X=X, y=y)
 
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return self
 
     @final
@@ -206,7 +204,7 @@ class BaseAnomalyDetector(BaseSeriesEstimator, ABC):
         X = self._preprocess_series(X, axis, True)
 
         if self.get_class_tag("fit_is_empty"):
-            self._is_fitted = True
+            self.is_fitted = True
             return self._predict(X)
 
         if y is not None:
@@ -215,7 +213,7 @@ class BaseAnomalyDetector(BaseSeriesEstimator, ABC):
         pred = self._fit_predict(X, y)
 
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return pred
 
     def _fit(self, X, y):
diff --git a/aeon/base/_base.py b/aeon/base/_base.py
index 80324d34..f25134c9 100644
--- a/aeon/base/_base.py
+++ b/aeon/base/_base.py
@@ -1,37 +1,4 @@
-"""
-Base class template for estimators.
-
-Interface specifications below.
-
----
-
-Parameter inspection and setter methods
-    inspect parameter values      - get_params()
-    setting parameter values      - set_params(**params)
-    fitted parameter inspection - get_fitted_params()
-
-Tag inspection and setter methods
-    inspect tags (all)            - get_tags()
-    inspect tags (one tag)        - get_tag(tag_name: str, tag_value_default=None)
-    inspect tags (class method)   - get_class_tags()
-    inspect tags (one tag, class) - get_class_tag(tag_name:str, tag_value_default=None)
-    setting dynamic tags          - set_tags(**tag_dict: dict)
-    set/clone dynamic tags        - clone_tags(estimator, tag_names=None)
-
-Blueprinting: resetting and cloning, post-init state with same hyper-parameters
-    reset estimator to post-init  - reset()
-    cloneestimator (copy&reset)   - clone()
-
-Testing with default parameters methods
-    getting default parameters (all sets)         - get_test_params()
-    get one test instance with default parameters - create_test_instance()
-    get list of all test instances plus name list - create_test_instances_and_names()
-
-State:
-    fitted model/strategy   - by convention, any attributes ending in "_"
-    fitted state flag       - is_fitted (property)
-    fitted state check      - check_is_fitted (raises error if not is_fitted)
-"""
+"""Base class template for aeon estimators."""
 
 __maintainer__ = ["MatthewMiddlehurst", "TonyBagnall"]
 __all__ = ["BaseEstimator"]
@@ -46,40 +13,72 @@ from sklearn.exceptions import NotFittedError
 
 
 class BaseEstimator(_BaseEstimator):
-    """Base class for defining estimators in aeon."""
+    """
+    Base class for defining estimators in aeon.
+
+    Contains the following methods:
+
+    reset estimator to post-init  - reset(keep)
+    clonee stimator (copy)        - clone(random_state)
+    inspect tags (class method)   - get_class_tags()
+    inspect tags (one tag, class) - get_class_tag(tag_name, tag_value_default,
+                                                                    raise_error)
+    inspect tags (all)            - get_tags()
+    inspect tags (one tag)        - get_tag(tag_name, tag_value_default, raise_error)
+    setting dynamic tags          - set_tags(**tag_dict)
+
+    All estimators have the attribute:
+
+    fitted state flag             - is_fitted
+    """
 
     _tags = {
         "python_version": None,
         "python_dependencies": None,
-        "cant-pickle": False,
-        "non-deterministic": False,
+        "cant_pickle": False,
+        "non_deterministic": False,
         "algorithm_type": None,
         "capability:missing_values": False,
         "capability:multithreading": False,
     }
 
     def __init__(self):
-        self._is_fitted = False  # flag to indicate if fit has been called
+        self.is_fitted = False  # flag to indicate if fit has been called
         self._tags_dynamic = dict()  # storage for dynamic tags
 
         super().__init__()
 
     def reset(self, keep=None):
-        """Reset the object to a clean post-init state.
+        """
+        Reset the object to a clean post-init state.
 
-        Equivalent to sklearn.clone but overwrites self.
-        After ``self.reset()`` call, self is equal in value to
-        ``type(self)(**self.get_params(deep=False))``
+        After a ``self.reset()`` call, self is equal or similar in value to
+        ``type(self)(**self.get_params(deep=False))``, assuming no other attributes
+        were kept using ``keep``.
 
-        Detail behaviour:
-        removes any object attributes, except:
-            hyper-parameters = arguments of ``__init__``
-            object attributes containing double-underscores, i.e., the string "__"
-        runs ``__init__`` with current values of hyper-parameters (result of get_params)
+        Detailed behaviour:
+            removes any object attributes, except:
+                hyper-parameters (arguments of ``__init__``)
+                object attributes containing double-underscores, i.e., the string "__"
+            runs ``__init__`` with current values of hyperparameters (result of
+            get_params)
 
         Not affected by the reset are:
-        object attributes containing double-underscores
-        class and object methods, class attributes
+            object attributes containing double-underscores
+            class and object methods, class attributes
+            any attributes specified in the ``keep`` argument
+
+        Parameters
+        ----------
+        keep : None, str, or list of str, default=None
+            If None, all attributes are removed except hyper-parameters.
+            If str, only the attribute with this name is kept.
+            If list of str, only the attributes with these names are kept.
+
+        Returns
+        -------
+        self
+            Reference to self.
         """
         # retrieve parameters to copy them later
         params = self.get_params(deep=False)
@@ -111,15 +110,23 @@ class BaseEstimator(_BaseEstimator):
 
     def clone(self, random_state=None):
         """
-        Obtain a clone of the object with same hyper-parameters.
+        Obtain a clone of the object with the same hyperparameters.
 
         A clone is a different object without shared references, in post-init state.
-        This function is equivalent to returning sklearn.clone of self.
+        This function is equivalent to returning ``sklearn.clone`` of self.
         Equal in value to ``type(self)(**self.get_params(deep=False))``.
 
+        Parameters
+        ----------
+        random_state : int, RandomState instance, or None, default=None
+            Sets the random state of the clone. If None, the random state is not set.
+            If int, random_state is the seed used by the random number generator.
+            If RandomState instance, random_state is the random number generator.
+
         Returns
         -------
-        instance of ``type(self)``, clone of self (see above)
+        estimator : object
+            Instance of ``type(self)``, clone of self (see above)
         """
         estimator = clone(self)
 
@@ -136,19 +143,18 @@ class BaseEstimator(_BaseEstimator):
         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.
+            Dictionary of tag name and tag value pairs.
+            Collected from ``_tags`` class attribute via nested inheritance.
+            These are not overridden by dynamic tags set by ``set_tags`` or class
+            ``__init__`` calls.
         """
         collected_tags = dict()
 
         # We exclude the last two parent classes: sklearn.base.BaseEstimator and
         # the basic Python object.
         for parent_class in reversed(inspect.getmro(cls)[:-2]):
+            # Need the if here because classes might not have non-default tags
             if hasattr(parent_class, "_tags"):
-                # Need the if here because mixins might not have _more_tags
-                # but might do redundant work in estimators
-                # (i.e. calling more tags on BaseEstimator multiple times)
                 more_tags = parent_class._tags
                 collected_tags.update(more_tags)
 
@@ -170,20 +176,16 @@ class BaseEstimator(_BaseEstimator):
 
         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`.
+        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()
-
-        See Also
-        --------
-        get_tag : Get a single tag from an object.
-        get_tags : Get all tags from an object.
-        get_class_tag : Get a single tag from a class.
+        ValueError
+            if raise_error is ``True`` and ``tag_name`` is not in
+            ``self.get_tags().keys()``
 
         Examples
         --------
@@ -202,41 +204,26 @@ class BaseEstimator(_BaseEstimator):
 
     def get_tags(self):
         """
-        Get tags from estimator class.
+        Get tags from estimator.
 
-        Includes the dynamic tag overrides.
+        Includes dynamic and overridden tags.
 
         Returns
         -------
-        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.
-
-        See Also
-        --------
-        get_tag : Get a single tag from an object.
-        get_class_tags : Get all tags from a class.
-        get_class_tag : Get a single tag from a class.
-
-        Examples
-        --------
-        >>> from aeon.classification import DummyClassifier
-        >>> d = DummyClassifier()
-        >>> tags = d.get_tags()
+        collected_tags : dict
+            Dictionary of tag name and tag value pairs.
+            Collected from ``_tags`` class attribute via nested inheritance and
+            then any overridden and new tags from ``__init__`` or ``set_tags``.
         """
         collected_tags = self.get_class_tags()
-
-        if hasattr(self, "_tags_dynamic"):
-            collected_tags.update(self._tags_dynamic)
-
+        collected_tags.update(self._tags_dynamic)
         return deepcopy(collected_tags)
 
     def get_tag(self, tag_name, tag_value_default=None, raise_error=True):
         """
         Get tag value from estimator class.
 
-        Uses dynamic tag overrides.
+        Includes dynamic and overridden tags.
 
         Parameters
         ----------
@@ -249,20 +236,16 @@ class BaseEstimator(_BaseEstimator):
 
         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`.
+        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()
-
-        See Also
-        --------
-        get_tags : Get all tags from an object.
-        get_clas_tags : Get all tags from a class.
-        get_class_tag : Get a single tag from a class.
+        ValueError
+            if raise_error is ``True`` and ``tag_name`` is not in
+            ``self.get_tags().keys()``
 
         Examples
         --------
@@ -287,24 +270,15 @@ class BaseEstimator(_BaseEstimator):
         Parameters
         ----------
         **tag_dict : dict
-            Dictionary of tag name : tag value pairs.
+            Dictionary of tag name and tag value pairs.
 
         Returns
         -------
-        Self :
+        self
             Reference to self.
-
-        Notes
-        -----
-        Changes object state by setting tag values in tag_dict as dynamic tags
-        in self.
         """
         tag_update = deepcopy(tag_dict)
-        if hasattr(self, "_tags_dynamic"):
-            self._tags_dynamic.update(tag_update)
-        else:
-            self._tags_dynamic = tag_update
-
+        self._tags_dynamic.update(tag_update)
         return self
 
     @classmethod
@@ -352,11 +326,7 @@ class BaseEstimator(_BaseEstimator):
             Instance of the class with default parameters. If return_first
             is False, returns list of instances.
         """
-        # todo, update all methods to use parameter_set and remove when done
-        if "parameter_set" in inspect.getfullargspec(cls.get_test_params).args:
-            params = cls.get_test_params(parameter_set=parameter_set)
-        else:
-            params = cls.get_test_params()
+        params = cls.get_test_params(parameter_set=parameter_set)
 
         if isinstance(params, list):
             if return_first:
@@ -369,113 +339,6 @@ class BaseEstimator(_BaseEstimator):
             else:
                 return [cls(**params)]
 
-    @classmethod
-    def create_test_instances_and_names(cls, parameter_set="default"):
-        """
-        Create list of all test instances and a list of names for them.
-
-        Parameters
-        ----------
-        parameter_set : str, default="default"
-            Name of the set of test parameters to return, for use in tests. If no
-            special parameters are defined for a value, will return `"default"` set.
-
-        Returns
-        -------
-        objs : list of instances of cls
-            i-th instance is cls(**cls.get_test_params()[i]).
-        names : list of str, same length as objs
-            i-th element is name of i-th instance of obj in tests
-            convention is {cls.__name__}-{i} if more than one instance
-            otherwise {cls.__name__}.
-        parameter_set : str, default="default"
-            Name of the set of test parameters to return, for use in tests. If no
-            special parameters are defined for a value, will return `"default"` set.
-        """
-        if "parameter_set" in inspect.getfullargspec(cls.get_test_params).args:
-            param_list = cls.get_test_params(parameter_set=parameter_set)
-        else:
-            param_list = cls.get_test_params()
-
-        objs = []
-        if not isinstance(param_list, (dict, list)):
-            raise RuntimeError(
-                f"Error in {cls.__name__}.get_test_params, "
-                "return must be param dict for class, or list thereof"
-            )
-        if isinstance(param_list, dict):
-            param_list = [param_list]
-        for params in param_list:
-            if not isinstance(params, dict):
-                raise RuntimeError(
-                    f"Error in {cls.__name__}.get_test_params, "
-                    "return must be param dict for class, or list thereof"
-                )
-            objs += [cls(**params)]
-
-        n_cases = len(param_list)
-        if n_cases > 1:
-            names = [cls.__name__ + "-" + str(i) for i in range(n_cases)]
-        else:
-            names = [cls.__name__]
-
-        return objs, names
-
-    @classmethod
-    def _has_implementation_of(cls, method):
-        """
-        Check if method has a concrete implementation in this class.
-
-        This assumes that having an implementation is equivalent to
-            one or more overrides of `method` in the method resolution order.
-
-        Parameters
-        ----------
-        method : str
-            name of method to check implementation of.
-
-        Returns
-        -------
-        bool, whether method has implementation in cls
-            True if cls.method has been overridden at least once in
-                the inheritance tree (according to method resolution order).
-        """
-        # walk through method resolution order and inspect methods
-        #   of classes and direct parents, "adjacent" classes in mro
-        mro = inspect.getmro(cls)
-        # collect all methods that are not none
-        methods = [getattr(c, method, None) for c in mro]
-        methods = [m for m in methods if m is not None]
-
-        for i in range(len(methods) - 1):
-            # the method has been overridden once iff
-            #  at least two of the methods collected are not equal
-            #  equivalently: some two adjacent methods are not equal
-            overridden = methods[i] != methods[i + 1]
-            if overridden:
-                return True
-
-        return False
-
-    def is_composite(self):
-        """
-        Check if the object is composite.
-
-        A composite object is an object which contains objects, as parameters.
-        Called on an instance, since this may differ by instance.
-
-        Returns
-        -------
-        composite: bool
-            Whether self contains a parameter which is BaseEstimator.
-        """
-        # walk through method resolution order and inspect methods
-        #   of classes and direct parents, "adjacent" classes in mro
-        params = self.get_params(deep=False)
-        composite = any(isinstance(x, BaseEstimator) for x in params.values())
-
-        return composite
-
     def _components(self, base_class=None):
         """
         Return references to all state changing BaseEstimator type attributes.
@@ -605,11 +468,6 @@ class BaseEstimator(_BaseEstimator):
         with ZipFile(serial, "r") as file:
             return pickle.loads(file.open("_obj").read())
 
-    @property
-    def is_fitted(self):
-        """Whether ``fit`` has been called."""
-        return self._is_fitted
-
     def check_is_fitted(self):
         """
         Check if the estimator has been fitted.
@@ -679,7 +537,7 @@ class BaseEstimator(_BaseEstimator):
         # add all nested parameters from components that are aeon BaseEstimator
         c_dict = self._components()
         for c, comp in c_dict.items():
-            if isinstance(comp, BaseEstimator) and comp._is_fitted:
+            if isinstance(comp, BaseEstimator) and comp.is_fitted:
                 c_f_params = comp.get_fitted_params()
                 c_f_params = {f"{sh(c)}__{k}": v for k, v in c_f_params.items()}
                 fitted_params.update(c_f_params)
@@ -745,8 +603,21 @@ class BaseEstimator(_BaseEstimator):
         """
         return self._get_fitted_params_default()
 
+    # override some sklearn private methods
+
+    def __sklearn_is_fitted__(self):
+        """Check fitted status and return a Boolean value."""
+        return self.is_fitted
+
+    def _validate_data(self, **kwargs):
+        """Sklearn data validation."""
+        raise NotImplementedError(
+            "aeon estimators do not have a _validate_data method."
+        )
+
 
 def _clone_estimator(base_estimator, random_state=None):
+    """Clone an estimator."""
     estimator = clone(base_estimator)
 
     if random_state is not None:
diff --git a/aeon/base/estimator/compose/collection_pipeline.py b/aeon/base/estimator/compose/collection_pipeline.py
index 0cac282a..a5708af7 100644
--- a/aeon/base/estimator/compose/collection_pipeline.py
+++ b/aeon/base/estimator/compose/collection_pipeline.py
@@ -111,9 +111,7 @@ class BaseCollectionPipeline(_HeterogenousMetaEstimator, BaseCollectionEstimator
             if (
                 isinstance(e[1], BaseEstimator)
                 and e[1].get_tag("capability:missing_values", False, raise_error=False)
-                and e[1].get_tag(
-                    "capability:missing_values:removes", False, raise_error=False
-                )
+                and e[1].get_tag("removes_missing_values", False, raise_error=False)
             ):
                 missing_rm_tag = True
                 break
@@ -142,9 +140,7 @@ class BaseCollectionPipeline(_HeterogenousMetaEstimator, BaseCollectionEstimator
                 isinstance(e[1], BaseEstimator)
                 and e[1].get_tag("capability:unequal_length", False, raise_error=False)
                 and (
-                    e[1].get_tag(
-                        "capability:unequal_length:removes", False, raise_error=False
-                    )
+                    e[1].get_tag("removes_unequal_length", False, raise_error=False)
                     or e[1].get_tag("output_data_type", raise_error=False) == "Tabular"
                 )
             ):
diff --git a/aeon/classification/base.py b/aeon/classification/base.py
index 81fcd74a..b43e5ec7 100644
--- a/aeon/classification/base.py
+++ b/aeon/classification/base.py
@@ -119,7 +119,7 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
 
         self.fit_time_ = int(round(time.time() * 1000)) - start
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return self
 
     @final
@@ -271,7 +271,7 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
             y_pred = self._fit_predict(X, y, **kwargs)
 
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return y_pred
 
     @final
@@ -340,7 +340,7 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
             y_proba = self._fit_predict_proba(X, y, **kwargs)
 
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return y_proba
 
     def score(
diff --git a/aeon/classification/deep_learning/_inception_time.py b/aeon/classification/deep_learning/_inception_time.py
index 6b377d56..90a9edd9 100644
--- a/aeon/classification/deep_learning/_inception_time.py
+++ b/aeon/classification/deep_learning/_inception_time.py
@@ -160,8 +160,8 @@ class InceptionTimeClassifier(BaseClassifier):
     _tags = {
         "python_dependencies": "tensorflow",
         "capability:multivariate": True,
-        "non-deterministic": True,
-        "cant-pickle": True,
+        "non_deterministic": True,
+        "cant_pickle": True,
         "algorithm_type": "deeplearning",
     }
 
diff --git a/aeon/classification/deep_learning/_lite_time.py b/aeon/classification/deep_learning/_lite_time.py
index f16e136d..e00abe79 100644
--- a/aeon/classification/deep_learning/_lite_time.py
+++ b/aeon/classification/deep_learning/_lite_time.py
@@ -110,8 +110,8 @@ class LITETimeClassifier(BaseClassifier):
     _tags = {
         "python_dependencies": "tensorflow",
         "capability:multivariate": True,
-        "non-deterministic": True,
-        "cant-pickle": True,
+        "non_deterministic": True,
+        "cant_pickle": True,
         "algorithm_type": "deeplearning",
     }
 
diff --git a/aeon/classification/deep_learning/base.py b/aeon/classification/deep_learning/base.py
index 837945ee..efa83ba6 100644
--- a/aeon/classification/deep_learning/base.py
+++ b/aeon/classification/deep_learning/base.py
@@ -42,8 +42,8 @@ class BaseDeepClassifier(BaseClassifier, ABC):
         "X_inner_type": "numpy3D",
         "capability:multivariate": True,
         "algorithm_type": "deeplearning",
-        "non-deterministic": True,
-        "cant-pickle": True,
+        "non_deterministic": True,
+        "cant_pickle": True,
         "python_dependencies": "tensorflow",
     }
 
@@ -184,7 +184,7 @@ class BaseDeepClassifier(BaseClassifier, ABC):
         import tensorflow as tf
 
         self.model_ = tf.keras.models.load_model(model_path)
-        self._is_fitted = True
+        self.is_fitted = True
 
         self.classes_ = classes
         self.n_classes_ = len(self.classes_)
diff --git a/aeon/classification/dictionary_based/_boss.py b/aeon/classification/dictionary_based/_boss.py
index b88b5e58..fb37e098 100644
--- a/aeon/classification/dictionary_based/_boss.py
+++ b/aeon/classification/dictionary_based/_boss.py
@@ -659,7 +659,7 @@ class IndividualBOSS(BaseClassifier):
         new_boss.classes_ = self.classes_
         new_boss._class_dictionary = self._class_dictionary
         new_boss.metadata_ = self.metadata_
-        new_boss._is_fitted = True
+        new_boss.is_fitted = True
 
         return new_boss
 
diff --git a/aeon/classification/dictionary_based/_mrsqm.py b/aeon/classification/dictionary_based/_mrsqm.py
index 3ebe00a0..ce793f54 100644
--- a/aeon/classification/dictionary_based/_mrsqm.py
+++ b/aeon/classification/dictionary_based/_mrsqm.py
@@ -73,7 +73,7 @@ class MrSQMClassifier(BaseClassifier):
     _tags = {
         "X_inner_type": "numpy3D",
         "algorithm_type": "dictionary",
-        "cant-pickle": True,
+        "cant_pickle": True,
         "python_dependencies": "mrsqm",
     }
 
diff --git a/aeon/classification/early_classification/base.py b/aeon/classification/early_classification/base.py
index 87449b7c..407954ea 100644
--- a/aeon/classification/early_classification/base.py
+++ b/aeon/classification/early_classification/base.py
@@ -120,7 +120,7 @@ class BaseEarlyClassifier(BaseCollectionEstimator, ABC):
         self._fit(X, y)
         self.fit_time_ = int(round(time.time() * 1000)) - start
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return self
 
     def predict(self, X) -> tuple[np.ndarray, np.ndarray]:
diff --git a/aeon/classification/hybrid/_rist.py b/aeon/classification/hybrid/_rist.py
index b991ba4f..3e85336e 100644
--- a/aeon/classification/hybrid/_rist.py
+++ b/aeon/classification/hybrid/_rist.py
@@ -142,7 +142,7 @@ class RISTClassifier(BaseRIST, BaseClassifier):
     }
 
     @classmethod
-    def get_test_params(cls, parameter_set=None):
+    def get_test_params(cls, parameter_set="default"):
         """Return unit test parameter settings for the estimator.
 
         Parameters
diff --git a/aeon/classification/shapelet_based/_ls.py b/aeon/classification/shapelet_based/_ls.py
index 1c667611..06cc9fbc 100644
--- a/aeon/classification/shapelet_based/_ls.py
+++ b/aeon/classification/shapelet_based/_ls.py
@@ -95,7 +95,7 @@ class LearningShapeletClassifier(BaseClassifier):
     _tags = {
         "capability:multivariate": True,
         "algorithm_type": "shapelet",
-        "cant-pickle": True,
+        "cant_pickle": True,
         "python_dependencies": ["tslearn", "tensorflow"],
     }
 
@@ -173,7 +173,7 @@ class LearningShapeletClassifier(BaseClassifier):
         array of shape=(n_ts, n_shapelets)
             Shapelet-Transform of the provided time series.
         """
-        if not self._is_fitted:
+        if not self.is_fitted:
             raise ValueError(
                 "You must fit the classifier before recovering the transform"
             )
@@ -197,7 +197,7 @@ class LearningShapeletClassifier(BaseClassifier):
         array of shape=(n_ts, n_shapelets)
             Location of the shapelet matches for the provided time series.
         """
-        if not self._is_fitted:
+        if not self.is_fitted:
             raise ValueError(
                 "You must fit the classifier before recovering the transform"
             )
diff --git a/aeon/classification/shapelet_based/_rdst.py b/aeon/classification/shapelet_based/_rdst.py
index 0c5c92bb..166ca100 100644
--- a/aeon/classification/shapelet_based/_rdst.py
+++ b/aeon/classification/shapelet_based/_rdst.py
@@ -132,7 +132,7 @@ class RDSTClassifier(BaseClassifier):
         "capability:unequal_length": True,
         "capability:multithreading": True,
         "X_inner_type": ["np-list", "numpy3D"],
-        "non-deterministic": True,  # due to random_state bug in MacOS #324
+        "non_deterministic": True,  # due to random_state bug in MacOS #324
         "algorithm_type": "shapelet",
     }
 
diff --git a/aeon/clustering/_k_means.py b/aeon/clustering/_k_means.py
index 54d05e8c..c947be4a 100644
--- a/aeon/clustering/_k_means.py
+++ b/aeon/clustering/_k_means.py
@@ -226,7 +226,6 @@ class TimeSeriesKMeans(BaseClusterer):
                     print("Resumed because of empty cluster")  # noqa: T001, T201
 
         if best_labels is None:
-            self._is_fitted = False
             raise ValueError(
                 "Unable to find a valid cluster configuration "
                 "with parameters specified (empty clusters kept "
diff --git a/aeon/clustering/base.py b/aeon/clustering/base.py
index 2cfbc9c2..0c9a6c56 100644
--- a/aeon/clustering/base.py
+++ b/aeon/clustering/base.py
@@ -63,7 +63,7 @@ class BaseClusterer(BaseCollectionEstimator, ABC):
         X = self._preprocess_collection(X)
         self._fit(X)
         self.fit_time_ = int(round(time.time() * 1000)) - _start_time
-        self._is_fitted = True
+        self.is_fitted = True
         return self
 
     @final
diff --git a/aeon/clustering/deep_learning/base.py b/aeon/clustering/deep_learning/base.py
index f0c7eba1..fc7b54c7 100644
--- a/aeon/clustering/deep_learning/base.py
+++ b/aeon/clustering/deep_learning/base.py
@@ -35,8 +35,8 @@ class BaseDeepClusterer(BaseClusterer, ABC):
         "X_inner_type": "numpy3D",
         "capability:multivariate": True,
         "algorithm_type": "deeplearning",
-        "non-deterministic": True,
-        "cant-pickle": True,
+        "non_deterministic": True,
+        "cant_pickle": True,
         "python_dependencies": "tensorflow",
     }
 
diff --git a/aeon/datasets/__init__.py b/aeon/datasets/__init__.py
index 52cc29cb..66c2cb06 100644
--- a/aeon/datasets/__init__.py
+++ b/aeon/datasets/__init__.py
@@ -16,7 +16,6 @@ __all__ = [
     "load_human_activity_segmentation_datasets",
     # Write functions
     "write_to_tsfile",
-    "write_to_tsf_file",
     "write_to_arff_file",
     "write_results_to_uea_format",
     # Single problem loaders
@@ -67,7 +66,6 @@ from aeon.datasets._data_loaders import (
 from aeon.datasets._data_writers import (
     write_results_to_uea_format,
     write_to_arff_file,
-    write_to_tsf_file,
     write_to_tsfile,
 )
 from aeon.datasets._dataframe_loaders import (
diff --git a/aeon/datasets/_data_writers.py b/aeon/datasets/_data_writers.py
index 4c4a7b11..3aa4ed50 100644
--- a/aeon/datasets/_data_writers.py
+++ b/aeon/datasets/_data_writers.py
@@ -107,7 +107,7 @@ def _write_data_to_tsfile(
     """
     # ensure data provided is a ndarray
     if not isinstance(X, np.ndarray) and not isinstance(X, list):
-        raise TypeError("Data provided must be a ndarray or a list")
+        raise ValueError("Data provided must be a ndarray or a list")
     class_labels = None
     if y is not None:
         # ensure number of cases is same as the class value list
@@ -245,201 +245,6 @@ def _write_header(
     return file
 
 
-def write_to_tsf_file(
-    X,
-    path,
-    y=None,
-    problem_name="sample_data.tsf",
-    header=None,
-    attribute=None,
-    frequency=None,
-    horizon=0,
-):
-    """Write an aeon collection of time series to text file in .tsf format.
-
-    Write metadata and data stored in aeon compatible data set to file.
-    A description of the tsf format is in examples/load_data.ipynb.
-
-    Parameters
-    ----------
-    X : pd.DataFrame, each cell a pd.Series
-        Collection of time series: univariate, multivariate, equal or unequal length.
-    path : string.
-        Location of the directory to write file
-    y: None or pd.Series, default = None
-        Response variable, discrete for classification, continuous for regression
-        None if clustering.
-    problem_name : string, default = "sample_data"
-        The file is written to <path>/<problem_name>/<problem_name>.tsf
-    header: string, default = None
-        Optional text at the top of the file that is ignored when loading.
-    """
-    if not (isinstance(X, pd.DataFrame)):
-        raise TypeError(f" Wrong input data type {type(X)} convert to pd.DataFrame")
-
-    # See if passed file name contains .tsf extension or not
-    split = problem_name.split(".")
-    if split[-1] != "tsf":
-        problem_name = problem_name + ".tsf"
-
-    _write_dataframe_to_tsf_file(
-        X,
-        path,
-        y=None,
-        problem_name=problem_name,
-        attribute=attribute,
-        frequency=frequency,
-        horizon=horizon,
-        comment=header,
-    )
-
-
-def _write_dataframe_to_tsf_file(
-    X,
-    path,
-    y=None,
-    problem_name="sample_data",
-    comment=None,
-    attribute=None,
-    frequency=None,
-    horizon=0,
-):
-    # ensure data provided is a dataframe
-    if not isinstance(X, pd.DataFrame):
-        raise ValueError(f"Data provided must be a DataFrame, passed a {type(X)}")
-    # See if passed file name contains .tsf extension or not
-    split = problem_name.split(".")
-    if split[-1] != "tsf":
-        problem_name = problem_name + ".tsf"
-    equal_length = not X.isnull().values.any()
-    missing = X.isnull().values.any()
-    if frequency is None:
-        frequency = calculate_frequency(X)
-    if attribute is None:
-        attribute = {"series_name": "string", "start_timestamp": "date"}
-
-    file = _write_header_tsf(
-        path,
-        problem_name=problem_name,
-        attribute=attribute,
-        equal_length=equal_length,
-        frequency=frequency,
-        horizon=horizon,
-        missing=missing,
-        comment=comment,
-    )
-
-    X = X.reset_index(drop=False, inplace=False)
-
-    n_cases, n_channels = X.shape
-
-    for j in range(1, n_channels):
-        column_name = X.columns[j]
-        file.write(f"{str(column_name)}:")
-
-        # Find the index of the first non-empty value in the column
-        first_non_empty_index = X.iloc[:, j].first_valid_index()
-        start_timestamp_index = None
-
-        if first_non_empty_index is not None:
-            start_timestamp_index = X.index[first_non_empty_index]
-            start_timestamp = X.iloc[start_timestamp_index, 0].strftime(
-                "%Y-%m-%d %H-%M-%S"
-            )
-        file.write(f"{str(start_timestamp)}:")
-
-        for i in range(start_timestamp_index, n_cases - 1):
-            series = X.iloc[i, j]
-            # Check if the value is NaN
-            if pd.notna(series):
-                series_str = str(series)
-            else:
-                series_str = "?"  # Replace NaN with a ?
-
-            # Write the series string to the file
-            file.write(f"{series_str},")
-
-        series = X.iloc[-1, j]
-        # Check if he value is NaN
-        if pd.notna(series):
-            series_str = str(series)
-        else:
-            series_str = "?"  # Replace NaN with a ?
-        # Write the series string to the file
-        file.write(f"{series_str}")
-
-        # Check if y is not None before accessing its elements
-        if y is not None:
-            file.write(f"{y[i]}\n")
-        else:
-            file.write("\n")  # Write a newline if y is None
-    file.close()
-
-
-def _write_header_tsf(
-    path,
-    problem_name,
-    attribute,
-    equal_length=True,
-    frequency=None,
-    horizon=0,
-    missing=False,
-    comment=None,
-):
-    if not os.path.exists(path):
-        os.makedirs(path)
-    # See if passed file name contains .tsf extension or not
-    split = problem_name.split(".")
-    if split[-1] != "tsf":
-        problem_name = problem_name + ".tsf"
-    load_path = f"{path}/{problem_name}"
-
-    file = open(load_path, "w")
-
-    if comment is not None:
-        file.write("\n# ".join(textwrap.wrap("# " + comment)))
-        file.write("\n")
-
-    file.write(f"@relation {str(split[0]).lower()}\n")
-    # Write attribute metadata for each column
-    if attribute is not None:
-        for attr in attribute:
-            file.write(f"@attribute {str(attr)} {str(attribute[attr])}\n")
-    file.write(f"@frequency {str(frequency).lower()}\n")
-    file.write(f"@horizon {str(horizon).lower()}\n")
-    file.write(f"@missing {str(missing).lower()}\n")
-    file.write(f"@equallength {str(equal_length).lower()}\n")
-    file.write("@data\n")
-
-    return file
-
-
-def calculate_frequency(df):
-    # Convert timestamps to DateTime format
-    df["Timestamp"] = pd.to_datetime(df.index)
-
-    # Calculate time differences
-    time_diffs = df["Timestamp"].diff().dropna()
-
-    # Calculate median time difference
-    median_diff = time_diffs.median()
-
-    # Determine frequency based on median time difference
-    if median_diff <= pd.Timedelta(days=1):
-        frequency = "daily"
-    elif median_diff <= pd.Timedelta(weeks=1):
-        frequency = "weekly"
-    elif median_diff <= pd.Timedelta(days=30):
-        frequency = "monthly"
-    elif median_diff <= pd.Timedelta(days=365):
-        frequency = "yearly"
-    else:
-        frequency = "other"  # You can define more granular frequencies as needed
-    df.drop("Timestamp", axis=1, inplace=True)
-
-    return frequency
-
-
 def write_to_arff_file(
     X,
     y,
diff --git a/aeon/distances/_shift_scale_invariant.py b/aeon/distances/_shift_scale_invariant.py
index 4c99182c..2a07beb2 100644
--- a/aeon/distances/_shift_scale_invariant.py
+++ b/aeon/distances/_shift_scale_invariant.py
@@ -132,7 +132,7 @@ def _univariate_shift_scale_invariant_distance(
     x: np.ndarray, y: np.ndarray, max_shift: int
 ) -> tuple[float, np.ndarray]:
     if np.array_equal(x, y):
-        return 0, y
+        return 0.0, y
     min_dist = _scale_d(x, y)
     best_shifted_y = y
 
diff --git a/aeon/regression/base.py b/aeon/regression/base.py
index ce3b1e34..f3e0be80 100644
--- a/aeon/regression/base.py
+++ b/aeon/regression/base.py
@@ -120,7 +120,7 @@ class BaseRegressor(BaseCollectionEstimator, ABC):
 
         self.fit_time_ = int(round(time.time() * 1000)) - start
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return self
 
     @final
@@ -209,7 +209,7 @@ class BaseRegressor(BaseCollectionEstimator, ABC):
         y_pred = self._fit_predict(X, y)
 
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return y_pred
 
     def score(self, X, y, metric="r2", metric_params=None) -> float:
diff --git a/aeon/regression/convolution_based/_mr_hydra.py b/aeon/regression/convolution_based/_mr_hydra.py
index bcb66d96..c360f437 100644
--- a/aeon/regression/convolution_based/_mr_hydra.py
+++ b/aeon/regression/convolution_based/_mr_hydra.py
@@ -61,7 +61,7 @@ class MultiRocketHydraRegressor(BaseRegressor):
         "capability:multithreading": True,
         "algorithm_type": "convolution",
         "python_dependencies": "torch",
-        "non-deterministic": True,  # todo, presumed issue with mutlirocket
+        "non_deterministic": True,  # todo, presumed issue with mutlirocket
     }
 
     def __init__(self, n_kernels=8, n_groups=64, n_jobs=1, random_state=None):
diff --git a/aeon/regression/deep_learning/_inception_time.py b/aeon/regression/deep_learning/_inception_time.py
index a5bd4595..19493ac6 100644
--- a/aeon/regression/deep_learning/_inception_time.py
+++ b/aeon/regression/deep_learning/_inception_time.py
@@ -165,8 +165,8 @@ class InceptionTimeRegressor(BaseRegressor):
     _tags = {
         "python_dependencies": "tensorflow",
         "capability:multivariate": True,
-        "non-deterministic": True,
-        "cant-pickle": True,
+        "non_deterministic": True,
+        "cant_pickle": True,
         "algorithm_type": "deeplearning",
     }
 
diff --git a/aeon/regression/deep_learning/_lite_time.py b/aeon/regression/deep_learning/_lite_time.py
index 5a2079df..0d010431 100644
--- a/aeon/regression/deep_learning/_lite_time.py
+++ b/aeon/regression/deep_learning/_lite_time.py
@@ -111,8 +111,8 @@ class LITETimeRegressor(BaseRegressor):
     _tags = {
         "python_dependencies": "tensorflow",
         "capability:multivariate": True,
-        "non-deterministic": True,
-        "cant-pickle": True,
+        "non_deterministic": True,
+        "cant_pickle": True,
         "algorithm_type": "deeplearning",
     }
 
diff --git a/aeon/regression/deep_learning/base.py b/aeon/regression/deep_learning/base.py
index 987ea890..bf12de8a 100644
--- a/aeon/regression/deep_learning/base.py
+++ b/aeon/regression/deep_learning/base.py
@@ -35,8 +35,8 @@ class BaseDeepRegressor(BaseRegressor, ABC):
         "X_inner_type": "numpy3D",
         "capability:multivariate": True,
         "algorithm_type": "deeplearning",
-        "non-deterministic": True,
-        "cant-pickle": True,
+        "non_deterministic": True,
+        "cant_pickle": True,
         "python_dependencies": "tensorflow",
     }
 
@@ -129,7 +129,7 @@ class BaseDeepRegressor(BaseRegressor, ABC):
         import tensorflow as tf
 
         self.model_ = tf.keras.models.load_model(model_path)
-        self._is_fitted = True
+        self.is_fitted = True
 
     def _get_model_checkpoint_callback(self, callbacks, file_path, file_name):
         import tensorflow as tf
diff --git a/aeon/regression/hybrid/_rist.py b/aeon/regression/hybrid/_rist.py
index 735a1321..cf17bbea 100644
--- a/aeon/regression/hybrid/_rist.py
+++ b/aeon/regression/hybrid/_rist.py
@@ -134,7 +134,7 @@ class RISTRegressor(BaseRIST, BaseRegressor):
     }
 
     @classmethod
-    def get_test_params(cls, parameter_set=None):
+    def get_test_params(cls, parameter_set="default"):
         """Return unit test parameter settings for the estimator.
 
         Parameters
diff --git a/aeon/regression/shapelet_based/_rdst.py b/aeon/regression/shapelet_based/_rdst.py
index 6e0ef58d..4bd5e53d 100644
--- a/aeon/regression/shapelet_based/_rdst.py
+++ b/aeon/regression/shapelet_based/_rdst.py
@@ -113,7 +113,7 @@ class RDSTRegressor(BaseRegressor):
         "capability:unequal_length": True,
         "capability:multithreading": True,
         "X_inner_type": ["np-list", "numpy3D"],
-        "non-deterministic": True,  # due to random_state bug in MacOS #324
+        "non_deterministic": True,  # due to random_state bug in MacOS #324
         "algorithm_type": "shapelet",
     }
 
diff --git a/aeon/segmentation/_eagglo.py b/aeon/segmentation/_eagglo.py
index 9605d579..8ffa22cc 100644
--- a/aeon/segmentation/_eagglo.py
+++ b/aeon/segmentation/_eagglo.py
@@ -350,7 +350,7 @@ class EAggloSegmenter(BaseSegmenter):
         )
 
     @classmethod
-    def get_test_params(cls) -> list[dict]:
+    def get_test_params(cls, parameter_set: str = "default") -> list[dict]:
         """Test parameters."""
         return [
             {"alpha": 1.0, "penalty": None},
diff --git a/aeon/segmentation/base.py b/aeon/segmentation/base.py
index dee4e21a..6c294fc3 100644
--- a/aeon/segmentation/base.py
+++ b/aeon/segmentation/base.py
@@ -72,7 +72,6 @@ class BaseSegmenter(BaseSeriesEstimator, ABC):
 
     def __init__(self, axis, n_segments=2):
         self.n_segments = n_segments
-        self._is_fitted = False
 
         super().__init__(axis=axis)
 
@@ -107,7 +106,7 @@ class BaseSegmenter(BaseSeriesEstimator, ABC):
             Fitted estimator
         """
         if self.get_class_tag("fit_is_empty"):
-            self._is_fitted = True
+            self.is_fitted = True
             return self
         if self.get_class_tag("requires_y"):
             if y is None:
@@ -120,7 +119,7 @@ class BaseSegmenter(BaseSeriesEstimator, ABC):
         if y is not None:
             y = self._check_y(y)
         self._fit(X=X, y=y)
-        self._is_fitted = True
+        self.is_fitted = True
         return self
 
     @final
diff --git a/aeon/transformations/base.py b/aeon/transformations/base.py
index bde6bc04..c6b53133 100644
--- a/aeon/transformations/base.py
+++ b/aeon/transformations/base.py
@@ -16,7 +16,7 @@ class BaseTransformer(BaseEstimator, ABC):
         "fit_is_empty": False,
         "capability:inverse_transform": False,
         "capability:missing_values": False,
-        "capability:missing_values:removes": False,
+        "removes_missing_values": False,
     }
 
     def __init__(self):
diff --git a/aeon/transformations/collection/_pad.py b/aeon/transformations/collection/_pad.py
index f7f66a63..f3e7f7de 100644
--- a/aeon/transformations/collection/_pad.py
+++ b/aeon/transformations/collection/_pad.py
@@ -53,7 +53,7 @@ class Padder(BaseCollectionTransformer):
         "fit_is_empty": False,
         "capability:multivariate": True,
         "capability:unequal_length": True,
-        "capability:unequal_length:removes": True,
+        "removes_unequal_length": True,
     }
 
     def __init__(self, pad_length=None, fill_value=0):
diff --git a/aeon/transformations/collection/_resize.py b/aeon/transformations/collection/_resize.py
index a622e7c0..c8189300 100644
--- a/aeon/transformations/collection/_resize.py
+++ b/aeon/transformations/collection/_resize.py
@@ -83,7 +83,7 @@ class Resizer(BaseCollectionTransformer):
         return np.array(Xt)
 
     @classmethod
-    def get_test_params(cls):
+    def get_test_params(cls, parameter_set="default"):
         """Return testing parameter settings for the estimator.
 
         Returns
diff --git a/aeon/transformations/collection/_truncate.py b/aeon/transformations/collection/_truncate.py
index ff90c5c2..8818b138 100644
--- a/aeon/transformations/collection/_truncate.py
+++ b/aeon/transformations/collection/_truncate.py
@@ -39,7 +39,7 @@ class Truncator(BaseCollectionTransformer):
         "X_inner_type": ["np-list", "numpy3D"],
         "capability:multivariate": True,
         "capability:unequal_length": True,
-        "capability:unequal_length:removes": True,
+        "removes_unequal_length": True,
     }
 
     def __init__(self, truncated_length=None):
diff --git a/aeon/transformations/collection/base.py b/aeon/transformations/collection/base.py
index edaa76be..10991aa7 100644
--- a/aeon/transformations/collection/base.py
+++ b/aeon/transformations/collection/base.py
@@ -43,7 +43,7 @@ class BaseCollectionTransformer(
     _tags = {
         "input_data_type": "Collection",
         "output_data_type": "Collection",
-        "capability:unequal_length:removes": False,
+        "removes_unequal_length": False,
     }
 
     def __init__(self):
@@ -93,7 +93,7 @@ class BaseCollectionTransformer(
                 raise ValueError("Tag requires_y is true, but fit called with y=None")
         # skip the rest if fit_is_empty is True
         if self.get_tag("fit_is_empty"):
-            self._is_fitted = True
+            self.is_fitted = True
             return self
         self.reset()
 
@@ -102,7 +102,7 @@ class BaseCollectionTransformer(
         y_inner = y
         self._fit(X=X_inner, y=y_inner)
 
-        self._is_fitted = True
+        self.is_fitted = True
 
         return self
 
@@ -203,7 +203,7 @@ class BaseCollectionTransformer(
         y_inner = y
         Xt = self._fit_transform(X=X_inner, y=y_inner)
 
-        self._is_fitted = True
+        self.is_fitted = True
 
         return Xt
 
diff --git a/aeon/transformations/collection/channel_selection/_channel_scorer.py b/aeon/transformations/collection/channel_selection/_channel_scorer.py
index 32c211dc..1559d7c5 100644
--- a/aeon/transformations/collection/channel_selection/_channel_scorer.py
+++ b/aeon/transformations/collection/channel_selection/_channel_scorer.py
@@ -1,30 +1,51 @@
+"""Selects channels based on an estimate of performance."""
+
+__maintainer__ = ["TonyBagnall"]
+__all__ = ["ChannelScorer"]
+
 import math
-from typing import Optional
+from typing import Callable
+from typing import Dict as TypingDict
+from typing import List as TypingList
+from typing import Optional, Union
 
 import numpy as np
-from sklearn.metrics import accuracy_score
+from sklearn.metrics import accuracy_score, mean_squared_error
 
+from aeon.base import BaseEstimator
 from aeon.classification.base import BaseClassifier
-from aeon.classification.convolution_based._rocket_classifier import RocketClassifier
+from aeon.regression.base import BaseRegressor
 from aeon.transformations.collection.channel_selection.base import BaseChannelSelector
 
-__maintainer__ = ["TonyBagnall"]
-__all__ = ["ChannelScorer"]
-
 
 class ChannelScorer(BaseChannelSelector):
-    """Channel scorer performs channel selection using a single channel classifier.
+    """Performs channel selection using a single channel classifier or regressor.
 
-    ChannelScorer uses a time series classifier to score each channel using an
-    estimate of accuracy on the training data, then selects a proportion of the top
+    ChannelScorer uses a time series classifier or a regressor to score each channel
+    using an estimate of accuracy on the training data fro classifier or mean
+    squared error for regressor, then selects a proportion of the top
     channels to keep. Can be configured through the constructor to use any time
-    series classifier and could easily be adapted to use forward selection or elbow
+    series estimator and could easily be adapted to use forward selection or elbow
     class methods. Approximately as described in [1]_.
 
     Parameters
     ----------
-    classifier, BaseClassifier, default = MiniROCKET
-    proportion : float, default = 0.2
+    estimator : BaseEstimator
+        The time series estimator used to score each channel.
+
+    scoring_function : Callable, optional (default=None)
+        Scoring function used to evaluate the performance of each channel.
+        Defaults to:
+        - `accuracy_score` if using a classifier.
+        - `mean_squared_error` if using a regressor.
+
+    score_sign : float, optional (default=None)
+        The sign used to determine whether higher or lower scores are better.
+        Defaults to:
+        - 1.0 for accuracy_score (higher scores are better).
+        - -1.0 for mean_squared_error (lower scores are better).
+
+    proportion : float, default = 0.4
         Proportion of channels to keep, rounded up to nearest integer.
 
     References
@@ -41,16 +62,20 @@ class ChannelScorer(BaseChannelSelector):
 
     def __init__(
         self,
-        classifier: Optional[BaseClassifier] = None,
+        estimator: Optional[BaseEstimator],
+        scoring_function: Callable = None,
+        score_sign: float = None,
         proportion: float = 0.4,
     ):
         self.proportion = proportion
-        self.classifier = classifier
+        self.estimator = estimator
+        self.scoring_function = scoring_function
+        self.score_sign = score_sign
         super().__init__()
 
-    def _fit(self, X, y):
+    def _fit(self, X: np.ndarray, y: Union[np.ndarray, TypingList]):
         """
-        Fit ECP to a specified X and y.
+        Fit to a specified X and y.
 
         Parameters
         ----------
@@ -66,23 +91,37 @@ class ChannelScorer(BaseChannelSelector):
         if self.proportion <= 0 or self.proportion > 1:
             raise ValueError("proportion must be in the range 0-1")
 
-        if self.classifier is None:
-            self.classifier_ = RocketClassifier(
-                rocket_transform="minirocket", num_kernels=5000
+        if isinstance(self.estimator, BaseClassifier):
+            self.estimator_ = self.estimator.clone()
+            scoring_function = (
+                accuracy_score
+                if self.scoring_function is None
+                else self.scoring_function
             )
-        elif not isinstance(self.classifier, BaseClassifier):
-            raise ValueError(
-                "parameter classifier must be None or an instance of a  "
-                "BaseClassifier."
+            score_sign = 1.0 if self.score_sign is None else self.score_sign
+        elif isinstance(self.estimator, BaseRegressor):
+            self.estimator_ = self.estimator.clone()
+            scoring_function = (
+                mean_squared_error
+                if self.scoring_function is None
+                else self.scoring_function
             )
+            score_sign = -1.0 if self.score_sign is None else self.score_sign
         else:
-            self.classifier_ = self.classifier.clone()
+            raise ValueError(
+                "parameter estimator must be an instance of BaseClassifier, "
+                "BaseRegressor."
+            )
+
         n_channels = X.shape[1]
         scores = np.zeros(n_channels)
         # Evaluate each channel with the classifier
         for i in range(n_channels):
-            preds = self.classifier_.fit_predict(X[:, i, :], y)
-            scores[i] = accuracy_score(y, preds)
+            preds = self.estimator_.fit_predict(X[:, i, :], y)
+            scores[i] = score_sign * scoring_function(
+                y, preds
+            )  # Applying the proper scoring function
+
         # Select the top n_keep channels
         sorted_indices = np.argsort(-scores)
         n_keep = math.ceil(n_channels * self.proportion)
@@ -90,7 +129,7 @@ class ChannelScorer(BaseChannelSelector):
         return self
 
     @classmethod
-    def get_test_params(cls, parameter_set="default"):
+    def get_test_params(cls, parameter_set: str = "default") -> TypingDict[str, any]:
         """Return testing parameter settings for the estimator.
 
         Parameters
@@ -106,4 +145,4 @@ class ChannelScorer(BaseChannelSelector):
         """
         from aeon.classification import DummyClassifier
 
-        return {"classifier": DummyClassifier(), "proportion": 0.4}
+        return {"estimator": DummyClassifier(), "proportion": 0.4}
diff --git a/aeon/transformations/collection/channel_selection/_elbow_class.py b/aeon/transformations/collection/channel_selection/_elbow_class.py
index dbbf12f0..d32b7fbb 100644
--- a/aeon/transformations/collection/channel_selection/_elbow_class.py
+++ b/aeon/transformations/collection/channel_selection/_elbow_class.py
@@ -288,7 +288,6 @@ class ElbowClassSum(BaseChannelSelector):
         self.distance = distance
         self.mean_center = mean_center
         self.prototype_type = prototype_type
-        self._is_fitted = False
 
         super().__init__()
 
@@ -321,7 +320,6 @@ class ElbowClassSum(BaseChannelSelector):
 
         self.channels_selected_.extend(_detect_knee_point(distance, indices))
         self.rank = self.channels_selected_
-        self._is_fitted = True
 
         return self
 
@@ -400,7 +398,6 @@ class ElbowClassPairwise(BaseChannelSelector):
         self.distance = distance
         self.prototype_type = prototype_type
         self.mean_center = mean_center
-        self._is_fitted = False
 
         super().__init__()
 
@@ -438,7 +435,7 @@ class ElbowClassPairwise(BaseChannelSelector):
 
         self.rank = self._rank()
         self.channels_selected_ = list(set(self.channels_selected_))
-        self._is_fitted = True
+
         return self
 
     def _rank(self) -> list[int]:
diff --git a/aeon/transformations/collection/convolution_based/rocketGPU/base.py b/aeon/transformations/collection/convolution_based/rocketGPU/base.py
index 9e87231e..c6e689cf 100644
--- a/aeon/transformations/collection/convolution_based/rocketGPU/base.py
+++ b/aeon/transformations/collection/convolution_based/rocketGPU/base.py
@@ -21,7 +21,7 @@ class BaseROCKETGPU(BaseCollectionTransformer):
         "capability:multivariate": True,
         "algorithm_type": "convolution",
         "capability:unequal_length": False,
-        "cant-pickle": True,
+        "cant_pickle": True,
         "python_dependencies": "tensorflow",
     }
 
diff --git a/aeon/transformations/collection/dictionary_based/_sfa.py b/aeon/transformations/collection/dictionary_based/_sfa.py
index df3f5160..8a67793c 100644
--- a/aeon/transformations/collection/dictionary_based/_sfa.py
+++ b/aeon/transformations/collection/dictionary_based/_sfa.py
@@ -261,7 +261,6 @@ class SFA(BaseCollectionTransformer):
         self.n_cases, self.n_timepoints = X.shape
         self.breakpoints = self._binning(X, y)
 
-        self._is_fitted = True
         return self
 
     def _transform(self, X, y=None):
diff --git a/aeon/transformations/collection/dictionary_based/_sfa_fast.py b/aeon/transformations/collection/dictionary_based/_sfa_fast.py
index 04421715..1e27b908 100644
--- a/aeon/transformations/collection/dictionary_based/_sfa_fast.py
+++ b/aeon/transformations/collection/dictionary_based/_sfa_fast.py
@@ -270,7 +270,6 @@ class SFAFast(BaseCollectionTransformer):
 
         self.n_cases, self.n_timepoints = X2.shape
         self.breakpoints = self._binning(X2, y)
-        self._is_fitted = True
 
         words, dfts = _transform_case(
             X2,
@@ -738,10 +737,6 @@ class SFAFast(BaseCollectionTransformer):
         }
         return params
 
-    def set_fitted(self):
-        """Whether `fit` has been called."""
-        self._is_fitted = True
-
     def __getstate__(self):
         """Return state as dictionary for pickling, required for typed Dict objects."""
         state = self.__dict__.copy()
diff --git a/aeon/transformations/collection/feature_based/_catch22.py b/aeon/transformations/collection/feature_based/_catch22.py
index 2134f45f..3867e3a5 100644
--- a/aeon/transformations/collection/feature_based/_catch22.py
+++ b/aeon/transformations/collection/feature_based/_catch22.py
@@ -192,11 +192,11 @@ class Catch22(BaseCollectionTransformer):
         self.n_jobs = n_jobs
         self.parallel_backend = parallel_backend
 
+        super().__init__()
+
         if use_pycatch22:
             self.set_tags(**{"python_dependencies": "pycatch22"})
 
-        super().__init__()
-
     def _transform(self, X, y=None):
         """Transform X into the catch22 features.
 
diff --git a/aeon/transformations/series/base.py b/aeon/transformations/series/base.py
index 462dc9de..e98c9e46 100644
--- a/aeon/transformations/series/base.py
+++ b/aeon/transformations/series/base.py
@@ -59,7 +59,7 @@ class BaseSeriesTransformer(BaseSeriesEstimator, BaseTransformer, metaclass=ABCM
         """
         # skip the rest if fit_is_empty is True
         if self.get_tag("fit_is_empty"):
-            self._is_fitted = True
+            self.is_fitted = True
             return self
         if self.get_tag("requires_y"):
             if y is None:
@@ -70,7 +70,7 @@ class BaseSeriesTransformer(BaseSeriesEstimator, BaseTransformer, metaclass=ABCM
         if y is not None:
             self._check_y(y)
         self._fit(X=X, y=y)
-        self._is_fitted = True
+        self.is_fitted = True
         return self
 
     @final
@@ -140,7 +140,7 @@ class BaseSeriesTransformer(BaseSeriesEstimator, BaseTransformer, metaclass=ABCM
         self.reset()
         X = self._preprocess_series(X, axis=axis, store_metadata=True)
         Xt = self._fit_transform(X=X, y=y)
-        self._is_fitted = True
+        self.is_fitted = True
         return self._postprocess_series(Xt, axis=axis)
 
     @final
diff --git a/aeon/utils/tags/_tags.py b/aeon/utils/tags/_tags.py
index 4f06c3cc..650a7c4d 100644
--- a/aeon/utils/tags/_tags.py
+++ b/aeon/utils/tags/_tags.py
@@ -34,13 +34,13 @@ ESTIMATOR_TAGS = {
         "as str or list of str. e.g. 'tsfresh>=0.20.0', "
         "'[tsfresh>=0.20.0, pandas<2.0.0]'. If None, no restriction.",
     },
-    "non-deterministic": {
+    "non_deterministic": {
         "class": "estimator",
         "type": "bool",
         "description": "The estimator is non-deterministic, and multiple runs will "
         "not produce the same output even if a `random_state` is set.",
     },
-    "cant-pickle": {
+    "cant_pickle": {
         "class": "estimator",
         "type": "bool",
         "description": "The estimator cannot be pickled.",
@@ -134,13 +134,13 @@ ESTIMATOR_TAGS = {
         "type": "bool",
         "description": "Does this estimator require y to be passed in its methods?",
     },
-    "capability:unequal_length:removes": {
+    "removes_unequal_length": {
         "class": "collection-transformer",
         "type": "bool",
         "description": "Is the transformer result guaranteed to be equal length series "
         "or tabular?",
     },
-    "capability:missing_values:removes": {
+    "removes_missing_values": {
         "class": "transformer",
         "type": "bool",
         "description": "Is the transformer result guaranteed to have no missing "


Test patch:
diff --git a/aeon/anomaly_detection/tests/test_base.py b/aeon/anomaly_detection/tests/test_base.py
index d61d01fd..02ef537b 100644
--- a/aeon/anomaly_detection/tests/test_base.py
+++ b/aeon/anomaly_detection/tests/test_base.py
@@ -33,18 +33,18 @@ def test_fit(series):
 
     # test fit is empty
     ad.fit(series)
-    assert ad._is_fitted
+    assert ad.is_fitted
 
     ad.fit(series, test_y)
-    assert ad._is_fitted
+    assert ad.is_fitted
 
     # test requires fit
     ad_fit.fit(series)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X.squeeze(), series)
 
     ad_fit.fit(series, test_y)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X.squeeze(), series)
 
     # test requires y
@@ -52,7 +52,7 @@ def test_fit(series):
         ad_y.fit(series)
 
     ad_y.fit(series, test_y)
-    assert ad_y._is_fitted
+    assert ad_y.is_fitted
     assert_almost_equal(ad_y._X.squeeze(), series)
     assert_almost_equal(ad_y._y, test_y)
 
@@ -65,11 +65,11 @@ def test_fit_axis(series):
     invert_series = series.T
 
     ad_fit.fit(series)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X, series)
 
     ad_fit.fit(invert_series, axis=0)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X, series)
 
 
@@ -129,27 +129,27 @@ def test_fit_predict(series):
 
     # test fit is empty
     pred = ad.fit_predict(series)
-    assert ad._is_fitted
+    assert ad.is_fitted
     assert isinstance(pred, np.ndarray)
     assert pred.shape == (10,)
     assert issubclass(pred.dtype.type, (np.integer, np.floating, np.bool_))
 
     pred = ad.fit_predict(series, test_y)
-    assert ad._is_fitted
+    assert ad.is_fitted
     assert isinstance(pred, np.ndarray)
     assert pred.shape == (10,)
     assert issubclass(pred.dtype.type, (np.integer, np.floating, np.bool_))
 
     # test requires fit
     pred = ad_fit.fit_predict(series)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X.squeeze(), series)
     assert isinstance(pred, np.ndarray)
     assert pred.shape == (10,)
     assert issubclass(pred.dtype.type, (np.integer, np.floating, np.bool_))
 
     pred = ad_fit.fit_predict(series, test_y)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X.squeeze(), series)
     assert isinstance(pred, np.ndarray)
     assert pred.shape == (10,)
@@ -160,7 +160,7 @@ def test_fit_predict(series):
         ad_y.fit_predict(series)
 
     pred = ad_y.fit_predict(series, test_y)
-    assert ad_y._is_fitted
+    assert ad_y.is_fitted
     assert_almost_equal(ad_y._X.squeeze(), series)
     assert_almost_equal(ad_y._y, test_y)
     assert isinstance(pred, np.ndarray)
@@ -176,12 +176,12 @@ def test_fit_predict_axis(series):
     invert_series = series.T
 
     pred = ad_fit.fit_predict(series)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X, series)
     assert len(pred) == series.shape[1]
 
     pred = ad_fit.fit_predict(invert_series, axis=0)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X, series)
     assert len(pred) == series.shape[1]
 
diff --git a/aeon/base/tests/test_base.py b/aeon/base/tests/test_base.py
index 3de208cf..988af2c4 100644
--- a/aeon/base/tests/test_base.py
+++ b/aeon/base/tests/test_base.py
@@ -52,8 +52,8 @@ FIXTURE_CLASSCHILD = FixtureClassChild
 FIXTURE_CLASSCHILD_TAGS = {
     "python_version": None,
     "python_dependencies": None,
-    "cant-pickle": False,
-    "non-deterministic": False,
+    "cant_pickle": False,
+    "non_deterministic": False,
     "algorithm_type": None,
     "capability:missing_values": False,
     "capability:multithreading": False,
@@ -70,8 +70,8 @@ FIXTURE_OBJECT._tags_dynamic = {"A": 42424241, "B": 3}
 FIXTURE_OBJECT_TAGS = {
     "python_version": None,
     "python_dependencies": None,
-    "cant-pickle": False,
-    "non-deterministic": False,
+    "cant_pickle": False,
+    "non_deterministic": False,
     "algorithm_type": None,
     "capability:missing_values": False,
     "capability:multithreading": False,
@@ -182,8 +182,8 @@ FIXTURE_OBJECT_SET = deepcopy(FIXTURE_OBJECT).set_tags(**FIXTURE_TAG_SET)
 FIXTURE_OBJECT_SET_TAGS = {
     "python_version": None,
     "python_dependencies": None,
-    "cant-pickle": False,
-    "non-deterministic": False,
+    "cant_pickle": False,
+    "non_deterministic": False,
     "algorithm_type": None,
     "capability:missing_values": False,
     "capability:multithreading": False,
@@ -218,20 +218,6 @@ class CompositionDummy(BaseEstimator):
         self.bar = bar
 
 
-def test_is_composite():
-    """Tests is_composite tag for correctness.
-
-    Raises
-    ------
-    AssertionError if logic behind is_composite is incorrect
-    """
-    non_composite = CompositionDummy(foo=42)
-    composite = CompositionDummy(foo=non_composite)
-
-    assert not non_composite.is_composite()
-    assert composite.is_composite()
-
-
 class ResetTester(BaseEstimator):
     clsvar = 210
 
@@ -325,7 +311,7 @@ class FittableCompositionDummy(BaseEstimator):
     def fit(self):
         if hasattr(self.foo_, "fit"):
             self.foo_.fit()
-        self._is_fitted = True
+        self.is_fitted = True
 
 
 def test_get_fitted_params():
diff --git a/aeon/classification/compose/tests/test_pipeline.py b/aeon/classification/compose/tests/test_pipeline.py
index fe3ab025..2d1e607f 100644
--- a/aeon/classification/compose/tests/test_pipeline.py
+++ b/aeon/classification/compose/tests/test_pipeline.py
@@ -118,10 +118,10 @@ def test_unequal_tag_inference():
     assert t1.get_tag("capability:unequal_length")
     assert t1.get_tag("output_data_type") == "Tabular"
     assert t2.get_tag("capability:unequal_length")
-    assert t2.get_tag("capability:unequal_length:removes")
+    assert t2.get_tag("removes_unequal_length")
     assert not t2.get_tag("output_data_type") == "Tabular"
     assert t3.get_tag("capability:unequal_length")
-    assert not t3.get_tag("capability:unequal_length:removes")
+    assert not t3.get_tag("removes_unequal_length")
     assert not t3.get_tag("output_data_type") == "Tabular"
     assert not t4.get_tag("capability:unequal_length")
 
@@ -179,14 +179,14 @@ def test_missing_tag_inference():
 
     t1 = MockCollectionTransformer()
     t1 = t1.set_tags(
-        **{"capability:missing_values": True, "capability:missing_values:removes": True}
+        **{"capability:missing_values": True, "removes_missing_values": True}
     )
     t2 = TimeSeriesScaler()
     t3 = StandardScaler()
     t4 = Tabularizer()
 
     assert t1.get_tag("capability:missing_values")
-    assert t1.get_tag("capability:missing_values:removes")
+    assert t1.get_tag("removes_missing_values")
     assert not t2.get_tag("capability:missing_values")
 
     c1 = DummyClassifier()
diff --git a/aeon/clustering/compose/tests/test_pipeline.py b/aeon/clustering/compose/tests/test_pipeline.py
index f239b704..5225e1d2 100644
--- a/aeon/clustering/compose/tests/test_pipeline.py
+++ b/aeon/clustering/compose/tests/test_pipeline.py
@@ -117,10 +117,10 @@ def test_unequal_tag_inference():
     assert t1.get_tag("capability:unequal_length")
     assert t1.get_tag("output_data_type") == "Tabular"
     assert t2.get_tag("capability:unequal_length")
-    assert t2.get_tag("capability:unequal_length:removes")
+    assert t2.get_tag("removes_unequal_length")
     assert not t2.get_tag("output_data_type") == "Tabular"
     assert t3.get_tag("capability:unequal_length")
-    assert not t3.get_tag("capability:unequal_length:removes")
+    assert not t3.get_tag("removes_unequal_length")
     assert not t3.get_tag("output_data_type") == "Tabular"
     assert not t4.get_tag("capability:unequal_length")
 
@@ -178,15 +178,13 @@ def test_missing_tag_inference():
     # X[5, 0, 4] = np.nan
 
     t1 = MockCollectionTransformer()
-    t1.set_tags(
-        **{"capability:missing_values": True, "capability:missing_values:removes": True}
-    )
+    t1.set_tags(**{"capability:missing_values": True, "removes_missing_values": True})
     t2 = TimeSeriesScaler()
     t3 = StandardScaler()
     t4 = Tabularizer()
 
     assert t1.get_tag("capability:missing_values")
-    assert t1.get_tag("capability:missing_values:removes")
+    assert t1.get_tag("removes_missing_values")
     assert not t2.get_tag("capability:missing_values")
 
     # todo revisit with mock clusterer
diff --git a/aeon/datasets/tests/test_data_writers.py b/aeon/datasets/tests/test_data_writers.py
index bf26cedc..8f5f2548 100644
--- a/aeon/datasets/tests/test_data_writers.py
+++ b/aeon/datasets/tests/test_data_writers.py
@@ -104,7 +104,7 @@ def test_write_data_to_tsfile_invalid():
     """Test function to check the handling of invalid inputs by write_to_tsfile."""
     with pytest.raises(TypeError, match="Wrong input data type"):
         write_to_tsfile("A string", "path")
-    with pytest.raises(TypeError, match="Data provided must be a ndarray or a list"):
+    with pytest.raises(ValueError, match="Data provided must be a ndarray or a list"):
         _write_data_to_tsfile("AFC", "49", "undefeated")
     X, _ = make_example_3d_numpy(n_cases=6, n_timepoints=10, n_channels=1)
     y = np.ndarray([0, 1, 1, 0, 1])
@@ -141,6 +141,37 @@ def test_write_dataframe_to_ts(tsfile_writer):
         np.testing.assert_array_almost_equal(newy.astype(int), y)
 
 
+def test_write_inputs():
+    """Tests whether error thrown if wrong input."""
+    # load an example dataset
+    problem_name = "Testy.ts"
+    with tempfile.TemporaryDirectory() as tmp:
+        # output the dataframe in a ts file
+        X, y = make_example_nested_dataframe(min_n_timepoints=12)
+        X2, y2 = make_example_3d_numpy()
+        X3, y3 = make_example_3d_numpy_list()
+        with pytest.raises(ValueError, match="Data provided must be a ndarray"):
+            _write_data_to_tsfile(
+                X=X,
+                path=tmp,
+                y=y,
+                problem_name=problem_name,
+            )
+        _write_data_to_tsfile(X=X3, path=tmp, y=y3, problem_name=problem_name)
+        with pytest.raises(ValueError, match="Data provided must be a DataFrame"):
+            _write_dataframe_to_tsfile(
+                X=X2,
+                path=tmp,
+                y=y2,
+                problem_name=problem_name,
+            )
+        with pytest.raises(TypeError, match="Wrong input data type"):
+            write_to_arff_file(X, y, tmp)
+        X2, y2 = make_example_3d_numpy(n_cases=5, n_channels=2)
+        with pytest.raises(ValueError, match="must be a 3D array with shape"):
+            write_to_arff_file(X2, y2, tmp)
+
+
 def test_write_header():
     """Test _write_header."""
     with tempfile.TemporaryDirectory() as tmp:
@@ -186,6 +217,18 @@ def test_write_results_to_uea_format():
             write_results_to_uea_format(
                 "HC", "Testy", y_pred=y_pred, y_true=y_true, output_path=tmp
             )
+        with pytest.raises(ValueError, match="Unknown 'split' value"):
+            write_results_to_uea_format(
+                "HC",
+                "Testy",
+                y_pred=y_pred,
+                output_path=tmp,
+                full_path=False,
+                split="FOO",
+                timing_type="seconds",
+                first_line_comment="Hello",
+            )
+
         y_true = np.array([0, 1, 1, 0])
         write_results_to_uea_format(
             "HC",
@@ -211,3 +254,14 @@ def test_write_results_to_uea_format():
             first_line_comment="Hello",
             predicted_probs=probs,
         )
+        write_results_to_uea_format(
+            "HC",
+            "Testy2",
+            y_pred=y_pred,
+            output_path=tmp,
+            full_path=False,
+            split="TEST",
+            timing_type="seconds",
+            first_line_comment="Hello",
+            predicted_probs=probs,
+        )
diff --git a/aeon/regression/compose/tests/test_pipeline.py b/aeon/regression/compose/tests/test_pipeline.py
index ce3085d2..c644e9f6 100644
--- a/aeon/regression/compose/tests/test_pipeline.py
+++ b/aeon/regression/compose/tests/test_pipeline.py
@@ -118,10 +118,10 @@ def test_unequal_tag_inference():
     assert t1.get_tag("capability:unequal_length")
     assert t1.get_tag("output_data_type") == "Tabular"
     assert t2.get_tag("capability:unequal_length")
-    assert t2.get_tag("capability:unequal_length:removes")
+    assert t2.get_tag("removes_unequal_length")
     assert not t2.get_tag("output_data_type") == "Tabular"
     assert t3.get_tag("capability:unequal_length")
-    assert not t3.get_tag("capability:unequal_length:removes")
+    assert not t3.get_tag("removes_unequal_length")
     assert not t3.get_tag("output_data_type") == "Tabular"
     assert not t4.get_tag("capability:unequal_length")
 
@@ -178,15 +178,13 @@ def test_missing_tag_inference():
     # X[5, 0, 4] = np.nan
 
     t1 = MockCollectionTransformer()
-    t1.set_tags(
-        **{"capability:missing_values": True, "capability:missing_values:removes": True}
-    )
+    t1.set_tags(**{"capability:missing_values": True, "removes_missing_values": True})
     t2 = TimeSeriesScaler()
     t3 = StandardScaler()
     t4 = Tabularizer()
 
     assert t1.get_tag("capability:missing_values")
-    assert t1.get_tag("capability:missing_values:removes")
+    assert t1.get_tag("removes_missing_values")
     assert not t2.get_tag("capability:missing_values")
 
     c1 = DummyRegressor()
diff --git a/aeon/testing/estimator_checking/_estimator_checking.py b/aeon/testing/estimator_checking/_estimator_checking.py
index bd5af8ff..1e0f1621 100644
--- a/aeon/testing/estimator_checking/_estimator_checking.py
+++ b/aeon/testing/estimator_checking/_estimator_checking.py
@@ -113,7 +113,7 @@ def check_estimator(
     general checks for all estimators, and module-specific tests for classifiers,
     anomaly detectors, transformer, etc.
     Some checks may be skipped if the estimator has certain tags i.e.
-    `non-deterministic`.
+    `non_deterministic`.
 
     Parameters
     ----------
diff --git a/aeon/testing/estimator_checking/_yield_collection_transformation_checks.py b/aeon/testing/estimator_checking/_yield_collection_transformation_checks.py
index 20f859d5..ee7636d5 100644
--- a/aeon/testing/estimator_checking/_yield_collection_transformation_checks.py
+++ b/aeon/testing/estimator_checking/_yield_collection_transformation_checks.py
@@ -35,7 +35,7 @@ def check_channel_selectors(estimator_class):
     """
     X, _ = make_example_3d_numpy(n_cases=20, n_channels=6, n_timepoints=30)
     y = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1])
-    cs = estimator_class()
+    cs = estimator_class.create_test_instance(return_first=True)
     assert not cs.get_tag("fit_is_empty")
     cs.fit(X, y)
     assert cs.channels_selected_ is not None
diff --git a/aeon/testing/estimator_checking/_yield_estimator_checks.py b/aeon/testing/estimator_checking/_yield_estimator_checks.py
index 172fc35b..7c623c05 100644
--- a/aeon/testing/estimator_checking/_yield_estimator_checks.py
+++ b/aeon/testing/estimator_checking/_yield_estimator_checks.py
@@ -177,9 +177,6 @@ def _yield_estimator_checks(estimator_class, estimator_instances, datatypes):
     """Yield all general checks for an aeon estimator."""
     # only class required
     yield partial(check_create_test_instance, estimator_class=estimator_class)
-    yield partial(
-        check_create_test_instances_and_names, estimator_class=estimator_class
-    )
     yield partial(check_inheritance, estimator_class=estimator_class)
     yield partial(check_has_common_interface, estimator_class=estimator_class)
     yield partial(check_set_params_sklearn, estimator_class=estimator_class)
@@ -219,14 +216,14 @@ def _yield_estimator_checks(estimator_class, estimator_instances, datatypes):
                 datatype=datatypes[i][0],
             )
 
-        if not _get_tag(estimator, "cant-pickle", default=False):
+        if not _get_tag(estimator, "cant_pickle", default=False):
             yield partial(
                 check_persistence_via_pickle,
                 estimator=estimator,
                 datatype=datatypes[i][0],
             )
 
-        if not _get_tag(estimator, "non-deterministic", default=False):
+        if not _get_tag(estimator, "non_deterministic", default=False):
             yield partial(
                 check_fit_deterministic, estimator=estimator, datatype=datatypes[i][0]
             )
@@ -261,43 +258,6 @@ def check_create_test_instance(estimator_class):
     assert hasattr(estimator, "_tags_dynamic"), msg
 
 
-# todo consider deprecation
-def check_create_test_instances_and_names(estimator_class):
-    """Check that create_test_instances_and_names works.
-
-    create_test_instance and create_test_instances_and_names are the
-    key methods used to create test instances in testing.
-    If this test does not pass, validity of the other tests cannot be guaranteed.
-
-    Tests expected function signature of create_test_instances_and_names.
-    """
-    estimators, names = estimator_class.create_test_instances_and_names()
-
-    assert isinstance(estimators, list), (
-        "first return of create_test_instances_and_names must be a list, "
-        f"found {type(estimators)}"
-    )
-    assert isinstance(names, list), (
-        "second return of create_test_instances_and_names must be a list, "
-        f"found {type(names)}"
-    )
-
-    assert np.all([isinstance(est, estimator_class) for est in estimators]), (
-        "list elements of first return returned by create_test_instances_and_names "
-        "all must be an instance of the class"
-    )
-
-    assert np.all([isinstance(name, str) for name in names]), (
-        "list elements of second return returned by create_test_instances_and_names"
-        " all must be strings"
-    )
-
-    assert len(estimators) == len(names), (
-        "the two lists returned by create_test_instances_and_names must have "
-        "equal length"
-    )
-
-
 # todo consider removing the multiple base class allowance.
 def check_inheritance(estimator_class):
     """Check that estimator inherits from BaseEstimator."""
@@ -620,8 +580,6 @@ def check_fit_updates_state(estimator, datatype):
     5. Check estimator hyper parameters are not changed in fit
     """
     # Check that fit updates the is-fitted states
-    attrs = ["_is_fitted", "is_fitted"]
-
     estimator = _clone_estimator(estimator)
 
     msg = (
@@ -630,13 +588,12 @@ def check_fit_updates_state(estimator, datatype):
         "but that does not seem to be the case. Please ensure to call the "
         f"parent class's constructor in {type(estimator).__name__}.__init__"
     )
-    assert hasattr(estimator, "_is_fitted"), msg
+    assert hasattr(estimator, "is_fitted"), msg
 
     # Check is_fitted attribute is set correctly to False before fit, at init
-    for attr in attrs:
-        assert not getattr(
-            estimator, attr
-        ), f"Estimator: {estimator} does not initiate attribute: {attr} to False"
+    assert (
+        not estimator.is_fitted
+    ), f"Estimator: {estimator} does not initiate attribute: is_fitted to False"
 
     # Make a physical copy of the original estimator parameters before fitting.
     original_params = deepcopy(estimator.get_params())
@@ -649,10 +606,9 @@ def check_fit_updates_state(estimator, datatype):
     ), f"Estimator: {estimator} does not return self when calling fit"
 
     # Check is_fitted attribute is updated correctly to True after calling fit
-    for attr in attrs:
-        assert getattr(
-            fitted_estimator, attr
-        ), f"Estimator: {estimator} does not update attribute: {attr} during fit"
+    assert (
+        fitted_estimator.is_fitted
+    ), f"Estimator: {estimator} does not update attribute: is_fitted during fit"
 
     # Compare the state of the model parameters with the original parameters
     new_params = fitted_estimator.get_params()
diff --git a/aeon/transformations/collection/channel_selection/tests/test_channel_scorer.py b/aeon/transformations/collection/channel_selection/tests/test_channel_scorer.py
index 5607be3a..a57ac558 100644
--- a/aeon/transformations/collection/channel_selection/tests/test_channel_scorer.py
+++ b/aeon/transformations/collection/channel_selection/tests/test_channel_scorer.py
@@ -3,32 +3,79 @@
 import numpy as np
 import pytest
 
+from aeon.regression import DummyRegressor
 from aeon.testing.data_generation import make_example_3d_numpy
 from aeon.testing.mock_estimators import MockClassifierFullTags
 from aeon.transformations.collection.channel_selection import ChannelScorer
 
 
-def test_channel_scorer():
-    """Test the channel scorer."""
+def test_channel_scorer_with_classifier():
+    """Test the channel scorer for classifier."""
     # Test selects the correct number of channels
-    cs = ChannelScorer(classifier=MockClassifierFullTags(), proportion=0.5)
+    cs = ChannelScorer(estimator=MockClassifierFullTags(), proportion=0.5)
     X, _ = make_example_3d_numpy(n_cases=20, n_channels=10)
     y = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1])
     cs.fit(X, y)
     assert len(cs.channels_selected_) == 5
     X, _ = make_example_3d_numpy(n_cases=20, n_channels=9)
     assert len(cs.channels_selected_) == 5
+
+    # Test for proportion out of range
     with pytest.raises(ValueError, match="proportion must be in the range 0-1"):
-        cs = ChannelScorer(classifier=MockClassifierFullTags(), proportion=1.1)
+        cs = ChannelScorer(estimator=MockClassifierFullTags(), proportion=1.1)
         cs.fit(X, y)
+
     with pytest.raises(ValueError, match="proportion must be in the range 0-1"):
-        ChannelScorer(classifier=MockClassifierFullTags(), proportion=-1)
+        ChannelScorer(estimator=MockClassifierFullTags(), proportion=-1)
         cs.fit(X, y)
-    cs = ChannelScorer(classifier=MockClassifierFullTags(), proportion=1.0)
+
+    # Test for selecting all channels when proportion=1.0
+    cs = ChannelScorer(estimator=MockClassifierFullTags(), proportion=1.0)
     cs.fit(X, y)
     assert len(cs.channels_selected_) == 9
+
+    # Test for invalid estimator
+    with pytest.raises(
+        ValueError,
+        match="parameter estimator must be an instance of BaseClassifier, "
+        "BaseRegressor",
+    ):
+        cs = ChannelScorer(estimator="FOOBAR", proportion=0.5)
+        cs.fit(X, y)
+
+
+def test_channel_scorer_with_regressor():
+    """Test the channel scorer for regressor."""
+    # Test selects the correct number of channels
+    cs = ChannelScorer(estimator=DummyRegressor(), proportion=0.5)
+    X, _ = make_example_3d_numpy(n_cases=20, n_channels=10)
+    y = np.random.rand(20)
+    cs.fit(X, y)
+    assert len(cs.channels_selected_) == 5
+
+    X, _ = make_example_3d_numpy(n_cases=20, n_channels=9)
+    cs.fit(X, y)
+    assert len(cs.channels_selected_) == 5
+
+    # Test for proportion out of range
+    with pytest.raises(ValueError, match="proportion must be in the range 0-1"):
+        cs = ChannelScorer(estimator=DummyRegressor(), proportion=1.1)
+        cs.fit(X, y)
+
+    with pytest.raises(ValueError, match="proportion must be in the range 0-1"):
+        cs = ChannelScorer(estimator=DummyRegressor(), proportion=-0.1)
+        cs.fit(X, y)
+
+    # Test for selecting all channels when proportion=1.0
+    cs = ChannelScorer(estimator=DummyRegressor(), proportion=1.0)
+    cs.fit(X, y)
+    assert len(cs.channels_selected_) == 9
+
+    # Test for invalid estimator
     with pytest.raises(
-        ValueError, match="parameter classifier must be None or an instance of"
+        ValueError,
+        match="parameter estimator must be an instance of BaseClassifier, "
+        "BaseRegressor",
     ):
-        cs = ChannelScorer(classifier="FOOBAR", proportion=0.5)
+        cs = ChannelScorer(estimator="FOOBAR", proportion=0.5)
         cs.fit(X, y)
diff --git a/aeon/transformations/collection/compose/tests/test_pipeline.py b/aeon/transformations/collection/compose/tests/test_pipeline.py
index 8320b6fd..21131233 100644
--- a/aeon/transformations/collection/compose/tests/test_pipeline.py
+++ b/aeon/transformations/collection/compose/tests/test_pipeline.py
@@ -69,10 +69,10 @@ def test_unequal_tag_inference():
     assert t1.get_tag("capability:unequal_length")
     assert t1.get_tag("output_data_type") == "Tabular"
     assert t2.get_tag("capability:unequal_length")
-    assert t2.get_tag("capability:unequal_length:removes")
+    assert t2.get_tag("removes_unequal_length")
     assert not t2.get_tag("output_data_type") == "Tabular"
     assert t3.get_tag("capability:unequal_length")
-    assert not t3.get_tag("capability:unequal_length:removes")
+    assert not t3.get_tag("removes_unequal_length")
     assert not t3.get_tag("output_data_type") == "Tabular"
     assert not t4.get_tag("capability:unequal_length")
 
@@ -115,15 +115,13 @@ def test_missing_tag_inference():
     X, y = make_example_3d_numpy(n_cases=10, n_timepoints=12)
 
     t1 = MockCollectionTransformer()
-    t1.set_tags(
-        **{"capability:missing_values": True, "capability:missing_values:removes": True}
-    )
+    t1.set_tags(**{"capability:missing_values": True, "removes_missing_values": True})
     t2 = TimeSeriesScaler()
     t3 = StandardScaler()
     t4 = Tabularizer()
 
     assert t1.get_tag("capability:missing_values")
-    assert t1.get_tag("capability:missing_values:removes")
+    assert t1.get_tag("removes_missing_values")
     assert not t2.get_tag("capability:missing_values")
 
     # transformer chain removes missing values
diff --git a/aeon/utils/tags/tests/test_lookup.py b/aeon/utils/tags/tests/test_lookup.py
index f36202eb..fd3d57fa 100644
--- a/aeon/utils/tags/tests/test_lookup.py
+++ b/aeon/utils/tags/tests/test_lookup.py
@@ -16,7 +16,7 @@ def test_all_tags_for_estimator_classification():
 
     assert isinstance(tags, dict)
     assert "python_version" in tags
-    assert "non-deterministic" in tags
+    assert "non_deterministic" in tags
     assert "capability:unequal_length" in tags
     assert "capability:contractable" in tags
 
@@ -37,7 +37,7 @@ def test_all_tags_for_estimator_anomaly_detection():
 
     assert isinstance(tags, dict)
     assert "python_version" in tags
-    assert "non-deterministic" in tags
+    assert "non_deterministic" in tags
     assert "capability:unequal_length" not in tags
     assert "capability:contractable" not in tags
 
diff --git a/aeon/utils/tags/tests/test_validate.py b/aeon/utils/tags/tests/test_validate.py
index daf1afeb..ca80135c 100644
--- a/aeon/utils/tags/tests/test_validate.py
+++ b/aeon/utils/tags/tests/test_validate.py
@@ -25,7 +25,7 @@ def test_check_valid_tags(estimator):
     tags = {
         "python_version": ">3.8",
         "python_dependencies": None,
-        "non-deterministic": False,
+        "non_deterministic": False,
     }
 
     check_valid_tags(estimator, tags=tags, error_on_missing=False)
@@ -61,8 +61,8 @@ def test_check_valid_tags_invalid():
     tags = {
         "python_version": ">3.8",
         "python_dependencies": None,
-        "non-deterministic": 1,
+        "non_deterministic": 1,
     }
 
-    with pytest.raises(ValueError, match="Tag non-deterministic has an invalid value"):
+    with pytest.raises(ValueError, match="Tag non_deterministic has an invalid value"):
         check_valid_tags(MockClassifier, tags=tags)