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

Add NaiveMovingAverage baseline model #1557

Merged
merged 24 commits into from
Mar 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1d0e44d
implement NaiveMovingAverage model
JanFidor Feb 10, 2023
af9307c
rename Moving Average Filter
JanFidor Feb 10, 2023
0aaf625
black
JanFidor Feb 10, 2023
f87fcb6
add NaiveMovingAverage to test local
JanFidor Feb 10, 2023
d9d68d0
delete debug print
JanFidor Feb 10, 2023
8191837
Merge branch 'master' into feature/moving-average
JanFidor Feb 10, 2023
a763327
black
JanFidor Feb 12, 2023
44fbb22
isort
JanFidor Feb 14, 2023
ba6d405
fix __init__.py file import
JanFidor Feb 14, 2023
6eeaf75
rename window param and delete unnecessary check
JanFidor Feb 15, 2023
ab712b0
Merge branch 'master' into feature/moving-average
hrzn Feb 16, 2023
f453990
fix MA test param
JanFidor Feb 16, 2023
da9e698
Merge branch 'feature/moving-average' of https://github.com/JanFidor/…
JanFidor Feb 16, 2023
1410ca3
add docs changes and deterministic assertion
JanFidor Feb 16, 2023
7332106
Merge branch 'master' into feature/moving-average
JanFidor Feb 16, 2023
1b8c2af
Merge branch 'master' into feature/moving-average
JanFidor Feb 17, 2023
681ca25
use more readable assert syntax
JanFidor Feb 22, 2023
4d6aa9f
Merge branch 'feature/moving-average' of https://github.com/JanFidor/…
JanFidor Feb 22, 2023
821e64f
add loger to raise_if_not
JanFidor Feb 22, 2023
851b5cb
Merge branch 'master' into feature/moving-average
JanFidor Feb 22, 2023
99301af
black (again)
JanFidor Feb 23, 2023
7853d47
Merge branch 'master' into feature/moving-average
madtoinou Feb 26, 2023
d364a04
Merge branch 'master' into feature/moving-average
dennisbader Feb 28, 2023
d14671b
Merge branch 'master' into feature/moving-average
madtoinou Mar 7, 2023
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
9 changes: 7 additions & 2 deletions darts/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
# Forecasting
from darts.models.forecasting.arima import ARIMA
from darts.models.forecasting.auto_arima import AutoARIMA
from darts.models.forecasting.baselines import NaiveDrift, NaiveMean, NaiveSeasonal
from darts.models.forecasting.baselines import (
NaiveDrift,
NaiveMean,
NaiveMovingAverage,
NaiveSeasonal,
)
from darts.models.forecasting.exponential_smoothing import ExponentialSmoothing
from darts.models.forecasting.fft import FFT
from darts.models.forecasting.kalman_forecaster import KalmanForecaster
Expand Down Expand Up @@ -134,7 +139,7 @@ class NotImportedXGBModel:
from darts.models.filtering.kalman_filter import KalmanFilter

# Filtering
from darts.models.filtering.moving_average import MovingAverage
from darts.models.filtering.moving_average_filter import MovingAverageFilter
from darts.models.forecasting.baselines import NaiveEnsembleModel

# Ensembling
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from darts.timeseries import TimeSeries


class MovingAverage(FilteringModel):
class MovingAverageFilter(FilteringModel):
madtoinou marked this conversation as resolved.
Show resolved Hide resolved
"""
A simple moving average filter. Works on deterministic and stochastic series.
"""
Expand Down
51 changes: 51 additions & 0 deletions darts/models/forecasting/baselines.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,57 @@ def predict(self, n: int, num_samples: int = 1, verbose: bool = False):
return self._build_forecast_series(forecast)


class NaiveMovingAverage(LocalForecastingModel):
def __init__(self, input_chunk_length: int = 1):
"""Naive Moving Average Model

This model forecasts using an auto-regressive moving average (ARMA).

Parameters
----------
input_chunk_length
The size of the sliding window used to calculate the moving average
"""
super().__init__()
self.input_chunk_length = input_chunk_length
self.rolling_window = None

@property
def min_train_series_length(self):
return self.input_chunk_length

def __str__(self):
return f"NaiveMovingAverage({self.input_chunk_length})"

def fit(self, series: TimeSeries):
super().fit(series)
madtoinou marked this conversation as resolved.
Show resolved Hide resolved
raise_if_not(
series.is_deterministic,
"This model expects deterministic time series",
logger,
madtoinou marked this conversation as resolved.
Show resolved Hide resolved
)

self.rolling_window = series[-self.input_chunk_length :].values(copy=False)
return self

