Repository: aeon-toolkit/aeon (python)

Issue:
[BUG][CI] signature_method notebook not working under python 3.10
### Describe the bug

When I bumped the python version to 3.10 for running notebook examples I got an error from one of the notebooks.

### Steps/Code to reproduce the bug

```
jupyter nbconvert --to notebook --inplace --execute --ExecutePreprocessor.timeout=600 examples/signature_method.ipynb
```

### Expected results

notebook executes 

### Actual results

```python-traceback
# First build a very simple signature transform module
signature_transform = SignatureTransformer(
    augmentation_list=("addtime",),
    window_name="global",
    window_depth=None,
    window_length=None,
    window_step=None,
    rescaling=None,
    sig_tfm="signature",
    depth=3,
)

# The simply transform the stream data
print("Raw data shape is: {}".format(train_x.shape))
train_signature_x = signature_transform.fit_transform(train_x)
print("Signature shape is: {}".format(train_signature_x.shape))
------------------


---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[4], line 2
      1 # First build a very simple signature transform module
----> 2 signature_transform = SignatureTransformer(
      3     augmentation_list=("addtime",),
      4     window_name="global",
      5     window_depth=None,
      6     window_length=None,
      7     window_step=None,
      8     rescaling=None,
      9     sig_tfm="signature",
     10     depth=3,
     11 )
     13 # The simply transform the stream data
     14 print("Raw data shape is: {}".format(train_x.shape))

File /opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/aeon/transformations/panel/signature_based/_signature_method.py:76, in SignatureTransformer.__init__(self, augmentation_list, window_name, window_depth, window_length, window_step, rescaling, sig_tfm, depth)
     73 self.sig_tfm = sig_tfm
     74 self.depth = depth
---> 76 super(SignatureTransformer, self).__init__()
     77 self.setup_feature_pipeline()

File /opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/aeon/transformations/base.py:152, in BaseTransformer.__init__(self, _output_convert)
    149 self._output_convert = _output_convert
    151 super(BaseTransformer, self).__init__()
--> 152 _check_estimator_deps(self)

File /opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/aeon/utils/validation/_dependencies.py:358, in _check_estimator_deps(obj, msg, severity)
    355         compatible = compatible and x_chk
    356     return compatible
--> 358 compatible = compatible and _check_python_version(obj, severity=severity)
    360 pkg_deps = obj.get_class_tag("python_dependencies", None)
    361 if pkg_deps is not None and not isinstance(pkg_deps, list):

File /opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/aeon/utils/validation/_dependencies.py:296, in _check_python_version(obj, package, msg, severity)
    291         msg += (
    292             f" This is due to python version requirements of the {package} package."
    293         )
    295 if severity == "error":
--> 296     raise ModuleNotFoundError(msg)
    297 elif severity == "warning":
    298     warnings.warn(msg)

ModuleNotFoundError: SignatureTransformer requires python version to be <3.10, but system python version is 3.10.11 (main, Apr  6 2023, 07:59:08) [GCC 11.3.0].
```

### Versions

_No response_

Pull request description:
[MNT] Loosen upper bound for `esig` and update Python version for notebook tests.
Fixes #378.

Still doesn't work for Python 3.11 apparently, but at least we have 3.10 now 🙂.

Sets `fail-fast: false` in the testing strategy, as early abandoning tests can cause some confusion with what the cause of certain failures is (it did in this PR at least).

Patch:
diff --git a/aeon/classification/base.py b/aeon/classification/base.py
index 9cf7902e..1b7b0a5c 100644
--- a/aeon/classification/base.py
+++ b/aeon/classification/base.py
@@ -107,7 +107,6 @@ class BaseClassifier(BaseEstimator, ABC):
 
         # behaviour is implemented only if other inherits from BaseTransformer
         #  in that case, distinctions arise from whether self or other is a pipeline
-        #  todo: this can probably be simplified further with "zero length" pipelines
         if isinstance(other, BaseTransformer):
             # ClassifierPipeline already has the dunder method defined
             if isinstance(self, ClassifierPipeline):
