Repository: aeon-toolkit/aeon
Pull request #1256 (2024-02-26 17:24:50)

[DOC] Conformed to new-standards
conformed the `_fit` function from `classification\deep_learning`  to new standards of Aeon's docstrings

<!--
Thanks for contributing a pull request! Please ensure you have taken a look at our
contribution guide: https://www.aeon-toolkit.org/en/stable/contributing.html

Feel free to delete sections of this template if they do not apply to your PR,
avoid submitting a blank template or empty sections.
-->

#### Reference Issues/PRs
closes: #1150 



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

I have added the following labels to this PR based on the title: **[ $\\color{#F3B9F8}{\\textsf{documentation}}$ ]**.
I have added the following labels to this PR based on the changes made: **[ $\\color{#BCAE15}{\\textsf{classification}}$ ]**. Feel free to change these if they do not properly represent the PR.

The [Checks](https://github.com/aeon-toolkit/aeon/pull/1256/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.
    
MatthewMiddlehurst: The comment should follow this format, only the datatype or relevant datatype info should be on the first line. `_fit` should also only be receiving `np.ndarray`, as other types will be converted in fit. Could you also update the #1245 shapedtw doc? I forgot this applies there as well 🙂 

```
    X : np.ndarray
        The training input samples of shape (n_cases, n_channels, n_timepoints)
    y : np.ndarray
        The training data class labels of shape (n_cases,).
```
AnonymousCodes911: sure thing!

Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index a6898f15..1cbcf299 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -2338,8 +2338,20 @@
       "profile": "https://github.com/codelionx",
       "contributions": [
         "bug",
-        "code"
+        "code",
+        "doc",
+        "research"
       ]
+    },
+    {
+      "login": "AnonymousCodes911",
+    "name": "Utkarsh Kumar",
+    "avatar_url": "https://avatars.githubusercontent.com/u/67187480?v=4",
+    "profile": "https://github.com/AnonymousCodes911",
+    "contributions": [
+      "code",
+      "doc"
+    ]
     }
   ],
   "commitType": "docs"
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 275ce217..bbc06e88 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -225,7 +225,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
     </tr>
     <tr>
       <td align="center" valign="top" width="12.5%"><a href="https://github.com/shagn"><img src="https://avatars.githubusercontent.com/u/16029092?v=4?s=80" width="80px;" alt="Sebastian Hagn"/><br /><sub><b>Sebastian Hagn</b></sub></a><br /><a href="https://github.com/aeon-toolkit/aeon/commits?author=shagn" title="Documentation">📖</a></td>
-      <td align="center" valign="top" width="12.5%"><a href="https://github.com/codelionx"><img src="https://avatars.githubusercontent.com/u/10573700?v=4?s=80" width="80px;" alt="Sebastian Schmidl"/><br /><sub><b>Sebastian Schmidl</b></sub></a><br /><a href="https://github.com/aeon-toolkit/aeon/issues?q=author%3Acodelionx" title="Bug reports">🐛</a> <a href="https://github.com/aeon-toolkit/aeon/commits?author=codelionx" title="Code">💻</a></td>
+      <td align="center" valign="top" width="12.5%"><a href="https://github.com/codelionx"><img src="https://avatars.githubusercontent.com/u/10573700?v=4?s=80" width="80px;" alt="Sebastian Schmidl"/><br /><sub><b>Sebastian Schmidl</b></sub></a><br /><a href="https://github.com/aeon-toolkit/aeon/issues?q=author%3Acodelionx" title="Bug reports">🐛</a> <a href="https://github.com/aeon-toolkit/aeon/commits?author=codelionx" title="Code">💻</a> <a href="https://github.com/aeon-toolkit/aeon/commits?author=codelionx" title="Documentation">📖</a> <a href="#research-codelionx" title="Research">🔬</a></td>
       <td align="center" valign="top" width="12.5%"><a href="https://github.com/AurumnPegasus"><img src="https://avatars.githubusercontent.com/u/54315149?v=4?s=80" width="80px;" alt="Shivansh Subramanian"/><br /><sub><b>Shivansh Subramanian</b></sub></a><br /><a href="https://github.com/aeon-toolkit/aeon/commits?author=AurumnPegasus" title="Documentation">📖</a> <a href="https://github.com/aeon-toolkit/aeon/commits?author=AurumnPegasus" title="Code">💻</a></td>
       <td align="center" valign="top" width="12.5%"><a href="https://www.linkedin.com/in/solomon-botchway-a1383821b/"><img src="https://avatars.githubusercontent.com/u/62394255?v=4?s=80" width="80px;" alt="Solomon Botchway"/><br /><sub><b>Solomon Botchway</b></sub></a><br /><a href="#maintenance-snnbotchway" title="Maintenance">🚧</a></td>
       <td align="center" valign="top" width="12.5%"><a href="https://github.com/khrapovs"><img src="https://avatars.githubusercontent.com/u/3774663?v=4?s=80" width="80px;" alt="Stanislav Khrapov"/><br /><sub><b>Stanislav Khrapov</b></sub></a><br /><a href="https://github.com/aeon-toolkit/aeon/commits?author=khrapovs" title="Code">💻</a></td>
diff --git a/aeon/classification/base.py b/aeon/classification/base.py
index 352690cf..50b96de6 100644
--- a/aeon/classification/base.py
+++ b/aeon/classification/base.py
@@ -30,6 +30,7 @@ from typing import final
 import numpy as np
 import pandas as pd
 from deprecated.sphinx import deprecated
+from sklearn.metrics import accuracy_score
 from sklearn.model_selection import cross_val_predict
 from sklearn.utils.multiclass import type_of_target
 
@@ -44,21 +45,25 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
     """
     Abstract base class for time series classifiers.
 
-    Attributes with an underscore suffix are set in the method fit.
-
+    The base classifier specifies the methods and method signatures that all
+    classifiers have to implement. Attributes with an underscore suffix are set in the
+    method fit.
 
     Attributes
     ----------
     classes_ : np.ndarray
-        Class labels, possibly strings.
-    n_classes_ : integer
+        Class labels, either integers or strings.
+    n_classes_ : int
         Number of classes (length of ``classes_``).
-    fit_time_ : integer
-        Time (in milliseconds) for fit to run.
+    fit_time_ : int
+        Time (in milliseconds) for ``fit`` to run.
     _class_dictionary : dict
-        Mapping of classes_ onto integers 0...``n_classes_``-1.
-    _n_jobs : number of threads to use in ``fit`` as determined by ``n_jobs``.
-    _estimator_type : string required by sklearn, set to "classifier"
+        Mapping of classes_ onto integers ``0 ... n_classes_-1``.
+    _n_jobs : int
+        Number of threads to use in estimator methods such as ``fit`` and ``predict``.
+        Determined by the ``n_jobs`` parameter if present.
+    _estimator_type : string
+        The type of estimator. Required by some ``sklearn`` tools, set to "classifier".
     """
 
     _tags = {
@@ -69,8 +74,10 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
     def __init__(self):
         # reserved attributes written to in fit
         self.classes_ = []  # classes seen in y, unique labels
-        self.n_classes_ = 0  # number of unique classes in y
+        self.n_classes_ = -1  # number of unique classes in y
         self._class_dictionary = {}
+        self.fit_time_ = -1
+        self._n_jobs = 1
 
         # required for compatibility with some sklearn interfaces e.g.
         # CalibratedClassifierCV
@@ -129,13 +136,13 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
 
         Parameters
         ----------
-        X : np.ndarray
+        X : np.ndarray or list
             Input data, any number of channels, equal length series of shape ``(
-            n_instances, n_channels, n_timepoints)``
+            n_cases, n_channels, n_timepoints)``
             or 2D np.array (univariate, equal length series) of shape
-            ``(n_instances, n_timepoints)``
+            ``(n_cases, n_timepoints)``
             or list of numpy arrays (any number of channels, unequal length series)
-            of shape ``[n_instances]``, 2D np.array ``(n_channels, n_timepoints_i)``,
+            of shape ``[n_cases]``, 2D np.array ``(n_channels, n_timepoints_i)``,
             where ``n_timepoints_i`` is length of series ``i``. Other types are
             allowed and converted into one of the above.
 
@@ -147,14 +154,14 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
             length input. In both situations, a ``ValueError`` is raised if X has a
             characteristic that the estimator does not have the capability for is
             passed.
-
-        np.ndarray
-            shape ``(n_instances)`` - class labels for fitting indices correspond to
-            instance indices in X.
+        y : np.ndarray
+            1D np.array of float or str, of shape ``(n_cases)`` - class labels
+            (ground truth) for fitting indices corresponding to instance indices in X.
 
         Returns
         -------
-        self : Reference to self.
+        self : BaseClassifier
+            Reference to self.
 
         Notes
         -----
@@ -178,13 +185,13 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
 
         Parameters
         ----------
-        X : np.ndarray
+        X : np.ndarray or list
             Input data, any number of channels, equal length series of shape ``(
-            n_instances, n_channels, n_timepoints)``
+            n_cases, n_channels, n_timepoints)``
             or 2D np.array (univariate, equal length series) of shape
-            ``(n_instances, n_timepoints)``
+            ``(n_cases, n_timepoints)``
             or list of numpy arrays (any number of channels, unequal length series)
-            of shape ``[n_instances]``, 2D np.array ``(n_channels, n_timepoints_i)``,
+            of shape ``[n_cases]``, 2D np.array ``(n_channels, n_timepoints_i)``,
             where ``n_timepoints_i`` is length of series ``i``
             other types are allowed and converted into one of the above.
 
@@ -199,15 +206,17 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
 
         Returns
         -------
-        np.ndarray
-            shape ``[n_instances]`` - predicted class labels indices correspond to
-            instance indices in X
+        predictions : np.ndarray
+            1D np.array of float, of shape (n_cases) - predicted class labels
+            indices correspond to instance indices in X
         """
         self.check_is_fitted()
+
         # handle the single-class-label case
         if len(self._class_dictionary) == 1:
-            n_instances = get_n_cases(X)
-            return np.repeat(list(self._class_dictionary.keys()), n_instances)
+            n_cases = get_n_cases(X)
+            return np.repeat(list(self._class_dictionary.keys()), n_cases)
+
         X = self._preprocess_collection(X)
         return self._predict(X)
 
@@ -217,13 +226,13 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
 
         Parameters
         ----------
-        X : np.ndarray
+        X : np.ndarray or list
             Input data, any number of channels, equal length series of shape ``(
-            n_instances, n_channels, n_timepoints)``
+            n_cases, n_channels, n_timepoints)``
             or 2D np.array (univariate, equal length series) of shape
-            ``(n_instances, n_timepoints)``
+            ``(n_cases, n_timepoints)``
             or list of numpy arrays (any number of channels, unequal length series)
-            of shape ``[n_instances]``, 2D np.array ``(n_channels, n_timepoints_i)``,
+            of shape ``[n_cases]``, 2D np.array ``(n_channels, n_timepoints_i)``,
             where ``n_timepoints_i`` is length of series ``i``. other types are
             allowed and converted into one of the above.
 
@@ -238,7 +247,7 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
 
         Returns
         -------
-        np.ndarray
+        probabilities : np.ndarray
             2D array of shape ``(n_cases, n_classes)`` - predicted class probabilities
             First dimension indices correspond to instance indices in X,
             second dimension indices correspond to class labels, (i, j)-th entry is
@@ -248,8 +257,9 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
 
         # handle the single-class-label case
         if len(self._class_dictionary) == 1:
-            n_instances = get_n_cases(X)
-            return np.repeat([[1]], n_instances, axis=0)
+            n_cases = get_n_cases(X)
+            return np.repeat([[1]], n_cases, axis=0)
+
         X = self._preprocess_collection(X)
         return self._predict_proba(X)
 
@@ -270,13 +280,13 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
 
         Parameters
         ----------
-        X : np.ndarray
+        X : np.ndarray or list
             Input data, any number of channels, equal length series of shape ``(
-            n_instances, n_channels, n_timepoints)``
+            n_cases, n_channels, n_timepoints)``
             or 2D np.array (univariate, equal length series) of shape
-            ``(n_instances, n_timepoints)``
+            ``(n_cases, n_timepoints)``
             or list of numpy arrays (any number of channels, unequal length series)
-            of shape ``[n_instances]``, 2D np.array ``(n_channels, n_timepoints_i)``,
+            of shape ``[n_cases]``, 2D np.array ``(n_channels, n_timepoints_i)``,
             where ``n_timepoints_i`` is length of series ``i``. other types are
             allowed and converted into one of the above.
 
@@ -288,18 +298,21 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
             length input. In both situations, a ``ValueError`` is raised if X has a
             characteristic that the estimator does not have the capability for is
             passed.
+        y : np.ndarray
+            1D np.array of float or str, of shape ``(n_cases)`` - class labels
+            (ground truth) for fitting indices corresponding to instance indices in X.
 
         Returns
         -------
-        np.ndarray
-            shape ``[n_instances]`` - predicted class labels indices correspond to
+        predictions : np.ndarray
+            shape ``[n_cases]`` - predicted class labels indices correspond to
             instance indices in
         """
         X, y, single_class = self._fit_setup(X, y)
 
         if single_class:
-            n_instances = get_n_cases(X)
-            y_pred = np.repeat(list(self._class_dictionary.keys()), n_instances)
+            n_cases = get_n_cases(X)
+            y_pred = np.repeat(list(self._class_dictionary.keys()), n_cases)
         else:
             y_pred = self._fit_predict(X, y)
 
@@ -325,13 +338,13 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
 
         Parameters
         ----------
-        X : np.ndarray
+        X : np.ndarray or list
             Input data, any number of channels, equal length series of shape ``(
-            n_instances, n_channels, n_timepoints)``
+            n_cases, n_channels, n_timepoints)``
             or 2D np.array (univariate, equal length series) of shape
-            ``(n_instances, n_timepoints)``
+            ``(n_cases, n_timepoints)``
             or list of numpy arrays (any number of channels, unequal length series)
-            of shape ``[n_instances]``, 2D np.array ``(n_channels, n_timepoints_i)``,
+            of shape ``[n_cases]``, 2D np.array ``(n_channels, n_timepoints_i)``,
             where ``n_timepoints_i`` is length of series ``i``. other types are
             allowed and converted into one of the above.
 
@@ -343,10 +356,13 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
             length input. In both situations, a ``ValueError`` is raised if X has a
             characteristic that the estimator does not have the capability for is
             passed.
+        y : np.ndarray
+            1D np.array of float or str, of shape ``(n_cases)`` - class labels
+            (ground truth) for fitting indices corresponding to instance indices in X.
 
         Returns
         -------
-        np.ndarray
+        probabilities : np.ndarray
             2D array of shape ``(n_cases, n_classes)`` - predicted class probabilities
             First dimension indices correspond to instance indices in X,
             second dimension indices correspond to class labels, (i, j)-th entry is
@@ -355,8 +371,8 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
         X, y, single_class = self._fit_setup(X, y)
 
         if single_class:
-            n_instances = get_n_cases(X)
-            y_proba = np.repeat([[1]], n_instances, axis=0)
+            n_cases = get_n_cases(X)
+            y_proba = np.repeat([[1]], n_cases, axis=0)
         else:
             y_proba = self._fit_predict_proba(X, y)
 
@@ -369,13 +385,13 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
 
         Parameters
         ----------
-        X : np.ndarray
+        X : np.ndarray or list
             Input data, any number of channels, equal length series of shape ``(
-            n_instances, n_channels, n_timepoints)``
+            n_cases, n_channels, n_timepoints)``
             or 2D np.array (univariate, equal length series) of shape
