Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions darts/ad/aggregators/aggregators.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
# - create show_all_combined (info about correlation, and from what path did
# the anomaly alarm came from)

import datetime
import os
import pickle
import sys
from typing import Literal

Expand Down Expand Up @@ -43,6 +46,65 @@ class Aggregator(ABC):
def __init__(self):
self.width_trained_on: int | None = None

def save(
self,
path: str | os.PathLike | None = None,
**pkl_kwargs,
) -> None:
"""
Saves the aggregator under a given path.

Parameters
----------
path
Path under which to save the aggregator at its current state. If no path is specified, the aggregator
is automatically saved under ``"{ClassName}_{YYYY-mm-dd_HH_MM_SS}.pkl"``.
pkl_kwargs
Keyword arguments passed to `pickle.dump()`
"""
if path is None:
path = f"{type(self).__name__}_{datetime.datetime.now().strftime('%Y-%m-%d_%H_%M_%S')}.pkl"
if isinstance(path, str | os.PathLike):
with open(path, "wb") as handle:
pickle.dump(obj=self, file=handle, **pkl_kwargs)
else:
raise_log(
ValueError(
"Argument 'path' has to be a filepath (str or PathLike), "
f"but was '{path.__class__}'."
),
logger=logger,
)

@staticmethod
def load(path: str | os.PathLike) -> "Aggregator":
"""
Loads an aggregator from a given path.

Parameters
----------
path
Path from which to load the aggregator.
"""
if isinstance(path, str | os.PathLike):
if not os.path.exists(path):
raise_log(
FileNotFoundError(f"The file {path} doesn't exist"),
logger=logger,
)
with open(path, "rb") as handle:
aggregator = pickle.load(file=handle)
else:
raise_log(
ValueError(
"Argument 'path' has to be a filepath (str or PathLike), "
f"but was '{path.__class__}'."
),
logger=logger,
)
return aggregator


@abstractmethod
def __str__(self):
"""returns the name of the aggregator"""
Expand Down
62 changes: 62 additions & 0 deletions darts/ad/anomaly_model/anomaly_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
------------------
"""

import datetime
import os
import pickle
import sys
from abc import ABC, abstractmethod
from collections.abc import Sequence
Expand Down Expand Up @@ -41,6 +44,65 @@ def __init__(self, model, scorer):
)
self.model = model

def save(
self,
path: str | os.PathLike | None = None,
**pkl_kwargs,
) -> None:
"""
Saves the anomaly model under a given path.

Parameters
----------
path
Path under which to save the anomaly model at its current state. If no path is specified, it
is automatically saved under ``"{ClassName}_{YYYY-mm-dd_HH_MM_SS}.pkl"``.
pkl_kwargs
Keyword arguments passed to `pickle.dump()`
"""
if path is None:
path = f"{type(self).__name__}_{datetime.datetime.now().strftime('%Y-%m-%d_%H_%M_%S')}.pkl"
if isinstance(path, str | os.PathLike):
with open(path, "wb") as handle:
pickle.dump(obj=self, file=handle, **pkl_kwargs)
else:
raise_log(
ValueError(
"Argument 'path' has to be a filepath (str or PathLike), "
f"but was '{path.__class__}'."
),
logger=logger,
)

@staticmethod
def load(path: str | os.PathLike) -> "AnomalyModel":
"""
Loads an anomaly model from a given path.

Parameters
----------
path
Path from which to load the anomaly model.
"""
if isinstance(path, str | os.PathLike):
if not os.path.exists(path):
raise_log(
FileNotFoundError(f"The file {path} doesn't exist"),
logger=logger,
)
with open(path, "rb") as handle:
model = pickle.load(file=handle)
else:
raise_log(
ValueError(
"Argument 'path' has to be a filepath (str or PathLike), "
f"but was '{path.__class__}'."
),
logger=logger,
)
return model


def fit(
self,
series: TimeSeriesLike,
Expand Down
62 changes: 62 additions & 0 deletions darts/ad/detectors/detectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
# - add more complex detectors
# - create an ensemble fittable detector

import datetime
import os
import pickle
import sys
from abc import ABC, abstractmethod
from collections.abc import Sequence
Expand Down Expand Up @@ -40,6 +43,65 @@ class Detector(ABC):
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.width_trained_on: int | None = None

def save(
self,
path: str | os.PathLike | None = None,
**pkl_kwargs,
) -> None:
"""
Saves the detector under a given path.

Parameters
----------
path
Path under which to save the detector at its current state. If no path is specified, the detector
is automatically saved under ``"{ClassName}_{YYYY-mm-dd_HH_MM_SS}.pkl"``.
pkl_kwargs
Keyword arguments passed to `pickle.dump()`
"""
if path is None:
path = f"{type(self).__name__}_{datetime.datetime.now().strftime('%Y-%m-%d_%H_%M_%S')}.pkl"
if isinstance(path, str | os.PathLike):
with open(path, "wb") as handle:
pickle.dump(obj=self, file=handle, **pkl_kwargs)
else:
raise_log(
ValueError(
"Argument 'path' has to be a filepath (str or PathLike), "
f"but was '{path.__class__}'."
),
logger=logger,
)

@staticmethod
def load(path: str | os.PathLike) -> "Detector":
"""
Loads a detector from a given path.

