diff --git a/docs/additional_information/changelog.rst b/docs/additional_information/changelog.rst index dfaf57c..e26e8fa 100644 --- a/docs/additional_information/changelog.rst +++ b/docs/additional_information/changelog.rst @@ -24,6 +24,10 @@ Added or labels - Added the property ``__version__`` to ``dtaianomaly``, which can be accessed from code. - Included the used version of ``dtaianomaly`` when logging errors. +- Implemented ``PrincipalComponentAnalysis``, ``KernelPrincipalComponentAnalysis`` and + ``RobustPrincipalComponentAnalysis`` anomaly detectors. +- Implemented ``HistogramBasedOutlierScore`` anomaly detector. +- Implemented ``OneClassSupportVectorMachine`` anomaly detector. Changed ^^^^^^^ diff --git a/docs/api/anomaly_detection_algorithms/ocsvm.rst b/docs/api/anomaly_detection_algorithms/ocsvm.rst new file mode 100644 index 0000000..f3c8926 --- /dev/null +++ b/docs/api/anomaly_detection_algorithms/ocsvm.rst @@ -0,0 +1,6 @@ +One Class Support Vector Machine +================================ + +.. autoclass:: dtaianomaly.anomaly_detection.OneClassSupportVectorMachine + :inherited-members: + :members: diff --git a/dtaianomaly/anomaly_detection/OneClassSupportVectorMachine.py b/dtaianomaly/anomaly_detection/OneClassSupportVectorMachine.py new file mode 100644 index 0000000..5386eb2 --- /dev/null +++ b/dtaianomaly/anomaly_detection/OneClassSupportVectorMachine.py @@ -0,0 +1,59 @@ + +from pyod.models.ocsvm import OCSVM +from dtaianomaly.anomaly_detection.BaseDetector import Supervision +from dtaianomaly.anomaly_detection.PyODAnomalyDetector import PyODAnomalyDetector + + +class OneClassSupportVectorMachine(PyODAnomalyDetector): + """ + Anomaly detector based on One-Class Support Vector Machines (OC-SVM). + + The OC-SVM [Scholkopf1999support]_ uses a Support Vector Machine to learn + a boundary around the normal behavior with minimal margin. New data can + then be identified as anomaly or not, depending on if the data falls within + this boundary (and thus is normal) or outside the boundary (and thus is + anomalous). + + Notes + ----- + The OC-SVM inherets from :py:class:`~dtaianomaly.anomaly_detection.PyodAnomalyDetector`. + + Parameters + ---------- + window_size: int or str + The window size to use for extracting sliding windows from the time series. This + value will be passed to :py:meth:`~dtaianomaly.anomaly_detection.compute_window_size`. + stride: int, default=1 + The stride, i.e., the step size for extracting sliding windows from the time series. + **kwargs: + Arguments to be passed to the PyOD OC-SVM + + Attributes + ---------- + window_size_: int + The effectively used window size for this anomaly detector + pyod_detector_ : OCSVM + A OCSVM-detector of PyOD + + Examples + -------- + >>> from dtaianomaly.anomaly_detection import OneClassSupportVectorMachine + >>> from dtaianomaly.data import demonstration_time_series + >>> x, y = demonstration_time_series() + >>> ocsvm = OneClassSupportVectorMachine(10).fit(x) + >>> ocsvm.decision_function(x) + array([-0.7442125 , -1.57019847, -1.86868112, ..., 13.33883568, + 12.6492399 , 11.8761641 ]) + + References + ---------- + .. [Scholkopf1999support] Schölkopf, Bernhard, et al. "Support vector method + for novelty detection." Advances in neural information processing systems 12 + (1999). + """ + + def _initialize_detector(self, **kwargs) -> OCSVM: + return OCSVM(**kwargs) + + def _supervision(self): + return Supervision.SEMI_SUPERVISED diff --git a/dtaianomaly/anomaly_detection/__init__.py b/dtaianomaly/anomaly_detection/__init__.py index 6a4462a..a89d7cf 100644 --- a/dtaianomaly/anomaly_detection/__init__.py +++ b/dtaianomaly/anomaly_detection/__init__.py @@ -20,6 +20,7 @@ from .LocalOutlierFactor import LocalOutlierFactor from .MatrixProfileDetector import MatrixProfileDetector from .MedianMethod import MedianMethod +from .OneClassSupportVectorMachine import OneClassSupportVectorMachine from .PrincipalComponentAnalysis import PrincipalComponentAnalysis from .RobustPrincipalComponentAnalysis import RobustPrincipalComponentAnalysis @@ -48,6 +49,7 @@ 'LocalOutlierFactor', 'MatrixProfileDetector', 'MedianMethod', + 'OneClassSupportVectorMachine', 'PrincipalComponentAnalysis', 'PyODAnomalyDetector', 'RobustPrincipalComponentAnalysis' diff --git a/dtaianomaly/workflow/workflow_from_config.py b/dtaianomaly/workflow/workflow_from_config.py index 943700c..f32ce08 100644 --- a/dtaianomaly/workflow/workflow_from_config.py +++ b/dtaianomaly/workflow/workflow_from_config.py @@ -299,6 +299,9 @@ def detector_entry(entry): elif detector_type == 'RobustPrincipalComponentAnalysis': return anomaly_detection.RobustPrincipalComponentAnalysis(**entry_without_type) + elif detector_type == 'OneClassSupportVectorMachine': + return anomaly_detection.OneClassSupportVectorMachine(**entry_without_type) + else: raise ValueError(f'Invalid detector entry: {entry}') diff --git a/tests/anomaly_detection/test_OneClassSupportVectorMachine.py b/tests/anomaly_detection/test_OneClassSupportVectorMachine.py new file mode 100644 index 0000000..a1d1182 --- /dev/null +++ b/tests/anomaly_detection/test_OneClassSupportVectorMachine.py @@ -0,0 +1,15 @@ + +from dtaianomaly.anomaly_detection import OneClassSupportVectorMachine, Supervision + + +class TestOneClassSupportVectorMachine: + + def test_supervision(self): + detector = OneClassSupportVectorMachine(1) + assert detector.supervision == Supervision.SEMI_SUPERVISED + + def test_str(self): + assert str(OneClassSupportVectorMachine(5)) == "OneClassSupportVectorMachine(window_size=5)" + assert str(OneClassSupportVectorMachine('fft')) == "OneClassSupportVectorMachine(window_size='fft')" + assert str(OneClassSupportVectorMachine(15, 3)) == "OneClassSupportVectorMachine(window_size=15,stride=3)" + assert str(OneClassSupportVectorMachine(25, kernel='poly')) == "OneClassSupportVectorMachine(window_size=25,kernel='poly')" diff --git a/tests/anomaly_detection/test_PyODAnomalyDetector.py b/tests/anomaly_detection/test_PyODAnomalyDetector.py index bb1ac3e..1af7068 100644 --- a/tests/anomaly_detection/test_PyODAnomalyDetector.py +++ b/tests/anomaly_detection/test_PyODAnomalyDetector.py @@ -6,8 +6,11 @@ @pytest.mark.parametrize('detector_class,kwargs', [ (anomaly_detection.HistogramBasedOutlierScore, {'n_bins': 'auto', 'alpha': 0.5}), (anomaly_detection.IsolationForest, {'n_estimators': 42, 'max_samples': 'auto'}), + (anomaly_detection.KernelPrincipalComponentAnalysis, {'kernel': 'poly', 'n_components': 0.5}), (anomaly_detection.KNearestNeighbors, {'n_neighbors': 42, 'metric': 'euclidean'}), (anomaly_detection.LocalOutlierFactor, {'n_neighbors': 3}), + (anomaly_detection.OneClassSupportVectorMachine, {'kernel': 'poly'}), + (anomaly_detection.PrincipalComponentAnalysis, {'n_components': 0.5}), ]) class TestPyodAnomalyDetectorAdditionalArgs: @@ -20,8 +23,11 @@ def test(self, detector_class, kwargs): @pytest.mark.parametrize('detector_class', [ anomaly_detection.HistogramBasedOutlierScore, anomaly_detection.IsolationForest, + anomaly_detection.KernelPrincipalComponentAnalysis, anomaly_detection.KNearestNeighbors, anomaly_detection.LocalOutlierFactor, + anomaly_detection.OneClassSupportVectorMachine, + anomaly_detection.PrincipalComponentAnalysis, ]) class TestPyodAnomalyDetector: diff --git a/tests/anomaly_detection/test_detectors.py b/tests/anomaly_detection/test_detectors.py index c1df758..71b9cc3 100644 --- a/tests/anomaly_detection/test_detectors.py +++ b/tests/anomaly_detection/test_detectors.py @@ -31,6 +31,7 @@ anomaly_detection.MatrixProfileDetector(15, novelty=True), anomaly_detection.MedianMethod(15), anomaly_detection.MedianMethod(15, 10), + anomaly_detection.OneClassSupportVectorMachine(15), anomaly_detection.PrincipalComponentAnalysis(15), anomaly_detection.RobustPrincipalComponentAnalysis(15), anomaly_detection.RobustPrincipalComponentAnalysis(15, svd_solver='randomized'), @@ -117,6 +118,7 @@ def test_fit_predict_on_different_time_series(self, detector, univariate_time_se (anomaly_detection.KNearestNeighbors, {}), (anomaly_detection.LocalOutlierFactor, {}), (anomaly_detection.MatrixProfileDetector, {}), + (anomaly_detection.OneClassSupportVectorMachine, {}), (anomaly_detection.PrincipalComponentAnalysis, {}), (anomaly_detection.RobustPrincipalComponentAnalysis, {}), ]) diff --git a/tests/workflow/test_workflow_from_config.py b/tests/workflow/test_workflow_from_config.py index b500459..bbca89d 100644 --- a/tests/workflow/test_workflow_from_config.py +++ b/tests/workflow/test_workflow_from_config.py @@ -354,6 +354,8 @@ def test_chained_preprocessor(self): (detector_entry, anomaly_detection.MatrixProfileDetector, {'window_size': 25, 'normalize': True, 'p': 1.5, 'k': 5}), (detector_entry, anomaly_detection.MedianMethod, {'neighborhood_size_before': 15}), (detector_entry, anomaly_detection.MedianMethod, {'neighborhood_size_before': 25, 'neighborhood_size_after': 5}), + (detector_entry, anomaly_detection.OneClassSupportVectorMachine, {'window_size': 15}), + (detector_entry, anomaly_detection.OneClassSupportVectorMachine, {'window_size': 15, 'kernel': 'poly'}), (detector_entry, anomaly_detection.PrincipalComponentAnalysis, {'window_size': 15}), (detector_entry, anomaly_detection.PrincipalComponentAnalysis, {'window_size': 15, 'n_components': 0.5}), (detector_entry, anomaly_detection.KernelPrincipalComponentAnalysis, {'window_size': 15}), @@ -429,6 +431,7 @@ def test_no_type(self, entry_function, object_type, entry): (detector_entry, anomaly_detection.LocalOutlierFactor), (detector_entry, anomaly_detection.MatrixProfileDetector), (detector_entry, anomaly_detection.MedianMethod), + (detector_entry, anomaly_detection.OneClassSupportVectorMachine), (detector_entry, anomaly_detection.PrincipalComponentAnalysis), (detector_entry, anomaly_detection.RobustPrincipalComponentAnalysis), # Preprocessors