@@ -129,10 +128,14 @@ class BaseClassifier(BaseEstimator, ABC):
         Parameters
         ----------
         X : 3D np.array (any number of channels, equal length series)
-                of shape [n_instances, n_channels, series_length]
+                of shape (n_instances, n_channels, n_timepoints)
             or 2D np.array (univariate, equal length series)
-                of shape [n_instances, series_length]
-        y : 1D np.array of int, of shape [n_instances] - class labels for fitting
+                of shape (n_instances, 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), where
+                n_timepoints_i is length of series i
+            other types are allowed and converted into one of the above.
+        y : 1D np.array, of shape [n_instances] - class labels for fitting
             indices correspond to instance indices in X
 
         Returns
@@ -198,12 +201,18 @@ class BaseClassifier(BaseEstimator, ABC):
 
         Parameters
         ----------
-        X : 3D np.array of shape (n_instances, n_channels, series_length)
-            or 2D np.array of shape (n_instances, series_length)
+        X : 3D np.array (any number of channels, equal length series)
+                of shape (n_instances, n_channels, n_timepoints)
+            or 2D np.array (univariate, equal length series)
+                of shape (n_instances, 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), where
+                n_timepoints_i is length of series i
+            other types are allowed and converted into one of the above.
 
         Returns
         -------
-        y : 1D np.array of int, of shape [n_instances] - predicted class labels
+        y : 1D np.array, of shape [n_instances] - predicted class labels
             indices correspond to instance indices in X
         """
         self.check_is_fitted()
@@ -213,7 +222,8 @@ class BaseClassifier(BaseEstimator, ABC):
 
         # handle the single-class-label case
         if len(self._class_dictionary) == 1:
-            return self._single_class_y_pred(X, method="predict")
+            n_instances = _get_n_cases(X)
+            return np.repeat(list(self._class_dictionary.keys()), n_instances)
 
         # call internal _predict_proba
         return self._predict(X)
@@ -223,8 +233,14 @@ class BaseClassifier(BaseEstimator, ABC):
 
         Parameters
         ----------
-        X : 3D np.array of shape (n_cases, n_channels, series_length)
-            or 2D np.array of shape (n_cases, series_length)
+        X : 3D np.array (any number of channels, equal length series)
+                of shape (n_instances, n_channels, n_timepoints)
+            or 2D np.array (univariate, equal length series)
+                of shape (n_instances, 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), where
+                n_timepoints_i is length of series i
+            other types are allowed and converted into one of the above.
 
         Returns
         -------
@@ -240,29 +256,25 @@ class BaseClassifier(BaseEstimator, ABC):
 
         # handle the single-class-label case
         if len(self._class_dictionary) == 1:
-            return self._single_class_y_pred(X, method="predict_proba")
+            n_instances = _get_n_cases(X)
+            return np.repeat([[1]], n_instances, axis=0)
 
         # call internal _predict_proba
         return self._predict_proba(X)
 
-    def _single_class_y_pred(self, X, method="predict"):
-        """Handle the prediction case where only single class label was seen in fit."""
-        _, _, X_meta = check_is_scitype(X, scitype="Panel", return_metadata=True)
-        n_instances = X_meta["n_instances"]
-        if method == "predict":
-            return np.repeat(list(self._class_dictionary.keys()), n_instances)
-        else:  # method == "predict_proba"
-            return np.repeat([[1]], n_instances, axis=0)
-
     def score(self, X, y) -> float:
         """Scores predicted labels against ground truth labels on X.
 
         Parameters
         ----------
         X : 3D np.array (any number of channels, equal length series)
-                of shape [n_instances, n_channels, series_length]
+                of shape (n_instances, n_channels, n_timepoints)
             or 2D np.array (univariate, equal length series)