-            ``(n_instances, n_timepoints)``
+            ``(n_cases, n_timepoints)``
             or list of numpy arrays (any number of channels, unequal length series)
-            of shape ``[n_instances]``, 2D np.array ``(n_channels, n_timepoints_i)``,
+            of shape ``[n_cases]``, 2D np.array ``(n_channels, n_timepoints_i)``,
             where ``n_timepoints_i`` is length of series ``i``. other types are
             allowed and converted into one of the above.
 
@@ -387,42 +403,19 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
             length input. In both situations, a ``ValueError`` is raised if X has a
             characteristic that the estimator does not have the capability for is
             passed.
-
         y : np.ndarray
-            array shape ``(n_instances)`` - class labels (ground truth)
-            indices correspond to instance indices in X.
+            1D np.array of float or str, of shape ``(n_cases)`` - class labels
+            (ground truth) for fitting indices corresponding to instance indices in X.
 
         Returns
         -------
-        float
-             accuracy score of predict(X) vs y.
+        score : float
+             Accuracy score of predict(X) vs y.
         """
-        from sklearn.metrics import accuracy_score
-
         self.check_is_fitted()
+        self._check_y(y, len(X), update_classes=False)
         return accuracy_score(y, self.predict(X), normalize=True)
 
-    @classmethod
-    def get_test_params(cls, parameter_set="default"):
-        """Return testing parameter settings for the estimator.
-
-        Parameters
-        ----------
-        parameter_set : str, default="default"
-            Name of the set of test parameters to return, for use in tests. If no
-            special parameters are defined for a value, will return `"default"` set.
-            For classifiers, a "default" set of parameters should be provided for
-            general testing, and a "results_comparison" set for comparing against
-            previously recorded results if the general set does not produce suitable
-            probabilities to compare against.
-
-        Returns
-        -------
-        params : dict or list of dict, default={}
-            Parameters to create testing instances of the class.
-        """
-        return super().get_test_params(parameter_set=parameter_set)
-
     @abstractmethod
     def _fit(self, X, y):
         """Fit time series classifier to training data.
