from sktime.datatypes import get_examples
+
+get_examples(mtype="np.ndarray", as_scitype="Series")[0]array([[ 1. ],
+ [ 4. ],
+ [ 0.5],
+ [-3. ]])
+diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 41c67a4..3a8185b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,7 +16,7 @@ permissions: contents: write # needed for gh-pages jobs: - build-docs: + build-book: name: Build and Deploy Documentation runs-on: ubuntu-latest @@ -45,8 +45,11 @@ jobs: quarto check - name: Render Quarto site - run: | - quarto render book + uses: quarto-dev/quarto-actions/render@v2 + with: + to: html # If set, it will be equivalent to `quarto render --to html` + path: book + # Deploy Preview for PRs - name: Publish PR Preview @@ -54,7 +57,7 @@ jobs: uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs/_site + publish_dir: ./book/_site destination_dir: previews/PR${{ github.event.number }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -65,7 +68,7 @@ jobs: uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs/_site + publish_dir: ./book/_site destination_dir: dev env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -76,7 +79,7 @@ jobs: uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs/_site + publish_dir: ./book/_site destination_dir: ${{ github.ref_name }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -87,7 +90,7 @@ jobs: version="${GITHUB_REF#refs/tags/}" echo "Detected version: $version" mkdir -p ./latest - cp -r ./docs/_site/* ./latest/ + cp -r ./book/_site/* ./latest/ - name: Publish stable release to 'latest' if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') diff --git a/book/.gitignore b/book/.gitignore index 8aec4bf..599a827 100644 --- a/book/.gitignore +++ b/book/.gitignore @@ -2,4 +2,5 @@ _book/ -**/lightning_logs/** \ No newline at end of file +**/lightning_logs/** +**/*.quarto_ipynb diff --git a/book/_quarto.yml b/book/_quarto.yml index 68fb66e..1bb9eb2 100644 --- a/book/_quarto.yml +++ b/book/_quarto.yml @@ -8,7 +8,6 @@ book: favicon: "logo.jpeg" search: true repo-url: https://github.com/sktime/python_brasil_2025/ - repo-actions: [edit] sharing: [twitter, facebook] cover-image: "logo.jpeg" cover-image-alt: "Uma ilustração de séries temporais" diff --git a/book/content/pt/part2/hierarchical_forecasting_files/figure-latex/mermaid-figure-1.png b/book/content/pt/part2/hierarchical_forecasting_files/figure-latex/mermaid-figure-1.png new file mode 100644 index 0000000..1f2188b Binary files /dev/null and b/book/content/pt/part2/hierarchical_forecasting_files/figure-latex/mermaid-figure-1.png differ diff --git "a/book/latest/Previs\303\243o-de-S\303\251ries-temporais-com-Python--um-pequeno-guia.pdf" "b/book/latest/Previs\303\243o-de-S\303\251ries-temporais-com-Python--um-pequeno-guia.pdf" new file mode 100644 index 0000000..cadd5ac Binary files /dev/null and "b/book/latest/Previs\303\243o-de-S\303\251ries-temporais-com-Python--um-pequeno-guia.pdf" differ diff --git a/book/latest/content/pt/extra/sktime_custom.html b/book/latest/content/pt/extra/sktime_custom.html new file mode 100644 index 0000000..e99f61d --- /dev/null +++ b/book/latest/content/pt/extra/sktime_custom.html @@ -0,0 +1,4422 @@ + +
+ + + + + + + +O sktime oferece um ecossistema robusto, mas em cenarios reais frequentemente precisamos ajustar comportamentos, incorporar dados hierarquicos ou adicionar pre-processamentos especificos. Felizmente, o sktime torna relativamente simples a criacao de modelos customizados, desde que sigamos algumas regras.
+Acredito que essa e uma das grandes vantagens da biblioteca: o foco em ser extensivel e customizavel.
+ +Agora, vamos implementar um modelo simples de previsão, o CustomNaiveForecaster, que prevê o valor médio dos últimos n pontos da série temporal.
É um exemplo simples, mas que ilustra bem como criar um forecaster customizado com sktime.
+from tsbook.datasets.retail import SyntheticRetail
+
+dataset = SyntheticRetail("panel")
+y_train, y_test = dataset.load("y_train", "y_test")
+y_train| + | + | sales | +
|---|---|---|
| sku_id | +date | ++ |
| 0 | +2020-01-01 | +0 | +
| 2020-01-02 | +0 | +|
| 2020-01-03 | +0 | +|
| 2020-01-04 | +0 | +|
| 2020-01-05 | +0 | +|
| ... | +... | +... | +
| 24 | +2024-07-01 | +218 | +
| 2024-07-02 | +198 | +|
| 2024-07-03 | +182 | +|
| 2024-07-04 | +210 | +|
| 2024-07-05 | +209 | +
41200 rows × 1 columns
+from sktime.utils.plotting import plot_series
+
+plot_series(
+ y_train.loc[0],
+ y_train.loc[24],
+ labels=[
+ "SKU 0",
+ "SKU 24",
+ ],
+)
Abaixo, implementamos o CustomNaiveForecaster seguindo as regras do sktime (clique para expandir). Em seguida, explicamos passo a passo.
from sktime.forecasting.base import BaseForecaster
+import pandas as pd
+
+
+class CustomNaiveForecaster(BaseForecaster):
+ """
+ A simple naive forecaster
+
+ Parameters
+ ----------
+ n : int
+ Number of past values to use.
+ """
+
+ _tags = {
+ "requires-fh-in-fit": False,
+ "y_inner_mtype": [
+ "pd.Series",
+ ],
+ }
+
+ # Add hyperparameters in init!
+ def __init__(self, n=1):
+ # 1. Set hyper-parameters
+ self.n = n
+
+ # 2. Initialize parent class
+ super().__init__()
+
+ # 3. Check hyper-parameters
+ assert self.n > 0, "n must be greater than 0"
+
+ def _fit(self, y, X, fh):
+ """
+ Fit necessary parameters.
+ """
+
+ self.value_ = y.iloc[-self.n :].mean()
+ return self
+
+ def _predict(self, fh, X):
+ """
+ Use forecasting horizon and optionally X to predict y
+ """
+
+ # During fit, BaseForecaster sets
+ # self.cutoff to the latest cutoff time point
+ index = fh.to_absolute_index(self.cutoff)
+ y_pred = pd.Series(
+ index=index,
+ data=[self.value_ for _ in range(len(index))],
+ )
+ y_pred.name = self._y.name
+
+ return y_pred
+
+ # Veremos mais tarde como usar esse método
+ @classmethod
+ def get_test_params(cls, parameter_set="default"):
+ return [
+ {"n": 1},
+ {"n": 2},
+ ]__init__O método __init__ possui 3 etapas:
super().__init__() para inicializar a classe pai.# Add hyperparameters in init!
+def __init__(self, n=1):
+ # 1. Set hyper-parameters
+ self.n = n
+
+ # 2. Initialize parent class
+ super().__init__()
+
+ # 3. Check hyper-parameters
+ assert self.n > 0, "n must be greater than 0"No caso de algum preprocessamento dos hiperparâmetros no __init__, devemos guardar em uma variável com nome diferente do hiperparâmetro. Por exemplo, se tivéssemos interesse em ter um atributo n diferente do passado no __init__, poderíamos fazer:
self._n = n + 1
+O self.n funciona como uma digital do modelo, e deve ser exatamente o que foi passado no __init__.
_fitNo método _fit, devemos implementar a lógica de ajuste do modelo. No nosso caso, calculamos a média dos últimos n valores e armazenamos em self.value_.
O _ após o nome do atributo indica que é um atributo aprendido durante o ajuste, e será retornado quando chamarmos get_fitted_params().
Note que podemos supor que y é do tipo definido na tag y_inner_mtype, ou seja, uma pd.Series.
_predictNo método _predict, implementamos a lógica de previsão. Usamos o horizonte de previsão fh para determinar os índices futuros e retornamos uma série com o valor previsto para cada ponto no horizonte.
O fh é um objeto do tipo ForecastingHorizon, que possui o método to_absolute_index(cutoff) para converter o horizonte relativo em índices absolutos, considerando o último ponto conhecido (self.cutoff).
Retornamos um pd.Series com os índices e os valores previstos.
CustomNaiveForecasterAgora, já podemos usar o nosso modelo customizado para fazer previsões.
+custom_naive_model = CustomNaiveForecaster()
+custom_naive_model.fit(y_train)CustomNaiveForecaster()Please rerun this cell to show the HTML repr or trust the notebook.
CustomNaiveForecaster()
Como passamos um dado hierárquico, o sktime converteu automaticamente para pd.Series, que é o mtype suportado pelo nosso modelo. Os modelos internos, para cada série, ficam disponíveis em forecasters_.
custom_naive_model.forecasters_| + | forecasters | +
|---|---|
| 0 | +CustomNaiveForecaster() | +
| 1 | +CustomNaiveForecaster() | +
| 2 | +CustomNaiveForecaster() | +
| 3 | +CustomNaiveForecaster() | +
| 4 | +CustomNaiveForecaster() | +
| 5 | +CustomNaiveForecaster() | +
| 6 | +CustomNaiveForecaster() | +
| 7 | +CustomNaiveForecaster() | +
| 8 | +CustomNaiveForecaster() | +
| 9 | +CustomNaiveForecaster() | +
| 10 | +CustomNaiveForecaster() | +
| 11 | +CustomNaiveForecaster() | +
| 12 | +CustomNaiveForecaster() | +
| 13 | +CustomNaiveForecaster() | +
| 14 | +CustomNaiveForecaster() | +
| 15 | +CustomNaiveForecaster() | +
| 16 | +CustomNaiveForecaster() | +
| 17 | +CustomNaiveForecaster() | +
| 18 | +CustomNaiveForecaster() | +
| 19 | +CustomNaiveForecaster() | +
| 20 | +CustomNaiveForecaster() | +
| 21 | +CustomNaiveForecaster() | +
| 22 | +CustomNaiveForecaster() | +
| 23 | +CustomNaiveForecaster() | +
| 24 | +CustomNaiveForecaster() | +
y_pred = custom_naive_model.predict(fh=y_test.index.get_level_values(-1).unique())
+
+fig, _ = plot_series(
+ y_train.loc[0],
+ y_pred.loc[0],
+ labels=[
+ "SKU 0",
+ "Previsão SKU 0",
+ ],
+)
+fig.show()
O sktime também fornece uma funcionalidade que traz testes unitários prontos para validar se o modelo customizado está funcionando corretamente.
+Ele usa os hiperparâmetros retornados pelo método get_test_params para criar instâncias do modelo e executar uma série de testes.
from sktime.utils.estimator_checks import check_estimator
+
+
+check_estimator(CustomNaiveForecaster, tests_to_exclude=["test_doctest_examples"])All tests PASSED!
+{'test_constructor[CustomNaiveForecaster]': 'PASSED',
+ 'test_inheritance[CustomNaiveForecaster]': 'PASSED',
+ 'test_repr_html[CustomNaiveForecaster-0]': 'PASSED',
+ 'test_repr_html[CustomNaiveForecaster-1]': 'PASSED',
+ 'test_clone[CustomNaiveForecaster-0]': 'PASSED',
+ 'test_clone[CustomNaiveForecaster-1]': 'PASSED',
+ 'test_random_tags[CustomNaiveForecaster]': 'PASSED',
+ 'test_get_test_params[CustomNaiveForecaster]': 'PASSED',
+ 'test_get_params[CustomNaiveForecaster-0]': 'PASSED',
+ 'test_get_params[CustomNaiveForecaster-1]': 'PASSED',
+ 'test_create_test_instances_and_names[CustomNaiveForecaster]': 'PASSED',
+ 'test_valid_estimator_class_tags[CustomNaiveForecaster]': 'PASSED',
+ 'test_estimator_tags[CustomNaiveForecaster]': 'PASSED',
+ 'test_repr[CustomNaiveForecaster-0]': 'PASSED',
+ 'test_repr[CustomNaiveForecaster-1]': 'PASSED',
+ 'test_create_test_instance[CustomNaiveForecaster]': 'PASSED',
+ 'test_no_cross_test_side_effects_part2[CustomNaiveForecaster-0]': 'PASSED',
+ 'test_no_cross_test_side_effects_part2[CustomNaiveForecaster-1]': 'PASSED',
+ 'test_get_test_params_coverage[CustomNaiveForecaster]': 'PASSED',
+ 'test_has_common_interface[CustomNaiveForecaster]': 'PASSED',
+ 'test_no_between_test_case_side_effects[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX-0]': 'PASSED',
+ 'test_no_between_test_case_side_effects[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX-1]': 'PASSED',
+ 'test_no_between_test_case_side_effects[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX-0]': 'PASSED',
+ 'test_no_between_test_case_side_effects[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX-1]': 'PASSED',
+ 'test_no_between_test_case_side_effects[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX-0]': 'PASSED',
+ 'test_no_between_test_case_side_effects[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX-1]': 'PASSED',
+ 'test_no_between_test_case_side_effects[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX-0]': 'PASSED',
+ 'test_no_between_test_case_side_effects[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX-1]': 'PASSED',
+ 'test_set_params_sklearn[CustomNaiveForecaster]': 'PASSED',
+ 'test_obj_vs_cls_signature[CustomNaiveForecaster]': 'PASSED',
+ 'test_no_cross_test_side_effects_part1[CustomNaiveForecaster-0]': 'PASSED',
+ 'test_no_cross_test_side_effects_part1[CustomNaiveForecaster-1]': 'PASSED',
+ 'test_valid_estimator_tags[CustomNaiveForecaster-0]': 'PASSED',
+ 'test_valid_estimator_tags[CustomNaiveForecaster-1]': 'PASSED',
+ 'test_set_params[CustomNaiveForecaster-0]': 'PASSED',
+ 'test_set_params[CustomNaiveForecaster-1]': 'PASSED',
+ 'test_raises_not_fitted_error[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX-predict]': 'PASSED',
+ 'test_raises_not_fitted_error[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX-get_fitted_params]': 'PASSED',
+ 'test_raises_not_fitted_error[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX-predict]': 'PASSED',
+ 'test_raises_not_fitted_error[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX-get_fitted_params]': 'PASSED',
+ 'test_raises_not_fitted_error[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX-predict]': 'PASSED',
+ 'test_raises_not_fitted_error[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX-get_fitted_params]': 'PASSED',
+ 'test_raises_not_fitted_error[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX-predict]': 'PASSED',
+ 'test_raises_not_fitted_error[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX-get_fitted_params]': 'PASSED',
+ 'test_persistence_via_pickle[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX-predict]': 'PASSED',
+ 'test_persistence_via_pickle[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX-predict]': 'PASSED',
+ 'test_persistence_via_pickle[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX-predict]': 'PASSED',
+ 'test_persistence_via_pickle[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX-predict]': 'PASSED',
+ 'test_fit_idempotent[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX-predict]': 'PASSED',
+ 'test_fit_idempotent[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX-predict]': 'PASSED',
+ 'test_fit_idempotent[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX-predict]': 'PASSED',
+ 'test_fit_idempotent[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX-predict]': 'PASSED',
+ 'test_non_state_changing_method_contract[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX-predict]': 'PASSED',
+ 'test_non_state_changing_method_contract[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX-get_fitted_params]': 'PASSED',
+ 'test_non_state_changing_method_contract[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX-predict]': 'PASSED',
+ 'test_non_state_changing_method_contract[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX-get_fitted_params]': 'PASSED',
+ 'test_non_state_changing_method_contract[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX-predict]': 'PASSED',
+ 'test_non_state_changing_method_contract[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX-get_fitted_params]': 'PASSED',
+ 'test_non_state_changing_method_contract[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX-predict]': 'PASSED',
+ 'test_non_state_changing_method_contract[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX-get_fitted_params]': 'PASSED',
+ 'test_methods_have_no_side_effects[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX-predict]': 'PASSED',
+ 'test_methods_have_no_side_effects[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX-get_fitted_params]': 'PASSED',
+ 'test_methods_have_no_side_effects[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX-predict]': 'PASSED',
+ 'test_methods_have_no_side_effects[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX-get_fitted_params]': 'PASSED',
+ 'test_methods_have_no_side_effects[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX-predict]': 'PASSED',
+ 'test_methods_have_no_side_effects[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX-get_fitted_params]': 'PASSED',
+ 'test_methods_have_no_side_effects[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX-predict]': 'PASSED',
+ 'test_methods_have_no_side_effects[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX-get_fitted_params]': 'PASSED',
+ 'test_fit_returns_self[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX]': 'PASSED',
+ 'test_fit_returns_self[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX]': 'PASSED',
+ 'test_fit_returns_self[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX]': 'PASSED',
+ 'test_fit_returns_self[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX]': 'PASSED',
+ 'test_dl_constructor_initializes_deeply[CustomNaiveForecaster]': 'PASSED',
+ 'test_save_estimators_to_file[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX-predict]': 'PASSED',
+ 'test_save_estimators_to_file[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX-predict]': 'PASSED',
+ 'test_save_estimators_to_file[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX-predict]': 'PASSED',
+ 'test_save_estimators_to_file[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX-predict]': 'PASSED',
+ 'test_fit_updates_state[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX]': 'PASSED',
+ 'test_fit_updates_state[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX]': 'PASSED',
+ 'test_fit_updates_state[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX]': 'PASSED',
+ 'test_fit_updates_state[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX]': 'PASSED',
+ 'test_multiprocessing_idempotent[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX-predict]': 'PASSED',
+ 'test_multiprocessing_idempotent[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX-predict]': 'PASSED',
+ 'test_multiprocessing_idempotent[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX-predict]': 'PASSED',
+ 'test_multiprocessing_idempotent[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX-predict]': 'PASSED',
+ 'test_fit_does_not_overwrite_hyper_params[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX]': 'PASSED',
+ 'test_fit_does_not_overwrite_hyper_params[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX]': 'PASSED',
+ 'test_fit_does_not_overwrite_hyper_params[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX]': 'PASSED',
+ 'test_fit_does_not_overwrite_hyper_params[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX]': 'PASSED',
+ 'test_fh_not_passed_error_handling[CustomNaiveForecaster-0-y:1cols]': 'PASSED',
+ 'test_fh_not_passed_error_handling[CustomNaiveForecaster-0-y:2cols]': 'PASSED',
+ 'test_fh_not_passed_error_handling[CustomNaiveForecaster-1-y:1cols]': 'PASSED',
+ 'test_fh_not_passed_error_handling[CustomNaiveForecaster-1-y:2cols]': 'PASSED',
+ 'test_raises_not_fitted_error[CustomNaiveForecaster-0]': 'PASSED',
+ 'test_raises_not_fitted_error[CustomNaiveForecaster-1]': 'PASSED',
+ 'test_predict_series_name_preserved[CustomNaiveForecaster-0]': 'PASSED',
+ 'test_predict_series_name_preserved[CustomNaiveForecaster-1]': 'PASSED',
+ 'test_different_fh_in_fit_and_predict_error_handling[CustomNaiveForecaster-0-y:1cols]': 'PASSED',
+ 'test_different_fh_in_fit_and_predict_error_handling[CustomNaiveForecaster-0-y:2cols]': 'PASSED',
+ 'test_different_fh_in_fit_and_predict_error_handling[CustomNaiveForecaster-1-y:1cols]': 'PASSED',
+ 'test_different_fh_in_fit_and_predict_error_handling[CustomNaiveForecaster-1-y:2cols]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-0-y:1cols-fh=1-alpha=0.05]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-0-y:1cols-fh=1-alpha=0.1]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-0-y:1cols-fh=1-alpha=[0.25, 0.75]]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-alpha=0.05]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-alpha=0.1]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-alpha=[0.25, 0.75]]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-0-y:2cols-fh=1-alpha=0.05]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-0-y:2cols-fh=1-alpha=0.1]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-0-y:2cols-fh=1-alpha=[0.25, 0.75]]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-alpha=0.05]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-alpha=0.1]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-alpha=[0.25, 0.75]]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-1-y:1cols-fh=1-alpha=0.05]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-1-y:1cols-fh=1-alpha=0.1]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-1-y:1cols-fh=1-alpha=[0.25, 0.75]]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-alpha=0.05]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-alpha=0.1]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-alpha=[0.25, 0.75]]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-1-y:2cols-fh=1-alpha=0.05]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-1-y:2cols-fh=1-alpha=0.1]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-1-y:2cols-fh=1-alpha=[0.25, 0.75]]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-alpha=0.05]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-alpha=0.1]': 'PASSED',
+ 'test_predict_quantiles[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-alpha=[0.25, 0.75]]': 'PASSED',
+ 'test_fit_predict[CustomNaiveForecaster-0-y:1cols]': 'PASSED',
+ 'test_fit_predict[CustomNaiveForecaster-0-y:2cols]': 'PASSED',
+ 'test_fit_predict[CustomNaiveForecaster-1-y:1cols]': 'PASSED',
+ 'test_fit_predict[CustomNaiveForecaster-1-y:2cols]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:1cols-update_params=True-0-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:1cols-update_params=True-0-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:1cols-update_params=True-1-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:1cols-update_params=True-1-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:1cols-update_params=False-step=1-0-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:1cols-update_params=False-step=1-0-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:1cols-update_params=False-step=1-1-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:1cols-update_params=False-step=1-1-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:1cols-update_params=False-step=5-0-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:1cols-update_params=False-step=5-0-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:1cols-update_params=False-step=5-1-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:1cols-update_params=False-step=5-1-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:2cols-update_params=True-0-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:2cols-update_params=True-0-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:2cols-update_params=True-1-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:2cols-update_params=True-1-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:2cols-update_params=False-step=1-0-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:2cols-update_params=False-step=1-0-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:2cols-update_params=False-step=1-1-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:2cols-update_params=False-step=1-1-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:2cols-update_params=False-step=5-0-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:2cols-update_params=False-step=5-0-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:2cols-update_params=False-step=5-1-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-0-y:2cols-update_params=False-step=5-1-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:1cols-update_params=True-0-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:1cols-update_params=True-0-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:1cols-update_params=True-1-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:1cols-update_params=True-1-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:1cols-update_params=False-step=1-0-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:1cols-update_params=False-step=1-0-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:1cols-update_params=False-step=1-1-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:1cols-update_params=False-step=1-1-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:1cols-update_params=False-step=5-0-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:1cols-update_params=False-step=5-0-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:1cols-update_params=False-step=5-1-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:1cols-update_params=False-step=5-1-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:2cols-update_params=True-0-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:2cols-update_params=True-0-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:2cols-update_params=True-1-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:2cols-update_params=True-1-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:2cols-update_params=False-step=1-0-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:2cols-update_params=False-step=1-0-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:2cols-update_params=False-step=1-1-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:2cols-update_params=False-step=1-1-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:2cols-update_params=False-step=5-0-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:2cols-update_params=False-step=5-0-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:2cols-update_params=False-step=5-1-fh=1]': 'PASSED',
+ 'test_update_predict_predicted_index[CustomNaiveForecaster-1-y:2cols-update_params=False-step=5-1-fh=[2 5]]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=1-int-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=1-int-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=1-range-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=1-range-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=1-period-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=1-period-period-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=1-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=1-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=1-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-int-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-int-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-range-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-range-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-period-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-period-period-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=1-int-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=1-int-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=1-range-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=1-range-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=1-period-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=1-period-period-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=1-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=1-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=1-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-int-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-int-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-range-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-range-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-period-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-period-period-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=1-int-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=1-int-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=1-range-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=1-range-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=1-period-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=1-period-period-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=1-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=1-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=1-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-int-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-int-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-range-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-range-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-period-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-period-period-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=1-int-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=1-int-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=1-range-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=1-range-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=1-period-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=1-period-period-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=1-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=1-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=1-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-int-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-int-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-range-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-range-int-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-period-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-period-period-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index_with_X[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-datetime-timedelta-True]': 'PASSED',
+ 'test_y_multivariate_raises_error[CustomNaiveForecaster-0]': 'PASSED',
+ 'test_y_multivariate_raises_error[CustomNaiveForecaster-1]': 'PASSED',
+ 'test_score[CustomNaiveForecaster-0-y:1cols-fh=1]': 'PASSED',
+ 'test_score[CustomNaiveForecaster-0-y:1cols-fh=[2 5]]': 'PASSED',
+ 'test_score[CustomNaiveForecaster-0-y:2cols-fh=1]': 'PASSED',
+ 'test_score[CustomNaiveForecaster-0-y:2cols-fh=[2 5]]': 'PASSED',
+ 'test_score[CustomNaiveForecaster-1-y:1cols-fh=1]': 'PASSED',
+ 'test_score[CustomNaiveForecaster-1-y:1cols-fh=[2 5]]': 'PASSED',
+ 'test_score[CustomNaiveForecaster-1-y:2cols-fh=1]': 'PASSED',
+ 'test_score[CustomNaiveForecaster-1-y:2cols-fh=[2 5]]': 'PASSED',
+ 'test_X_invalid_type_raises_error[CustomNaiveForecaster-0-y:1cols-0]': 'PASSED',
+ 'test_X_invalid_type_raises_error[CustomNaiveForecaster-0-y:1cols-1]': 'PASSED',
+ 'test_X_invalid_type_raises_error[CustomNaiveForecaster-0-y:2cols-0]': 'PASSED',
+ 'test_X_invalid_type_raises_error[CustomNaiveForecaster-0-y:2cols-1]': 'PASSED',
+ 'test_X_invalid_type_raises_error[CustomNaiveForecaster-1-y:1cols-0]': 'PASSED',
+ 'test_X_invalid_type_raises_error[CustomNaiveForecaster-1-y:1cols-1]': 'PASSED',
+ 'test_X_invalid_type_raises_error[CustomNaiveForecaster-1-y:2cols-0]': 'PASSED',
+ 'test_X_invalid_type_raises_error[CustomNaiveForecaster-1-y:2cols-1]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-0-y:1cols-update_params=True-fh=1]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-0-y:1cols-update_params=True-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-0-y:1cols-update_params=False-fh=1]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-0-y:1cols-update_params=False-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-0-y:2cols-update_params=True-fh=1]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-0-y:2cols-update_params=True-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-0-y:2cols-update_params=False-fh=1]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-0-y:2cols-update_params=False-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-1-y:1cols-update_params=True-fh=1]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-1-y:1cols-update_params=True-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-1-y:1cols-update_params=False-fh=1]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-1-y:1cols-update_params=False-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-1-y:2cols-update_params=True-fh=1]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-1-y:2cols-update_params=True-fh=[2 5]]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-1-y:2cols-update_params=False-fh=1]': 'PASSED',
+ 'test_update_predict_single[CustomNaiveForecaster-1-y:2cols-update_params=False-fh=[2 5]]': 'PASSED',
+ 'test_get_fitted_params[CustomNaiveForecaster-0-ForecasterFitPredictUnivariateWithX]': 'PASSED',
+ 'test_get_fitted_params[CustomNaiveForecaster-0-ForecasterFitPredictMultivariateNoX]': 'PASSED',
+ 'test_get_fitted_params[CustomNaiveForecaster-1-ForecasterFitPredictUnivariateWithX]': 'PASSED',
+ 'test_get_fitted_params[CustomNaiveForecaster-1-ForecasterFitPredictMultivariateNoX]': 'PASSED',
+ 'test_hierarchical_with_exogeneous[CustomNaiveForecaster-0-y:1cols]': 'PASSED',
+ 'test_hierarchical_with_exogeneous[CustomNaiveForecaster-0-y:2cols]': 'PASSED',
+ 'test_hierarchical_with_exogeneous[CustomNaiveForecaster-1-y:1cols]': 'PASSED',
+ 'test_hierarchical_with_exogeneous[CustomNaiveForecaster-1-y:2cols]': 'PASSED',
+ 'test_pred_int_tag[CustomNaiveForecaster-0]': 'PASSED',
+ 'test_pred_int_tag[CustomNaiveForecaster-1]': 'PASSED',
+ 'test__y_when_refitting[CustomNaiveForecaster-0-y:1cols]': 'PASSED',
+ 'test__y_when_refitting[CustomNaiveForecaster-0-y:2cols]': 'PASSED',
+ 'test__y_when_refitting[CustomNaiveForecaster-1-y:1cols]': 'PASSED',
+ 'test__y_when_refitting[CustomNaiveForecaster-1-y:2cols]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:1cols-fh=1-alpha=0.05-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:1cols-fh=1-alpha=0.05-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:1cols-fh=1-alpha=0.1-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:1cols-fh=1-alpha=0.1-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:1cols-fh=1-alpha=[0.25, 0.75]-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:1cols-fh=1-alpha=[0.25, 0.75]-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-alpha=0.05-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-alpha=0.05-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-alpha=0.1-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-alpha=0.1-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-alpha=[0.25, 0.75]-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-alpha=[0.25, 0.75]-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:2cols-fh=1-alpha=0.05-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:2cols-fh=1-alpha=0.05-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:2cols-fh=1-alpha=0.1-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:2cols-fh=1-alpha=0.1-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:2cols-fh=1-alpha=[0.25, 0.75]-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:2cols-fh=1-alpha=[0.25, 0.75]-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-alpha=0.05-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-alpha=0.05-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-alpha=0.1-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-alpha=0.1-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-alpha=[0.25, 0.75]-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-alpha=[0.25, 0.75]-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:1cols-fh=1-alpha=0.05-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:1cols-fh=1-alpha=0.05-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:1cols-fh=1-alpha=0.1-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:1cols-fh=1-alpha=0.1-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:1cols-fh=1-alpha=[0.25, 0.75]-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:1cols-fh=1-alpha=[0.25, 0.75]-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-alpha=0.05-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-alpha=0.05-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-alpha=0.1-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-alpha=0.1-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-alpha=[0.25, 0.75]-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-alpha=[0.25, 0.75]-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:2cols-fh=1-alpha=0.05-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:2cols-fh=1-alpha=0.05-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:2cols-fh=1-alpha=0.1-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:2cols-fh=1-alpha=0.1-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:2cols-fh=1-alpha=[0.25, 0.75]-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:2cols-fh=1-alpha=[0.25, 0.75]-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-alpha=0.05-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-alpha=0.05-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-alpha=0.1-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-alpha=0.1-1]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-alpha=[0.25, 0.75]-0]': 'PASSED',
+ 'test_predict_interval[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-alpha=[0.25, 0.75]-1]': 'PASSED',
+ 'test_update_with_exogenous_variables[CustomNaiveForecaster-0-y:1cols-update_params=True]': 'PASSED',
+ 'test_update_with_exogenous_variables[CustomNaiveForecaster-0-y:1cols-update_params=False]': 'PASSED',
+ 'test_update_with_exogenous_variables[CustomNaiveForecaster-0-y:2cols-update_params=True]': 'PASSED',
+ 'test_update_with_exogenous_variables[CustomNaiveForecaster-0-y:2cols-update_params=False]': 'PASSED',
+ 'test_update_with_exogenous_variables[CustomNaiveForecaster-1-y:1cols-update_params=True]': 'PASSED',
+ 'test_update_with_exogenous_variables[CustomNaiveForecaster-1-y:1cols-update_params=False]': 'PASSED',
+ 'test_update_with_exogenous_variables[CustomNaiveForecaster-1-y:2cols-update_params=True]': 'PASSED',
+ 'test_update_with_exogenous_variables[CustomNaiveForecaster-1-y:2cols-update_params=False]': 'PASSED',
+ 'test_fh_attribute[CustomNaiveForecaster-0-y:1cols]': 'PASSED',
+ 'test_fh_attribute[CustomNaiveForecaster-0-y:2cols]': 'PASSED',
+ 'test_fh_attribute[CustomNaiveForecaster-1-y:1cols]': 'PASSED',
+ 'test_fh_attribute[CustomNaiveForecaster-1-y:2cols]': 'PASSED',
+ 'test_categorical_y_raises_error[CustomNaiveForecaster-0]': 'PASSED',
+ 'test_categorical_y_raises_error[CustomNaiveForecaster-1]': 'PASSED',
+ 'test_y_invalid_type_raises_error[CustomNaiveForecaster-0-0]': 'PASSED',
+ 'test_y_invalid_type_raises_error[CustomNaiveForecaster-0-1]': 'PASSED',
+ 'test_y_invalid_type_raises_error[CustomNaiveForecaster-1-0]': 'PASSED',
+ 'test_y_invalid_type_raises_error[CustomNaiveForecaster-1-1]': 'PASSED',
+ 'test_predict_proba[CustomNaiveForecaster-0-y:1cols-fh=1]': 'PASSED',
+ 'test_predict_proba[CustomNaiveForecaster-0-y:1cols-fh=[2 5]]': 'PASSED',
+ 'test_predict_proba[CustomNaiveForecaster-0-y:2cols-fh=1]': 'PASSED',
+ 'test_predict_proba[CustomNaiveForecaster-0-y:2cols-fh=[2 5]]': 'PASSED',
+ 'test_predict_proba[CustomNaiveForecaster-1-y:1cols-fh=1]': 'PASSED',
+ 'test_predict_proba[CustomNaiveForecaster-1-y:1cols-fh=[2 5]]': 'PASSED',
+ 'test_predict_proba[CustomNaiveForecaster-1-y:2cols-fh=1]': 'PASSED',
+ 'test_predict_proba[CustomNaiveForecaster-1-y:2cols-fh=[2 5]]': 'PASSED',
+ 'test__y_and_cutoff[CustomNaiveForecaster-0-y:1cols]': 'PASSED',
+ 'test__y_and_cutoff[CustomNaiveForecaster-0-y:2cols]': 'PASSED',
+ 'test__y_and_cutoff[CustomNaiveForecaster-1-y:1cols]': 'PASSED',
+ 'test__y_and_cutoff[CustomNaiveForecaster-1-y:2cols]': 'PASSED',
+ 'test_categorical_X_raises_error[CustomNaiveForecaster-0]': 'PASSED',
+ 'test_categorical_X_raises_error[CustomNaiveForecaster-1]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=1-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=1-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=1-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=1-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=1-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=1-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=1-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=1-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=1-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[2 5]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=-3-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=-3-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=-3-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=-3-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=-3-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=-3-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=-3-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=-3-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=-3-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-2 -5]-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-2 -5]-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-2 -5]-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-2 -5]-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-2 -5]-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-2 -5]-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-2 -5]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-2 -5]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-2 -5]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=0-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=0-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=0-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=0-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=0-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=0-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=0-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=0-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=0-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-3 2]-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-3 2]-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-3 2]-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-3 2]-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-3 2]-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-3 2]-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-3 2]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-3 2]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:1cols-fh=[-3 2]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=1-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=1-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=1-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=1-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=1-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=1-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=1-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=1-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=1-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[2 5]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=-3-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=-3-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=-3-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=-3-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=-3-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=-3-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=-3-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=-3-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=-3-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-2 -5]-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-2 -5]-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-2 -5]-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-2 -5]-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-2 -5]-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-2 -5]-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-2 -5]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-2 -5]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-2 -5]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=0-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=0-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=0-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=0-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=0-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=0-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=0-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=0-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=0-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-3 2]-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-3 2]-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-3 2]-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-3 2]-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-3 2]-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-3 2]-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-3 2]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-3 2]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-0-y:2cols-fh=[-3 2]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=1-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=1-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=1-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=1-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=1-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=1-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=1-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=1-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=1-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[2 5]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=-3-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=-3-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=-3-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=-3-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=-3-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=-3-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=-3-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=-3-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=-3-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-2 -5]-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-2 -5]-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-2 -5]-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-2 -5]-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-2 -5]-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-2 -5]-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-2 -5]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-2 -5]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-2 -5]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=0-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=0-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=0-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=0-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=0-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=0-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=0-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=0-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=0-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-3 2]-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-3 2]-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-3 2]-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-3 2]-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-3 2]-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-3 2]-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-3 2]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-3 2]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:1cols-fh=[-3 2]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=1-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=1-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=1-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=1-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=1-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=1-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=1-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=1-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=1-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[2 5]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=-3-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=-3-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=-3-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=-3-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=-3-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=-3-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=-3-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=-3-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=-3-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-2 -5]-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-2 -5]-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-2 -5]-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-2 -5]-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-2 -5]-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-2 -5]-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-2 -5]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-2 -5]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-2 -5]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=0-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=0-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=0-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=0-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=0-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=0-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=0-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=0-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=0-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-3 2]-int-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-3 2]-int-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-3 2]-range-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-3 2]-range-int-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-3 2]-period-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-3 2]-period-period-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-3 2]-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-3 2]-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index[CustomNaiveForecaster-1-y:2cols-fh=[-3 2]-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:1cols-int-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:1cols-int-int-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:1cols-range-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:1cols-range-int-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:1cols-period-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:1cols-period-period-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:1cols-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:1cols-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:1cols-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:2cols-int-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:2cols-int-int-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:2cols-range-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:2cols-range-int-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:2cols-period-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:2cols-period-period-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:2cols-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:2cols-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-0-y:2cols-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:1cols-int-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:1cols-int-int-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:1cols-range-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:1cols-range-int-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:1cols-period-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:1cols-period-period-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:1cols-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:1cols-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:1cols-datetime-timedelta-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:2cols-int-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:2cols-int-int-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:2cols-range-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:2cols-range-int-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:2cols-period-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:2cols-period-period-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:2cols-datetime-int-True]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:2cols-datetime-datetime-False]': 'PASSED',
+ 'test_predict_time_index_in_sample_full[CustomNaiveForecaster-1-y:2cols-datetime-timedelta-True]': 'PASSED'}
+Nesse capítulo, vamos começar a usar um dataset mais realista, com dados simulando vendas diárias de uma empresa de varejo.
+Vamos aprender os seguintes pontos:
+sktimePara acessar os dados, vamos usar a classe SyntheticRetail da biblioteca tsbook, que contém dados simulados de vendas diárias de uma empresa de varejo.
from tsbook.datasets.retail import SyntheticRetail
+from sktime.utils.plotting import plot_series
+
+dataset = SyntheticRetail("univariate")
+y_train, y_test = dataset.load("y_train", "y_test")
+
+plot_series(y_train, y_test, labels=["Treino", "Teste"])
Os dados são diários, e vemos que sempre positivos. Também notamos que existe alguma sazonalidade, aparentemente algo mensal e anual, que aumenta de magnitude ao longo do tempo.
+Algo que deve chamar a atenção nesse gráfico é que a magnitude da série temporal está aumentando ao longo do tempo. Isso não deve passar desapercebido, pois é um ponto importante para entendermos o que vem a seguir.
+É interessante entender o quanto de informação o passado de uma série temporal carrega sobre o seu futuro. Uma maneira de capturar essa relação (considerando variações lineares) é através da auto-correlação. A autocorrelação para um determinado lag \(k\) é definida como:
+\[ +\text{Corr}(Y_t,Y_{t-k})=\frac{\text{Cov}(Y_t,Y_{t-k})}{\sqrt{\text{Var}(Y_t)\text{Var}(Y_{t-k})}} = \frac{E[(Y_t - \mu)(Y_{t-k} - \mu)]}{\sqrt{\text{Var}(Y_t)\text{Var}(Y_{t-k})}} +\]
+Em outras palavras, quando o valor em \(k\) observações atrás está acima (ou abaixo) da média, o valor atual também tende a estar acima (ou abaixo) da média?
+Com plot_correlations, podemos visualizar algumas informações úteis:
from sktime.utils.plotting import plot_correlations
+
+
+fig, ax = plot_correlations(y_train, lags=60)
+fig.show()/var/folders/_2/9y4tsvdd2n3gqjgd2zmlr1km0000gn/T/ipykernel_24222/2914460266.py:5: UserWarning: FigureCanvasAgg is non-interactive, and thus cannot be shown
+ fig.show()
+
No gráfico de autocorrelação, vemos algumas características interessantes:
+É um erro comum usar a correlação de lags de uma série para seleção de variáveis (lags). Sempre que possível, devemos eliminar a tendência antes de analisar a autocorrelação.
+Veremos que esses padrões são indicativos de que a série temporal possui componentes importantes, e que valores passados dizem muito sobre valores futuros.
+Séries temporais podem ser decompostas em 3 componentes principais:
+Uma série aditiva pode ser representada como:
+\[ +Y(t) = T(t) + S(t) + R(t) +\]
+onde \(T(t)\) é a tendência, \(S(t)\) é a sazonalidade, e \(R(t)\) é o ruído.
+Em séries aditivas, o impacto da sazonalidade se dá em termos absolutos, dizemos: “em janeiro, as vendas aumentam em 100 unidades com relação a média do ano”.
+Mas também existem séries multiplicativas, onde os componentes interagem de forma diferente:
+\[ +Y(t) = T(t) \cdot S(t) \cdot R(t) +\]
+Nessas séries, o impacto da sazonalidade se dá em termos relativos, dizemos: “em janeiro, as vendas aumentam em 20% com relação a média do ano”. Esse é o caso mais comum para séries não-negativas, como vendas. Isso vem por definição: se temos vendas muito baixas, por exemplo, 10 unidades, não faz sentido dizer que em janeiro as vendas diminuem em 100 unidades, pois isso levaria a vendas negativas. Já dizer que as vendas diminuem em 20% é perfeitamente razoável.
+Em alguns casos, as séries multiplicativas são definidas como:
+\[ +Y(t) = T(t) + T(t) \cdot S(t) + T(t) \cdot R(t) +\]
+Quando a série é multiplicativa, podemos fazer recurso ao logaritmo para transformá-la em aditiva:
+\[ +log(Y(t)) = log(T(t)) + log(S(t)) + log(R(t)) +\]
+Para fazer isso no sktime, usamos o transformador LogTransformer. Transformadores são usados para pré-processar ou pós-processar os dados antes de aplicar um modelo de previsão, e sua interface é similar a dos modelos de previsão:
__init__: define os hiperparâmetros do transformadorfit: aprende os parâmetros do transformador a partir dos dadostransform: aplica a transformação nos dadosinverse_transform (opcional): aplica a transformação inversa nos dadosfrom sktime.transformations.series.boxcox import LogTransformer
+log_transformer = LogTransformer()
+log_transformer.fit(y_train)LogTransformer()Please rerun this cell to show the HTML repr or trust the notebook.
LogTransformer()
y_train_log = log_transformer.transform(y_train)
+plot_series(y_train_log, labels=["Logaritmo"])
Ainda que não esteja perfeito, essa transformação estabiliza as variações da série temporal, o que é importante para alguns modelos de previsão.
+Sktime fornece algumas opções para decompor séries temporais. Aqui, vamos usar o Detrender para remover a tendência, e o Deseasonalizer para remover a sazonalidade.
from sktime.transformations.series.detrend import Detrender, Deseasonalizer
+
+
+detrender = LogTransformer() * Detrender(model="additive")
+detrender.fit(y_train)
+y_train_detrended = detrender.transform(y_train)
+plot_series(y_train_detrended, labels=["Detrended"])
Vemos uma mudança importante no gráfico de autocorrelação:
+fig, _ = plot_correlations(y_train_detrended, lags=60)
+fig.show()/var/folders/_2/9y4tsvdd2n3gqjgd2zmlr1km0000gn/T/ipykernel_24222/935228866.py:2: UserWarning: FigureCanvasAgg is non-interactive, and thus cannot be shown
+ fig.show()
+
O que indica que, ao eliminar a tendencia, a informação que o passado carrega sobre o futuro diminuiu bastante. Na verdade, a existência da tendência - um efeito de longo prazo - faz com que valores passados sejam altamente correlacionados com valores futuros, e pode dar a falsa impressão de que a série é “fácil” de modelar.
+Agora, usamos o Deseasonalizer para remover a sazonalidade:
deseasonalizer = LogTransformer() * Deseasonalizer(model="additive", sp=365)
+deseasonalizer.fit(y_train)
+y_train_deseasonalized = deseasonalizer.transform(y_train)
+plot_series(y_train_log, y_train_deseasonalized, labels=["Log with seasonality", "Deseasonalized"])
Podemos usar o Detrender e o Deseasonalizer juntos para remover ambos os componentes:
remove_components = LogTransformer() * Detrender(model="additive") * Deseasonalizer(model="additive", sp=365)
+remove_components.fit(y_train)
+y_train_removed = remove_components.transform(y_train)
+plot_series(y_train_log, y_train_removed, labels=["Log with seasonality", "Deseasonalized and detrended"])
fig, _ = plot_correlations(y_train_removed, lags=60)
+fig.show()/var/folders/_2/9y4tsvdd2n3gqjgd2zmlr1km0000gn/T/ipykernel_24222/10449850.py:2: UserWarning: FigureCanvasAgg is non-interactive, and thus cannot be shown
+ fig.show()
+
Tente adicionar mais um deseasonalizer para remover a sazonalidade semanal.
+O conceito de estacionariedade é fundamental em séries temporais. Uma série temporal é dita estacionária se suas propriedades estatísticas, como média, variância e autocovariância, são constantes ao longo do tempo, não importando a janela e quando ela é observada.
+Mais precisamente, se \(Y(t)\), onde \(t\) é o indice temporal, então dizemos que ela é estacionária se:
+\[ +P(Y(t_{start}:t_{end})) = P(Y(t_{start}+k:t_{end}+k)), \quad \forall k, t_{start}, t_{end} \in \mathbb{Z} +\]
+Claramente, a série temporal que estamos analisando não é estacionária. Basta percebermos que para valores maiores de \(t\), a média e a variância são maiores.
+O aumento da média da série ao longo do tempo é chamado de tendência. A tendência é um padrão de longo prazo na série temporal, e um grande desafio para previsões de longo prazo.
+Existem definições mais “suaves” de estacionariedade, como a estacionariedade fraca, que requer apenas que propriedades como média e autovariância sejam constantes ao longo do tempo.
+No fundo, o que nos interessa mais é ter uma série temporal que seja “fácil”de modelar. Para alguns algoritmos, como Naive, é importante que ela seja o mais próxima possível de estacionária. Veja abaixo o problema geraodo quando aplicamos o modelo Naive diretamente na série temporal original.
+from sktime.forecasting.naive import NaiveForecaster
+
+naive = NaiveForecaster(strategy="mean", window_length=24)
+naive.fit(y_train)
+y_pred = naive.predict(fh=y_test.index)
+
+plot_series(y_train, y_test, y_pred, labels=["Treino", "Teste", "Previsão Naive"])
Uma técnica simples e eficaz para lidar com séries não estacionárias é a diferenciação. Calculamos:
+\[ +Y'(t) = Y(t) - Y(t-1) +\]
+e fazemos previsões em \(Y'(t)\) ao invés de \(Y(t)\). Para obter a previsão de \(Y(t)\), precisamos fazer o processo inverso: somar a previsão de \(Y'(t)\) com o valor anterior de \(Y(t-1)\).
+\[ +\hat{Y(t)} = \hat{Y'}(t) + \hat{Y}(t-1), \quad \hat{Y}(0) \text{ conhecido} +\]
+Com sktime, isso é extremamente fácil. Aqui, vamos usar um transformador chamado Differencer.
from sktime.transformations.series.difference import Differencer
+diff = Differencer()
+diff.fit(y_train)Differencer()Please rerun this cell to show the HTML repr or trust the notebook.
Differencer()
y_train_diff = diff.transform(y_train)
+
+plot_series(y_train, y_train_diff, labels=["Original", "Diferenciado"])
Agora, podemos criar um modelo de forecasting mais complexo, composto por dois passos:
+Para isso, usamos a classe TransformedTargetForecaster, que cria um pipeline de transformadores e um modelo de previsão.
from sktime.forecasting.compose import TransformedTargetForecaster
+
+model = TransformedTargetForecaster(steps=[
+ ("differencer", Differencer()),
+ ("naive", NaiveForecaster(strategy="mean", window_length=24))
+])
+model.fit(y_train)TransformedTargetForecaster(steps=[('differencer', Differencer()),
+ ('naive',
+ NaiveForecaster(strategy='mean',
+ window_length=24))])Please rerun this cell to show the HTML repr or trust the notebook.TransformedTargetForecaster(steps=[('differencer', Differencer()),
+ ('naive',
+ NaiveForecaster(strategy='mean',
+ window_length=24))])Differencer()
NaiveForecaster(strategy='mean', window_length=24)
Ou apenas:
+model = Differencer() * NaiveForecaster(strategy="mean", window_length=24)
+model.fit(y_train)TransformedTargetForecaster(steps=[Differencer(), + NaiveForecaster(strategy='mean', + window_length=24)])Please rerun this cell to show the HTML repr or trust the notebook.
TransformedTargetForecaster(steps=[Differencer(), + NaiveForecaster(strategy='mean', + window_length=24)])
Differencer()
NaiveForecaster(strategy='mean', window_length=24)
E agora podemos prever:
+y_pred = model.predict(fh=y_test.index)
+plot_series(y_train, y_test, y_pred, labels=["Treino", "Teste", "Previsão Naive com diferenciação"])
Existem ainda alguns problemas com a diferenciação. Note que a variância da série temporal diferenciada não é constante ao longo do tempo. Aqui, podemos combinar nossa transformação logarítmica com a diferenciação.
+Primeiro, vamos criar um transformador que combina as duas transformações:
+from sktime.transformations.compose import TransformerPipeline
+
+log_diff = TransformerPipeline(steps=[
+ ("log", LogTransformer()),
+ ("diff", Differencer())
+])
+log_diff.fit(y_train)TransformerPipeline(steps=[('log', LogTransformer()), ('diff', Differencer())])Please rerun this cell to show the HTML repr or trust the notebook.TransformerPipeline(steps=[('log', LogTransformer()), ('diff', Differencer())])LogTransformer()
Differencer()
y_train_log_diff = log_diff.transform(y_train)
+plot_series(y_train_log_diff, labels=["Log + Diferenciado"])
Para fazer forecast, criamos um pipeline com o transformador combinado e o modelo Naive:
+model = log_diff * NaiveForecaster(strategy="mean", window_length=24)
+model.fit(y_train)
+y_pred = model.predict(fh=y_test.index)
+plot_series(y_train, y_test, y_pred, labels=["Treino", "Teste", "Previsão Naive com log + diferenciação"])
O Modelo Naive, que calculada a média dos últimos valores, é um modelo muito simples, mas que pode ser melhorado. Analizando o que ele faz:
+\[ +\hat{Y}(t) = \frac{Y(t-1) + Y(t-2) + \dots + Y(t-n)}{n} +\]
+Ele basicamente atribui o mesmo peso para todas as observações passadas. Mas, intuitivamente, faz mais sentido dar mais peso para as observações mais recentes, e menos peso para as observações mais antigas.
+O exponential smoothing faz exatamente isso. Ele atribui pesos decrescentes para observações mais antigas. O peso da observação decresce exponencialmente segundo um fator \(0 < \alpha < 1\) (Hyndman and Athanasopoulos 2018):
+\[ +\hat{Y}(t) = \alpha Y(t-1) + \alpha(1 - \alpha)Y(t-1) + \alpha(1 - \alpha)^2 Y(t-2) + \dots +\]
+import matplotlib.pyplot as plt
+
+fig, ax = plt.subplots(1, 1, figsize=(6, 4))
+alpha = 0.3
+weights = [alpha * (1 - alpha) ** i for i in range(20)]
+ax.bar(range(len(weights)), weights)
+ax.set_title("Pesos do Exponential Smoothing")
+ax.set_xlabel("Observações passadas")
+ax.set_ylabel("Peso")
+plt.show()
from tsbook.datasets.retail import SyntheticRetail
+from sktime.utils.plotting import plot_series
+
+dataset = SyntheticRetail("univariate")
+y_train, y_test = dataset.load("y_train", "y_test")from sktime.utils.plotting import plot_series
+from sktime.forecasting.exp_smoothing import ExponentialSmoothing
+
+model = ExponentialSmoothing()
+model.fit(y_train)ExponentialSmoothing()Please rerun this cell to show the HTML repr or trust the notebook.
ExponentialSmoothing()
y_pred = model.predict(fh=y_test.index)
+plot_series(y_train, y_test, y_pred, labels=["Treino", "Teste", "Previsão Exponential Smoothing"])
Existem versões alternativas que consideram sazonalidade e tendências. Veja a documentação para mais detalhes.
+model = ExponentialSmoothing(trend="add", seasonal="add", sp=7)
+model.fit(y_train)
+y_pred = model.predict(fh=y_test.index)
+plot_series(
+ y_train,
+ y_test,
+ y_pred,
+ labels=["Treino", "Teste", "Previsão Exponential Smoothing"],
+)
Modelos autoregressivos (AR) são modelos que prevem o valor atual de uma série temporal como uma combinação dos valores passados. O modelo AR(p) usa os últimos p valores para fazer a previsão:
+\[ +\hat{Y}(t) = \phi_1 Y(t-1) + \phi_2 Y(t-2) + \dots + \phi_p Y(t-p) +\]
+onde \(\phi_1, \phi_2, \dots, \phi_p\) são os parâmetros do modelo que precisam ser estimados a partir dos dados.
+from sktime.forecasting.auto_reg import AutoREG
+from sktime.utils.plotting import plot_series
+
+model = AutoREG(lags=31)
+model.fit(y_train)
+
+y_pred = model.predict(fh=y_test.index)
+plot_series(
+ y_train,
+ y_test,
+ y_pred,
+ labels=["Treino", "Teste", "Previsão AR"],
+)
Um modelo auto-regressivo mais complexo é o ARIMA, que combina autoregressão (AR), média móvel (MA) e diferenciação integrada (I) para lidar com séries temporais não estacionárias. Não vamos estudar o ARIMA aqui pois envolve conceitos mais avançados, mas temos ele disponível no sktime, sktime.forecasting.arima.ARIMA.
Sabemos que séries temporais podem ser decompostas em componentes de tendência, sazonalidade e resíduos. O modelo STL (Seasonal and Trend decomposition using Loess) é uma técnica que permite fazer essa decomposição de forma robusta.
+Temos no sktime o STLTransformer, que permite fazer a decomposição STL:
from sktime.transformations.series.detrend import STLTransformer
+
+stl = STLTransformer(sp=365)
+stl.fit(y_train)STLTransformer(sp=365)Please rerun this cell to show the HTML repr or trust the notebook.
STLTransformer(sp=365)
E agora podemos inspecionar os componentes:
+fig, ax = plt.subplots(3, 1, figsize=(10, 8), sharex=True)
+stl.trend_.plot.line(ax=ax[0])
+ax[0].set_title("Tendência")
+stl.seasonal_.plot.line(ax=ax[1])
+ax[1].set_title("Sazonalidade")
+stl.resid_.plot.line(ax=ax[2])
+ax[2].set_title("Resíduos")
+fig.show()/var/folders/_2/9y4tsvdd2n3gqjgd2zmlr1km0000gn/T/ipykernel_24269/1542615419.py:8: UserWarning: FigureCanvasAgg is non-interactive, and thus cannot be shown
+ fig.show()
+
Uma possibilidade, uma vez que temos os diferentes componentes, é modelar cada componente separadamente e depois combinar as previsões. O sktime tem o STLForecaster, que faz exatamente isso:
from sktime.forecasting.trend import STLForecaster
+from sktime.forecasting.naive import NaiveForecaster
+
+model = STLForecaster(
+ forecaster_trend=AutoREG(lags=31),
+ forecaster_seasonal=NaiveForecaster(sp=7),
+ forecaster_resid=AutoREG(lags=31),
+ sp=7,
+)
+
+model.fit(y_train)
+y_pred = model.predict(fh=y_test.index)
+
+plot_series(
+ y_train,
+ y_test,
+ y_pred,
+ labels=["Treino", "Teste", "Previsão STL + AR"],
+)
Para fins de demonstração, podemos complicar um pouco mais o modelo, modelando os resíduos com outro STLForecaster:
model = STLForecaster(
+ forecaster_trend=AutoREG(lags=31),
+ forecaster_seasonal=NaiveForecaster(sp=7),
+ forecaster_resid=STLForecaster(
+ forecaster_trend=AutoREG(lags=31),
+ forecaster_seasonal=NaiveForecaster(sp=365),
+ forecaster_resid=AutoREG(lags=31),
+ sp=365,
+ ),
+ sp=7,
+)
+
+model.fit(y_train)STLForecaster(forecaster_resid=STLForecaster(forecaster_resid=AutoREG(lags=31), + forecaster_seasonal=NaiveForecaster(sp=365), + forecaster_trend=AutoREG(lags=31), + sp=365), + forecaster_seasonal=NaiveForecaster(sp=7), + forecaster_trend=AutoREG(lags=31), sp=7)Please rerun this cell to show the HTML repr or trust the notebook.
STLForecaster(forecaster_resid=STLForecaster(forecaster_resid=AutoREG(lags=31), + forecaster_seasonal=NaiveForecaster(sp=365), + forecaster_trend=AutoREG(lags=31), + sp=365), + forecaster_seasonal=NaiveForecaster(sp=7), + forecaster_trend=AutoREG(lags=31), sp=7)
STLForecaster(forecaster_resid=AutoREG(lags=31), + forecaster_seasonal=NaiveForecaster(sp=365), + forecaster_trend=AutoREG(lags=31), sp=365)
AutoREG(lags=31)
AutoREG(lags=31)
NaiveForecaster(sp=365)
NaiveForecaster(sp=365)
AutoREG(lags=31)
AutoREG(lags=31)
NaiveForecaster(sp=7)
NaiveForecaster(sp=7)
AutoREG(lags=31)
AutoREG(lags=31)
y_pred = model.predict(fh=y_test.index)
+
+plot_series(
+ y_train,
+ y_test,
+ y_pred,
+ labels=["Treino", "Teste", "Previsão STL + AR"],
+)
Em modelos de regressão e classificação básicos, supomos que os dados são i.i.d: variaveis aleatórias independentes e identicamente distribuidas. Ou seja, a ordem que observamos os dados não tem nenhum impacto em nossa habilidade de prever quais serão as próximas observações.
+Vamos considerar um experimento simples: temos uma caixa com bolas pretas e vermelhas dentro. Digamos que as bolas pretas valem 1 e as bolas vermelhas valem 0. Queremos descobrir qual a quantidade de pontos vamos ter na próxima rodada.
+
Se as bolas estão distribuidas aleatóriamente dentro da caixa, podemos esperar ver algo como a seguinte sequência de pontos:
+\[ +\{0, 1, 1, 0, 1, 0, 0, \dots \} +\]
+Nesse caso, a ordem dos pontos não importa. Não importa o que vimos até então; a próxima bola que vamos tirar da caixa é independente das bolas que já tiramos.
+Vamos supor um caso ligeiramente diferente. Agora, as bolas estão organizadas em blocos: primeiro todas as bolas pretas, depois todas as bolas vermelhas, depois todas as bolas pretas de novo, e assim por diante. Nesse caso, podemos esperar ver algo como:
+\[ +\{1, 1, 1, 0, 0, 0, 1, 1, 1, \dots \} +\]
+Nesse caso, a ordem dos pontos importa. Se vimos muitas bolas pretas recentemente, é mais provável que vejamos uma bola preta na próxima rodada. A próxima bola que vamos tirar da caixa depende das bolas que já tiramos. Esse é um caso básico de aplicação de modelos de previsão de séries temporais.
+Considerando que cada observação é indexada por uma variável temporal, podemos definir uma série temporal como uma sequência de observações ordenadas no tempo. Exemplos comuns de séries temporais incluem:
+Esse livro é uma introdução prática a modelos de séries temporais, com objetivo de dar contexto das ferramentas mais relevantes em 3h de leitura.
+O conteúdo está organizado nas seguintes seções:
+Até então, vimos como criar modelos de séries temporais e avaliamos qualitativamente seu desempenho, em apenas uma simples divisão treino-teste. Agora, vamos explorar como avaliar modelos de séries temporais de forma mais robusta, utilizando métricas específicas e técnicas de validação cruzada adaptadas para dados temporais.
+Primeiro vamos baixar os dados e criar um modelo simples.
+import matplotlib.pyplot as plt
+from tsbook.datasets.retail import SyntheticRetail
+from sktime.utils.plotting import plot_series
+from sktime.forecasting.naive import NaiveForecaster
+
+dataset = SyntheticRetail("univariate")
+y_train, y_test = dataset.load("y_train", "y_test")
+
+# Predict
+model = NaiveForecaster(strategy="last")
+model.fit(y_train)
+y_pred = model.predict(fh=y_test.index)Para avaliar o desempenho de modelos de séries temporais, existem diversas métricas que podem ser utilizadas.
+Duas métricas básicas são:
+from sktime.performance_metrics.forecasting import MeanAbsoluteError
+
+metric = MeanAbsoluteError()
+metric(y_true=y_test, y_pred=y_pred)np.float64(615.0555555555555)
+\[ +MSE = \frac{1}{h} \sum_{t=T+1}^{T+h} (y_t - \hat{y}_t)^2 +\]
+from sktime.performance_metrics.forecasting import MeanSquaredError
+
+metric = MeanSquaredError()
+metric(y_true=y_test, y_pred=y_pred)np.float64(633773.8777777777)
+No entanto, essas métricas não levam em consideração a escala dos dados, o que pode ser um problema ao comparar desempenho entre diferentes séries temporais, e também na comunicação dos resultados para stakeholders.
+Em muitos meios, é comum usar métricas que eliminam o fator escala, como:
+\[ +MAPE = \frac{1}{h} \sum_{t=T+1}^{T+h} \left| \frac{y_t - \hat{y}_t}{y_t} \right| +\]
+from sktime.performance_metrics.forecasting import MeanAbsolutePercentageError
+
+metric = MeanAbsolutePercentageError()
+metric(y_true=y_test, y_pred=y_pred)np.float64(0.21292470853237802)
+metric = MeanAbsolutePercentageError(symmetric=True)
+metric(y_true=y_test, y_pred=y_pred)np.float64(0.22359840490570626)
+\[ +sMAPE = \frac{1}{h} \sum_{t=T+1}^{T+h} \frac{|y_t - \hat{y}_t|}{(|y_t| + |\hat{y}_t|)/2} +\]
+Essas métricas, no entanto, apresentam seus próprios problemas. Note que os valores podem ser exageradamente alto quando o denominador é próximo de zero. Ou seja, o MAPE e sMAPE podem ser problemáticos quando a série temporal contém valores próximos de zero.
+Outro tema é que essas métricas não consideram a dificuldade da série. Por exemplo, 5% de erro pode ser muito em uma série, e pouco em outra. Para isso, foram propostas métricas “escalas”:
+\[ +e_{naive} = \frac{1}{T-1} \sum_{t=2}^{T} |y_t - y_{t-1}| +\]
+são os erros nos dados de treino, então:
+\[ +MASE = \frac{1}{h} \sum_{t=T+1}^{T+h} \frac{|y_t - \hat{y}_t|}{e_{naive}} +\]
+from sktime.performance_metrics.forecasting import MeanAbsoluteScaledError
+
+metric = MeanAbsoluteScaledError()
+metric(y_true=y_test, y_pred=y_pred, y_train=y_train)np.float64(10.127736897882464)
+\[ +MSSE = \frac{1}{h} \sum_{t=T+1}^{T+h} \frac{(y_t - \hat{y}_t)^2}{e_{naive}^2} +\]
+from sktime.performance_metrics.forecasting import MeanSquaredScaledError
+
+metric = MeanSquaredScaledError()
+metric(y_true=y_test, y_pred=y_pred, y_train=y_train)np.float64(57.09087480132447)
+Em um cross-validation, nossa intenção é estimar corretamente o erro de generalização do modelo, ou seja, o erro que o modelo terá em dados futuros.
+\[ +\mathbb{E}[L(Y, \hat{Y})] +\]
+Onde a média é sobre a distribuição que mais representa o que será visto em produção.
+Tipicamente, fazemos back-testing com dois tipos de janelas:
+from sktime.utils.plotting import plot_windows
+
+from sktime.split import SlidingWindowSplitter
+
+sliding_window_cv = SlidingWindowSplitter(
+ window_length=365 * 3, step_length=100, fh=list(range(1, 90 + 1))
+)
+plot_windows(cv=sliding_window_cv, y=y_train)
+plt.show()
from sktime.split import ExpandingWindowSplitter
+
+
+
+expanding_window_cv = ExpandingWindowSplitter(
+ initial_window=365 * 3, step_length=100, fh=list(range(1, 90 + 1))
+)
+plot_windows(cv=expanding_window_cv, y=y_train)
+plt.show()
Qual escolher? Bom, escolha o que mais representa o que você espera ver em produção.
+Para executar cross-validation, usamos a função evaluate, que recebe o modelo, o esquema de cross-validation, os dados, e a métrica a ser usada.
from sktime.forecasting.model_evaluation import evaluate
+from sktime.performance_metrics.forecasting import MeanAbsoluteScaledError
+from sktime.forecasting.naive import NaiveForecaster
+
+model = NaiveForecaster(strategy="last")
+
+evaluate(
+ forecaster=model,
+ cv=expanding_window_cv,
+ y=y_train,
+ X=None, # Veremos na próxima seçao!
+ scoring=MeanAbsoluteScaledError(),
+ error_score="raise",
+ return_data=True,
+)| + | test_MeanAbsoluteScaledError | +fit_time | +pred_time | +len_train_window | +cutoff | +y_train | +y_test | +y_pred | +
|---|---|---|---|---|---|---|---|---|
| 0 | +12.839683 | +0.002296 | +0.005386 | +1095 | +2022-12-30 | +sales date 2020-01-01... | +sales date 2022-12-31... | +sales 2022-12-31 458.0 2023-01-01... | +
| 1 | +3.813045 | +0.001674 | +0.003905 | +1195 | +2023-04-09 | +sales date 2020-01-01... | +sales date 2023-04-10... | +sales 2023-04-10 866.0 2023-04-11... | +
| 2 | +8.068216 | +0.001615 | +0.003741 | +1295 | +2023-07-18 | +sales date 2020-01-01... | +sales date 2023-07-19... | +sales 2023-07-19 748.0 2023-07-20... | +
| 3 | +7.854970 | +0.001626 | +0.003664 | +1395 | +2023-10-26 | +sales date 2020-01-01... | +sales date 2023-10-27... | +sales 2023-10-27 1229.0 2023-10-... | +
| 4 | +5.955709 | +0.001576 | +0.003626 | +1495 | +2024-02-03 | +sales date 2020-01-01... | +sales date 2024-02-04... | +sales 2024-02-04 1916.0 2024-02-... | +
Considerando o nosso exemplo da caixa com bolas pretas e vermelhas, um modelo simples para prever a próxima bola que vamos tirar da caixa é o Modelo Naive.
+Existem algumas versões de modelo naive:
+Esse modelo é simples, mas é extremamente eficaz e dificil de ser vencido em muitos casos. No cenário onde os dados não vêm organizados em nenhuma ordem específica, a melhor aposta que podemos fazer é o valor médio das bolas no histórico recente.
+Vamos ver como fazer um forecast simples com sktime, usando o modelo Naive.
+Aqui, baixamos o dataset simples que vem na biblioteca desse repositório.
+from tsbook.datasets.simple import SimpleDataset
+
+
+dataset = SimpleDataset(True)
+y = dataset.load("y")
+
+y.head()| + | points | +
|---|---|
| 2021-01-01 | +0 | +
| 2021-01-02 | +0 | +
| 2021-01-03 | +0 | +
| 2021-01-04 | +1 | +
| 2021-01-05 | +1 | +
Esse é um dataset simples com uma série temporal mensal. Vamos dividir os dados em treino e teste, usando os últimos 36 meses como teste. Para isso, devemos respeitar a ordem temporal dos dados.
+A função temporal_train_test_split faz isso para nós.
from sktime.forecasting.model_selection import temporal_train_test_split
+y_train, y_test = temporal_train_test_split(y, test_size=36)Também temos uma função de plotagem simples para visualizar séries temporais.
+from sktime.utils.plotting import plot_series
+
+plot_series(y, labels=["Observações"])
No sktime, os modelos são usados em 3 passos:
+__init__): aqui, definimos os hiperparâmetros do modelo. Pense nessa parte como a configuração do modelo.fit): aqui, o modelo aprende com os dados de treino.predict): com esse método, o modelo faz previsões para os dados futuros.Quando inicializamos o modelo em um notebook, o sktime mostra uma ilustração do modelo, o que é útil para entender o que está acontecendo “por baixo dos panos”. Em casos mais complexos com composição de modelos, isso pode ser útil para ilustrar o que estamos fazendo para outros cientistas.
+from sktime.forecasting.naive import NaiveForecaster
+
+model = NaiveForecaster(strategy="last")
+modelNaiveForecaster()Please rerun this cell to show the HTML repr or trust the notebook.
NaiveForecaster()
Ao treinar o modelo, passamos dados de treinamento.
+model.fit(y_train)NaiveForecaster()Please rerun this cell to show the HTML repr or trust the notebook.
NaiveForecaster()
Para a previsão, temos que passar um argumento obrigatório: fh, abreviatura de “forecasting horizon” (horizonte de previsão). Tipicamente passamos um fh relativo, ou seja, no formato de uma lista
model.predict(fh=[1,2,3,4])| + | points | +
|---|---|
| 2021-03-06 | +1.0 | +
| 2021-03-07 | +1.0 | +
| 2021-03-08 | +1.0 | +
| 2021-03-09 | +1.0 | +
Onde cada número representa o número de períodos à frente que queremos prever. Também podemos passar o fh como um índice de tempo absoluto, que é o que faremos aqui. Vamos passar o índice de tempo do conjunto de teste.
y_pred = model.predict(fh=y_test.index)
+
+plot_series(y_train, y_test, y_pred, labels=["Treino", "Teste", "Previsão"])
Para alterar hiperparametros de um modelo já existente, podemos usar o método set_params, que modifica in-place os hiperparâmetros do modelo.
model.set_params(
+ strategy="mean",
+ window_length=12,
+)
+
+model.fit(y_train)
+y_pred = model.predict(fh=y_test.index)
+
+
+plot_series(y_train, y_test, y_pred, labels=["Treino", "Teste", "Previsão"])
Podemos também testar o Naive sazonal, que repete a última observação de 6 períodos atrás.
model.set_params(
+ sp=6,
+ strategy="last"
+
+)
+model.fit(y_train)
+y_pred = model.predict(fh=y_test.index)
+plot_series(y_train, y_test, y_pred, labels=["Treino", "Teste", "Previsão"])
Claro, como esse exemplo é muito simples, o modelo naive sazonal captura perfeitamente a sazonalidade dos dados. No entanto, vamos ver no próximo capítulo um caso de uso mais realista, com dados de varejo, e estudaremos modelos mais avançados.
+ + +Além de modelos de ML simples, também podemos usar modelos de deep learning para forecasting. Podemos com certeza usar uma rede neural simples como um regressor, assim como fizemos com os modelos de ML tradicionais. No entanto, existem alguns modelos com arquiteturas específicas para forecasting de séries temporais. Por exemplo, o N-BEATS é um modelo de deep learning que pode ser usado para forecasting.
+Importante notar que esse livro/tutorial tem o objetivo de ser curto e prático, então não entraremos em muitos detalhes sobre deep learning ou sobre todos métodos existentes.
+import pandas as pd
+import matplotlib.pyplot as plt
+
+from sktime.utils.plotting import plot_seriesfrom tsbook.datasets.retail import SyntheticRetail
+
+dataset = SyntheticRetail("panel")
+y_train, X_train, y_test, X_test = dataset.load(
+ "y_train", "X_train", "y_test", "X_test"
+)
+
+fh = y_test.index.get_level_values(-1).unique()N-BEATS é um modelo de séries temporais totalmente baseado em camadas densas (MLP)—nada de RNN/LSTM nem convolução. Ele pega uma janela do passado e entrega diretamente a previsão do futuro, aprendendo padrões como tendência e sazonalidade de forma pura, só com perceptrons.
+O N-BEATS usa bases para construir sinais interpretáveis:
+Assim, internamente, ele determina os coeficientes das funções base para modelar a série temporal, baseado no histórico observado, fazendo uma espécie de meta-aprendizado interno.
+Os blocos são empilhados e executados de forma sucessiva. Pense numa fila de especialistas olhando a mesma janela do passado:
+
O Sktime nao é uma biblioteca especializada em deep learning, mas sim uma API uniforme que provê acesso aos mais diversos algoritmos.
+Logo, também provemos acesso a bibliotecas especializadas em deep learning, como o Pytorch Forecasting, que implementa o N-BEATS.
+Aqui, temos que definir os hiperparâmetros do modelo, como o número de blocos, o tamanho do contexto (janela de entrada), e o número de coeficientes para as funcões de base.
+from sktime.forecasting.pytorchforecasting import PytorchForecastingNBeats
+from pytorch_forecasting.data.encoders import EncoderNormalizer
+
+CONTEXT_LENGTH = 120
+nbeats = PytorchForecastingNBeats(
+ train_to_dataloader_params={"batch_size": 256},
+ trainer_params={"max_epochs": 1},
+ model_params={
+ "stack_types": ["trend", "seasonality"], # One of the following values: “generic”, “seasonality” or “trend”.
+ "num_blocks" : [2,2], # The number of blocks per stack.
+ "context_length": CONTEXT_LENGTH, # lookback period
+ "expansion_coefficient_lengths" : [2, 5],
+ "learning_rate": 1e-3,
+ },
+ dataset_params={
+
+ "max_encoder_length": CONTEXT_LENGTH,
+ "target_normalizer": EncoderNormalizer()
+ },
+)
+
+nbeats.fit(y_train.astype(float), fh=fh)💡 Tip: For seamless cloud uploads and versioning, try installing [litmodels](https://pypi.org/project/litmodels/) to enable LitModelCheckpoint, which syncs automatically with the Lightning model registry.
+GPU available: True (mps), used: True
+TPU available: False, using: 0 TPU cores
+HPU available: False, using: 0 HPUs
+
+ | Name | Type | Params | Mode
+-------------------------------------------------------
+0 | loss | MASE | 0 | train
+1 | logging_metrics | ModuleList | 0 | train
+2 | net_blocks | ModuleList | 1.4 M | train
+-------------------------------------------------------
+1.4 M Trainable params
+0 Non-trainable params
+1.4 M Total params
+5.484 Total estimated model params size (MB)
+60 Modules in train mode
+0 Modules in eval mode
+`Trainer.fit` stopped: `max_epochs=1` reached.
+PytorchForecastingNBeats(dataset_params={'max_encoder_length': 120,
+ 'target_normalizer': EncoderNormalizer(
+ method='standard',
+ center=True,
+ max_length=None,
+ transformation=None,
+ method_kwargs={}
+)},
+ model_params={'context_length': 120,
+ 'expansion_coefficient_lengths': [2, 5],
+ 'learning_rate': 0.001,
+ 'num_blocks': [2, 2],
+ 'stack_types': ['trend', 'seasonality']},
+ train_to_dataloader_params={'batch_size': 256},
+ trainer_params={'max_epochs': 1})Please rerun this cell to show the HTML repr or trust the notebook.PytorchForecastingNBeats(dataset_params={'max_encoder_length': 120,
+ 'target_normalizer': EncoderNormalizer(
+ method='standard',
+ center=True,
+ max_length=None,
+ transformation=None,
+ method_kwargs={}
+)},
+ model_params={'context_length': 120,
+ 'expansion_coefficient_lengths': [2, 5],
+ 'learning_rate': 0.001,
+ 'num_blocks': [2, 2],
+ 'stack_types': ['trend', 'seasonality']},
+ train_to_dataloader_params={'batch_size': 256},
+ trainer_params={'max_epochs': 1})y_pred_nbeats = nbeats.predict(fh=fh, X=X_test)/Users/felipeangelim/Workspace/python_brasil_2025/.venv/lib/python3.11/site-packages/sktime/forecasting/base/adapters/_pytorchforecasting.py:655: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
+ _y.fillna(0, inplace=True)
+💡 Tip: For seamless cloud uploads and versioning, try installing [litmodels](https://pypi.org/project/litmodels/) to enable LitModelCheckpoint, which syncs automatically with the Lightning model registry.
+GPU available: True (mps), used: True
+TPU available: False, using: 0 TPU cores
+HPU available: False, using: 0 HPUs
+from sktime.performance_metrics.forecasting import MeanSquaredScaledError
+
+metric = MeanSquaredScaledError(multilevel="uniform_average_time")
+metric(y_true=y_test, y_pred=y_pred_nbeats, y_train=y_train)np.float64(16.453837132521112)
+Agora, podemos visualizar o forecast para uma das séries. Vemos que, mesmo com poucas épocas de treinamento ou tuning, o N-BEATS já consegue capturar a tendência.
+fig, ax = plt.subplots(figsize=(10, 4))
+y_train.loc[10].plot(ax=ax, label="Train")
+y_test.loc[10].plot(ax=ax, label="Test")
+y_pred_nbeats.loc[10].plot(ax=ax, label="N-BEATS")
+fig.show()
Zero-shot forecasting se refere ao fato de fazer previsão para uma série jamais vista pelo modeo, sem utilizar a série para treinar ou ajustar parâmetros dele.
+Aqui, para simular esse cenário, vamos criar uma nova série temporal combinando duas séries do conjunto de treino.
+new_y_train = (y_train.loc[0]**2 + y_train.loc[20]).astype(float)
+new_y_test = (y_test.loc[0]**2 + y_test.loc[20]).astype(float)
+
+# Plotting the new series
+fig, ax = plt.subplots(figsize=(10, 4))
+new_y_train["sales"].plot.line(ax=ax, label="New Train")
+new_y_test["sales"].plot.line(ax=ax, label="New Test")
+fig.show()
Na interface atual do sktime, usamos o argumento y do método predict para passar a nova série temporal para o modelo:
y_pred_zeroshot = nbeats.predict(fh=fh, y=new_y_train)/Users/felipeangelim/Workspace/python_brasil_2025/.venv/lib/python3.11/site-packages/sktime/forecasting/base/adapters/_pytorchforecasting.py:655: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
+ _y.fillna(0, inplace=True)
+💡 Tip: For seamless cloud uploads and versioning, try installing [litmodels](https://pypi.org/project/litmodels/) to enable LitModelCheckpoint, which syncs automatically with the Lightning model registry.
+GPU available: True (mps), used: True
+TPU available: False, using: 0 TPU cores
+HPU available: False, using: 0 HPUs
+fig, ax = plt.subplots(figsize=(10, 4))
+new_y_train["sales"].plot.line(ax=ax, label="New Train")
+new_y_test["sales"].plot.line(ax=ax, label="New Test")
+y_pred_zeroshot["sales"].plot.line(ax=ax, label="N-BEATS Zero-shot")
+plt.legend()
+plt.show()
Como séries temporais possuem uma ordem temporal, a sua própria história tem poder preditivo muito forte. No entanto, em alguns casos, apenas a história não é suficiente para fazer previsões precisas.
+Por exemplo, em vendas de varejo, fatores como promoções, feriados e eventos sazonais podem impactar significativamente as vendas.
+Agora, vamos ver como usar variáveis exógenas em modelos de séries temporais com sktime, as famosas “features”.
+A interface é sempre a mesma, vamos ver que a diferença é o uso do parâmetro X nos métodos fit e predict.
Usaremos os mesmos dados de varejo sintético dos exemplos anteriores. Agora, teremos também X_train e X_test, que são as variáveis exógenas.
from tsbook.datasets.retail import SyntheticRetail
+from sktime.utils.plotting import plot_series
+from sktime.forecasting.naive import NaiveForecaster
+
+dataset = SyntheticRetail("univariate", macro_trend=True)
+y_train, X_train, y_test, X_test = dataset.load(
+ "y_train", "X_train", "y_test", "X_test"
+)
+
+X_train.head()| + | promo | +macro_trend | +
|---|---|---|
| date | ++ | + |
| 2020-01-01 | +0.0 | +1.304000 | +
| 2020-01-02 | +0.0 | +1.327826 | +
| 2020-01-03 | +0.0 | +1.350638 | +
| 2020-01-04 | +0.0 | +1.361667 | +
| 2020-01-05 | +0.0 | +1.361633 | +
import matplotlib.pyplot as plt
+
+X_train.plot.line()
fig, ax = plt.subplots( figsize=(10, 6))
+
+y_train.plot(ax=ax, label="y")
+X_train["macro_trend"].plot(ax=ax.twinx(), label="macro_trend")
Nem todos modelos suportam variáveis exógenas. Para ver uma lista de possibilidades, podemos usar a função all_estimators do sktime.
from sktime.registry import all_estimators
+
+all_estimators(
+ "forecaster", filter_tags={"capability:exogenous": True}, as_dataframe=True
+)| + | name | +object | +
|---|---|---|
| 0 | +ARDL | +<class 'sktime.forecasting.ardl.ARDL'> | +
| 1 | +ARIMA | +<class 'sktime.forecasting.arima._pmdarima.ARI... | +
| 2 | +AutoARIMA | +<class 'sktime.forecasting.arima._pmdarima.Aut... | +
| 3 | +AutoEnsembleForecaster | +<class 'sktime.forecasting.compose._ensemble.A... | +
| 4 | +AutoREG | +<class 'sktime.forecasting.auto_reg.AutoREG'> | +
| ... | +... | +... | +
| 77 | +UpdateEvery | +<class 'sktime.forecasting.stream._update.Upda... | +
| 78 | +UpdateRefitsEvery | +<class 'sktime.forecasting.stream._update.Upda... | +
| 79 | +VARMAX | +<class 'sktime.forecasting.varmax.VARMAX'> | +
| 80 | +VECM | +<class 'sktime.forecasting.vecm.VECM'> | +
| 81 | +YfromX | +<class 'sktime.forecasting.compose._reduce.Yfr... | +
82 rows × 2 columns
+Antes de prosseguirmos, vamos separar as variáveis exógenas em dois tipos:
+Variáveis exógenas com valores futuros conhecidos: São variáveis cujos valores futuros já são conhecidos no momento da previsão. Variáveis indicadoras (dummies) para feriados ou eventos especiais, bem como recursos de sazonalidade, são exemplos comuns desse tipo de variável.
Variáveis exógenas com valores futuros desconhecidos: São variáveis cujos valores futuros não são conhecidos para o horizonte de previsão. Por exemplo, se quisermos incluir indicadores econômicos que ainda não foram divulgados, devemos tratá-los como variáveis exógenas de valor futuro desconhecido.
Nesse último cenário, para realizar a previsão, existem três opções: 1. Prever os valores futuros das variáveis exógenas usando um modelo separado (ou o mesmo modelo, se ele permitir) e usar essas previsões como entrada para o modelo principal de previsão. 2. Usar um valor de preenchimento (por exemplo, o último valor conhecido) para as variáveis exógenas de valor futuro desconhecido durante a previsão. 3. Usar valores defasados (lags) das variáveis exógenas como características (features), o que pode ser útil se o modelo conseguir aprender com os valores passados dessas variáveis.
+Vamos usar AutoREG como modelo base para nosso exemplo de vairáveis exógenas. Primeiramente, vamos supor que conhecemos a variável macro_trend no futuro
from sktime.forecasting.auto_reg import AutoREG
+
+model = AutoREG(lags=30)
+model.fit(y_train, X=X_train)
+y_pred = model.predict(fh=y_test.index, X=X_test)plot_series(y_train, y_test, y_pred, labels=["Treino", "Teste", "Previsão com Exógenas"])
Fácil, no caso que conhecemos a variável exógena no futuro, basta passar X em fit e predict.
Antes de avançar, é importante revisar que os transformadores com fit e transform também podem ser aplicados a variávei exógenas! Inclusive, podemos fazer pipelines compostos de várias etapas de preprocessamento de exógenas. Isso será importante para os próximos exemplos.
Vamos eliminar a variável macro_trend do conjunto de teste, para simular o cenário onde não conhecemos o valor futuro dessa variável.
import numpy as np
+
+X_test_missing = X_test.copy()
+X_test_missing["macro_trend"] = np.nanAgora, vamos supor que não sabemos o valor futuro de macro_trend. Nesse caso, podemos criar um modelo separado para prever macro_trend e usar essa previsão como entrada para o modelo principal.
Sktime possui uma funcionalidade pronta para isso: um forecaster chamado ForecastX. Ele é composto de dois modelos, um para a variável exógena, e outro para o alvo principal.
Aqui, o forecaster necessita que o horizonte de previsão (fh) seja passado já na etapa de fit.
from sktime.forecasting.compose import ForecastX
+
+model = ForecastX(
+ forecaster_y=AutoREG(lags=30),
+ forecaster_X=AutoREG(lags=30),
+)
+
+fh = [i for i in range(1, len(y_test) + 1)]
+model.fit(y_train, X=X_train, fh=fh)
+
+y_pred_case1 = model.predict(X=X_test_missing)plot_series(y_train, y_test, y_pred_case1, labels=["Treino", "Teste", "Previsão com Exógenas Previstas"])
Para essa solução, vamos usar transformadores para preencher os valores faltantes na variável exógena. Aqui, usaremos o Imputer do sktime, que suporta variáveis exógenas.
from sktime.transformations.series.impute import Imputer
+imputer = Imputer(method="mean")
+imputer.fit(X_train)
+
+# Agora imputamos
+X_test_imputed = imputer.transform(X_test_missing)
+X_test_imputed.tail()| + | promo | +macro_trend | +
|---|---|---|
| date | ++ | + |
| 2024-12-28 | +0.0 | +22.805784 | +
| 2024-12-29 | +0.0 | +22.805784 | +
| 2024-12-30 | +0.0 | +22.805784 | +
| 2024-12-31 | +1.0 | +22.805784 | +
| 2025-01-01 | +0.0 | +22.805784 | +
Para usar preprocessamento de exógenas + forecasting, podemos usar a composição ForecastingPipeline.
from sktime.forecasting.compose import ForecastingPipeline
+from sktime.transformations.series.difference import Differencer
+
+
+model = ForecastingPipeline(
+ steps=[("imputer", Imputer(method="mean")), ("forecaster", AutoREG(lags=30))]
+)
+model.fit(y_train, X=X_train)ForecastingPipeline(steps=[('imputer', Imputer(method='mean')),
+ ('forecaster', AutoREG(lags=30))])Please rerun this cell to show the HTML repr or trust the notebook.ForecastingPipeline(steps=[('imputer', Imputer(method='mean')),
+ ('forecaster', AutoREG(lags=30))])Imputer(method='mean')
AutoREG(lags=30)
y_pred_case2 = model.predict(fh=y_test.index, X=X_test_missing)
+plot_series(y_train, y_test, y_pred_case2, labels=["Treino", "Teste", "Previsão com Exógenas Imputadas"])
Nossa previsão não ficou boa. Claro! A variável exógena possui uma tendência - que naturalmente faz com que a imputação do ultimo valor ou a média não funcione bem. A solução ótima varia de caso para caso.
+Dica: Podemos também usar o operador ** como atalho para criar pipelines de variáveis exógenas.
model = Imputer(method="mean") ** AutoREG(lags=30)Note a diferença do que aprendemos para criar pipelines com transformações na variável target, que usam * como operador. Claramente, podemos fazer composições mais complexas, como:
model = Imputer(method="mean") ** (Differencer() * AutoREG())Outra opção é criar versões defasadas das variáveis exógenas e usá-las como features.
+Para isso, podemos usar o transformador Lag do sktime. Ao utilizar defasagens (lags), surgem dois desafios principais:
+O aparecimento de valores NaN, que muitos modelos de previsão não conseguem tratar.
O número de variáveis exógenas pode aumentar significativamente, o que pode levar a overfitting ou, no caso do nosso conjunto de dados, a um número de features maior que o número de amostras — o que pode gerar erros no processo de ajuste (fitting).
Para lidar com isso, no exemplo abaixo utilizamos um TransformerPipeline que realiza as seguintes etapas:
+from sktime.transformations.compose import TransformerPipeline
+from sktime.transformations.series.feature_selection import FeatureSelection
+from sktime.transformations.series.impute import Imputer
+from sktime.transformations.series.lag import Lag
+from sktime.transformations.series.subset import IndexSubset
+
+
+transformer_pipeline = TransformerPipeline(
+ steps=[
+ ("lag", Lag(lags=list(range(1, 180 + 1)))), # Cria lags 3 e 4
+ ("subset", IndexSubset()), # Seleciona apenas macro_trend
+ ("impute", Imputer(method="backfill", value=0)), # Imputa valores NaN
+ ("feature_selection", FeatureSelection()), # Seleciona features
+ ]
+)transformer_pipeline.fit(X=X_train, y=y_train)
+X_test_transformed = transformer_pipeline.transform(X=X_test_missing)
+
+X_test_transformed.head()| + | lag_98__macro_trend | +lag_34__macro_trend | +lag_29__macro_trend | +lag_104__macro_trend | +lag_67__macro_trend | +lag_35__macro_trend | +lag_75__macro_trend | +lag_39__macro_trend | +lag_6__macro_trend | +lag_32__macro_trend | +... | +lag_66__promo | +lag_129__macro_trend | +lag_97__macro_trend | +lag_174__promo | +lag_136__macro_trend | +lag_119__promo | +lag_72__macro_trend | +lag_99__macro_trend | +lag_33__promo | +lag_29__promo | +
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| date | ++ | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
| 2024-07-06 | +82.233778 | +81.711556 | +82.607111 | +81.633778 | +81.764000 | +82.042667 | +81.536000 | +82.793778 | +82.909333 | +82.084889 | +... | +0.0 | +77.054667 | +82.288444 | +0.0 | +76.021333 | +0.0 | +81.565333 | +82.200000 | +0.0 | +0.0 | +
| 2024-07-07 | +82.288444 | +81.823556 | +82.515556 | +81.660444 | +81.880444 | +81.711556 | +81.282667 | +82.855556 | +83.153778 | +82.555556 | +... | +0.0 | +77.324889 | +82.319556 | +0.0 | +76.131111 | +0.0 | +81.838222 | +82.233778 | +0.0 | +0.0 | +
| 2024-07-08 | +82.319556 | +82.084889 | +82.460889 | +81.724444 | +82.063111 | +81.823556 | +81.306222 | +82.756444 | +83.440444 | +82.657333 | +... | +0.0 | +77.896000 | +82.393778 | +0.0 | +76.268889 | +0.0 | +82.044889 | +82.288444 | +0.0 | +0.0 | +
| 2024-07-09 | +82.393778 | +82.555556 | +82.498667 | +81.875556 | +82.175556 | +82.084889 | +81.565333 | +82.488444 | +83.594667 | +82.607111 | +... | +0.0 | +78.460444 | +82.383556 | +0.0 | +76.441333 | +0.0 | +82.003111 | +82.319556 | +0.0 | +1.0 | +
| 2024-07-10 | +82.383556 | +82.657333 | +82.444444 | +82.086222 | +82.288000 | +82.555556 | +81.838222 | +82.042667 | +83.721778 | +82.515556 | +... | +0.0 | +79.001778 | +82.550222 | +0.0 | +76.692889 | +0.0 | +81.840889 | +82.393778 | +0.0 | +0.0 | +
5 rows × 180 columns
+model = ForecastingPipeline(
+ steps=[
+ ("preprocessing", transformer_pipeline),
+ ("forecaster", AutoREG(lags=30)),
+ ]
+).fit(X=X_train, y=y_train)
+
+
+modelForecastingPipeline(steps=[('preprocessing',
+ TransformerPipeline(steps=[('lag',
+ Lag(lags=[1, 2, 3, 4, 5,
+ 6, 7, 8, 9,
+ 10, 11, 12,
+ 13, 14, 15,
+ 16, 17, 18,
+ 19, 20, 21,
+ 22, 23, 24,
+ 25, 26, 27,
+ 28, 29, 30, ...])),
+ ('subset',
+ IndexSubset()),
+ ('impute',
+ Imputer(method='backfill',
+ value=0)),
+ ('feature_selection',
+ FeatureSelection())])),
+ ('forecaster', AutoREG(lags=30))])Please rerun this cell to show the HTML repr or trust the notebook.ForecastingPipeline(steps=[('preprocessing',
+ TransformerPipeline(steps=[('lag',
+ Lag(lags=[1, 2, 3, 4, 5,
+ 6, 7, 8, 9,
+ 10, 11, 12,
+ 13, 14, 15,
+ 16, 17, 18,
+ 19, 20, 21,
+ 22, 23, 24,
+ 25, 26, 27,
+ 28, 29, 30, ...])),
+ ('subset',
+ IndexSubset()),
+ ('impute',
+ Imputer(method='backfill',
+ value=0)),
+ ('feature_selection',
+ FeatureSelection())])),
+ ('forecaster', AutoREG(lags=30))])TransformerPipeline(steps=[('lag',
+ Lag(lags=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
+ 24, 25, 26, 27, 28, 29, 30, ...])),
+ ('subset', IndexSubset()),
+ ('impute', Imputer(method='backfill', value=0)),
+ ('feature_selection', FeatureSelection())])Lag(lags=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, ...])
IndexSubset()
Imputer(method='backfill', value=0)
FeatureSelection()
AutoREG(lags=30)
y_pred_case3 = model.predict(fh=y_test.index, X=X_test_missing)
+plot_series(y_train, y_test, y_pred_case3, labels=["Treino", "Teste", "Previsão com Exógenas Lag"])
Muitas vezes, não apenas temos múltiplas séries temporais, mas essas séries também estão organizadas em uma hierarquia. Por exemplo, vendas de produtos podem ser organizadas por SKU, categoria, departamento e total da loja.
+Vamos usar o mesmo dataset sintético, mas agora com uma hierarquia de produtos.
+graph TD + root["__total"] + + %% group -1 + root --> g_minus1["-1"] + g_minus1 --> sku20["20"] + g_minus1 --> sku21["21"] + g_minus1 --> sku22["22"] + g_minus1 --> sku23["23"] + g_minus1 --> sku24["24"] + + %% group 0 + root --> g0["0"] + g0 --> sku0["0"] + g0 --> sku1["1"] + g0 --> sku2["2"] + g0 --> sku3["3"] + g0 --> sku4["4"] + + %% group 1 + root --> g1["..."] + + + %% group 3 + root --> g3["3"] + g3 --> sku15["15"] + g3 --> sku16["16"] + g3 --> sku17["17"] + g3 --> sku18["18"] + g3 --> sku19["19"] ++
Ao mesmo tempo que dados hierarárquicos são interessantes pois nos trazem mais informação, eles também trazem desafios adicionais. Imagine que queremos prever as vendas futuras de cada produto. Se fizermos previsões independetes para cada produto, não há garantia que a soma das previsões dos produtos será igual à previsão do total da loja. Isso é chamado de incoerência nas previsões hierárquicas. O processo de ajustar as previsões para garantir coerência é chamado de reconciliação.
+Vamos usar os dados sintéticos, agora com sua versao hierárquica.
+from tsbook.datasets.retail import SyntheticRetail
+
+dataset = SyntheticRetail("hierarchical")
+y_train, X_train, y_test, X_test = dataset.load("y_train", "X_train", "y_test", "X_test")Agora, os dataframes possuem mais de 2 ou mais índices, representando a hierarquia.
+y_train| + | + | + | sales | +
|---|---|---|---|
| group_id | +sku_id | +date | ++ |
| -1 | +20 | +2020-01-01 | +0 | +
| 2020-01-02 | +0 | +||
| 2020-01-03 | +0 | +||
| 2020-01-04 | +0 | +||
| 2020-01-05 | +2 | +||
| ... | +... | +... | +... | +
| __total | +__total | +2024-07-01 | +2000 | +
| 2024-07-02 | +1616 | +||
| 2024-07-03 | +1917 | +||
| 2024-07-04 | +2384 | +||
| 2024-07-05 | +2462 | +
51088 rows × 1 columns
+Para obter o número de pontos de série únicos (séries temporais individuais), podemos fazer o seguinte:
+y_train.index.droplevel(-1).nunique()31
+Note que existem algumas séries com um identificador __total. Esse identificador representa o total para aquele nível da hierarquia. Por exemplo, se o id completo é (-1, "__total"), isso representa o total do grupo -1.
y_train.loc[(-1, "__total")].head()| + | sales | +
|---|---|
| date | ++ |
| 2020-01-01 | +4 | +
| 2020-01-02 | +2 | +
| 2020-01-03 | +3 | +
| 2020-01-04 | +14 | +
| 2020-01-05 | +16 | +
O total de todas as séries é representado por ("__total", "__total").
y_train.loc[("__total", "__total")]| + | sales | +
|---|---|
| date | ++ |
| 2020-01-01 | +24 | +
| 2020-01-02 | +19 | +
| 2020-01-03 | +14 | +
| 2020-01-04 | +23 | +
| 2020-01-05 | +23 | +
| ... | +... | +
| 2024-07-01 | +2000 | +
| 2024-07-02 | +1616 | +
| 2024-07-03 | +1917 | +
| 2024-07-04 | +2384 | +
| 2024-07-05 | +2462 | +
1648 rows × 1 columns
+Para contabilizar o número de séries temporais individuais, podemos fazer o seguinte:
+y_train.index.droplevel(-1).nunique()31
+Vamos fazer uma previsão e entender o problema da incoerência.
+fh = y_test.index.get_level_values(-1).unique()from tsbook.forecasting.reduction import ReductionForecaster
+from lightgbm import LGBMRegressor
+
+forecaster = ReductionForecaster(
+ LGBMRegressor(n_estimators=50, verbose=-1, objective="tweedie"),
+ window_length=30,
+ normalization_strategy="divide_mean",
+)
+forecaster.fit(y_train, X=X_train)
+y_pred = forecaster.predict(fh, X=X_test)Para somar as previsões de baixo para cima, podemos usar o transformador Aggregator. Vamos ver que, quando somarmos as previsões das séries filhas, o resultado não é igual à previsão da série total.
from sktime.transformations.hierarchical.aggregate import Aggregator
+
+Aggregator().fit_transform(y_pred) - y_pred| + | + | + | sales | +
|---|---|---|---|
| group_id | +sku_id | +date | ++ |
| -1 | +20 | +2024-07-06 | +0.000000 | +
| 2024-07-07 | +0.000000 | +||
| 2024-07-08 | +0.000000 | +||
| 2024-07-09 | +0.000000 | +||
| 2024-07-10 | +0.000000 | +||
| ... | +... | +... | +... | +
| __total | +__total | +2024-12-28 | +-195.866868 | +
| 2024-12-29 | +-113.260571 | +||
| 2024-12-30 | +-294.224862 | +||
| 2024-12-31 | +-311.490070 | +||
| 2025-01-01 | +-256.487505 | +
5580 rows × 1 columns
+Existe uma diferença… ou seja, os valores não batem. Imagine o impacto de levar previsões incoerentes para a tomada de decisão em uma empresa? A raiz do problema é que temos mais modelos que graus de liberdade. Para ilustrar, suponha que temos 3 séries: \(A\), \(B\) e \(C\), onde:
+\[ +C(t) = A(t) + B(t) +\]
+Aqui, temos 3 séries, mas apenas 2 graus de liberdade, pois \(C\) é completamente determinado por \(A\) e \(B\). Se fizermos previsões independentes para \(A\), \(B\) e \(C\), não há garantia de que a relação acima será mantida nas previsões.
+
Existem diferentes métodos para reconciliar previsões em séries temporais hierárquicas. Não existe uma solução única, e o melhor método depende dos dados e do contexto.
+A maneira mais simples de reconcialiar previsões hierárquicas é a abordagem bottom-up. Nessa abordagem, fazemos previsões apenas para as séries mais baixas na hierarquia (as séries filhas) e depois somamos essas previsões para obter as previsões das séries superiores (as séries pais).
+
Lados positivos:
+No entanto, essa abordagem também tem desvantagens: é sucetível ao ruído nas séries filhas, e se as séries filhas tiverem pouca informação, as previsões podem ser ruins. Por exemplo, muitos zeros nas séries de níveis baixos pode levar a previsões ruins a niveis agregados.
+from sktime.transformations.hierarchical.reconcile import BottomUpReconciler
+
+bottom_up = BottomUpReconciler() * forecaster
+bottom_up.fit(y_train)
+
+y_pred_bottomup = bottom_up.predict(fh=fh)Agora vemos que as previsões são coerentes:
+Aggregator().fit_transform(y_pred_bottomup) - y_pred_bottomup| + | + | + | sales | +
|---|---|---|---|
| group_id | +sku_id | +date | ++ |
| -1 | +20 | +2024-07-06 | +0.0 | +
| 2024-07-07 | +0.0 | +||
| 2024-07-08 | +0.0 | +||
| 2024-07-09 | +0.0 | +||
| 2024-07-10 | +0.0 | +||
| ... | +... | +... | +... | +
| __total | +__total | +2024-12-28 | +0.0 | +
| 2024-12-29 | +0.0 | +||
| 2024-12-30 | +0.0 | +||
| 2024-12-31 | +0.0 | +||
| 2025-01-01 | +0.0 | +
5580 rows × 1 columns
+Outra abordagem é a top-down. Nessa abordagem, fazemos previsões apenas para as séries superiores na hierarquia (as séries pais) e depois distribuímos essas previsões para as séries filhas com base em proporções previstas.
+Suponha que temos a seguinte hierarquia \(C(t) = A(t) + B(t)\). Considere \(\hat{C}(t)\), \(\hat{A}(t)\) e \(\hat{B}(t)\) como as previsões para \(C\), \(A\) e \(B\), respectivamente. Na abordagem top-down, faríamos o seguinte:
+\[ +p_B(t) = \frac{\hat{B}(t)}{\hat{A}(t) + \hat{B}(t)} +\]
+\[ +\tilde{B}(t) = p_B(t) \cdot \hat{C}(t) +\]
+Essa abordagem é capaz de usufruir da qualidade do forecast total, e ainda consegue distribuir para as séries filhas baseadas no histórico.
+
O que chamam de “Proporções históricas” é equivalente a esse método, mas com um modelo Naive para prever as proporções.
+Esse método pode ser bom quando o forecast total é de boa qualidade. No entanto, dependemos profundamente da qualidade do forecast total e das proporções.
+from sktime.transformations.hierarchical.reconcile import TopdownReconciler
+
+top_down_fcst = TopdownReconciler() * forecaster
+top_down_fcst.fit(y_train)
+
+y_pred_topdown = top_down_fcst.predict(fh=fh)Existe uma abordagem mais sofisticada, com uma intuição geométrica interessante. A ideia é ajustar as previsões iniciais para que elas satisfaçam as restrições de soma da hierarquia. Por exemplo, para a hierarquia \(C(t) = A(t) + B(t)\), queremos garantir que:
+\[ +\hat{C}(t) = \hat{A}(t) + \hat{B}(t) +\]
+Se consideramos nosso espaço 3D de observações \((\hat{A}, \hat{B}, \hat{C})\), a condição acima é satisfeita para um plano 2D nesse universo.
+
Podemos então projetar nossas previsões iniciais nesse plano para obter previsões coerentes. Essa projeção pode ser feita de várias maneiras, levando a diferentes métodos de reconciliação ótima. Os métodos levam o nome “OLS” pois a projeção é feita minimizando o erro quadrático (Ordinary Least Squares).
+Para a reconciliação ótima com OLS, podemos usar o OptimalReconciler do sktime:
from sktime.transformations.hierarchical.reconcile import OptimalReconciler
+
+optimal = OptimalReconciler("ols") * forecaster
+optimal.fit(y_train)
+y_pred_optimal = optimal.predict(fh=fh)from sktime.utils.plotting import plot_series
+import matplotlib.pyplot as plt
+import pandas as pd
+
+
+idx = y_train.index.droplevel(-1).unique()[10]
+
+plot_series(
+ y_train.loc[idx,],
+ y_test.loc[idx,],
+ y_pred.loc[idx,],
+ y_pred_optimal.loc[idx,],
+ labels=["Train", "Test", "Predicted (sem reconciliação)", "Predicted (ótimo)"],
+)
+plt.xlim(pd.to_datetime("2024-05-01"), None)
+plt.show()
Para reconciliações ótimas (que usam a covariância do erro), podemos usar o ReconcilerForecaster do sktime, que internamente já faz o cálculo da covariância do erro:
from sktime.forecasting.reconcile import ReconcilerForecaster
+
+
+mint_forecaster = ReconcilerForecaster(
+ forecaster=forecaster,
+ method="mint_shrink")
+
+mint_forecaster.fit(y_train)
+y_pred_mint = mint_forecaster.predict(fh=fh)from sktime.performance_metrics.forecasting import MeanSquaredScaledError
+
+metric = MeanSquaredScaledError(multilevel="uniform_average_time")
+
+pd.DataFrame(
+ {
+ "Baseline": metric(y_test, y_pred, y_train=y_train),
+ "BottomUpReconciler": metric(y_test, y_pred_bottomup, y_train=y_train),
+ "TopDownReconciler": metric(y_test, y_pred_topdown, y_train=y_train),
+ "OptimalReconciler (ols)": metric(y_test, y_pred_optimal, y_train=y_train),
+ "Mint Reconciler": metric(y_test, y_pred_mint, y_train=y_train),
+ },
+ index=["Mean Absolute Scaled Error"],
+)| + | Baseline | +BottomUpReconciler | +TopDownReconciler | +OptimalReconciler (ols) | +Mint Reconciler | +
|---|---|---|---|---|---|
| Mean Absolute Scaled Error | +40.172892 | +82.744957 | +38.150928 | +39.847359 | +33.014674 | +
Nesse capítulo vamos ver as maneiras de usar modelos de Machine Learning para forecasting. Aqui é onde mais acontecem erros de novos praticantes, pois muitas vezes tentam aplicar modelos de ML diretamente na série temporal.
+Para usar um modelo de ML, precisamos transformar a série temporal em um problema de regressão tradicional. Isso é feito criando janelas deslizantes (sliding windows) da série temporal, onde cada janela é usada como uma amostra de treinamento para o modelo de ML.
+Ou seja, se temos uma série temporal \((y_t)\), podemos criar janelas de tamanho \(n\) e usar os valores \(y_{t-n}, y_{t-n+1}, \ldots, y_{t-1}\) como características (features) para prever o valor \(y_t\).
+
Para prever mais de um passo à frente, existem duas abordagens:
+A verdade é que as duas abordagens podem ser vistas como uma: a previsão recursiva pode ser vista como uma previsão direta para \(h=1\).
+from tsbook.datasets.retail import SyntheticRetail
+from sktime.utils.plotting import plot_series
+from sktime.forecasting.naive import NaiveForecaster
+
+dataset = SyntheticRetail("univariate")
+y_train, X_train, y_test, X_test = dataset.load(
+ "y_train", "X_train", "y_test", "X_test"
+)A tendência em séries temporais é como um constante problema de data drift:
+_X = [y_train.iloc[i : i + 7] for i in range(0, 700)]
+
+_X_test = [y_train.iloc[i : i + 7] for i in range(700, 800)]
+
+
+def set_index(x):
+ x.index = range(len(x))
+ return x
+
+
+_X = [set_index(x) for x in _X]
+_X_test = [set_index(x) for x in _X_test]
+
+import matplotlib.pyplot as plt
+
+fig, ax = plt.subplots()
+for x in _X:
+ ax.plot(x, color="gray", alpha=0.3)
+for x in _X_test:
+ ax.plot(x, color="red", alpha=0.3)
+
+# Add legend, with 1 red line for test and 1 gray for train
+from matplotlib.lines import Line2D
+
+legend_handles = [
+ Line2D([0], [0], color="gray", alpha=0.3, lw=2, label="Treino"),
+ Line2D([0], [0], color="red", alpha=0.3, lw=2, label="Teste"),
+]
+ax.legend(handles=legend_handles, loc="best")
+ax.set_title("Série original - Magnitudes diferentes para cada janela")
+fig.show()
Quando criamos nossas janelas e olhamos treine e teste, esse problema fica claro. A informação de uma série em treino não é util para prever a série de teste, pois elas estão em magnitudes diferentes.
+Uma possível solução para isso é normalizar cada janela, dividindo pelo valor médio da janela. Assim, todas as janelas ficam na mesma escala:
+_X = [x / x.mean() for x in _X]
+_X_test = [x / x.mean() for x in _X_test]
+
+fig, ax = plt.subplots()
+for x in _X:
+ ax.plot(x, color="gray", alpha=0.3)
+for x in _X_test:
+ ax.plot(x, color="red", alpha=0.3)
+
+ax.legend(handles=legend_handles, loc="best")
+ax.set_title("Série normalizada")
+fig.show()
e podemos prever sem problemas.
+Outra possibilidade é a diferenciação, como já vimos em capítulos anteriores. A diferenciação remove a tendência da série, tornando-a estacionária.
+Primeiro, vamos import ReductionForecaster, que é a classe que implementa a abordagem de janelas deslizantes para usar modelos de ML em séries temporais. Vamos testar um primeiro caso sem nenhum tipo de preprocessamento, apenas criando as janelas:
from tsbook.forecasting.reduction import ReductionForecaster
+from sklearn.ensemble import RandomForestRegressor
+
+model = ReductionForecaster(
+ RandomForestRegressor(n_estimators=100, random_state=42),
+ window_length=30,
+)
+
+model.fit(y_train, X=X_train)ReductionForecaster(estimator=RandomForestRegressor(random_state=42), + window_length=30)Please rerun this cell to show the HTML repr or trust the notebook.
ReductionForecaster(estimator=RandomForestRegressor(random_state=42), + window_length=30)
RandomForestRegressor(random_state=42)
RandomForestRegressor(random_state=42)
y_pred = model.predict(fh=y_test.index, X=X_test)
+plot_series(y_train, y_test, y_pred, labels=["Treino", "Teste", "Previsão com ML"])
+plt.show()
Claramente, tivemos o problema que mencionamos anteriormente.
+Uma solução é usar a diferenciação para remover a tendência da série.
+from sktime.transformations.series.difference import Differencer
+
+regressor = RandomForestRegressor(n_estimators=100, random_state=42)
+
+model = Differencer() * ReductionForecaster(
+ regressor,
+ window_length=30,
+ steps_ahead=1,
+)
+
+model.fit(y_train, X=X_train)TransformedTargetForecaster(steps=[Differencer(), + ReductionForecaster(estimator=RandomForestRegressor(random_state=42), + window_length=30)])Please rerun this cell to show the HTML repr or trust the notebook.
TransformedTargetForecaster(steps=[Differencer(), + ReductionForecaster(estimator=RandomForestRegressor(random_state=42), + window_length=30)])
Differencer()
ReductionForecaster(estimator=RandomForestRegressor(random_state=42), + window_length=30)
RandomForestRegressor(random_state=42)
RandomForestRegressor(random_state=42)
y_pred_diff = model.predict(fh=y_test.index, X=X_test)
+
+plot_series(
+ y_train, y_test, y_pred_diff, labels=["Treino", "Teste", "Previsão com ML + Diferença"]
+)
+plt.show()
Aqui, já vemos uma melhora significativa. Mas tem algo que podemos melhorar para realizar a diferenciação? Sim. Lembre que essa série tem um padrão multiplicativo. Então, antes de aplicar a diferenciação, podemos aplicar uma transformação logarítmica para estabilizar a variância:
+from sktime.transformations.series.boxcox import LogTransformer
+
+model_log = LogTransformer() * model
+
+model_log.fit(y_train, X=X_train)
+y_pred_log_diff = model_log.predict(fh=y_test.index, X=X_test)
+plot_series(
+ y_train,
+ y_test,
+ y_pred_log_diff,
+ labels=["Treino", "Teste", "Previsão com ML + Log + Diferença"],
+)
A diferenciação aumenta o ruído da série, o que pode dificultar o trabalho do modelo de ML. Outra opção é normalizar em cada janela. A classe ReductionForecaster tem um parâmetro chamado normalization_strategy, que pode ser usado para determinar a estratégia de normalização. Vamos usar a estratégia divide_mean, que divide cada janela pelo seu valor médio.
model = ReductionForecaster(
+ regressor,
+ window_length=30,
+ steps_ahead=1,
+ normalization_strategy="divide_mean",
+)
+
+model.fit(y_train, X=X_train)
+y_pred_norm = model.predict(fh=y_test.index, X=X_test)
+
+plot_series(
+ y_train, y_test, y_pred_norm, labels=["Treino", "Teste", "Previsão com ML + Normalização"]
+)
Podemos fazer um conjunto de modelos, um para cada passo à frente. Abaixo, definimos steps_ahead=12, o que significa que o modelo vai prever 12 passos à frente diretamente.
model = ReductionForecaster(
+ regressor,
+ window_length=30,
+ steps_ahead=12,
+ normalization_strategy="divide_mean",
+)
+
+model.fit(y_train, X=X_train)
+y_pred_norm_direct = model.predict(fh=y_test.index, X=X_test)plot_series(
+ y_train,
+ y_test,
+ y_pred_norm_direct,
+ labels=["Treino", "Teste", "Previsão com ML + Normalização"],
+)
Podemos comparar o MAPE de todos os modelos:
+from sktime.performance_metrics.forecasting import MeanAbsolutePercentageError
+
+mape = MeanAbsolutePercentageError()
+
+results = {}
+for _y_pred, label in zip(
+ [
+ y_pred,
+ y_pred_diff,
+ y_pred_log_diff,
+ y_pred_norm,
+ y_pred_norm_direct,
+ ],
+ [
+ "ML",
+ "ML + Diferença",
+ "ML + Log + Diferença",
+ "ML + Normalização",
+ "ML + Normalização + Direto",
+ ],
+):
+ results[label] = mape(y_test, _y_pred)
+
+import pandas as pd
+
+results = pd.DataFrame.from_dict(results, orient="index", columns=["MAPE"])
+results.sort_values("MAPE")| + | MAPE | +
|---|---|
| ML + Log + Diferença | +0.125494 | +
| ML + Normalização | +0.128436 | +
| ML + Normalização + Direto | +0.138206 | +
| ML + Diferença | +0.178693 | +
| ML | +0.243593 | +
Em muitas aplicações, não temos acesso a uma única série temporal, mas sim a um conjunto de séries temporais relacionadas. Isso é comum em cenários como vendas de produtos em diferentes lojas, consumo de energia em diferentes regiões, etc. Esses dados são chamados de dados em painel.
+Uma ideia poderosa é aproveitar a similaridade entre as séries para melhorar as previsões. Chamamos de modelos globais os modelos capazes de aprender padrões comuns entre as séries, ao contrário dos modelos locais que aprendem apenas com uma única série.
+A maioria dos modelos clássicos de séries temporais são locais. Modelos globais são, em geral, baseados em modelos tabulares de ML ou deep learning. Segundo competições de séries temporais, como a M5, em forecasts de painel os modelos globais são os que apresentam melhor desempenho (Makridakis, Spiliotis, and Assimakopoulos 2022).
+Aqui, vamos usar o dataset sintético que vimos antes, mas agora teremos acesso às várias séries temporais que compõe o total.
+Esse dataset é feito para simular um caso de varejo, onde temos vendas diárias de vários produtos:
+import pandas as pd
+import matplotlib.pyplot as plt
+
+from sktime.utils.plotting import plot_seriesfrom tsbook.datasets.retail import SyntheticRetail
+dataset = SyntheticRetail("panel")
+y_train, X_train, y_test, X_test = dataset.load(
+ "y_train", "X_train", "y_test", "X_test"
+)Note que, para dados em painel, os dataframes possuem mais um nível de índice, que identifica a série temporal a que cada observação pertence:
+display(X_train)| + | + | promo | +
|---|---|---|
| sku_id | +date | ++ |
| 0 | +2020-01-01 | +0.0 | +
| 2020-01-02 | +0.0 | +|
| 2020-01-03 | +0.0 | +|
| 2020-01-04 | +0.0 | +|
| 2020-01-05 | +0.0 | +|
| ... | +... | +... | +
| 24 | +2024-07-01 | +1.0 | +
| 2024-07-02 | +0.0 | +|
| 2024-07-03 | +0.0 | +|
| 2024-07-04 | +0.0 | +|
| 2024-07-05 | +0.0 | +
41200 rows × 1 columns
+Podemos visualizar algumas séries. Vemos que há mais zeros nesse dataset, em comparação ao que usamos antes.
+from sktime.utils.plotting import plot_series
+
+fig, ax = plt.subplots(figsize=(10, 4))
+y_train.unstack(level=0).droplevel(0, axis=1).iloc[:, [0,10]].plot(ax=ax, alpha=0.7)
+plt.show()
Para trabalhar com essas estruturas de dados, é importante revisar algumas operações do pandas.
+y_train.index.get_level_values(-1)PeriodIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-04',
+ '2020-01-05', '2020-01-06', '2020-01-07', '2020-01-08',
+ '2020-01-09', '2020-01-10',
+ ...
+ '2024-06-26', '2024-06-27', '2024-06-28', '2024-06-29',
+ '2024-06-30', '2024-07-01', '2024-07-02', '2024-07-03',
+ '2024-07-04', '2024-07-05'],
+ dtype='period[D]', name='date', length=41200)
+As seguintes operações são bem úteis para trabalhar com multi-índices:
+y_train.indexMultiIndex([( 0, '2020-01-01'),
+ ( 0, '2020-01-02'),
+ ( 0, '2020-01-03'),
+ ( 0, '2020-01-04'),
+ ( 0, '2020-01-05'),
+ ( 0, '2020-01-06'),
+ ( 0, '2020-01-07'),
+ ( 0, '2020-01-08'),
+ ( 0, '2020-01-09'),
+ ( 0, '2020-01-10'),
+ ...
+ (24, '2024-06-26'),
+ (24, '2024-06-27'),
+ (24, '2024-06-28'),
+ (24, '2024-06-29'),
+ (24, '2024-06-30'),
+ (24, '2024-07-01'),
+ (24, '2024-07-02'),
+ (24, '2024-07-03'),
+ (24, '2024-07-04'),
+ (24, '2024-07-05')],
+ names=['sku_id', 'date'], length=41200)
+Acessar valores únicos no primeiro nivel (nível 0, mais à esquerda):
+y_train.index.get_level_values(0).unique()Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
+ 18, 19, 20, 21, 22, 23, 24],
+ dtype='int64', name='sku_id')
+Selecionar uma série específica (nível 0 igual a 0):
+y_train.loc[0]| + | sales | +
|---|---|
| date | ++ |
| 2020-01-01 | +0 | +
| 2020-01-02 | +0 | +
| 2020-01-03 | +0 | +
| 2020-01-04 | +0 | +
| 2020-01-05 | +0 | +
| ... | +... | +
| 2024-07-01 | +220 | +
| 2024-07-02 | +128 | +
| 2024-07-03 | +47 | +
| 2024-07-04 | +73 | +
| 2024-07-05 | +165 | +
1648 rows × 1 columns
+Aqui, podemos usar pd.IndexSlice para selecionar várias séries ao mesmo tempo. Note que pd.IndexSlice é passado diretamente para .loc:
y_train.loc[pd.IndexSlice[[0,2], :]]| + | + | sales | +
|---|---|---|
| sku_id | +date | ++ |
| 0 | +2020-01-01 | +0 | +
| 2020-01-02 | +0 | +|
| 2020-01-03 | +0 | +|
| 2020-01-04 | +0 | +|
| 2020-01-05 | +0 | +|
| ... | +... | +... | +
| 2 | +2024-07-01 | +125 | +
| 2024-07-02 | +185 | +|
| 2024-07-03 | +179 | +|
| 2024-07-04 | +210 | +|
| 2024-07-05 | +236 | +
3296 rows × 1 columns
+Agora, para selecionar o horizonte de forecasting, temos que chamar unique:
fh = y_test.index.get_level_values(1).unique()
+
+fhPeriodIndex(['2024-07-06', '2024-07-07', '2024-07-08', '2024-07-09',
+ '2024-07-10', '2024-07-11', '2024-07-12', '2024-07-13',
+ '2024-07-14', '2024-07-15',
+ ...
+ '2024-12-23', '2024-12-24', '2024-12-25', '2024-12-26',
+ '2024-12-27', '2024-12-28', '2024-12-29', '2024-12-30',
+ '2024-12-31', '2025-01-01'],
+ dtype='period[D]', name='date', length=180)
+Nem todos modelos suportam nativamente dados em painel. Por exemplo, exponential smoothing. Aqui, temos uma boa notícia: sem linhas extras necessárias. O sktime faz upcasting automático para dados em painel ao usar estimadores do sktime.
from sktime.forecasting.naive import NaiveForecaster
+
+
+naive_forecaster = NaiveForecaster(strategy="last", window_length=1)
+naive_forecaster.fit(y_train)
+y_pred_naive = naive_forecaster.predict(fh=fh)
+
+y_pred_naive| + | + | sales | +
|---|---|---|
| sku_id | +date | ++ |
| 0 | +2024-07-06 | +165.0 | +
| 2024-07-07 | +165.0 | +|
| 2024-07-08 | +165.0 | +|
| 2024-07-09 | +165.0 | +|
| 2024-07-10 | +165.0 | +|
| ... | +... | +... | +
| 24 | +2024-12-28 | +209.0 | +
| 2024-12-29 | +209.0 | +|
| 2024-12-30 | +209.0 | +|
| 2024-12-31 | +209.0 | +|
| 2025-01-01 | +209.0 | +
4500 rows × 1 columns
+Internamente, o sktime cria um clone do estimador para cada série nos dados em painel. Em seguida, cada clone é treinado com a série correspondente. Isso é feito de forma transparente para usuário, mas sem exigir esforço.
O atributo forecasters_ armazena um DataFrame com os estimatores de cada série.
naive_forecaster.forecasters_.head()| + | forecasters | +
|---|---|
| 0 | +NaiveForecaster(window_length=1) | +
| 1 | +NaiveForecaster(window_length=1) | +
| 2 | +NaiveForecaster(window_length=1) | +
| 3 | +NaiveForecaster(window_length=1) | +
| 4 | +NaiveForecaster(window_length=1) | +
É dificil explicar o quanto isso é extremamente útil para código limpo e prototipagem rápida. Foi um dos motivos que me levaram a usar o sktime.
Agora que temos várias séries, precisamos explicar como calcular métricas de avaliação. O sktime oferece duas opções para isso, como argumentos na criação da métrica:
+multilevel="uniform_average_time" para calcular a média das séries temporais no painel.multilevel="raw_values" para obter o erro por série.from sktime.performance_metrics.forecasting import MeanSquaredScaledError
+
+metric = MeanSquaredScaledError(multilevel="uniform_average_time")metric(y_true=y_test, y_pred=y_pred_naive, y_train=y_train)np.float64(19.04893558607091)
+Na prática, as métricas que a sua aplicação exige podem ser diferentes. Por exemplo, as séries temporais podem ter diferentes importâncias, e você pode querer ponderar as métricas de acordo.
+Para isso, é possível criar uma métrica customizada no sktime, mas não entraremos nesse mérito aqui.
+Quando vimos como usar modelos de Machine Learning para forecasting, já mencionamos como é necessário traduzir o problema de séries temporais para um problema de regressão tradicional.
+No caso de dados em painel, também podemos usar essa abordagem, mas agora aproveitando todas as séries temporais para treinar um único modelo global.
+
Abaixo, vamos comparar um LightGBM global com um local. Veremos o seguinte: o modelo local é melhor que o modelo global, se não processarmos os dados corretamente para o modelo global aproveitar as similaridades entre as séries!
+from tsbook.forecasting.reduction import ReductionForecaster
+from lightgbm import LGBMRegressor
+
+global_forecaster1 = ReductionForecaster(
+ LGBMRegressor(n_estimators=100, verbose=-1),
+ window_length=30,
+)
+
+global_forecaster1.fit(y_train, X_train)ReductionForecaster(estimator=LGBMRegressor(verbose=-1), window_length=30)Please rerun this cell to show the HTML repr or trust the notebook.
ReductionForecaster(estimator=LGBMRegressor(verbose=-1), window_length=30)
LGBMRegressor(verbose=-1)
LGBMRegressor(verbose=-1)
y_pred_global1 = global_forecaster1.predict(fh=fh, X=X_test)fig, ax = plt.subplots(figsize=(10, 4))
+y_train.loc[10, "sales"].plot(ax=ax, label="Treino")
+y_test.loc[10, "sales"].plot(ax=ax, label="Teste")
+y_pred_global1.loc[10, "sales"].plot(ax=ax, label="Global 1")
+plt.legend()
+plt.show()
Para forçar que um modelo global funcione como um modelo local, podemos usar ForecastByLevel, que cria um modelo separado para cada série temporal, mesmo quando o estimador suporta dados em painel.
from sktime.forecasting.compose import ForecastByLevel
+
+local_forecaster1 = ForecastByLevel(global_forecaster1, groupby="local")
+
+local_forecaster1.fit(y_train, X=X_train)ForecastByLevel(forecaster=ReductionForecaster(estimator=LGBMRegressor(verbose=-1), + window_length=30))Please rerun this cell to show the HTML repr or trust the notebook.
ForecastByLevel(forecaster=ReductionForecaster(estimator=LGBMRegressor(verbose=-1), + window_length=30))
ReductionForecaster(estimator=LGBMRegressor(verbose=-1), window_length=30)
LGBMRegressor(verbose=-1)
LGBMRegressor(verbose=-1)
y_pred_local1 = local_forecaster1.predict(fh=fh, X=X_test)
+
+err_global1 = metric(y_true=y_test, y_pred=y_pred_global1, y_train=y_train)
+err_local1 = metric(y_true=y_test, y_pred=y_pred_local1, y_train=y_train)
+
+errors = pd.DataFrame(
+ {
+ "Global (1)": [err_global1],
+ "Local (1)": [err_local1],
+ },
+ index=["MSE"],
+)Sabemos como preprocessar séries temporais univariadas para melhorar o desempenho dos modelos de ML. Aplicamos da mesma maneira que fizemos anteriormente o Differencer, com objetivo de remover tendências.
from sktime.transformations.series.difference import Differencer
+
+
+global_forecaster2 = Differencer() * global_forecaster1
+global_forecaster2.fit(y_train, X_train)TransformedTargetForecaster(steps=[Differencer(), + ReductionForecaster(estimator=LGBMRegressor(verbose=-1), + window_length=30)])Please rerun this cell to show the HTML repr or trust the notebook.
TransformedTargetForecaster(steps=[Differencer(), + ReductionForecaster(estimator=LGBMRegressor(verbose=-1), + window_length=30)])
Differencer()
ReductionForecaster(estimator=LGBMRegressor(verbose=-1), window_length=30)
LGBMRegressor(verbose=-1)
LGBMRegressor(verbose=-1)
y_pred_global2 = global_forecaster2.predict(fh=fh, X=X_test)
+metric_global2 = metric(y_true=y_test, y_pred=y_pred_global2, y_train=y_train)E agora sua versão local:
+local_forecaster2 = ForecastByLevel(global_forecaster2, groupby="local")
+local_forecaster2.fit(y_train, X=X_train)
+
+y_pred_local2 = local_forecaster2.predict(fh=fh, X=X_test)
+metric_local2 = metric(y_true=y_test, y_pred=y_pred_local2, y_train=y_train)Agora, podemos comparar:
+errors["Global (2)"] = metric_global2
+errors["Local (2)"] = metric_local2
+
+errors| + | Global (1) | +Local (1) | +Global (2) | +Local (2) | +
|---|---|---|---|---|
| MSE | +41.057873 | +23.720592 | +22.238809 | +67.626298 | +
Note como já superamos o modelo global incial, e o modelo local. Isso é para destacar que é essencial realizar um bom preprocessamento e engenharia de features para que modelos de Machine Learning tenham bom desempenho em dados em painel.
+Agora, vamos usar a normalização por janela, que é especialmente útil em dados em painel, onde as séries podem ter diferentes escalas.
+global_forecaster3 = global_forecaster1.clone().set_params(
+ normalization_strategy="divide_mean"
+)
+
+global_forecaster3.fit(y_train, X_train)ReductionForecaster(estimator=LGBMRegressor(verbose=-1), + normalization_strategy='divide_mean', window_length=30)Please rerun this cell to show the HTML repr or trust the notebook.
ReductionForecaster(estimator=LGBMRegressor(verbose=-1), + normalization_strategy='divide_mean', window_length=30)
LGBMRegressor(verbose=-1)
LGBMRegressor(verbose=-1)
# Predict
+y_pred_global3 = global_forecaster3.predict(fh=fh, X=X_test)
+
+# Métrica
+metric_global3 = metric(y_true=y_test, y_pred=y_pred_global3, y_train=y_train)
+
+errors["Global 3 (window norm)"] = metric_global3
+
+display(errors)| + | Global (1) | +Local (1) | +Global (2) | +Local (2) | +Global 3 (window norm) | +
|---|---|---|---|---|---|
| MSE | +41.057873 | +23.720592 | +22.238809 | +67.626298 | +15.317953 | +
Vemos que resultados são ainda melhores!
+Podemos ajudar o modelo a capturar sazonalidades adicionando features de Fourier como features exógenas.
+Usamos ** para criar um pipeline aplicado sobre as features exógenas:
from sktime.transformations.series.fourier import FourierFeatures
+
+fourier_features = FourierFeatures(
+ sp_list=[365.25, 365.25 / 12], fourier_terms_list=[1, 1], freq="D"
+)
+
+global_forecaster4 = fourier_features**global_forecaster3
+global_forecaster4.fit(y_train, X_train)ForecastingPipeline(steps=[FourierFeatures(fourier_terms_list=[1, 1], freq='D', + sp_list=[365.25, 30.4375]), + ReductionForecaster(estimator=LGBMRegressor(verbose=-1), + normalization_strategy='divide_mean', + window_length=30)])Please rerun this cell to show the HTML repr or trust the notebook.
ForecastingPipeline(steps=[FourierFeatures(fourier_terms_list=[1, 1], freq='D', + sp_list=[365.25, 30.4375]), + ReductionForecaster(estimator=LGBMRegressor(verbose=-1), + normalization_strategy='divide_mean', + window_length=30)])
FourierFeatures(fourier_terms_list=[1, 1], freq='D', sp_list=[365.25, 30.4375])
ReductionForecaster(estimator=LGBMRegressor(verbose=-1), + normalization_strategy='divide_mean', window_length=30)
LGBMRegressor(verbose=-1)
LGBMRegressor(verbose=-1)
y_pred_global4 = global_forecaster4.predict(fh=fh, X=X_test)
+metric_global4 = metric(y_true=y_test, y_pred=y_pred_global4, y_train=y_train)
+
+errors["Global 4 (fourier)"] = metric_global4
+errors| + | Global (1) | +Local (1) | +Global (2) | +Local (2) | +Global 3 (window norm) | +Global 4 (fourier) | +
|---|---|---|---|---|---|---|
| MSE | +41.057873 | +23.720592 | +22.238809 | +67.626298 | +15.317953 | +15.143648 | +
Uma técnica muito adotada é fazer modelos globais por clusters de séries temporais similares.
+Uma maneira de categorizar é usando ADI (Average Demand Interval) e CV² (squared Coefficient of Variation). O componente ADI é calculado como o percentual de períodos com demanda (y>0), e \(CV^2\) é o quadrado do coeficiente de variação das demandas positivas.
+| Categoria | +ADI | +CV² | +Padrão típico | +Exemplos | +
|---|---|---|---|---|
| Suave (Smooth) | +≤ 1,32 | +≤ 0,49 | +Demanda contínua e estável | +Itens de consumo diário, alimentos | +
| Errática (Erratic) | +≤ 1,32 | +> 0,49 | +Demanda contínua, porém muito variável | +Moda, eletrônicos | +
| Intermitente (Intermittent) | +> 1,32 | +≤ 0,49 | +Muitos períodos sem venda, mas valores estáveis quando ocorre | +Peças de reposição, ferramentas | +
| Irregular (Lumpy) | +> 1,32 | +> 0,49 | +Muitos períodos com zero e valores muito variáveis quando há demanda | +Equipamentos caros, sobressalentes grandes | +

