diff --git a/CHANGELOG.md b/CHANGELOG.md index a2539f434..696a2bd3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add default `params_to_tune` for `TimeSeriesImputerTransform` ([#1232](https://github.com/tinkoff-ai/etna/pull/1232)) - Add default `params_to_tune` for `DifferencingTransform`, `MedianTransform`, `MaxTransform`, `MinTransform`, `QuantileTransform`, `StdTransform`, `MeanTransform`, `MADTransform`, `MinMaxDifferenceTransform`, `SumTransform`, `BoxCoxTransform`, `YeoJohnsonTransform`, `MaxAbsScalerTransform`, `MinMaxScalerTransform`, `RobustScalerTransform` and `StandardScalerTransform` ([#1233](https://github.com/tinkoff-ai/etna/pull/1233)) - Add default `params_to_tune` for `LabelEncoderTransform` ([#1242](https://github.com/tinkoff-ai/etna/pull/1242)) +- Add default `params_to_tune` for `ChangePointsSegmentationTransform`, `ChangePointsTrendTransform`, `ChangePointsLevelTransform`, `TrendTransform`, `LinearTrendTransform`, `TheilSenTrendTransform` and `STLTransform` ([#1243](https://github.com/tinkoff-ai/etna/pull/1243)) ### Fixed - Fix bug in `GaleShapleyFeatureSelectionTransform` with wrong number of remaining features ([#1110](https://github.com/tinkoff-ai/etna/pull/1110)) - `ProphetModel` fails with additional seasonality set ([#1157](https://github.com/tinkoff-ai/etna/pull/1157)) diff --git a/etna/transforms/decomposition/change_points_based/detrend.py b/etna/transforms/decomposition/change_points_based/detrend.py index 9dd49301f..d9c1e9e0a 100644 --- a/etna/transforms/decomposition/change_points_based/detrend.py +++ b/etna/transforms/decomposition/change_points_based/detrend.py @@ -1,3 +1,4 @@ +from typing import Dict from typing import Optional import numpy as np @@ -5,6 +6,7 @@ from ruptures.detection import Binseg from sklearn.linear_model import LinearRegression +from etna import SETTINGS from etna.transforms.decomposition.change_points_based.base import ReversibleChangePointsTransform from etna.transforms.decomposition.change_points_based.base import _OneSegmentChangePointsTransform from etna.transforms.decomposition.change_points_based.change_points_models import BaseChangePointsModelAdapter @@ -13,6 +15,11 @@ from etna.transforms.decomposition.change_points_based.per_interval_models import SklearnRegressionPerIntervalModel from etna.transforms.utils import match_target_quantiles +if SETTINGS.auto_required: + from optuna.distributions import BaseDistribution + from optuna.distributions import CategoricalDistribution + from optuna.distributions import IntUniformDistribution + class _OneSegmentChangePointsTrendTransform(_OneSegmentChangePointsTransform): """_OneSegmentChangePointsTransform subtracts multiple linear trend from series.""" @@ -38,7 +45,16 @@ def _apply_inverse_transformation(self, df: pd.DataFrame, transformed_series: pd class ChangePointsTrendTransform(ReversibleChangePointsTransform): - """ChangePointsTrendTransform uses :py:class:`ruptures.detection.Binseg` model as a change point detection model. + """Transform that makes a detrending of change-point intervals. + + This class differs from :py:class:`~etna.transforms.decomposition.change_points_based.level.ChangePointsLevelTransform` + only by default values for ``change_points_model`` and ``per_interval_model``. + + Transform divides each segment into intervals using ``change_points_model``. + Then a separate model is fitted on each interval using ``per_interval_model``. + Values predicted by the model are subtracted from each interval. + + Evaluated function can be linear, mean, median, etc. Look at the signature to find out which models can be used. Warning ------- @@ -46,6 +62,12 @@ class ChangePointsTrendTransform(ReversibleChangePointsTransform): it uses information from the whole train part. """ + _default_change_points_model = RupturesChangePointsModel( + change_points_model=Binseg(model="ar"), + n_bkps=5, + ) + _default_per_interval_model = SklearnRegressionPerIntervalModel(model=LinearRegression()) + def __init__( self, in_column: str, @@ -59,24 +81,21 @@ def __init__( in_column: name of column to apply transform to change_points_model: - model to get trend change points + model to get trend change points, + by default :py:class:`ruptures.detection.Binseg` in a wrapper with ``n_bkps=5`` is used per_interval_model: - model to process intervals of segment + model to process intervals of segment, + by default :py:class:`sklearn.linear_models.LinearRegression` in a wrapper is used """ self.in_column = in_column + self.change_points_model = ( - change_points_model - if change_points_model is not None - else RupturesChangePointsModel( - change_points_model=Binseg(model="ar"), - n_bkps=5, - ) + change_points_model if change_points_model is not None else self._default_change_points_model ) self.per_interval_model = ( - per_interval_model - if per_interval_model is not None - else SklearnRegressionPerIntervalModel(model=LinearRegression()) + per_interval_model if per_interval_model is not None else self._default_per_interval_model ) + super().__init__( transform=_OneSegmentChangePointsTrendTransform( in_column=self.in_column, @@ -85,3 +104,30 @@ def __init__( ), required_features=[in_column], ) + + @property + def _is_change_points_model_default(self) -> bool: + # it can't see the difference between Binseg(model="ar") and Binseg(model="l1") + return self.change_points_model.to_dict() == self._default_change_points_model.to_dict() + + def params_to_tune(self) -> Dict[str, "BaseDistribution"]: + """Get default grid for tuning hyperparameters. + + If ``change_points_model`` is equal to default then this grid tunes parameters: + ``change_points_model.change_points_model.model``, ``change_points_model.n_bkps``. + Other parameters are expected to be set by the user. + + Returns + ------- + : + Grid to tune. + """ + if self._is_change_points_model_default: + return { + "change_points_model.change_points_model.model": CategoricalDistribution( + ["l1", "l2", "normal", "rbf", "cosine", "linear", "clinear", "ar", "mahalanobis", "rank"] + ), + "change_points_model.n_bkps": IntUniformDistribution(low=5, high=30), + } + else: + return {} diff --git a/etna/transforms/decomposition/change_points_based/level.py b/etna/transforms/decomposition/change_points_based/level.py index c2df1c5bd..41a3f7c8a 100644 --- a/etna/transforms/decomposition/change_points_based/level.py +++ b/etna/transforms/decomposition/change_points_based/level.py @@ -1,7 +1,9 @@ +from typing import Dict from typing import Optional from ruptures import Binseg +from etna import SETTINGS from etna.transforms.decomposition.change_points_based.base import BaseChangePointsModelAdapter from etna.transforms.decomposition.change_points_based.base import ReversibleChangePointsTransform from etna.transforms.decomposition.change_points_based.change_points_models.ruptures_based import ( @@ -11,6 +13,11 @@ from etna.transforms.decomposition.change_points_based.per_interval_models import MeanPerIntervalModel from etna.transforms.decomposition.change_points_based.per_interval_models import StatisticsPerIntervalModel +if SETTINGS.auto_required: + from optuna.distributions import BaseDistribution + from optuna.distributions import CategoricalDistribution + from optuna.distributions import IntUniformDistribution + class _OneSegmentChangePointsLevelTransform(_OneSegmentChangePointsTrendTransform): def __init__( @@ -36,7 +43,16 @@ def __init__( class ChangePointsLevelTransform(ReversibleChangePointsTransform): - """ChangePointsLevelTransform uses :py:class:`ruptures.detection.Binseg` model as a change point detection model. + """Transform that makes a detrending of change-point intervals. + + This class differs from :py:class:`~etna.transforms.decomposition.change_points_based.detrend.ChangePointsTrendTransform` + only by default values for ``change_points_model`` and ``per_interval_model``. + + Transform divides each segment into intervals using ``change_points_model``. + Then a separate model is fitted on each interval using ``per_interval_model``. + Values predicted by the model are subtracted from each interval. + + Evaluated function can be linear, mean, median, etc. Look at the signature to find out which models can be used. Warning ------- @@ -44,6 +60,12 @@ class ChangePointsLevelTransform(ReversibleChangePointsTransform): it uses information from the whole train part. """ + _default_change_points_model = RupturesChangePointsModel( + change_points_model=Binseg(model="ar"), + n_bkps=5, + ) + _default_per_interval_model = MeanPerIntervalModel() + def __init__( self, in_column: str, @@ -57,20 +79,21 @@ def __init__( in_column: name of column to apply transform to change_points_model: - model to get trend change points + model to get trend change points, + by default :py:class:`ruptures.detection.Binseg` in a wrapper with ``n_bkps=5`` is used per_interval_model: - model to process intervals of segment + model to process intervals of segment, + by default mean value is used to evaluate the interval """ self.in_column = in_column + self.change_points_model = ( - change_points_model - if change_points_model is not None - else RupturesChangePointsModel( - change_points_model=Binseg(model="l2"), - n_bkps=5, - ) + change_points_model if change_points_model is not None else self._default_change_points_model + ) + self.per_interval_model = ( + per_interval_model if per_interval_model is not None else self._default_per_interval_model ) - self.per_interval_model = per_interval_model if per_interval_model is not None else MeanPerIntervalModel() + super().__init__( transform=_OneSegmentChangePointsLevelTransform( in_column=self.in_column, @@ -79,3 +102,30 @@ def __init__( ), required_features=[in_column], ) + + @property + def _is_change_points_model_default(self) -> bool: + # it can't see the difference between Binseg(model="ar") and Binseg(model="l1") + return self.change_points_model.to_dict() == self._default_change_points_model.to_dict() + + def params_to_tune(self) -> Dict[str, "BaseDistribution"]: + """Get default grid for tuning hyperparameters. + + If ``change_points_model`` is equal to default then this grid tunes parameters: + ``change_points_model.change_points_model.model``, ``change_points_model.n_bkps``. + Other parameters are expected to be set by the user. + + Returns + ------- + : + Grid to tune. + """ + if self._is_change_points_model_default: + return { + "change_points_model.change_points_model.model": CategoricalDistribution( + ["l1", "l2", "normal", "rbf", "cosine", "linear", "clinear", "ar", "mahalanobis", "rank"] + ), + "change_points_model.n_bkps": IntUniformDistribution(low=5, high=30), + } + else: + return {} diff --git a/etna/transforms/decomposition/change_points_based/per_interval_models/base.py b/etna/transforms/decomposition/change_points_based/per_interval_models/base.py index 429ee0bf5..a657429dd 100644 --- a/etna/transforms/decomposition/change_points_based/per_interval_models/base.py +++ b/etna/transforms/decomposition/change_points_based/per_interval_models/base.py @@ -10,7 +10,7 @@ class PerIntervalModel(BaseMixin, ABC): """Class to handle intervals in change point based transforms. PerIntervalModel is a class to process intervals between change points - in `~etna.transforms.decomposition.change_points_based` transforms. + in :py:mod:`~etna.transforms.decomposition.change_points_based` transforms. """ @abstractmethod diff --git a/etna/transforms/decomposition/change_points_based/segmentation.py b/etna/transforms/decomposition/change_points_based/segmentation.py index b279a4363..092a7cc05 100644 --- a/etna/transforms/decomposition/change_points_based/segmentation.py +++ b/etna/transforms/decomposition/change_points_based/segmentation.py @@ -1,13 +1,24 @@ +from typing import Dict from typing import Optional import numpy as np import pandas as pd +from ruptures import Binseg +from etna import SETTINGS from etna.transforms.decomposition.change_points_based.base import IrreversibleChangePointsTransform from etna.transforms.decomposition.change_points_based.base import _OneSegmentChangePointsTransform from etna.transforms.decomposition.change_points_based.change_points_models import BaseChangePointsModelAdapter +from etna.transforms.decomposition.change_points_based.change_points_models.ruptures_based import ( + RupturesChangePointsModel, +) from etna.transforms.decomposition.change_points_based.per_interval_models import ConstantPerIntervalModel +if SETTINGS.auto_required: + from optuna.distributions import BaseDistribution + from optuna.distributions import CategoricalDistribution + from optuna.distributions import IntUniformDistribution + class _OneSegmentChangePointsSegmentationTransform(_OneSegmentChangePointsTransform): """_OneSegmentChangePointsSegmentationTransform make label encoder to change points.""" @@ -20,7 +31,7 @@ def __init__(self, in_column: str, out_column: str, change_points_model: BaseCha name of column to apply transform to out_column: result column name. If not given use ``self.__repr__()`` - change_point_model: + change_points_model: model to get change points """ self.out_column = out_column @@ -46,7 +57,11 @@ def _apply_inverse_transformation(self, df: pd.DataFrame, transformed_series: pd class ChangePointsSegmentationTransform(IrreversibleChangePointsTransform): - """ChangePointsSegmentationTransform make label encoder to change points. + """Transform that makes label encoding of change-point intervals. + + Transform divides each segment into intervals using ``change_points_model``. + Each interval is enumerated based on its index from the start of the segment. + New column is created with number of interval for each timestamp. Warning ------- @@ -54,10 +69,15 @@ class ChangePointsSegmentationTransform(IrreversibleChangePointsTransform): it uses information from the whole train part. """ + _default_change_points_model = RupturesChangePointsModel( + change_points_model=Binseg(model="ar"), + n_bkps=5, + ) + def __init__( self, in_column: str, - change_points_model: BaseChangePointsModelAdapter, + change_points_model: BaseChangePointsModelAdapter = None, out_column: Optional[str] = None, ): """Init ChangePointsSegmentationTransform. @@ -66,19 +86,51 @@ def __init__( ---------- in_column: name of column to fit change point model + change_points_model: + model to get change points, + by default :py:class:`ruptures.detection.Binseg` in a wrapper with ``n_bkps=5`` is used out_column: result column name. If not given use ``self.__repr__()`` - change_points_model: - model to get change points """ self.in_column = in_column self.out_column = out_column if out_column is not None else self.__repr__() - self.change_points_model = change_points_model + + self.change_points_model = ( + change_points_model if change_points_model is not None else self._default_change_points_model + ) + super().__init__( transform=_OneSegmentChangePointsSegmentationTransform( in_column=self.in_column, - out_column=self.out_column, change_points_model=self.change_points_model, + out_column=self.out_column, ), required_features=[in_column], ) + + @property + def _is_change_points_model_default(self) -> bool: + # it can't see the difference between Binseg(model="ar") and Binseg(model="l1") + return self.change_points_model.to_dict() == self._default_change_points_model.to_dict() + + def params_to_tune(self) -> Dict[str, "BaseDistribution"]: + """Get default grid for tuning hyperparameters. + + If ``change_points_model`` is equal to default then this grid tunes parameters: + ``change_points_model.change_points_model.model``, ``change_points_model.n_bkps``. + Other parameters are expected to be set by the user. + + Returns + ------- + : + Grid to tune. + """ + if self._is_change_points_model_default: + return { + "change_points_model.change_points_model.model": CategoricalDistribution( + ["l1", "l2", "normal", "rbf", "cosine", "linear", "clinear", "ar", "mahalanobis", "rank"] + ), + "change_points_model.n_bkps": IntUniformDistribution(low=5, high=30), + } + else: + return {} diff --git a/etna/transforms/decomposition/change_points_based/trend.py b/etna/transforms/decomposition/change_points_based/trend.py index 4a66e94c2..2a435c1f7 100644 --- a/etna/transforms/decomposition/change_points_based/trend.py +++ b/etna/transforms/decomposition/change_points_based/trend.py @@ -1,13 +1,25 @@ +from typing import Dict from typing import Optional import pandas as pd +from ruptures import Binseg +from sklearn.linear_model import LinearRegression +from etna import SETTINGS from etna.transforms.decomposition.change_points_based.base import IrreversibleChangePointsTransform from etna.transforms.decomposition.change_points_based.change_points_models import BaseChangePointsModelAdapter +from etna.transforms.decomposition.change_points_based.change_points_models.ruptures_based import ( + RupturesChangePointsModel, +) from etna.transforms.decomposition.change_points_based.detrend import _OneSegmentChangePointsTrendTransform from etna.transforms.decomposition.change_points_based.per_interval_models import PerIntervalModel from etna.transforms.decomposition.change_points_based.per_interval_models import SklearnRegressionPerIntervalModel +if SETTINGS.auto_required: + from optuna.distributions import BaseDistribution + from optuna.distributions import CategoricalDistribution + from optuna.distributions import IntUniformDistribution + class _OneSegmentTrendTransform(_OneSegmentChangePointsTrendTransform): """_OneSegmentTrendTransform adds trend as a feature.""" @@ -48,10 +60,13 @@ def _apply_inverse_transformation(self, df: pd.DataFrame, transformed_series: pd class TrendTransform(IrreversibleChangePointsTransform): - """TrendTransform adds trend as a feature. + """Transform that adds trend as a feature. + + Transform divides each segment into intervals using ``change_points_model``. + Then a separate model is fitted on each interval using ``per_interval_model``. + New column is created with values predicted by the model of each interval. - TrendTransform uses uses :py:class:`ruptures.detection.Binseg` model as a change point detection model - in _TrendTransform. + Evaluated function can be linear, mean, median, etc. Look at the signature to find out which models can be used. Warning ------- @@ -59,10 +74,16 @@ class TrendTransform(IrreversibleChangePointsTransform): it uses information from the whole train part. """ + _default_change_points_model = RupturesChangePointsModel( + change_points_model=Binseg(model="ar"), + n_bkps=5, + ) + _default_per_interval_model = SklearnRegressionPerIntervalModel(model=LinearRegression()) + def __init__( self, in_column: str, - change_points_model: BaseChangePointsModelAdapter, + change_points_model: BaseChangePointsModelAdapter = None, per_interval_model: Optional[PerIntervalModel] = None, out_column: Optional[str] = None, ): @@ -72,16 +93,26 @@ def __init__( ---------- in_column: name of column to apply transform to + change_points_model: + model to get trend change points, + by default :py:class:`ruptures.detection.Binseg` in a wrapper with ``n_bkps=5`` is used + per_interval_model: + model to process intervals of segment, + by default :py:class:`sklearn.linear_models.LinearRegression` in a wrapper is used out_column: name of added column. If not given, use ``self.__repr__()`` """ self.in_column = in_column + self.out_column = out_column + + self.change_points_model = ( + change_points_model if change_points_model is not None else self._default_change_points_model + ) self.per_interval_model = ( - SklearnRegressionPerIntervalModel() if per_interval_model is None else per_interval_model + per_interval_model if per_interval_model is not None else self._default_per_interval_model ) - self.change_points_model = change_points_model - self.out_column = out_column + super().__init__( transform=_OneSegmentTrendTransform( in_column=self.in_column, @@ -91,3 +122,30 @@ def __init__( ), required_features=[in_column], ) + + @property + def _is_change_points_model_default(self) -> bool: + # it can't see the difference between Binseg(model="ar") and Binseg(model="l1") + return self.change_points_model.to_dict() == self._default_change_points_model.to_dict() + + def params_to_tune(self) -> Dict[str, "BaseDistribution"]: + """Get default grid for tuning hyperparameters. + + If ``change_points_model`` is equal to default then this grid tunes parameters: + ``change_points_model.change_points_model.model``, ``change_points_model.n_bkps``. + Other parameters are expected to be set by the user. + + Returns + ------- + : + Grid to tune. + """ + if self._is_change_points_model_default: + return { + "change_points_model.change_points_model.model": CategoricalDistribution( + ["l1", "l2", "normal", "rbf", "cosine", "linear", "clinear", "ar", "mahalanobis", "rank"] + ), + "change_points_model.n_bkps": IntUniformDistribution(low=5, high=30), + } + else: + return {} diff --git a/etna/transforms/decomposition/detrend.py b/etna/transforms/decomposition/detrend.py index cbdf20e57..77290184b 100644 --- a/etna/transforms/decomposition/detrend.py +++ b/etna/transforms/decomposition/detrend.py @@ -1,3 +1,4 @@ +from typing import Dict from typing import List import numpy as np @@ -8,13 +9,18 @@ from sklearn.pipeline import Pipeline from sklearn.preprocessing import PolynomialFeatures +from etna import SETTINGS from etna.transforms.base import OneSegmentTransform from etna.transforms.base import ReversiblePerSegmentWrapper from etna.transforms.utils import match_target_quantiles +if SETTINGS.auto_required: + from optuna.distributions import BaseDistribution + from optuna.distributions import IntUniformDistribution + class _OneSegmentLinearTrendBaseTransform(OneSegmentTransform): - """LinearTrendBaseTransform is a base class that implements trend subtraction and reconstruction feature.""" + """Transform for one segment that implements trend subtraction and reconstruction feature.""" def __init__(self, in_column: str, regressor: RegressorMixin, poly_degree: int = 1): """ @@ -137,8 +143,10 @@ def inverse_transform(self, df: pd.DataFrame) -> pd.DataFrame: class LinearTrendTransform(ReversiblePerSegmentWrapper): - """ - Transform that uses :py:class:`sklearn.linear_model.LinearRegression` to find linear or polynomial trend in data. + """Transform that uses linear regression with polynomial features to make a detrending. + + Transform fits a :py:class:`sklearn.linear_model.LinearRegression` with polynomial features on each segment. + Values predicted by the model are subtracted from each segment. Warning ------- @@ -174,10 +182,26 @@ def get_regressors_info(self) -> List[str]: """Return the list with regressors created by the transform.""" return [] + def params_to_tune(self) -> Dict[str, "BaseDistribution"]: + """Get default grid for tuning hyperparameters. + + This grid tunes only ``poly_degree`` parameter. Other parameters are expected to be set by the user. + + Returns + ------- + : + Grid to tune. + """ + return { + "poly_degree": IntUniformDistribution(low=1, high=2), + } + class TheilSenTrendTransform(ReversiblePerSegmentWrapper): - """ - Transform that uses :py:class:`sklearn.linear_model.TheilSenRegressor` to find linear or polynomial trend in data. + """Transform that uses Theil–Sen regression with polynomial features to make a detrending. + + Transform fits a :py:class:`sklearn.linear_model.TheilSenRegressor` with polynomial features on each segment. + Values predicted by the model are subtracted from each segment. Warning ------- @@ -217,3 +241,17 @@ def __init__(self, in_column: str, poly_degree: int = 1, **regression_params): def get_regressors_info(self) -> List[str]: """Return the list with regressors created by the transform.""" return [] + + def params_to_tune(self) -> Dict[str, "BaseDistribution"]: + """Get default grid for tuning hyperparameters. + + This grid tunes only ``poly_degree`` parameter. Other parameters are expected to be set by the user. + + Returns + ------- + : + Grid to tune. + """ + return { + "poly_degree": IntUniformDistribution(low=1, high=2), + } diff --git a/etna/transforms/decomposition/stl.py b/etna/transforms/decomposition/stl.py index 4b33a3ef7..554bb14b6 100644 --- a/etna/transforms/decomposition/stl.py +++ b/etna/transforms/decomposition/stl.py @@ -11,10 +11,15 @@ from statsmodels.tsa.forecasting.stl import STLForecast from statsmodels.tsa.forecasting.stl import STLForecastResults +from etna import SETTINGS from etna.transforms.base import OneSegmentTransform from etna.transforms.base import ReversiblePerSegmentWrapper from etna.transforms.utils import match_target_quantiles +if SETTINGS.auto_required: + from optuna.distributions import BaseDistribution + from optuna.distributions import CategoricalDistribution + class _OneSegmentSTLTransform(OneSegmentTransform): def __init__( @@ -224,3 +229,18 @@ def __init__( def get_regressors_info(self) -> List[str]: """Return the list with regressors created by the transform.""" return [] + + def params_to_tune(self) -> Dict[str, "BaseDistribution"]: + """Get default grid for tuning hyperparameters. + + This grid tunes parameters: ``model``, ``robust``. Other parameters are expected to be set by the user. + + Returns + ------- + : + Grid to tune. + """ + return { + "model": CategoricalDistribution(["arima", "holt"]), + "robust": CategoricalDistribution([False, True]), + } diff --git a/tests/test_transforms/test_decomposition/test_change_points_based/test_detrend.py b/tests/test_transforms/test_decomposition/test_change_points_based/test_detrend.py index 5fc21ee15..5edb3ca43 100644 --- a/tests/test_transforms/test_decomposition/test_change_points_based/test_detrend.py +++ b/tests/test_transforms/test_decomposition/test_change_points_based/test_detrend.py @@ -18,6 +18,7 @@ from etna.transforms.decomposition import ChangePointsTrendTransform from etna.transforms.decomposition import RupturesChangePointsModel from etna.transforms.decomposition.change_points_based.detrend import _OneSegmentChangePointsTrendTransform +from tests.test_transforms.utils import assert_sampling_is_valid from tests.test_transforms.utils import assert_transformation_equals_loaded_original @@ -97,3 +98,35 @@ def test_get_features(example_tsds: TSDataset): def test_save_load(example_tsds): transform = ChangePointsTrendTransform(in_column="target") assert_transformation_equals_loaded_original(transform=transform, ts=example_tsds) + + +@pytest.mark.parametrize( + "transform, expected_length", + [ + (ChangePointsTrendTransform(in_column="target"), 2), + ( + ChangePointsTrendTransform( + in_column="target", + change_points_model=RupturesChangePointsModel( + change_points_model=Binseg(model="ar"), + n_bkps=5, + ), + ), + 2, + ), + ( + ChangePointsTrendTransform( + in_column="target", + change_points_model=RupturesChangePointsModel( + change_points_model=Binseg(model="ar"), + n_bkps=10, + ), + ), + 0, + ), + ], +) +def test_params_to_tune(transform, expected_length, example_tsds): + ts = example_tsds + assert len(transform.params_to_tune()) == expected_length + assert_sampling_is_valid(transform=transform, ts=ts) diff --git a/tests/test_transforms/test_decomposition/test_change_points_based/test_level.py b/tests/test_transforms/test_decomposition/test_change_points_based/test_level.py index 334d17171..56423407f 100644 --- a/tests/test_transforms/test_decomposition/test_change_points_based/test_level.py +++ b/tests/test_transforms/test_decomposition/test_change_points_based/test_level.py @@ -9,6 +9,7 @@ from etna.transforms import ChangePointsLevelTransform from etna.transforms.decomposition.change_points_based.change_points_models import RupturesChangePointsModel from etna.transforms.decomposition.change_points_based.per_interval_models import MeanPerIntervalModel +from tests.test_transforms.utils import assert_sampling_is_valid from tests.test_transforms.utils import assert_transformation_equals_loaded_original @@ -70,3 +71,35 @@ def test_save_load(ts_with_local_levels): per_interval_model=MeanPerIntervalModel(), ) assert_transformation_equals_loaded_original(transform=transform, ts=ts_with_local_levels) + + +@pytest.mark.parametrize( + "transform, expected_length", + [ + (ChangePointsLevelTransform(in_column="target"), 2), + ( + ChangePointsLevelTransform( + in_column="target", + change_points_model=RupturesChangePointsModel( + change_points_model=Binseg(model="ar"), + n_bkps=5, + ), + ), + 2, + ), + ( + ChangePointsLevelTransform( + in_column="target", + change_points_model=RupturesChangePointsModel( + change_points_model=Binseg(model="ar"), + n_bkps=10, + ), + ), + 0, + ), + ], +) +def test_params_to_tune(transform, expected_length, ts_with_local_levels): + ts = ts_with_local_levels + assert len(transform.params_to_tune()) == expected_length + assert_sampling_is_valid(transform=transform, ts=ts) diff --git a/tests/test_transforms/test_decomposition/test_change_points_based/test_segmentation.py b/tests/test_transforms/test_decomposition/test_change_points_based/test_segmentation.py index 9cda038ea..4f1045c46 100644 --- a/tests/test_transforms/test_decomposition/test_change_points_based/test_segmentation.py +++ b/tests/test_transforms/test_decomposition/test_change_points_based/test_segmentation.py @@ -11,6 +11,7 @@ from etna.transforms import ChangePointsSegmentationTransform from etna.transforms.decomposition.change_points_based.change_points_models import RupturesChangePointsModel from etna.transforms.decomposition.change_points_based.segmentation import _OneSegmentChangePointsSegmentationTransform +from tests.test_transforms.utils import assert_sampling_is_valid from tests.test_transforms.utils import assert_transformation_equals_loaded_original OUT_COLUMN = "result" @@ -131,3 +132,35 @@ def test_save_load(simple_ar_ts): in_column="target", change_points_model=change_points_model, out_column=OUT_COLUMN ) assert_transformation_equals_loaded_original(transform=transform, ts=simple_ar_ts) + + +@pytest.mark.parametrize( + "transform, expected_length", + [ + (ChangePointsSegmentationTransform(in_column="target"), 2), + ( + ChangePointsSegmentationTransform( + in_column="target", + change_points_model=RupturesChangePointsModel( + change_points_model=Binseg(model="ar"), + n_bkps=5, + ), + ), + 2, + ), + ( + ChangePointsSegmentationTransform( + in_column="target", + change_points_model=RupturesChangePointsModel( + change_points_model=Binseg(model="ar"), + n_bkps=10, + ), + ), + 0, + ), + ], +) +def test_params_to_tune(transform, expected_length, simple_ar_ts): + ts = simple_ar_ts + assert len(transform.params_to_tune()) == expected_length + assert_sampling_is_valid(transform=transform, ts=ts) diff --git a/tests/test_transforms/test_decomposition/test_change_points_based/test_trend.py b/tests/test_transforms/test_decomposition/test_change_points_based/test_trend.py index abd04d317..69576f4a1 100644 --- a/tests/test_transforms/test_decomposition/test_change_points_based/test_trend.py +++ b/tests/test_transforms/test_decomposition/test_change_points_based/test_trend.py @@ -11,6 +11,7 @@ from etna.transforms.decomposition.change_points_based.change_points_models import RupturesChangePointsModel from etna.transforms.decomposition.change_points_based.per_interval_models import SklearnRegressionPerIntervalModel from etna.transforms.decomposition.change_points_based.trend import _OneSegmentTrendTransform +from tests.test_transforms.utils import assert_sampling_is_valid from tests.test_transforms.utils import assert_transformation_equals_loaded_original DEFAULT_SEGMENT = "segment_1" @@ -168,3 +169,35 @@ def test_save_load(example_tsds): change_points_model=RupturesChangePointsModel(change_points_model=Binseg(model="rbf"), n_bkps=5), ) assert_transformation_equals_loaded_original(transform=transform, ts=example_tsds) + + +@pytest.mark.parametrize( + "transform, expected_length", + [ + (TrendTransform(in_column="target"), 2), + ( + TrendTransform( + in_column="target", + change_points_model=RupturesChangePointsModel( + change_points_model=Binseg(model="ar"), + n_bkps=5, + ), + ), + 2, + ), + ( + TrendTransform( + in_column="target", + change_points_model=RupturesChangePointsModel( + change_points_model=Binseg(model="ar"), + n_bkps=10, + ), + ), + 0, + ), + ], +) +def test_params_to_tune(transform, expected_length, example_tsds): + ts = example_tsds + assert len(transform.params_to_tune()) == expected_length + assert_sampling_is_valid(transform=transform, ts=ts) diff --git a/tests/test_transforms/test_decomposition/test_detrend_transform.py b/tests/test_transforms/test_decomposition/test_detrend_transform.py index 3c98d4397..91c8e866b 100644 --- a/tests/test_transforms/test_decomposition/test_detrend_transform.py +++ b/tests/test_transforms/test_decomposition/test_detrend_transform.py @@ -11,6 +11,7 @@ from etna.transforms.decomposition import LinearTrendTransform from etna.transforms.decomposition import TheilSenTrendTransform from etna.transforms.decomposition.detrend import _OneSegmentLinearTrendBaseTransform +from tests.test_transforms.utils import assert_sampling_is_valid from tests.test_transforms.utils import assert_transformation_equals_loaded_original DEFAULT_SEGMENT = "segment_1" @@ -377,3 +378,13 @@ def test_fit_transform_with_nans(transformer, ts_with_nans, decimal): def test_save_load(transform, ts_two_segments_linear): ts = ts_two_segments_linear assert_transformation_equals_loaded_original(transform=transform, ts=ts) + + +@pytest.mark.parametrize( + "transform", + [LinearTrendTransform(in_column="target"), TheilSenTrendTransform(in_column="target")], +) +def test_params_to_tune(transform, ts_two_segments_linear): + ts = ts_two_segments_linear + assert len(transform.params_to_tune()) > 0 + assert_sampling_is_valid(transform=transform, ts=ts) diff --git a/tests/test_transforms/test_decomposition/test_stl_transform.py b/tests/test_transforms/test_decomposition/test_stl_transform.py index 325c5e16d..42ec48d06 100644 --- a/tests/test_transforms/test_decomposition/test_stl_transform.py +++ b/tests/test_transforms/test_decomposition/test_stl_transform.py @@ -6,6 +6,7 @@ from etna.models import NaiveModel from etna.transforms.decomposition import STLTransform from etna.transforms.decomposition.stl import _OneSegmentSTLTransform +from tests.test_transforms.utils import assert_sampling_is_valid from tests.test_transforms.utils import assert_transformation_equals_loaded_original @@ -197,3 +198,10 @@ def test_fit_transform_with_nans_in_middle_raise_error(ts_with_nans): ) def test_save_load(transform, ts_trend_seasonal): assert_transformation_equals_loaded_original(transform=transform, ts=ts_trend_seasonal) + + +def test_params_to_tune(ts_trend_seasonal): + ts = ts_trend_seasonal + transform = STLTransform(in_column="target", period=7) + assert len(transform.params_to_tune()) > 0 + assert_sampling_is_valid(transform=transform, ts=ts)