@@ -434,16 +427,16 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
         X : Train data
             guaranteed to be of a type in self.get_tag("X_inner_type")
             if ``self.get_tag("X_inner_type")`` equals "numpy3D":
-                3D np.ndarray of shape ``(n_instances, n_channels, n_timepoints)``
+                3D np.ndarray of shape ``(n_cases, n_channels, n_timepoints)``
             if ``self.get_tag("X_inner_type")`` equals "np-list":
-                list of 2D np.ndarray of shape ``(n_instances)``
-        y : np.array
-            1D of int, of shape ``(n_instances,)`` - class labels for fitting
-            indices correspond to instance indices in X
+                list of 2D np.ndarray of shape ``(n_cases)``
+        y : np.ndarray
+            1D np.array of float or str, of shape ``(n_cases)`` - class labels
+            (ground truth) for fitting indices corresponding to instance indices in X.
 
         Returns
         -------
-        self :
+        self : BaseClassifier
             Reference to self.
 
         Notes
@@ -464,14 +457,15 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
         X : Train data
             guaranteed to be of a type in self.get_tag("X_inner_type")
             if ``self.get_tag("X_inner_type")`` equals "numpy3D":
-                3D np.ndarray of shape ``(n_instances, n_channels, n_timepoints)``
+                3D np.ndarray of shape ``(n_cases, n_channels, n_timepoints)``
             if ``self.get_tag("X_inner_type")`` equals "np-list":
-                list of 2D np.ndarray of shape ``(n_instances)``
+                list of 2D np.ndarray of shape ``(n_cases)``
 
         Returns
         -------
-        y : 1D np.array of int, of shape (n_instances,) - predicted class labels
-            indices correspond to instance indices in X
+        predictions : np.ndarray
+            shape ``[n_cases]`` - predicted class labels indices correspond to
+            instance indices in
         """
         ...
 
@@ -487,16 +481,17 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
         X : Train data
             guaranteed to be of a type in self.get_tag("X_inner_type")
             if ``self.get_tag("X_inner_type")`` equals "numpy3D":
-                3D np.ndarray of shape ``(n_instances, n_channels, n_timepoints)``
+                3D np.ndarray of shape ``(n_cases, n_channels, n_timepoints)``
             if ``self.get_tag("X_inner_type")`` equals "np-list":
-                list of 2D np.ndarray of shape ``(n_instances)``
+                list of 2D np.ndarray of shape ``(n_cases)``
 
         Returns
         -------
-        y : 2D array of shape [n_instances, n_classes] - predicted class probabilities
-            1st dimension indices correspond to instance indices in X
-            2nd dimension indices correspond to possible labels (integers)
-            (i, j)-th entry is predictive probability that i-th instance is of class j
+        probabilities : np.ndarray
+            2D array of shape ``(n_cases, n_classes)`` - predicted class probabilities
+            First dimension indices correspond to instance indices in X,
+            second dimension indices correspond to class labels, (i, j)-th entry is
+            estimated probability that i-th instance is of class j
         """
         preds = self._predict(X)
         n_pred = len(preds)
@@ -507,9 +502,51 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
         return dists
 
     def _fit_predict(self, X, y) -> np.ndarray:
+        """Fits and predicts labels for sequences in X.
+
+        Parameters
+        ----------
+        X : Train data
+            guaranteed to be of a type in self.get_tag("X_inner_type")
+            if ``self.get_tag("X_inner_type")`` equals "numpy3D":
+                3D np.ndarray of shape ``(n_cases, n_channels, n_timepoints)``
+            if ``self.get_tag("X_inner_type")`` equals "np-list":
+                list of 2D np.ndarray of shape ``(n_cases)``
+        y : np.ndarray
+            1D np.array of float or str, of shape ``(n_cases)`` - class labels
+            (ground truth) for fitting indices corresponding to instance indices in X.
+
+        Returns
+        -------
+        predictions : np.ndarray
+            shape ``[n_cases]`` - predicted class labels indices correspond to
+            instance indices in
+        """
         return self._fit_predict_default(X, y, "predict")
 
     def _fit_predict_proba(self, X, y) -> np.ndarray:
+        """Fits and predicts labels probabilities for sequences in X.
+
+        Parameters
+        ----------
+        X : Train data
+            guaranteed to be of a type in self.get_tag("X_inner_type")
+            if ``self.get_tag("X_inner_type")`` equals "numpy3D":
+                3D np.ndarray of shape ``(n_cases, n_channels, n_timepoints)``
+            if ``self.get_tag("X_inner_type")`` equals "np-list":
+                list of 2D np.ndarray of shape ``(n_cases)``
+        y : np.ndarray
+            1D np.array of float or str, of shape ``(n_cases)`` - class labels
+            (ground truth) for fitting indices corresponding to instance indices in X.
+
+        Returns
+        -------
+        probabilities : np.ndarray
+            2D array of shape ``(n_cases, n_classes)`` - predicted class probabilities
+            First dimension indices correspond to instance indices in X,
+            second dimension indices correspond to class labels, (i, j)-th entry is
+            estimated probability that i-th instance is of class j
+        """
         return self._fit_predict_default(X, y, "predict_proba")
 
     def _fit_setup(self, X, y):
@@ -522,35 +559,41 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
         # return processed X and y, and whether there is only one class
         return X, y, len(self.classes_) == 1
 
-    def _check_y(self, y, n_cases):
-        # Check y valid input for classification task
+    def _check_y(self, y, n_cases, update_classes=True):
+        # Check y valid input for classification
         if not isinstance(y, (pd.Series, np.ndarray)):
             raise TypeError(
                 f"y must be a np.array or a pd.Series, but found type: {type(y)}"
             )
         if isinstance(y, np.ndarray) and y.ndim > 1:
             raise TypeError(f"y must be 1-dimensional, found {y.ndim} dimensions")
+
         # Check matching number of labels
         n_labels = y.shape[0]
         if n_cases != n_labels:
             raise ValueError(
-                f"Mismatch in number of cases. Number in X = {n_cases} nos in y = "
-                f"{n_labels}"
+                f"Mismatch in number of cases. Found X = {n_cases} and y = {n_labels}"
             )
+
         y_type = type_of_target(y)
         if y_type != "binary" and y_type != "multiclass":
             raise ValueError(
                 f"y type is {y_type} which is not valid for classification. "
-                f"Should be binary or multiclass according to type_of_target"
+                f"Should be binary or multiclass according to "
+                f"sklearn.utils.multiclass.type_of_target"
             )
+
         if isinstance(y, pd.Series):
             y = pd.Series.to_numpy(y)
+
         # remember class labels
-        self.classes_ = np.unique(y)
-        self.n_classes_ = self.classes_.shape[0]
-        self._class_dictionary = {}
-        for index, class_val in enumerate(self.classes_):
-            self._class_dictionary[class_val] = index
+        if update_classes:
+            self.classes_ = np.unique(y)
+            self.n_classes_ = self.classes_.shape[0]
+            self._class_dictionary = {}
+            for index, class_val in enumerate(self.classes_):
+                self._class_dictionary[class_val] = index
+
         return y
 
     def _fit_predict_default(self, X, y, method):
diff --git a/aeon/classification/deep_learning/_cnn.py b/aeon/classification/deep_learning/_cnn.py
index 4db6900e..10563255 100644
--- a/aeon/classification/deep_learning/_cnn.py
+++ b/aeon/classification/deep_learning/_cnn.py
@@ -216,10 +216,11 @@ class CNNClassifier(BaseDeepClassifier):
 
         Parameters
         ----------
-        X : np.ndarray of shape = (n_instances (n), n_channels (d), series_length (m))
-            The training input samples.
-        y : np.ndarray of shape n
-            The training data class labels.
+        X : np.ndarray
+            The training input samples of shape (n_cases, n_channels, n_timepoints)
+        y : np.ndarray
+            The training data class labels of shape (n_cases,).
+
 
         Returns
         -------
diff --git a/aeon/classification/deep_learning/_encoder.py b/aeon/classification/deep_learning/_encoder.py
index 2c77b70f..76daaf9a 100644
--- a/aeon/classification/deep_learning/_encoder.py
+++ b/aeon/classification/deep_learning/_encoder.py
@@ -199,10 +199,10 @@ class EncoderClassifier(BaseDeepClassifier):
 
         Parameters
         ----------
-        X : np.ndarray of shape = (n_instances (n), n_channels (d), series_length (m))
-            The training input samples.
-        y : np.ndarray of shape n
-            The training data class labels.
+        X : np.ndarray
+            The training input samples of shape (n_cases, n_channels, n_timepoints)
+        y : np.ndarray
+            The training data class labels of shape (n_cases,).
 
         Returns
         -------