Parameters
----------
path
Path from which to load the detector.
"""
if isinstance(path, str | os.PathLike):
if not os.path.exists(path):
raise_log(
FileNotFoundError(f"The file {path} doesn't exist"),
logger=logger,
)
with open(path, "rb") as handle:
detector = pickle.load(file=handle)
else:
raise_log(
ValueError(
"Argument 'path' has to be a filepath (str or PathLike), "
f"but was '{path.__class__}'."
),
logger=logger,
)
return detector


def detect(
self,
series: TimeSeriesLike,
Expand Down
77 changes: 77 additions & 0 deletions darts/ad/scorers/scorers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
# - add option to normalize the windows for kmeans? capture only the form and not the values.

import copy
import datetime
import io
import os
import pickle
import sys
from abc import ABC, abstractmethod
from collections.abc import Sequence
Expand Down Expand Up @@ -64,6 +68,79 @@ def __init__(self, is_univariate: bool, window: int) -> None:
self.window = window
self._is_univariate = is_univariate

def save(
self,
path: str | os.PathLike | None = None,
**pkl_kwargs,
) -> None:
"""
Saves the anomaly scorer under a given path or file handle.

Example for saving and loading a :class:`KMeansScorer`:

.. highlight:: python
.. code-block:: python

from darts.ad.scorers import KMeansScorer

scorer = KMeansScorer(window=10, k=8)
scorer.fit(series)
scorer.save("my_scorer.pkl")
scorer_loaded = KMeansScorer.load("my_scorer.pkl")
..

Parameters
----------
path
Path under which to save the scorer at its current state. If no path is specified, the scorer
is automatically saved under ``"{ClassName}_{YYYY-mm-dd_HH_MM_SS}.pkl"``.
E.g., ``"KMeansScorer_2024-01-01_12_00_00.pkl"``.
pkl_kwargs
Keyword arguments passed to `pickle.dump()`
"""
if path is None:
path = f"{type(self).__name__}_{datetime.datetime.now().strftime('%Y-%m-%d_%H_%M_%S')}.pkl"
if isinstance(path, str | os.PathLike):
with open(path, "wb") as handle:
pickle.dump(obj=self, file=handle, **pkl_kwargs)
else:
raise_log(
ValueError(
"Argument 'path' has to be a filepath (str or PathLike), "
f"but was '{path.__class__}'."
),
logger=logger,
)

@staticmethod
def load(path: str | os.PathLike) -> "AnomalyScorer":
"""
Loads an anomaly scorer from a given path.

Parameters
----------
path
Path from which to load the scorer.
"""
if isinstance(path, str | os.PathLike):
if not os.path.exists(path):
raise_log(
FileNotFoundError(f"The file {path} doesn't exist"),
logger=logger,
)
with open(path, "rb") as handle:
scorer = pickle.load(file=handle)
else:
raise_log(
ValueError(
"Argument 'path' has to be a filepath (str or PathLike), "
f"but was '{path.__class__}'."
),
logger=logger,
)
return scorer


def score_from_prediction(
self,
series: TimeSeriesLike,
Expand Down
22 changes: 22 additions & 0 deletions darts/tests/ad/test_aggregators.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
FittableAggregator,
OrAggregator,
)
from darts.ad.scorers import KMeansScorer
from darts.models import MovingAverageFilter

# element shape : (model_cls, model_kwargs, expected metrics)
Expand Down Expand Up @@ -631,3 +632,24 @@ def test_ensemble_aggregator_multiple_series(self):
self.mts_anomalies1,
self.mts_anomalies2,
] == input_series_copy

def test_save_load_aggregator(self, tmp_path):
"""Test save/load for aggregators."""
from darts.ad.aggregators import AndAggregator, OrAggregator
for agg in [AndAggregator(), OrAggregator()]:
path = tmp_path / f"{type(agg).__name__}.pkl"
agg.save(str(path))
loaded = type(agg).load(str(path))
assert type(loaded) == type(agg)

def test_save_load_fittable_aggregator(self, tmp_path):
"""Test save/load preserves fitted FittableAggregator."""
from darts.ad.aggregators import EnsembleSklearnAggregator, FittableAggregator
from sklearn.ensemble import GradientBoostingClassifier
agg = EnsembleSklearnAggregator(
model=GradientBoostingClassifier(),
)
path = tmp_path / "ensemble.pkl"
agg.save(str(path))
loaded = EnsembleSklearnAggregator.load(str(path))
assert type(loaded) == type(agg)
30 changes: 30 additions & 0 deletions darts/tests/ad/test_anomaly_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1491,3 +1491,33 @@ def test_immutabilty(self):

# Check that the original series is not modified
assert series == input_series_copy

def test_save_load_filtering_anomaly_model(self, tmp_path):
"""Test save/load for FilteringAnomalyModel."""
model = FilteringAnomalyModel(
model=MovingAverageFilter(window=10),
scorer=Norm(),
)
model.fit(self.train, allow_model_training=True)
path = tmp_path / "filtering_am.pkl"
model.save(str(path))
loaded = FilteringAnomalyModel.load(str(path))
scores_orig = model.score(self.test)
scores_loaded = loaded.score(self.test)
for s_orig, s_loaded in zip(scores_orig, scores_loaded):
assert s_orig == s_loaded

def test_save_load_forecasting_anomaly_model(self, tmp_path):
"""Test save/load for ForecastingAnomalyModel."""
model = ForecastingAnomalyModel(
model=SKLearnModel(lags=5),
scorer=Norm(),
)
model.fit(self.train, allow_model_training=True)
path = tmp_path / "forecasting_am.pkl"
model.save(str(path))
loaded = ForecastingAnomalyModel.load(str(path))
scores_orig = model.score(self.test)
scores_loaded = loaded.score(self.test)
for s_orig, s_loaded in zip(scores_orig, scores_loaded):
assert s_orig == s_loaded
Loading
Loading