Skip to content

Commit 976b382

Browse files
committed
Merge branch 'main' into ajb/kill_with_fire
# Conflicts: # aeon/benchmarking/forecasting.py # aeon/datatypes/_check.py # aeon/forecasting/compose/_bagging.py # aeon/forecasting/model_evaluation/_functions.py # aeon/forecasting/model_selection/_split.py # aeon/forecasting/statsforecast.py # aeon/transformations/_legacy/_mbb.py
2 parents 65b1b31 + edff6a6 commit 976b382

File tree

138 files changed

+6349
-1270
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

138 files changed

+6349
-1270
lines changed

.all-contributorsrc

+9
Original file line numberDiff line numberDiff line change
@@ -2511,6 +2511,15 @@
25112511
"contributions": [
25122512
"code"
25132513
]
2514+
},
2515+
{
2516+
"login": "wenig",
2517+
"name": "Phillip Wenig",
2518+
"avatar_url": "https://avatars.githubusercontent.com/u/6967289?v=4",
2519+
"profile": "https://github.com/wenig",
2520+
"contributions": [
2521+
"code"
2522+
]
25142523
}
25152524
],
25162525
"commitType": "docs"

.github/workflows/pr_precommit.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
python-version: "3.10"
3838

3939
- name: Get changed files
40-
uses: tj-actions/changed-files@v44
40+
uses: tj-actions/changed-files@v45
4141
id: changed-files
4242

4343
- name: List changed files

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ repos:
3838
rev: v3.17.0
3939
hooks:
4040
- id: pyupgrade
41-
args: [ "--py38-plus" ]
41+
args: [ "--py39-plus" ]
4242

4343
- repo: https://github.com/pycqa/isort
4444
rev: 5.13.2

CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ aeon/forecasting/ @aiwalter @guzalbulatova @ltsaprounis
2020
aeon/networks/ @hadifawaz1999
2121

2222
aeon/performance_metrics/forecasting/ @aiwalter
23+
aeon/performance_metrics/anomaly_detection/ @codelionx @MatthewMiddlehurst
2324

2425
aeon/pipeline/ @aiwalter
2526

CONTRIBUTORS.md

+13-12
Large diffs are not rendered by default.

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ We strive to provide a broad library of time series algorithms including the
1313
latest advances, offer efficient implementations using numba, and interfaces with other
1414
time series packages to provide a single framework for algorithm comparison.
1515

