Repository: aeon-toolkit/aeon
Pull request #820 (2023-10-12 19:04:04)

[ENH] Adapt ShapeletTransform to work with unequal length
this adapts the RandomShapeletTransform and the ShapeletTransformClassifier to accept unequal length series through np-list. This is simply done by avoiding calls to shape and using len instead. There is however a numba issue with using "reflective" lists. It is warning about deprecation and suggesting using using TypedList. However, there were issues with this when we turn off numba for overnight tests, I would appreciate any advice on how to handle this.



sweep-ai[bot]: ## Apply [Sweep Rules](https://docs.sweep.dev/usage/config#rules) to your PR?
- [ ] **Apply:**  There should not be large chunks of code that are just commented out. Docstrings and explanations in code are okay though.
- [ ] **Apply:**  Update the relevant API page in 'docs/api_reference/' when new public functions and classes are added and not included in the API documentation. Only add functions and classes which are not already in the relevant API documentation and avoid duplicate entries.
baraline: > this adapts the RandomShapeletTransform and the ShapeletTransformClassifier to accept unequal length series through np-list. This is simply done by avoiding calls to shape and using len instead. There is however a numba issue with using "reflective" lists. It is warning about deprecation and suggesting using using TypedList. However, there were issues with this when we turn off numba for overnight tests, I would appreciate any advice on how to handle this.

One workaround to work with uneven length series in numba is to create a numpy array of the maximum length within the inputs, and then have another array of integers which store the true length of the series. Then, when making operations on a sample, you simply call `X[i_sample, :, :length[i_sample]]`. It is a bit of a ductape fix, but it should work for the overnight tests also.
TonyBagnall: > > this adapts the RandomShapeletTransform and the ShapeletTransformClassifier to accept unequal length series through np-list. This is simply done by avoiding calls to shape and using len instead. There is however a numba issue with using "reflective" lists. It is warning about deprecation and suggesting using using TypedList. However, there were issues with this when we turn off numba for overnight tests, I would appreciate any advice on how to handle this.
> 
> One workaround to work with uneven length series in numba is to create a numpy array `X` of the maximum length within the inputs, and then have another array of integers `length` which store the true length of the series. Then, when making operations on a sample, you simply call `X[i_sample, :, :length[i_sample]]`. It is a bit of a duct tape fix, but it should work for the overnight tests also.

thats quite involved and will require either a definition of a bespoke ArrayList like data structure or a lot of internal changes. The list solution here requires three changes, and I doubt there is much performance hit, I can experiment to see. 

This solution will be fine with the overnights, but might start failing with numba updates ate some point, so may need to resolve the typed list through some ugly switching


review-notebook-app[bot]: Check out this pull request on&nbsp; <a href="https://app.reviewnb.com/aeon-toolkit/aeon/pull/820"><img align="absmiddle"  alt="ReviewNB" height="28" class="BotMessageButtonImage" src="https://raw.githubusercontent.com/ReviewNB/support/master/images/button_reviewnb.png"/></a> 

 See visual diffs & provide feedback on Jupyter Notebooks. 

---

 <i>Powered by <a href='https://www.reviewnb.com/?utm_source=gh'>ReviewNB</a></i>