-                of shape [n_instances, series_length]
+                of shape (n_instances, 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), where
+                n_timepoints_i is length of series i
+            other types are allowed and converted into one of the above.
         y : 1D np.ndarray of shape [n_instances] - class labels (ground truth)
             indices correspond to instance indices in X
 
@@ -307,8 +319,10 @@ class BaseClassifier(BaseEstimator, ABC):
         ----------
         X : guaranteed to be of a type in self.get_tag("X_inner_mtype")
             if self.get_tag("X_inner_mtype") = "numpy3D":
-                3D np.ndarray of shape = [n_instances, n_channels, series_length]
-        y : 1D np.array of int, of shape [n_instances] - class labels for fitting
+                3D np.ndarray of shape = (n_instances, n_channels, n_timepoints)
+            if self.get_tag("X_inner_mtype") = "np-list":
+                list of 2D np.ndarray of shape = [n_instances]
+        y : 1D np.array of int, of shape (n_instances,) - class labels for fitting
             indices correspond to instance indices in X
 
         Returns
@@ -333,11 +347,13 @@ class BaseClassifier(BaseEstimator, ABC):
         ----------
         X : guaranteed to be of a type in self.get_tag("X_inner_mtype")
             if self.get_tag("X_inner_mtype") = "numpy3D":
-                3D np.ndarray of shape = [n_instances, n_channels, series_length]
+                3D np.ndarray of shape = (n_instances, n_channels, n_timepoints)
+            if self.get_tag("X_inner_mtype") = "np-list":
+                list of 2D np.ndarray of shape = (n_instances,)
 
         Returns
         -------
