Repository: aeon-toolkit/aeon
Pull request #892 (2023-11-06 12:41:34)

[MNT] Setup file for Dependabot (automatic dependency updates)
Adds a .yml file to enable dependabot. For the most part follows [this guidance](https://til.simonwillison.net/github/dependabot-python-setup) and the [numpy version of the file](https://github.com/numpy/numpy/blob/main/.github/dependabot.yml). The configuration options are available [here](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file).

What this should do is check for updates to the dependencies in our workflows and `pyproject.toml` every Monday and create a singular PR for each (if any have updates). Some of these probably won't be easy to merge, but it adds a bit of automation to the process of keeping it all up to date.

Follows on from #881 somewhat which does the same for `pre-commit` versions.

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

I have added the following labels to this PR based on the title: **[ $\\color{#EC843A}{\\textsf{maintenance}}$ ]**.


The "Checks" tab above or the panel below will show the status of our automated tests. From the panel below you can click on "Details" to the right of the check to see more information if there is a failure. If our `pre-commit` code quality check fails, any trivial fixes will automatically be pushed to your PR.

Don't hesitate to ask questions on the `aeon` Slack channel if you have any!
    
sweep-ai[bot]: ## Apply [Sweep Rules](https://docs.sweep.dev/usage/config#rules) to your PR?
- [ ] **Apply:**  All new business logic should have corresponding unit tests.
- [ ] **Apply:**  Refactor large functions to be more modular.

Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..56d5c604
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,34 @@
+version: 2
+updates:
+  # update GitHub actions versions
+  - package-ecosystem: github-actions
+    directory: /
+    schedule:
+      interval: weekly
+      time: "01:00"
+    commit-message:
+      prefix: "[MNT]"
+    labels:
+      - "maintenance"
+      - "no changelog"
+    groups:
+      github-actions:
+        patterns:
+          - "*"
+
+  # update Python dependencies
+  - package-ecosystem: pip
+    directory: /
+    schedule:
+      interval: weekly
+      time: "01:00"
+    commit-message:
+      prefix: "[MNT]"
+    labels:
+      - "maintenance"
+      - "full pytest actions"
+      - "no changelog"
+    groups:
+      python-packages:
+        patterns:
+          - "*"
diff --git a/.github/workflows/pr_opened.yml b/.github/workflows/pr_opened.yml
index 6b9026fe..74aaa608 100644
--- a/.github/workflows/pr_opened.yml
+++ b/.github/workflows/pr_opened.yml
@@ -25,18 +25,18 @@ jobs:
       - name: Install PyGithub
         run: pip install -Uq PyGithub
 
-      - name: Label pull request
-        id: label-pr
-        run: python build_tools/pr_labeler.py
-        env:
-          CONTEXT_GITHUB: ${{ toJson(github) }}
-
       - uses: actions/create-github-app-token@v1
         id: app-token
         with:
           app-id: ${{ vars.PR_APP_ID }}
           private-key: ${{ secrets.PR_APP_KEY }}
 
+      - name: Label pull request
+        id: label-pr
+        run: python build_tools/pr_labeler.py ${{ steps.app-token.outputs.token }}
+        env:
+          CONTEXT_GITHUB: ${{ toJson(github) }}
+
       - name: Write pull request comment
         run: python build_tools/pr_open_commenter.py ${{ steps.app-token.outputs.token }} ${{ steps.label-pr.outputs.title-labels }} ${{ steps.label-pr.outputs.title-labels-new }} ${{ steps.label-pr.outputs.content-labels }} ${{ steps.label-pr.outputs.content-labels-status }}
         env:
diff --git a/aeon/datasets/_data_writers.py b/aeon/datasets/_data_writers.py
index 05de0282..0618ad5e 100644
--- a/aeon/datasets/_data_writers.py
+++ b/aeon/datasets/_data_writers.py
@@ -203,7 +203,7 @@ def write_results_to_uea_format(
     third_line : str
         summary performance information (see comment below)
     """
-    if len(y_true) != len(y_pred):
+    if y_true is not None and len(y_true) != len(y_pred):
         raise IndexError(
             "The number of predicted values is not the same as the "
             "number of actual class values"
@@ -287,11 +287,7 @@ def _write_header(
     extension=None,
 ):
     if class_labels is not None and regression:
-        raise ValueError(
-            "Cannot have class_labels and targetlabel. If the problem "
-            "is classification, add class_labels. If regression, "
-            "set targetlabel to true."
-        )
+        raise ValueError("Cannot have class_labels true for a regression problem")
     # create path if it does not exist
     dir = f"{str(path)}/"
     try:
diff --git a/build_tools/pr_labeler.py b/build_tools/pr_labeler.py
index 3e00eb76..eed9ec4a 100644
--- a/build_tools/pr_labeler.py
+++ b/build_tools/pr_labeler.py
@@ -8,13 +8,14 @@ Based on the scikit-learn v1.3.1 label_title_regex.py script.
 import json
 import os
 import re
+import sys
 
 from github import Github
 
 context_dict = json.loads(os.getenv("CONTEXT_GITHUB"))
 
 repo = context_dict["repository"]
-g = Github(context_dict["token"])
+g = Github(sys.argv[1])
 repo = g.get_repo(repo)
 pr_number = context_dict["event"]["number"]
 pr = repo.get_pull(number=pr_number)
diff --git a/build_tools/pr_open_commenter.py b/build_tools/pr_open_commenter.py
index 6f226a4a..d23d66ff 100644
--- a/build_tools/pr_open_commenter.py
+++ b/build_tools/pr_open_commenter.py
@@ -97,8 +97,10 @@ pr.create_issue_comment(
 {title_labels_str}
 {content_labels_str}
 
-The "Checks" tab above or the panel below will show the status of our automated tests. From the panel below you can click on "Details" to the right of the check to see more information if there is a failure. If our `pre-commit` code quality check fails, any trivial fixes will automatically be pushed to your PR.
+The [Checks](https://github.com/aeon-toolkit/aeon/pull/{pr_number}/checks) tab will show the status of our automated tests. You can click on individual test runs in the tab or "Details" in the panel below to see more information if there is a failure.
 
-Don't hesitate to ask questions on the `aeon` Slack channel if you have any!
+If our `pre-commit` code quality check fails, any trivial fixes will automatically be pushed to your PR unless it is a draft.
+
+Don't hesitate to ask questions on the `aeon` [Slack](https://join.slack.com/t/aeon-toolkit/shared_invite/zt-22vwvut29-HDpCu~7VBUozyfL_8j3dLA) channel if you have any!
     """  # noqa
 )
diff --git a/sweep.yaml b/sweep.yaml
index 671c0f71..08422e7d 100644
--- a/sweep.yaml
+++ b/sweep.yaml
@@ -2,7 +2,7 @@ gha_enabled: True
 branch: "main"
 blocked_dirs: [".github/"]
 draft: False
-description: "aeon-toolkit/aeon is a Python project for machine learning algorithms using time series data. Write code that adheres to PEP8 and is formatted to the Black code style. The projects documentation is built using Sphinx and MyST, and unit testing is done using pytest."
+description: "aeon-toolkit/aeon is a Python project for machine learning algorithms using time series data.  We have generalised tests for all of our ML algorithms which are in a separate class. Write code that adheres to PEP8 and is formatted to the Black code style. The projects documentation is built using Sphinx and MyST, and unit testing is done using pytest."
 
 docs:
   scikit-learn: ["https://scikit-learn.org/stable/", "We use the scikit-learn framework for our machine learning algorithms."]
@@ -21,4 +21,5 @@ sandbox:
   check:
     - git add . && pre-commit run --files {file_path} || exit 0
 
-rules: []
+rules:
+  - ""


Test patch:
diff --git a/aeon/datasets/tests/test_data_loaders.py b/aeon/datasets/tests/test_data_loaders.py
index 38d62e2b..8b76dd58 100644
--- a/aeon/datasets/tests/test_data_loaders.py
+++ b/aeon/datasets/tests/test_data_loaders.py
@@ -6,6 +6,7 @@ __all__ = []
 
 import os
 import shutil
+import tempfile
 
 import numpy as np
 import pandas as pd
@@ -158,6 +159,29 @@ def test__load_header_info():
         assert meta_data["classlabel"]
         assert meta_data["class_values"][0] == "1"
         assert meta_data["class_values"][1] == "2"
+    WRONG_STRINGS = ["@data 55", "@missing 42, @classlabel True"]
+    count = 1
+    with tempfile.TemporaryDirectory() as tmp:
+        for name in WRONG_STRINGS:
+            problem_name = f"temp{count}.ts"
+            load_path = os.path.join(tmp, problem_name)
+            temp_file = open(load_path, "w", encoding="utf-8")
+            temp_file.write(name)
+            temp_file.close()
+            with open(load_path, "r", encoding="utf-8") as file:
+                with pytest.raises(IOError):
+                    _load_header_info(file)
+            count = count + 1
+        name = "@missing true \n @classlabel True 0 1"
+        problem_name = "temp_correct.ts"
+        load_path = os.path.join(tmp, problem_name)
+        temp_file = open(load_path, "w", encoding="utf-8")
+        temp_file.write(name)
+        temp_file.close()
+        with open(load_path, "r", encoding="utf-8") as file:
+            meta = _load_header_info(file)
+            assert meta["missing"] is True
+            assert meta["classlabel"] is True
 
 
 def test__load_data():
@@ -182,6 +206,38 @@ def test__load_data():
         meta_data["equallength"] = True
         with pytest.raises(IOError):
             X, y, _ = _load_data(file, meta_data)
+    WRONG_DATA = [
+        "1.0,2.0,3.0:1.0,2.0,3.0, 4.0:0",
+        "1.0,2.0,3.0:1.0,2.0,3.0, 4.0:0\n1.0,2.0,3.0,4.0:0",
+    ]
+    meta_data = {"classlabel": True, "class_values": [0, 1], "equallength": False}
+    count = 1
+    with tempfile.TemporaryDirectory() as tmp:
+        for data in WRONG_DATA:
+            problem_name = f"temp{count}.ts"
+            load_path = os.path.join(tmp, problem_name)
+            temp_file = open(load_path, "w", encoding="utf-8")
+            temp_file.write(data)
+            temp_file.close()
+            with open(load_path, "r", encoding="utf-8") as file:
+                with pytest.raises(IOError):
+                    _load_data(file, meta_data)
+            count = count + 1
+        meta_data = {
+            "classlabel": True,
+            "class_values": [0, 1],
+            "equallength": True,
+            "univariate": True,
+        }
+        data = "1.0,2.0,3.0:0.0\n 1.0,2.0,3.0:1.0\n 2.0,3.0,4.0:0"
+        problem_name = "tempCorrect.ts"
+        load_path = os.path.join(tmp, problem_name)
+        temp_file = open(load_path, "w", encoding="utf-8")
+        temp_file.write(data)
+        temp_file.close()
+        with open(load_path, "r", encoding="utf-8") as file:
+            X, y, meta_data = _load_data(file, meta_data)
+            assert isinstance(X, np.ndarray)
 
 
 @pytest.mark.parametrize("return_X_y", [True, False])
diff --git a/aeon/datasets/tests/test_data_writers.py b/aeon/datasets/tests/test_data_writers.py
index 2ba1d683..c9b5a799 100644
--- a/aeon/datasets/tests/test_data_writers.py
+++ b/aeon/datasets/tests/test_data_writers.py
@@ -8,7 +8,12 @@ import pandas as pd
 import pytest
 
 from aeon.datasets import load_from_tsfile, write_to_tsfile
-from aeon.datasets._data_writers import _write_dataframe_to_tsfile
+from aeon.datasets._data_writers import (
+    _write_data_to_tsfile,
+    _write_dataframe_to_tsfile,
+    _write_header,
+    write_results_to_uea_format,
+)
 from aeon.datasets._dataframe_loaders import load_from_tsfile_to_dataframe
 from aeon.utils._testing.collection import (
     make_3d_test_data,
@@ -101,3 +106,71 @@ def test_write_dataframe_to_ts(tsfile_writer):
         pd.testing.assert_frame_equal(newX, X)
         y2 = pd.Series(y)
         pd.testing.assert_series_equal(y, y2)
+
+
+def test_write_data_to_tsfile():
+    with pytest.raises(TypeError, match="Wrong input data type"):
+        write_to_tsfile("A string", "path")
+    with pytest.raises(TypeError, match="Data provided must be a ndarray or a list"):
+        _write_data_to_tsfile("AFC", "49", "undefeated")
+    X, _ = make_3d_test_data(n_cases=6, n_timepoints=10, n_channels=1)
+    y = np.ndarray([0, 1, 1, 0, 1])
+    with pytest.raises(
+        IndexError,
+        match="The number of cases in X does not match the number of values in y",
+    ):
+        _write_data_to_tsfile(X, "temp", "temp", y=y)
+
+
+def test_write_results_to_uea_format():
+    with tempfile.TemporaryDirectory() as tmp:
+        y_true = np.array([0, 1, 1, 0, 0])
+        y_pred = np.array([0, 1, 1, 0])
+        with pytest.raises(
+            IndexError, match="The number of predicted values is not the same"
+        ):
+            write_results_to_uea_format(
+                "HC", "Testy", y_pred=y_pred, y_true=y_true, output_path=tmp
+            )
+        y_true = np.array([0, 1, 1, 0])
+        write_results_to_uea_format(
+            "HC",
+            "Testy",
+            y_pred=y_pred,
+            output_path=tmp,
+            full_path=False,
+            split="TEST",
+            timing_type="seconds",
+            first_line_comment="Hello",
+        )
+
+        probs = [[0.0, 1.0], [0.0, 1.0], [0.0, 1.0], [0.0, 1.0]]
+        write_results_to_uea_format(
+            "HC",
+            "Testy2",
+            y_pred=y_pred,
+            y_true=y_true,
+            output_path=tmp,
+            full_path=False,
+            split="TEST",
+            timing_type="seconds",
+            first_line_comment="Hello",
+            predicted_probs=probs,
+        )
+
+
+def test__write_header():
+    with tempfile.TemporaryDirectory() as tmp:
+        problem_name = "header.csv"
+        with pytest.raises(
+            ValueError, match="Cannot have class_labels true for a regression problem"
+        ):
+            _write_header(tmp, problem_name, class_labels=True, regression=True)
+    _write_header(
+        tmp,
+        problem_name,
+        suffix="_TRAIN",
+        extension=".csv",
+        comment="Hello",
+        regression=True,
+    )
diff --git a/aeon/datasets/tests/test_single_problem_loaders.py b/aeon/datasets/tests/test_single_problem_loaders.py
index b5649b5b..7b40d661 100644
--- a/aeon/datasets/tests/test_single_problem_loaders.py
+++ b/aeon/datasets/tests/test_single_problem_loaders.py
@@ -1,4 +1,6 @@
 """Test single problem loaders with varying return types."""
+import os
+
 import numpy as np
 import pandas as pd
 import pytest
@@ -13,9 +15,11 @@ from aeon.datasets import (  # Univariate; Unequal length; Multivariate
     load_osuleaf,
     load_plaid,
     load_solar,
+    load_tsf_to_dataframe,
     load_unit_test,
     load_unit_test_tsf,
 )
+from aeon.datasets._single_problem_loaders import MODULE
 
 UNIVARIATE_PROBLEMS = [
     load_acsf1,
@@ -77,6 +81,18 @@ def test_load_unit_test_tsf():
     assert not tuple[4]
 
 
+def test_basic_load_tsf_to_dataframe():
+    """Simple loader test."""
+
+    full_path = os.path.join(MODULE, "data", "UnitTest", "UnitTest_Tsf_Loader.tsf")
+    df, metadata = load_tsf_to_dataframe(full_path)
+    assert isinstance(df, pd.DataFrame)
+    assert metadata["frequency"] == "yearly"
+    assert metadata["forecast_horizon"] == 4
+    assert metadata["contain_missing_values"] is False
+    assert metadata["contain_equal_length"] is False
+
+
 def test_load_solar():
     solar = load_solar(api_version=None)
     assert type(solar) == pd.Series