"""
Base Aggregator
---------------
"""
# TODO:
# - add customize aggregators
# - add in trainable aggregators
# - log regression
# - decision tree
# - create show_all_combined (info about correlation, and from what path did
# the anomaly alarm came from)
import sys
from typing import Literal
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self
from abc import ABC, abstractmethod
from collections.abc import Sequence
import numpy as np
from darts import TimeSeries
from darts.ad.utils import (
_assert_fit_called,
_check_input,
eval_metric_from_binary_prediction,
series2seq,
)
from darts.logging import raise_log
from darts.typing import TimeSeriesLike
class Aggregator(ABC):
"""Base class for Aggregators."""
def __init__(self):
self.width_trained_on: int | None = None
@abstractmethod
def __str__(self):
"""returns the name of the aggregator"""
pass
@abstractmethod
def _predict_core(self, series: Sequence[TimeSeries]) -> Sequence[TimeSeries]:
"""Aggregates the sequence of multivariate binary series given as
input into a sequence of univariate binary series. assuming the input is
in the correct shape.
Parameters
----------
series
The sequence of multivariate binary series to aggregate
Returns
-------
TimeSeries
Sequence of aggregated results
"""
pass
def predict(
self,
series: TimeSeriesLike,
name: str = "series",
) -> TimeSeriesLike:
"""Aggregates the (sequence of) multivariate binary series given as
input into a (sequence of) univariate binary series.
Parameters
----------
series
The (sequence of) multivariate binary series to aggregate.
name
The name of `series`.
Returns
-------
TimeSeries
(Sequence of) aggregated results.
"""
called_with_single_series = isinstance(series, TimeSeries)
series = _check_input(
series,
name=name,
width_expected=self.width_trained_on,
check_deterministic=True,
check_binary=True,
check_multivariate=True,
)
pred = self._predict_core(series)
return pred[0] if called_with_single_series else pred
def eval_metric(
self,
anomalies: TimeSeriesLike,
series: TimeSeriesLike,
window: int = 1,
metric: Literal["recall", "precision", "f1", "accuracy"] = "recall",
) -> float | Sequence[float]:
"""Aggregates the (sequence of) multivariate series given as input into one (sequence of)
series and evaluates the results against the ground truth anomaly labels.
Parameters
----------
anomalies
The (sequence of) binary ground truth anomaly labels (1 if it is an anomaly and 0 if not).
series
The (sequence of) predicted multivariate binary series to aggregate.
window
(Sequence of) integer value indicating the number of past samples each point
represents in the (sequence of) series. The parameter will be used by the
function `_window_adjustment_anomalies()` in darts.ad.utils to transform
anomalies.
metric
The name of the metric function to use. Must be one of "recall", "precision", "f1", and "accuracy".
Default: "recall".
Returns
-------
float | Sequence[float]
(Sequence of) score for the (sequence of) series.
"""
pred_anomalies = self.predict(series)
return eval_metric_from_binary_prediction(
anomalies=anomalies,
pred_anomalies=pred_anomalies,
window=window,
metric=metric,
)
class FittableAggregator(Aggregator):
"""Base class for Aggregators that require training."""
def __init__(self):
super().__init__()
self._fit_called = False
@abstractmethod
def _fit_core(self, anomalies: Sequence[np.ndarray], series: Sequence[np.ndarray]):
"""Fits the aggregator, assuming the input is in the correct shape.
Parameters
----------
anomalies
The (sequence of) binary ground truth anomaly labels (1 if it is an anomaly and 0 if not).
series
The (sequence of) multivariate binary anomalies (predicted labels) to aggregate.
"""
pass
def fit(
self,
anomalies: TimeSeriesLike,
series: TimeSeriesLike,
) -> Self:
"""Fit the aggregators on the (sequence of) multivariate binary anomaly series.
If a list of series is given, they must have the same number of components.
Parameters
----------
anomalies
The (sequence of) binary ground truth anomaly labels (1 if it is an anomaly and 0 if not).
series
The (sequence of) multivariate binary series (predicted labels) to aggregate.
"""
pred_width = series2seq(series)[0].width
series = _check_input(
series,
name="series",
width_expected=pred_width,
check_deterministic=True,
check_binary=True,
check_multivariate=True,
)
self.width_trained_on = pred_width
anomalies = _check_input(
anomalies,
name="anomalies",
width_expected=1,
check_deterministic=True,
check_binary=True,
check_multivariate=False,
)
if len(anomalies) != len(series):
raise_log(
ValueError(
"`anomalies` and `series` must contain the same number of series."
),
)
anomalies_vals, series_vals = [], []
for anom, pred_anom in zip(anomalies, series):
anomalies_vals.append(anom.slice_intersect_values(pred_anom)[:, :, 0])
series_vals.append(pred_anom.slice_intersect_values(anom)[:, :, 0])
self._fit_core(anomalies_vals, series_vals)
self._fit_called = True
return self
def predict(
self,
series: TimeSeriesLike,
name: str = "series",
) -> TimeSeriesLike:
_assert_fit_called(self._fit_called, name="Aggregator")
return super().predict(series=series, name=name)