-        y : 1D np.array of int, of shape [n_instances] - predicted class labels
+        y : 1D np.array of int, of shape (n_instances,) - predicted class labels
             indices correspond to instance indices in X
         """
         ...
@@ -353,7 +369,9 @@ class BaseClassifier(BaseEstimator, ABC):
         ----------
         X : guaranteed to be of a type in self.get_tag("X_inner_mtype")
             if self.get_tag("X_inner_mtype") = "numpy3D":
-                3D np.ndarray of shape = [n_instances, n_channels, series_length]
+                3D np.ndarray of shape = (n_instances, n_channels, n_timepoints)
+            if self.get_tag("X_inner_mtype") = "np-list":
+                list of 2D np.ndarray of shape = (n_instances,)
 
         Returns
         -------
@@ -553,3 +571,10 @@ class BaseClassifier(BaseEstimator, ABC):
         if y is None:
             return X
         return X, y
+
+
+def _get_n_cases(X):
+    """Handle the single exception of multi index DataFrame."""
+    if isinstance(X, pd.DataFrame) and isinstance(X.index, pd.MultiIndex):
+        return len(X.index.get_level_values(0).unique())
+    return len(X)
diff --git a/aeon/classification/feature_based/_fresh_prince.py b/aeon/classification/feature_based/_fresh_prince.py
index 59d09b46..3ac12402 100644
--- a/aeon/classification/feature_based/_fresh_prince.py
+++ b/aeon/classification/feature_based/_fresh_prince.py
@@ -65,7 +65,6 @@ class FreshPRINCEClassifier(BaseClassifier):
         "capability:multithreading": True,
         "capability:train_estimate": True,
         "algorithm_type": "feature",
-        "python_version": "<3.10",
         "python_dependencies": "tsfresh",
     }
 
diff --git a/aeon/classification/feature_based/_signature_classifier.py b/aeon/classification/feature_based/_signature_classifier.py
index 5acc96fa..17685be1 100644
--- a/aeon/classification/feature_based/_signature_classifier.py
+++ b/aeon/classification/feature_based/_signature_classifier.py
@@ -94,7 +94,7 @@ class SignatureClassifier(BaseClassifier):
         "capability:multivariate": True,
         "algorithm_type": "feature",
         "python_dependencies": "esig",
-        "python_version": "<3.10",
+        "python_version": "<3.11",
     }
 
     def __init__(
diff --git a/aeon/datasets/__init__.py b/aeon/datasets/__init__.py
index 0a98ab67..1a076dbb 100644
--- a/aeon/datasets/__init__.py
+++ b/aeon/datasets/__init__.py
@@ -1,4 +1,5 @@
 # -*- coding: utf-8 -*-
+# copyright: aeon developers, BSD-3-Clause License (see LICENSE file)
 """Functions to load and write datasets."""
 
 __all__ = [
@@ -15,6 +16,7 @@ __all__ = [
     "write_to_tsfile",
     "write_results_to_uea_format",
     # Data generators
+    "make_example_3d_numpy",
     "make_example_long_table",
     "make_example_multi_index_dataframe",
     # Single problem loaders
@@ -48,6 +50,7 @@ __all__ = [
 ]
 
 from aeon.datasets._data_generators import (
+    make_example_3d_numpy,
     make_example_long_table,
     make_example_multi_index_dataframe,
 )
@@ -64,6 +67,7 @@ from aeon.datasets._data_loaders import (
 from aeon.datasets._data_writers import write_results_to_uea_format, write_to_tsfile
 from aeon.datasets._dataframe_loaders import (
     load_from_long_to_dataframe,
+    load_from_tsfile_to_dataframe,
     load_tsf_to_dataframe,
 )
 from aeon.datasets._single_problem_loaders import (
diff --git a/aeon/datasets/_data_generators.py b/aeon/datasets/_data_generators.py
index 1a41b799..bcbc9c74 100644
--- a/aeon/datasets/_data_generators.py
+++ b/aeon/datasets/_data_generators.py
@@ -1,28 +1,104 @@
 # -*- coding: utf-8 -*-
-from typing import Dict
+# copyright: aeon developers, BSD-3-Clause License (see LICENSE file)
+"""Data generators."""
+
+__author__ = ["MatthewMiddlehurst", "TonyBagnall"]
+__all__ = [
+    "make_example_3d_numpy",
+    "make_example_long_table",
+    "make_example_multi_index_dataframe",
+]
+
+from typing import Dict, Tuple, Union
 
 import numpy as np
 import pandas as pd
 
+from aeon.utils._testing.collection import make_3d_test_data
+
+
+def make_example_3d_numpy(
+    n_cases: int = 10,
+    n_channels: int = 1,
+    n_timepoints: int = 12,
+    return_y: bool = False,
+    n_labels: int = 2,
+    regression_target: bool = False,
+    random_state: Union[int, None] = None,
+) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
+    """Randomly generate 3D X and y data for testing.
+
+    Will ensure there is at least one sample per label if a regression problems.
+
+    Parameters
+    ----------
+    n_cases : int
+        The number of samples to generate.
+    n_channels : int
+        The number of series channels to generate.
+    n_timepoints : int
+        The number of features/series length to generate.
+    return_y : bool
+        If True, return the labels as well as the data.
+    n_labels : int
+        The number of unique labels to generate.
+    regression_target : bool
+        If True, the target will be a float, otherwise an int.
+    random_state : int or None
+        Seed for random number generation.
 
-def make_example_long_table(n_cases=50, n_channels=2, series_length=20):
+    Returns
+    -------
+    X : np.ndarray
+        Randomly generated 3D data.
+    y : np.ndarray
+        Randomly generated labels.
+
+    Examples
+    --------
+    >>> from aeon.datasets import make_example_3d_numpy
+    >>> data, labels = make_example_3d_numpy(n_cases=2, n_channels=2, n_timepoints=6,
+    ...                                      return_y=True, n_labels=2, random_state=0)
+    >>> print(data)
+    [[[0.         1.43037873 1.20552675 1.08976637 0.8473096  1.29178823]
+      [0.87517442 1.783546   1.92732552 0.76688304 1.58345008 1.05778984]]
+    <BLANKLINE>
+     [[2.         3.70238655 0.28414423 0.3485172  0.08087359 3.33047938]
+      [3.112627   3.48004859 3.91447337 3.19663426 1.84591745 3.12211671]]]
+    >>> print(labels)
+    [0 1]
+    """
+    X, y = make_3d_test_data(
+        n_cases=n_cases,
+        n_channels=n_channels,
+        n_timepoints=n_timepoints,
+        n_labels=n_labels,
+        regression_target=regression_target,
+        random_state=random_state,
+    )
+    if return_y:
+        return X, y
+    return X
+
+
+def make_example_long_table(n_cases=50, n_channels=2, n_timepoints=20):
     """Generate example from long table format file.
 
     Parameters
     ----------
     n_cases: int
         Number of cases.