def predict(self, n: int, num_samples: int = 1, verbose: bool = False):
super().predict(n, num_samples)

predictions_with_observations = np.concatenate(
(self.rolling_window, np.zeros(shape=(n, self.rolling_window.shape[1]))),
axis=0,
)
rolling_sum = sum(self.rolling_window)

chunk_length = self.input_chunk_length
for i in range(chunk_length, chunk_length + n):
prediction = rolling_sum / chunk_length
predictions_with_observations[i] = prediction
lost_value = predictions_with_observations[i - chunk_length]
rolling_sum += prediction - lost_value
return self._build_forecast_series(predictions_with_observations[-n:])


class NaiveEnsembleModel(EnsembleModel):
def __init__(
self, models: Union[List[LocalForecastingModel], List[GlobalForecastingModel]]
Expand Down
4 changes: 2 additions & 2 deletions darts/tests/ad/test_aggregators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from darts.ad.aggregators.and_aggregator import AndAggregator
from darts.ad.aggregators.ensemble_sklearn_aggregator import EnsembleSklearnAggregator
from darts.ad.aggregators.or_aggregator import OrAggregator
from darts.models import MovingAverage
from darts.models import MovingAverageFilter
from darts.tests.base_test_class import DartsBaseTestClass

list_NonFittableAggregator = [
Expand Down Expand Up @@ -725,7 +725,7 @@ def test_EnsembleSklearn(self):

# Need to input an EnsembleSklearn model
with self.assertRaises(ValueError):
EnsembleSklearnAggregator(model=MovingAverage(window=10))
EnsembleSklearnAggregator(model=MovingAverageFilter(window=10))

# simple case
# series has 3 components, and real_anomalies_3w is equal to
Expand Down
66 changes: 42 additions & 24 deletions darts/tests/ad/test_anomaly_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
WassersteinScorer,
)
from darts.ad.utils import eval_accuracy_from_scores, show_anomalies_from_scores
from darts.models import MovingAverage, NaiveSeasonal, RegressionModel
from darts.models import MovingAverageFilter, NaiveSeasonal, RegressionModel
from darts.tests.base_test_class import DartsBaseTestClass


Expand Down Expand Up @@ -59,8 +59,8 @@ class ADAnomalyModelTestCase(DartsBaseTestClass):
train._time_index, np_only_0_anomalies
)

modified_train = MovingAverage(window=10).filter(train)
modified_test = MovingAverage(window=10).filter(test)
modified_train = MovingAverageFilter(window=10).filter(train)
modified_test = MovingAverageFilter(window=10).filter(test)

np_probabilistic = np.random.normal(loc=10, scale=1, size=[100, 1, 20])
probabilistic = TimeSeries.from_times_and_values(
Expand Down Expand Up @@ -95,7 +95,9 @@ def test_Scorer(self):
for scorers in list_NonFittableAnomalyScorer:
for anomaly_model in [
ForecastingAnomalyModel(model=RegressionModel(lags=10), scorer=scorers),
FilteringAnomalyModel(model=MovingAverage(window=20), scorer=scorers),
FilteringAnomalyModel(
model=MovingAverageFilter(window=20), scorer=scorers
),
]:

# scorer are trainable
Expand All @@ -110,7 +112,9 @@ def test_Scorer(self):
for scorers in list_FittableAnomalyScorer:
for anomaly_model in [
ForecastingAnomalyModel(model=RegressionModel(lags=10), scorer=scorers),
FilteringAnomalyModel(model=MovingAverage(window=20), scorer=scorers),
FilteringAnomalyModel(
model=MovingAverageFilter(window=20), scorer=scorers
),
]:

# scorer are not trainable
Expand All @@ -123,7 +127,9 @@ def test_Score(self):
)
am1.fit(self.train, allow_model_training=True)

am2 = FilteringAnomalyModel(model=MovingAverage(window=20), scorer=NormScorer())
am2 = FilteringAnomalyModel(
model=MovingAverageFilter(window=20), scorer=NormScorer()
)

for am in [am1, am2]:
# Parameter return_model_prediction
Expand All @@ -149,12 +155,15 @@ def test_Score(self):
def test_FitFilteringAnomalyModelInput(self):

for anomaly_model in [
FilteringAnomalyModel(model=MovingAverage(window=20), scorer=NormScorer()),
FilteringAnomalyModel(
model=MovingAverage(window=20), scorer=[NormScorer(), KMeansScorer()]
model=MovingAverageFilter(window=20), scorer=NormScorer()
),
FilteringAnomalyModel(
model=MovingAverageFilter(window=20),
scorer=[NormScorer(), KMeansScorer()],
),
FilteringAnomalyModel(
model=MovingAverage(window=20), scorer=KMeansScorer()
model=MovingAverageFilter(window=20), scorer=KMeansScorer()
),
]:

Expand Down Expand Up @@ -336,12 +345,15 @@ def test_ScoreForecastingAnomalyModelInput(self):
def test_ScoreFilteringAnomalyModelInput(self):

for anomaly_model in [
FilteringAnomalyModel(model=MovingAverage(window=10), scorer=NormScorer()),
FilteringAnomalyModel(
model=MovingAverage(window=10), scorer=[NormScorer(), KMeansScorer()]
model=MovingAverageFilter(window=10), scorer=NormScorer()
),
FilteringAnomalyModel(
model=MovingAverageFilter(window=10),
scorer=[NormScorer(), KMeansScorer()],
),
FilteringAnomalyModel(
model=MovingAverage(window=10), scorer=KMeansScorer()
model=MovingAverageFilter(window=10), scorer=KMeansScorer()
),
]:

Expand All @@ -355,15 +367,18 @@ def test_eval_accuracy(self):
)
am1.fit(self.train, allow_model_training=True)

am2 = FilteringAnomalyModel(model=MovingAverage(window=20), scorer=NormScorer())
am2 = FilteringAnomalyModel(
model=MovingAverageFilter(window=20), scorer=NormScorer()
)

am3 = ForecastingAnomalyModel(
model=RegressionModel(lags=10), scorer=[NormScorer(), WassersteinScorer()]
)
am3.fit(self.train, allow_model_training=True)

am4 = FilteringAnomalyModel(
model=MovingAverage(window=20), scorer=[NormScorer(), WassersteinScorer()]
model=MovingAverageFilter(window=20),
scorer=[NormScorer(), WassersteinScorer()],
)
am4.fit(self.train)

Expand Down Expand Up @@ -500,7 +515,9 @@ def test_ForecastingAnomalyModelInput(self):
with self.assertRaises(ValueError):
ForecastingAnomalyModel(model=1, scorer=NormScorer())
with self.assertRaises(ValueError):
ForecastingAnomalyModel(model=MovingAverage(window=10), scorer=NormScorer())
ForecastingAnomalyModel(
model=MovingAverageFilter(window=10), scorer=NormScorer()
)
with self.assertRaises(ValueError):
ForecastingAnomalyModel(
model=[RegressionModel(lags=10), RegressionModel(lags=5)],
Expand Down Expand Up @@ -534,23 +551,24 @@ def test_FilteringAnomalyModelInput(self):
FilteringAnomalyModel(model=RegressionModel(lags=10), scorer=NormScorer())
with self.assertRaises(ValueError):
FilteringAnomalyModel(
model=[MovingAverage(window=10), MovingAverage(window=10)],
model=[MovingAverageFilter(window=10), MovingAverageFilter(window=10)],
scorer=NormScorer(),
)

# scorer input
# scorer input must be of type AnomalyScorer
with self.assertRaises(ValueError):
FilteringAnomalyModel(model=MovingAverage(window=10), scorer=1)
FilteringAnomalyModel(model=MovingAverageFilter(window=10), scorer=1)
with self.assertRaises(ValueError):
FilteringAnomalyModel(model=MovingAverage(window=10), scorer="str")
FilteringAnomalyModel(model=MovingAverageFilter(window=10), scorer="str")
with self.assertRaises(ValueError):
FilteringAnomalyModel(
model=MovingAverage(window=10), scorer=MovingAverage(window=10)
model=MovingAverageFilter(window=10),
scorer=MovingAverageFilter(window=10),
)
with self.assertRaises(ValueError):
FilteringAnomalyModel(
model=MovingAverage(window=10), scorer=[NormScorer(), "str"]
model=MovingAverageFilter(window=10), scorer=[NormScorer(), "str"]
)

def test_univariate_ForecastingAnomalyModel(self):
Expand Down Expand Up @@ -700,7 +718,7 @@ def test_univariate_FilteringAnomalyModel(self):
)