from sktime.forecasting.compose import GroupbyCategoryForecaster
+from sktime.transformations.series.adi_cv import ADICVTransformer
+
+# TODO: customize yours!
+group_forecaster = GroupbyCategoryForecaster(
+ forecasters =
+ {"smooth": global_forecaster3.clone(),
+ "erratic": global_forecaster3.clone(),
+ "intermittent": global_forecaster3.clone(),
+ "lumpy": global_forecaster3.clone(),
+ },
+ transformer=ADICVTransformer(features=["class"],))
+
+
+group_forecaster.fit(y_train, X_train)GroupbyCategoryForecaster(forecasters={'erratic': ReductionForecaster(estimator=LGBMRegressor(verbose=-1),
+ normalization_strategy='divide_mean',
+ window_length=30),
+ 'intermittent': ReductionForecaster(estimator=LGBMRegressor(verbose=-1),
+ normalization_strategy='divide_mean',
+ window_length=30),
+ 'lumpy': ReductionForecaster(estimator=LGBMRegressor(verbose=-1),
+ normalization_strategy='divide_mean',
+ window_length=30),
+ 'smooth': ReductionForecaster(estimator=LGBMRegressor(verbose=-1),
+ normalization_strategy='divide_mean',
+ window_length=30)},
+ transformer=ADICVTransformer(features=['class']))Please rerun this cell to show the HTML repr or trust the notebook.GroupbyCategoryForecaster(forecasters={'erratic': ReductionForecaster(estimator=LGBMRegressor(verbose=-1),
+ normalization_strategy='divide_mean',
+ window_length=30),
+ 'intermittent': ReductionForecaster(estimator=LGBMRegressor(verbose=-1),
+ normalization_strategy='divide_mean',
+ window_length=30),
+ 'lumpy': ReductionForecaster(estimator=LGBMRegressor(verbose=-1),
+ normalization_strategy='divide_mean',
+ window_length=30),
+ 'smooth': ReductionForecaster(estimator=LGBMRegressor(verbose=-1),
+ normalization_strategy='divide_mean',
+ window_length=30)},
+ transformer=ADICVTransformer(features=['class']))ADICVTransformer(features=['class'])
ADICVTransformer(features=['class'])
ReductionForecaster(estimator=LGBMRegressor(verbose=-1), + normalization_strategy='divide_mean', window_length=30)
LGBMRegressor(verbose=-1)
LGBMRegressor(verbose=-1)
ReductionForecaster(estimator=LGBMRegressor(verbose=-1), + normalization_strategy='divide_mean', window_length=30)
LGBMRegressor(verbose=-1)
LGBMRegressor(verbose=-1)
ReductionForecaster(estimator=LGBMRegressor(verbose=-1), + normalization_strategy='divide_mean', window_length=30)
LGBMRegressor(verbose=-1)
LGBMRegressor(verbose=-1)
ReductionForecaster(estimator=LGBMRegressor(verbose=-1), + normalization_strategy='divide_mean', window_length=30)
LGBMRegressor(verbose=-1)
LGBMRegressor(verbose=-1)
None
None
y_pred_group = group_forecaster.predict(fh=fh, X=X_test)
+metric_group = metric(y_true=y_test, y_pred=y_pred_group, y_train=y_train)
+
+metric_groupnp.float64(53.56051503265084)
+Although it did not perform better than the best global model in this synthetic example, in real-world scenarios this approach can be very effective, particularly when there are large samples for each category, allowing the model to learn specific patterns for each group of series.
+In addition, this can be useful when there are computation constraints, as training multiple smaller models for each cluster can be more efficient than training a single large global model.
+Aqui, vimos como trabalhar com dados em painel (multi-series) usando o sktime. Vimos, especialmente, como criar modelos globais de Machine Learning que aproveitam as similaridades entre as séries para melhorar o desempenho das previsões. Também destacamos a importância do preprocessamento e da engenharia de features para obter bons resultados com esses modelos.
+Para uma análise mais aprofundada, recomendo o artigo de (Montero-Manso and Hyndman 2021), que discute princípios para forecasting em dados em painel usando modelos globais de Machine Learning.
+ + +
Esse é um pequeno livro feito para o workshop de sktime na Python Brasil 2025. O objetivo é apresentar os conceitos básicos de séries temporais e como usar o sktime para modelagem e previsão.
+Esse livro é raso em diversos aspectos, mas pode ser útil como ponto de partida para quem quer aprender mais sobre séries temporais em Python.
+Caso queira contribuir para sktime, visite o repositório oficial no GitHub e participe do nosso Discord.
+ + +Zi))l.moveTo(0,0);else if(v>O0-Zi)l.moveTo(m*_h(g),m*xl(g)),l.arc(0,0,m,g,y,!x),p>Zi&&(l.moveTo(p*_h(y),p*xl(y)),l.arc(0,0,p,y,g,x));else{var b=g,w=y,C=g,T=y,E=v,A=v,S=s.apply(this,arguments)/2,_=S>Zi&&(n?+n.apply(this,arguments):Td(p*p+m*m)),I=L5(t9(m-p)/2,+r.apply(this,arguments)),D=I,k=I,L,R;if(_>Zi){var O=r9(_/p*xl(S)),M=r9(_/m*xl(S));(E-=O*2)>Zi?(O*=x?1:-1,C+=O,T-=O):(E=0,C=T=(g+y)/2),(A-=M*2)>Zi?(M*=x?1:-1,b+=M,w-=M):(A=0,b=w=(g+y)/2)}var B=m*_h(b),F=m*xl(b),P=p*_h(T),z=p*xl(T);if(I>Zi){var $=m*_h(w),H=m*xl(w),Q=p*_h(C),j=p*xl(C),ie;if(v "),t.result!==null&&g.kind!==t.kind&&Qt(t,"unacceptable node kind for !<"+t.tag+'> tag; it should be "'+g.kind+'", not "'+t.kind+'"'),g.resolve(t.result,t.tag)?(t.result=g.construct(t.result,t.tag),t.anchor!==null&&(t.anchorMap[t.anchor]=t.result)):Qt(t,"cannot resolve a node with !<"+t.tag+"> explicit tag")}return t.listener!==null&&t.listener("close",t),t.tag!==null||t.anchor!==null||f}function xAe(t){var e=t.position,r,n,i,a=!1,s;for(t.version=null,t.checkLineBreaks=t.legacy,t.tagMap=Object.create(null),t.anchorMap=Object.create(null);(s=t.input.charCodeAt(t.position))!==0&&(Ci(t,!0,-1),s=t.input.charCodeAt(t.position),!(t.lineIndent>0||s!==37));){for(a=!0,s=t.input.charCodeAt(++t.position),r=t.position;s!==0&&!Ls(s);)s=t.input.charCodeAt(++t.position);for(n=t.input.slice(r,t.position),i=[],n.length<1&&Qt(t,"directive name must not be less than one character in length");s!==0;){for(;Nd(s);)s=t.input.charCodeAt(++t.position);if(s===35){do s=t.input.charCodeAt(++t.position);while(s!==0&&!dc(s));break}if(dc(s))break;for(r=t.position;s!==0&&!Ls(s);)s=t.input.charCodeAt(++t.position);i.push(t.input.slice(r,t.position))}s!==0&&hD(t),Gh.call(JX,n)?JX[n](t,n,i):bw(t,'unknown document directive "'+n+'"')}if(Ci(t,!0,-1),t.lineIndent===0&&t.input.charCodeAt(t.position)===45&&t.input.charCodeAt(t.position+1)===45&&t.input.charCodeAt(t.position+2)===45?(t.position+=3,Ci(t,!0,-1)):a&&Qt(t,"directives end mark is expected"),om(t,t.lineIndent-1,xw,!1,!0),Ci(t,!0,-1),t.checkLineBreaks&&nAe.test(t.input.slice(e,t.position))&&bw(t,"non-ASCII line breaks are interpreted as content"),t.documents.push(t.result),t.position===t.lineStart&&kw(t)){t.input.charCodeAt(t.position)===46&&(t.position+=3,Ci(t,!0,-1));return}if(t.position ${this.parser.parseInline(e)} An error occurred: ${i.tokens?.map(n).join("")}{"use strict";rm();Y0();o($Se,"copyObject");Po=$Se});function zSe(t,e){for(var r=-1,n=Array(t);++r
"},r),Ze.lineBreakRegex.test(t)))return t;let n=t.split(" ").filter(Boolean),i=[],a="";return n.forEach((s,l)=>{let u=ra(`${s} `,r),h=ra(a,r);if(u>e){let{hyphenatedStrings:p,remainingWord:m}=ACe(s,e,"-",r);i.push(a,...p),a=m}else h+u>=e?(i.push(a),a=s):a=[a,s].filter(Boolean).join(" ");l+1===n.length&&i.push(a)}),i.filter(s=>s!=="").join(r.joinWith)},(t,e,r)=>`${t}${e}${r.fontSize}${r.fontWeight}${r.fontFamily}${r.joinWith}`),ACe=H0((t,e,r="-",n)=>{n=Object.assign({fontSize:12,fontWeight:400,fontFamily:"Arial",margin:0},n);let i=[...t],a=[],s="";return i.forEach((l,u)=>{let h=`${s}${l}`;if(ra(h,n)>=e){let d=u+1,p=i.length===d,m=`${h}${r}`;a.push(p?h:m),s=""}else s=h}),{hyphenatedStrings:a,remainingWord:s}},(t,e,r="-",n)=>`${t}${e}${r}${n.fontSize}${n.fontWeight}${n.fontFamily}`);o(dw,"calculateTextHeight");o(ra,"calculateTextWidth");Q9=H0((t,e)=>{let{fontSize:r=12,fontFamily:n="Arial",fontWeight:i=400}=e;if(!t)return{width:0,height:0};let[,a]=Bo(r),s=["sans-serif",n],l=t.split(Ze.lineBreakRegex),u=[],h=Ge("body");if(!h.remove)return{width:0,height:0,lineHeight:0};let f=h.append("svg");for(let p of s){let m=0,g={width:0,height:0,lineHeight:0};for(let y of l){let v=SCe();v.text=y||H9;let x=CCe(f,v).style("font-size",a).style("font-weight",i).style("font-family",p),b=(x._groups||x)[0][0].getBBox();if(b.width===0&&b.height===0)throw new Error("svg element not in render tree");g.width=Math.round(Math.max(g.width,b.width)),m=Math.round(b.height),g.height+=m,g.lineHeight=Math.round(Math.max(g.lineHeight,m))}u.push(g)}f.remove();let d=isNaN(u[1].height)||isNaN(u[1].width)||isNaN(u[1].lineHeight)||u[0].height>u[1].height&&u[0].width>u[1].width&&u[0].lineHeight>u[1].lineHeight?0:1;return u[d]},(t,e)=>`${t}${e.fontSize}${e.fontWeight}${e.fontFamily}`),U9=class{constructor(e=!1,r){this.count=0;this.count=r?r.length:0,this.next=e?()=>this.count++:()=>Date.now()}static{o(this,"InitIDGenerator")}},_Ce=o(function(t){return fw=fw||document.createElement("div"),t=escape(t).replace(/%26/g,"&").replace(/%23/g,"#").replace(/%3B/g,";"),fw.innerHTML=t,unescape(fw.textContent)},"entityDecode");o(Z9,"isDetailedError");DCe=o((t,e,r,n)=>{if(!n)return;let i=t.node()?.getBBox();i&&t.append("text").text(n).attr("text-anchor","middle").attr("x",i.x+i.width/2).attr("y",-r).attr("class",e)},"insertTitle"),Bo=o(t=>{if(typeof t=="number")return[t,t+"px"];let e=parseInt(t??"",10);return Number.isNaN(e)?[void 0,void 0]:t===String(e)?[e,t+"px"]:[e,t]},"parseFontSize");o(Fi,"cleanAndMerge");Gt={assignWithDepth:Gn,wrapLabel:K9,calculateTextHeight:dw,calculateTextWidth:ra,calculateTextDimensions:Q9,cleanAndMerge:Fi,detectInit:gCe,detectDirective:MX,isSubstringInArray:yCe,interpolateToCurve:W9,calcLabelPosition:wCe,calcCardinalityPosition:TCe,calcTerminalLabelPosition:kCe,formatUrl:vCe,getStylesFromArray:Y9,generateId:X9,random:j9,runFunc:xCe,entityDecode:_Ce,insertTitle:DCe,parseFontSize:Bo,InitIDGenerator:U9},PX=o(function(t){let e=t;return e=e.replace(/style.*:\S*#.*;/g,function(r){return r.substring(0,r.length-1)}),e=e.replace(/classDef.*:\S*#.*;/g,function(r){return r.substring(0,r.length-1)}),e=e.replace(/#\w+;/g,function(r){let n=r.substring(1,r.length-1);return/^\+?\d+$/.test(n)?"\uFB02\xB0\xB0"+n+"\xB6\xDF":"\uFB02\xB0"+n+"\xB6\xDF"}),e},"encodeEntities"),na=o(function(t){return t.replace(/fl°°/g,"").replace(/fl°/g,"&").replace(/¶ß/g,";")},"decodeEntities"),$h=o((t,e,{counter:r=0,prefix:n,suffix:i},a)=>a||`${n?`${n}_`:""}${t}_${e}_${r}${i?`_${i}`:""}`,"getEdgeId");o($n,"handleUndefinedAttr")});function Cl(t,e,r,n,i){if(!e[t].width)if(r)e[t].text=K9(e[t].text,i,n),e[t].textLines=e[t].text.split(Ze.lineBreakRegex).length,e[t].width=i,e[t].height=dw(e[t].text,n);else{let a=e[t].text.split(Ze.lineBreakRegex);e[t].textLines=a.length;let s=0;e[t].height=0,e[t].width=0;for(let l of a)e[t].width=Math.max(ra(l,n),e[t].width),s=dw(l,n),e[t].height=e[t].height+s}}function GX(t,e,r,n,i){let a=new yw(i);a.data.widthLimit=r.data.widthLimit/Math.min(J9,n.length);for(let[s,l]of n.entries()){let u=0;l.image={width:0,height:0,Y:0},l.sprite&&(l.image.width=48,l.image.height=48,l.image.Y=u,u=l.image.Y+l.image.height);let h=l.wrap&&Vt.wrap,f=pw(Vt);if(f.fontSize=f.fontSize+2,f.fontWeight="bold",Cl("label",l,h,f,a.data.widthLimit),l.label.Y=u+8,u=l.label.Y+l.label.height,l.type&&l.type.text!==""){l.type.text="["+l.type.text+"]";let g=pw(Vt);Cl("type",l,h,g,a.data.widthLimit),l.type.Y=u+5,u=l.type.Y+l.type.height}if(l.descr&&l.descr.text!==""){let g=pw(Vt);g.fontSize=g.fontSize-2,Cl("descr",l,h,g,a.data.widthLimit),l.descr.Y=u+20,u=l.descr.Y+l.descr.height}if(s==0||s%J9===0){let g=r.data.startx+Vt.diagramMarginX,y=r.data.stopy+Vt.diagramMarginY+u;a.setData(g,g,y,y)}else{let g=a.data.stopx!==a.data.startx?a.data.stopx+Vt.diagramMarginX:a.data.startx,y=a.data.starty;a.setData(g,g,y,y)}a.name=l.alias;let d=i.db.getC4ShapeArray(l.alias),p=i.db.getC4ShapeKeys(l.alias);p.length>0&&zX(a,t,d,p),e=l.alias;let m=i.db.getBoundarys(e);m.length>0&&GX(t,e,a,m,i),l.alias!=="global"&&$X(t,l,a),r.data.stopy=Math.max(a.data.stopy+Vt.c4ShapeMargin,r.data.stopy),r.data.stopx=Math.max(a.data.stopx+Vt.c4ShapeMargin,r.data.stopx),mw=Math.max(mw,r.data.stopx),gw=Math.max(gw,r.data.stopy)}}var mw,gw,FX,J9,Vt,yw,eD,a2,pw,LCe,$X,zX,_s,BX,RCe,NCe,MCe,tD,VX=N(()=>{"use strict";dr();Bq();vt();$C();gr();uA();zt();s0();ir();Ei();mw=0,gw=0,FX=4,J9=2;Ty.yy=Qy;Vt={},yw=class{static{o(this,"Bounds")}constructor(e){this.name="",this.data={},this.data.startx=void 0,this.data.stopx=void 0,this.data.starty=void 0,this.data.stopy=void 0,this.data.widthLimit=void 0,this.nextData={},this.nextData.startx=void 0,this.nextData.stopx=void 0,this.nextData.starty=void 0,this.nextData.stopy=void 0,this.nextData.cnt=0,eD(e.db.getConfig())}setData(e,r,n,i){this.nextData.startx=this.data.startx=e,this.nextData.stopx=this.data.stopx=r,this.nextData.starty=this.data.starty=n,this.nextData.stopy=this.data.stopy=i}updateVal(e,r,n,i){e[r]===void 0?e[r]=n:e[r]=i(n,e[r])}insert(e){this.nextData.cnt=this.nextData.cnt+1;let r=this.nextData.startx===this.nextData.stopx?this.nextData.stopx+e.margin:this.nextData.stopx+e.margin*2,n=r+e.width,i=this.nextData.starty+e.margin*2,a=i+e.height;(r>=this.data.widthLimit||n>=this.data.widthLimit||this.nextData.cnt>FX)&&(r=this.nextData.startx+e.margin+Vt.nextLinePaddingX,i=this.nextData.stopy+e.margin*2,this.nextData.stopx=n=r+e.width,this.nextData.starty=this.nextData.stopy,this.nextData.stopy=a=i+e.height,this.nextData.cnt=1),e.x=r,e.y=i,this.updateVal(this.data,"startx",r,Math.min),this.updateVal(this.data,"starty",i,Math.min),this.updateVal(this.data,"stopx",n,Math.max),this.updateVal(this.data,"stopy",a,Math.max),this.updateVal(this.nextData,"startx",r,Math.min),this.updateVal(this.nextData,"starty",i,Math.min),this.updateVal(this.nextData,"stopx",n,Math.max),this.updateVal(this.nextData,"stopy",a,Math.max)}init(e){this.name="",this.data={startx:void 0,stopx:void 0,starty:void 0,stopy:void 0,widthLimit:void 0},this.nextData={startx:void 0,stopx:void 0,starty:void 0,stopy:void 0,cnt:0},eD(e.db.getConfig())}bumpLastMargin(e){this.data.stopx+=e,this.data.stopy+=e}},eD=o(function(t){Gn(Vt,t),t.fontFamily&&(Vt.personFontFamily=Vt.systemFontFamily=Vt.messageFontFamily=t.fontFamily),t.fontSize&&(Vt.personFontSize=Vt.systemFontSize=Vt.messageFontSize=t.fontSize),t.fontWeight&&(Vt.personFontWeight=Vt.systemFontWeight=Vt.messageFontWeight=t.fontWeight)},"setConf"),a2=o((t,e)=>({fontFamily:t[e+"FontFamily"],fontSize:t[e+"FontSize"],fontWeight:t[e+"FontWeight"]}),"c4ShapeFont"),pw=o(t=>({fontFamily:t.boundaryFontFamily,fontSize:t.boundaryFontSize,fontWeight:t.boundaryFontWeight}),"boundaryFont"),LCe=o(t=>({fontFamily:t.messageFontFamily,fontSize:t.messageFontSize,fontWeight:t.messageFontWeight}),"messageFont");o(Cl,"calcC4ShapeTextWH");$X=o(function(t,e,r){e.x=r.data.startx,e.y=r.data.starty,e.width=r.data.stopx-r.data.startx,e.height=r.data.stopy-r.data.starty,e.label.y=Vt.c4ShapeMargin-35;let n=e.wrap&&Vt.wrap,i=pw(Vt);i.fontSize=i.fontSize+2,i.fontWeight="bold";let a=ra(e.label.text,i);Cl("label",e,n,i,a),kl.drawBoundary(t,e,Vt)},"drawBoundary"),zX=o(function(t,e,r,n){let i=0;for(let a of n){i=0;let s=r[a],l=a2(Vt,s.typeC4Shape.text);switch(l.fontSize=l.fontSize-2,s.typeC4Shape.width=ra("\xAB"+s.typeC4Shape.text+"\xBB",l),s.typeC4Shape.height=l.fontSize+2,s.typeC4Shape.Y=Vt.c4ShapePadding,i=s.typeC4Shape.Y+s.typeC4Shape.height-4,s.image={width:0,height:0,Y:0},s.typeC4Shape.text){case"person":case"external_person":s.image.width=48,s.image.height=48,s.image.Y=i,i=s.image.Y+s.image.height;break}s.sprite&&(s.image.width=48,s.image.height=48,s.image.Y=i,i=s.image.Y+s.image.height);let u=s.wrap&&Vt.wrap,h=Vt.width-Vt.c4ShapePadding*2,f=a2(Vt,s.typeC4Shape.text);if(f.fontSize=f.fontSize+2,f.fontWeight="bold",Cl("label",s,u,f,h),s.label.Y=i+8,i=s.label.Y+s.label.height,s.type&&s.type.text!==""){s.type.text="["+s.type.text+"]";let m=a2(Vt,s.typeC4Shape.text);Cl("type",s,u,m,h),s.type.Y=i+5,i=s.type.Y+s.type.height}else if(s.techn&&s.techn.text!==""){s.techn.text="["+s.techn.text+"]";let m=a2(Vt,s.techn.text);Cl("techn",s,u,m,h),s.techn.Y=i+5,i=s.techn.Y+s.techn.height}let d=i,p=s.label.width;if(s.descr&&s.descr.text!==""){let m=a2(Vt,s.typeC4Shape.text);Cl("descr",s,u,m,h),s.descr.Y=i+20,i=s.descr.Y+s.descr.height,p=Math.max(s.label.width,s.descr.width),d=i-s.descr.textLines*5}p=p+Vt.c4ShapePadding,s.width=Math.max(s.width||Vt.width,p,Vt.width),s.height=Math.max(s.height||Vt.height,d,Vt.height),s.margin=s.margin||Vt.c4ShapeMargin,t.insert(s),kl.drawC4Shape(e,s,Vt)}t.bumpLastMargin(Vt.c4ShapeMargin)},"drawC4ShapeArray"),_s=class{static{o(this,"Point")}constructor(e,r){this.x=e,this.y=r}},BX=o(function(t,e){let r=t.x,n=t.y,i=e.x,a=e.y,s=r+t.width/2,l=n+t.height/2,u=Math.abs(r-i),h=Math.abs(n-a),f=h/u,d=t.height/t.width,p=null;return n==a&&ri?p=new _s(r,l):r==i&&na&&(p=new _s(s,n)),r>i&&n=f?p=new _s(r,l+f*t.width/2):p=new _s(s-u/h*t.height/2,n+t.height):r=f?p=new _s(r+t.width,l+f*t.width/2):p=new _s(s+u/h*t.height/2,n+t.height):ra?d>=f?p=new _s(r+t.width,l-f*t.width/2):p=new _s(s+t.height/2*u/h,n):r>i&&n>a&&(d>=f?p=new _s(r,l-t.width/2*f):p=new _s(s-t.height/2*u/h,n)),p},"getIntersectPoint"),RCe=o(function(t,e){let r={x:0,y:0};r.x=e.x+e.width/2,r.y=e.y+e.height/2;let n=BX(t,r);r.x=t.x+t.width/2,r.y=t.y+t.height/2;let i=BX(e,r);return{startPoint:n,endPoint:i}},"getIntersectPoints"),NCe=o(function(t,e,r,n){let i=0;for(let a of e){i=i+1;let s=a.wrap&&Vt.wrap,l=LCe(Vt);n.db.getC4Type()==="C4Dynamic"&&(a.label.text=i+": "+a.label.text);let h=ra(a.label.text,l);Cl("label",a,s,l,h),a.techn&&a.techn.text!==""&&(h=ra(a.techn.text,l),Cl("techn",a,s,l,h)),a.descr&&a.descr.text!==""&&(h=ra(a.descr.text,l),Cl("descr",a,s,l,h));let f=r(a.from),d=r(a.to),p=RCe(f,d);a.startPoint=p.startPoint,a.endPoint=p.endPoint}kl.drawRels(t,e,Vt)},"drawRels");o(GX,"drawInsideBoundary");MCe=o(function(t,e,r,n){Vt=me().c4;let i=me().securityLevel,a;i==="sandbox"&&(a=Ge("#i"+e));let s=i==="sandbox"?Ge(a.nodes()[0].contentDocument.body):Ge("body"),l=n.db;n.db.setWrap(Vt.wrap),FX=l.getC4ShapeInRow(),J9=l.getC4BoundaryInRow(),Y.debug(`C:${JSON.stringify(Vt,null,2)}`);let u=i==="sandbox"?s.select(`[id="${e}"]`):Ge(`[id="${e}"]`);kl.insertComputerIcon(u),kl.insertDatabaseIcon(u),kl.insertClockIcon(u);let h=new yw(n);h.setData(Vt.diagramMarginX,Vt.diagramMarginX,Vt.diagramMarginY,Vt.diagramMarginY),h.data.widthLimit=screen.availWidth,mw=Vt.diagramMarginX,gw=Vt.diagramMarginY;let f=n.db.getTitle(),d=n.db.getBoundarys("");GX(u,"",h,d,n),kl.insertArrowHead(u),kl.insertArrowEnd(u),kl.insertArrowCrossHead(u),kl.insertArrowFilledHead(u),NCe(u,n.db.getRels(),n.db.getC4Shape,n),h.data.stopx=mw,h.data.stopy=gw;let p=h.data,g=p.stopy-p.starty+2*Vt.diagramMarginY,v=p.stopx-p.startx+2*Vt.diagramMarginX;f&&u.append("text").text(f).attr("x",(p.stopx-p.startx)/2-4*Vt.diagramMarginX).attr("y",p.starty+Vt.diagramMarginY),vn(u,g,v,Vt.useMaxWidth);let x=f?60:0;u.attr("viewBox",p.startx-Vt.diagramMarginX+" -"+(Vt.diagramMarginY+x)+" "+v+" "+(g+x)),Y.debug("models:",p)},"draw"),tD={drawPersonOrSystemArray:zX,drawBoundary:$X,setConf:eD,draw:MCe}});var ICe,UX,HX=N(()=>{"use strict";ICe=o(t=>`.person {
+ stroke: ${t.personBorder};
+ fill: ${t.personBkg};
+ }
+`,"getStyles"),UX=ICe});var WX={};hr(WX,{diagram:()=>OCe});var OCe,qX=N(()=>{"use strict";$C();uA();VX();HX();OCe={parser:JF,db:Qy,renderer:tD,styles:UX,init:o(({c4:t,wrap:e})=>{tD.setConf(t),Qy.setWrap(e)},"init")}});function uj(t){return typeof t>"u"||t===null}function $Ce(t){return typeof t=="object"&&t!==null}function zCe(t){return Array.isArray(t)?t:uj(t)?[]:[t]}function GCe(t,e){var r,n,i,a;if(e)for(a=Object.keys(e),r=0,n=a.length;r"u"&&Au(t,e,null,!1,!1))&&(n!==""&&(n+=","+(t.condenseFlow?"":" ")),n+=t.dump);t.tag=i,t.dump="["+n+"]"}function lj(t,e,r,n){var i="",a=t.tag,s,l,u;for(s=0,l=r.length;s tag resolver accepts not "'+u+'" style');t.dump=n}return!0}return!1}function Au(t,e,r,n,i,a,s){t.tag=null,t.dump=r,cj(t,r,!1)||cj(t,r,!0);var l=Sj.call(t.dump),u=n,h;n&&(n=t.flowLevel<0||t.flowLevel>e);var f=l==="[object Object]"||l==="[object Array]",d,p;if(f&&(d=t.duplicates.indexOf(r),p=d!==-1),(t.tag!==null&&t.tag!=="?"||p||t.indent!==2&&e>0)&&(i=!1),p&&t.usedDuplicates[d])t.dump="*ref_"+d;else{if(f&&p&&!t.usedDuplicates[d]&&(t.usedDuplicates[d]=!0),l==="[object Object]")n&&Object.keys(t.dump).length!==0?(t8e(t,e,t.dump,i),p&&(t.dump="&ref_"+d+t.dump)):(e8e(t,e,t.dump),p&&(t.dump="&ref_"+d+" "+t.dump));else if(l==="[object Array]")n&&t.dump.length!==0?(t.noArrayIndent&&!s&&e>0?lj(t,e-1,t.dump,i):lj(t,e,t.dump,i),p&&(t.dump="&ref_"+d+t.dump)):(JAe(t,e,t.dump),p&&(t.dump="&ref_"+d+" "+t.dump));else if(l==="[object String]")t.tag!=="?"&&KAe(t,t.dump,e,a,u);else{if(l==="[object Undefined]")return!1;if(t.skipInvalid)return!1;throw new Ds("unacceptable kind of an object to dump "+l)}t.tag!==null&&t.tag!=="?"&&(h=encodeURI(t.tag[0]==="!"?t.tag.slice(1):t.tag).replace(/!/g,"%21"),t.tag[0]==="!"?h="!"+h:h.slice(0,18)==="tag:yaml.org,2002:"?h="!!"+h.slice(18):h="!<"+h+">",t.dump=h+" "+t.dump)}return!0}function r8e(t,e){var r=[],n=[],i,a;for(cD(t,r,n),i=0,a=n.length;i{"use strict";o(uj,"isNothing");o($Ce,"isObject");o(zCe,"toArray");o(GCe,"extend");o(VCe,"repeat");o(UCe,"isNegativeZero");HCe=uj,WCe=$Ce,qCe=zCe,YCe=VCe,XCe=UCe,jCe=GCe,$i={isNothing:HCe,isObject:WCe,toArray:qCe,repeat:YCe,isNegativeZero:XCe,extend:jCe};o(hj,"formatError");o(o2,"YAMLException$1");o2.prototype=Object.create(Error.prototype);o2.prototype.constructor=o2;o2.prototype.toString=o(function(e){return this.name+": "+hj(this,e)},"toString");Ds=o2;o(rD,"getLine");o(nD,"padStart");o(KCe,"makeSnippet");QCe=KCe,ZCe=["kind","multi","resolve","construct","instanceOf","predicate","represent","representName","defaultStyle","styleAliases"],JCe=["scalar","sequence","mapping"];o(e7e,"compileStyleAliases");o(t7e,"Type$1");_a=t7e;o(jX,"compileList");o(r7e,"compileMap");o(aD,"Schema$1");aD.prototype.extend=o(function(e){var r=[],n=[];if(e instanceof _a)n.push(e);else if(Array.isArray(e))n=n.concat(e);else if(e&&(Array.isArray(e.implicit)||Array.isArray(e.explicit)))e.implicit&&(r=r.concat(e.implicit)),e.explicit&&(n=n.concat(e.explicit));else throw new Ds("Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })");r.forEach(function(a){if(!(a instanceof _a))throw new Ds("Specified list of YAML types (or a single Type object) contains a non-Type object.");if(a.loadKind&&a.loadKind!=="scalar")throw new Ds("There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.");if(a.multi)throw new Ds("There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.")}),n.forEach(function(a){if(!(a instanceof _a))throw new Ds("Specified list of YAML types (or a single Type object) contains a non-Type object.")});var i=Object.create(aD.prototype);return i.implicit=(this.implicit||[]).concat(r),i.explicit=(this.explicit||[]).concat(n),i.compiledImplicit=jX(i,"implicit"),i.compiledExplicit=jX(i,"explicit"),i.compiledTypeMap=r7e(i.compiledImplicit,i.compiledExplicit),i},"extend");n7e=aD,i7e=new _a("tag:yaml.org,2002:str",{kind:"scalar",construct:o(function(t){return t!==null?t:""},"construct")}),a7e=new _a("tag:yaml.org,2002:seq",{kind:"sequence",construct:o(function(t){return t!==null?t:[]},"construct")}),s7e=new _a("tag:yaml.org,2002:map",{kind:"mapping",construct:o(function(t){return t!==null?t:{}},"construct")}),o7e=new n7e({explicit:[i7e,a7e,s7e]});o(l7e,"resolveYamlNull");o(c7e,"constructYamlNull");o(u7e,"isNull");h7e=new _a("tag:yaml.org,2002:null",{kind:"scalar",resolve:l7e,construct:c7e,predicate:u7e,represent:{canonical:o(function(){return"~"},"canonical"),lowercase:o(function(){return"null"},"lowercase"),uppercase:o(function(){return"NULL"},"uppercase"),camelcase:o(function(){return"Null"},"camelcase"),empty:o(function(){return""},"empty")},defaultStyle:"lowercase"});o(f7e,"resolveYamlBoolean");o(d7e,"constructYamlBoolean");o(p7e,"isBoolean");m7e=new _a("tag:yaml.org,2002:bool",{kind:"scalar",resolve:f7e,construct:d7e,predicate:p7e,represent:{lowercase:o(function(t){return t?"true":"false"},"lowercase"),uppercase:o(function(t){return t?"TRUE":"FALSE"},"uppercase"),camelcase:o(function(t){return t?"True":"False"},"camelcase")},defaultStyle:"lowercase"});o(g7e,"isHexCode");o(y7e,"isOctCode");o(v7e,"isDecCode");o(x7e,"resolveYamlInteger");o(b7e,"constructYamlInteger");o(w7e,"isInteger");T7e=new _a("tag:yaml.org,2002:int",{kind:"scalar",resolve:x7e,construct:b7e,predicate:w7e,represent:{binary:o(function(t){return t>=0?"0b"+t.toString(2):"-0b"+t.toString(2).slice(1)},"binary"),octal:o(function(t){return t>=0?"0o"+t.toString(8):"-0o"+t.toString(8).slice(1)},"octal"),decimal:o(function(t){return t.toString(10)},"decimal"),hexadecimal:o(function(t){return t>=0?"0x"+t.toString(16).toUpperCase():"-0x"+t.toString(16).toUpperCase().slice(1)},"hexadecimal")},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}}),k7e=new RegExp("^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");o(E7e,"resolveYamlFloat");o(S7e,"constructYamlFloat");C7e=/^[-+]?[0-9]+e/;o(A7e,"representYamlFloat");o(_7e,"isFloat");D7e=new _a("tag:yaml.org,2002:float",{kind:"scalar",resolve:E7e,construct:S7e,predicate:_7e,represent:A7e,defaultStyle:"lowercase"}),fj=o7e.extend({implicit:[h7e,m7e,T7e,D7e]}),L7e=fj,dj=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),pj=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$");o(R7e,"resolveYamlTimestamp");o(N7e,"constructYamlTimestamp");o(M7e,"representYamlTimestamp");I7e=new _a("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:R7e,construct:N7e,instanceOf:Date,represent:M7e});o(O7e,"resolveYamlMerge");P7e=new _a("tag:yaml.org,2002:merge",{kind:"scalar",resolve:O7e}),uD=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=
+\r`;o(B7e,"resolveYamlBinary");o(F7e,"constructYamlBinary");o($7e,"representYamlBinary");o(z7e,"isBinary");G7e=new _a("tag:yaml.org,2002:binary",{kind:"scalar",resolve:B7e,construct:F7e,predicate:z7e,represent:$7e}),V7e=Object.prototype.hasOwnProperty,U7e=Object.prototype.toString;o(H7e,"resolveYamlOmap");o(W7e,"constructYamlOmap");q7e=new _a("tag:yaml.org,2002:omap",{kind:"sequence",resolve:H7e,construct:W7e}),Y7e=Object.prototype.toString;o(X7e,"resolveYamlPairs");o(j7e,"constructYamlPairs");K7e=new _a("tag:yaml.org,2002:pairs",{kind:"sequence",resolve:X7e,construct:j7e}),Q7e=Object.prototype.hasOwnProperty;o(Z7e,"resolveYamlSet");o(J7e,"constructYamlSet");eAe=new _a("tag:yaml.org,2002:set",{kind:"mapping",resolve:Z7e,construct:J7e}),mj=L7e.extend({implicit:[I7e,P7e],explicit:[G7e,q7e,K7e,eAe]}),Gh=Object.prototype.hasOwnProperty,vw=1,gj=2,yj=3,xw=4,iD=1,tAe=2,KX=3,rAe=/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,nAe=/[\x85\u2028\u2029]/,iAe=/[,\[\]\{\}]/,vj=/^(?:!|!!|![a-z\-]+!)$/i,xj=/^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i;o(QX,"_class");o(dc,"is_EOL");o(Nd,"is_WHITE_SPACE");o(Ls,"is_WS_OR_EOL");o(am,"is_FLOW_INDICATOR");o(aAe,"fromHexCode");o(sAe,"escapedHexLen");o(oAe,"fromDecimalCode");o(ZX,"simpleEscapeSequence");o(lAe,"charFromCodepoint");bj=new Array(256),wj=new Array(256);for(Rd=0;Rd<256;Rd++)bj[Rd]=ZX(Rd)?1:0,wj[Rd]=ZX(Rd);o(cAe,"State$1");o(Tj,"generateError");o(Qt,"throwError");o(bw,"throwWarning");JX={YAML:o(function(e,r,n){var i,a,s;e.version!==null&&Qt(e,"duplication of %YAML directive"),n.length!==1&&Qt(e,"YAML directive accepts exactly one argument"),i=/^([0-9]+)\.([0-9]+)$/.exec(n[0]),i===null&&Qt(e,"ill-formed argument of the YAML directive"),a=parseInt(i[1],10),s=parseInt(i[2],10),a!==1&&Qt(e,"unacceptable YAML version of the document"),e.version=n[0],e.checkLineBreaks=s<2,s!==1&&s!==2&&bw(e,"unsupported YAML version of the document")},"handleYamlDirective"),TAG:o(function(e,r,n){var i,a;n.length!==2&&Qt(e,"TAG directive accepts exactly two arguments"),i=n[0],a=n[1],vj.test(i)||Qt(e,"ill-formed tag handle (first argument) of the TAG directive"),Gh.call(e.tagMap,i)&&Qt(e,'there is a previously declared suffix for "'+i+'" tag handle'),xj.test(a)||Qt(e,"ill-formed tag prefix (second argument) of the TAG directive");try{a=decodeURIComponent(a)}catch{Qt(e,"tag prefix is malformed: "+a)}e.tagMap[i]=a},"handleTagDirective")};o(zh,"captureSegment");o(ej,"mergeMappings");o(sm,"storeMappingPair");o(hD,"readLineBreak");o(Ci,"skipSeparationSpace");o(kw,"testDocumentSeparator");o(fD,"writeFoldedLines");o(uAe,"readPlainScalar");o(hAe,"readSingleQuotedScalar");o(fAe,"readDoubleQuotedScalar");o(dAe,"readFlowCollection");o(pAe,"readBlockScalar");o(tj,"readBlockSequence");o(mAe,"readBlockMapping");o(gAe,"readTagProperty");o(yAe,"readAnchorProperty");o(vAe,"readAlias");o(om,"composeNode");o(xAe,"readDocument");o(kj,"loadDocuments");o(bAe,"loadAll$1");o(wAe,"load$1");TAe=bAe,kAe=wAe,Ej={loadAll:TAe,load:kAe},Sj=Object.prototype.toString,Cj=Object.prototype.hasOwnProperty,dD=65279,EAe=9,l2=10,SAe=13,CAe=32,AAe=33,_Ae=34,sD=35,DAe=37,LAe=38,RAe=39,NAe=42,Aj=44,MAe=45,ww=58,IAe=61,OAe=62,PAe=63,BAe=64,_j=91,Dj=93,FAe=96,Lj=123,$Ae=124,Rj=125,Da={};Da[0]="\\0";Da[7]="\\a";Da[8]="\\b";Da[9]="\\t";Da[10]="\\n";Da[11]="\\v";Da[12]="\\f";Da[13]="\\r";Da[27]="\\e";Da[34]='\\"';Da[92]="\\\\";Da[133]="\\N";Da[160]="\\_";Da[8232]="\\L";Da[8233]="\\P";zAe=["y","Y","yes","Yes","YES","on","On","ON","n","N","no","No","NO","off","Off","OFF"],GAe=/^[-+]?[0-9_]+(?::[0-9_]+)+(?:\.[0-9_]*)?$/;o(VAe,"compileStyleMap");o(UAe,"encodeHex");HAe=1,c2=2;o(WAe,"State");o(rj,"indentString");o(oD,"generateNextLine");o(qAe,"testImplicitResolving");o(Tw,"isWhitespace");o(u2,"isPrintable");o(nj,"isNsCharOrWhitespace");o(ij,"isPlainSafe");o(YAe,"isPlainSafeFirst");o(XAe,"isPlainSafeLast");o(s2,"codePointAt");o(Nj,"needIndentIndicator");Mj=1,lD=2,Ij=3,Oj=4,im=5;o(jAe,"chooseScalarStyle");o(KAe,"writeScalar");o(aj,"blockHeader");o(sj,"dropEndingNewline");o(QAe,"foldString");o(oj,"foldLine");o(ZAe,"escapeString");o(JAe,"writeFlowSequence");o(lj,"writeBlockSequence");o(e8e,"writeFlowMapping");o(t8e,"writeBlockMapping");o(cj,"detectType");o(Au,"writeNode");o(r8e,"getDuplicateReferences");o(cD,"inspectNode");o(n8e,"dump$1");i8e=n8e,a8e={dump:i8e};o(pD,"renamed");lm=fj,cm=Ej.load,okt=Ej.loadAll,lkt=a8e.dump,ckt=pD("safeLoad","load"),ukt=pD("safeLoadAll","loadAll"),hkt=pD("safeDump","dump")});function vD(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function Gj(t){Id=t}function nn(t,e=""){let r=typeof t=="string"?t:t.source,n={replace:o((i,a)=>{let s=typeof a=="string"?a:a.source;return s=s.replace(ts.caret,"$1"),r=r.replace(i,s),n},"replace"),getRegex:o(()=>new RegExp(r,e),"getRegex")};return n}function pc(t,e){if(e){if(ts.escapeTest.test(t))return t.replace(ts.escapeReplace,Bj)}else if(ts.escapeTestNoEncode.test(t))return t.replace(ts.escapeReplaceNoEncode,Bj);return t}function Fj(t){try{t=encodeURI(t).replace(ts.percentDecode,"%")}catch{return null}return t}function $j(t,e){let r=t.replace(ts.findPipe,(a,s,l)=>{let u=!1,h=s;for(;--h>=0&&l[h]==="\\";)u=!u;return u?"|":" |"}),n=r.split(ts.splitPipe),i=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length
+`:"'+(n?a:pc(a,!0))+`
+`}blockquote({tokens:e}){return`"+(n?a:pc(a,!0))+`
+${this.parser.parse(e)}
+`}html({text:e}){return e}heading({tokens:e,depth:r}){return`
+`}list(e){let r=e.ordered,n=e.start,i="";for(let l=0;l
+
+`+r+`
+`+i+`
+`}tablerow({text:e}){return`
+${e}
+`}tablecell(e){let r=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+r+`${n}>
+`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${pc(e,!0)}`}br(e){return"
"}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:r,tokens:n}){let i=this.parser.parseInline(n),a=Fj(e);if(a===null)return i;e=a;let s='"+i+"",s}image({href:e,title:r,text:n}){let i=Fj(e);if(i===null)return pc(n);e=i;let a=`",a}text(e){return"tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):"escaped"in e&&e.escaped?e.text:pc(e.text)}},p2=class{static{o(this,"_TextRenderer")}strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return""+e}image({text:e}){return""+e}br(){return""}},_l=class t{static{o(this,"_Parser")}options;renderer;textRenderer;constructor(e){this.options=e||Id,this.options.renderer=this.options.renderer||new fm,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new p2}static parse(e,r){return new t(r).parse(e)}static parseInline(e,r){return new t(r).parseInline(e)}parse(e,r=!0){let n="";for(let i=0;i
"+pc(n.message+"",!0)+"
";return r?Promise.resolve(i):i}if(r)return Promise.reject(n);throw n}}},Md=new yD;o(Jr,"marked");Jr.options=Jr.setOptions=function(t){return Md.setOptions(t),Jr.defaults=Md.defaults,Gj(Jr.defaults),Jr};Jr.getDefaults=vD;Jr.defaults=Id;Jr.use=function(...t){return Md.use(...t),Jr.defaults=Md.defaults,Gj(Jr.defaults),Jr};Jr.walkTokens=function(t,e){return Md.walkTokens(t,e)};Jr.parseInline=Md.parseInline;Jr.Parser=_l;Jr.parser=_l.parse;Jr.Renderer=fm;Jr.TextRenderer=p2;Jr.Lexer=Al;Jr.lexer=Al.lex;Jr.Tokenizer=hm;Jr.Hooks=um;Jr.parse=Jr;dkt=Jr.options,pkt=Jr.setOptions,mkt=Jr.use,gkt=Jr.walkTokens,ykt=Jr.parseInline,vkt=_l.parse,xkt=Al.lex});function G8e(t,{markdownAutoWrap:e}){let n=t.replace(/
/g,`
+`).replace(/\n{2,}/g,`
+`),i=B4(n);return e===!1?i.replace(/ /g," "):i}function Jj(t,e={}){let r=G8e(t,e),n=Jr.lexer(r),i=[[]],a=0;function s(l,u="normal"){l.type==="text"?l.text.split(`
+`).forEach((f,d)=>{d!==0&&(a++,i.push([])),f.split(" ").forEach(p=>{p=p.replace(/'/g,"'"),p&&i[a].push({content:p,type:u})})}):l.type==="strong"||l.type==="em"?l.tokens.forEach(h=>{s(h,l.type)}):l.type==="html"&&i[a].push({content:l.text,type:"normal"})}return o(s,"processNode"),n.forEach(l=>{l.type==="paragraph"?l.tokens?.forEach(u=>{s(u)}):l.type==="html"&&i[a].push({content:l.text,type:"normal"})}),i}function eK(t,{markdownAutoWrap:e}={}){let r=Jr.lexer(t);function n(i){return i.type==="text"?e===!1?i.text.replace(/\n */g,"
").replace(/ /g," "):i.text.replace(/\n */g,"
"):i.type==="strong"?`${i.tokens?.map(n).join("")}`:i.type==="em"?`${i.tokens?.map(n).join("")}`:i.type==="paragraph"?`
/g,"
"),d=Jj(f.replace("
","
"),h),p=q8e(l,t,d,e?u:!1);if(s){/stroke:/.exec(r)&&(r=r.replace("stroke:","lineColor:"));let m=r.replace(/stroke:[^;]+;?/g,"").replace(/stroke-width:[^;]+;?/g,"").replace(/fill:[^;]+;?/g,"").replace(/color:/g,"fill:");Ge(p).attr("style",m)}else{let m=r.replace(/stroke:[^;]+;?/g,"").replace(/stroke-width:[^;]+;?/g,"").replace(/fill:[^;]+;?/g,"").replace(/background:/g,"fill:");Ge(p).select("rect").attr("style",m.replace(/background:/g,"fill:"));let g=r.replace(/stroke:[^;]+;?/g,"").replace(/stroke-width:[^;]+;?/g,"").replace(/fill:[^;]+;?/g,"").replace(/color:/g,"fill:");Ge(p).select("text").attr("style",g)}return p}},"createText")});function Xt(t){let e=t.map((r,n)=>`${n===0?"M":"L"}${r.x},${r.y}`);return e.push("Z"),e.join(" ")}function Fo(t,e,r,n,i,a){let s=[],u=r-t,h=n-e,f=u/a,d=2*Math.PI/f,p=e+h/2;for(let m=0;m<=50;m++){let g=m/50,y=t+g*u,v=p+i*Math.sin(d*(y-t));s.push({x:y,y:v})}return s}function Lw(t,e,r,n,i,a){let s=[],l=i*Math.PI/180,f=(a*Math.PI/180-l)/(n-1);for(let d=0;d]*>/g,"").trim()==="";await Promise.all([...m].map(y=>new Promise(v=>{function x(){if(y.style.display="flex",y.style.flexDirection="column",g){let b=me().fontSize?me().fontSize:window.getComputedStyle(document.body).fontSize,w=5,[C=or.fontSize]=Bo(b),T=C*w+"px";y.style.minWidth=T,y.style.maxWidth=T}else y.style.width="100%";v(y)}o(x,"setupImage"),setTimeout(()=>{y.complete&&x()}),y.addEventListener("error",x),y.addEventListener("load",x)})))}h=d.getBoundingClientRect(),p.attr("width",h.width),p.attr("height",h.height)}return i?s.attr("transform","translate("+-h.width/2+", "+-h.height/2+")"):s.attr("transform","translate(0, "+-h.height/2+")"),e.centerLabel&&s.attr("transform","translate("+-h.width/2+", "+-h.height/2+")"),s.insert("rect",":first-child"),{shapeSvg:a,bbox:h,halfPadding:f,label:s}},"labelHelper"),Dw=o(async(t,e,r)=>{let n=r.useHtmlLabels||fr(me()?.flowchart?.htmlLabels),i=t.insert("g").attr("class","label").attr("style",r.labelStyle||""),a=await Hn(i,Tr(na(e),me()),{useHtmlLabels:n,width:r.width||me()?.flowchart?.wrappingWidth,style:r.labelStyle,addSvgBackground:!!r.icon||!!r.img}),s=a.getBBox(),l=r.padding/2;if(fr(me()?.flowchart?.htmlLabels)){let u=a.children[0],h=Ge(a);s=u.getBoundingClientRect(),h.attr("width",s.width),h.attr("height",s.height)}return n?i.attr("transform","translate("+-s.width/2+", "+-s.height/2+")"):i.attr("transform","translate(0, "+-s.height/2+")"),r.centerLabel&&i.attr("transform","translate("+-s.width/2+", "+-s.height/2+")"),i.insert("rect",":first-child"),{shapeSvg:t,bbox:s,halfPadding:l,label:i}},"insertLabel"),je=o((t,e)=>{let r=e.node().getBBox();t.width=r.width,t.height=r.height},"updateNodeBounds"),ht=o((t,e)=>(t.look==="handDrawn"?"rough-node":"node")+" "+t.cssClasses+" "+(e||""),"getNodeClasses");o(Xt,"createPathFromPoints");o(Fo,"generateFullSineWavePoints");o(Lw,"generateCirclePoints")});function Y8e(t,e){return t.intersect(e)}var oK,lK=N(()=>{"use strict";o(Y8e,"intersectNode");oK=Y8e});function X8e(t,e,r,n){var i=t.x,a=t.y,s=i-n.x,l=a-n.y,u=Math.sqrt(e*e*l*l+r*r*s*s),h=Math.abs(e*r*s/u);n.x{"use strict";o(X8e,"intersectEllipse");Rw=X8e});function j8e(t,e,r){return Rw(t,e,e,r)}var cK,uK=N(()=>{"use strict";LD();o(j8e,"intersectCircle");cK=j8e});function K8e(t,e,r,n){var i,a,s,l,u,h,f,d,p,m,g,y,v,x,b;if(i=e.y-t.y,s=t.x-e.x,u=e.x*t.y-t.x*e.y,p=i*r.x+s*r.y+u,m=i*n.x+s*n.y+u,!(p!==0&&m!==0&&hK(p,m))&&(a=n.y-r.y,l=r.x-n.x,h=n.x*r.y-r.x*n.y,f=a*t.x+l*t.y+h,d=a*e.x+l*e.y+h,!(f!==0&&d!==0&&hK(f,d))&&(g=i*l-a*s,g!==0)))return y=Math.abs(g/2),v=s*h-l*u,x=v<0?(v-y)/g:(v+y)/g,v=a*u-i*h,b=v<0?(v-y)/g:(v+y)/g,{x,y:b}}function hK(t,e){return t*e>0}var fK,dK=N(()=>{"use strict";o(K8e,"intersectLine");o(hK,"sameSign");fK=K8e});function Q8e(t,e,r){let n=t.x,i=t.y,a=[],s=Number.POSITIVE_INFINITY,l=Number.POSITIVE_INFINITY;typeof e.forEach=="function"?e.forEach(function(f){s=Math.min(s,f.x),l=Math.min(l,f.y)}):(s=Math.min(s,e.x),l=Math.min(l,e.y));let u=n-t.width/2-s,h=i-t.height/2-l;for(let f=0;f
"),Y.info("vertexText"+i);let a={isNode:n,label:na(i).replace(/fa[blrs]?:fa-[\w-]+/g,l=>``),labelStyle:e&&e.replace("fill:","color:")};return await v_e(a)}else{let a=document.createElementNS("http://www.w3.org/2000/svg","text");a.setAttribute("style",e.replace("color:","fill:"));let s=[];typeof i=="string"?s=i.split(/\\n|\n|
/gi):Array.isArray(i)?s=i:s=[];for(let l of s){let u=document.createElementNS("http://www.w3.org/2000/svg","tspan");u.setAttributeNS("http://www.w3.org/XML/1998/namespace","xml:space","preserve"),u.setAttribute("dy","1em"),u.setAttribute("x","0"),r?u.setAttribute("class","title-row"):u.setAttribute("class","row"),u.textContent=l.trim(),a.appendChild(u)}return a}},"createLabel"),gc=x_e});async function KQ(t,e){let{labelStyles:r,nodeStyles:n}=Qe(e);e.labelStyle=r;let i;e.cssClasses?i="node "+e.cssClasses:i="node default";let a=t.insert("g").attr("class",i).attr("id",e.domId||e.id),s=a.insert("g"),l=a.insert("g").attr("class","label").attr("style",n),u=e.description,h=e.label,f=l.node().appendChild(await gc(h,e.labelStyle,!0,!0)),d={width:0,height:0};if(fr(me()?.flowchart?.htmlLabels)){let S=f.children[0],_=Ge(f);d=S.getBoundingClientRect(),_.attr("width",d.width),_.attr("height",d.height)}Y.info("Text 2",u);let p=u||[],m=f.getBBox(),g=l.node().appendChild(await gc(p.join?p.join("
"):p,e.labelStyle,!0,!0)),y=g.children[0],v=Ge(g);d=y.getBoundingClientRect(),v.attr("width",d.width),v.attr("height",d.height);let x=(e.padding||0)/2;Ge(g).attr("transform","translate( "+(d.width>m.width?0:(m.width-d.width)/2)+", "+(m.height+x+5)+")"),Ge(f).attr("transform","translate( "+(d.width
").length,d.innerHTML.includes("")&&(f+=d.innerHTML.split("]*>/g,"").trim()==="";await Promise.all([...m].map(y=>new Promise(v=>{function x(){if(y.style.display="flex",y.style.flexDirection="column",g){let b=a.fontSize?.toString()??window.getComputedStyle(document.body).fontSize,C=parseInt(b,10)*5+"px";y.style.minWidth=C,y.style.maxWidth=C}else y.style.width="100%";v(y)}o(x,"setupImage"),setTimeout(()=>{y.complete&&x()}),y.addEventListener("error",x),y.addEventListener("load",x)})))}h=d.getBoundingClientRect(),p.attr("width",h.width),p.attr("height",h.height)}else{n.includes("font-weight: bolder")&&Ge(u).selectAll("tspan").attr("font-weight",""),f=u.children.length;let d=u.children[0];(u.textContent===""||u.textContent.includes(">"))&&(d.textContent=l[0]+l.substring(1).replaceAll(">",">").replaceAll("<","<").trim(),l[1]===" "&&(d.textContent=d.textContent[0]+" "+d.textContent.substring(1))),d.textContent==="undefined"&&(d.textContent=""),h=u.getBBox()}return i.attr("transform","translate(0,"+(-h.height/(2*f)+r)+")"),h.height}var FZ=N(()=>{"use strict";dr();ji();Ft();ir();zt();to();gr();o(BZ,"textHelper");o(Vw,"addText")});async function $Z(t,e){let r=me(),n=r.class.padding??12,i=n,a=e.useHtmlLabels??fr(r.htmlLabels)??!0,s=e;s.annotations=s.annotations??[],s.members=s.members??[],s.methods=s.methods??[];let{shapeSvg:l,bbox:u}=await BZ(t,e,r,a,i),{labelStyles:h,nodeStyles:f}=Qe(e);e.labelStyle=h,e.cssStyles=s.styles||"";let d=s.styles?.join(";")||f||"";e.cssStyles||(e.cssStyles=d.replaceAll("!important","").split(";"));let p=s.members.length===0&&s.methods.length===0&&!r.class?.hideEmptyMembersBox,m=Xe.svg(l),g=Ke(e,{});e.look!=="handDrawn"&&(g.roughness=0,g.fillStyle="solid");let y=u.width,v=u.height;s.members.length===0&&s.methods.length===0?v+=i:s.members.length>0&&s.methods.length===0&&(v+=i*2);let x=-y/2,b=-v/2,w=m.rectangle(x-n,b-n-(p?n:s.members.length===0&&s.methods.length===0?-n/2:0),y+2*n,v+2*n+(p?n*2:s.members.length===0&&s.methods.length===0?-n:0),g),C=l.insert(()=>w,":first-child");C.attr("class","basic label-container");let T=C.node().getBBox();l.selectAll(".text").each((_,I,D)=>{let k=Ge(D[I]),L=k.attr("transform"),R=0;if(L){let F=RegExp(/translate\(([^,]+),([^)]+)\)/).exec(L);F&&(R=parseFloat(F[2]))}let O=R+b+n-(p?n:s.members.length===0&&s.methods.length===0?-n/2:0);a||(O-=4);let M=x;(k.attr("class").includes("label-group")||k.attr("class").includes("annotation-group"))&&(M=-k.node()?.getBBox().width/2||0,l.selectAll("text").each(function(B,F,P){window.getComputedStyle(P[F]).textAnchor==="middle"&&(M=0)})),k.attr("transform",`translate(${M}, ${O})`)});let E=l.select(".annotation-group").node().getBBox().height-(p?n/2:0)||0,A=l.select(".label-group").node().getBBox().height-(p?n/2:0)||0,S=l.select(".members-group").node().getBBox().height-(p?n/2:0)||0;if(s.members.length>0||s.methods.length>0||p){let _=m.line(T.x,E+A+b+n,T.x+T.width,E+A+b+n,g);l.insert(()=>_).attr("class","divider").attr("style",d)}if(p||s.members.length>0||s.methods.length>0){let _=m.line(T.x,E+A+S+b+i*2+n,T.x+T.width,E+A+S+b+n+i*2,g);l.insert(()=>_).attr("class","divider").attr("style",d)}if(s.look!=="handDrawn"&&l.selectAll("path").attr("style",d),C.select(":nth-child(2)").attr("style",d),l.selectAll(".divider").select("path").attr("style",d),e.labelStyle?l.selectAll("span").attr("style",e.labelStyle):l.selectAll("span").attr("style",d),!a){let _=RegExp(/color\s*:\s*([^;]*)/),I=_.exec(d);if(I){let D=I[0].replace("color","fill");l.selectAll("tspan").attr("style",D)}else if(h){let D=_.exec(h);if(D){let k=D[0].replace("color","fill");l.selectAll("tspan").attr("style",k)}}}return je(e,C),e.intersect=function(_){return Ye.rect(e,_)},l}var zZ=N(()=>{"use strict";Ft();zt();dr();Wt();Ut();Ht();FZ();gr();o($Z,"classBox")});async function GZ(t,e){let{labelStyles:r,nodeStyles:n}=Qe(e);e.labelStyle=r;let i=e,a=e,s=20,l=20,u="verifyMethod"in e,h=ht(e),f=t.insert("g").attr("class",h).attr("id",e.domId??e.id),d;u?d=await Lu(f,`<<${i.type}>>`,0,e.labelStyle):d=await Lu(f,"<<Element>>",0,e.labelStyle);let p=d,m=await Lu(f,i.name,p,e.labelStyle+"; font-weight: bold;");if(p+=m+l,u){let E=await Lu(f,`${i.requirementId?`Id: ${i.requirementId}`:""}`,p,e.labelStyle);p+=E;let A=await Lu(f,`${i.text?`Text: ${i.text}`:""}`,p,e.labelStyle);p+=A;let S=await Lu(f,`${i.risk?`Risk: ${i.risk}`:""}`,p,e.labelStyle);p+=S,await Lu(f,`${i.verifyMethod?`Verification: ${i.verifyMethod}`:""}`,p,e.labelStyle)}else{let E=await Lu(f,`${a.type?`Type: ${a.type}`:""}`,p,e.labelStyle);p+=E,await Lu(f,`${a.docRef?`Doc Ref: ${a.docRef}`:""}`,p,e.labelStyle)}let g=(f.node()?.getBBox().width??200)+s,y=(f.node()?.getBBox().height??200)+s,v=-g/2,x=-y/2,b=Xe.svg(f),w=Ke(e,{});e.look!=="handDrawn"&&(w.roughness=0,w.fillStyle="solid");let C=b.rectangle(v,x,g,y,w),T=f.insert(()=>C,":first-child");if(T.attr("class","basic label-container").attr("style",n),f.selectAll(".label").each((E,A,S)=>{let _=Ge(S[A]),I=_.attr("transform"),D=0,k=0;if(I){let M=RegExp(/translate\(([^,]+),([^)]+)\)/).exec(I);M&&(D=parseFloat(M[1]),k=parseFloat(M[2]))}let L=k-y/2,R=v+s/2;(A===0||A===1)&&(R=D),_.attr("transform",`translate(${R}, ${L+s})`)}),p>d+m+l){let E=b.line(v,x+d+m+l,v+g,x+d+m+l,w);f.insert(()=>E).attr("style",n)}return je(e,T),e.intersect=function(E){return Ye.rect(e,E)},f}async function Lu(t,e,r,n=""){if(e==="")return 0;let i=t.insert("g").attr("class","label").attr("style",n),a=me(),s=a.htmlLabels??!0,l=await Hn(i,Xy(na(e)),{width:ra(e,a)+50,classes:"markdown-node-label",useHtmlLabels:s,style:n},a),u;if(s){let h=l.children[0],f=Ge(l);u=h.getBoundingClientRect(),f.attr("width",u.width),f.attr("height",u.height)}else{let h=l.children[0];for(let f of h.children)f.textContent=f.textContent.replaceAll(">",">").replaceAll("<","<"),n&&f.setAttribute("style",n);u=l.getBBox(),u.height+=6}return i.attr("transform",`translate(${-u.width/2},${-u.height/2+r})`),u.height}var VZ=N(()=>{"use strict";Ft();Ht();Ut();Wt();ir();zt();to();dr();o(GZ,"requirementBox");o(Lu,"addText")});async function UZ(t,e,{config:r}){let{labelStyles:n,nodeStyles:i}=Qe(e);e.labelStyle=n||"";let a=10,s=e.width;e.width=(e.width??200)-10;let{shapeSvg:l,bbox:u,label:h}=await pt(t,e,ht(e)),f=e.padding||10,d="",p;"ticket"in e&&e.ticket&&r?.kanban?.ticketBaseUrl&&(d=r?.kanban?.ticketBaseUrl.replace("#TICKET#",e.ticket),p=l.insert("svg:a",":first-child").attr("class","kanban-ticket-link").attr("xlink:href",d).attr("target","_blank"));let m={useHtmlLabels:e.useHtmlLabels,labelStyle:e.labelStyle||"",width:e.width,img:e.img,padding:e.padding||8,centerLabel:!1},g,y;p?{label:g,bbox:y}=await Dw(p,"ticket"in e&&e.ticket||"",m):{label:g,bbox:y}=await Dw(l,"ticket"in e&&e.ticket||"",m);let{label:v,bbox:x}=await Dw(l,"assigned"in e&&e.assigned||"",m);e.width=s;let b=10,w=e?.width||0,C=Math.max(y.height,x.height)/2,T=Math.max(u.height+b*2,e?.height||0)+C,E=-w/2,A=-T/2;h.attr("transform","translate("+(f-w/2)+", "+(-C-u.height/2)+")"),g.attr("transform","translate("+(f-w/2)+", "+(-C+u.height/2)+")"),v.attr("transform","translate("+(f+w/2-x.width-2*a)+", "+(-C+u.height/2)+")");let S,{rx:_,ry:I}=e,{cssStyles:D}=e;if(e.look==="handDrawn"){let k=Xe.svg(l),L=Ke(e,{}),R=_||I?k.path(Na(E,A,w,T,_||0),L):k.rectangle(E,A,w,T,L);S=l.insert(()=>R,":first-child"),S.attr("class","basic label-container").attr("style",D||null)}else{S=l.insert("rect",":first-child"),S.attr("class","basic label-container __APA__").attr("style",i).attr("rx",_??5).attr("ry",I??5).attr("x",E).attr("y",A).attr("width",w).attr("height",T);let k="priority"in e&&e.priority;if(k){let L=l.append("line"),R=E+2,O=A+Math.floor((_??0)/2),M=A+T-Math.floor((_??0)/2);L.attr("x1",R).attr("y1",O).attr("x2",R).attr("y2",M).attr("stroke-width","4").attr("stroke",k_e(k))}}return je(e,S),e.height=T,e.intersect=function(k){return Ye.rect(e,k)},l}var k_e,HZ=N(()=>{"use strict";Ft();Ht();qh();Ut();Wt();k_e=o(t=>{switch(t){case"Very High":return"red";case"High":return"orange";case"Medium":return null;case"Low":return"blue";case"Very Low":return"lightblue"}},"colorFromPriority");o(UZ,"kanbanItem")});function WZ(t){return t in QD}var E_e,S_e,QD,ZD=N(()=>{"use strict";NK();OK();BK();$K();GK();UK();WK();YK();jK();QK();JK();tQ();nQ();aQ();oQ();cQ();hQ();dQ();mQ();yQ();xQ();wQ();kQ();SQ();AQ();DQ();RQ();MQ();OQ();BQ();$Q();GQ();UQ();WQ();YQ();jQ();QQ();JQ();tZ();nZ();aZ();oZ();cZ();hZ();dZ();mZ();yZ();xZ();wZ();kZ();SZ();AZ();DZ();RZ();MZ();OZ();PZ();zZ();VZ();HZ();E_e=[{semanticName:"Process",name:"Rectangle",shortName:"rect",description:"Standard process shape",aliases:["proc","process","rectangle"],internalAliases:["squareRect"],handler:iZ},{semanticName:"Event",name:"Rounded Rectangle",shortName:"rounded",description:"Represents an event",aliases:["event"],internalAliases:["roundedRect"],handler:ZQ},{semanticName:"Terminal Point",name:"Stadium",shortName:"stadium",description:"Terminal point",aliases:["terminal","pill"],handler:sZ},{semanticName:"Subprocess",name:"Framed Rectangle",shortName:"fr-rect",description:"Subprocess",aliases:["subprocess","subproc","framed-rectangle","subroutine"],handler:pZ},{semanticName:"Database",name:"Cylinder",shortName:"cyl",description:"Database storage",aliases:["db","database","cylinder"],handler:ZK},{semanticName:"Start",name:"Circle",shortName:"circle",description:"Starting point",aliases:["circ"],handler:zK},{semanticName:"Decision",name:"Diamond",shortName:"diam",description:"Decision-making step",aliases:["decision","diamond","question"],handler:qQ},{semanticName:"Prepare Conditional",name:"Hexagon",shortName:"hex",description:"Preparation or condition step",aliases:["hexagon","prepare"],handler:fQ},{semanticName:"Data Input/Output",name:"Lean Right",shortName:"lean-r",description:"Represents input or output",aliases:["lean-right","in-out"],internalAliases:["lean_right"],handler:NQ},{semanticName:"Data Input/Output",name:"Lean Left",shortName:"lean-l",description:"Represents output or input",aliases:["lean-left","out-in"],internalAliases:["lean_left"],handler:LQ},{semanticName:"Priority Action",name:"Trapezoid Base Bottom",shortName:"trap-b",description:"Priority action",aliases:["priority","trapezoid-bottom","trapezoid"],handler:EZ},{semanticName:"Manual Operation",name:"Trapezoid Base Top",shortName:"trap-t",description:"Represents a manual task",aliases:["manual","trapezoid-top","inv-trapezoid"],internalAliases:["inv_trapezoid"],handler:CQ},{semanticName:"Stop",name:"Double Circle",shortName:"dbl-circ",description:"Represents a stop point",aliases:["double-circle"],internalAliases:["doublecircle"],handler:rQ},{semanticName:"Text Block",name:"Text Block",shortName:"text",description:"Text block",handler:bZ},{semanticName:"Card",name:"Notched Rectangle",shortName:"notch-rect",description:"Represents a card",aliases:["card","notched-rectangle"],handler:PK},{semanticName:"Lined/Shaded Process",name:"Lined Rectangle",shortName:"lin-rect",description:"Lined process shape",aliases:["lined-rectangle","lined-process","lin-proc","shaded-process"],handler:eZ},{semanticName:"Start",name:"Small Circle",shortName:"sm-circ",description:"Small starting point",aliases:["start","small-circle"],internalAliases:["stateStart"],handler:fZ},{semanticName:"Stop",name:"Framed Circle",shortName:"fr-circ",description:"Stop point",aliases:["stop","framed-circle"],internalAliases:["stateEnd"],handler:uZ},{semanticName:"Fork/Join",name:"Filled Rectangle",shortName:"fork",description:"Fork or join in process flow",aliases:["join"],internalAliases:["forkJoin"],handler:lQ},{semanticName:"Collate",name:"Hourglass",shortName:"hourglass",description:"Represents a collate operation",aliases:["hourglass","collate"],handler:pQ},{semanticName:"Comment",name:"Curly Brace",shortName:"brace",description:"Adds a comment",aliases:["comment","brace-l"],handler:HK},{semanticName:"Comment Right",name:"Curly Brace",shortName:"brace-r",description:"Adds a comment",handler:qK},{semanticName:"Comment with braces on both sides",name:"Curly Braces",shortName:"braces",description:"Adds a comment",handler:XK},{semanticName:"Com Link",name:"Lightning Bolt",shortName:"bolt",description:"Communication link",aliases:["com-link","lightning-bolt"],handler:IQ},{semanticName:"Document",name:"Document",shortName:"doc",description:"Represents a document",aliases:["doc","document"],handler:LZ},{semanticName:"Delay",name:"Half-Rounded Rectangle",shortName:"delay",description:"Represents a delay",aliases:["half-rounded-rectangle"],handler:uQ},{semanticName:"Direct Access Storage",name:"Horizontal Cylinder",shortName:"h-cyl",description:"Direct access storage",aliases:["das","horizontal-cylinder"],handler:TZ},{semanticName:"Disk Storage",name:"Lined Cylinder",shortName:"lin-cyl",description:"Disk storage",aliases:["disk","lined-cylinder"],handler:PQ},{semanticName:"Display",name:"Curved Trapezoid",shortName:"curv-trap",description:"Represents a display",aliases:["curved-trapezoid","display"],handler:KK},{semanticName:"Divided Process",name:"Divided Rectangle",shortName:"div-rect",description:"Divided process shape",aliases:["div-proc","divided-rectangle","divided-process"],handler:eQ},{semanticName:"Extract",name:"Triangle",shortName:"tri",description:"Extraction process",aliases:["extract","triangle"],handler:_Z},{semanticName:"Internal Storage",name:"Window Pane",shortName:"win-pane",description:"Internal storage",aliases:["internal-storage","window-pane"],handler:IZ},{semanticName:"Junction",name:"Filled Circle",shortName:"f-circ",description:"Junction point",aliases:["junction","filled-circle"],handler:iQ},{semanticName:"Loop Limit",name:"Trapezoidal Pentagon",shortName:"notch-pent",description:"Loop limit step",aliases:["loop-limit","notched-pentagon"],handler:CZ},{semanticName:"Manual File",name:"Flipped Triangle",shortName:"flip-tri",description:"Manual file operation",aliases:["manual-file","flipped-triangle"],handler:sQ},{semanticName:"Manual Input",name:"Sloped Rectangle",shortName:"sl-rect",description:"Manual input step",aliases:["manual-input","sloped-rectangle"],handler:rZ},{semanticName:"Multi-Document",name:"Stacked Document",shortName:"docs",description:"Multiple documents",aliases:["documents","st-doc","stacked-document"],handler:VQ},{semanticName:"Multi-Process",name:"Stacked Rectangle",shortName:"st-rect",description:"Multiple processes",aliases:["procs","processes","stacked-rectangle"],handler:zQ},{semanticName:"Stored Data",name:"Bow Tie Rectangle",shortName:"bow-rect",description:"Stored data",aliases:["stored-data","bow-tie-rectangle"],handler:IK},{semanticName:"Summary",name:"Crossed Circle",shortName:"cross-circ",description:"Summary",aliases:["summary","crossed-circle"],handler:VK},{semanticName:"Tagged Document",name:"Tagged Document",shortName:"tag-doc",description:"Tagged document",aliases:["tag-doc","tagged-document"],handler:vZ},{semanticName:"Tagged Process",name:"Tagged Rectangle",shortName:"tag-rect",description:"Tagged process",aliases:["tagged-rectangle","tag-proc","tagged-process"],handler:gZ},{semanticName:"Paper Tape",name:"Flag",shortName:"flag",description:"Paper tape",aliases:["paper-tape"],handler:NZ},{semanticName:"Odd",name:"Odd",shortName:"odd",description:"Odd shape",internalAliases:["rect_left_inv_arrow"],handler:XQ},{semanticName:"Lined Document",name:"Lined Document",shortName:"lin-doc",description:"Lined document",aliases:["lined-document"],handler:FQ}],S_e=o(()=>{let e=[...Object.entries({state:lZ,choice:FK,note:HQ,rectWithTitle:KQ,labelRect:_Q,iconSquare:TQ,iconCircle:vQ,icon:gQ,iconRounded:bQ,imageSquare:EQ,anchor:RK,kanbanItem:UZ,classBox:$Z,erBox:KD,requirementBox:GZ}),...E_e.flatMap(r=>[r.shortName,..."aliases"in r?r.aliases:[],..."internalAliases"in r?r.internalAliases:[]].map(i=>[i,r.handler]))];return Object.fromEntries(e)},"generateShapeMap"),QD=S_e();o(WZ,"isValidShape")});var C_e,Uw,qZ=N(()=>{"use strict";dr();Ew();zt();vt();ZD();ir();gr();mi();C_e="flowchart-",Uw=class{constructor(){this.vertexCounter=0;this.config=me();this.vertices=new Map;this.edges=[];this.classes=new Map;this.subGraphs=[];this.subGraphLookup=new Map;this.tooltips=new Map;this.subCount=0;this.firstGraphFlag=!0;this.secCount=-1;this.posCrossRef=[];this.funs=[];this.setAccTitle=Lr;this.setAccDescription=Nr;this.setDiagramTitle=$r;this.getAccTitle=Rr;this.getAccDescription=Mr;this.getDiagramTitle=Ir;this.funs.push(this.setupToolTips.bind(this)),this.addVertex=this.addVertex.bind(this),this.firstGraph=this.firstGraph.bind(this),this.setDirection=this.setDirection.bind(this),this.addSubGraph=this.addSubGraph.bind(this),this.addLink=this.addLink.bind(this),this.setLink=this.setLink.bind(this),this.updateLink=this.updateLink.bind(this),this.addClass=this.addClass.bind(this),this.setClass=this.setClass.bind(this),this.destructLink=this.destructLink.bind(this),this.setClickEvent=this.setClickEvent.bind(this),this.setTooltip=this.setTooltip.bind(this),this.updateLinkInterpolate=this.updateLinkInterpolate.bind(this),this.setClickFun=this.setClickFun.bind(this),this.bindFunctions=this.bindFunctions.bind(this),this.lex={firstGraph:this.firstGraph.bind(this)},this.clear(),this.setGen("gen-2")}static{o(this,"FlowDB")}sanitizeText(e){return Ze.sanitizeText(e,this.config)}lookUpDomId(e){for(let r of this.vertices.values())if(r.id===e)return r.domId;return e}addVertex(e,r,n,i,a,s,l={},u){if(!e||e.trim().length===0)return;let h;if(u!==void 0){let m;u.includes(`
+`)?m=u+`
+`:m=`{
+`+u+`
+}`,h=cm(m,{schema:lm})}let f=this.edges.find(m=>m.id===e);if(f){let m=h;m?.animate!==void 0&&(f.animate=m.animate),m?.animation!==void 0&&(f.animation=m.animation);return}let d,p=this.vertices.get(e);if(p===void 0&&(p={id:e,labelType:"text",domId:C_e+e+"-"+this.vertexCounter,styles:[],classes:[]},this.vertices.set(e,p)),this.vertexCounter++,r!==void 0?(this.config=me(),d=this.sanitizeText(r.text.trim()),p.labelType=r.type,d.startsWith('"')&&d.endsWith('"')&&(d=d.substring(1,d.length-1)),p.text=d):p.text===void 0&&(p.text=e),n!==void 0&&(p.type=n),i?.forEach(m=>{p.styles.push(m)}),a?.forEach(m=>{p.classes.push(m)}),s!==void 0&&(p.dir=s),p.props===void 0?p.props=l:l!==void 0&&Object.assign(p.props,l),h!==void 0){if(h.shape){if(h.shape!==h.shape.toLowerCase()||h.shape.includes("_"))throw new Error(`No such shape: ${h.shape}. Shape names should be lowercase.`);if(!WZ(h.shape))throw new Error(`No such shape: ${h.shape}.`);p.type=h?.shape}h?.label&&(p.text=h?.label),h?.icon&&(p.icon=h?.icon,!h.label?.trim()&&p.text===e&&(p.text="")),h?.form&&(p.form=h?.form),h?.pos&&(p.pos=h?.pos),h?.img&&(p.img=h?.img,!h.label?.trim()&&p.text===e&&(p.text="")),h?.constraint&&(p.constraint=h.constraint),h.w&&(p.assetWidth=Number(h.w)),h.h&&(p.assetHeight=Number(h.h))}}addSingleLink(e,r,n,i){let l={start:e,end:r,type:void 0,text:"",labelType:"text",classes:[],isUserDefinedId:!1,interpolate:this.edges.defaultInterpolate};Y.info("abc78 Got edge...",l);let u=n.text;if(u!==void 0&&(l.text=this.sanitizeText(u.text.trim()),l.text.startsWith('"')&&l.text.endsWith('"')&&(l.text=l.text.substring(1,l.text.length-1)),l.labelType=u.type),n!==void 0&&(l.type=n.type,l.stroke=n.stroke,l.length=n.length>10?10:n.length),i&&!this.edges.some(h=>h.id===i))l.id=i,l.isUserDefinedId=!0;else{let h=this.edges.filter(f=>f.start===l.start&&f.end===l.end);h.length===0?l.id=$h(l.start,l.end,{counter:0,prefix:"L"}):l.id=$h(l.start,l.end,{counter:h.length+1,prefix:"L"})}if(this.edges.length<(this.config.maxEdges??500))Y.info("Pushing edge..."),this.edges.push(l);else throw new Error(`Edge limit exceeded. ${this.edges.length} edges found, but the limit is ${this.config.maxEdges}.
+
+Initialize mermaid with maxEdges set to a higher number to allow more edges.
+You cannot set this config via configuration inside the diagram as it is a secure config.
+You have to call mermaid.initialize.`)}isLinkData(e){return e!==null&&typeof e=="object"&&"id"in e&&typeof e.id=="string"}addLink(e,r,n){let i=this.isLinkData(n)?n.id.replace("@",""):void 0;Y.info("addLink",e,r,i);for(let a of e)for(let s of r){let l=a===e[e.length-1],u=s===r[0];l&&u?this.addSingleLink(a,s,n,i):this.addSingleLink(a,s,n,void 0)}}updateLinkInterpolate(e,r){e.forEach(n=>{n==="default"?this.edges.defaultInterpolate=r:this.edges[n].interpolate=r})}updateLink(e,r){e.forEach(n=>{if(typeof n=="number"&&n>=this.edges.length)throw new Error(`The index ${n} for linkStyle is out of bounds. Valid indices for linkStyle are between 0 and ${this.edges.length-1}. (Help: Ensure that the index is within the range of existing edges.)`);n==="default"?this.edges.defaultStyle=r:(this.edges[n].style=r,(this.edges[n]?.style?.length??0)>0&&!this.edges[n]?.style?.some(i=>i?.startsWith("fill"))&&this.edges[n]?.style?.push("fill:none"))})}addClass(e,r){let n=r.join().replace(/\\,/g,"\xA7\xA7\xA7").replace(/,/g,";").replace(/§§§/g,",").split(";");e.split(",").forEach(i=>{let a=this.classes.get(i);a===void 0&&(a={id:i,styles:[],textStyles:[]},this.classes.set(i,a)),n?.forEach(s=>{if(/color/.exec(s)){let l=s.replace("fill","bgFill");a.textStyles.push(l)}a.styles.push(s)})})}setDirection(e){this.direction=e,/.*/.exec(this.direction)&&(this.direction="LR"),/.*v/.exec(this.direction)&&(this.direction="TB"),this.direction==="TD"&&(this.direction="TB")}setClass(e,r){for(let n of e.split(",")){let i=this.vertices.get(n);i&&i.classes.push(r);let a=this.edges.find(l=>l.id===n);a&&a.classes.push(r);let s=this.subGraphLookup.get(n);s&&s.classes.push(r)}}setTooltip(e,r){if(r!==void 0){r=this.sanitizeText(r);for(let n of e.split(","))this.tooltips.set(this.version==="gen-1"?this.lookUpDomId(n):n,r)}}setClickFun(e,r,n){let i=this.lookUpDomId(e);if(me().securityLevel!=="loose"||r===void 0)return;let a=[];if(typeof n=="string"){a=n.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);for(let l=0;l
")),s.classed("hover",!0)}).on("mouseout",a=>{r.transition().duration(500).style("opacity",0),Ge(a.currentTarget).classed("hover",!1)})}clear(e="gen-2"){this.vertices=new Map,this.classes=new Map,this.edges=[],this.funs=[this.setupToolTips.bind(this)],this.subGraphs=[],this.subGraphLookup=new Map,this.subCount=0,this.tooltips=new Map,this.firstGraphFlag=!0,this.version=e,this.config=me(),Ar()}setGen(e){this.version=e||"gen-2"}defaultStyle(){return"fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;"}addSubGraph(e,r,n){let i=e.text.trim(),a=n.text;e===n&&/\s/.exec(n.text)&&(i=void 0);let s=o(f=>{let d={boolean:{},number:{},string:{}},p=[],m;return{nodeList:f.filter(function(y){let v=typeof y;return y.stmt&&y.stmt==="dir"?(m=y.value,!1):y.trim()===""?!1:v in d?d[v].hasOwnProperty(y)?!1:d[v][y]=!0:p.includes(y)?!1:p.push(y)}),dir:m}},"uniq"),{nodeList:l,dir:u}=s(r.flat());if(this.version==="gen-1")for(let f=0;f0&&r(l)?e>1?PJ(l,e-1,r,n,i):Sm(i,l):n||(i[i.length]=l)}return i}var wc,Cm=N(()=>{"use strict";cT();OJ();o(PJ,"baseFlatten");wc=PJ});function j9e(t){var e=t==null?0:t.length;return e?wc(t,1):[]}var qr,uT=N(()=>{"use strict";Cm();o(j9e,"flatten");qr=j9e});function K9e(t){return uw(cw(t,void 0,qr),t+"")}var BJ,FJ=N(()=>{"use strict";uT();F9();z9();o(K9e,"flatRest");BJ=K9e});function Q9e(t,e,r){var n=-1,i=t.length;e<0&&(e=-e>i?0:i+e),r=r>i?i:r,r<0&&(r+=i),i=e>r?0:r-e>>>0,e>>>=0;for(var a=Array(i);++n{"use strict";o(Q9e,"baseSlice");hT=Q9e});function sDe(t){return aDe.test(t)}var Z9e,J9e,eDe,tDe,rDe,nDe,iDe,aDe,$J,zJ=N(()=>{"use strict";Z9e="\\ud800-\\udfff",J9e="\\u0300-\\u036f",eDe="\\ufe20-\\ufe2f",tDe="\\u20d0-\\u20ff",rDe=J9e+eDe+tDe,nDe="\\ufe0e\\ufe0f",iDe="\\u200d",aDe=RegExp("["+iDe+Z9e+rDe+nDe+"]");o(sDe,"hasUnicode");$J=sDe});function oDe(t,e,r,n){var i=-1,a=t==null?0:t.length;for(n&&a&&(r=t[++i]);++i{"use strict";o(oDe,"arrayReduce");GJ=oDe});function lDe(t,e){return t&&Po(e,zr(e),t)}var UJ,HJ=N(()=>{"use strict";Dd();xc();o(lDe,"baseAssign");UJ=lDe});function cDe(t,e){return t&&Po(e,Cs(e),t)}var WJ,qJ=N(()=>{"use strict";Dd();Bh();o(cDe,"baseAssignIn");WJ=cDe});function uDe(t,e){for(var r=-1,n=t==null?0:t.length,i=0,a=[];++r