-    series_length: int
-        Length of the series.
     n_channels: int
         Number of dimensions.
+    n_timepoints: int
+        Length of the series.
 
     Returns
     -------
     DataFrame containing random data in long format.
     """
-    rows_per_case = series_length * n_channels
-    total_rows = n_cases * series_length * n_channels
+    rows_per_case = n_timepoints * n_channels
+    total_rows = n_cases * n_timepoints * n_channels
 
     case_ids = np.empty(total_rows, dtype=int)
     idxs = np.empty(total_rows, dtype=int)
@@ -32,8 +108,8 @@ def make_example_long_table(n_cases=50, n_channels=2, series_length=20):
     for i in range(total_rows):
         case_ids[i] = int(i / rows_per_case)
         rem = i % rows_per_case
-        dims[i] = int(rem / series_length)
-        idxs[i] = rem % series_length
+        dims[i] = int(rem / n_timepoints)
+        idxs[i] = rem % n_timepoints
 
     df = pd.DataFrame()
     df["case_id"] = pd.Series(case_ids)
@@ -43,7 +119,7 @@ def make_example_long_table(n_cases=50, n_channels=2, series_length=20):
     return df
 
 
-def make_example_multi_index_dataframe(n_instances=50, n_channels=3, series_length=20):
+def make_example_multi_index_dataframe(n_instances=50, n_channels=3, n_timepoints=20):
     """Generate example multi-index DataFrame.
 
     Parameters