diff --git a/aeon/classification/deep_learning/_fcn.py b/aeon/classification/deep_learning/_fcn.py
index f7255b4e..582d6f4f 100644
--- a/aeon/classification/deep_learning/_fcn.py
+++ b/aeon/classification/deep_learning/_fcn.py
@@ -212,10 +212,10 @@ class FCNClassifier(BaseDeepClassifier):
 
         Parameters
         ----------
-        X : np.ndarray of shape = (n_instances (n), n_channels (d), series_length (m))
-            The training input samples.
-        y : np.ndarray of shape n
-            The training data class labels.
+        X : np.ndarray
+            The training input samples of shape (n_cases, n_channels, n_timepoints)
+        y : np.ndarray
+            The training data class labels of shape (n_cases,).
 
         Returns
         -------
diff --git a/aeon/classification/deep_learning/_inception_time.py b/aeon/classification/deep_learning/_inception_time.py
index 0b9e9a8c..420ce177 100644
--- a/aeon/classification/deep_learning/_inception_time.py
+++ b/aeon/classification/deep_learning/_inception_time.py
@@ -233,10 +233,10 @@ class InceptionTimeClassifier(BaseClassifier):
 
         Parameters
         ----------
-        X : np.ndarray of shape = (n_instances (n), n_channels (c), n_timepoints (m))
-            The training input samples.
-        y : np.ndarray of shape n
-            The training data class labels.
+        X : np.ndarray
+            The training input samples of shape (n_cases, n_channels, n_timepoints)
+        y : np.ndarray
+            The training data class labels of shape (n_cases,).
 
         Returns
         -------
@@ -612,11 +612,13 @@ class IndividualInceptionClassifier(BaseDeepClassifier):
 
         Parameters
         ----------
-        X : array-like of shape = (n_instances, n_channels, n_timepoints)
-            The training input samples. If a 2D array-like is passed,
-            n_channels is assumed to be 1.
-        y : array-like, shape = (n_instances)
-            The training data class labels.
+        X : np.ndarray
+            The training input samples of,
+            shape (n_instances, n_channels, n_timepoints).
+            If a 2D array-like is passed, n_channels is assumed to be 1.
+        y : np.ndarray
+            The training data class labels of shape (n_instances,).
+
 
         Returns
         -------
diff --git a/aeon/classification/deep_learning/_lite_time.py b/aeon/classification/deep_learning/_lite_time.py
index 09c05724..048eb420 100644
--- a/aeon/classification/deep_learning/_lite_time.py
+++ b/aeon/classification/deep_learning/_lite_time.py
@@ -161,10 +161,10 @@ class LITETimeClassifier(BaseClassifier):
 
         Parameters
         ----------
-        X : np.ndarray of shape = (n_instances (n), n_channels (c), n_timepoints (m))
-            The training input samples.
-        y : np.ndarray of shape n
-            The training data class labels.
+        X : np.ndarray
+            The training input samples of shape (n_cases, n_channels, n_timepoints)
+        y : np.ndarray
+            The training data class labels of shape (n_cases,).
 
         Returns
         -------
@@ -456,11 +456,12 @@ class IndividualLITEClassifier(BaseDeepClassifier):
 
         Parameters
         ----------
-        X : array-like of shape = (n_instances, n_channels, n_timepoints)
-            The training input samples. If a 2D array-like is passed,
-            n_channels is assumed to be 1.
-        y : array-like, shape = (n_instances)
-            The training data class labels.
+        X : np.ndarray
+            The training input samples of,
+            shape (n_instances, n_channels, n_timepoints).
+            If a 2D array-like is passed, n_channels is assumed to be 1.
+        y : np.ndarray
+            The training data class labels of shape (n_instances,).
 
         Returns
         -------
diff --git a/aeon/classification/deep_learning/_mlp.py b/aeon/classification/deep_learning/_mlp.py
index 26bbfe93..241b0fc5 100644
--- a/aeon/classification/deep_learning/_mlp.py
+++ b/aeon/classification/deep_learning/_mlp.py
@@ -173,10 +173,10 @@ class MLPClassifier(BaseDeepClassifier):
 
         Parameters
         ----------
-        X : np.ndarray of shape = (n_instances (n), n_channels (d), series_length (m))
-            The training input samples.
-        y : np.ndarray of shape n
-            The training data class labels.
+        X : np.ndarray
+            The training input samples of shape (n_cases, n_channels, n_timepoints)
+        y : np.ndarray
+            The training data class labels of shape (n_cases,).
 
         Returns
         -------
diff --git a/aeon/classification/deep_learning/_resnet.py b/aeon/classification/deep_learning/_resnet.py
index fff1190f..639adbda 100644
--- a/aeon/classification/deep_learning/_resnet.py
+++ b/aeon/classification/deep_learning/_resnet.py
@@ -223,10 +223,10 @@ class ResNetClassifier(BaseDeepClassifier):
 
         Parameters
         ----------
-        X : np.ndarray of shape = (n_instances (n), n_channels (d), series_length (m))
-            The training input samples.
-        y : np.ndarray of shape n
-            The training data class labels.
+        X : np.ndarray
+            The training input samples of shape (n_cases, n_channels, n_timepoints)
+        y : np.ndarray
+            The training data class labels of shape (n_cases,).
 
         Returns
         -------
diff --git a/aeon/classification/deep_learning/_tapnet.py b/aeon/classification/deep_learning/_tapnet.py
index 90978573..28acc4f7 100644
--- a/aeon/classification/deep_learning/_tapnet.py
+++ b/aeon/classification/deep_learning/_tapnet.py
@@ -209,10 +209,10 @@ class TapNetClassifier(BaseDeepClassifier):
 
         Parameters
         ----------
-        X : np.ndarray of shape = (n_instances(n), n_channels(d), series_length(m))
-            Input training samples
-        y : np.ndarray of shape n
-            Input training class labels
+        X : np.ndarray
+            The training input samples of shape (n_cases, n_channels, n_timepoints)
+        y : np.ndarray
+            The training data class labels of shape (n_cases,).
 
         Returns
         -------
diff --git a/aeon/classification/distance_based/_shape_dtw.py b/aeon/classification/distance_based/_shape_dtw.py
index 50867979..b37ca244 100644
--- a/aeon/classification/distance_based/_shape_dtw.py
+++ b/aeon/classification/distance_based/_shape_dtw.py
@@ -115,11 +115,10 @@ class ShapeDTW(BaseClassifier):
 
         Parameters
         ----------
-        X : np.ndarray of shape = (n_cases, n_channels, n_timepoints)
-            or list of [n_cases] np.ndarray shape (n_channels, n_timepoints_i)
-            The training input samples.
-        y : array-like, shape = (n_cases)
-            The class labels.
+        X : np.ndarray
+            The training input samples of shape (n_cases, n_channels, n_timepoints)
+        y : np.ndarray
+            The training data class labels of shape (n_cases,).
 
         Returns
         -------
diff --git a/aeon/distances/__init__.py b/aeon/distances/__init__.py
index f3604163..e8915766 100644
--- a/aeon/distances/__init__.py
+++ b/aeon/distances/__init__.py
@@ -63,6 +63,8 @@ __all__ = [
     "shape_dtw_cost_matrix",
     "shape_dtw_alignment_path",
     "shape_dtw_pairwise_distance",
+    "sbd_distance",
+    "sbd_pairwise_distance",
 ]
 
 
@@ -123,6 +125,7 @@ from aeon.distances._msm import (
     msm_distance,
     msm_pairwise_distance,
 )
