diff --git a/CHANGELOG.md b/CHANGELOG.md index 40952f1413..db2fd6fb07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ but cannot always guarantee backwards compatibility. Changes that may **break co ### For users of the library: **Improved** -- Improvements to `ARIMA` documentation: Specified possible `p`, `d`, `P`, `D`, `trend` advanced options that are available in statsmodels. More explanations on the behaviour of the parameters were added. [#2142](https://github.com/unit8co/darts/pull/2142) by [MarcBresson](https://github.com/MarcBresson) +- Improvements to `ARIMA` documentation: Specified possible `p`, `d`, `P`, `D`, `trend` advanced options that are available in statsmodels. More explanations on the behaviour of the parameters were added. [#2142](https://github.com/unit8co/darts/pull/2142) by [MarcBresson](https://github.com/MarcBresson). - Improvements to `TimeSeries`: [#2196](https://github.com/unit8co/darts/pull/2196) by [Dennis Bader](https://github.com/dennisbader). - 🚀🚀🚀 Significant performance boosts for several `TimeSeries` methods resulting increased efficiency across the entire `Darts` library. Up to 2x faster creation times for series indexed with "regular" frequencies (e.g. Daily, hourly, ...), and >100x for series indexed with "special" frequencies (e.g. "W-MON", ...). Affects: - All `TimeSeries` creation methods @@ -29,9 +29,16 @@ but cannot always guarantee backwards compatibility. Changes that may **break co - Fixed a bug in `coefficient_of_variation()` with `intersect=True`, where the coefficient was not computed on the intersection. [#2202](https://github.com/unit8co/darts/pull/2202) by [Antoine Madrona](https://github.com/madtoinou). - Fixed a bug in `TimeSeries.append/prepend_values()`, where the components names and the hierarchy were dropped. [#2237](https://github.com/unit8co/darts/pull/2237) by [Antoine Madrona](https://github.com/madtoinou). +**Dependencies** +- Removed upper version cap (<=v2.1.2) for PyTorch Lightning. [#2251](https://github.com/unit8co/darts/pull/2251) by [Dennis Bader](https://github.com/dennisbader). +- Bumped dev dependencies to newest versions: [#2248](https://github.com/unit8co/darts/pull/2248) by [Dennis Bader](https://github.com/dennisbader). + - black[jupyter]: from 22.3.0 to 24.1.1 + - flake8: from 4.0.1 to 7.0.0 + - isort: from 5.11.5 to 5.13.2 + - pyupgrade: 2.31.0 from to v3.15.0 + ### For developers of the library: -- Updated pre-commit hooks to the latest version using `pre-commit autoupdate`. -- Change `pyupgrade` pre-commit hook argument to `--py38-plus`. This allows for [type rewriting](https://github.com/asottile/pyupgrade?tab=readme-ov-file#pep-585-typing-rewrites). +- Updated pre-commit hooks to the latest version using `pre-commit autoupdate`. Change `pyupgrade` pre-commit hook argument to `--py38-plus`. [#2228](https://github.com/unit8co/darts/pull/2248) by [MarcBresson](https://github.com/MarcBresson). ## [0.27.2](https://github.com/unit8co/darts/tree/0.27.2) (2023-01-21) ### For users of the library: diff --git a/darts/tests/explainability/test_tft_explainer.py b/darts/tests/explainability/test_tft_explainer.py index 7b16e88bd5..700d2d2c4e 100644 --- a/darts/tests/explainability/test_tft_explainer.py +++ b/darts/tests/explainability/test_tft_explainer.py @@ -25,6 +25,24 @@ if TORCH_AVAILABLE: + def helper_create_test_cases(series_options: list): + covariates_options = [ + {}, + {"past_covariates"}, + {"future_covariates"}, + {"past_covariates", "future_covariates"}, + ] + relative_index_options = [False, True] + use_encoders_options = [False, True] + return itertools.product( + *[ + series_options, + covariates_options, + relative_index_options, + use_encoders_options, + ] + ) + class TestTFTExplainer: freq = "MS" series_lin_pos = tg.linear_timeseries( @@ -53,289 +71,252 @@ def helper_get_input(self, series_option: str): else: # multiple return self.series_multi, self.pc_multi, self.fc_multi - def helper_create_test_cases(self, series_options: list): - covariates_options = [ - {}, - {"past_covariates"}, - {"future_covariates"}, - {"past_covariates", "future_covariates"}, - ] - relative_index_options = [False, True] - use_encoders_options = [False, True] - return itertools.product( - *[ - series_options, - covariates_options, - relative_index_options, - use_encoders_options, - ] - ) - - def test_explainer_single_univariate_multivariate_series(self): + @pytest.mark.parametrize( + "test_case", helper_create_test_cases(["univariate", "multivariate"]) + ) + def test_explainer_single_univariate_multivariate_series(self, test_case): """Test TFTExplainer with single univariate and multivariate series and a combination of encoders, covariates, and addition of relative index.""" - series_option: str - cov_option: set - add_relative_idx: bool - use_encoders: bool - - series_options = [ - "univariate", - "multivariate", - # "multiple", - ] - test_cases = self.helper_create_test_cases(series_options) - for series_option, cov_option, add_relative_idx, use_encoders in test_cases: - series, pc, fc = self.helper_get_input(series_option) - cov_test_case = dict() - use_pc, use_fc = False, False - if "past_covariates" in cov_option: - cov_test_case["past_covariates"] = pc - use_pc = True - if "future_covariates" in cov_option: - cov_test_case["future_covariates"] = fc - use_fc = True - - # expected number of features for past covs, future covs, and static covs, and encoder/decoder - n_target_expected = series.n_components - n_pc_expected = 1 if "past_covariates" in cov_test_case else 0 - n_fc_expected = 1 if "future_covariates" in cov_test_case else 0 - n_sc_expected = 2 - # encoder is number of past and future covs plus 4 optional encodings (future and past) - # plus 1 univariate target plus 1 optional relative index - n_enc_expected = ( - n_pc_expected - + n_fc_expected - + n_target_expected - + (4 if use_encoders else 0) - + (1 if add_relative_idx else 0) - ) - # encoder is number of future covs plus 2 optional encodings (future) - # plus 1 optional relative index - n_dec_expected = ( - n_fc_expected - + (2 if use_encoders else 0) - + (1 if add_relative_idx else 0) - ) - model = self.helper_create_model( - use_encoders=use_encoders, add_relative_idx=add_relative_idx - ) - # TFTModel requires future covariates - if ( - not add_relative_idx - and "future_covariates" not in cov_test_case - and not use_encoders - ): - with pytest.raises(ValueError): - model.fit(series=series, **cov_test_case) - continue - - model.fit(series=series, **cov_test_case) - explainer = TFTExplainer(model) - explainer2 = TFTExplainer( - model, - background_series=series, - background_past_covariates=pc if use_pc else None, - background_future_covariates=fc if use_fc else None, - ) - assert explainer.background_series == explainer2.background_series + series_option, cov_option, add_relative_idx, use_encoders = test_case + series, pc, fc = self.helper_get_input(series_option) + cov_test_case = dict() + use_pc, use_fc = False, False + if "past_covariates" in cov_option: + cov_test_case["past_covariates"] = pc + use_pc = True + if "future_covariates" in cov_option: + cov_test_case["future_covariates"] = fc + use_fc = True + + # expected number of features for past covs, future covs, and static covs, and encoder/decoder + n_target_expected = series.n_components + n_pc_expected = 1 if "past_covariates" in cov_test_case else 0 + n_fc_expected = 1 if "future_covariates" in cov_test_case else 0 + n_sc_expected = 2 + # encoder is number of past and future covs plus 4 optional encodings (future and past) + # plus 1 univariate target plus 1 optional relative index + n_enc_expected = ( + n_pc_expected + + n_fc_expected + + n_target_expected + + (4 if use_encoders else 0) + + (1 if add_relative_idx else 0) + ) + # encoder is number of future covs plus 2 optional encodings (future) + # plus 1 optional relative index + n_dec_expected = ( + n_fc_expected + + (2 if use_encoders else 0) + + (1 if add_relative_idx else 0) + ) + model = self.helper_create_model( + use_encoders=use_encoders, add_relative_idx=add_relative_idx + ) + # TFTModel requires future covariates + if ( + not add_relative_idx + and "future_covariates" not in cov_test_case + and not use_encoders + ): + with pytest.raises(ValueError): + model.fit(series=series, **cov_test_case) + return + + model.fit(series=series, **cov_test_case) + explainer = TFTExplainer(model) + explainer2 = TFTExplainer( + model, + background_series=series, + background_past_covariates=pc if use_pc else None, + background_future_covariates=fc if use_fc else None, + ) + assert explainer.background_series == explainer2.background_series + assert ( + explainer.background_past_covariates + == explainer2.background_past_covariates + ) + assert ( + explainer.background_future_covariates + == explainer2.background_future_covariates + ) + + assert hasattr(explainer, "model") + assert explainer.background_series[0] == series + if use_pc: + assert explainer.background_past_covariates[0] == pc assert ( - explainer.background_past_covariates - == explainer2.background_past_covariates + explainer.background_past_covariates[0].n_components + == n_pc_expected ) + else: + assert explainer.background_past_covariates is None + if use_fc: + assert explainer.background_future_covariates[0] == fc assert ( - explainer.background_future_covariates - == explainer2.background_future_covariates + explainer.background_future_covariates[0].n_components + == n_fc_expected ) - - assert hasattr(explainer, "model") - assert explainer.background_series[0] == series - if use_pc: - assert explainer.background_past_covariates[0] == pc - assert ( - explainer.background_past_covariates[0].n_components - == n_pc_expected - ) - else: - assert explainer.background_past_covariates is None - if use_fc: - assert explainer.background_future_covariates[0] == fc - assert ( - explainer.background_future_covariates[0].n_components - == n_fc_expected + else: + assert explainer.background_future_covariates is None + result = explainer.explain() + assert isinstance(result, TFTExplainabilityResult) + + enc_imp = result.get_encoder_importance() + dec_imp = result.get_decoder_importance() + stc_imp = result.get_static_covariates_importance() + imps = [enc_imp, dec_imp, stc_imp] + assert all([isinstance(imp, pd.DataFrame) for imp in imps]) + # importances must sum up to 100 percent + assert all( + [imp.squeeze().sum() == pytest.approx(100.0, rel=0.2) for imp in imps] + ) + # importances must have the expected number of columns + assert all( + [ + len(imp.columns) == n + for imp, n in zip( + imps, [n_enc_expected, n_dec_expected, n_sc_expected] ) - else: - assert explainer.background_future_covariates is None - result = explainer.explain() - assert isinstance(result, TFTExplainabilityResult) - - enc_imp = result.get_encoder_importance() - dec_imp = result.get_decoder_importance() - stc_imp = result.get_static_covariates_importance() - imps = [enc_imp, dec_imp, stc_imp] - assert all([isinstance(imp, pd.DataFrame) for imp in imps]) - # importances must sum up to 100 percent - assert all( - [ - imp.squeeze().sum() == pytest.approx(100.0, rel=0.2) - for imp in imps - ] - ) - # importances must have the expected number of columns - assert all( - [ - len(imp.columns) == n - for imp, n in zip( - imps, [n_enc_expected, n_dec_expected, n_sc_expected] - ) - ] - ) + ] + ) - attention = result.get_attention() - assert isinstance(attention, TimeSeries) - # input chunk length + output chunk length = 5 + 2 = 7 - icl, ocl = 5, 2 - freq = series.freq - assert len(attention) == icl + ocl - assert attention.start_time() == series.end_time() - (icl - 1) * freq - assert attention.end_time() == series.end_time() + ocl * freq - assert attention.n_components == ocl - - def test_explainer_multiple_multivariate_series(self): + attention = result.get_attention() + assert isinstance(attention, TimeSeries) + # input chunk length + output chunk length = 5 + 2 = 7 + icl, ocl = 5, 2 + freq = series.freq + assert len(attention) == icl + ocl + assert attention.start_time() == series.end_time() - (icl - 1) * freq + assert attention.end_time() == series.end_time() + ocl * freq + assert attention.n_components == ocl + + @pytest.mark.parametrize("test_case", helper_create_test_cases(["multiple"])) + def test_explainer_multiple_multivariate_series(self, test_case): """Test TFTExplainer with multiple multivaraites series and a combination of encoders, covariates, and addition of relative index.""" - series_option: str - cov_option: set - add_relative_idx: bool - use_encoders: bool - - series_options = ["multiple"] - test_cases = self.helper_create_test_cases(series_options) - for series_option, cov_option, add_relative_idx, use_encoders in test_cases: - series, pc, fc = self.helper_get_input(series_option) - cov_test_case = dict() - use_pc, use_fc = False, False - if "past_covariates" in cov_option: - cov_test_case["past_covariates"] = pc - use_pc = True - if "future_covariates" in cov_option: - cov_test_case["future_covariates"] = fc - use_fc = True - - # expected number of features for past covs, future covs, and static covs, and encoder/decoder - n_target_expected = series[0].n_components - n_pc_expected = 1 if "past_covariates" in cov_test_case else 0 - n_fc_expected = 1 if "future_covariates" in cov_test_case else 0 - n_sc_expected = 2 - # encoder is number of past and future covs plus 4 optional encodings (future and past) - # plus 1 univariate target plus 1 optional relative index - n_enc_expected = ( - n_pc_expected - + n_fc_expected - + n_target_expected - + (4 if use_encoders else 0) - + (1 if add_relative_idx else 0) - ) - # encoder is number of future covs plus 2 optional encodings (future) - # plus 1 optional relative index - n_dec_expected = ( - n_fc_expected - + (2 if use_encoders else 0) - + (1 if add_relative_idx else 0) - ) - model = self.helper_create_model( - use_encoders=use_encoders, add_relative_idx=add_relative_idx - ) - # TFTModel requires future covariates - if ( - not add_relative_idx - and "future_covariates" not in cov_test_case - and not use_encoders - ): - with pytest.raises(ValueError): - model.fit(series=series, **cov_test_case) - continue - - model.fit(series=series, **cov_test_case) - # explainer requires background if model trained on multiple time series + series_option, cov_option, add_relative_idx, use_encoders = test_case + series, pc, fc = self.helper_get_input(series_option) + cov_test_case = dict() + use_pc, use_fc = False, False + if "past_covariates" in cov_option: + cov_test_case["past_covariates"] = pc + use_pc = True + if "future_covariates" in cov_option: + cov_test_case["future_covariates"] = fc + use_fc = True + + # expected number of features for past covs, future covs, and static covs, and encoder/decoder + n_target_expected = series[0].n_components + n_pc_expected = 1 if "past_covariates" in cov_test_case else 0 + n_fc_expected = 1 if "future_covariates" in cov_test_case else 0 + n_sc_expected = 2 + # encoder is number of past and future covs plus 4 optional encodings (future and past) + # plus 1 univariate target plus 1 optional relative index + n_enc_expected = ( + n_pc_expected + + n_fc_expected + + n_target_expected + + (4 if use_encoders else 0) + + (1 if add_relative_idx else 0) + ) + # encoder is number of future covs plus 2 optional encodings (future) + # plus 1 optional relative index + n_dec_expected = ( + n_fc_expected + + (2 if use_encoders else 0) + + (1 if add_relative_idx else 0) + ) + model = self.helper_create_model( + use_encoders=use_encoders, add_relative_idx=add_relative_idx + ) + # TFTModel requires future covariates + if ( + not add_relative_idx + and "future_covariates" not in cov_test_case + and not use_encoders + ): with pytest.raises(ValueError): - explainer = TFTExplainer(model) - explainer = TFTExplainer( - model, - background_series=series, - background_past_covariates=pc if use_pc else None, - background_future_covariates=fc if use_fc else None, - ) - assert hasattr(explainer, "model") - assert explainer.background_series, series - if use_pc: - assert explainer.background_past_covariates == pc - assert ( - explainer.background_past_covariates[0].n_components - == n_pc_expected - ) - else: - assert explainer.background_past_covariates is None - if use_fc: - assert explainer.background_future_covariates == fc - assert ( - explainer.background_future_covariates[0].n_components - == n_fc_expected - ) - else: - assert explainer.background_future_covariates is None - result = explainer.explain() - assert isinstance(result, TFTExplainabilityResult) - - enc_imp = result.get_encoder_importance() - dec_imp = result.get_decoder_importance() - stc_imp = result.get_static_covariates_importance() - imps = [enc_imp, dec_imp, stc_imp] - assert all([isinstance(imp, list) for imp in imps]) - assert all([len(imp) == len(series) for imp in imps]) - assert all( - [isinstance(imp_, pd.DataFrame) for imp in imps for imp_ in imp] - ) - # importances must sum up to 100 percent - assert all( - [ - imp_.squeeze().sum() == pytest.approx(100.0, abs=0.11) - for imp in imps - for imp_ in imp - ] - ) - # importances must have the expected number of columns - assert all( - [ - len(imp_.columns) == n - for imp, n in zip( - imps, [n_enc_expected, n_dec_expected, n_sc_expected] - ) - for imp_ in imp - ] - ) + model.fit(series=series, **cov_test_case) + return - attention = result.get_attention() - assert isinstance(attention, list) - assert len(attention) == len(series) - assert all([isinstance(att, TimeSeries) for att in attention]) - # input chunk length + output chunk length = 5 + 2 = 7 - icl, ocl = 5, 2 - freq = series[0].freq - assert all([len(att) == icl + ocl for att in attention]) - assert all( - [ - att.start_time() == series_.end_time() - (icl - 1) * freq - for att, series_ in zip(attention, series) - ] + model.fit(series=series, **cov_test_case) + # explainer requires background if model trained on multiple time series + with pytest.raises(ValueError): + explainer = TFTExplainer(model) + explainer = TFTExplainer( + model, + background_series=series, + background_past_covariates=pc if use_pc else None, + background_future_covariates=fc if use_fc else None, + ) + assert hasattr(explainer, "model") + assert explainer.background_series, series + if use_pc: + assert explainer.background_past_covariates == pc + assert ( + explainer.background_past_covariates[0].n_components + == n_pc_expected ) - assert all( - [ - att.end_time() == series_.end_time() + ocl * freq - for att, series_ in zip(attention, series) - ] + else: + assert explainer.background_past_covariates is None + if use_fc: + assert explainer.background_future_covariates == fc + assert ( + explainer.background_future_covariates[0].n_components + == n_fc_expected ) - assert all([att.n_components == ocl for att in attention]) + else: + assert explainer.background_future_covariates is None + result = explainer.explain() + assert isinstance(result, TFTExplainabilityResult) + + enc_imp = result.get_encoder_importance() + dec_imp = result.get_decoder_importance() + stc_imp = result.get_static_covariates_importance() + imps = [enc_imp, dec_imp, stc_imp] + assert all([isinstance(imp, list) for imp in imps]) + assert all([len(imp) == len(series) for imp in imps]) + assert all([isinstance(imp_, pd.DataFrame) for imp in imps for imp_ in imp]) + # importances must sum up to 100 percent + assert all( + [ + imp_.squeeze().sum() == pytest.approx(100.0, abs=0.21) + for imp in imps + for imp_ in imp + ] + ) + # importances must have the expected number of columns + assert all( + [ + len(imp_.columns) == n + for imp, n in zip( + imps, [n_enc_expected, n_dec_expected, n_sc_expected] + ) + for imp_ in imp + ] + ) + + attention = result.get_attention() + assert isinstance(attention, list) + assert len(attention) == len(series) + assert all([isinstance(att, TimeSeries) for att in attention]) + # input chunk length + output chunk length = 5 + 2 = 7 + icl, ocl = 5, 2 + freq = series[0].freq + assert all([len(att) == icl + ocl for att in attention]) + assert all( + [ + att.start_time() == series_.end_time() - (icl - 1) * freq + for att, series_ in zip(attention, series) + ] + ) + assert all( + [ + att.end_time() == series_.end_time() + ocl * freq + for att, series_ in zip(attention, series) + ] + ) + assert all([att.n_components == ocl for att in attention]) def test_variable_selection_explanation(self): """Test variable selection (feature importance) explanation results and plotting.""" diff --git a/requirements/torch.txt b/requirements/torch.txt index b38e319e03..617ef86948 100644 --- a/requirements/torch.txt +++ b/requirements/torch.txt @@ -1,3 +1,3 @@ -pytorch-lightning>=1.5.0,<=2.1.2 +pytorch-lightning>=1.5.0 tensorboardX>=2.1 torch>=1.8.0