@@ -52,7 +128,7 @@ def make_example_multi_index_dataframe(n_instances=50, n_channels=3, series_leng
         Number of instances.
     n_channels : int
         Number of columns (series) in multi-indexed DataFrame.
-    series_length : int
+    n_timepoints : int
         Number of timepoints per instance-column pair.
 
     Returns
@@ -63,7 +139,7 @@ def make_example_multi_index_dataframe(n_instances=50, n_channels=3, series_leng
     """
     # Make long DataFrame
     long_df = make_example_long_table(
-        n_cases=n_instances, series_length=series_length, n_channels=n_channels
+        n_cases=n_instances, n_timepoints=n_timepoints, n_channels=n_channels
     )
     # Make Multi index DataFrame
     mi_df = long_df.set_index(["case_id", "reading_id"]).pivot(columns="dim_id")
diff --git a/aeon/transformations/collection/signature_based/_compute.py b/aeon/transformations/collection/signature_based/_compute.py
index 38ef8747..ff976db0 100644
--- a/aeon/transformations/collection/signature_based/_compute.py
+++ b/aeon/transformations/collection/signature_based/_compute.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 """Class for signature computation over windows."""
+
 import numpy as np
 
 from aeon.transformations.base import BaseTransformer
@@ -8,9 +9,6 @@ from aeon.transformations.collection.signature_based._rescaling import (
     _rescale_signature,
 )
 from aeon.transformations.collection.signature_based._window import _window_getter
-from aeon.utils.validation._dependencies import _check_soft_dependencies
-
-_check_soft_dependencies("esig", severity="warning")
 
 
 class _WindowSignatureTransform(BaseTransformer):
@@ -37,7 +35,7 @@ class _WindowSignatureTransform(BaseTransformer):
         "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for X?
         "fit_is_empty": True,
         "python_dependencies": "esig",
-        "python_version": "<3.10",
+        "python_version": "<3.11",
     }
 
     def __init__(
diff --git a/aeon/transformations/collection/signature_based/_rescaling.py b/aeon/transformations/collection/signature_based/_rescaling.py
index 2971b348..687b72c4 100644
--- a/aeon/transformations/collection/signature_based/_rescaling.py
+++ b/aeon/transformations/collection/signature_based/_rescaling.py
@@ -8,14 +8,11 @@ rescaling methods along with generic scaling methods along feature
 dimensions of 3D tensors.
 Code for `rescale_path` and `rescale_signature` written by Patrick Kidger.
 """
+
 import math
 
 import numpy as np
 
-from aeon.utils.validation._dependencies import _check_soft_dependencies
-
-_check_soft_dependencies("esig", severity="warning")
-
 
 def _rescale_path(path, depth):
     """Rescale input path by depth! ** (1 / depth).
diff --git a/aeon/transformations/collection/signature_based/_signature_method.py b/aeon/transformations/collection/signature_based/_signature_method.py
index b988bcb9..a76569de 100644
--- a/aeon/transformations/collection/signature_based/_signature_method.py
+++ b/aeon/transformations/collection/signature_based/_signature_method.py
@@ -50,7 +50,7 @@ class SignatureTransformer(BaseTransformer):
         "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for X?#
         "fit_is_empty": False,
         "python_dependencies": "esig",
-        "python_version": "<3.10",
+        "python_version": "<3.11",
     }
 
     def __init__(
diff --git a/extension_templates/classification.py b/extension_templates/classification.py
index 796b2435..07254550 100644
--- a/extension_templates/classification.py
+++ b/extension_templates/classification.py
@@ -86,8 +86,8 @@ class MyTimeSeriesClassifier(BaseClassifier):
     # these are the default values, only add if different to these.
     _tags = {
         "X_inner_mtype": "numpy3D",  # which type do _fit/_predict accept, usually
-        # this is usually "numpy3D" Other types are allowable, see
-        # datatypes/panel/_registry.py for options.
+        # this is usually "numpy3D" for equal length time series, np-list for unequal
+        # length time series, see datatypes/panel/_registry.py for options.
         "capability:multivariate": False,
         "capability:unequal_length": False,
         "capability:missing_values": False,
@@ -97,10 +97,11 @@ class MyTimeSeriesClassifier(BaseClassifier):
         "python_version": None,  # PEP 440 python version specifier to limit versions
     }
 
-    # todo: add any hyper-parameters and components to constructor
+    # todo: add any parameters to constructor
     def __init__(self, param_a, param_b="default", param_c=None):
-        # estimators should precede parameters
-        #  if estimators have default values, set None and initalize below
+        # Note that parameters passed and set in constructor should not be changed
+        # anywhere else. This is in order to comply with scikit learn structure.
+        # Instead, copy into local variables in fit and predict and use these.
 
         # todo: copy parameters to self, use same names
         self.param_a = param_a
@@ -151,7 +152,9 @@ class MyTimeSeriesClassifier(BaseClassifier):
         ----------
         X : guaranteed to be of a type in self.get_tag("X_inner_mtype")
             if self.get_tag("X_inner_mtype") = "numpy3D":
-                3D np.ndarray of shape = [n_instances, n_dimensions, series_length]
+                3D np.ndarray of shape = (n_instances, n_channels, n_timepoints)
+            if self.get_tag("X_inner_mtype") = "np-list":
+                list of 2D np.ndarray of shape = (n_instances,)
 
         Returns
         -------
@@ -178,7 +181,9 @@ class MyTimeSeriesClassifier(BaseClassifier):
         ----------
         X : guaranteed to be of a type in self.get_tag("X_inner_mtype")
             if self.get_tag("X_inner_mtype") = "numpy3D":
-                3D np.ndarray of shape = [n_instances, n_dimensions, series_length]
+                3D np.ndarray of shape = (n_instances, n_channels, n_timepoints)
+            if self.get_tag("X_inner_mtype") = "np-list":
+                list of 2D np.ndarray of shape = (n_instances,)
 
         Returns
         -------
diff --git a/pyproject.toml b/pyproject.toml
index 262c6152..6a3c63d5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -53,7 +53,7 @@ dependencies = [
 all_extras = [
     "cloudpickle",
     "dask",
-    "esig==0.9.7; python_version < '3.10'",
+    "esig>=0.9.7; python_version < '3.11'",
     "filterpy>=1.4.5",
     "h5py",
     "hmmlearn>=0.2.7",


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 12d8b75d..ea2891c9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -20,7 +20,7 @@ jobs:
 
       - uses: actions/setup-python@v4
         with:
-          python-version: 3.9
+          python-version: '3.10'
 
       - uses: tj-actions/changed-files@v35.9.0
         id: changed-files
@@ -44,7 +44,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v4
         with:
-          python-version: '3.9'
+          python-version: '3.10'
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
@@ -103,6 +103,7 @@ jobs:
     needs: test-nosoftdeps
     runs-on: windows-2019
     strategy:
+      fail-fast: false
       matrix:
         python-version: ['3.8', '3.9', '3.10', '3.11']
 
@@ -144,6 +145,7 @@ jobs:
   test-unix:
     needs: test-nosoftdeps
     strategy:
+      fail-fast: false
       matrix:
         python-version: ['3.8', '3.9', '3.10', '3.11']
         os: [ubuntu-20.04, macOS-11]
diff --git a/aeon/datatypes/tests/test_panel_converters.py b/aeon/datatypes/tests/test_panel_converters.py
index 2eb317a1..fca11e97 100644
--- a/aeon/datatypes/tests/test_panel_converters.py
+++ b/aeon/datatypes/tests/test_panel_converters.py
@@ -109,7 +109,7 @@ def test_from_3d_numpy_to_2d_array(n_instances, n_columns, n_timepoints):
 def test_from_multi_index_to_3d_numpy(n_instances, n_columns, n_timepoints):
     """Test from_multi_index_to_3d_numpy for correctness."""
     mi_df = make_example_multi_index_dataframe(
-        n_instances=n_instances, series_length=n_timepoints, n_channels=n_columns
+        n_instances=n_instances, n_timepoints=n_timepoints, n_channels=n_columns
     )
 
     array = from_multi_index_to_3d_numpy(mi_df)
@@ -149,7 +149,7 @@ def test_from_3d_numpy_to_multi_index(n_instances, n_columns, n_timepoints):
 def test_from_multi_index_to_nested(n_instances, n_columns, n_timepoints):
     """Test from_multi_index_to_nested for correctness."""
     mi_df = make_example_multi_index_dataframe(
-        n_instances=n_instances, series_length=n_timepoints, n_channels=n_columns
+        n_instances=n_instances, n_timepoints=n_timepoints, n_channels=n_columns
     )
     nested_df = from_multi_index_to_nested(
         mi_df, instance_index="case_id", cells_as_numpy=False
@@ -188,7 +188,7 @@ def test_is_nested_dataframe(n_instances, n_columns, n_timepoints):
     nested_heterogenous = pd.concat([zero_df, nested], axis=1)
 
     mi_df = make_example_multi_index_dataframe(
-        n_instances=n_instances, series_length=n_timepoints, n_channels=n_columns
+        n_instances=n_instances, n_timepoints=n_timepoints, n_channels=n_columns
     )
 
     assert not is_nested_dataframe(array)
@@ -216,7 +216,7 @@ def test_from_2d_array_to_nested(n_instances, n_columns, n_timepoints):
 def test_from_long_to_nested(n_instances, n_columns, n_timepoints):
     """Test from_long_to_nested for correctness."""
     X_long = make_example_long_table(
-        n_cases=n_instances, series_length=n_timepoints, n_channels=n_columns
+        n_cases=n_instances, n_timepoints=n_timepoints, n_channels=n_columns
     )
     nested_df = from_long_to_nested(X_long)