Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anomaly Detection (anomaly model, scorer, detector, aggregator) #1256

Merged
merged 151 commits into from
Dec 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
151 commits
Select commit Hold shift + click to select a range
85c7a7d
AD first Ver
Oct 4, 2022
5ac2fd5
AD first Version
Oct 4, 2022
5743d58
added ForecastingAnomalyModel/FilteringAnomalyModel, and scorers: Kme…
Oct 5, 2022
2450e9a
implemented GaussianMixtureScorer and allow multiple scorer inputs
Oct 7, 2022
900b95f
Added comments and possibility to input a list of scorers in AnomalyM…
Oct 10, 2022
8408c8f
Clean whitespace
Oct 10, 2022
e9880e1
Clean whitespace2
Oct 10, 2022
6b65a0e
Clean whitespace2
Oct 10, 2022
626da3d
Clean whitespace with VScode
Oct 10, 2022
887f933
Merge branch 'master' into feat/anomaly_detection_API
julien12234 Oct 14, 2022
babe8c7
Changed diff() position and added characteristic_length parameters
Oct 14, 2022
cd48097
renamed submodule
hrzn Oct 15, 2022
7d0b369
small changes
hrzn Oct 15, 2022
56eaf9d
small improvements
hrzn Oct 16, 2022
1c0d4f4
small changes
hrzn Oct 16, 2022
72799b0
Accepts all types UTS, MTS, list(UTS or MTS)
Oct 28, 2022
7f20166
move _diff() in child, so that scorers have all the same signature
Oct 28, 2022
7a37038
replaced L1, L2, and Abs_diff with Norm
Oct 31, 2022
e6a72da
add component_wise to WassersteinScorer
Oct 31, 2022
b117bd7
add component_wise to Kmeans
Oct 31, 2022
c63cef8
add component_wise to LOF
Oct 31, 2022
247001c
add component_wise to GaussianMixture
Oct 31, 2022
9782272
Accept num_samples for probabilistic models forecasting
Nov 3, 2022
729a1d9
Minor changes
Nov 4, 2022
d8fb10f
add comments, add likelihood
Nov 9, 2022
8661d6d
add laplace, + window parameter + parameter alllow_retrain
Nov 10, 2022
3b83b11
add cauchy and gamma likelihood
Nov 11, 2022
71f12dc
add utils.py, detectors, aggregators
julien12234 Nov 14, 2022
71e3a6e
removed show function for now
julien12234 Nov 14, 2022
e17a046
add show_anomalies() and show_anomalies_from_scores()
julien12234 Nov 16, 2022
c0bd73f
small changes
julien12234 Nov 16, 2022
a34c479
Merge branch 'master' of github.com:unit8co/darts into feat/anomaly_d…
hrzn Nov 20, 2022
c89c1c2
Merge branch 'master' into feat/anomaly_detection_API
hrzn Nov 21, 2022
fc29b78
Merge branch 'feat/anomaly_detection_API' of github.com:unit8co/darts…
hrzn Nov 21, 2022
ca551e3
Some docstring improvements to AnomalyModels
hrzn Nov 21, 2022
1f8c52a
corrected Kmeans, LFO and Gaussian Scorer + added input from PR
julien12234 Nov 22, 2022
fa0618e
test commit
julien12234 Nov 22, 2022
003febd
negative LFO and gaussian
julien12234 Nov 23, 2022
45757b4
Merge branch 'master' into feat/anomaly_detection_API
hrzn Nov 25, 2022
b63e65c
Merge branch 'master' into feat/anomaly_detection_API
julien12234 Nov 28, 2022
7683bae
pre pull
julien12234 Nov 28, 2022
323fbc1
from prediciton structure
julien12234 Nov 28, 2022
0765d49
improved show_anomalies, changed structure _from_prediction
julien12234 Nov 30, 2022
8ffd0c1
small mistake in eval_accuracy in utils.py
julien12234 Nov 30, 2022
c8bea7f
return type of eval_acc
julien12234 Dec 1, 2022
17f5d68
changed way eval_acc returns in anomaly_model
julien12234 Dec 1, 2022
fa46383
added test for agg, dect, and scorers. upgrade agg trainable
julien12234 Dec 2, 2022
92013d1
added parameter return_UTS, and added test for scorers and anomaly_model
julien12234 Dec 3, 2022
124a221
small mistake in anomaly_model
julien12234 Dec 3, 2022
441bf24
New structure in files
julien12234 Dec 6, 2022
c3a56f5
Added warnings
julien12234 Dec 7, 2022
9987ad6
small change in wasserstein
julien12234 Dec 8, 2022
0a8b3f7
Merge branch 'master' into feat/anomaly_detection_API
hrzn Dec 9, 2022
d294740
filtering_am and forecasting_am
julien12234 Dec 9, 2022
c3efb69
Small improvements
hrzn Dec 9, 2022
13a365d
Merge branch 'feat/anomaly_detection_API' of github.com:unit8co/darts…
hrzn Dec 9, 2022
b5ad2fa
Fix test names
hrzn Dec 9, 2022
7f1582f
add pyod to requirements
hrzn Dec 9, 2022
6d771b2
rename scorers
hrzn Dec 9, 2022
7c02b1d
scorers imports
hrzn Dec 9, 2022
5c67ce2
Changed handling of kwargs in AD models
hrzn Dec 9, 2022
6948b61
update tests
hrzn Dec 9, 2022
3f1b21e
return single TimeSeries from score() in some cases
hrzn Dec 10, 2022
ec6baf4
small naming improvements
hrzn Dec 10, 2022
5e6f65e
Some improvements to anomaly models
hrzn Dec 10, 2022
68d388d
Small improvements to scorers
hrzn Dec 11, 2022
11ee748
Some small improvements
hrzn Dec 11, 2022
0d3464b
Fix tests
hrzn Dec 11, 2022
40aa67f
Merge branch 'master' into feat/anomaly_detection_API
hrzn Dec 12, 2022
e7640d7
Norm scorer docstring
hrzn Dec 12, 2022
d6e79af
test toy example agg and detectors
julien12234 Dec 12, 2022
7f5c30b
small docstring improvements
hrzn Dec 12, 2022
759ec9a
Merge branch 'feat/anomaly_detection_API' of github.com:unit8co/darts…
hrzn Dec 12, 2022
b9e50fb
Add vectorization todos
hrzn Dec 12, 2022
0695719
test toy example scorers
julien12234 Dec 12, 2022
a76759a
test toy example scorers
julien12234 Dec 12, 2022
b0047c5
test toy example PyOD
julien12234 Dec 13, 2022
0b09b8f
test toy example NLL scorers
julien12234 Dec 13, 2022
3f1ddf6
test toy example poisson nll scorer
julien12234 Dec 13, 2022
fcd623e
test toy example univariate anomaly_models
julien12234 Dec 13, 2022
b0d405a
test toy example univariate covariates forecasting_anomaly_models
julien12234 Dec 13, 2022
af4a489
update threshold detector docstring
hrzn Dec 13, 2022
34ca833
Merge branch 'feat/anomaly_detection_API' of github.com:unit8co/darts…
hrzn Dec 13, 2022
6df9ed6
change way to output string messages
julien12234 Dec 13, 2022
e3f486c
first implementation of julien H's PR review
julien12234 Dec 13, 2022
c160234
first implementation of julien H's PR review 2
julien12234 Dec 13, 2022
f745a45
anomaly_model forecasting multivariate test
julien12234 Dec 14, 2022
457ad5b
anomaly_model multivariate, w=1,2, len()=2 test for NLL scorers
julien12234 Dec 14, 2022
b40c7c7
changed NLL scorers: call scipy.stats function
julien12234 Dec 14, 2022
6d9279f
changed in anomaly_models (inner to outer for series and scorers)
julien12234 Dec 14, 2022
7b80625
Small changes to PyOD detector
hrzn Dec 15, 2022
4164c57
Merge branch 'feat/anomaly_detection_API' of github.com:unit8co/darts…
hrzn Dec 15, 2022
d5cc56a
improvements to wasserstein scorer docstring
hrzn Dec 15, 2022
10c8d6b
change in eval acc
julien12234 Dec 15, 2022
5cdfc6d
change in eval acc, new function _eval_accuracy_from_scores
julien12234 Dec 15, 2022
cb59cd4
Small improvements to aggregators
hrzn Dec 15, 2022
10a79ab
Small docstrings improvements
hrzn Dec 15, 2022
a435f6a
Merge branch 'feat/anomaly_detection_API' of github.com:unit8co/darts…
hrzn Dec 15, 2022
7a0f5e7
Utils docstring
hrzn Dec 15, 2022
33aeea4
change in detectors (vectorization and accepts list of param if multi…
julien12234 Dec 15, 2022
afd7c61
remove exp in PyODScorer... and updated test
julien12234 Dec 15, 2022
080e0f9
new test with np.testing
julien12234 Dec 15, 2022
883d587
agg accept only MTS or sequence of MTS
julien12234 Dec 16, 2022
409f215
removed old detectors
julien12234 Dec 16, 2022
005003a
new multivariate test for filtering anomaly model
julien12234 Dec 16, 2022
fa7f271
small changes to utils docstrings
hrzn Dec 16, 2022
aef8127
Merge branch 'feat/anomaly_detection_API' of github.com:unit8co/darts…
hrzn Dec 16, 2022
a4b8e53
test assert_array_almost_equal decimal 2
julien12234 Dec 16, 2022
81554b0
test assert_array_almost_equal decimal 1
julien12234 Dec 16, 2022
0e70677
test assert_array_almost_equal decimal 1
julien12234 Dec 16, 2022
7a7b553
second implementation of julien H's PR review
julien12234 Dec 16, 2022
1f1a9b1
vectorization of NLL scorers
julien12234 Dec 16, 2022
09a8ac6
problem with test_univariate_FilteringAnomalyModel
julien12234 Dec 16, 2022
107ebaf
replace abs by __abs__ in test_univariate_covariate_ForecastingAnomal…
julien12234 Dec 16, 2022
6a5bed4
Merge branch 'master' into feat/anomaly_detection_API
hrzn Dec 16, 2022
cb2a127
replace abs by __abs__ in ALL test_univariate_covariate_ForecastingAn…
julien12234 Dec 17, 2022
4a9619b
Increase coverage of scorers tests
hrzn Dec 20, 2022
816377b
Imports in submodules
hrzn Dec 20, 2022
323bda8
Some improvements to utils
hrzn Dec 20, 2022
edad060
Some improvements
hrzn Dec 20, 2022
8ea8ea7
significant rework of quantile detector
hrzn Dec 21, 2022
f4ef944
Rework threshold detector
hrzn Dec 21, 2022
ff62c3a
Rework NLL scorers
hrzn Dec 22, 2022
7e59cea
Rename NLL scorers files
hrzn Dec 22, 2022
ca9efc3
vectorize windowing in k-means
hrzn Dec 22, 2022
b1a73d9
vectorization of windowing in PyOD and Wasserstein
hrzn Dec 22, 2022
6041a67
Docstring improvements
hrzn Dec 22, 2022
b545d18
Update darts/ad/anomaly_model/filtering_am.py
hrzn Dec 22, 2022
96206fa
Update darts/ad/anomaly_model/filtering_am.py
hrzn Dec 22, 2022
5899096
Update darts/ad/anomaly_model/filtering_am.py
hrzn Dec 22, 2022
3213467
Update darts/ad/anomaly_model/filtering_am.py
hrzn Dec 22, 2022
18f9f49
Update darts/ad/anomaly_model/filtering_am.py
hrzn Dec 22, 2022
2b8c9a9
Update darts/ad/anomaly_model/forecasting_am.py
hrzn Dec 22, 2022
eb714ef
Update darts/ad/anomaly_model/__init__.py
hrzn Dec 22, 2022
8e0a488
Update darts/ad/anomaly_model/__init__.py
hrzn Dec 22, 2022
22d9474
Update darts/ad/anomaly_model/forecasting_am.py
hrzn Dec 22, 2022
46b7603
Update darts/ad/anomaly_model/forecasting_am.py
hrzn Dec 22, 2022
18613c1
Update darts/ad/anomaly_model/forecasting_am.py
hrzn Dec 22, 2022
eb00de2
Update darts/ad/anomaly_model/forecasting_am.py
hrzn Dec 22, 2022
cbcbe1b
Update darts/ad/anomaly_model/forecasting_am.py
hrzn Dec 22, 2022
5a45fe6
Update darts/ad/scorers/__init__.py
hrzn Dec 22, 2022
b5ffd08
Update darts/ad/scorers/scorers.py
hrzn Dec 22, 2022
f597d21
Update darts/ad/scorers/scorers.py
hrzn Dec 22, 2022
b366367
Update darts/ad/scorers/scorers.py
hrzn Dec 22, 2022
ffbd101
Update darts/ad/scorers/kmeans_scorer.py
hrzn Dec 22, 2022
986aa2a
Merge branch 'master' into feat/anomaly_detection_API
hrzn Dec 22, 2022
f362b5a
PR comments
hrzn Dec 22, 2022
05f4378
Formatting
hrzn Dec 22, 2022
d5195ab
Update darts/ad/scorers/pyod_scorer.py
hrzn Dec 22, 2022
a13edd2
Small docstring improvement
hrzn Dec 22, 2022
82b65cb
Merge branch 'feat/anomaly_detection_API' of github.com:unit8co/darts…
hrzn Dec 22, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 51 additions & 0 deletions darts/ad/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
Anomaly Detection
-----------------

A suite of tools for performing anomaly detection and classification
on time series.

* `Anomaly Scorers <https://unit8co.github.io/darts/generated_api/darts.ad.scorers.html>`_
are at the core of the anomaly detection module. They
produce anomaly scores time series, either for single series (``score()``),
or for series accompanied by some predictions (``score_from_prediction()``).
Scorers can be trainable (e.g., ``KMeansScorer``) or not (e.g., ``NormScorer``).

* `Anomaly Models <https://unit8co.github.io/darts/generated_api/darts.ad.anomaly_model.html>`_
offer a convenient way to produce anomaly scores from any of Darts
forecasting models (``ForecastingAnomalyModel``) or filtering models (``FilteringAnomalyModel``),
by comparing models' predictions with actual observations.
These classes take as parameters one Darts model, and one or multiple scorers, and can be readily
used to produce anomaly scores with the ``score()`` method.

* `Anomaly Detectors <https://unit8co.github.io/darts/generated_api/darts.ad.detectors.html>`_:
transform raw time series (such as anaomly scores) into binary anomaly time series.

* `Anomaly Aggregators <https://unit8co.github.io/darts/generated_api/darts.ad.aggregators.html>`_:
combine multiple binary anomaly time series (in the form of multivariate time series)
into a single binary anomaly time series applying boolean logic.
"""

# anomaly aggregators
from .aggregators import AndAggregator, EnsembleSklearnAggregator, OrAggregator

# anomaly models
from .anomaly_model import FilteringAnomalyModel, ForecastingAnomalyModel
julien12234 marked this conversation as resolved.
Show resolved Hide resolved

# anomaly detectors
from .detectors import QuantileDetector, ThresholdDetector

# anomaly scorers
from .scorers import (
CauchyNLLScorer,
DifferenceScorer,
ExponentialNLLScorer,
GammaNLLScorer,
GaussianNLLScorer,
KMeansScorer,
LaplaceNLLScorer,
NormScorer,
PoissonNLLScorer,
PyODScorer,
WassersteinScorer,
)
18 changes: 18 additions & 0 deletions darts/ad/aggregators/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
Anomaly Aggregators
-------------------

An anomaly aggregator can take multiple detected anomalies
(in the form of binary TimeSeries, as coming from an anomaly detector)
and combine them into one. It can typically be used to combine
the detections of multiple models into one final detection.

The key method is ``predict()``, which takes as input one (or multiple)
multivariate binary TimeSeries where each component represents the
detection of a single model, and returns one (or multiple) univariate
binary TimeSeries representing the final detection.
"""

from .and_aggregator import AndAggregator
from .ensemble_sklearn_aggregator import EnsembleSklearnAggregator
from .or_aggregator import OrAggregator
300 changes: 300 additions & 0 deletions darts/ad/aggregators/aggregators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
"""
Anomaly aggregators base classes
"""

# 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)

from abc import ABC, abstractmethod
from typing import Any, Sequence, Union

import numpy as np

from darts import TimeSeries
from darts.ad.utils import _to_list, eval_accuracy_from_binary_prediction
hrzn marked this conversation as resolved.
Show resolved Hide resolved
from darts.logging import raise_if_not


class Aggregator(ABC):
def __init__(self, *args: Any, **kwargs: Any) -> None:
pass

@abstractmethod
def __str__(self):
"""returns the name of the aggregator"""
pass

@abstractmethod
def _predict_core(self):
"""returns the aggregated results"""
pass

@abstractmethod
def predict(
self, series: Union[TimeSeries, Sequence[TimeSeries]]
) -> Union[TimeSeries, Sequence[TimeSeries]]:
"""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

Returns
-------
TimeSeries
(Sequence of) aggregated results
"""
pass

def _check_input(self, series: Union[TimeSeries, Sequence[TimeSeries]]):
"""
Checks for input if:
- it is a (sequence of) multivariate series (width>1)
- (sequence of) series must be:
* a deterministic TimeSeries
* binary (only values equal to 0 or 1)
"""

list_series = _to_list(series)

raise_if_not(
all([isinstance(s, TimeSeries) for s in list_series]),
"all series in `series` must be of type TimeSeries.",
)

raise_if_not(
all([s.width > 1 for s in list_series]),
"all series in `series` must be multivariate (width>1).",
)

raise_if_not(
all([s.is_deterministic for s in list_series]),
"all series in `series` must be deterministic (number of samples=1).",
)

raise_if_not(
all(
[
np.array_equal(
s.values(copy=False), s.values(copy=False).astype(bool)
)
for s in list_series
]
),
"all series in `series` must be binary (only 0 and 1 values).",
)

return list_series

def eval_accuracy(
self,
actual_anomalies: Sequence[TimeSeries],
series: Sequence[TimeSeries],
window: int = 1,
metric: str = "recall",
) -> Union[float, Sequence[float]]:
"""Aggregates the (sequence of) multivariate series given as input into one (sequence of)
series and evaluates the results against true anomalies.

Parameters
----------
actual_anomalies
The (sequence of) ground truth of the anomalies (1 if it is an anomaly and 0 if not)
series
The (sequence of) 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
actual_anomalies.
metric
Metric function to use. Must be one of "recall", "precision",
"f1", and "accuracy".
Default: "recall"

Returns
-------
Union[float, Sequence[float]]
(Sequence of) score for the (sequence of) series
"""

list_actual_anomalies = _to_list(actual_anomalies)

raise_if_not(
all([isinstance(s, TimeSeries) for s in list_actual_anomalies]),
"all series in `actual_anomalies` must be of type TimeSeries.",
)

raise_if_not(
all([s.is_deterministic for s in list_actual_anomalies]),
"all series in `actual_anomalies` must be deterministic (number of samples=1).",
)

raise_if_not(
all([s.width == 1 for s in list_actual_anomalies]),
"all series in `actual_anomalies` must be univariate (width=1).",
)

raise_if_not(
len(list_actual_anomalies) == len(_to_list(series)),
"`actual_anomalies` and `series` must contain the same number of series.",
)

preds = self.predict(series)

return eval_accuracy_from_binary_prediction(
list_actual_anomalies, preds, window, metric
)


class NonFittableAggregator(Aggregator):
"Base class of Aggregators that do not need training."

def __init__(self) -> None:
super().__init__()

# indicates if the Aggregator is trainable or not
self.trainable = False

def predict(
self, series: Union[TimeSeries, Sequence[TimeSeries]]
) -> Union[TimeSeries, Sequence[TimeSeries]]:
"""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

Returns
-------
TimeSeries
(Sequence of) aggregated results
"""
list_series = self._check_input(series)

if isinstance(series, TimeSeries):
return self._predict_core(list_series)[0]
else:
return self._predict_core(list_series)


class FittableAggregator(Aggregator):
"Base class of Aggregators that do need training."

def __init__(self) -> None:
super().__init__()

# indicates if the Aggregator is trainable or not
self.trainable = True

# indicates if the Aggregator has been trained yet
self._fit_called = False

def _assert_fit_called(self):
"""Checks if the Aggregator has been fitted before calling its `score()` function."""

raise_if_not(
self._fit_called,
f"The Aggregator {self.__str__()} has not been fitted yet. Call `fit()` first.",
)

def fit(
self,
actual_anomalies: Union[TimeSeries, Sequence[TimeSeries]],
series: Union[TimeSeries, Sequence[TimeSeries]],
):
"""Fit the aggregators on the (sequence of) multivariate binary series.

If a list of series is given, they must have the same number of components.

Parameters
----------
actual_anomalies
The (sequence of) ground truth of the anomalies (1 if it is an anomaly and 0 if not)
series
The (sequence of) multivariate binary series
"""
list_series = self._check_input(series)
self.width_trained_on = list_series[0].width

raise_if_not(
all([s.width == self.width_trained_on for s in list_series]),
"all series in `list_series` must have the same number of components.",
)

list_actual_anomalies = _to_list(actual_anomalies)

raise_if_not(
all([isinstance(s, TimeSeries) for s in list_actual_anomalies]),
"all series in `actual_anomalies` must be of type TimeSeries.",
)

raise_if_not(
all([s.is_deterministic for s in list_actual_anomalies]),
"all series in `actual_anomalies` must be deterministic (width=1).",
)

raise_if_not(
all([s.width == 1 for s in list_actual_anomalies]),
"all series in `actual_anomalies` must be univariate (width=1).",
)

raise_if_not(
len(list_actual_anomalies) == len(list_series),
"`actual_anomalies` and `series` must contain the same number of series.",
)

same_intersection = list(
zip(
*[
[anomalies.slice_intersect(series), series.slice_intersect(series)]
for (anomalies, series) in zip(list_actual_anomalies, list_series)
]
)
)
list_actual_anomalies = list(same_intersection[0])
list_series = list(same_intersection[1])

ret = self._fit_core(list_actual_anomalies, list_series)
self._fit_called = True
return ret

def predict(
self, series: Union[TimeSeries, Sequence[TimeSeries]]
) -> Union[TimeSeries, Sequence[TimeSeries]]:
"""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

Returns
-------
TimeSeries
(Sequence of) aggregated results
"""
self._assert_fit_called()
list_series = self._check_input(series)

raise_if_not(
all([s.width == self.width_trained_on for s in list_series]),
"all series in `series` must have the same number of components as the data"
+ " used for training the detector model, number of components in training:"
+ f" {self.width_trained_on}.",
)

if isinstance(series, TimeSeries):
return self._predict_core(list_series)[0]
else:
return self._predict_core(list_series)
25 changes: 25 additions & 0 deletions darts/ad/aggregators/and_aggregator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
AND Aggregator
--------------

Aggregator that identifies a time step as anomalous if all the components
are flagged as anomalous (logical AND).
"""

from typing import Sequence

from darts import TimeSeries
from darts.ad.aggregators.aggregators import NonFittableAggregator


class AndAggregator(NonFittableAggregator):
def __init__(self) -> None:
super().__init__()

def __str__(self):
return "AndAggregator"

def _predict_core(self, series: Sequence[TimeSeries]) -> Sequence[TimeSeries]:
return [
s.sum(axis=1).map(lambda x: (x >= s.width).astype(s.dtype)) for s in series
]