+from aeon.distances._sbd import sbd_distance, sbd_pairwise_distance
 from aeon.distances._shape_dtw import (
     shape_dtw_alignment_path,
     shape_dtw_cost_matrix,
diff --git a/aeon/distances/_distance.py b/aeon/distances/_distance.py
index f4113e25..4935d69c 100644
--- a/aeon/distances/_distance.py
+++ b/aeon/distances/_distance.py
@@ -49,6 +49,7 @@ from aeon.distances._msm import (
     msm_distance,
     msm_pairwise_distance,
 )
+from aeon.distances._sbd import sbd_distance, sbd_pairwise_distance
 from aeon.distances._shape_dtw import (
     shape_dtw_alignment_path,
     shape_dtw_cost_matrix,
@@ -222,6 +223,8 @@ def distance(
             kwargs.get("itakura_max_slope"),
             kwargs.get("warp_penalty", 1.0),
         )
+    elif metric == "sbd":
+        return sbd_distance(x, y, kwargs.get("standardize", True))
     else:
         if isinstance(metric, Callable):
             return metric(x, y, **kwargs)
@@ -385,6 +388,8 @@ def pairwise_distance(
             kwargs.get("itakura_max_slope"),
             kwargs.get("warp_penalty", 1.0),
         )
+    elif metric == "sbd":
+        return sbd_pairwise_distance(x, y, kwargs.get("standardize", True))
     else:
         if isinstance(metric, Callable):
             return _custom_func_pairwise(x, y, metric, **kwargs)
@@ -758,6 +763,7 @@ def get_distance_function(metric: Union[str, DistanceFunction]) -> DistanceFunct
     'squared'       distances.squared_distance
     'manhattan'     distances.manhattan_distance
     'minkowski'     distances.minkowski_distance
+    'sbd'           distances.sbd_distance
     =============== ========================================
 
     Parameters
@@ -814,6 +820,7 @@ def get_pairwise_distance_function(
     'squared'       distances.squared_pairwise_distance
     'manhattan'     distances.manhattan_pairwise_distance
     'minkowski'     distances.minkowski_pairwise_distance
+    'sbd'           distances.sbd_pairwise_distance
     =============== ========================================
 
     Parameters
@@ -1062,6 +1069,11 @@ DISTANCES = [
         "cost_matrix": shape_dtw_cost_matrix,
         "alignment_path": shape_dtw_alignment_path,
     },
+    {
+        "name": "sbd",
+        "distance": sbd_distance,
+        "pairwise_distance": sbd_pairwise_distance,
+    },
 ]
 
 DISTANCES_DICT = {d["name"]: d for d in DISTANCES}
diff --git a/aeon/distances/_sbd.py b/aeon/distances/_sbd.py
new file mode 100644
index 00000000..c3db181d
--- /dev/null
+++ b/aeon/distances/_sbd.py
@@ -0,0 +1,232 @@
+"""Shape-based distance (SBD) between two time series."""
+
+__maintainer__ = ["codelionx"]
+
+from typing import Optional
+
+import numpy as np
+from numba import njit, objmode
+from scipy.signal import correlate
+
+from aeon.distances._utils import reshape_pairwise_to_multiple
+
+
+@njit(cache=True, fastmath=True)
+def sbd_distance(x: np.ndarray, y: np.ndarray, standardize: bool = True) -> float:
+    r"""
+    Compute the shape-based distance (SBD) between two time series.
+
+    Shape-based distance (SBD) [1]_ is a normalized version of
+    cross-correlation (CC) that is shifting and scaling (if standardization
+    is used) invariant.
+
+    For two series, possibly of unequal length, :math:`\mathbf{x}=\{x_1,x_2,\ldots,
+    x_n\}` and :math:`\mathbf{y}=\{y_1,y_2, \ldots,y_m\}`, SBD works by (optionally)
+    first standardizing both time series using the z-score
+    (:math:`x' = \frac{x - \mu}{\sigma}`), then computing the cross-correlation
+    between x and y (:math:`CC(\mathbf{x}, \mathbf{y})`), then deviding it by the
+    geometric mean of both autocorrelations of the individual sequences to normalize
+    it to :math:`[-1, 1]` (coefficient normalization), and finally detecting the
+    position with the maximum normalized cross-correlation:
+
+    .. math::
+        SBD(\mathbf{x}, \mathbf{y}) = 1 - max_w\left( \frac{
+            CC_w(\mathbf{x}, \mathbf{y})
+        }{
+            \sqrt{ (\mathbf{x} \cdot \mathbf{x}) * (\mathbf{y} \cdot \mathbf{y}) }
+        }\right)
+
+    This distance measure has values between 0 and 2; 0 is perfect similarity.
+
+    The computation of the cross-correlation :math:`CC(\mathbf{x}, \mathbf{y})` for
+    all values of w requires :math:`O(m^2)` time, where m is the maximum time-series
+    length. We can however use the convolution theorem to our advantage, and use the
+    fast (inverse) fourier transform (FFT) to perform the computation of
+    :math:`CC(\mathbf{x}, \mathbf{y})` in :math:`O(m \cdot log(m))`:
+
+    .. math::
+        CC(x, y) = \mathcal{F}^{-1}\{
+            \mathcal{F}(\mathbf{x}) * \mathcal{F}(\mathbf{y})
+        \}
+
+    For multivariate time series, SBD is computed independently for each channel and
+    then averaged.
+
+    Parameters
+    ----------
+    x : np.ndarray
+        First time series, either univariate, shape ``(n_timepoints,)``, or
+        multivariate, shape ``(n_channels, n_timepoints)``.
+    y : np.ndarray
+        Second time series, either univariate, shape ``(n_timepoints,)``, or
+        multivariate, shape ``(n_channels, n_timepoints)``.
+    standardize : bool, default=True
+        Apply z-score to both input time series for standardization before
+        computing the distance. This makes SBD scaling invariant. Default is True.
+
+    Returns
+    -------
+    float
+        SBD distance between x and y.
+
+    Raises
+    ------
+    ValueError
+        If x and y are not 1D or 2D arrays.
+
+    See Also
+    --------
+    :func:`~aeon.distances.sbd_pairwise_distance` : Compute the shape-based
+        distance (SBD) between all pairs of time series.
+
+    References
+    ----------
+    .. [1] Paparrizos, John, and Luis Gravano: Fast and Accurate Time-Series
+           Clustering. ACM Transactions on Database Systems 42, no. 2 (2017):
+           8:1-8:49. https://doi.org/10.1145/3044711.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from aeon.distances import sbd_distance
+    >>> x = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
+    >>> y = np.array([[11, 12, 13, 14, 15, 16, 17, 18, 19, 20]])
+    >>> dist = sbd_distance(x, y)
+    """
+    if x.ndim == 1 and y.ndim == 1:
+        return _univariate_sbd_distance(x, y, standardize)
+    if x.ndim == 2 and y.ndim == 2:
+        if x.shape[0] == y.shape[0] == 1:
+            _x = x.ravel()
+            _y = y.ravel()
+            return _univariate_sbd_distance(_x, _y, standardize)
+        else:
+            # independent
+            nchannels = min(x.shape[0], y.shape[0])
+            distance = 0.0
+            for i in range(nchannels):
+                distance += _univariate_sbd_distance(x[i], y[i], standardize)
+            return distance / nchannels
+
+    raise ValueError("x and y must be 1D or 2D")
+
+
+@njit(cache=True, fastmath=True)
+def sbd_pairwise_distance(
+    x: np.ndarray, y: Optional[np.ndarray] = None, standardize: bool = True
+) -> np.ndarray:
+    """
+    Compute the shape-based distance (SBD) between all pairs of time series.
+
+    Parameters
+    ----------
+    x : np.ndarray
+        A collection of time series instances  of shape ``(n_instances, n_timepoints)``
+        or ``(n_instances, n_channels, n_timepoints)``.
+    y : np.ndarray or None, default=None
+        A single series or a collection of time series of shape ``(m_timepoints,)`` or
+        ``(m_instances, m_timepoints)`` or ``(m_instances, m_channels, m_timepoints)``.
+        If None, then the SBD is calculated between pairwise instances of x.
+    standardize : bool, default=True
+        Apply z-score to both input time series for standardization before
+        computing the distance. This makes SBD scaling invariant. Default is True.
+
+    Returns
+    -------
+    np.ndarray (n_instances, n_instances)
+        SBD matrix between the instances of x (and y).
+
+    Raises
+    ------
+    ValueError
+        If x is not 2D or 3D array when only passing x.
+        If x and y are not 1D, 2D or 3D arrays when passing both x and y.
+
+    See Also
+    --------
+    :func:`~aeon.distances.sbd_distance` : Compute the shape-based distance between
+        two time series.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from aeon.distances import sbd_pairwise_distance
+    >>> # Distance between each time series in a collection of time series
+    >>> X = np.array([[[1, 2, 3]],[[4, 5, 6]], [[7, 8, 9]]])
+    >>> sbd_pairwise_distance(X)
+    array([[0., 0., 0.],
+           [0., 0., 0.],
+           [0., 0., 0.]])
+
+    >>> # Distance between two collections of time series
+    >>> X = np.array([[[1, 2, 3]],[[4, 5, 6]], [[7, 8, 9]]])
+    >>> y = np.array([[[11, 12, 13]],[[14, 15, 16]], [[17, 18, 19]]])
+    >>> sbd_pairwise_distance(X, y)
+    array([[0., 0., 0.],
+           [0., 0., 0.],
+           [0., 0., 0.]])
+
+    >>> X = np.array([[[1, 2, 3]],[[4, 5, 6]], [[7, 8, 9]]])
+    >>> y_univariate = np.array([[11, 12, 13],[14, 15, 16], [17, 18, 19]])
+    >>> sbd_pairwise_distance(X, y_univariate)
+    array([[0.],
+           [0.],
+           [0.]])
+    """
+    if y is None:
+        # To self
+        if x.ndim == 3:
+            return _sbd_pairwise_distance_single(x, standardize)
+        elif x.ndim == 2:
+            _X = x.reshape((x.shape[0], 1, x.shape[1]))
+            return _sbd_pairwise_distance_single(_X, standardize)
+        raise ValueError("X must be 2D or 3D")
+
+    _x, _y = reshape_pairwise_to_multiple(x, y)
+    return _sbd_pairwise_distance(_x, _y, standardize)
+
+
+@njit(cache=True, fastmath=True)
+def _sbd_pairwise_distance_single(x: np.ndarray, standardize: bool) -> np.ndarray:
+    n_instances = x.shape[0]
+    distances = np.zeros((n_instances, n_instances))
+
+    for i in range(n_instances):
+        for j in range(i + 1, n_instances):
+            distances[i, j] = sbd_distance(x[i], x[j], standardize)
+            distances[j, i] = distances[i, j]
+
+    return distances
+
+
+@njit(cache=True, fastmath=True)
+def _sbd_pairwise_distance(
+    x: np.ndarray, y: np.ndarray, standardize: bool
+) -> np.ndarray:
+    n_instances = x.shape[0]
+    m_instances = y.shape[0]
+    distances = np.zeros((n_instances, m_instances))
+
+    for i in range(n_instances):
+        for j in range(m_instances):
+            distances[i, j] = sbd_distance(x[i], y[j], standardize)
+    return distances
+
+
+@njit(cache=True, fastmath=True)
+def _univariate_sbd_distance(x: np.ndarray, y: np.ndarray, standardize: bool) -> float:
+    x = x.astype(np.float64)
+    y = y.astype(np.float64)
+
+    if standardize:
+        if x.size == 1 or y.size == 1:
+            return 0.0
+
+        x = (x - np.mean(x)) / np.std(x)
+        y = (y - np.mean(y)) / np.std(y)
+
+    with objmode(a="float64[:]"):
+        a = correlate(x, y, method="fft")
+
+    b = np.sqrt(np.dot(x, x) * np.dot(y, y))
+    return np.abs(1.0 - np.max(a / b))
diff --git a/aeon/regression/base.py b/aeon/regression/base.py
index 17cc54c1..8c9c2b84 100644
--- a/aeon/regression/base.py
+++ b/aeon/regression/base.py
@@ -17,18 +17,24 @@ State:
     fitted state inspection - check_is_fitted()
 """
 
+__maintainer__ = []
 __all__ = [
     "BaseRegressor",
 ]
-__maintainer__ = []
+
 import time
 from abc import ABC, abstractmethod
 from typing import final
 
 import numpy as np
 import pandas as pd
+from deprecated.sphinx import deprecated
+from sklearn.model_selection import cross_val_predict
+from sklearn.utils.multiclass import type_of_target
 
 from aeon.base import BaseCollectionEstimator
+from aeon.base._base import _clone_estimator
+from aeon.performance_metrics.forecasting import mean_squared_error
 from aeon.utils.sklearn import is_sklearn_transformer
 
 
@@ -39,27 +45,44 @@ class BaseRegressor(BaseCollectionEstimator, ABC):
     regressors have to implement. Attributes with a underscore suffix are set in the
     method fit.
 
-    Parameters
+    Attributes
     ----------
     fit_time_ : int
         Time (in milliseconds) for fit to run.
-    _class_dictionary : dict
-        Dictionary mapping classes_ onto integers 0...n_classes_-1.
-    _n_jobs : int, default =1
+    _n_jobs : int
         Number of threads to use in fit as determined by n_jobs.
+
+    fit_time_ : int
+        Time (in milliseconds) for ``fit`` to run.
+    _n_jobs : int
+        Number of threads to use in estimator methods such as ``fit`` and ``predict``.
+        Determined by the ``n_jobs`` parameter if present.
+    _estimator_type : string
+        The type of estimator. Required by some ``sklearn`` tools, set to "regressor".
     """
 
     _tags = {
         "capability:train_estimate": False,
         "capability:contractable": False,
-        "capability:multithreading": False,
     }
 
     def __init__(self):
+        # reserved attributes written to in fit
+        self.fit_time_ = -1
+        self._n_jobs = 1
+
+        # required for compatibility with some sklearn interfaces
         self._estimator_type = "regressor"
 
         super().__init__()
 
+    # TODO: remove in v0.9.0
+    @deprecated(
+        version="0.8.0",
+        reason="The BaseRegressor __rmul__ (*) functionality will be removed "
+        "in v0.9.0.",
+        category=FutureWarning,
+    )
     def __rmul__(self, other):
         """Magic * method, return concatenated RegressorPipeline, transformers on left.
 
@@ -103,20 +126,31 @@ class BaseRegressor(BaseCollectionEstimator, ABC):
 
         Parameters
         ----------
-        X : np.ndarray
-            train data of shape ``(n_instances, n_channels, n_timepoints)`` for any
-            number of channels, equal length series, ``(n_instances, n_timepoints)``
-            for univariate, equal length series.
-            or list of shape ``[n_instances]`` of 2D np.array shape ``(n_channels,
-            n_timepoints_i)``, where n_timepoints_i is length of series i
-            other types are allowed and converted into one of the above.
+        X : np.ndarray or list
+            Input data, any number of channels, equal length series of shape ``(
+            n_cases, n_channels, n_timepoints)``
+            or 2D np.array (univariate, equal length series) of shape
+            ``(n_cases, n_timepoints)``
+            or list of numpy arrays (any number of channels, unequal length series)
+            of shape ``[n_cases]``, 2D np.array ``(n_channels, n_timepoints_i)``,
+            where ``n_timepoints_i`` is length of series ``i``. Other types are
+            allowed and converted into one of the above.
+
+            Different estimators have different capabilities to handle different
+            types of input. If `self.get_tag("capability:multivariate")`` is False,
+            they cannot handle multivariate series, so either ``n_channels == 1`` is
+            true or X is 2D of shape ``(n_cases, n_timepoints)``. If ``self.get_tag(
+            "capability:unequal_length")`` is False, they cannot handle unequal
+            length input. In both situations, a ``ValueError`` is raised if X has a
+            characteristic that the estimator does not have the capability for is
+            passed.
         y : np.ndarray
-            1D np.array of float, of shape ``(n_instances)`` - regression targets or
-            fitting indices correspond to instance indices in X.
+            1D np.array of float, of shape ``(n_cases)`` - regression targets
+            (ground truth) for fitting indices corresponding to instance indices in X.
 
         Returns
         -------
-        BaseCollectionEstimator
+        self : BaseRegressor
             Reference to self.
 
         Notes
@@ -124,12 +158,12 @@ class BaseRegressor(BaseCollectionEstimator, ABC):
         Changes state by creating a fitted model that updates attributes
         ending in "_" and sets is_fitted flag to True.
         """
-        self.reset()
-        _start_time = int(round(time.time() * 1000))
-        X = self._preprocess_collection(X)
-        y = self._check_y(y, self.metadata_["n_cases"])
+        start = int(round(time.time() * 1000))
+        X, y = self._fit_setup(X, y)
+
         self._fit(X, y)
-        self.fit_time_ = int(round(time.time() * 1000)) - _start_time
+
+        self.fit_time_ = int(round(time.time() * 1000)) - start
         # this should happen last
         self._is_fitted = True
         return self
@@ -140,51 +174,123 @@ class BaseRegressor(BaseCollectionEstimator, ABC):
 
         Parameters
         ----------
-        X : np.ndarray
-            train data of shape ``(n_instances, n_channels, n_timepoints)`` for any
-            number of channels, equal length series, ``(n_instances, n_timepoints)``
-            for univariate, equal length series.
-            or list of shape ``[n_instances]`` of 2D np.array shape ``(n_channels,
-            n_timepoints_i)``, where n_timepoints_i is length of series i
+        X : np.ndarray or list
+            Input data, any number of channels, equal length series of shape ``(
+            n_cases, n_channels, n_timepoints)``
+            or 2D np.array (univariate, equal length series) of shape
+            ``(n_cases, n_timepoints)``
+            or list of numpy arrays (any number of channels, unequal length series)
+            of shape ``[n_cases]``, 2D np.array ``(n_channels, n_timepoints_i)``,
+            where ``n_timepoints_i`` is length of series ``i``
             other types are allowed and converted into one of the above.
 
+            Different estimators have different capabilities to handle different
+            types of input. If `self.get_tag("capability:multivariate")`` is False,
+            they cannot handle multivariate series, so either ``n_channels == 1`` is
+            true or X is 2D of shape ``(n_cases, n_timepoints)``. If ``self.get_tag(
+            "capability:unequal_length")`` is False, they cannot handle unequal
+            length input. In both situations, a ``ValueError`` is raised if X has a
+            characteristic that the estimator does not have the capability for is
+            passed.
+
         Returns
         -------
-        np.ndarray
-            1D np.array of float, of shape (n_instances) - predicted regression labels
+        predictions : np.ndarray
+            1D np.array of float, of shape (n_cases) - predicted regression labels
             indices correspond to instance indices in X
         """
         self.check_is_fitted()
         X = self._preprocess_collection(X)
         return self._predict(X)
 
+    @final
+    def fit_predict(self, X, y) -> np.ndarray:
+        """Fits the regressor and predicts class labels for X.
+
+        fit_predict produces prediction estimates using just the train data.
+        By default, this is through 10x cross validation, although some estimators may
+        utilise specialist techniques such as out-of-bag estimates or leave-one-out
+        cross-validation.
+
+        Regressors which override _fit_predict will have the
+        ``capability:train_estimate`` tag set to True.
+
+        Generally, this will not be the same as fitting on the whole train data
+        then making train predictions. To do this, you should call fit(X,y).predict(X)
+
+        Parameters
+        ----------
+        X : np.ndarray or list
+            Input data, any number of channels, equal length series of shape ``(
+            n_cases, n_channels, n_timepoints)``
+            or 2D np.array (univariate, equal length series) of shape
+            ``(n_cases, n_timepoints)``
+            or list of numpy arrays (any number of channels, unequal length series)
+            of shape ``[n_cases]``, 2D np.array ``(n_channels, n_timepoints_i)``,
+            where ``n_timepoints_i`` is length of series ``i``. other types are
+            allowed and converted into one of the above.
+
+            Different estimators have different capabilities to handle different
+            types of input. If `self.get_tag("capability:multivariate")`` is False,
+            they cannot handle multivariate series, so either ``n_channels == 1`` is
+            true or X is 2D of shape ``(n_cases, n_timepoints)``. If ``self.get_tag(
+            "capability:unequal_length")`` is False, they cannot handle unequal
+            length input. In both situations, a ``ValueError`` is raised if X has a
+            characteristic that the estimator does not have the capability for is
+            passed.
+        y : np.ndarray
+            1D np.array of float, of shape ``(n_cases)`` - regression targets
+            (ground truth) for fitting indices corresponding to instance indices in X.
+
+        Returns
+        -------
+        predictions : np.ndarray
+            1D np.array of float, of shape (n_cases) - predicted regression labels
+            indices correspond to instance indices in X
+        """
+        X, y = self._fit_setup(X, y)
+
+        y_pred = self._fit_predict(X, y)
+
+        # this should happen last
+        self._is_fitted = True
+        return y_pred
+
     def score(self, X, y) -> float:
         """Scores predicted labels against ground truth labels on X.
 
         Parameters
         ----------
-        X : np.ndarray
-            train data of shape ``(n_instances, n_channels, n_timepoints)`` for any
-            number of channels, equal length series, ``(n_instances, n_timepoints)``
-            for univariate, equal length series.
-            or list of shape ``[n_instances]`` of 2D np.array shape ``(n_channels,
-            n_timepoints_i)``, where n_timepoints_i is length of series i
-            other types are allowed and converted into one of the above.
+        X : np.ndarray or list
+            Input data, any number of channels, equal length series of shape ``(
+            n_cases, n_channels, n_timepoints)``
+            or 2D np.array (univariate, equal length series) of shape
+            ``(n_cases, n_timepoints)``
+            or list of numpy arrays (any number of channels, unequal length series)
+            of shape ``[n_cases]``, 2D np.array ``(n_channels, n_timepoints_i)``,
+            where ``n_timepoints_i`` is length of series ``i``. other types are
+            allowed and converted into one of the above.
+
+            Different estimators have different capabilities to handle different
+            types of input. If `self.get_tag("capability:multivariate")`` is False,
+            they cannot handle multivariate series, so either ``n_channels == 1`` is
+            true or X is 2D of shape ``(n_cases, n_timepoints)``. If ``self.get_tag(
+            "capability:unequal_length")`` is False, they cannot handle unequal
+            length input. In both situations, a ``ValueError`` is raised if X has a
+            characteristic that the estimator does not have the capability for is
+            passed.
         y : np.ndarray
-            1D np.array of float, of shape ``(n_instances)`` - regression targets or
-            fitting indices correspond to instance indices in X.
+            1D np.array of float, of shape ``(n_cases)`` - regression targets
+            (ground truth) for fitting indices corresponding to instance indices in X.
 
         Returns
         -------
-        float, R-squared score of predict(X) vs y
+        score : float
+            MSE score of predict(X) vs y
         """
-        from sklearn.metrics import r2_score
-
         self.check_is_fitted()
-        if isinstance(y, pd.Series):
-            y = pd.Series.to_numpy(y)
-        y = y.astype("float")
-        return r2_score(y, self.predict(X))
+        y = self._check_y(y, len(X))
+        return mean_squared_error(y, self.predict(X))
 
     @abstractmethod
     def _fit(self, X, y):
@@ -194,15 +300,20 @@ class BaseRegressor(BaseCollectionEstimator, ABC):
 
         Parameters
         ----------
-        X : guaranteed to be of a type in self.get_tag("X_inner_type")
-            if self.get_tag("X_inner_type") = "numpy3D":
-                3D np.ndarray of shape = (n_instances, n_channels, n_timepoints)
-        y : 1D np.array of float, of shape (n_instances) - regression labels for
-        fitting indices correspond to instance indices in X
+        X : Train data
+            guaranteed to be of a type in self.get_tag("X_inner_type")
+            if ``self.get_tag("X_inner_type")`` equals "numpy3D":
+                3D np.ndarray of shape ``(n_cases, n_channels, n_timepoints)``
+            if ``self.get_tag("X_inner_type")`` equals "np-list":
+                list of 2D np.ndarray of shape ``(n_cases)``
+        y : np.ndarray
+            1D np.array of float, of shape ``(n_cases)`` - regression targets for
+            fitting indices corresponding to instance indices in X.
 
         Returns
         -------
-        self : Reference to self.
+        self : BaseRegressor
+            Reference to self.
 
         Notes
         -----
@@ -218,17 +329,68 @@ class BaseRegressor(BaseCollectionEstimator, ABC):
 
         Parameters
         ----------
-        X : guaranteed to be of a type in self.get_tag("X_inner_type")
-            if self.get_tag("X_inner_type") = "numpy3D":
-                3D np.ndarray of shape = (n_instances, n_channels, n_timepoints)
+        X : Train data
+            guaranteed to be of a type in self.get_tag("X_inner_type")
+            if ``self.get_tag("X_inner_type")`` equals "numpy3D":
+                3D np.ndarray of shape ``(n_cases, n_channels, n_timepoints)``
+            if ``self.get_tag("X_inner_type")`` equals "np-list":
+                list of 2D np.ndarray of shape ``(n_cases)``
 
         Returns
         -------
-        y : 1D np.array of float, of shape (n_instances) - predicted regression labels
+        predictions : np.ndarray
+            1D np.array of float, of shape (n_cases) - predicted regression labels
             indices correspond to instance indices in X
         """
         ...
 
+    def _fit_predict(self, X, y) -> np.ndarray:
+        """Fits and predicts labels for sequences in X.
+
+        Parameters
+        ----------
+        X : Train data
+            guaranteed to be of a type in self.get_tag("X_inner_type")
+            if ``self.get_tag("X_inner_type")`` equals "numpy3D":
+                3D np.ndarray of shape ``(n_cases, n_channels, n_timepoints)``
+            if ``self.get_tag("X_inner_type")`` equals "np-list":
+                list of 2D np.ndarray of shape ``(n_cases)``
+        y : np.ndarray
+            1D np.array of float, of shape ``(n_cases)`` - regression targets
+            (ground truth) for fitting indices corresponding to instance indices in X.
+
+        Returns
+        -------
+        predictions : np.ndarray
+            1D np.array of float, of shape (n_cases) - predicted regression labels
+            indices correspond to instance indices in X
+        """
+        # fit the regressor
+        self._fit(X, y)
+
+        # predict using cross-validation
+        random_state = getattr(self, "random_state", None)
+        estimator = _clone_estimator(self, random_state)
+
+        return cross_val_predict(
+            estimator,
+            X=X,
+            y=y,
+            cv=10,
+            method="predict",
+            n_jobs=self._n_jobs,
+        )
+
+    def _fit_setup(self, X, y):
+        # reset estimator at the start of fit
+        self.reset()
+
+        X = self._preprocess_collection(X)
+        y = self._check_y(y, self.metadata_["n_cases"])
+
+        # return processed X and y
+        return X, y
+
     def _check_y(self, y, n_cases):
         # Check y valid input for regression
         if not isinstance(y, (pd.Series, np.ndarray)):
@@ -237,18 +399,29 @@ class BaseRegressor(BaseCollectionEstimator, ABC):
             )
         if isinstance(y, np.ndarray) and y.ndim > 1:
             raise TypeError(f"y must be 1-dimensional, found {y.ndim} dimensions")
+
         # Check matching number of labels
         n_labels = len(y)
         if n_cases != n_labels:
             raise ValueError(
-                f"Mismatch in number of cases. Number in X = {n_cases} nos in y = "
-                f"{n_labels}"
+                f"Mismatch in number of cases. Found X = {n_cases} and y = {n_labels}"
+            )
+
+        y_type = type_of_target(y)
+        if y_type != "continuous" and y_type != "binary" and y_type != "multiclass":
+            raise ValueError(
+                f"y type is {y_type} which is not valid for regression. "
+                f"Should be continuous, binary or multiclass according to "
+                f"sklearn.utils.multiclass.type_of_target"
             )
+
         if isinstance(y, pd.Series):
             y = pd.Series.to_numpy(y)
-        if isinstance(y[0], str):
+
+        if any([isinstance(label, str) for label in y]):
             raise ValueError(
                 "y contains strings, cannot fit a regressor. If suitable, convert "
-                "to string."
+                "to floats or consider classification."
             )
-        return y
+
+        return y.astype(float)
diff --git a/aeon/regression/deep_learning/_cnn.py b/aeon/regression/deep_learning/_cnn.py
index af7fbacf..d483b48c 100644
--- a/aeon/regression/deep_learning/_cnn.py
+++ b/aeon/regression/deep_learning/_cnn.py
@@ -221,10 +221,10 @@ class CNNRegressor(BaseDeepRegressor):
 
         Parameters
         ----------
-        X : np.ndarray of shape = (n_instances (n), n_channels (d), series_length (m))
-            The training input samples.
-        y : np.ndarray of shape n
-            The training data target values.
+        X : np.ndarray
+            The training input samples of shape (n_cases, n_channels, n_timepoints).
+        y : np.ndarray
+            The training data target values of shape (n_cases,).
 
         Returns
         -------
diff --git a/aeon/regression/deep_learning/_fcn.py b/aeon/regression/deep_learning/_fcn.py
index fb3505f7..67b1254b 100644
--- a/aeon/regression/deep_learning/_fcn.py
+++ b/aeon/regression/deep_learning/_fcn.py
@@ -211,10 +211,10 @@ class FCNRegressor(BaseDeepRegressor):
 
         Parameters
         ----------
-        X : np.ndarray of shape = (n_instances (n), n_channels (d), series_length (m))
-            The training input samples.
-        y : np.ndarray of shape n
-            The training data target values.
+        X : np.ndarray
+            The training input samples of shape (n_cases, n_channels, n_timepoints).
+        y : np.ndarray
+            The training data target values of shape (n_cases,).
 
         Returns
         -------
diff --git a/aeon/regression/deep_learning/_inception_time.py b/aeon/regression/deep_learning/_inception_time.py
index 538ca13a..5c763d09 100644
--- a/aeon/regression/deep_learning/_inception_time.py
+++ b/aeon/regression/deep_learning/_inception_time.py
@@ -237,10 +237,10 @@ class InceptionTimeRegressor(BaseRegressor):
 
         Parameters
         ----------
-        X : np.ndarray of shape (n_instances, n_channels, series_length)
-            The training input samples.
-        y : np.ndarray of shape n
-            The training data target values.
+        X : np.ndarray
+            The training input samples of shape (n_cases, n_channels, n_timepoints).
+        y : np.ndarray
+            The training data target values of shape (n_cases,).
 
         Returns
         -------
@@ -588,11 +588,12 @@ class IndividualInceptionRegressor(BaseDeepRegressor):
 
         Parameters
         ----------
-        X : np.ndarray of shape (n_instances, n_channels, series_length)
-            The training input samples. If a 2D array-like is passed,
-            n_channels is assumed to be 1.
-        y : np.ndarray of shape (n_instances)
-            The training data target values.
+        X : np.ndarray
+            The training input samples of,
+            shape (n_instances, n_channels, n_timepoints).
+            If a 2D array-like is passed, n_channels is assumed to be 1.
+        y : np.ndarray
+            The training data target values of shape (n_instances,).
 
         Returns
         -------
diff --git a/aeon/regression/deep_learning/_resnet.py b/aeon/regression/deep_learning/_resnet.py
index 22d0d5d3..8455c159 100644
--- a/aeon/regression/deep_learning/_resnet.py
+++ b/aeon/regression/deep_learning/_resnet.py
@@ -232,10 +232,10 @@ class ResNetRegressor(BaseDeepRegressor):
 
         Parameters
         ----------
-        X : np.ndarray of shape = (n_instances (n), n_channels (d), series_length (m))
-            The training input samples.
-        y : np.ndarray of shape n
-            The training data target values.
+        X : np.ndarray
+            The training input samples of shape (n_cases, n_channels, n_timepoints).
+        y : np.ndarray
+            The training data target values of shape (n_cases,).
 
         Returns
         -------
diff --git a/aeon/regression/deep_learning/_tapnet.py b/aeon/regression/deep_learning/_tapnet.py
index f746b26b..8ff83538 100644
--- a/aeon/regression/deep_learning/_tapnet.py
+++ b/aeon/regression/deep_learning/_tapnet.py
@@ -200,10 +200,10 @@ class TapNetRegressor(BaseDeepRegressor):
 
         Parameters
         ----------
-        X   : np.ndarray of shape = (n_instances(n), n_channels(d), series_length(m))
-            Input training samples
-        y   : np.ndarray of shape n
-            Input training responses
+        X : np.ndarray
+            The training input samples of shape (n_cases, n_channels, n_timepoints).
+        y : np.ndarray
+            The training data target values of shape (n_cases,).
 
         Returns
         -------
diff --git a/docs/api_reference/distances.rst b/docs/api_reference/distances.rst
index 9780e827..ef799123 100644
--- a/docs/api_reference/distances.rst
+++ b/docs/api_reference/distances.rst
@@ -128,6 +128,18 @@ Move-Split-Merge (MSM)
     msm_cost_matrix
     msm_alignment_path
 
+Shape-based Distance (SBD)
+--------------------------
+
+.. currentmodule:: aeon.distances
+
+.. autosummary::
+    :toctree: auto_generated/
+    :template: function.rst
+
+    sbd_distance
+    sbd_pairwise_distance
+
 Shape Dynamic Time Warping (Shape DTW)
 --------------------------------------
 


Test patch:
diff --git a/aeon/classification/tests/test_base.py b/aeon/classification/tests/test_base.py
index 77cf374c..f2e079a3 100644
--- a/aeon/classification/tests/test_base.py
+++ b/aeon/classification/tests/test_base.py
@@ -233,7 +233,7 @@ def test_predict_proba_default():
     cls = MockClassifier()
 
     # fails if not fitted
-    with pytest.raises(KeyError):
+    with pytest.raises(ValueError, match="negative dimensions are not allowed"):
         cls._predict_proba(X)
 
     cls.fit(X, y)
diff --git a/aeon/distances/tests/test_expected_results.py b/aeon/distances/tests/test_expected_results.py
index 2c20ec80..24de5ab2 100644
--- a/aeon/distances/tests/test_expected_results.py
+++ b/aeon/distances/tests/test_expected_results.py
@@ -97,6 +97,13 @@ _expected_distance_results = {
         4.788349892989078,
         28.544576071968976,
     ],
+    "sbd": [
+        0.0,
+        0.33449276,
+        0.53488311,
+        0.59674837,
+        0.51688041,
+    ],
 }
 
 _expected_distance_results_params = {
diff --git a/aeon/distances/tests/test_numba_distance_parameters.py b/aeon/distances/tests/test_numba_distance_parameters.py
index 2eabbe40..e7cd0238 100644
--- a/aeon/distances/tests/test_numba_distance_parameters.py
+++ b/aeon/distances/tests/test_numba_distance_parameters.py
@@ -69,6 +69,7 @@ DIST_PARAMS = {
     "msm": BASIC_BOUNDING_PARAMS + [{"independent": False}, {"c": 0.1}],
     "adtw": BASIC_BOUNDING_PARAMS + [{"warp_penalty": 5.0}],
     "minkowski": [{"p": 1.0}, {"p": 2.0}],
+    "sbd": [{"standardize": False}],
 }
 
 
diff --git a/aeon/regression/tests/test_base.py b/aeon/regression/tests/test_base.py
index af7b17c7..0d06236a 100644
--- a/aeon/regression/tests/test_base.py
+++ b/aeon/regression/tests/test_base.py
@@ -168,4 +168,4 @@ def test_score():
     x_test, y_test = load_covid_3month(split="test")
     dummy.fit(x_train, y_train)
     r = dummy.score(x_test, y_test)
-    np.testing.assert_almost_equal(r, -0.004303695, decimal=6)
+    np.testing.assert_almost_equal(r, 0.0019998715745554777, decimal=6)