16-
The latest `aeon` release is `v0.11.0`. You can view the full changelog
16+
The latest `aeon` release is `v0.11.1`. You can view the full changelog
1717
[here](https://www.aeon-toolkit.org/en/stable/changelog.html).
1818

1919
Our webpage and documentation is available at https://aeon-toolkit.org.

aeon/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""aeon toolkit."""
22

3-
__version__ = "0.11.0"
3+
__version__ = "0.11.1"
44

55
__all__ = ["show_versions"]
66

aeon/anomaly_detection/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
"MERLIN",
77
"STRAY",
88
"PyODAdapter",
9+
"STOMP",
910
]
1011

1112
from aeon.anomaly_detection._dwt_mlead import DWT_MLEAD
1213
from aeon.anomaly_detection._kmeans import KMeansAD
1314
from aeon.anomaly_detection._merlin import MERLIN
1415
from aeon.anomaly_detection._pyodadapter import PyODAdapter
16+
from aeon.anomaly_detection._stomp import STOMP
1517
from aeon.anomaly_detection._stray import STRAY

aeon/anomaly_detection/_dwt_mlead.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
__all__ = ["DWT_MLEAD"]
55

66
import warnings
7-
from typing import Any, Iterable, List, Tuple
7+
from collections.abc import Iterable
8+
from typing import Any
89

910
import numpy as np
1011
from numpy.lib.stride_tricks import sliding_window_view
@@ -14,15 +15,15 @@
1415
from aeon.utils.numba.wavelets import multilevel_haar_transform
1516

1617

17-
def _pad_series(x: np.ndarray) -> Tuple[np.ndarray, int, int]:
18+
def _pad_series(x: np.ndarray) -> tuple[np.ndarray, int, int]:
1819
"""Pad input signal to the next power of 2 using periodic padding mode."""
1920
n = x.shape[0]
2021
exp = np.ceil(np.log2(n))
2122
m = int(np.power(2, exp))
2223
return np.pad(x, (0, m - n), mode="wrap"), n, m
2324

2425

25-
def _combine_alternating(xs: List[Any], ys: List[Any]) -> Iterable[Any]:
26+
def _combine_alternating(xs: list[Any], ys: list[Any]) -> Iterable[Any]:
2627
"""Combine two lists by alternating their elements."""
2728
for x, y in zip(xs, ys):
2829
yield x
@@ -173,7 +174,7 @@ def _predict(self, X) -> np.ndarray:
173174

174175
def _multilevel_dwt(
175176
self, X: np.ndarray, max_level: int
176-
) -> Tuple[np.ndarray, List[np.ndarray], List[np.ndarray]]:
177+
) -> tuple[np.ndarray, list[np.ndarray], list[np.ndarray]]:
177178
ls_ = np.arange(self.start_level - 1, max_level - 1, dtype=np.int_) + 1
178179
as_, ds_ = multilevel_haar_transform(X, max_level - 1)
179180
as_ = as_[self.start_level :]
@@ -220,7 +221,7 @@ def _reverse_windowing(
220221
return np.sum(mapped, axis=1)
221222

222223
def _push_anomaly_counts_down_to_points(
223-
self, coef_anomaly_counts: List[np.ndarray]
224+
self, coef_anomaly_counts: list[np.ndarray]
224225
) -> np.ndarray:
225226
# sum up counters of detail coeffs (orig. D^l) and approx coeffs (orig. C^l)
226227
anomaly_counts = coef_anomaly_counts[0::2] + coef_anomaly_counts[1::2]

aeon/anomaly_detection/_stomp.py

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""Implements an adapter for PyOD models to be used in the Aeon framework."""
2+
3+
from __future__ import annotations
4+
5+
__maintainer__ = ["CodeLionX"]
6+
__all__ = ["STOMP"]
7+
8+
import numpy as np
9+
10+
from aeon.anomaly_detection.base import BaseAnomalyDetector
11+
from aeon.utils.validation._dependencies import _check_soft_dependencies
12+
from aeon.utils.windowing import reverse_windowing
13+
14+
15+
class STOMP(BaseAnomalyDetector):
16+
"""STOMP anomaly detector.
17+
18+
STOMP calculates the matrix profile of a time series which is the distance to the
19+
nearest neighbor of each subsequence in the time series. The matrix profile is then
20+
used to calculate the anomaly score for each time point. The larger the distance to
21+
the nearest neighbor, the more anomalous the time point is.
22+
23+
STOMP supports univariate time series only.
24+
25+
.. list-table:: Capabilities
26+
:stub-columns: 1
27+
28+
* - Input data format
29+
- univariate
30+
* - Output data format
31+
- anomaly scores
32+
* - Learning Type
33+
- unsupervised
34+
35+
36+
Parameters
37+
----------
38+
window_size : int, default=10
39+
Size of the sliding window.
40+
ignore_trivial : bool, default=True
41+
Whether to ignore trivial matches in the matrix profile.
42+
normalize : bool, default=True
43+
Whether to normalize the windows before computing the distance.
44+
p : float, default=2.0
45+
The p-norm to use for the distance calculation.
46+
k : int, default=1
47+
The number of top distances to return.
48+
49+
Examples
50+
--------
51+
>>> import numpy as np
52+
>>> from aeon.anomaly_detection import STOMP # doctest: +SKIP
53+
>>> X = np.random.default_rng(42).random((10, 2), dtype=np.float_)
54+
>>> detector = STOMP(X, window_size=2) # doctest: +SKIP
55+
>>> detector.fit_predict(X, axis=0) # doctest: +SKIP
56+
array([1.02352234 1.00193038 0.98584441 0.99630753 1.00656619 1.00682081 1.00781515
57+
0.99709741 0.98878895 0.99723947])
58+
59+
References
60+
----------
61+
.. [1] Zhu, Yan and Zimmerman, Zachary and Senobari, Nader Shakibay and Yeh,
62+
Chin-Chia Michael and Funning, Gareth and Mueen, Abdullah and Brisk,
63+
Philip and Keogh, Eamonn. "Matrix Profile II: Exploiting a Novel
64+
Algorithm and GPUs to Break the One Hundred Million Barrier for Time
65+
Series Motifs and Joins." In Proceedings of the 16th International
66+
Conference on Data Mining (ICDM), 2016.
67+
"""
68+
69+
_tags = {
70+
"capability:univariate": True,
71+
"capability:multivariate": False,
72+
"capability:missing_values": False,
73+
"fit_is_empty": True,
74+
"python_dependencies": ["stumpy"],
75+
}
76+
77+
def __init__(
78+
self,
79+
window_size: int = 10,
80+
ignore_trivial: bool = True,
81+
normalize: bool = True,
82+
p: float = 2.0,
83+
k: int = 1,
84+
):
85+
self.mp: np.ndarray | None = None
86+
self.window_size = window_size
87+
self.ignore_trivial = ignore_trivial
88+
self.normalize = normalize
89+
self.p = p
90+
self.k = k
91+
92+
super().__init__(axis=0)
93+
94+
def _predict(self, X: np.ndarray) -> np.ndarray:
95+
_check_soft_dependencies("stumpy", severity="error")
96+
import stumpy
97+
98+
self._check_params(X)
99+
self.mp = stumpy.stump(
100+
X[:, 0],
101+
m=self.window_size,
102+
ignore_trivial=self.ignore_trivial,
103+
normalize=self.normalize,
104+
p=self.p,
105+
k=self.k,
106+
)
107+
point_anomaly_scores = reverse_windowing(self.mp[:, 0], self.window_size)
108+
return point_anomaly_scores
109+
110+
def _check_params(self, X: np.ndarray) -> None:
111+
if self.window_size < 1 or self.window_size > X.shape[0]:
112+
raise ValueError(
113+
"The window size must be at least 1 and at most the length of the "
114+
"time series."
115+
)
116+
117+
if self.k < 1 or self.k > X.shape[0] - self.window_size:
118+
raise ValueError(
119+
"The top `k` distances must be at least 1 and at most the length of "
120+
"the time series minus the window size."
121+
)
122+
123+
@classmethod
124+
def get_test_params(cls, parameter_set="default"):
125+
"""Return testing parameter settings for the estimator.
126+
127+
Parameters
128+
----------
129+
parameter_set : str, default="default"
130+
Name of the set of test parameters to return, for use in tests. If no
131+
special parameters are defined for a value, will return `"default"` set.
132+
133+
Returns
134+
-------
135+
params : dict or list of dict, default={}
136+
Parameters to create testing instances of the class.
137+
Each dict are parameters to construct an "interesting" test instance, i.e.,
138+
`MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
139+
`create_test_instance` uses the first (or only) dictionary in `params`.
140+
"""
141+
_check_soft_dependencies(*cls._tags["python_dependencies"])
142+
143+
return {
144+
"window_size": 10,
145+
"ignore_trivial": True,
146+
"normalize": True,
147+
"p": 2.0,
148+
"k": 1,
149+
}

aeon/anomaly_detection/_stray.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
__maintainer__ = ["MatthewMiddlehurst"]
44
__all__ = ["STRAY"]
55

6-
from typing import Dict
76

87
import numpy as np
98
import numpy.typing as npt
@@ -119,7 +118,7 @@ def _predict(self, X, y=None) -> npt.ArrayLike:
119118

120119
return outlier_bool.astype(bool)
121120

122-
def _find_outliers_kNN(self, X: np.ndarray, n: int) -> Dict:
121+
def _find_outliers_kNN(self, X: np.ndarray, n: int) -> dict:
123122
"""Find outliers using kNN distance with maximum gap.
124123
125124
Parameters
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Tests for the PyODAdapter class."""
2+
3+
__maintainer__ = ["CodeLionX"]
4+
5+
import numpy as np
6+
import pytest
7+
8+
from aeon.anomaly_detection import STOMP
9+
from aeon.testing.data_generation._legacy import make_series
10+
from aeon.utils.validation._dependencies import _check_soft_dependencies
11+
12+
13+
@pytest.mark.skipif(
14+
not _check_soft_dependencies("stumpy", severity="none"),
15+
reason="required soft dependency stumpy not available",
16+
)
17+
def test_STOMP_default():
18+
"""Test STOMP."""
19+
series = make_series(n_timepoints=80, return_numpy=True, random_state=0)
20+
series[50:58] -= 2
21+
22+
ad = STOMP(window_size=10)
23+
pred = ad.fit_predict(series, axis=0)
24+
25+
assert pred.shape == (80,)
26+
assert pred.dtype == np.float_
27+
assert 40 <= np.argmax(pred) <= 60

aeon/benchmarking/results_loaders.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@
112112
"TS-CHIEF": {"TSCHIEF", "TS_CHIEF"},
113113
"TSF": {"tsf", "TimeSeriesForest", "TimeSeriesForestClassifier"},
114114
"TSFresh": {"tsfresh", "TSFreshClassifier"},
115-
"WEASEL-1.0": {"WEASEL", "WEASEL2", "weasel", "WEASEL 1.0"},
115+
"WEASEL-1.0": {"WEASEL", "WEASEL1", "weasel", "WEASEL 1.0"},
116116
"WEASEL-2.0": {"WEASEL-D", "WEASEL-Dilation", "WEASEL2", "weasel 2.0", "WEASEL_V2"},
117117
"1NN-DTW": {
118118
"1NNDTW",

aeon/classification/dictionary_based/_mrsqm.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
__maintainer__ = ["TonyBagnall"]
44
__all__ = ["MrSQMClassifier"]
55

6-
from typing import List, Union
6+
from typing import Union
77

88
import numpy as np
99
import pandas as pd
@@ -149,7 +149,7 @@ def _predict_proba(self, X) -> np.ndarray:
149149
return self.clf_.predict_proba(_X)
150150

151151
@classmethod
152-
def get_test_params(cls, parameter_set: str = "default") -> Union[dict, List[dict]]:
152+
def get_test_params(cls, parameter_set: str = "default") -> Union[dict, list[dict]]:
153153
"""Return testing parameter settings for the estimator.
154154
155155
Parameters

aeon/classification/distance_based/_elastic_ensemble.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import math
1010
import time
1111
from itertools import product
12-
from typing import List, Union
12+
from typing import Union
1313

1414
import numpy as np
1515
from sklearn.metrics import accuracy_score
@@ -101,7 +101,7 @@ class ElasticEnsemble(BaseClassifier):
101101

102102
def __init__(
103103
self,
104-
distance_measures: Union[str, List[str]] = "all",
104+
distance_measures: Union[str, list[str]] = "all",
105105
proportion_of_param_options: float = 1.0,
106106
proportion_train_in_param_finding: float = 1.0,
107107
proportion_train_for_test: float = 1.0,
@@ -491,7 +491,7 @@ def get_inclusive(min_val: float, max_val: float, num_vals: float):
491491
)
492492

493493
@classmethod
494-
def get_test_params(cls, parameter_set: str = "default") -> Union[dict, List[dict]]:
494+
def get_test_params(cls, parameter_set: str = "default") -> Union[dict, list[dict]]:
495495
"""Return testing parameter settings for the estimator.
496496
497497
Parameters

aeon/classification/distance_based/_proximity_forest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
__all__ = ["ProximityForest"]
77

8-
from typing import Type, Union
8+
from typing import Union
99

1010
import numpy as np
1111
from joblib import Parallel, delayed
@@ -89,7 +89,7 @@ def __init__(
8989
n_splitters: int = 5,
9090
max_depth: int = None,
9191
min_samples_split: int = 2,
92-
random_state: Union[int, Type[np.random.RandomState], None] = None,
92+
random_state: Union[int, np.random.RandomState, None] = None,
9393
n_jobs: int = 1,
9494
parallel_backend=None,
9595
):

0 commit comments

Comments
 (0)