anomaly_model = FilteringAnomalyModel(
model=MovingAverage(window=5),
model=MovingAverageFilter(window=5),
scorer=[
NormScorer(),
Difference(),
Expand Down Expand Up @@ -974,7 +992,7 @@ def test_multivariate__FilteringAnomalyModel(self):

# first case: scorers that return univariate scores
anomaly_model = FilteringAnomalyModel(
model=MovingAverage(window=10),
model=MovingAverageFilter(window=10),
scorer=[
NormScorer(component_wise=False),
WassersteinScorer(),
Expand Down Expand Up @@ -1058,7 +1076,7 @@ def test_multivariate__FilteringAnomalyModel(self):

# second case: scorers that return scorers that have the same width as the input
anomaly_model = FilteringAnomalyModel(
model=MovingAverage(window=10),
model=MovingAverageFilter(window=10),
scorer=[
NormScorer(component_wise=True),
Difference(),
Expand Down Expand Up @@ -1379,7 +1397,7 @@ def test_show_anomalies(self):
forecasting_anomaly_model.fit(self.train, allow_model_training=True)

filtering_anomaly_model = FilteringAnomalyModel(
model=MovingAverage(window=10), scorer=NormScorer()
model=MovingAverageFilter(window=10), scorer=NormScorer()
)

for anomaly_model in [forecasting_anomaly_model, filtering_anomaly_model]:
Expand Down
12 changes: 6 additions & 6 deletions darts/tests/ad/test_scorers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
)
from darts.ad.scorers import NormScorer as Norm
from darts.ad.scorers import PoissonNLLScorer, PyODScorer, WassersteinScorer
from darts.models import MovingAverage
from darts.models import MovingAverageFilter
from darts.tests.base_test_class import DartsBaseTestClass

list_NonFittableAnomalyScorer = [
Expand Down Expand Up @@ -70,8 +70,8 @@ class ADAnomalyScorerTestCase(DartsBaseTestClass):
train._time_index, np_only_0_anomalies
)

modified_train = MovingAverage(window=10).filter(train)
modified_test = MovingAverage(window=10).filter(test)
modified_train = MovingAverageFilter(window=10).filter(train)
modified_test = MovingAverageFilter(window=10).filter(test)

np_probabilistic = np.random.normal(loc=10, scale=2, size=[100, 1, 20])
probabilistic = TimeSeries.from_times_and_values(
Expand All @@ -90,8 +90,8 @@ class ADAnomalyScorerTestCase(DartsBaseTestClass):
mts_train._time_index, np_mts_anomalies
)

modified_mts_train = MovingAverage(window=10).filter(mts_train)
modified_mts_test = MovingAverage(window=10).filter(mts_test)
modified_mts_train = MovingAverageFilter(window=10).filter(mts_train)
modified_mts_test = MovingAverageFilter(window=10).filter(mts_test)

np_mts_probabilistic = np.random.normal(
loc=[[10], [5]], scale=[[1], [1.5]], size=[100, 2, 20]
Expand Down Expand Up @@ -1278,7 +1278,7 @@ def test_PyODScorer(self):

# model parameter must be pyod.models typy BaseDetector
with self.assertRaises(ValueError):
PyODScorer(model=MovingAverage(window=10))
PyODScorer(model=MovingAverageFilter(window=10))

# component_wise parameter
# component_wise must be bool
Expand Down
6 changes: 3 additions & 3 deletions darts/tests/models/filtering/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from darts.metrics import rmse
from darts.models.filtering.gaussian_process_filter import GaussianProcessFilter
from darts.models.filtering.kalman_filter import KalmanFilter
from darts.models.filtering.moving_average import MovingAverage
from darts.models.filtering.moving_average_filter import MovingAverageFilter
from darts.tests.base_test_class import DartsBaseTestClass
from darts.utils import timeseries_generation as tg

Expand Down Expand Up @@ -137,15 +137,15 @@ def test_kalman_given_kf(self):

class MovingAverageTestCase(FilterBaseTestClass):
def test_moving_average_univariate(self):
ma = MovingAverage(window=3, centered=False)
ma = MovingAverageFilter(window=3, centered=False)
sine_ts = tg.sine_timeseries(length=30, value_frequency=0.1)
sine_filtered = ma.filter(sine_ts)
self.assertGreater(
np.mean(np.abs(sine_ts.values())), np.mean(np.abs(sine_filtered.values()))
)

def test_moving_average_multivariate(self):
ma = MovingAverage(window=3)
ma = MovingAverageFilter(window=3)
sine_ts = tg.sine_timeseries(length=30, value_frequency=0.1)
noise_ts = tg.gaussian_timeseries(length=30) * 0.1
ts = sine_ts.stack(noise_ts)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
LinearRegressionModel,
NaiveDrift,
NaiveMean,
NaiveMovingAverage,
NaiveSeasonal,
Prophet,
RandomForest,
Expand Down Expand Up @@ -84,6 +85,7 @@
(NaiveSeasonal(), 32),
(NaiveMean(), 37),
(NaiveDrift(), 39),
(NaiveMovingAverage(input_chunk_length=5), 34),
]

dual_models = [
Expand Down