Patch:
diff --git a/.gitattributes b/.gitattributes
index 3fcf907c..d1a58853 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,1 +1,1 @@
-aeon/datasets/data/* linguist-vendored
+*.ts linguist-detectable=false
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 4584d7bc..4ae18444 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -47,19 +47,14 @@ Please go through the checklist below. Please feel free to remove points if they
 -->
 
 ##### For all contributions
-- [ ] I've added myself to the [list of contributors](https://github.com/aeon-toolkit/aeon/blob/main/.all-contributorsrc).
-Alternatively, you can use the [@all-contributors](https://allcontributors.org/docs/en/bot/usage)
-bot to do this for you.
-- [ ] The PR title starts with either [ENH], [MNT], [DOC], [BUG] or [GOV] indicating
-whether the PR topic is related to enhancement, maintenance, documentation, bugs or
-governance.
+- [ ] I've added myself to the [list of contributors](https://github.com/aeon-toolkit/aeon/blob/main/.all-contributorsrc). Alternatively, you can use the [@all-contributors](https://allcontributors.org/docs/en/bot/usage) bot to do this for you.
+- [ ] The PR title starts with either [ENH], [MNT], [DOC], [BUG] or [GOV] indicating whether the PR topic is related to enhancement, maintenance, documentation, bugs or governance.
 
 ##### For new estimators and functions
 - [ ] I've added the estimator to the online [API documentation](https://www.aeon-toolkit.org/en/latest/api_reference.html).
 
 ##### For developers with write access
-- [ ] Optionally, I've updated aeon's [CODEOWNERS](https://github.com/aeon-toolkit/aeon/blob/main/CODEOWNERS)
-to receive notifications about future changes to these files.
+- [ ] Optionally, I've updated aeon's [CODEOWNERS](https://github.com/aeon-toolkit/aeon/blob/main/CODEOWNERS) to receive notifications about future changes to these files.
 
 
 <!--
diff --git a/aeon/classification/shapelet_based/_stc.py b/aeon/classification/shapelet_based/_stc.py
index 2d6d29d9..d398af11 100644
--- a/aeon/classification/shapelet_based/_stc.py
+++ b/aeon/classification/shapelet_based/_stc.py
@@ -132,6 +132,8 @@ class ShapeletTransformClassifier(BaseClassifier):
         "capability:train_estimate": True,
         "capability:contractable": True,
         "capability:multithreading": True,
+        "capability:unequal_length": True,
+        "X_inner_mtype": ["np-list", "numpy3D"],
         "algorithm_type": "shapelet",
     }
 
@@ -164,8 +166,7 @@ class ShapeletTransformClassifier(BaseClassifier):
         self.n_jobs = n_jobs
 
         self.n_instances_ = 0
-        self.n_dims_ = 0
-        self.series_length_ = 0
+        self.n_channels_ = 0
         self.transformed_data_ = []
 
         self._transformer = None
@@ -195,7 +196,8 @@ class ShapeletTransformClassifier(BaseClassifier):
         Changes state by creating a fitted model that updates attributes
         ending in "_".
         """
-        self.n_instances_, self.n_dims_, self.series_length_ = X.shape
+        self.n_instances_ = len(X)
+        self.n_channels_ = X[0].shape[0]
 
         if self.time_limit_in_minutes > 0:
             # contracting 2/3 transform (with 1/5 of that taken away for final
@@ -290,7 +292,7 @@ class ShapeletTransformClassifier(BaseClassifier):
 
         n_instances, n_dims = X.shape
 
-        if n_instances != self.n_instances_ or n_dims != self.n_dims_:
+        if n_instances != self.n_instances_ or n_dims != self.n_channels_:
             raise ValueError(
                 "n_instances, n_dims mismatch. X should be "
                 "the same as the training data used in fit for generating train "
diff --git a/aeon/transformations/collection/shapelet_based/_shapelet_transform.py b/aeon/transformations/collection/shapelet_based/_shapelet_transform.py
index 0a012f20..c664f965 100644
--- a/aeon/transformations/collection/shapelet_based/_shapelet_transform.py
+++ b/aeon/transformations/collection/shapelet_based/_shapelet_transform.py
@@ -140,6 +140,9 @@ class RandomShapeletTransform(BaseCollectionTransformer):
     _tags = {
         "output_data_type": "Tabular",
         "capability:multivariate": True,
+        "capability:unequal_length": True,
+        "X_inner_mtype": ["np-list", "numpy3D"],
+        "y_inner_type": "numpy1D",
         "requires_y": True,
     }
 
@@ -172,13 +175,14 @@ class RandomShapeletTransform(BaseCollectionTransformer):
         self.random_state = random_state
 
         # The following set in method fit
-        self.n_classes = 0
-        self.n_instances = 0
-        self.n_channels = 0
-        self.series_length = 0
+        self.n_classes_ = 0
+        self.n_instances_ = 0
+        self.n_channels_ = 0
+        self.min_series_length_ = 0
         self.classes_ = []
         self.shapelets = []
 
+        # Protected attributes
         self._n_shapelet_samples = n_shapelet_samples
         self._max_shapelets = max_shapelets
         self._max_shapelet_length = max_shapelet_length
@@ -190,12 +194,12 @@ class RandomShapeletTransform(BaseCollectionTransformer):
 
         super(RandomShapeletTransform, self).__init__()
 
-    def _fit(self, X, y=None):
+    def _fit(self, X, y):
         """Fit the shapelet transform to a specified X and y.
 
         Parameters
         ----------
-        X: np.ndarray shape (n_time_series, n_channels, series_length)
+        X: np.ndarray shape (n_time_series, n_channels, n_timepoints)
             The training input samples.
         y: array-like or list
             The class values for X.
@@ -208,32 +212,38 @@ class RandomShapeletTransform(BaseCollectionTransformer):
         self._n_jobs = check_n_jobs(self.n_jobs)
 
         self.classes_, self._class_counts = np.unique(y, return_counts=True)
-        self.n_classes = self.classes_.shape[0]
+        self.n_classes_ = self.classes_.shape[0]
         for index, classVal in enumerate(self.classes_):
             self._class_dictionary[classVal] = index
 
         le = preprocessing.LabelEncoder()
         y = le.fit_transform(y)
 
-        self.n_instances, self.n_channels, self.series_length = X.shape
+        self.n_instances_ = len(X)
+        self.n_channels_ = X[0].shape[0]
+        # Set series length to the minimum
+        self.min_series_length_ = X[0].shape[1]
+        for i in range(1, self.n_instances_):
+            if X[i].shape[1] < self.min_series_length_:
+                self.min_series_length_ = X[i].shape[1]
 
         if self.max_shapelets is None:
-            self._max_shapelets = min(10 * self.n_instances, 1000)
-        if self._max_shapelets < self.n_classes:
-            self._max_shapelets = self.n_classes
+            self._max_shapelets = min(10 * self.n_instances_, 1000)
+        if self._max_shapelets < self.n_classes_:
+            self._max_shapelets = self.n_classes_
         if self.max_shapelet_length is None:
-            self._max_shapelet_length = self.series_length
+            self._max_shapelet_length = self.min_series_length_
 
         time_limit = self.time_limit_in_minutes * 60
         start_time = time.time()
         fit_time = 0
 
-        max_shapelets_per_class = int(self._max_shapelets / self.n_classes)
+        max_shapelets_per_class = int(self._max_shapelets / self.n_classes_)
         if max_shapelets_per_class < 1:
             max_shapelets_per_class = 1
         # shapelet list content: quality, length, position, channel, inst_idx, cls_idx
         shapelets = List(
-            [List([(-1.0, -1, -1, -1, -1, -1)]) for _ in range(self.n_classes)]
+            [List([(-1.0, -1, -1, -1, -1, -1)]) for _ in range(self.n_classes_)]
         )
         n_shapelets_extracted = 0
 
@@ -319,7 +329,7 @@ class RandomShapeletTransform(BaseCollectionTransformer):
                 s[3],
                 s[4],
                 self.classes_[s[5]],
-                z_normalise_series(X[s[4], s[3], s[2] : s[2] + s[1]]),
+                z_normalise_series(X[s[4]][s[3]][s[2] : s[2] + s[1]]),
             )
             for class_shapelets in shapelets
             for s in class_shapelets
@@ -338,7 +348,8 @@ class RandomShapeletTransform(BaseCollectionTransformer):
                     sorted(range(s[1]), reverse=True, key=lambda j, sabs=sabs: sabs[j])
                 )
             )
-        return self
+        # find max shapelet length
+        self.max_fitted_shapelet_length_ = max(self.shapelets, key=lambda x: x[1])[1]
 
     def _transform(self, X, y=None):
         """Transform X according to the extracted shapelets.
@@ -355,6 +366,14 @@ class RandomShapeletTransform(BaseCollectionTransformer):
         """
         output = np.zeros((len(X), len(self.shapelets)))
 
+        for i in range(0, len(X)):
+            if X[i].shape[1] < self.max_fitted_shapelet_length_:
+                raise ValueError(
+                    "The shortest series in transform is smaller than "
+                    "the min shapelet length, pad to min length prior to "
+                    "calling transform."
+                )
+
         for i, series in enumerate(X):
             dists = Parallel(
                 n_jobs=self._n_jobs, backend=self.parallel_backend, prefer="threads"
@@ -399,7 +418,7 @@ class RandomShapeletTransform(BaseCollectionTransformer):
     def _extract_random_shapelet(
         self, X, y, i, shapelets, max_shapelets_per_class, rng
     ):
-        inst_idx = i % self.n_instances
+        inst_idx = i % self.n_instances_
         cls_idx = int(y[inst_idx])
         worst_quality = (
             shapelets[cls_idx][0][0]
@@ -411,10 +430,12 @@ class RandomShapeletTransform(BaseCollectionTransformer):
             rng.randint(0, self._max_shapelet_length - self.min_shapelet_length)
             + self.min_shapelet_length
         )
-        position = rng.randint(0, self.series_length - length)
-        dim = rng.randint(0, self.n_channels)
+        position = rng.randint(0, self.min_series_length_ - length)
+        channel = rng.randint(0, self.n_channels_)
 
-        shapelet = z_normalise_series(X[inst_idx, dim, position : position + length])
+        shapelet = z_normalise_series(
+            X[inst_idx][channel][position : position + length]
+        )
         sabs = np.abs(shapelet)
         sorted_indicies = np.array(
             sorted(range(length), reverse=True, key=lambda j: sabs[j])
@@ -427,14 +448,14 @@ class RandomShapeletTransform(BaseCollectionTransformer):
             sorted_indicies,
             position,
             length,
-            dim,
+            channel,
             inst_idx,
             self._class_counts[cls_idx],
-            self.n_instances - self._class_counts[cls_idx],
+            self.n_instances_ - self._class_counts[cls_idx],
             worst_quality,
         )
 
-        return np.round(quality, 8), length, position, dim, inst_idx, cls_idx
+        return np.round(quality, 8), length, position, channel, inst_idx, cls_idx
 
     @staticmethod
     @njit(fastmath=True, cache=True)
diff --git a/aeon/utils/plotting.py b/aeon/utils/plotting.py
index 5ff87de0..1585696c 100644
--- a/aeon/utils/plotting.py
+++ b/aeon/utils/plotting.py
@@ -31,17 +31,17 @@ def plot_series(
     Parameters
     ----------
     series : pd.Series or iterable of pd.Series
-        One or more time series
+        One or more time series.
     labels : list, default = None
-        Names of series, will be displayed in figure legend
-    markers: list, default = None
+        Names of series, will be displayed in figure legend.
+    markers : list, default = None
         Markers of data points, if None the marker "o" is used by default.
         The length of the list has to match with the number of series.
-    colors: list, default = None
+    colors : list, default = None
         The colors to use for plotting each series. Must contain one color per series
-    title: str, default = None
-        The text to use as the figure's suptitle
-    pred_interval: pd.DataFrame, default = None
+    title : str, default = None
+        The text to use as the figure's suptitle.
+    pred_interval : pd.DataFrame, default = None
         Output of `forecaster.predict_interval()`. Contains columns for lower
         and upper boundaries of confidence interval.
 
diff --git a/aeon/utils/validation/_convert_collection.py b/aeon/utils/validation/_convert_collection.py
index c812d6df..9e3ce082 100644
--- a/aeon/utils/validation/_convert_collection.py
+++ b/aeon/utils/validation/_convert_collection.py
@@ -22,7 +22,11 @@ import pandas as pd
 
 
 def _nested_univ_is_equal(X):
-    """Check whether series are unequal length."""
+    """Check whether series in a nested DataFrame are unequal length.
+
+    Note that the function _nested_univ_is_equal assumes series are equal length
+    over channels so only tests the first channel.
+    """
     length = X.iloc[0, 0].size
     for i in range(1, X.shape[0]):
         if X.iloc[i, 0].size != length:
@@ -65,6 +69,7 @@ numpy3D_error = (
     "Error: Input should be 3-dimensional NumPy array with shape ("
     "n_instances, n_channels, n_timepoints)."
 )
+numpy2D_error = "Error: Cannot convert multivariate series to numpy 2D arrays."
 
 
 def _from_numpy3d_to_np_list(X):
@@ -83,7 +88,7 @@ def _from_numpy3d_to_np_list(X):
     list : list [n_instances] np.ndarray
         A list of np.ndarray
     """
-    if not isinstance(X, np.ndarray) and X.ndim != 3:
+    if X.ndim != 3:
         raise TypeError(numpy3D_error)
     np_list = []
     for arr in X:
@@ -110,7 +115,7 @@ def _from_numpy3d_to_df_list(X):
     -----
     TypeError if X not 3D numpy array
     """
-    if not isinstance(X, np.ndarray) and X.ndim != 3:
+    if X.ndim != 3:
         raise TypeError(numpy3D_error)
     df_list = []
     for x in X:
@@ -137,23 +142,27 @@ def _from_numpy3d_to_pd_wide(X):
     -----
     TypeError if X not 3D numpy array
     """
-    if not isinstance(X, np.ndarray) and X.ndim != 3:
+    if X.ndim != 3:
         raise TypeError(numpy3D_error)
+    if X.shape[1] != 1:
+        raise TypeError("Cannot convert multivariate series to pd-wide")
     X_flat = _from_numpy3d_to_numpyflat(X)
     return pd.DataFrame(X_flat)
 
 
 def _from_numpy3d_to_numpyflat(X):
-    """Convert 3D np.darray to a 2D np.ndarray."""
-    if not isinstance(X, np.ndarray) and X.ndim != 3:
+    """Convert 3D np.darray to a 2D np.ndarray if shape[1] == 1."""
+    if X.ndim != 3:
         raise TypeError(numpy3D_error)
-    X_flat = X.reshape(X.shape[0], X.shape[1] * X.shape[2])
+    if X.shape[1] > 1:
+        raise TypeError(numpy2D_error)
+    X_flat = X.reshape(X.shape[0], X.shape[2])
     return X_flat
 
 
 def _from_numpy3d_to_pd_multiindex(X):
     """Convert numpy3D collection to pandas multi-index collection."""
-    if not isinstance(X, np.ndarray) and X.ndim != 3:
+    if X.ndim != 3:
         raise TypeError(numpy3D_error)
 
     n_instances, n_channels, n_timepoints = X.shape
@@ -170,7 +179,7 @@ def _from_numpy3d_to_pd_multiindex(X):
 
 def _from_numpy3d_to_nested_univ(X):
     """Convert numpy3D collection to nested_univ pd.DataFrame."""
-    if not isinstance(X, np.ndarray) and X.ndim != 3:
+    if X.ndim != 3:
         raise TypeError(numpy3D_error)
     n_instances, n_channels, n_timepoints = X.shape
     array_type = X.dtype
@@ -198,9 +207,7 @@ def _from_np_list_to_numpy3d(X):
         raise TypeError(np_list_error)
     for i in range(1, len(X)):
         if len(X[i][0]) != len(X[0][0]):
-            raise TypeError(
-                "Error time series not equal length, cannot convert to numpy3D"
-            )
+            raise TypeError("Cannot convert unequal length to numpy3D")
     return np.array(X)
 
 
@@ -233,6 +240,8 @@ def _from_np_list_to_numpyflat(X):
     if not isinstance(X, list) or not isinstance(X[0], np.ndarray):
         raise TypeError(np_list_error)
     X_3d = _from_np_list_to_numpy3d(X)
+    if X_3d.shape[1] > 1:
+        raise TypeError(numpy2D_error)
     return _from_numpy3d_to_numpyflat(X_3d)
 
 
@@ -266,7 +275,7 @@ def _from_df_list_to_numpy3d(X):
 
     for i in range(len(X)):
         if not n == len(X[i]) or not set(X[i].columns) == cols:
-            raise TypeError("elements of obj must have same length and columns")
+            raise TypeError("Cannot convert unequal length series to numpy3D")
     nump3D = np.array([x.to_numpy().transpose() for x in X])
     return nump3D
 
@@ -274,8 +283,8 @@ def _from_df_list_to_numpy3d(X):
 def _from_df_list_to_numpyflat(X):
     if not _equal_length(X, "df-list"):
         raise TypeError(
-            f"{type(X)} does not store equal length series, "
-            f"cannot convert to  numpy flat"
+            f"{type(X)} does not store equal length series."
+            f"Cannot convert unequal length to numpy flat"
         )
     np_list = _from_df_list_to_np_list(X)
     return _from_np_list_to_numpyflat(np_list)
@@ -285,7 +294,7 @@ def _from_df_list_to_pd_wide(X):
     if not _equal_length(X, "df-list"):
         raise TypeError(
             f"{type(X)} does not store equal length series, "
-            f"cannot convert to  pd wide"
+            f"Cannot convert unequal length pd wide"
         )
     np_list = _from_df_list_to_np_list(X)
     return _from_np_list_to_pd_wide(np_list)
@@ -413,15 +422,8 @@ def _pd_wide_to_pd_multiindex(X):
 
 
 def _from_nested_univ_to_numpy3d(X):
-    """Convert nested Panel to 3D numpy Panel.
-
-    Needs to check equal length, but this is legacy only.
-    """
     if not _nested_univ_is_equal(X):
-        raise TypeError(
-            "Error, nested univ contains unequal length series, "
-            "cannot convert to numpy3D"
-        )
+        raise TypeError("Cannot convert unequal length series to numpy3D")
 
     def _convert_series_cell_to_numpy(cell):
         if isinstance(cell, pd.Series):
@@ -475,28 +477,9 @@ def _from_nested_univ_to_df_list(X):
 def _from_nested_univ_to_numpyflat(X):
     if not _nested_univ_is_equal(X):
         raise TypeError("Cannot convert unequal length series to numpyflat")
-    # convert nested data into tabular data
-    if isinstance(X, pd.Series):
-        Xt = np.array(X.tolist())
-
-    elif isinstance(X, pd.DataFrame):
-        try:
-            Xt = np.hstack([X.iloc[:, i].tolist() for i in range(X.shape[1])])
-
-        # except strange key error for specific case
-        except KeyError:
-            if (X.shape == (1, 1)) and (X.iloc[0, 0].shape == (1,)):
-                # in fact only breaks when an additional condition is met,
-                Xt = X.iloc[0, 0].values
-            else:
-                raise
-    #        if Xt.ndim != 2:
-    #            raise TypeError("Cannot convert nested_univ to numpyflat")
-    else:
-        raise TypeError(
-            f"Expected input is pandas Series or pandas DataFrame, "
-            f"but found: {type(X)}"
-        )
+    if X.shape[1] > 1:
+        raise TypeError("Cannot convert multivariate nested into numpyflat")
+    Xt = np.array([np.array(series) for series in X.iloc[:, 0]])
     return Xt
 
 
@@ -512,7 +495,6 @@ def _from_pd_multiindex_to_df_list(X):
 
 
 def _from_pd_multiindex_to_np_list(X):
-    """Convert from a nested pd.DataFrame to a list of 2D numpy."""
     df_list = _from_pd_multiindex_to_df_list(X)
     return _from_df_list_to_np_list(df_list)
 
@@ -533,7 +515,6 @@ def _from_pd_multiindex_to_pd_wide(X):
 
 
 def _from_pd_multiindex_to_nested_univ(X):
-    """Convert a pandas DataFrame witha multi-index to a nested DataFrame."""
     instance_index = 0
 
     # get number of distinct cases (note: a case may have 1 or many dimensions)
@@ -576,7 +557,7 @@ def _equal_length(X, input_type):
 
     Raises
     ------
-    ValueError if input_type equals "dask_panel" or not in COLLECTIONS_DATA_TYPES.
+    ValueError if input_type not in COLLECTIONS_DATA_TYPES.
 
     Example
     -------
@@ -603,10 +584,7 @@ def _equal_length(X, input_type):
     if input_type == "nested_univ":  # Nested univariate or hierachical
         return _nested_univ_is_equal(X)
     if input_type == "pd-multiindex":
-        # TEMPORARY: WORK OUT HOW TO TEST THESE
+        # TEMPORARY: WORK OUT HOW TO TEST IF pd.MULTINDEX are equal length
         return True
-    #        raise ValueError(" Multi index not supported here ")
-    if input_type == "dask_panel":
-        raise ValueError(" DASK panel not supported here ")
     raise ValueError(f" unknown input type {input_type}")
     return False
diff --git a/aeon/utils/validation/_dependencies.py b/aeon/utils/validation/_dependencies.py
index 92e00ee9..c2d14897 100644
--- a/aeon/utils/validation/_dependencies.py
+++ b/aeon/utils/validation/_dependencies.py
@@ -82,10 +82,6 @@ def _check_soft_dependencies(
         class_name = type(obj).__name__
     elif isclass(obj):
         class_name = obj.__name__
-    elif isinstance(obj, str):
-        class_name = obj
-    else:
-        raise TypeError("obj must be a class, an object, a str, or None")
 
     for package in packages:
         try:
diff --git a/aeon/utils/validation/series.py b/aeon/utils/validation/series.py
index a8fabb83..5697762b 100644
--- a/aeon/utils/validation/series.py
+++ b/aeon/utils/validation/series.py
@@ -57,7 +57,11 @@ def _check_is_univariate(y, var_name="input"):
 
 
 def _check_is_multivariate(Z, var_name="input"):
-    """Check if series is multivariate."""
+    """Check if series is multivariate.
+
+    Warning: this function assumes ndarrays are in (n_timepoints, n_channels) shape. Do
+    not use with collections of time series.
+    """
     if isinstance(Z, pd.Series):
         raise ValueError(f"{var_name} must have 2 or more variables, but found 1.")
     if isinstance(Z, pd.DataFrame):
diff --git a/docs/about.md b/docs/about.md
index a665f6ce..6c5e8a07 100644
--- a/docs/about.md
+++ b/docs/about.md
@@ -58,7 +58,7 @@ The core developers push forward `aeon`'s development and maintain the package.
 #### Pre-fork Core Developers
 
 The following listed contributors were part of the sktime core developer team at some
-point prior to our split from the project.
+point prior to the split of the project.
 
 <details><summary>sktime v0.16.0 core developers</summary>
 <p>
@@ -122,7 +122,7 @@ The project developers would like to thank the following funders.
 #### Pre-fork funding
 
 As a fork of the sktime project, `aeon` has benefited from funding given to sktime prior
-to our split. We would like to thank the funders from before the split.
+to the projects split. We would like to thank the funders from before the split.
 
 <details><summary>sktime v0.16.0 funders</summary>
 <p>
diff --git a/docs/api_reference.md b/docs/api_reference.md
new file mode 100644
index 00000000..e71b3f07
--- /dev/null
+++ b/docs/api_reference.md
@@ -0,0 +1,30 @@
+# API Reference
+
+Welcome to the API reference for `aeon`.
+
+The API reference describes the public classes and functions included in `aeon`, split
+by package.
+
+For an overview of the `aeon` modules see the [getting started](getting_started.md)
+and [examples](examples.md) page.
+
+```{toctree}
+:maxdepth: 1
+
+api_reference/annotation
+api_reference/base
+api_reference/benchmarking
+api_reference/classification
+api_reference/clustering
+api_reference/datasets
+api_reference/deployment
+api_reference/distances
+api_reference/exceptions
+api_reference/forecasting
+api_reference/networks
+api_reference/performance_metrics
+api_reference/regression
+api_reference/transformations
+api_reference/utils
+api_reference/file_specifications/ts
+```
diff --git a/docs/api_reference.rst b/docs/api_reference.rst
deleted file mode 100644
index 1641fcde..00000000
--- a/docs/api_reference.rst
+++ /dev/null
@@ -1,30 +0,0 @@
-.. _api_reference:
-
-=============
-API Reference
-=============
-
-Welcome to the API reference for ``aeon``.
-
-The API reference describes the classes and functions included in aeon.
-For an overview of the aeon modules see the :ref:`examples`.
-
-.. include:: includes/api_css.rst
-
-.. toctree::
-    :maxdepth: 1
-
-    api_reference/base
-    api_reference/forecasting
-    api_reference/classification
-    api_reference/regression
-    api_reference/clustering
-    api_reference/distances
-    api_reference/transformations
-    api_reference/annotation
-    api_reference/datasets
-    api_reference/data_format
-    api_reference/deployment
-    api_reference/utils
-    api_reference/exceptions
-    api_reference/performance_metrics
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 00000000..dc603ac3
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,149 @@
+# Contributing to aeon
+
+`aeon` is a community-driven project and contributions are most welcome. We value all
+kinds of contributions, not just code. Improvements to docs, bug reports, and taking
+on communications or code of conduct responsibilities are all examples of valuable
+contributions beyond code which help make `aeon` a great package.
+
+In the following we will give a brief overview of how to contribute to `aeon`. Making
+contributions to open source projects takes a bit of proactivity and can be daunting at
+first, but members of the community are here to help and answer questions. If you get
+stuck, please don’t hesitate to talk with us or raise an issue.
+
+Recommended steps for first time contributors, or to get started with regular
+contributions:
+
+1. Say hello in the `introductions` or `contributors` channel on [Slack](https://join.slack.com/t/aeon-toolkit/shared_invite/zt-22vwvut29-HDpCu~7VBUozyfL_8j3dLA)
+and mention which places you are interested in contributing to.
+2. Get setup for development, see the [developer install instructions](developer_guide/dev_installation.md)
+for creating a fork of `aeon`.
+3. Pick an enhancement, documentation or maintenance issue to complete i.e. improving
+an algorithm, docstring or test. The [good first issue](https://github.com/aeon-toolkit/aeon/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
+list may be a good place to start.
+4. Create a [pull request (PR)](https://github.com/aeon-toolkit/aeon/compare)
+with your changes from your fork. For help, see the [GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)
+or ask in Slack. Follow the PR template and checklist.
+5. A core developer will review your PR and may provide feedback, which you can then
+address. If you are unsure about any feedback, please ask for clarification.
+6. Once your PR is approved, it will be merged into the `aeon` repository. Thanks for
+making a contribution! Make sure you are included in the [list of contributors](contributors.md).
+
+Further guidance for contributing to `aeon` via GitHub can be found on the
+[developer guide](developer_guide.md).
+
+If your intended method of contribution does not fit into the above steps, please
+reach out to us on [Slack](https://join.slack.com/t/aeon-toolkit/shared_invite/zt-22vwvut29-HDpCu~7VBUozyfL_8j3dLA)
+for discussion. While GitHub contributions are the most common, it is not the only
+way to contribute to `aeon`.
+
+## Acknowledging Contributions
+
+We follow the [all-contributors specification](https://allcontributors.org) and
+recognise various types of contributions. Take a look at our past and current
+[contributors](contributors.md)!
+
+If you are a new contributor, make sure we add you to our list of contributors. All
+contributions are recorded in [.all-contributorsrc](https://github.com/aeon-toolkit/aeon/blob/main/.all-contributorsrc).
+Alternatively, you can use the [@all-contributors](https://allcontributors.org/docs/en/bot/usage)
+bot to do this for you.
+
+## Further Reading
+
+For further information on contributing to `aeon`, please see the following pages.
+
+::::{grid} 1 2 2 2
+:gutter: 3
+
+:::{grid-item-card}
+:text-align: center
+
+Developer Guide
+
+^^^
+
+Guidance for `aeon` developers on a range of topics.
+
++++
+
+```{button-ref} developer_guide
+:color: primary
+:click-parent:
+:expand:
+
+Developer Guide
+```
+
+:::
+
+:::{grid-item-card}
+:text-align: center
+
+Enhancement Proposals
+
+^^^
+
+Information on contributing `aeon` enhancement proposals.
+
++++
+
+```{button-ref} contributing/enhancement_proposals
+:color: primary
+:click-parent:
+:expand:
+
+Enhancement Proposals
+```
+
+:::
+
+:::{grid-item-card}
+:text-align: center
+
+Reporting Bugs
+
+^^^
+
+Guidance for reporting bugs in `aeon`.
+
++++
+
+```{button-ref} contributing/reporting_bugs
+:color: primary
+:click-parent:
+:expand:
+
+Reporting Bugs
+```
+
+:::
+
+:::{grid-item-card}
+:text-align: center
+
+Reviewer Guide
+
+^^^
+
+Guidance for reviewing `aeon` pull requests.
+
++++
+
+```{button-ref} contributing/reviewer_guide
+:color: primary
+:click-parent:
+:expand:
+
+Reviewer Guide
+```
+
+:::
+
+::::
+
+```{toctree}
+:hidden:
+
+contributing/enhancement_proposals
+contributing/reporting_bugs
+contributing/reviewer_guide
+```
diff --git a/docs/contributing/enhancement_proposals.rst b/docs/contributing/enhancement_proposals.rst
index 95160da7..a4108429 100644
--- a/docs/contributing/enhancement_proposals.rst
+++ b/docs/contributing/enhancement_proposals.rst
@@ -8,30 +8,28 @@ Enhancement Proposals
 Description
 ===========
 
-An aeon enhancement proposal (STEP) is a software design document providing information to the aeon community.
+An aeon enhancement proposal (AEP) is a software design document providing information to the aeon community.
 The proposal should provide a rationale and concise technical specification of proposed design.
 
-We collect and discuss proposals in aeon’s STEP `repository`_.
+We collect and discuss proposals in aeon’s AEP `repository`_.
 
-.. _repository: https://github.com/aeon/enhancement-proposals
+.. _repository: https://github.com/aeon-toolkit/aeon-admin/tree/main/aep
 
-We intend STEPs to be the primary mechanisms for proposing major changes, for collecting community input on an issue, and for documenting the design decisions that have gone into aeon.
+We intend AEPs to be the primary mechanisms for proposing major changes, for collecting community input on an issue, and for documenting the design decisions that have gone into aeon.
 Smaller changes can be discussed and implemented directly on issues and pull requests.
 
-For the general design principles and patterns followed in aeon, we refer to our paper: `Designing ML Toolboxes: Concepts, Principles and Patterns <https://arxiv.org/abs/2101.04938>`_.
-
-Submitting a STEP
+Submitting a AEP
 =================
 
-To create a new STEP, please copy and use `template`_ and open a pull request on the `repository`_.
+To create a new AEP, please copy and use `template`_ and open a pull request on the `repository`_.
 
-.. _template: https://github.com/aeon/enhancement-proposals/blob/main/TEMPLATE.md
+.. _template: https://github.com/aeon-toolkit/aeon-admin/blob/main/aep/aep_template.md
 
-It is highly recommended that a single STEP contains a single key proposal or new idea.
+It is highly recommended that a single AEP contains a single key proposal or new idea.
 The more focused the proposal, the more successful it tends to be.
-If in doubt, split your STEP into several well-focused ones.
+If in doubt, split your AEP into several well-focused ones.
 
-A STEP should be a consolidated document, including:
+A AEP should be a consolidated document, including:
 
 * a concise problem statement,
 * a clear description of the proposed solution,
diff --git a/docs/contributing/reviewer_guide.rst b/docs/contributing/reviewer_guide.rst
index 1c708e86..b20b7f11 100644
--- a/docs/contributing/reviewer_guide.rst
+++ b/docs/contributing/reviewer_guide.rst
@@ -37,7 +37,7 @@ Documenation
 * If the same parameter, attribute, return object or error is included elsewhere in aeon are the docstring descriptions
   as similar as possible
 * Does the online documentation render correctly with the changes?
-* Do the docstrings contain links to the relevant topics in the :ref:`glossary` or :ref:`user_guide`?
+* Do the docstrings contain links to the relevant topics in the `glossary` or `user_guide`?
 
 .. warning::
 
diff --git a/docs/developer_guide.md b/docs/developer_guide.md
new file mode 100644
index 00000000..96652713
--- /dev/null
+++ b/docs/developer_guide.md
@@ -0,0 +1,43 @@
+# Developer Guide
+
+Welcome to the `aeon` developer guide! This guide is intended for new developers who
+want to start contributing code to `aeon` and current developers who want to learn
+about specific topics for code and non-code developments.
+
+The following is a quick checklist for new developers. At any point during the process,
+feel free to post questions on Slack or ask [core developers](./about.md#core-developers)
+for help.
+
+- Fork the repository and install a [development version](developer_guide/dev_installation.md)
+of `aeon`.
+- Get familiar with the [git workflow](developer_guide/git_workflow.rst).
+- Set up [Continuous Integration (CI)](developer_guide/continuous_integration.rst)
+testing locally and learn how to check the status on pull requests.
+- Read up on the `aeon` [coding standards](developer_guide/coding_standards.rst) and
+pre-commit setup.
+
+`aeon` follows the `scikit-learn` API whenever possible. If you’re new to
+`scikit-learn`, take a look at their [getting-started guide](https://scikit-learn.org/stable/getting_started.html).
+If you’re already familiar with `scikit-learn`, you may still learn something new from
+their [developer's guide](https://scikit-learn.org/stable/developers/index.html).
+
+## Specific Topics
+
+Below we list further reading and guidance for specific topics in the development of
+`aeon`.
+
+```{toctree}
+:maxdepth: 1
+
+developer_guide/add_dataset
+developer_guide/add_estimators
+developer_guide/coding_standards
+developer_guide/continuous_integration
+developer_guide/dependencies
+developer_guide/deprecation.md
+developer_guide/dev_installation.md
+developer_guide/documentation
+developer_guide/git_workflow
+developer_guide/release.md
+developer_guide/testing_framework
+```
diff --git a/docs/developer_guide.rst b/docs/developer_guide.rst
deleted file mode 100644
index 177074ff..00000000
--- a/docs/developer_guide.rst
+++ /dev/null
@@ -1,44 +0,0 @@
-
-.. _developer_guide:
-
-===============
-Developer Guide
-===============
-
-Welcome to aeon's developer guide!
-
-.. note::
-
-    The developer guide is under development. We welcome contributions! For
-    more details, please go to :issue:`464`.
-
-New developers should:
-
-* sign up to the developer Slack (see link in README) and say hello in the ``#contributors`` channel
-* install a development version of ``aeon``, see :ref:`dev_installation`
-* set up CI tests locally and ensure they know how to check them remotely, see :ref:`continuous_integration`
-* get familiar with the git workflow (:ref:`git_workflow`) and coding standards (:ref:`coding_standards`)
-* feel free, at any point in time, to post questions on Slack, or ask core developers for help (see here for a `list of core developers <https://www.aeon-toolkit.org/en/stable/about/team.html>`_)
-
-* feel free to join the collaborative coding sessions for pair programming or getting help on developer set-up
-
-Further special topics are listed below.
-
-aeon follows `scikit-learn <https://scikit-learn.org/stable/>`_\ ’s API whenever possible.
-If you’re new to scikit-learn, take a look at their `getting-started guide <https://scikit-learn.org/stable/getting_started.html>`_.
-If you’re already familiar with scikit-learn, you may still learn something new from their `developers’ guide <https://scikit-learn.org/stable/developers/index.html>`_.
-
-.. toctree::
-   :maxdepth: 1
-
-   developer_guide/dev_installation.md
-   developer_guide/git_workflow
-   developer_guide/continuous_integration
-   developer_guide/coding_standards
-   developer_guide/add_estimators
-   developer_guide/add_dataset
-   developer_guide/deprecation
-   developer_guide/dependencies
-   developer_guide/documentation
-   developer_guide/testing_framework
-   developer_guide/release
diff --git a/docs/developers.rst b/docs/developers.rst
deleted file mode 100644
index 99b82b8b..00000000
--- a/docs/developers.rst
+++ /dev/null
@@ -1,112 +0,0 @@
-.. _developers:
-
-Development
-===========
-
-.. note::
-
-   If you are new to aeon, it may be helpful to take a look at the
-   :ref:`get_involved` page first.
-
-.. toctree::
-   :maxdepth: 1
-   :hidden:
-
-   developer_guide
-   reviewer_guide
-   enhancement_proposals
-   roadmap
-   governance
-
-.. grid:: 1 2 2 2
-    :gutter: 3
-
-    .. grid-item-card::
-        :text-align: center
-
-        Developer Guide
-
-        ^^^
-
-        Learn our development conventions.
-
-        +++
-
-        .. button-ref:: developer_guide
-            :color: primary
-            :click-parent:
-            :expand:
-
-            Developer Guide
-
-    .. grid-item-card::
-        :text-align: center
-
-        Reviewer Guide
-
-        ^^^
-
-        How we review contributions.
-
-        +++
-
-        .. button-ref:: reviewer_guide
-            :color: primary
-            :click-parent:
-            :expand:
-
-            Reviewer Guide
-
-    .. grid-item-card::
-        :text-align: center
-
-        Enhancement Proposals
-
-        ^^^
-
-        Thought of a project enhancement? See when and how to submit a proposal.
-
-        +++
-
-        .. button-ref:: enhancement_proposals
-            :color: primary
-            :click-parent:
-            :expand:
-
-            Enhancement Proposals
-
-    .. grid-item-card::
-        :text-align: center
-
-        Roadmap
-
-        ^^^
-
-        What's on the development horizon?
-
-        +++
-
-        .. button-ref:: roadmap
-            :color: primary
-            :click-parent:
-            :expand:
-
-            Roadmap
-
-    .. grid-item-card::
-        :text-align: center
-
-        Governance
-
-        ^^^
-
-        How ``aeon`` is run.
-
-        +++
-
-        .. button-ref:: governance
-            :color: primary
-            :click-parent:
-            :expand:
-
-            Governance
diff --git a/docs/get_involved.rst b/docs/get_involved.rst
deleted file mode 100644
index 3002a4a3..00000000
--- a/docs/get_involved.rst
+++ /dev/null
@@ -1,72 +0,0 @@
-.. _get_involved:
-
-Get Involved
-============
-
-.. toctree::
-   :maxdepth: 1
-   :hidden:
-
-   get_involved/contributing
-   get_involved/code_of_conduct
-   get_involved/governance
-
-aeon is a community-driven project and your help is most welcome.
-If you get stuck, please don't hesitate to talk with us or raise an issue.
-
-.. grid:: 1 2 2 2
-    :gutter: 3
-
-    .. grid-item-card::
-        :text-align: center
-
-        Contributing
-
-        ^^^
-
-        New to aeon? Check out the contributing guide.
-
-        +++
-
-        .. button-ref:: contributing
-            :color: primary
-            :click-parent:
-            :expand:
-
-            Contributing guide
-
-    .. grid-item-card::
-        :text-align: center
-
-        Code of Conduct
-
-        ^^^
-
-
-
-        +++
-
-        .. button-ref:: code_of_conduct
-            :color: primary
-            :click-parent:
-            :expand:
-
-            Code of Conduct
-
-    .. grid-item-card::
-        :text-align: center
-
-        Governance
-
-        ^^^
-
-
-
-        +++
-
-        .. button-ref:: governance
-            :color: primary
-            :click-parent:
-            :expand:
-
-            Governance
diff --git a/docs/get_involved/contributing.rst b/docs/get_involved/contributing.rst
deleted file mode 100644
index de747ca3..00000000
--- a/docs/get_involved/contributing.rst
+++ /dev/null
@@ -1,148 +0,0 @@
-.. _contributing:
-
-=================
-How to contribute
-=================
-
-
-
-Welcome to aeon's contributing guide!
-
-We value all kinds of contributions - not just code.
-For a detailed overview of current and future work, check out our :ref:`roadmap`.
-
-We are particularly motivated to support new contributors
-and people who are looking to learn and develop their skills.
-
-Recommended steps for first time contributors, or to get started with regular contributions:
-
-1. Say hello (in the contributors channel on `Slack`_)!
-2. Get set up for development, see `instructions in the developer guide <https://www.aeon-toolkit.org/en/stable/developer_guide.html>`_.
-3. Pick a good first issue to work on from `this list <https://github.com/aeon-toolkit/aeon/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22>`_
-   suggestion: pick something small with simple content to learn the “process”
-4. Feel free to attend the regular community meetings (see schedule on `Slack`_)
-
-.. _Slack: https://join.slack.com/t/aeon-toolkit/shared_invite/zt-22vwvut29-HDpCu~7VBUozyfL_8j3dLA
-
-.. note::
-
-   If you get stuck, chat with us on `Slack`_.
-
-.. grid:: 1 2 2 2
-    :gutter: 3
-
-    .. grid-item-card::
-        :text-align: center
-
-        Developer Guide
-
-        ^^^
-
-        ``aeon`` developer guide.
-
-        +++
-
-        .. button-ref:: developer_guide
-            :color: primary
-            :click-parent:
-            :expand:
-
-            Developer Guide
-
-    .. grid-item-card::
-        :text-align: center
-
-        Installation
-
-        ^^^
-
-        ``aeon`` developer installation guide.
-
-        +++
-
-        .. button-ref:: installation
-            :color: primary
-            :click-parent:
-            :expand:
-
-            Developer Installation
-
-    .. grid-item-card::
-        :text-align: center
-
-        Enhancement Proposals
-
-        ^^^
-
-        ``aeon`` enhancement proposals.
-
-        +++
-
-        .. button-ref:: enhancement_proposals
-            :color: primary
-            :click-parent:
-            :expand:
-
-            Enhancement Proposals
-
-    .. grid-item-card::
-        :text-align: center
-
-        Reporting Bugs
-
-        ^^^
-
-        ``aeon`` reporting bugs.
-
-        +++
-
-        .. button-ref:: reporting_bugs
-            :color: primary
-            :click-parent:
-            :expand:
-
-            Reporting Bugs
-
-    .. grid-item-card::
-        :text-align: center
-
-        Reviewer Guide
-
-        ^^^
-
-        ``aeon`` reviewer guide.
-
-        +++
-
-        .. button-ref:: reviewer_guide
-            :color: primary
-            :click-parent:
-            :expand:
-
-            Reviewer Guide
-
-Acknowledging contributions
----------------------------
-
-We follow the `all-contributors
-specification <https://allcontributors.org>`_ and recognize various
-types of contributions.
-Take a look at our past and current
-`contributors <https://github.com/aeon-toolkit/aeon/blob/main/CONTRIBUTORS.md>`_!
-
-If you are a new contributor, make sure we add you to our list of
-contributors.
-All contributions are recorded in
-`.all-contributorsrc <https://github.com/aeon-toolkit/aeon/blob/main/.all-contributorsrc>`_.
-
-.. note::
-
-   If we have missed anything, please `raise an issue <https://github.com/aeon-toolkit/aeon/issues/new/choose>`_ or chat with us on `Slack`_.
-
-
-.. toctree::
-   :maxdepth: 1
-
-   ../contributing/enhancement_proposals
-   ../contributing/reporting_bugs
-   ../contributing/reviewer_guide
diff --git a/docs/includes/api_css.rst b/docs/includes/api_css.rst
deleted file mode 100644
index eaf189fd..00000000
--- a/docs/includes/api_css.rst
+++ /dev/null
@@ -1,29 +0,0 @@
-..
-    File to ..include in the API ref document.
-
-.. raw:: html
-
-    <style type="text/css">
-        h1 {
-            /* margin: 0 -10px 0 -10px; */
-            text-align: center;
-            background-color: #cde8ef;
-            /* font-family: Helvetica, Arial, sans-serif; */
-            /* font-size: 190%; */
-            border-radius: 0 15px 0 15px;
-            -moz-border-radius: 0 15px 0 15px;
-            padding: 10px;
-        }
-
-        h2 {
-            background-color: #BED4EB;
-            border-radius: 10px;
-            padding: 10px;
-        }
-
-        h3 {
-            padding: 5px;
-            border-radius: 10px;
-            background-color: #eee;
-        }
-    </style>
diff --git a/docs/index.md b/docs/index.md
index 9ae06b15..4b69d58e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -20,7 +20,6 @@ comparison.
 :class-img-top: aeon-card-image
 :text-align: center
 
-
 Get started with time series forecasting.
 
 +++
@@ -40,7 +39,6 @@ Forecasting
 :class-img-top: aeon-card-image
 :text-align: center
 
-
 Get started with time series classification.
 
 +++
@@ -60,7 +58,6 @@ Classification
 :class-img-top: aeon-card-image
 :text-align: center
 
-
 Get started with time series extrinsic regression.
 
 +++
@@ -80,7 +77,6 @@ Regression
 :class-img-top: aeon-card-image
 :text-align: center
 
-
 Get started with time series clustering.
 
 +++
@@ -100,7 +96,6 @@ Clustering
 :class-img-top: aeon-card-image
 :text-align: center
 
-
 Get started with time series transformations.
 
 +++
@@ -120,7 +115,6 @@ Transformations
 :class-img-top: aeon-card-image
 :text-align: center
 
-
 Get started with time series distances.
 
 +++
@@ -159,7 +153,6 @@ Networks
 :class-img-top: aeon-card-image
 :text-align: center
 
-
 Annotation
 
 +++
@@ -230,8 +223,7 @@ Benchmarking
 
 installation.md
 getting_started.md
-user_guide
-api_reference
+api_reference.md
 examples.md
 ```
 
@@ -239,8 +231,8 @@ examples.md
 :caption: Developing aeon
 :hidden:
 
-get_involved
-developers
+contributing.md
+developer_guide.md
 ```
 
 ```{toctree}
diff --git a/sweep.yaml b/sweep.yaml
index f85051a0..6542f53c 100644
--- a/sweep.yaml
+++ b/sweep.yaml
@@ -24,5 +24,5 @@ sandbox:
 rules:
   - "Comments should be instructive, such as inline and docstring comments. Remove any code that has been commented out."
   - "Any clearly inefficient or redundant code can be optimized or refactored. Any improvements should not change the functionality of the code."
-  - "All public classes and functions should have a numpydoc style docstring. This should include a description of the class or function, the parameters, the class attributes or function return values, and a usage example for the class or function."
-  - "Update the relevant API page in 'docs/api_reference/' when new public functions and classes are added and not included in the API documentation. For example, if a new function is added to 'aeon/distances/', a `sphinx.ext.autosummary link` should also be added to 'docs/api_reference/distances.rst'. New sections in the page should not be created for individual functions and classes, add it to the most relevant existing one. Only add functions and classes which are not already in the relevant API page and avoid duplicate entries."
+  - "All public classes and functions should have a `numpydoc` style docstring. This should include a description of the class or function, the parameters, the class attributes or function return values, and a usage example for the class or function."
+  - "Update the relevant API page in `docs/api_reference/` when new public functions and classes are added and not included in the API documentation. For example, if a new function is added to `aeon/distances/`, a `sphinx.ext.autosummary` link should also be added to `docs/api_reference/distances.rst`. New sections in the page should not be created for individual functions and classes, add it to the most relevant existing one. Only add functions and classes which are not already in the relevant API page and avoid duplicate entries."


Test patch:
diff --git a/aeon/transformations/collection/shapelet_based/tests/test_shapelet_transform.py b/aeon/transformations/collection/shapelet_based/tests/test_shapelet_transform.py
new file mode 100644
index 00000000..325f18ec
--- /dev/null
+++ b/aeon/transformations/collection/shapelet_based/tests/test_shapelet_transform.py
@@ -0,0 +1,25 @@
+"""Test shapelet transform."""
+
+import pytest
+
+from aeon.transformations.collection.shapelet_based import RandomShapeletTransform
+from aeon.utils._testing.collection import make_3d_test_data
+
+
+def test_shapelet_transform():
+    """Test edge cases for RandomShapeletTransform."""
+    X, y = make_3d_test_data(n_cases=50, n_timepoints=20, n_labels=4)
+    rst = RandomShapeletTransform(max_shapelets=3, remove_self_similar=True)
+    rst._fit(X, y)
+    # Assert at least one shapelet per class when max_shapelets < n_labels
+    assert len(rst.shapelets) == 4
+    assert rst._max_shapelets == 4
+    X, y = make_3d_test_data(
+        n_cases=3, n_timepoints=rst.max_fitted_shapelet_length_ - 1, n_labels=4
+    )
+    with pytest.raises(
+        ValueError,
+        match="The shortest series in transform is "
+        "smaller than the min shapelet length",
+    ):
+        rst._transform(X)
diff --git a/aeon/utils/validation/tests/test_annotation.py b/aeon/utils/validation/tests/test_annotation.py
new file mode 100644
index 00000000..dfcb65f1
--- /dev/null
+++ b/aeon/utils/validation/tests/test_annotation.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3 -u
+"""Test functions in the _dependencies file."""
+import pytest
+
+from aeon.utils.validation.annotation import check_fmt, check_labels
+
+
+def test_check_fmt():
+    """Check function to test annotation format."""
+    with pytest.raises(ValueError):
+        check_fmt("Stupid function")
+    with pytest.raises(ValueError):
+        check_labels("Stupid function")
diff --git a/aeon/utils/validation/tests/test_collection.py b/aeon/utils/validation/tests/test_collection.py
index acdf3cfb..2b1e380c 100644
--- a/aeon/utils/validation/tests/test_collection.py
+++ b/aeon/utils/validation/tests/test_collection.py
@@ -7,8 +7,23 @@ from aeon.datasets import make_example_multi_index_dataframe
 from aeon.utils._testing.tests.test_collection import make_nested_dataframe_data
 from aeon.utils.validation._convert_collection import (
     _equal_length,
+    _from_nested_univ_to_numpyflat,
+    _from_nested_univ_to_pd_multiindex,
+    _from_numpy3d_to_df_list,
+    _from_numpy3d_to_nested_univ,
+    _from_numpy3d_to_np_list,
+    _from_numpy3d_to_numpyflat,
+    _from_numpy3d_to_pd_multiindex,
+    _from_numpy3d_to_pd_wide,
+    _from_numpyflat_to_df_list,
+    _from_numpyflat_to_nested_univ,
+    _from_numpyflat_to_np_list,
+    _from_numpyflat_to_numpy3d,
+    _from_numpyflat_to_pd_multiindex,
+    _from_numpyflat_to_pd_wide,
     _is_nested_univ_dataframe,
     _is_pd_wide,
+    _nested_univ_is_equal,
 )
 from aeon.utils.validation.collection import (
     COLLECTIONS_DATA_TYPES,
@@ -99,15 +114,21 @@ def test_convert_collection(input_data, output_data):
     assert get_type(X) == output_data
     # Test with multivariate
     if input_data in EQUAL_LENGTH_MULTIVARIATE:
-        X = convert_collection(EQUAL_LENGTH_MULTIVARIATE[input_data], output_data)
-        assert get_type(X) == output_data
+        if output_data in EQUAL_LENGTH_MULTIVARIATE:
+            X = convert_collection(EQUAL_LENGTH_MULTIVARIATE[input_data], output_data)
+            assert get_type(X) == output_data
+        else:
+            with pytest.raises(TypeError, match="Cannot convert multivariate"):
+                X = convert_collection(
+                    EQUAL_LENGTH_MULTIVARIATE[input_data], output_data
+                )
     # Test with unequal length
     if input_data in UNEQUAL_LENGTH_UNIVARIATE:
         if output_data in UNEQUAL_LENGTH_UNIVARIATE or output_data == "pd-multiindex":
             X = convert_collection(UNEQUAL_LENGTH_UNIVARIATE[input_data], output_data)
             assert get_type(X) == output_data
         else:
-            with pytest.raises(TypeError):
+            with pytest.raises(TypeError, match="Cannot convert unequal"):
                 X = convert_collection(
                     UNEQUAL_LENGTH_UNIVARIATE[input_data], output_data
                 )
@@ -188,7 +209,6 @@ def test_has_missing(data):
     assert not has_missing(EQUAL_LENGTH_UNIVARIATE[data])
     X = np.random.random(size=(10, 2, 20))
     X[5][1][12] = np.NAN
-    # TODO: Test others with missing when converters complete
     assert has_missing(X)
 
 
@@ -213,3 +233,87 @@ def test__is_pd_wide(data):
         assert _is_pd_wide(EQUAL_LENGTH_UNIVARIATE[data])
     else:
         assert not _is_pd_wide(EQUAL_LENGTH_UNIVARIATE[data])
+
+
+NUMPY3D = [
+    _from_numpy3d_to_pd_wide,
+    _from_numpy3d_to_np_list,
+    _from_numpy3d_to_df_list,
+    _from_numpy3d_to_pd_wide,
+    _from_numpy3d_to_numpyflat,
+    _from_numpy3d_to_nested_univ,
+    _from_numpy3d_to_pd_multiindex,
+]
+
+
+@pytest.mark.parametrize("function", NUMPY3D)
+def test_numpy3D_error(function):
+    X = np.random.random(size=(10, 20))
+    with pytest.raises(TypeError, match="Input should be 3-dimensional NumPy array"):
+        function(X)
+
+
+NUMPY2D = [
+    _from_numpyflat_to_numpy3d,
+    _from_numpyflat_to_np_list,
+    _from_numpyflat_to_df_list,
+    _from_numpyflat_to_pd_wide,
+    _from_numpyflat_to_nested_univ,
+    _from_numpyflat_to_pd_multiindex,
+]
+
+
+@pytest.mark.parametrize("function", NUMPY2D)
+def test_numpy2D_error(function):
+    """Test numpy flat converters only work with 2D numpy"""
+    X = np.random.random(size=(10, 2, 20))
+    with pytest.raises(TypeError, match="Input should be 2-dimensional np.ndarray"):
+        function(X)
+
+
+def test__nested_univ_is_equal():
+    """Test _nested_univ_is_equal function for pd.DataFrame.
+
+    Note that the function _nested_univ_is_equal assumes series are equal length
+    over channels so only tests the first channel.
+    """
+
+    data = {
+        "A": [pd.Series([1, 2, 3, 4]), pd.Series([4, 5, 6])],
+        "B": [pd.Series([1, 2, 3, 4]), pd.Series([4, 5, 6])],
+        "C": [pd.Series([1, 2, 3, 4]), pd.Series([4, 5, 6])],
+    }
+    X = pd.DataFrame(data)
+    assert not _nested_univ_is_equal(X)
+    X, _ = make_nested_dataframe_data(n_cases=10, n_channels=1, n_timepoints=20)
+    assert _nested_univ_is_equal(X)
+
+
+def test_from_nested():
+    """Test with multiple nested columns and non-nested columns."""
+    data = {
+        "A": [pd.Series([1, 2, 3]), pd.Series([4, 5, 6])],
+        "B": [pd.Series(["a", "b", "c"]), pd.Series(["x", "y", "z"])],
+        "C": [7, 8],
+    }
+    X = pd.DataFrame(data)
+    result = _from_nested_univ_to_pd_multiindex(X)
+    assert isinstance(result, pd.DataFrame)
+    data = {
+        "A": [pd.Series([1, 2, 3, 4]), pd.Series([4, 5, 6])],
+        "B": [pd.Series([1, 2, 3, 4]), pd.Series([4, 5, 6])],
+        "C": [pd.Series([1, 2, 3, 4]), pd.Series([4, 5, 6])],
+    }
+    X = pd.DataFrame(data)
+    with pytest.raises(
+        TypeError, match="Cannot convert unequal length series to " "numpyflat"
+    ):
+        _from_nested_univ_to_numpyflat(X)
+    X, _ = make_nested_dataframe_data(n_cases=10, n_channels=1, n_timepoints=20)
+    result = _from_nested_univ_to_numpyflat(X)
+    assert result.shape == (10, 20)
+    X, _ = make_nested_dataframe_data(n_cases=10, n_channels=2, n_timepoints=20)
+    with pytest.raises(
+        TypeError, match="Cannot convert multivariate nested into " "numpyflat"
+    ):
+        _from_nested_univ_to_numpyflat(X)
diff --git a/aeon/utils/validation/tests/test_dependencies.py b/aeon/utils/validation/tests/test_dependencies.py
new file mode 100644
index 00000000..84d99d22
--- /dev/null
+++ b/aeon/utils/validation/tests/test_dependencies.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3 -u
+"""Test functions in the _dependencies file."""
+
+import pytest
+
+from aeon.utils.validation._dependencies import _check_soft_dependencies
+
+
+# Currently just partial testing of soft dep checks.
+def test__check_soft_dependencies():
+    with pytest.raises(TypeError, match="packages must be str or tuple of str"):
+        _check_soft_dependencies((1, 2, 3))
+    with pytest.raises(TypeError, match="package_import_alias must be a dict"):
+        _check_soft_dependencies("tslearn", package_import_alias="alias")
+    with pytest.raises(TypeError, match="must be a dict with str keys"):
+        _check_soft_dependencies("tslearn", package_import_alias={1: "1", 2: "2"})
+    with pytest.raises(TypeError, match="must be a dict with str keys and values"):
+        _check_soft_dependencies("tslearn", package_import_alias={"1": 1, "2": 2})
+    with pytest.raises(ModuleNotFoundError, match="No module named 'FOOBAR'"):
+        _check_soft_dependencies("FOOBAR")
+    with pytest.raises(ModuleNotFoundError, match="No module named 'FOOBAR'"):
+        _check_soft_dependencies("FOOBAR", suppress_import_stdout=True)
+    with pytest.raises(ModuleNotFoundError, match="str requires package 'FOOBAR'"):
+        _check_soft_dependencies("FOOBAR", obj="FOOBAR")
+    with pytest.raises(RuntimeError, match="severity argument must be "):
+        _check_soft_dependencies("FOOBAR", severity="FOOBAR")
+    assert not _check_soft_dependencies("FOOBAR", severity="none")
+    with pytest.warns(UserWarning, match="No module named 'FOOBAR'"):
+        _check_soft_dependencies("FOOBAR", severity="warning")
diff --git a/aeon/utils/validation/tests/test_forecasting.py b/aeon/utils/validation/tests/test_forecasting.py
index ec24221c..3ec5deb0 100644
--- a/aeon/utils/validation/tests/test_forecasting.py
+++ b/aeon/utils/validation/tests/test_forecasting.py
@@ -2,12 +2,24 @@
 
 __author__ = ["mloning"]
 
+from datetime import timedelta
+
 import numpy as np
 import pandas as pd
 import pytest
 from pytest import raises
 
-from aeon.utils.validation.forecasting import check_fh
+from aeon.forecasting.base import ForecastingHorizon
+from aeon.forecasting.model_selection._split import BaseSplitter
+from aeon.utils.validation.forecasting import (
+    check_cv,
+    check_fh,
+    check_sp,
+    check_step_length,
+    check_X,
+    check_y,
+    check_y_X,
+)
 
 empty_input = (np.array([], dtype=int), [], pd.Index([], dtype=int))
 
@@ -17,3 +29,53 @@ def test_check_fh_empty_input(arg):
     """Test that fh validation throws an error with empty container."""
     with raises(ValueError, match="`fh` must not be empty"):
         check_fh(arg)
+
+
+def test_check_fh():
+    """Test enforce relative"""
+    fh = ForecastingHorizon(1, is_relative=False)
+    with raises(ValueError, match="must be relative"):
+        check_fh(fh, enforce_relative=True)
+
+
+def test_check_functions():
+    X = pd.DataFrame(np.random.randint(0, 10, size=(6, 4)), columns=list("ABCD"))
+    y = pd.Series([1, 2, 3, 4, 5, 6])
+    y2, X2 = check_y_X(y, X)
+    assert isinstance(y2, pd.Series)
+    assert isinstance(X2, pd.DataFrame)
+    X2 = check_X(X)
+    assert isinstance(X2, pd.DataFrame)
+    y2 = check_y(y)
+    assert isinstance(y2, pd.Series)
+    y = pd.Series([1, 1, 1, 1, 1])
+    with raises(ValueError, match="All values of `y` are the same"):
+        check_y(y, allow_constant=False)
+    with raises(TypeError, match=f"`cv` is not an instance of {BaseSplitter}"):
+        check_cv("Arsenal")
+    bs = BaseSplitter()
+    bs.start_with_window = False
+    with raises(ValueError, match="`start_with_window` must be set to True"):
+        check_cv(bs, enforce_start_with_window=True)
+    assert check_step_length(None) is None
+    with raises(ValueError, match="`step_length` must be a integer >= 1"):
+        check_step_length(0)
+    td = timedelta(0)
+    with raises(ValueError, match="`step_length` must be a positive timedelta"):
+        check_step_length(td)
+    do = pd.DateOffset(0)
+    with raises(ValueError, match="must be a positive pd.DateOffset"):
+        check_step_length(do)
+    with raises(
+        ValueError,
+        match="`step_length` must be an integer, timedelta, pd.DateOffset, or None",
+    ):
+        check_step_length("Arsenal")
+    check_sp([1], enforce_list=True)
+    res = check_sp(1, enforce_list=True)
+    assert isinstance(res, list)
+    assert res[0] == 1
+    with raises(ValueError, match="`sp` must be an int >= 1 or None"):
+        check_sp("FOO")
+    with raises(ValueError, match="`sp` must be an int"):
+        check_sp("BAR", enforce_list=True)
diff --git a/aeon/utils/validation/tests/test_check_window_length.py b/aeon/utils/validation/tests/test_init.py
similarity index 100%
rename from aeon/utils/validation/tests/test_check_window_length.py
rename to aeon/utils/validation/tests/test_init.py
diff --git a/aeon/utils/validation/tests/test_panel.py b/aeon/utils/validation/tests/test_panel.py
index a36de83a..5299c463 100644
--- a/aeon/utils/validation/tests/test_panel.py
+++ b/aeon/utils/validation/tests/test_panel.py
@@ -63,3 +63,31 @@ def test_check_X_enforce_min_columns():
 
     with pytest.raises(ValueError, match=msg):
         check_X_y(X, y, enforce_min_columns=3)
+
+
+def test_check_X():
+    """Test check X."""
+    with pytest.raises(ValueError, match="cannot both be set to True"):
+        check_X(None, coerce_to_pandas=True, coerce_to_numpy=True)
+    X = np.random.random(size=(5, 10))
+    X2 = check_X(X)
+    assert X2.shape == (5, 1, 10)
+    check_X(X2, enforce_min_instances=5)
+    with pytest.raises(ValueError, match="but a minimum of: 6 is required"):
+        check_X(X2, enforce_min_instances=6)
+    X, y = make_nested_dataframe_data(n_cases=5, n_channels=2, n_timepoints=10)
+    X2 = check_X(X, coerce_to_numpy=True)
+    assert isinstance(X2, np.ndarray)
+    assert X2.shape == (5, 2, 10)
+
+
+def test_check_y():
+    """Test check y."""
+    with pytest.raises(ValueError, match=" must be either a pd.Series or a np.ndarray"):
+        check_y("Up the Arsenal")
+    y = pd.Series([1, 2, 3, 4])
+    y2 = check_y(y, coerce_to_numpy=True)
+    assert isinstance(y2, np.ndarray)
+    check_y(y2, enforce_min_instances=4)
+    with pytest.raises(ValueError, match="but a minimum of: 5 is required"):
+        check_y(y2, enforce_min_instances=5)
diff --git a/aeon/utils/validation/tests/test_series.py b/aeon/utils/validation/tests/test_series.py
index 2dc3c291..8c0a5c03 100644
--- a/aeon/utils/validation/tests/test_series.py
+++ b/aeon/utils/validation/tests/test_series.py
@@ -1,18 +1,102 @@
 """Test series module."""
 
-__author__ = ["benheid"]
+__author__ = ["benheid", "TonyBagnall"]
 
 import numpy as np
+import pandas as pd
 import pytest
 
-from aeon.utils.validation.series import check_equal_time_index
+from aeon.utils._testing.collection import make_nested_dataframe_data
+from aeon.utils.validation.series import (
+    _check_is_multivariate,
+    _check_is_univariate,
+    check_consistent_index_type,
+    check_equal_time_index,
+    check_series,
+    check_time_index,
+)
 
-first_arrays = (np.random.random(1000),)
-second_arrays = (np.random.random(1000),)
 
+def test_check_equal_time_index():
+    """Test check equal time index."""
+    assert check_equal_time_index(None) is None
+    x = (pd.Series([1, 2, 3, 4, 5]), pd.Series([2, 3, 4, 5, 6]))
+    with pytest.raises(ValueError, match="mode must be "):
+        check_equal_time_index(*x, mode="FOO")
+    index1 = pd.date_range(start="2023-01-01", end="2023-01-05")
+    index2 = pd.date_range(start="2023-01-06", end="2023-01-10")
+    ys = (
+        pd.Series([1, 2, 3, 4, 5], index=index1),
+        pd.Series([6, 7, 8, 9, 10], index=index2),
+    )
+    with pytest.raises(ValueError):
+        check_equal_time_index(*ys, mode="contains")
+    with pytest.raises(ValueError):
+        check_equal_time_index(*ys, mode="equal")
 
-@pytest.mark.parametrize("first_array", first_arrays)
-@pytest.mark.parametrize("second_array", second_arrays)
-def test_check_equal_time_index(first_array, second_array):
-    """Test that fh validation throws an error with empty container."""
-    check_equal_time_index(first_array, second_array)
+
+def test__check_is_univariate():
+    """Test _check_is_univariate."""
+    X = np.random.random(size=(10, 1, 20))
+    _check_is_univariate(X)
+    X = np.random.random(size=(10, 3, 20))
+    with pytest.raises(ValueError, match="must be univariate"):
+        _check_is_univariate(X)
+    X, _ = make_nested_dataframe_data(n_cases=4, n_channels=1, n_timepoints=10)
+    _check_is_univariate(X)
+    X, _ = make_nested_dataframe_data(n_cases=4, n_channels=2, n_timepoints=10)
+    with pytest.raises(ValueError, match="must be univariate"):
+        _check_is_univariate(X)
+
+
+def test__check_is_multivariate():
+    """Test _check_is_multivariate.
+
+    This function assumes ndarrays are in (n_timepoints, n_channels) shape.
+    """
+    X = pd.Series([1, 2, 3, 4, 5])
+    with pytest.raises(ValueError, match=" must have 2 or more variables, but found 1"):
+        _check_is_multivariate(X)
+    X, _ = make_nested_dataframe_data(n_cases=4, n_channels=2, n_timepoints=10)
+    _check_is_multivariate(X)
+    X, _ = make_nested_dataframe_data(n_cases=4, n_channels=1, n_timepoints=10)
+    with pytest.raises(ValueError, match="must have 2 or more variables"):
+        _check_is_multivariate(X)
+    X = np.random.random(size=(10, 1))
+
+
+def test_check_series():
+    """Test check_series."""
+    check_series(None)
+    with pytest.raises(ValueError, match="cannot be None"):
+        check_series(None, allow_None=False)
+    X, _ = make_nested_dataframe_data(n_cases=4, n_channels=2, n_timepoints=10)
+    with pytest.raises(ValueError, match="cannot both be set to True"):
+        check_series(Z=X, enforce_univariate=True, enforce_multivariate=True)
+    X, _ = make_nested_dataframe_data(n_cases=4, n_channels=2, n_timepoints=10)
+    check_series(Z=X, enforce_multivariate=True)
+
+
+def test_check_time_index():
+    """Test check_time_index."""
+    x = np.array([1, 2, 3, 4, 5])
+    with pytest.raises(NotImplementedError, match="is not supported"):
+        check_time_index("HELLO")
+    with pytest.raises(NotImplementedError, match="is not supported"):
+        check_time_index(x, enforce_index_type=pd.Series)
+    x = np.array([1, 2, 3, 5, 4])
+    with pytest.raises(ValueError, match="must be sorted monotonically increasing"):
+        check_time_index(x)
+    x = pd.RangeIndex(0)
+    with pytest.raises(ValueError, match="must contain at least some values"):
+        check_time_index(x)
+
+
+def test_check_consistent_index_type():
+    """Test check_consistent_index_type."""
+    index1 = pd.RangeIndex(start=0, stop=5)
+    index2 = pd.Index(["A", "B", "C", "D", "E"])
+
+    # An exception should be raised because index types are inconsistent
+    with pytest.raises(TypeError):
+        check_consistent_index_type(index1, index2)