From d981c37aac3e56870be8bb641be18a900c8f15c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20L=C3=B6ning?= Date: Sun, 15 Aug 2021 14:52:23 +0100 Subject: [PATCH] Update documentation backend and reduce warnings in doc creation (#1199) (#1205) Update in documentation back-end, all docstrings fixed for pydocstype. #### Reference issues/PR Fixes #1181 Fixes #1230 Partly addresses #1152 #### What does this implement/fix? Explain your changes. * replaces `readthedocs-theme` with `pydata-sphinx-theme` * restructures content in line with new theme * adds new getting started page * fixes autodoc display of summaries of classes and functions * fixes sphinx build and warnings * fixes docstrings and numpydoc warnings * adds tutorial notebook thumbnail gallery * adds links to Twitter, Discord and GitHub * fixes wrong auto-generation directory in sphinx build * updates version of sphinx and related packages * integrates `myst-parser` replacing `m2r2` * update `.gitignore` to exclude auto-generated documentation files * adds glossary * removes full module path from page titles in API reference * removes module names from table of content in API reference using more readable names (e.g. instead of "sktime.forecasting" we now simply use "Forecasting") * adds page for adding estimators using extension templates * adds page for enhancement proposals #### Does your contribution introduce a new dependency? If yes, which one? For generating docs only: * `pydata-sphinx-theme` (replaces `readthedocs` theme) * `myst-parser` (replaces `m2r2`) * `sphinx-gallery` --- .gitignore | 7 +- .readthedocs.yml | 1 + CHANGELOG.rst | 3 +- docs/Makefile | 2 +- docs/estimator_overview_table.md | 112 ----------- docs/requirements.txt | 11 +- docs/source/_static/{ => css}/fields.css | 0 docs/source/_templates/apidoc/module.rst_t | 9 - docs/source/_templates/apidoc/package.rst_t | 52 ----- docs/source/_templates/apidoc/toc.rst_t | 8 - docs/source/_templates/class.rst | 2 +- docs/source/_templates/class_with_call.rst | 8 +- docs/source/_templates/class_without_init.rst | 6 - docs/source/_templates/function.rst | 2 +- docs/source/_templates/module.rst | 42 ---- docs/source/about.rst | 163 +++++++--------- docs/source/about/artwork.md | 7 + docs/source/about/citation.md | 7 + docs/source/about/contributors.md | 2 + docs/source/about/funding.md | 48 +++++ docs/source/about/history.md | 11 ++ docs/source/about/mission.md | 7 + docs/source/api_reference.rst | 31 +-- docs/source/api_reference/annotation.rst | 10 +- docs/source/api_reference/base.rst | 22 +++ docs/source/api_reference/classification.rst | 24 +-- docs/source/api_reference/datasets.rst | 12 +- docs/source/api_reference/exceptions.rst | 10 +- docs/source/api_reference/forecasting.rst | 51 +++-- .../api_reference/performance_metrics.rst | 17 +- docs/source/api_reference/regression.rst | 9 +- .../api_reference/series_as_features.rst | 9 +- docs/source/api_reference/transformations.rst | 62 +++--- docs/source/api_reference/utils.rst | 12 +- docs/source/conf.py | 120 +++++++++--- docs/source/contributing.rst | 3 - docs/source/contributors.rst | 3 - docs/source/developer_guide.rst | 5 +- docs/source/developer_guide/add_dataset.rst | 14 +- .../source/developer_guide/add_estimators.rst | 14 ++ .../source/developer_guide/classification.rst | 11 -- docs/source/developer_guide/documentation.rst | 19 ++ docs/source/developer_guide/forecasting.rst | 11 -- docs/source/developers.rst | 106 ++++++++++ docs/source/enhancement_proposals.rst | 6 + docs/source/estimator_overview.md | 11 ++ docs/source/estimator_overview.rst | 14 -- docs/source/get_involved.rst | 73 +++++++ docs/source/get_involved/contributing.md | 2 + docs/source/get_involved/meetups.rst | 13 ++ docs/source/{ => get_involved}/mentoring.rst | 0 docs/source/get_started.rst | 181 ++++++++++++++++++ docs/source/glossary.rst | 80 ++++++++ .../images/sktime-logo-text-horizontal.png | Bin 0 -> 141419 bytes docs/source/index.rst | 120 +++++++++--- docs/source/reviewer_guide.rst | 38 ++++ docs/source/roadmap.rst | 16 +- docs/source/tutorials.rst | 32 +--- docs/source/user_guide.rst | 3 + docs/source/user_guide/annotation.rst | 11 ++ docs/source/user_guide/clustering.rst | 11 ++ docs/source/user_guide/learning_tasks.rst | 12 -- .../source/user_guide/performance_metrics.rst | 15 ++ docs/source/users.rst | 15 ++ sktime/annotation/__init__.py | 2 + sktime/annotation/adapters/__init__.py | 4 + sktime/annotation/adapters/_pyod.py | 14 +- sktime/annotation/base/__init__.py | 4 + sktime/annotation/base/_base.py | 100 +++++----- sktime/base/_base.py | 66 ++++--- sktime/base/_meta.py | 1 + sktime/benchmarking/evaluation.py | 73 +++---- sktime/benchmarking/metrics.py | 46 ++++- sktime/classification/base.py | 21 +- .../compose/_column_ensemble.py | 99 +++++----- .../classification/dictionary_based/_boss.py | 20 +- .../classification/dictionary_based/_cboss.py | 8 +- .../classification/dictionary_based/_muse.py | 4 +- .../classification/dictionary_based/_tde.py | 4 +- .../dictionary_based/_weasel.py | 4 +- .../distance_based/_proximity_forest.py | 38 ++-- .../distance_based/_time_series_neighbors.py | 4 +- .../feature_based/_catch22_classifier.py | 10 +- .../_matrix_profile_classifier.py | 6 +- .../feature_based/_signature_classifier.py | 6 +- .../feature_based/_tsfresh_classifier.py | 6 +- sktime/classification/interval_based/_rise.py | 31 ++- sktime/classification/shapelet_based/_stc.py | 2 + .../shapelet_based/mrseql/mrseql.pyx | 12 +- sktime/datasets/__init__.py | 3 +- sktime/datasets/_data_io.py | 65 +++---- sktime/datasets/setup.py | 7 +- sktime/datasets/tsc_dataset_names.py | 3 +- sktime/forecasting/__init__.py | 2 + sktime/forecasting/all/__init__.py | 7 +- sktime/forecasting/arima.py | 19 +- sktime/forecasting/base/__init__.py | 4 + sktime/forecasting/base/_base.py | 41 ++-- sktime/forecasting/base/_fh.py | 109 +++++++---- sktime/forecasting/base/_meta.py | 2 + sktime/forecasting/base/_sktime.py | 8 +- sktime/forecasting/base/adapters/__init__.py | 4 +- .../forecasting/base/adapters/_fbprophet.py | 4 +- sktime/forecasting/base/adapters/_pmdarima.py | 4 +- .../forecasting/base/adapters/_statsmodels.py | 16 +- sktime/forecasting/base/adapters/_tbats.py | 4 +- sktime/forecasting/bats.py | 12 +- sktime/forecasting/compose/__init__.py | 5 +- sktime/forecasting/compose/_ensemble.py | 5 +- sktime/forecasting/compose/_multiplexer.py | 3 +- sktime/forecasting/compose/_pipeline.py | 35 ++-- sktime/forecasting/compose/_reduce.py | 34 ++-- sktime/forecasting/compose/_stack.py | 7 +- sktime/forecasting/croston.py | 25 ++- sktime/forecasting/ets.py | 23 ++- sktime/forecasting/exp_smoothing.py | 16 +- sktime/forecasting/fbprophet.py | 7 +- sktime/forecasting/hcrystalball.py | 23 ++- .../forecasting/model_evaluation/__init__.py | 1 + .../model_evaluation/_functions.py | 9 +- .../forecasting/model_selection/__init__.py | 1 + sktime/forecasting/model_selection/_split.py | 40 ++-- sktime/forecasting/model_selection/_tune.py | 2 + sktime/forecasting/naive.py | 12 +- .../forecasting/online_learning/__init__.py | 3 +- .../online_learning/_online_ensemble.py | 16 +- .../_prediction_weighted_ensembler.py | 63 ++++-- sktime/forecasting/tbats.py | 12 +- sktime/forecasting/theta.py | 45 ++--- sktime/forecasting/trend.py | 11 +- sktime/performance_metrics/base/_base.py | 1 + .../forecasting/_classes.py | 9 + .../forecasting/_functions.py | 12 +- sktime/registry/__init__.py | 2 +- sktime/registry/_base_classes.py | 3 +- sktime/registry/_tags.py | 3 +- sktime/regression/__init__.py | 4 +- sktime/regression/all/__init__.py | 1 + sktime/regression/base.py | 45 ++++- sktime/regression/compose/__init__.py | 2 + sktime/regression/compose/_ensemble.py | 10 +- sktime/regression/interval_based/__init__.py | 1 + sktime/regression/interval_based/_tsf.py | 20 +- .../base/estimators/_ensemble.py | 24 ++- sktime/transformations/series/__init__.py | 2 + sktime/transformations/series/acf.py | 18 +- sktime/transformations/series/adapt.py | 9 +- sktime/transformations/series/boxcox.py | 54 +++++- sktime/transformations/series/compose.py | 22 +-- sktime/transformations/series/cos.py | 25 ++- .../series/detrend/_deseasonalize.py | 21 +- .../series/detrend/_detrend.py | 21 +- sktime/transformations/series/exponent.py | 11 +- sktime/transformations/series/impute.py | 6 +- .../transformations/series/matrix_profile.py | 19 +- .../series/outlier_detection.py | 25 ++- sktime/transformations/series/summarize.py | 11 +- sktime/transformations/series/theta.py | 23 ++- 158 files changed, 2153 insertions(+), 1286 deletions(-) delete mode 100644 docs/estimator_overview_table.md rename docs/source/_static/{ => css}/fields.css (100%) delete mode 100644 docs/source/_templates/apidoc/module.rst_t delete mode 100644 docs/source/_templates/apidoc/package.rst_t delete mode 100644 docs/source/_templates/apidoc/toc.rst_t delete mode 100644 docs/source/_templates/class_without_init.rst delete mode 100644 docs/source/_templates/module.rst create mode 100644 docs/source/about/artwork.md create mode 100644 docs/source/about/citation.md create mode 100644 docs/source/about/contributors.md create mode 100644 docs/source/about/funding.md create mode 100644 docs/source/about/history.md create mode 100644 docs/source/about/mission.md create mode 100644 docs/source/api_reference/base.rst delete mode 100644 docs/source/contributing.rst delete mode 100644 docs/source/contributors.rst create mode 100644 docs/source/developer_guide/add_estimators.rst delete mode 100644 docs/source/developer_guide/classification.rst create mode 100644 docs/source/developer_guide/documentation.rst delete mode 100644 docs/source/developer_guide/forecasting.rst create mode 100644 docs/source/developers.rst create mode 100644 docs/source/enhancement_proposals.rst create mode 100644 docs/source/estimator_overview.md delete mode 100644 docs/source/estimator_overview.rst create mode 100644 docs/source/get_involved.rst create mode 100644 docs/source/get_involved/contributing.md create mode 100644 docs/source/get_involved/meetups.rst rename docs/source/{ => get_involved}/mentoring.rst (100%) create mode 100644 docs/source/get_started.rst create mode 100644 docs/source/glossary.rst create mode 100644 docs/source/images/sktime-logo-text-horizontal.png create mode 100644 docs/source/reviewer_guide.rst create mode 100644 docs/source/user_guide/annotation.rst create mode 100644 docs/source/user_guide/clustering.rst create mode 100644 docs/source/user_guide/performance_metrics.rst create mode 100644 docs/source/users.rst diff --git a/.gitignore b/.gitignore index e4e90f2fc0b..5459b8631b0 100644 --- a/.gitignore +++ b/.gitignore @@ -73,11 +73,8 @@ instance/ # Sphinx documentation docs/_build/ -# automatically generated content -docs/source/modules/auto_generated/ -docs/source/api_reference/modules/auto_generated/ - -# automatically sym-linked example notebooks +docs/source/api_reference/auto_generated/ +docs/estimator_overview_table.md docs/source/examples # PyBuilder diff --git a/.readthedocs.yml b/.readthedocs.yml index 7d9d25e8037..0692fbe32e4 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -18,3 +18,4 @@ python: sphinx: configuration: docs/source/conf.py +# fail_on_warning: True diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0323318b859..9fb15437f5c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -310,8 +310,7 @@ Changed Fixed ~~~~~ -* Fix links in Readthedocs and Binder launch button (#416) -@mloning +* Fix links in Readthedocs and Binder launch button (#416) @mloning * Fixed small bug in performance metrics (#422) @krumeto * Resolved warnings in notebook examples (#418) @alwinw * Resolves #325 ModuleNotFoundError for soft dependencies (#410) @alwinw diff --git a/docs/Makefile b/docs/Makefile index c7946fbd17d..957e8aa0894 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line. PREPROCESS = sphinx-apidoc APIDOCTEMPLATEDIR = source/_templates/apidoc -AUTOGENDIR = source/modules/auto_generated +AUTOGENDIR = source/api_reference/auto_generated SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = source diff --git a/docs/estimator_overview_table.md b/docs/estimator_overview_table.md deleted file mode 100644 index 7aa1545d2ef..00000000000 --- a/docs/estimator_overview_table.md +++ /dev/null @@ -1,112 +0,0 @@ -| Class Name | Estimator Type | Authors | -|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------|:---------------------------------------------------------------------------| -| ARIMA | forecasting | Markus Löning & Hongyi Yang | -| AggrDist | dists_kernels | fkiraly | -| Arsenal | classification::kernel_based | Matthew Middlehurst & Oleksii Kachaiev | -| AutoARIMA | forecasting | Markus Löning & Hongyi Yang | -| AutoCorrelationTransformer | transformations::series | Afzal Ansari | -| AutoETS | forecasting | Hongyi Yang | -| BATS | forecasting | Martin Walter | -| BOSSEnsemble | classification::dictionary_based | Matthew Middlehurst | -| BoxCoxTransformer | transformations::series | Markus Löning | -| CanonicalIntervalForest | classification::interval_based | Matthew Middlehurst | -| Catch22 | transformations::panel | Matthew Middlehurst | -| Catch22ForestClassifier | classification::hybrid | Matthew Middlehurst | -| ColumnConcatenator | transformations::panel | Markus Löning & Sajay Ganesh | -| ColumnEnsembleClassifier | classification::compose | Aaron Bostrom | -| ColumnTransformer | transformations::panel | Markus Löning & Sajay Ganesh | -| ComposableTimeSeriesForestClassifier | classification::compose | Markus Löning & Ayushmaan Seth | -| ComposableTimeSeriesForestRegressor | regression::compose | Markus Löning & Ayushmaan Seth | -| ConditionalDeseasonalizer | transformations::series::detrend | Markus Löning | -| ContractableBOSS | classification::dictionary_based | Matthew Middlehurst | -| ContractedShapeletTransform | transformations::panel | Jason Lines & David Guijo | -| CosineTransformer | transformations::series | Afzal Ansari | -| Croston | forecasting | no author info | -| DWTTransformer | transformations::panel | Vincent Nicholson | -| DerivativeSlopeTransformer | transformations::panel::summarize | no author info | -| Deseasonalizer | transformations::series::detrend | Markus Löning | -| Detrender | transformations::series::detrend | Markus Löning & Svea Meyer | -| DirRecTabularRegressionForecaster | forecasting::compose | Ayushmaan Seth, Kavin Anand, Luis Zugasti, Lovkush Agarwal & Markus Löning | -| DirRecTimeSeriesRegressionForecaster | forecasting::compose | Ayushmaan Seth, Kavin Anand, Luis Zugasti, Lovkush Agarwal & Markus Löning | -| DirectTabularRegressionForecaster | forecasting::compose | Ayushmaan Seth, Kavin Anand, Luis Zugasti, Lovkush Agarwal & Markus Löning | -| DirectTimeSeriesRegressionForecaster | forecasting::compose | Ayushmaan Seth, Kavin Anand, Luis Zugasti, Lovkush Agarwal & Markus Löning | -| DrCIF | classification::interval_based | Matthew Middlehurst | -| ElasticEnsemble | classification::distance_based | Jason Lines | -| EnsembleForecaster | forecasting::compose | Markus Löning | -| ExponentialSmoothing | forecasting | Markus Löning & @big-o | -| FeatureUnion | series_as_features::compose | Markus Löning | -| FittedParamExtractor | transformations::panel::summarize | no author info | -| ForecastingGridSearchCV | forecasting::model_selection | Markus Löning | -| ForecastingPipeline | forecasting::compose | Markus Löning & Martin Walter | -| ForecastingRandomizedSearchCV | forecasting::model_selection | Markus Löning | -| HCrystalBallForecaster | forecasting | no author info | -| HIVECOTEV1 | classification::hybrid | Matthew Middlehurst | -| HOG1DTransformer | transformations::panel | no author info | -| HampelFilter | transformations::series | Martin Walter | -| Imputer | transformations::series | Martin Walter | -| IndividualBOSS | classification::dictionary_based | Matthew Middlehurst | -| IndividualTDE | classification::dictionary_based | Matthew Middlehurst | -| IntervalSegmenter | transformations::panel | no author info | -| KNeighborsTimeSeriesClassifier | classification::distance_based | Jason Lines & TonyBagnall | -| LogTransformer | transformations::series | Markus Löning | -| MUSE | classification::dictionary_based | Patrick Schäfer | -| MatrixProfile | transformations::panel | no author info | -| MatrixProfileTransformer | transformations::series | Markus Löning | -| MeanTransformer | transformations::series | Markus Löning | -| MiniRocket | transformations::panel::rocket | Angus Dempster | -| MiniRocketMultivariate | transformations::panel::rocket | Angus Dempster | -| MrSEQLClassifier | classification::shapelet_based::mrseql | Thach Le Nguyen | -| MultioutputTabularRegressionForecaster | forecasting::compose | Ayushmaan Seth, Kavin Anand, Luis Zugasti, Lovkush Agarwal & Markus Löning | -| MultioutputTimeSeriesRegressionForecaster | forecasting::compose | Ayushmaan Seth, Kavin Anand, Luis Zugasti, Lovkush Agarwal & Markus Löning | -| MultiplexForecaster | forecasting::compose | Kutay Koralturk | -| NaiveForecaster | forecasting | Markus Löning & Piyush Gade | -| OnlineEnsembleForecaster | forecasting::online_learning | no author info | -| OptionalPassthrough | transformations::series | Martin Walter | -| PAA | transformations::panel::dictionary_based | Matthew Middlehurst | -| PCATransformer | transformations::panel | Patrick Rockenschaub | -| PaddingTransformer | transformations::panel | Aaron Bostrom | -| PartialAutoCorrelationTransformer | transformations::series | Afzal Ansari | -| PlateauFinder | transformations::panel::summarize | no author info | -| PolynomialTrendForecaster | forecasting | Markus Löning | -| Prophet | forecasting | Martin Walter | -| ProximityForest | classification::distance_based | George Oastler | -| ProximityStump | classification::distance_based | George Oastler (linkedin.com/goastler; github.com/goastler) | -| ProximityTree | classification::distance_based | George Oastler | -| PyODAnnotator | annotation::adapters | mloning, satya-pattnaik & fkiraly | -| ROCKETClassifier | classification::kernel_based | Matthew Middlehurst | -| RandomIntervalFeatureExtractor | transformations::panel::summarize | no author info | -| RandomIntervalSegmenter | transformations::panel | no author info | -| RandomIntervalSpectralForest | classification::interval_based | Tony Bagnall & Yi-Xuan Xu | -| RecursiveTabularRegressionForecaster | forecasting::compose | Ayushmaan Seth, Kavin Anand, Luis Zugasti, Lovkush Agarwal & Markus Löning | -| RecursiveTimeSeriesRegressionForecaster | forecasting::compose | Ayushmaan Seth, Kavin Anand, Luis Zugasti, Lovkush Agarwal & Markus Löning | -| Rocket | transformations::panel::rocket | Angus Dempster | -| SAX | transformations::panel::dictionary_based | Matthew Middlehurst | -| SFA | transformations::panel::dictionary_based | Matthew Middlehurst & Patrick Schäfer | -| ScipyDist | dists_kernels | fkiraly | -| SeriesToPrimitivesRowTransformer | transformations::panel | Markus Löning & Sajay Ganesh | -| SeriesToSeriesRowTransformer | transformations::panel | Markus Löning & Sajay Ganesh | -| ShapeDTW | classification::distance_based | Vincent Nicholson | -| ShapeletTransform | transformations::panel | Jason Lines & David Guijo | -| ShapeletTransformClassifier | classification::shapelet_based | Tony Bagnall | -| SignatureClassifier | classification::signature_based | no author info | -| SignatureTransformer | transformations::panel::signature_based | no author info | -| SlidingWindowSegmenter | transformations::panel | no author info | -| SlopeTransformer | transformations::panel | no author info | -| StackingForecaster | forecasting::compose | Markus Löning | -| SupervisedTimeSeriesForest | classification::interval_based | Matthew Middlehurst | -| TBATS | forecasting | Martin Walter | -| TSFreshFeatureExtractor | transformations::panel | Ayushmaan Seth, Markus Löning & Alwin Wang | -| TSFreshRelevantFeatureExtractor | transformations::panel | Ayushmaan Seth, Markus Löning & Alwin Wang | -| TSInterpolator | transformations::panel | no author info | -| TabularToSeriesAdaptor | transformations::series | Markus Löning | -| Tabularizer | transformations::panel | Markus Löning | -| TemporalDictionaryEnsemble | classification::dictionary_based | Matthew Middlehurst | -| ThetaForecaster | forecasting | @big-o & Markus Löning | -| ThetaLinesTransformer | transformations::series | Guzal Bulatova & Markus Löning | -| TimeSeriesForestClassifier | classification::interval_based | Tony Bagnall, kkoziara, luiszugasti & kanand77 | -| TimeSeriesForestRegressor | regression::interval_based | Tony Bagnall, kkoziara, luiszugasti, kanand77 & Markus Löning | -| TimeSeriesKMeans | clustering | Christopher Holder & Tony Bagnall | -| TimeSeriesKMedoids | clustering | Christopher Holder & Tony Bagnall | -| TransformedTargetForecaster | forecasting::compose | Markus Löning & Martin Walter | -| TruncationTransformer | transformations::panel | Aaron Bostrom | -| WEASEL | classification::dictionary_based | Patrick Schäfer & Arik Ermshaus | diff --git a/docs/requirements.txt b/docs/requirements.txt index c4cc19bc6b8..b6abafc9234 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,10 @@ jupyter -m2r2 -nbsphinx +myst-parser +nbsphinx==0.8.6 numpydoc -sphinx==3.2.* -sphinx_rtd_theme +pydata-sphinx-theme +sphinx==4.1.1 +sphinx-gallery==0.6.0 +sphinx-panels==0.6.0 +sphinx_issues==1.2.0 tabulate diff --git a/docs/source/_static/fields.css b/docs/source/_static/css/fields.css similarity index 100% rename from docs/source/_static/fields.css rename to docs/source/_static/css/fields.css diff --git a/docs/source/_templates/apidoc/module.rst_t b/docs/source/_templates/apidoc/module.rst_t deleted file mode 100644 index 0c394131045..00000000000 --- a/docs/source/_templates/apidoc/module.rst_t +++ /dev/null @@ -1,9 +0,0 @@ -{%- if show_headings %} -{{- basename | e | heading(2) }} - -{% endif -%} -.. automodule:: {{ qualname }} -{%- for option in automodule_options %} - :{{ option }}: -{%- endfor %} - diff --git a/docs/source/_templates/apidoc/package.rst_t b/docs/source/_templates/apidoc/package.rst_t deleted file mode 100644 index 08bafcc2a83..00000000000 --- a/docs/source/_templates/apidoc/package.rst_t +++ /dev/null @@ -1,52 +0,0 @@ -{%- macro automodule(modname, options) -%} -.. automodule:: {{ modname }} -{%- for option in options %} - :{{ option }}: -{%- endfor %} -{%- endmacro %} - -{%- macro toctree(docnames) -%} -.. toctree:: -{% for docname in docnames %} - {{ docname }} -{%- endfor %} -{%- endmacro %} - -{%- if is_namespace %} -{{- [pkgname, "namespace"] | join(" ") | e | heading }} -{% else %} -{{- pkgname | e | heading(2) }} -{% endif %} - -{%- if modulefirst and not is_namespace %} -{{ automodule(pkgname, automodule_options) }} -{% endif %} - -{%- if subpackages %} -Subpackages ------------ - -{{ toctree(subpackages) }} -{% endif %} - -{%- if submodules %} -Submodules ----------- -{% if separatemodules %} -{{ toctree(submodules) }} -{%- else %} -{%- for submodule in submodules %} -{% if show_headings %} -{{- submodule | e | heading(2) }} -{% endif %} -{{ automodule(submodule, automodule_options) }} -{% endfor %} -{%- endif %} -{% endif %} - -{%- if not modulefirst and not is_namespace %} -Module contents ---------------- - -{{ automodule(pkgname, automodule_options) }} -{% endif %} diff --git a/docs/source/_templates/apidoc/toc.rst_t b/docs/source/_templates/apidoc/toc.rst_t deleted file mode 100644 index f0877eeb2f8..00000000000 --- a/docs/source/_templates/apidoc/toc.rst_t +++ /dev/null @@ -1,8 +0,0 @@ -{{ header | heading }} - -.. toctree:: - :maxdepth: {{ maxdepth }} -{% for docname in docnames %} - {{ docname }} -{%- endfor %} - diff --git a/docs/source/_templates/class.rst b/docs/source/_templates/class.rst index 79ff2cf8077..e45bca4524c 100644 --- a/docs/source/_templates/class.rst +++ b/docs/source/_templates/class.rst @@ -1,4 +1,4 @@ -:mod:`{{module}}`.{{objname}} +{{objname}} {{ underline }}============== .. currentmodule:: {{ module }} diff --git a/docs/source/_templates/class_with_call.rst b/docs/source/_templates/class_with_call.rst index 70e46d35831..9fc4b65ce10 100644 --- a/docs/source/_templates/class_with_call.rst +++ b/docs/source/_templates/class_with_call.rst @@ -1,14 +1,10 @@ -:mod:`{{module}}`.{{objname}} +{{objname}} {{ underline }}=============== .. currentmodule:: {{ module }} .. autoclass:: {{ objname }} - - {% block methods %} - .. automethod:: __init__ - .. automethod:: __call__ - {% endblock %} + :special-members: __call__ .. include:: {{module}}.{{objname}}.examples diff --git a/docs/source/_templates/class_without_init.rst b/docs/source/_templates/class_without_init.rst deleted file mode 100644 index 307b0199c30..00000000000 --- a/docs/source/_templates/class_without_init.rst +++ /dev/null @@ -1,6 +0,0 @@ -:mod:`{{module}}`.{{objname}} -{{ underline }}============== - -.. currentmodule:: {{ module }} - -.. autoclass:: {{ objname }} diff --git a/docs/source/_templates/function.rst b/docs/source/_templates/function.rst index f4b11eda770..61d178dcbfe 100644 --- a/docs/source/_templates/function.rst +++ b/docs/source/_templates/function.rst @@ -1,4 +1,4 @@ -:mod:`{{module}}`.{{objname}} +{{objname}} {{ underline }}==================== .. currentmodule:: {{ module }} diff --git a/docs/source/_templates/module.rst b/docs/source/_templates/module.rst deleted file mode 100644 index e5a4b5402d3..00000000000 --- a/docs/source/_templates/module.rst +++ /dev/null @@ -1,42 +0,0 @@ -.. _mod-{{ fullname }}: - -{{ fullname | underline }} - -.. automodule:: {{ fullname }} - - {% block functions %} - {% if functions %} - .. rubric:: Functions - - .. autosummary:: - :toctree: {{ objname }} - :template: function.rst - {% for item in functions %} - {{ item }} - {%- endfor %} - {% endif %} - {% endblock %} - - {% block classes %} - {% if classes %} - .. rubric:: Classes - - .. autosummary:: - :toctree: {{ objname }} - :template: class.rst - {% for item in classes %} - {{ item }} - {%- endfor %} - {% endif %} - {% endblock %} - - {% block exceptions %} - {% if exceptions %} - .. rubric:: Exceptions - - .. autosummary:: - {% for item in exceptions %} - {{ item }} - {%- endfor %} - {% endif %} - {% endblock %} diff --git a/docs/source/about.rst b/docs/source/about.rst index ed4e54ce341..b21d3ce941a 100644 --- a/docs/source/about.rst +++ b/docs/source/about.rst @@ -1,130 +1,105 @@ .. _about: -About us -======== +===== +About +===== -Mission statement ------------------ +.. toctree:: + :maxdepth: 1 + :hidden: -sktime enables understandable and composable machine learning with time -series. It provides `scikit-learn `_ -compatible algorithms and model composition tools, supported by a clear -taxonomy of learning tasks, with instructive documentation and a friendly community. + about/mission.md + about/contributors.md + about/history.md + about/funding.md + about/citation.md + about/artwork.md +Learn more about the sktime project and its community. -History -------- +.. panels:: + :card: + intro-card text-center -sktime was started in April 2019 as a collaborative project between -Franz Király, Markus Löning, Tony Bagnall and Jason -Lines. In the first year, it grew into a community-driven project with -contributions from researchers and practitioners from around the globe. + --- + Mission + ^^^^^^^ -Authors -------- + Learm more about sktime's mission. -For an overview of current and past contributors, please see our -:ref:`contributors page `. + +++ + .. link-button:: mission + :type: ref + :text: Our Mission + :classes: btn-block btn-secondary stretched-link -Citing sktime -------------- + --- -If you use sktime in a scientific publication, we would appreciate -citations to the following paper: + Contributors + ^^^^^^^^^^^^ -* [paper] `Markus Löning, Anthony Bagnall, Sajaysurya Ganesh, Viktor Kazakov, Jason Lines, Franz Király (2019): “sktime: A Unified Interface for Machine Learning with Time Series” `_ -* [software] `Markus Löning, Tony Bagnall, Sajaysurya Ganesh, George Oastler, Jason Lines, ViktorKaz, …, Aadesh Deshmukh (2020). alan-turing-institute/sktime. Zenodo. http://doi.org/10.5281/zenodo.3749000 `_ + The wonderful people who make the project possible. + +++ -Artwork -------- + .. link-button:: contributors + :type: ref + :text: Contributors + :classes: btn-block btn-secondary stretched-link -High-quality logos are available in the `docs/source/images/ `_ directory. + --- -.. image:: images/sktime-logo-no-text.jpg - :align: center + History + ^^^^^^^ -Funding -------- + Learn how sktime got here. -sktime is a community-driven project, however institutional and private -grants help to assure its sustainability. + +++ -We would like to thank the following funders. + .. link-button:: history + :type: ref + :text: History + :classes: btn-block btn-secondary stretched-link -................................... + --- -.. raw:: html + Funding + ^^^^^^^ -
-
+ Thank you to sktime's supporters. -`The Alan Turing Institute `_ -funded three months of the initial development under the UKRI Strategic -Priorities Fund (EPSRC grant no EP/T001569/1), particularly the `Tools, -Practices and Systems `_ theme within that grant. + +++ + .. link-button:: funding + :type: ref + :text: Fund sktime + :classes: btn-block btn-secondary stretched-link -.. raw:: html + --- -
-
+ Citation + ^^^^^^^^ -.. image:: images/the-alan-turing-institute.png - :width: 100pt - :target: https://turing.ac.uk/ + Learn how to cite sktime. -.. raw:: html + +++ -
-
+ .. link-button:: citation + :type: ref + :text: Citation + :classes: btn-block btn-secondary stretched-link + --- -................................... + Artwork + ^^^^^^^ -.. raw:: html + Our logo and other graphics. -
-
+ +++ -Markus Löning's contribution was supported by the `UK Economic and Social -Research Council (ESRC) `_, the `Consumer Data -Research Centre (CDRC) `_, and the Enrichment -Scheme at the `The Alan Turing Institute `_. - - -.. raw:: html - -
-
- -.. image:: images/esrc-ukri.png - :width: 100pt - :target: https://esrc.ukri.org - -.. image:: images/cdrc.jpg - :width: 100pt - :target: https://www.cdrc.ac.uk - -.. raw:: html - -
-
- - -Sprints -------- - -The `2019 joint sktime MLJ development sprint `_ was kindly hosted by `UCL -`_ and `The Alan Turing Institute `_. Some participants could attend thanks to the -initial funding of the `The Alan Turing Institute `_. - - -Infrastructure support ----------------------- - -We would also like to thank `Microsoft Azure `_, `GitHub Actions `_, and `AppVeyor `_, `ReadtheDocs `_ for the free computing time on their Continuous Integration servers. + .. link-button:: artwork + :type: ref + :text: Artwork + :classes: btn-block btn-secondary stretched-link diff --git a/docs/source/about/artwork.md b/docs/source/about/artwork.md new file mode 100644 index 00000000000..c3c390c2584 --- /dev/null +++ b/docs/source/about/artwork.md @@ -0,0 +1,7 @@ +# Artwork + +High-quality logos are available in the [`docs/source/images/`](https://github.com/alan-turing-institute/sktime/tree/main/docs/source/images) directory on GitHub. + +```{image} ../images/sktime-logo-no-text.jpg +:align: center +``` diff --git a/docs/source/about/citation.md b/docs/source/about/citation.md new file mode 100644 index 00000000000..f5e199800b0 --- /dev/null +++ b/docs/source/about/citation.md @@ -0,0 +1,7 @@ +# Citing sktime + +If you use sktime in a scientific publication, we would appreciate +citations to the following paper: + +* [`Markus Löning, Anthony Bagnall, Sajaysurya Ganesh, Viktor Kazakov, Jason Lines, Franz Király (2019): “sktime: A Unified Interface for Machine Learning with Time Series”](http://learningsys.org/neurips19/assets/papers/sktime_ml_systems_neurips2019.pdf) +* [Markus Löning, Tony Bagnall, Sajaysurya Ganesh, George Oastler, Jason Lines, ViktorKaz, …, Aadesh Deshmukh (2020). alan-turing-institute/sktime. Zenodo. http://doi.org/10.5281/zenodo.3749000](http://doi.org/10.5281/zenodo.3749000) diff --git a/docs/source/about/contributors.md b/docs/source/about/contributors.md new file mode 100644 index 00000000000..362e062fc5d --- /dev/null +++ b/docs/source/about/contributors.md @@ -0,0 +1,2 @@ +```{include} ../../../CONTRIBUTORS.md +``` diff --git a/docs/source/about/funding.md b/docs/source/about/funding.md new file mode 100644 index 00000000000..11def470908 --- /dev/null +++ b/docs/source/about/funding.md @@ -0,0 +1,48 @@ +# Funding + +sktime is a community-driven project, however institutional and private grants help to assure its sustainability. + +We would like to thank the following supporters. + +## Research grants + +[The Alan Turing Institute] funded three months of the initial development under the UKRI Strategic +Priorities Fund (EPSRC grant no EP/T001569/1), particularly the [Tools, +Practices and Systems](https://www.turing.ac.uk/events/tools-practices-and-systems-data-science-and-artificial-intelligence-scoping-workshop) theme within that grant. + +Markus Löning's contribution was supported by the [UK Economic and Social +Research Council (ESRC)](https://esrc.ukri.org), the [Consumer Data +Research Centre (CDRC)](https://www.cdrc.ac.uk), and the Enrichment +Scheme at the [The Alan Turing Institute]. + +```{image} ../images/the-alan-turing-institute.png +:width: 100pt +:target: https://turing.ac.uk/ +``` + +```{image} ../images/esrc-ukri.png +:width: 100pt +:target: https://esrc.ukri.org +``` + +```{image} ../images/cdrc.jpg +:width: 100pt +:target: https://www.cdrc.ac.uk +``` + +## Institutional sponsorship + +The [2019 joint sktime MLJ development sprint](https://github.com/sktime/sktime-workshops/tree/master/previous_workshops/2019_sktime_MLJ_joint_dev_sprint) was kindly hosted by [UCL] and [The Alan Turing Institute]. Some participants could attend thanks to the +initial funding of the [The Alan Turing Institute]. + +## Infrastructure support + +We would also like to thank [Microsoft Azure], [GitHub Actions], [AppVeyor] and [ReadtheDocs] for the free compute time on their servers. + +[microsoft azure]: https://azure.microsoft.com/en-gb/services/devops/ +[github actions]: https://docs.github.com/en/free-pro-team@latest/actions +[appveyor]: https://www.appveyor.com +[readthedocs]: https://readthedocs.org + +[the alan turing institute]: https://turing.ac.uk +[ucl]: https://www.ucl.ac.uk diff --git a/docs/source/about/history.md b/docs/source/about/history.md new file mode 100644 index 00000000000..831ce9a74f9 --- /dev/null +++ b/docs/source/about/history.md @@ -0,0 +1,11 @@ +# History + +sktime was started in April 2019 as a collaborative project between Franz Király, Markus Löning, Anthony Bagnall and Jason Lines. +In the first year, it grew into a community-driven project with contributions from researchers and practitioners from around the globe. + +Today, sktime continues to undergo rapid development to refine its API, while adding new features and algorithms for a range of time series machine learning tasks. +Development is supported by the original project members, new core developers and the broader community (see [contributors]). + +If your interested in contributing, you can find out how you can contribute in our developer information. + +[contributors]: contributors.md diff --git a/docs/source/about/mission.md b/docs/source/about/mission.md new file mode 100644 index 00000000000..5120dd4430c --- /dev/null +++ b/docs/source/about/mission.md @@ -0,0 +1,7 @@ +# Mission + +sktime enables understandable and composable machine learning with time +series. It provides [scikit-learn] compatible algorithms and model composition tools, supported by a clear +taxonomy of learning tasks, with instructive documentation and a friendly community. + +[scikit-learn]: https://scikit-learn.org/stable/ diff --git a/docs/source/api_reference.rst b/docs/source/api_reference.rst index aa95926c975..38fc7f4e81e 100644 --- a/docs/source/api_reference.rst +++ b/docs/source/api_reference.rst @@ -4,20 +4,25 @@ API Reference ============= -This is the class and function reference for ``sktime``. +Welcome to the API reference for ``sktime``. -.. autosummary:: - :toctree: modules/auto_generated/ +The API reference provides a technical manual. +It describes the classes and functions included in sktime. +For a scientific manual, see the :ref:`user_guide`. .. include:: includes/api_css.rst -.. include:: api_reference/classification.rst -.. include:: api_reference/regression.rst -.. include:: api_reference/series_as_features.rst -.. include:: api_reference/forecasting.rst -.. include:: api_reference/annotation.rst -.. include:: api_reference/transformations.rst -.. include:: api_reference/performance_metrics.rst -.. include:: api_reference/datasets.rst -.. include:: api_reference/utils.rst -.. include:: api_reference/exceptions.rst +.. toctree:: + :maxdepth: 1 + + api_reference/base + api_reference/forecasting + api_reference/annotation + api_reference/classification + api_reference/regression + api_reference/series_as_features + api_reference/transformations + api_reference/performance_metrics + api_reference/datasets + api_reference/utils + api_reference/exceptions diff --git a/docs/source/api_reference/annotation.rst b/docs/source/api_reference/annotation.rst index 7a6a829fd4d..6c28f5f2309 100644 --- a/docs/source/api_reference/annotation.rst +++ b/docs/source/api_reference/annotation.rst @@ -1,13 +1,15 @@ .. _annotation_ref: -sktime.annotation: Time series annotation -========================================= +Time series annotation +====================== The :mod:`sktime.annotation` module contains algorithms and composition tools for time series annotation (for example, anomaly or outlier detection). .. automodule:: sktime.annotation - :no-members: + :no-members: + :no-inherited-members: + Adapters -------- @@ -15,7 +17,7 @@ Adapters .. currentmodule:: sktime.annotation.adapters .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst PyODAnnotator diff --git a/docs/source/api_reference/base.rst b/docs/source/api_reference/base.rst new file mode 100644 index 00000000000..c8f24a0a307 --- /dev/null +++ b/docs/source/api_reference/base.rst @@ -0,0 +1,22 @@ +.. _base_ref: + +Base +==== + +The :mod:`sktime.base` module contains abstract base classes. + +.. automodule:: sktime.base + :no-members: + :no-inherited-members: + +Base classes +------------ + +.. currentmodule:: sktime.base + +.. autosummary:: + :toctree: auto_generated/ + :template: class.rst + + BaseObject + BaseEstimator diff --git a/docs/source/api_reference/classification.rst b/docs/source/api_reference/classification.rst index ddbf2d9a58c..590cf86dade 100644 --- a/docs/source/api_reference/classification.rst +++ b/docs/source/api_reference/classification.rst @@ -1,12 +1,14 @@ .. _classification_ref: -sktime.classification: Time series classification -================================================= +Time series classification +========================== The :mod:`sktime.classification` module contains algorithms and composition tools for time series classification. .. automodule:: sktime.classification :no-members: + :no-inherited-members: + Composition ----------- @@ -14,7 +16,7 @@ Composition .. currentmodule:: sktime.classification.compose .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst ColumnEnsembleClassifier @@ -25,7 +27,7 @@ Dictionary-based .. currentmodule:: sktime.classification.dictionary_based .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst IndividualBOSS @@ -42,7 +44,7 @@ Distance-based .. currentmodule:: sktime.classification.distance_based .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst KNeighborsTimeSeriesClassifier @@ -57,7 +59,7 @@ Hybrid .. currentmodule:: sktime.classification.hybrid .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst HIVECOTEV1 @@ -68,7 +70,7 @@ Interval-based .. currentmodule:: sktime.classification.interval_based .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst TimeSeriesForestClassifier @@ -83,19 +85,19 @@ Shapelet-based .. currentmodule:: sktime.classification.shapelet_based .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst ShapeletTransformClassifier MrSEQLClassifier Kernel-based --------------- +------------ .. currentmodule:: sktime.classification.kernel_based .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst ROCKETClassifier @@ -107,7 +109,7 @@ Feature-based .. currentmodule:: sktime.classification.feature_based .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst Catch22Classifier diff --git a/docs/source/api_reference/datasets.rst b/docs/source/api_reference/datasets.rst index 1ce0b562255..0263e40c0bd 100644 --- a/docs/source/api_reference/datasets.rst +++ b/docs/source/api_reference/datasets.rst @@ -1,12 +1,16 @@ .. _datasets_ref: -sktime.datasets: Datasets -========================= +Datasets +======== -.. currentmodule:: sktime.datasets.base +.. automodule:: sktime.datasets + :no-members: + :no-inherited-members: + +.. currentmodule:: sktime.datasets .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: function.rst load_airline diff --git a/docs/source/api_reference/exceptions.rst b/docs/source/api_reference/exceptions.rst index 2ccb53b3d8f..2d18760405e 100644 --- a/docs/source/api_reference/exceptions.rst +++ b/docs/source/api_reference/exceptions.rst @@ -1,14 +1,18 @@ .. _exceptions_ref: -sktime.exceptions: Exceptions -============================= +Exceptions +========== The :mod:`sktime.exceptions` module contains classes for exceptions and warnings. +.. automodule:: sktime.exceptions + :no-members: + :no-inherited-members: + .. currentmodule:: sktime.exceptions .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst NotEvaluatedError diff --git a/docs/source/api_reference/forecasting.rst b/docs/source/api_reference/forecasting.rst index d970284b95d..bb20f0b18d7 100644 --- a/docs/source/api_reference/forecasting.rst +++ b/docs/source/api_reference/forecasting.rst @@ -1,13 +1,15 @@ .. _forecasting_ref: -sktime.forecasting: Time series forecasting -=========================================== +Forecasting +=========== The :mod:`sktime.forecasting` module contains algorithms and composition tools for forecasting. .. automodule:: sktime.forecasting :no-members: + :no-inherited-members: + Base ---- @@ -15,7 +17,7 @@ Base .. currentmodule:: sktime.forecasting.base .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst ForecastingHorizon @@ -26,7 +28,7 @@ Naive .. currentmodule:: sktime.forecasting.naive .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst NaiveForecaster @@ -37,7 +39,7 @@ Trend .. currentmodule:: sktime.forecasting.trend .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst PolynomialTrendForecaster @@ -48,7 +50,7 @@ Exponential Smoothing .. currentmodule:: sktime.forecasting.exp_smoothing .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst ExponentialSmoothing @@ -56,7 +58,7 @@ Exponential Smoothing .. currentmodule:: sktime.forecasting.ets .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst AutoETS @@ -67,7 +69,7 @@ ARIMA .. currentmodule:: sktime.forecasting.arima .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst AutoARIMA @@ -79,7 +81,7 @@ Theta .. currentmodule:: sktime.forecasting.theta .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst ThetaForecaster @@ -90,7 +92,7 @@ BATS/TBATS .. currentmodule:: sktime.forecasting.bats .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst BATS @@ -98,18 +100,29 @@ BATS/TBATS .. currentmodule:: sktime.forecasting.tbats .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst TBATS +Croston +------- + +.. currentmodule:: sktime.forecasting.croston + +.. autosummary:: + :toctree: auto_generated/ + :template: class.rst + + Croston + Prophet ------- .. currentmodule:: sktime.forecasting.fbprophet .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst Prophet @@ -120,7 +133,7 @@ Composition .. currentmodule:: sktime.forecasting.compose .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst ColumnEnsembleForecaster @@ -139,7 +152,7 @@ Composition MultiplexForecaster .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: function.rst make_reduction @@ -150,7 +163,7 @@ Online Forecasting .. currentmodule:: sktime.forecasting.online_learning .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst OnlineEnsembleForecaster @@ -163,7 +176,7 @@ Model Selection .. currentmodule:: sktime.forecasting.model_selection .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst CutoffSplitter @@ -174,18 +187,18 @@ Model Selection ForecastingRandomizedSearchCV .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: function.rst temporal_train_test_split Model Evaluation (Backtesting) ----------------- +------------------------------ .. currentmodule:: sktime.forecasting.model_evaluation .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: function.rst evaluate diff --git a/docs/source/api_reference/performance_metrics.rst b/docs/source/api_reference/performance_metrics.rst index 763755740bf..3d6858bb0b6 100644 --- a/docs/source/api_reference/performance_metrics.rst +++ b/docs/source/api_reference/performance_metrics.rst @@ -1,23 +1,25 @@ .. _performance_metric_ref: -sktime.performance_metrics: Measuring time series model performance -======================================================= +Performance metrics +=================== The :mod:`sktime.performance_metrics` module contains metrics for evaluating and tuning time series models. .. automodule:: sktime.performance_metrics :no-members: + :no-inherited-members: Forecasting ----------- .. currentmodule:: sktime.performance_metrics.forecasting -Tunable Classes -*************** +Classes +~~~~~~~ + .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class_with_call.rst MeanAbsoluteScaledError @@ -40,9 +42,10 @@ Tunable Classes RelativeLoss Functions -********* +~~~~~~~~~ + .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: function.rst make_forecasting_scorer diff --git a/docs/source/api_reference/regression.rst b/docs/source/api_reference/regression.rst index 197e20f05aa..de1d8e51e88 100644 --- a/docs/source/api_reference/regression.rst +++ b/docs/source/api_reference/regression.rst @@ -1,12 +1,13 @@ .. _regression_ref: -sktime.regression: Time series regression -========================================= +Time series regression +====================== The :mod:`sktime.regression` module contains algorithms and composition tools for time series regression. .. automodule:: sktime.regression :no-members: + :no-inherited-members: Composition ----------- @@ -14,7 +15,7 @@ Composition .. currentmodule:: sktime.regression.compose .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst ComposableTimeSeriesForestRegressor @@ -25,7 +26,7 @@ Interval-based .. currentmodule:: sktime.regression.interval_based .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst TimeSeriesForestRegressor diff --git a/docs/source/api_reference/series_as_features.rst b/docs/source/api_reference/series_as_features.rst index ab5d1d9bffb..09cec5aefd1 100644 --- a/docs/source/api_reference/series_as_features.rst +++ b/docs/source/api_reference/series_as_features.rst @@ -1,13 +1,14 @@ .. _series_as_features_ref: -sktime.series_as_features: Series-as-features tools -=================================================== +Series-as-features tools +======================== The :mod:`sktime.series_as_features` module contains algorithms and composition tools that are shared by the classification and regression modules. .. automodule:: sktime.series_as_features :no-members: + :no-inherited-members: Composition ----------- @@ -15,7 +16,7 @@ Composition .. currentmodule:: sktime.series_as_features.compose .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst FeatureUnion @@ -26,7 +27,7 @@ Model selection .. currentmodule:: sktime.series_as_features.model_selection .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst PresplitFilesCV diff --git a/docs/source/api_reference/transformations.rst b/docs/source/api_reference/transformations.rst index 622ca22880c..36b13acbe94 100644 --- a/docs/source/api_reference/transformations.rst +++ b/docs/source/api_reference/transformations.rst @@ -1,14 +1,14 @@ .. _transformations_ref: -sktime.transformations: Time series transformers -============================================= +Time series transformations +=========================== The :mod:`sktime.transformations` module contains classes for data transformations. .. automodule:: sktime.transformations - :members: - :inherited-members: + :no-members: + :no-inherited-members: Panel transformers ------------------ @@ -19,7 +19,7 @@ Dictionary-based .. currentmodule:: sktime.transformations.panel.dictionary_based .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst PAA @@ -32,7 +32,7 @@ Summarize .. currentmodule:: sktime.transformations.panel.summarize .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst DerivativeSlopeTransformer @@ -46,7 +46,7 @@ tsfresh .. currentmodule:: sktime.transformations.panel.tsfresh .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst TSFreshRelevantFeatureExtractor @@ -58,7 +58,7 @@ Catch22 .. currentmodule:: sktime.transformations.panel.catch22 .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst Catch22 @@ -69,7 +69,7 @@ Compose .. currentmodule:: sktime.transformations.panel.compose .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst ColumnTransformer @@ -78,7 +78,7 @@ Compose SeriesToPrimitivesRowTransformer .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: function.rst make_row_transformer @@ -89,7 +89,7 @@ Matrix profile .. currentmodule:: sktime.transformations.panel.matrix_profile .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst MatrixProfile @@ -100,7 +100,7 @@ PCA .. currentmodule:: sktime.transformations.panel.pca .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst PCATransformer @@ -111,7 +111,7 @@ Reduce .. currentmodule:: sktime.transformations.panel.reduce .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst Tabularizer @@ -122,7 +122,7 @@ Rocket .. currentmodule:: sktime.transformations.panel.rocket .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst Rocket @@ -135,7 +135,7 @@ Segment .. currentmodule:: sktime.transformations.panel.segment .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst IntervalSegmenter @@ -147,7 +147,7 @@ Shapelet .. currentmodule:: sktime.transformations.panel.shapelets .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst ShapeletTransform @@ -159,7 +159,7 @@ Signature .. currentmodule:: sktime.transformations.panel.signature_based .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst SignatureTransformer @@ -173,7 +173,7 @@ Detrend .. currentmodule:: sktime.transformations.series.detrend .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst Detrender @@ -186,7 +186,7 @@ Adapt .. currentmodule:: sktime.transformations.series.adapt .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst TabularToSeriesAdaptor @@ -197,7 +197,7 @@ Box-cox .. currentmodule:: sktime.transformations.series.boxcox .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst BoxCoxTransformer @@ -209,7 +209,7 @@ Auto-correlation .. currentmodule:: sktime.transformations.series.acf .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst AutoCorrelationTransformer @@ -221,7 +221,7 @@ Cosine .. currentmodule:: sktime.transformations.series.cos .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst CosineTransformer @@ -232,7 +232,7 @@ Exponent .. currentmodule:: sktime.transformations.series.exponent .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst ExponentTransformer @@ -244,40 +244,40 @@ Matrix Profile .. currentmodule:: sktime.transformations.series.matrix_profile .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst MatrixProfileTransformer Missing value imputation -~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~s .. currentmodule:: sktime.transformations.series.impute .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst Imputer Outlier detection -~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~ .. currentmodule:: sktime.transformations.series.outlier_detection .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst HampelFilter Composition -~~~~~~~~~~~~~~ +~~~~~~~~~~~ .. currentmodule:: sktime.transformations.series.compose .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst OptionalPassthrough @@ -289,7 +289,7 @@ Theta .. currentmodule:: sktime.transformations.series.theta .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: class.rst ThetaLinesTransformer diff --git a/docs/source/api_reference/utils.rst b/docs/source/api_reference/utils.rst index ee1e29972a0..0bce14c70c7 100644 --- a/docs/source/api_reference/utils.rst +++ b/docs/source/api_reference/utils.rst @@ -1,17 +1,21 @@ .. _utils_ref: -sktime.utils: Utility function -============================== +Utility functions +================= The :mod:`sktime.utils` module contains utility functions. +.. automodule:: sktime.utils + :no-members: + :no-inherited-members: + Plotting -------- .. currentmodule:: sktime.utils.plotting .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: function.rst plot_series @@ -23,7 +27,7 @@ Data Processing .. currentmodule:: sktime.datatypes._panel._convert .. autosummary:: - :toctree: modules/auto_generated/ + :toctree: auto_generated/ :template: function.rst are_columns_nested diff --git a/docs/source/conf.py b/docs/source/conf.py index 176595d14cd..d501a8a8683 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -25,7 +25,7 @@ # -- Project information ----------------------------------------------------- project = "sktime" -copyright = "2019 - 2020 (BSD-3-Clause License)" +copyright = "2019 - 2021 (BSD-3-Clause License)" author = "sktime developers" # The full version, including alpha/beta/rc tags @@ -46,18 +46,19 @@ extensions = [ "sphinx.ext.autodoc", "sphinx.ext.autosummary", + "numpydoc", "sphinx.ext.intersphinx", - "sphinx.ext.autosectionlabel", - "sphinx.ext.todo", - "sphinx.ext.mathjax", - # 'sphinx.ext.viewcode', # link to auto-generated source code files (rst) - "sphinx.ext.githubpages", "sphinx.ext.linkcode", # link to GitHub source code via linkcode_resolve() - "sphinx.ext.napoleon", "nbsphinx", # integrates example notebooks - "m2r2", # markdown rendering + "sphinx_gallery.load_style", + "myst_parser", + "sphinx_panels", + "sphinx_issues", ] +# Use bootstrap CSS from theme. +panels_add_bootstrap_css = False + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -81,22 +82,53 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ["_build", ".ipynb_checkpoints", "Thumbs.db", ".DS_Store"] +exclude_patterns = [ + "_build", + ".ipynb_checkpoints", + "Thumbs.db", + ".DS_Store", +] + +add_module_names = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # see http://stackoverflow.com/q/12206334/562769 numpydoc_show_class_members = True +# this is needed for some reason... +# see https://github.com/numpy/numpydoc/issues/69 numpydoc_class_members_toctree = False +numpydoc_validation_checks = {"all"} + # generate autosummary even if no references autosummary_generate = True -autodoc_default_flags = ["members", "inherited-members"] + +# Members and inherited-members default to showing methods and attributes from a +# class or those inherited. +# Member-order orders the documentation in the order of how the members are defined in +# the source code. +autodoc_default_options = { + "members": True, + "inherited-members": True, + "member-order": "bysource", +} + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = False + +# When building HTML using the sphinx.ext.mathjax (enabled by default), +# Myst-Parser injects the tex2jax_ignore (MathJax v2) and mathjax_ignore (MathJax v3) +# classes in to the top-level section of each MyST document, and adds some default +# configuration. This ensures that MathJax processes only math, identified by the +# dollarmath and amsmath extensions, or specified in math directives. We here silence +# the corresponding warning that this override happens. +suppress_warnings = ["myst.mathjax"] def linkcode_resolve(domain, info): - """Return URL to source code correponding. + """Return URL to source code corresponding. Parameters ---------- @@ -139,18 +171,54 @@ def find_source(): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = "sphinx_rtd_theme" -# html_theme = 'bootstrap' +html_theme = "pydata_sphinx_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { - "prev_next_buttons_location": None, + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/alan-turing-institute/sktime", + "icon": "fab fa-github", + }, + { + "name": "Twitter", + "url": "https://twitter.com/sktime_toolbox", + "icon": "fab fa-twitter", + }, + { + "name": "Discord", + "url": "https://discord.com/invite/gqSab2K", + "icon": "fab fa-discord", + }, + ], + "favicons": [ + { + "rel": "icon", + "sizes": "16x16", + "href": "images/sktime-favicon.ico", + } + ], + "show_prev_next": False, + "use_edit_page_button": False, + "navbar_start": ["navbar-logo"], + "navbar_center": ["navbar-nav"], + "navbar_end": ["navbar-icon-links"], +} +html_logo = "images/sktime-logo-text-horizontal.png" +html_context = { + "github_user": "alan-turing-institute", + "github_repo": "sktime", + "github_version": "main", + "doc_path": "docs/source/", } - html_favicon = "images/sktime-favicon.ico" +html_sidebars = { + "**": ["search-field.html", "sidebar-nav-bs.html", "sidebar-ethical-ads.html"] +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -322,32 +390,40 @@ def adds(pth): nbsphinx_timeout = 600 # seconds, set to -1 to disable timeout # add Binder launch buttom at the top -CURRENT_FILE = "{{ env.doc2path( env.docname, base=None) }}" +current_file = "{{ env.doc2path( env.docname, base=None) }}" # make sure Binder points to latest stable release, not main -BINDER_URL = f"https://mybinder.org/v2/gh/alan-turing-institute/sktime/{CURRENT_VERSION}?filepath={CURRENT_FILE}" # noqa +binder_url = f"https://mybinder.org/v2/gh/alan-turing-institute/sktime/{CURRENT_VERSION}?filepath={current_file}" # noqa nbsphinx_prolog = f""" .. |binder| image:: https://mybinder.org/badge_logo.svg -.. _Binder: {BINDER_URL} +.. _Binder: {binder_url} |Binder|_ """ # add link to original notebook at the bottom -NOTEBOOK_URL = f"https://github.com/alan-turing-institute/sktime/tree/{CURRENT_VERSION}/{CURRENT_FILE}" # noqa +notebook_url = f"https://github.com/alan-turing-institute/sktime/tree/{CURRENT_VERSION}/{current_file}" # noqa nbsphinx_epilog = f""" ---- -Generated by nbsphinx_. The Jupyter notebook can be found here_. +Generated using nbsphinx_. The Jupyter notebook can be found here_. -.. _here: {NOTEBOOK_URL} +.. _here: {notebook_url} .. _nbsphinx: https://nbsphinx.readthedocs.io/ """ # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {"https://docs.python.org/": None} +intersphinx_mapping = { + "python": ("https://docs.python.org/{.major}".format(sys.version_info), None), + "numpy": ("https://docs.scipy.org/doc/numpy/", None), + "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), + "matplotlib": ("https://matplotlib.org/", None), + "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), + "joblib": ("https://joblib.readthedocs.io/en/latest/", None), + "scikit-learn": ("https://scikit-learn.org/stable/", None), +} # -- Options for _todo extension ---------------------------------------------- todo_include_todos = False diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst deleted file mode 100644 index 3fb74dd66b5..00000000000 --- a/docs/source/contributing.rst +++ /dev/null @@ -1,3 +0,0 @@ -.. _contributing: - -.. mdinclude:: ../../CONTRIBUTING.md diff --git a/docs/source/contributors.rst b/docs/source/contributors.rst deleted file mode 100644 index 9c8e0e8a1e8..00000000000 --- a/docs/source/contributors.rst +++ /dev/null @@ -1,3 +0,0 @@ -.. _contributors: - -.. mdinclude:: ../../CONTRIBUTORS.md diff --git a/docs/source/developer_guide.rst b/docs/source/developer_guide.rst index 47be4aacd49..361fdf418f8 100644 --- a/docs/source/developer_guide.rst +++ b/docs/source/developer_guide.rst @@ -17,8 +17,9 @@ Welcome to sktime's developer guide! .. toctree:: :maxdepth: 1 + :glob: developer_guide/introduction - developer_guide/forecasting - developer_guide/classification developer_guide/add_dataset + developer_guide/add_estimators + developer_guide/documentation diff --git a/docs/source/developer_guide/add_dataset.rst b/docs/source/developer_guide/add_dataset.rst index 5582da7f6e9..6fc0844604f 100644 --- a/docs/source/developer_guide/add_dataset.rst +++ b/docs/source/developer_guide/add_dataset.rst @@ -1,13 +1,13 @@ -.. _developer_guide_forecasting: +.. _developer_guide_add_datset: + +==================== +Adding a New Dataset +==================== -Adding Datasets to sktime -========================= Follow these steps to add a new dataset to sktime: -* Include CSV file or supported other format under :code:`sktime/datasets/data/` +* Include CSV file or other supported format under :code:`sktime/datasets/data/` * Add :code:`load_(...)` function in file :code:`sktime/datasets/base.py` -* Add :code:`` to the list :code:`__all__ = [...` in file :code:`sktime/datasets/__init__.py` +* Add :code:`` to the list :code:`__all__ = [...]` in file :code:`sktime/datasets/__init__.py` * Add :code:`` as argument to method :code:`included_datasets = (...` in file :code:`sktime/sktime/datasets/setup.py` * Add :code:`` to the list of included problems in file :code:`sktime/sktime/datasets/setup.py` - -Thank you for your contribution! diff --git a/docs/source/developer_guide/add_estimators.rst b/docs/source/developer_guide/add_estimators.rst new file mode 100644 index 00000000000..04d3ef9bb89 --- /dev/null +++ b/docs/source/developer_guide/add_estimators.rst @@ -0,0 +1,14 @@ +.. _developer_guide_add_estimators: + +====================== +Adding a New Estimator +====================== + +Please use the extension templates below to implement new estimators. In addition to following the templates, +please ensure that code also meets ``sktime's`` :ref:`documentation ` standards. + +Forecasting +=========== + +.. literalinclude:: ../../../extension_templates/forecasting.py + :language: python diff --git a/docs/source/developer_guide/classification.rst b/docs/source/developer_guide/classification.rst deleted file mode 100644 index dd03b1ea716..00000000000 --- a/docs/source/developer_guide/classification.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. _developer_guide_classification: - -Time Series Classification -========================== - -.. note:: - - The developer guide is under development. We have created a basic - structure and are looking for contributions to develop the guide - further. For more details, please go to issue `#464 `_ on GitHub. diff --git a/docs/source/developer_guide/documentation.rst b/docs/source/developer_guide/documentation.rst new file mode 100644 index 00000000000..953097c6a00 --- /dev/null +++ b/docs/source/developer_guide/documentation.rst @@ -0,0 +1,19 @@ +.. _developer_guide_documentation: + +============= +Documentation +============= + +Providing instructive documentation is a key part of ``sktime's`` mission. In order to meet this, +developers are expected to follow ``sktime's`` documentation standards. + +These include: + +* Documenting code using NumPy docstrings +* Following ``sktime's`` docstring convention for public code artifacts and modules +* Adding new public functionality to the :ref:`api_refernce` and :ref:`user guide ` + +More detailed information on ``sktime's`` documentation format is provided below. + +Docstring Conventions +===================== diff --git a/docs/source/developer_guide/forecasting.rst b/docs/source/developer_guide/forecasting.rst deleted file mode 100644 index 33049f8e42b..00000000000 --- a/docs/source/developer_guide/forecasting.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. _developer_guide_forecasting: - -Forecasting -=========== - -.. note:: - - The developer guide is under development. We have created a basic - structure and are looking for contributions to develop the guide - further. For more details, please go to issue `#464 `_ on GitHub. diff --git a/docs/source/developers.rst b/docs/source/developers.rst new file mode 100644 index 00000000000..30d2ab57e67 --- /dev/null +++ b/docs/source/developers.rst @@ -0,0 +1,106 @@ +.. _developers: + +Development +=========== + +.. note:: + + If you are new to sktime, it may be helpful to take a look at the + :ref:`get_involved` page first. + +.. toctree:: + :maxdepth: 1 + :hidden: + + developer_guide + reviewer_guide + enhancement_proposals + roadmap + code_of_conduct + governance + +.. panels:: + :card: + intro-card text-center + + --- + + Developer Guide + ^^^^^^^^^^^^^^^ + + Learn our development conventions. + + +++ + + .. link-button:: developer_guide + :type: ref + :text: Developer Guide + :classes: btn-block btn-secondary stretched-link + + --- + + Reviewer Guide + ^^^^^^^^^^^^^^ + + How we review contributions. + + +++ + + .. link-button:: reviewer_guide + :type: ref + :text: Reviewer Guide + :classes: btn-block btn-secondary stretched-link + + --- + + Enhancement Proposals + ^^^^^^^^^^^^^^^^^^^^^ + + Thought of a project enhancement? See when and how to submit a proposal. + + +++ + + .. link-button:: enhancement_proposals + :type: ref + :text: Enhancement Proposals + :classes: btn-block btn-secondary stretched-link + + --- + + Roadmap + ^^^^^^^ + + What's on the development horizon? + + +++ + + .. link-button:: roadmap + :type: ref + :text: Roadmap + :classes: btn-block btn-secondary stretched-link + + --- + + Code of Conduct + ^^^^^^^^^^^^^^^ + + + +++ + + .. link-button:: code_of_conduct + :type: ref + :text: Code of Conduct + :classes: btn-block btn-secondary stretched-link + + --- + + Governance + ^^^^^^^^^^ + + How ``sktime`` is run. + + +++ + + .. link-button:: governance + :type: ref + :text: Governance + :classes: btn-block btn-secondary stretched-link diff --git a/docs/source/enhancement_proposals.rst b/docs/source/enhancement_proposals.rst new file mode 100644 index 00000000000..a2052b319af --- /dev/null +++ b/docs/source/enhancement_proposals.rst @@ -0,0 +1,6 @@ +.. _enhancement_proposals: + +Enhancement Proposals +===================== + +Please visit our GitHub repository for `sktime enhancement proposals `_. diff --git a/docs/source/estimator_overview.md b/docs/source/estimator_overview.md new file mode 100644 index 00000000000..0dd4f557a4b --- /dev/null +++ b/docs/source/estimator_overview.md @@ -0,0 +1,11 @@ +# Estimator Overview + +The table below gives an overview of all estimators in sktime. + +

+ +
+

+ +```{include} estimator_overview_table.md +``` diff --git a/docs/source/estimator_overview.rst b/docs/source/estimator_overview.rst deleted file mode 100644 index 794c8f87c44..00000000000 --- a/docs/source/estimator_overview.rst +++ /dev/null @@ -1,14 +0,0 @@ -.. _estimator_overview: - -================== -Estimator Overview -================== - -The table below gives an overview of all estimators in sktime. - -.. raw:: html - - -
- -.. mdinclude:: estimator_overview_table.md diff --git a/docs/source/get_involved.rst b/docs/source/get_involved.rst new file mode 100644 index 00000000000..09796dc87d5 --- /dev/null +++ b/docs/source/get_involved.rst @@ -0,0 +1,73 @@ +.. _get_involved: + +Get Involved +============ + +.. toctree:: + :maxdepth: 1 + :hidden: + + get_involved/contributing + get_involved/mentoring + get_involved/meetups + +sktime is a community-driven project and your help is extremely welcome. +If you get stuck, please don’t hesitate to chat with us or raise an issue. + +.. panels:: + :card: + intro-card text-center + + --- + + Contributing + ^^^^^^^^^^^^ + + New to sktime? Check out the contributing guide. + + +++ + + .. link-button:: contributing + :type: ref + :text: Contributing guide + :classes: btn-block btn-secondary stretched-link + + --- + + Mentoring + ^^^^^^^^^ + + New to open source? Apply to our mentoring program! + + +++ + + .. link-button:: mentoring + :type: ref + :text: Mentoring + :classes: btn-block btn-secondary stretched-link + + --- + + Sponsoring + ^^^^^^^^^^ + + Fund sktime maintenance and development. + + +++ + + .. link-button:: https://opencollective.com/sktime + :text: Donate + :classes: btn-block btn-secondary stretched-link + + --- + + Meet-ups + ^^^^^^^^ + + Join our discussions, tutorials, workshops and sprints! + + +++ + + .. link-button:: meetups + :type: ref + :text: Participate + :classes: btn-block btn-secondary stretched-link diff --git a/docs/source/get_involved/contributing.md b/docs/source/get_involved/contributing.md new file mode 100644 index 00000000000..004f419c741 --- /dev/null +++ b/docs/source/get_involved/contributing.md @@ -0,0 +1,2 @@ +```{include} ../../../CONTRIBUTING.md +``` diff --git a/docs/source/get_involved/meetups.rst b/docs/source/get_involved/meetups.rst new file mode 100644 index 00000000000..dd6dfb11bc5 --- /dev/null +++ b/docs/source/get_involved/meetups.rst @@ -0,0 +1,13 @@ +.. _meetups: + +Community meetups +================= + +Join our discussions, tutorials, workshops and sprints! + +Most of our meetups take place on Discord. You can join sktime's community +server `here `_. + +.. raw:: html + + diff --git a/docs/source/mentoring.rst b/docs/source/get_involved/mentoring.rst similarity index 100% rename from docs/source/mentoring.rst rename to docs/source/get_involved/mentoring.rst diff --git a/docs/source/get_started.rst b/docs/source/get_started.rst new file mode 100644 index 00000000000..7fc0934f2e6 --- /dev/null +++ b/docs/source/get_started.rst @@ -0,0 +1,181 @@ +.. _get_started: + +=========== +Get Started +=========== + +The following information is designed to get users up and running with ``sktime`` quickly. For more detailed information, see the links in each of the subsections. + +Installation +------------ + +``sktime`` currently supports: + +* environments with python version 3.6, 3.7, or 3.8. +* operating systems Mac OS X, Unix-like OS, Windows 8.1 and higher +* installation via ``PyPi`` or ``conda`` + +To install ``sktime`` with its core dependencies via ``pip`` use: + +.. code-block:: bash + + pip install sktime + +To install ``sktime`` via ``pip`` with maximum dependencies, including soft dependencies, install using the `all_extras` modifier: + +.. code-block:: bash + + pip install sktime[all_extras] + + +To install ``sktime`` with its core dependencies via ``conda`` from ``conda-forge`` use: + +.. code-block:: bash + + conda install -c conda-forge sktime + +To install ``sktime`` via ``conda`` with maximum dependencies, including soft dependencies, install using the `all-extras` conda recipe: + +.. code-block:: bash + + conda install -c conda-forge sktime-all-extras + +Key Concepts +------------ + +``sktime`` seeks to provide a unified framework for multiple time series machine learning tasks. This (hopefully) makes ``sktime's`` functionality intuitive for users +and lets developers extend the framework more easily. But time series data and the related scientific use cases each can take multiple forms. +Therefore, a key set of common concepts and terminology is important. + +Data Types +~~~~~~~~~~ + +``sktime`` is designed for time series machine learning. Time series data refers to data where the variables are ordered over time or +an index indicating the position of an observation in the sequence of values. + +In ``sktime`` time series data can refer to data that is univariate, multivariate or panel, with the difference relating to the number and interrelation +between time series :term:`variables `, as well as the number of :term:`instances ` for which each variable is observed. + +- :term:`Univariate time series` data refers to data where a single :term:`variable` is tracked over time. +- :term:`Multivariate time series` data refers to data where multiple :term:`variables ` are tracked over time for the same :term:`instance`. For example, multiple quarterly economic indicators for a country or multiple sensor readings from the same machine. +- :term:`Panel time series` data refers to data where the variables (univariate or multivariate) are tracked for multiple :term:`instances `. For example, multiple quarterly economic indicators for several countries or multiple sensor readings for multiple machines. + +Learning Tasks +~~~~~~~~~~~~~~ + +``sktime's`` functionality for each learning tasks is centered around providing a set of code artifacts that match a common interface to a given +scientific purpose (i.e. :term:`scientific type` or :term:`scitype`). For example, ``sktime`` includes a common interface for "forecaster" classes designed to predict future values +of a time series. + +``sktime's`` interface currently supports: + +- :term:`Time series classification` where the time series data for a given instance are used to predict a categorical target class. +- :term:`Time series regression` where the time series data for a given instance are used to predict a continuous target value. +- :term:`Time series clustering` where the goal is to discover groups consisting of instances with similar time series. +- :term:`Forecasting` where the goal is to predict future values of the input series. +- :term:`Time series annotation` which is focused on outlier detection, anomaly detection, change point detection and segmentation. + +Reduction +~~~~~~~~~ + +While the list above presents each learning task separately, in many cases it is possible to adapt one learning task to help solve another related learning task. For example, +one approach to forecasting would be to use a regression model that explicitly accounts for the data's time dimension. However, another approach is to reduce the forecasting problem +to cross-sectional regression, where the input data are tabularized and lags of the data are treated as independent features in `scikit-learn` style +tabular regression algorithms. Likewise one approach to the time series annotation task like anomaly detection is to reduce the problem to using forecaster to predict future values and flag +observations that are too far from these predictions as anomalies. ``sktime`` typically incorporates these type of :term:`reductions ` through the use of composable classes that +let users adapt one learning task to solve another related one. + +For more information on ``sktime's`` terminology and functionality see the :ref:`glossary` and the :ref:`user guide `. + +Quickstart +---------- +The code snippets below are designed to introduce ``sktime's`` functionality so you can start using its functionality quickly. For more detailed information see the :ref:`tutorials`, :ref:`user_guide` and :ref:`api_reference` in ``sktime's`` :ref:`user_documentation`. + +Forecasting +~~~~~~~~~~~ + +.. code-block:: python + + from sktime.datasets import load_airline + from sktime.forecasting.base import ForecastingHorizon + from sktime.forecasting.model_selection import temporal_train_test_split + from sktime.forecasting.theta import ThetaForecaster + from sktime.performance_metrics.forecasting import mean_absolute_percentage_error + + y = load_airline() + y_train, y_test = temporal_train_test_split(y) + fh = ForecastingHorizon(y_test.index, is_relative=False) + forecaster = ThetaForecaster(sp=12) # monthly seasonal periodicity + forecaster.fit(y_train) + y_pred = forecaster.predict(fh) + mean_absolute_percentage_error(y_test, y_pred) + >>> 0.08661467738190656 + +Time Series Classification +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from sktime.classification.interval_based import TimeSeriesForestClassifier + from sktime.datasets import load_arrow_head + from sklearn.model_selection import train_test_split + from sklearn.metrics import accuracy_score + + X, y = load_arrow_head(return_X_y=True) + X_train, X_test, y_train, y_test = train_test_split(X, y) + classifier = TimeSeriesForestClassifier() + classifier.fit(X_train, y_train) + y_pred = classifier.predict(X_test) + accuracy_score(y_test, y_pred) + >>> 0.8679245283018868 + +Time Series Regression +~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + The time series regression API is stable. But the inclusion of a dataset to illustrate + its features is still in progress. + +.. code-block:: python + from sktime.regression.compose import ComposableTimeSeriesForestRegressor + +Time Series Clustering +~~~~~~~~~~~~~~~~~~~~~~ + +.. warning:: + + The time series clustering API is still experimental. Features may change + in future releases. + +.. code-block:: python + + from sklearn.model_selection import train_test_split + from sktime.clustering import TimeSeriesKMeans + from sktime.clustering.evaluation._plot_clustering import plot_cluster_algorithm + from sktime.datasets import load_arrow_head + + X, y = load_arrow_head(return_X_y=True) + X_train, X_test, y_train, y_test = train_test_split(X, y) + + k_means = TimeSeriesKMeans(n_clusters=5, init_algorithm="forgy", metric="dtw") + k_means.fit(X_train) + plot_cluster_algorithm(k_means, X_test, k_means.n_clusters) + +Time Series Annotation +~~~~~~~~~~~~~~~~~~~~~~ + +.. warning:: + + The time series annotation API is still experimental. Features may change + in future releases. + +.. code-block:: python + + from sktime.annotation.adapters import PyODAnnotator + from pyod.models.iforest import IForest + from sktime.datasets import load_airline + y = load_airline() + pyod_model = IForest() + pyod_sktime_annotator = PyODAnnotator(pyod_model) + pyod_sktime_annotator.fit(y) + annotated_series = pyod_sktime_annotator.predict(y) diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst new file mode 100644 index 00000000000..e310b14bda7 --- /dev/null +++ b/docs/source/glossary.rst @@ -0,0 +1,80 @@ +.. _glossary: + +Glossary of Common Terms +======================== + +The glossary below defines common terms and API elements used throughout +sktime. + +.. note:: + + The glossary is under development. Important terms are still missing. + Please create a pull request if you want to add one. + + +.. glossary:: + :sorted: + + Scitype + See :term:`scientific type`. + + Scientific type + A class or object type to denote a category of objects defined by a + common interface and data scientific purpose. For example, "forecaster" + or "classifier". + + Forecasting + A learning task focused on prediction future values of a time series. For more details, see the :ref:`user_guide_forecasting`. + + Time series + Data where the :term:`variable` measurements are ordered over time or an index indicating the position of an observation in the sequence of values. + + Time series classification + A learning task focused on using the patterns across instances between the time series and a categorical target variable. + + Time series regression + A learning task focused on using the patterns across instances between the time series and a continuous target variable. + + Time series clustering + A learning task focused on discovering groups consisting of instances with similar time series. + + Time series annotation + A learning task focused on labeling the timepoints of a time series. This includes the related tasks of outlier detection, anomaly detection, change point detection and segmentation. + + Panel time series + A form of time series data where the same time series are observed observed for multiple observational units. The observed series may consist of :term:`univariate time series` or + :term:`multivariate time series`. Accordingly, the data varies across time, observational unit and series (i.e. variables). + + Univariate time series + A single time series. While univariate analysis often only uses information contained in the series itself, + univariate time series regression and forecasting can also include :term:`exogenous` data. + + Multivariate time series + Multiple time series. Typically observed for the same observational unit. Multivariate time series + is typically used to refer to cases where the series evolve together over time. This is related, but different than the cases where + a :term:`univariate time series` is dependent on :term:`exogenous` data. + + Endogenous + Within a learning task endogenous variables are determined by exogenous variables or past timepoints of the variable itself. Also referred to + as the dependent variable or target. + + Exogenous + Within a learning task exogenous variables are external factors whose pattern of impact on tasks' endogenous variables must be learned. + Also referred to as independent variables or features. + + Reduction + Reduction refers to decomposing a given learning task into simpler tasks that can be composed to create a solution to the original task. + In ``sktime`` reduction is used to allow one learning task to be adapted as a solution for an alternative task. + + Variable + Refers to some measurement of in terest. Variables may be cross-sectional (e.g. time-invarient measurements like a patient's place of birth) or + :term:`time series`. + + Timepoint + The point in time that an observation is made. A timee point may represent an exact point in time (a timestamp), + a timeperiod (e.g. minutes, hours or days), or simply an index indicating the position of an observation in the sequence of values. + + Instance + A member of the set of entities being studied and which an ML practitioner wishes to generalize. For example, + patients, chemical process runs, machines, countries, etc. May also be referred to as samples, examples, observations or records + depending on the discipline and context. diff --git a/docs/source/images/sktime-logo-text-horizontal.png b/docs/source/images/sktime-logo-text-horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..070bb31a76e3b47dd1f5b559771c1571cf08a30b GIT binary patch literal 141419 zcma%j1z1(z(l;O>AR&kdNOvO&NOyO4OLuoEjexXtcgLX{>Fx#r$wS8>4)GoQ-}}Ay zdtULo`1r8dd&R7oH8a1NHG7B1$%vvL;UYmnL7|9?2`NB9Awd2^w>^i4+*xmoJBIwB zZzd=xXCWvmXk%$(uV|}pXe4CjU~8%GCifl+iYm%bPftOdj;c>zUr(=Zn3fvJ-bEoe z_^X1RPhUH#fAb527f2!f-Ea}m&^&zPFPLz#JezvjpJy=wKM!$`vjD^aI_)Ijh64%#|lX#I1_4~YOMLzpu1XR$dd zvA(9;x1R^HhR(lYz=`Tn<D`H55FP?55I=Y z$35O{)+3_lCPF|74?pAa@Zh5YVO081`>{*s!$TYP-NS>+JGnD__`qjwP*}u9hb+D0 zU0AlJ{KV4yLeo6(!DPVv`woIa#^~WECRM_Ud?Mi&8%||{~6@p;|Li! z7}}fJI-1#76FrWrZ(!r($W21>IMM(8`G-y;SF`__$=cyx%YrPB@$m`cI|e4kKXr37 zGk&7mPViJZRYTWM3)Q^QM`-kBBtE-?RddF z#+N+_PH(&fePDWn(N5&}OxMo3NV9HlN_86L{;+MdP)WXD4BZzBmIw;^IUf|vUoPcH ztPb?MI zU-F+F*scVNG(e{&{}A;TlJwZo3p7zvb0Ytlz;74Bw(tWv;6t>O;ubc$_QT}(UHnMo z{4jp<{@#qTimoC^GpFj5+D9QWRYfZ%Ng-b1TDx4qBQ@qPb7a^f?r4{_4U_Z#OET!s z{o6u?X~nj`=1jzLyYAJuTxOPQ*tq||)zCbMSGL&ijBs*R-8OD2D(zyhRM)3x0pvp$ zMSO;xQ<77P!?(y=%iwKl{1V}w^xEWU#?VAV3789`B8&WQeWhAye7av_hxLwZr8$$nVFV>0RN!0EuEw7RfcB^J5(Ud&Yh zt@mOt8ul-mfF%V*4ET(IF!`GqdX>-XHES~x0IS$)^3S+7D@b-sh<&va#W6g>fu7HgP#Ps zt%h-l67qIWna?4Q<&ozxt%{WY#LtMm(8A}gb2_{z{UKCwuSMe?%=p~^PPIjq{blC5 zCcF!7X;xONdM+i0H2~>h6-JwxN$N7jm*!pJ3Y+<1cJO~$EV2khF*@WX(I4l}75sUA zy1+nmpZgk{K4n%<(B>+7eF#6ld?agzZ-+ zUfwnP@ZSBp+&^;RE&kT(luHmeGGeYOBH^Uj(LK>j0?jse)1$(vYp~ZeV_mD;u&-fZ zV~R0bz$LI(qIdSVj6bi|kDd6ZO_2D%6a?p`E!vbNvCT#6VLtK~hFk`G{AnI1)BL;1 zR;#FB4r2eKuS{`R&L7_q@%Ft}wl3pmI>5!NDl`02n+*(ZvDgT3j`3HLEqr59uZQsG z`F`I;meH%NM8aN@!~Ja}{RW6CD7h`$D(`|ojYmEmtusz?q3N+5YNRDEkWH+Ge?8!E z*EyKB^W5GFQuxRlU8#7xY&oTScLJ$bZxt>)=6-6dn$zP6?Ouh@n7B8tru3Fb#A6&0dRLWE2mM$IEyK$1#d{Y;Tji-j?1D5W(w+U)^E?7=+FK!!DJ`C=iDcW$# zu%lfA3yZFVDXN2>6nVwebD|suz{_0TM*$d%5l!qXgpT1AhAOiYJ6vC%k;nLZj3uHL0^ zySgR_O9|VWonJ_9)bNp*Pic(z)Et)NGrwRYbw}kYV5IiGu)p#z$-y_5My!>gf-#vfs<8UOq&n5jQLUZtN#?7WqjUK$^qG|Kj|RI zV-zOP@{t0`INzXSWg=ZCS%yfAm&+%1Wa-uAX&*oNc(~Sii4qiSV7&ffVoZ`DFseOs0&zaIdleJ43^btFk&{VI%V09Ym}J3xAqnQAUnPkc+*?k zgfBdQQOw9?AnQV?kdF5$--O}YyUpRg2umJoS71SmV?h|XsP3e`^ZUy%NGt$eA22Onpubb^2u#tkA;a1%y%~x^u z32-WO-9Y?<*!Ii=vmzJXLaYom+K1H1(lcq=J3VG-PwsRm*b$OnS++ymzjH_J8T6bD7bc$qC9PEU^JWVs#75B4~Tk0#v< zL;2OGqgJ5BlXs08V_tbgAmX?Vbvb0y7k}X&282RH z#E3_+#xwb)I5x-<>8D{tN@Tc%+B9@9BIeFt3X>fPR;t98n&%H@h$p7CSKVQwUgU ze98`vlwCIAy`H)(VN9l0#{V!oaZ~udzyV%ZJ13l#`xUHCwJ2C-;mFFv6do|-Bd^Oo z)AH$GZt9PoMGgm6jTFG#15Nv=%|m#8l<@8cXu9r0huk%@_N}i?OK12{_6owB6aMXK>|F`Mo&cGFwKu6FU52Zrt9$b5_1`M3@5Hi^MN__20&#q$CCK$0>-j6*qZ+rlLf$HCb5Qk4FO z^xYOjYbrMoj3jS0v#u>%MYwI>_k3|FlcD*MhvVei`Lr>Oe*+0j$XPQS5g;<)Vk&#hufE^+gdy2TJ zPH3m){8GS?%&NohC{;+fGF{@V%TqggZ{NFt5!+2#P|V@#LrWGOCqS?IO&+Tg{{7R& zmF@$y0GQ!}-NF}$gRJm{)k`2?-AA(o z%eCEgu++_OZK`+T>CuJh`9*v zygHPQoU`G&{4!Q{eN&NU;E)y8z1&1SkFLVbT%6Let|0>a3)d(iT-#(nJjaDWhgb<# zK&`rTYU`mx>wVs!6BfyM!<(Y>>*dkKh&3MYdGU(gY^{?<%V`+Fx(YBciR**lJ2p-I z5N$dqS#@lQGn$xLuQnE{%JX*Dg^3My&M1Ofe9}8m*BZYh8k*YZTrfV~=o}wK>0xo8 zkkCaV4Rs+YY2~*)of@q7GI2kiYyO3oL=ZCJ{G}BiSMvHxI~IL$Np@kkh$uX5KlFQn zHJZX^#q>iC%Okfd3s4Z-lG)L!YbFcTxiz|^ksbef)?n@z!;Km84o4}2-KcY4s~EM$ z;qHm^4EzMKMvD(thKcVGd(@{>pGR@PldEJ|hHood3wdCNcno)2%VLo@i}qCf1?A8| z&^*JSe*Pr7Z~G~+T+N_LNev);*jZ{NQP|B1KD;sus{Xr%Nf6fhr3oQ8@L<8+0$tHX zlFW-@*#`XSvZC{p02e=Ajl;m~lFn&VdREvg!L^(sEze2g;lon$k~l!I|0d@qin=t4 zW8S&!FTt!{<|8Hl-fQlczjdk@B(Z8RA+!D`|tREwH zHDg$vy2i;)MiYVWi%18a@)8F7${cBto!9VdH7W#ot=BCQwGUdAjZh`te~pirA-GYc z=c69nBaC$;x1BCxnIOK1)9ojSJWRx8bs3&&KrSAnDQxTZoZ65e$Two4aD4rgDOVeT zV4Q@rok^GHe0uZfb7PGY8X6j}DC#=9#j(*bF=IBWaL6yX36Tp2^F|$CJvEFk-{vEB zT&nC~e)MY&B0mS>9D zRHWaQ^*bj_6z;6bs$S}_6}ic1Lvec2)>mINWx5>rx+SCSC{7cy!skzt)t-V5$sAyQ zRf2&y4oJ9877?ymeps{cN*9d9sc;yg87x8z&}y-#b+%=C(%q>=z`AC)z7Wx!hRWk|kPRTgFW;D%Vxs{SUwsEWYw>0L<24)L3k)?&NAgUd4-TSV`jbez_LWQsX-FjBstk%un-IYJ`Gog@UZ9^cc*EN{sWiMVREc*A zzG}gt;N(GM_GfuWra@46G@${fNjIh|(TqV9p`$jZeW@8YElewg4LNw$dZ97G;}CL3Mt4TRCi zU5P4>Ui34Lxdbjnw!x!$|vtAq{Hp7Nu%X)chbK@lB5Vi8`Par$vTZMa(TV6)1K z*XLHCi}56;g$(z~@0Zn$06>)U_v97NVMFa)S4*ud=#+s~MUwUmOXL_pk~hLJCK%tD z-ZmCU&i9{J+|5j)&H1=)@htK(e`=^OEs7+p+DnA$_j@uxWO9M1=5_=*%cEQiv|#u> z-)qi}85~H{=5}u&acOSI5li!GzIs&MVu%RR(WKSS}mY-vZPkUpeRRKXbM*H;w?*BgTYE` zy-kx9kXygaWf>n-hLT)bDAE=lnyPm9qnG=|NzO@nQMNpL!h)B^RSuQ^wxGxywQSw; zv)YW#*6=3Rq|wF0HE0jxidlEjn*;i7ZG&=a-TW-`^nNE?!Y4C!e9DiuaX`Pz^pRE7 zl*fUf5p??6ojrBx%z}71rS1?Rh21<(um{%OB@kqhuxile-hEMJ336A7&M#IcXLLu|q8kL;^;@2L8n6*xQ5TI?Z<>vICQhZ| z#sI(Wk%saFmFcc_N9!MG_J+5$&3zn~t!j;wgQg{6>8VCr1oGFu;pX`U?KsIo*xuZE zP9TwQW#irlLQAg#yUQ%xXDZIHMAsgk=vQm;Bw1wIW9w=A(Q8AmpsEJ1@o|Do;%IYN8pZB0?o+(@PlIMqVO~hu8*nHC zveL2WXtc=NZN5Fh)t>4rt`cF}6gLBK1WIq~lyAdjjsZ`$yGCq;E+ySEE3oAx282|R z+f>1OA`XHrcN3dzbLBP-{WCKx65y`_=kBIDXwyK449zuH7ZlL^7o<@S+l*dkbFaxe zvyi-bt;Bf)m6et%P7`kBJ86KE^X0Hjm2SgMQ9IKFpoL_6L-c8z>92J~6f@r$;hb`~oQ z{CLZa+fF{qPcOyLIV`-;4}XUV-Q={<4FapKngj9hEPM6ZymYg6-?=4O6Yc|2ccEz` zihbZN`V}Q)VF|`Jhb|FyOG{{=J%C97=T-b803ckNx%aFYg;DV7M6Mh+#PAiHlvF%I zrS_8N{<#(823}3zZ+FF_5A!nN;o?*F9nzgDS+w zAmv}7uEIkZNzPgyT!^Io%V~5XiXtSRew>x&ND*xn&r$^kn-lY7gr!mF1B$X)!t+8M zU25{tAc5Sa47$K=@(=E*YdCRsnKO#$sZQlOI>q!DDyk@nrLpx3ISwulQoA|@_5y}$ z<8aI5Bt>kv&9)megTwZmgXMRg%n?_W>i8cRr~zJvy&_H9PBwLC$*O&{jRojk&kL*9 z`vsps2)^xEFTZIbA^t`X@koADU(F?OHFDywiHif?^ERHDEjmvKPuXKhZ*P8H$l?n= zup6HYKOyIs#E=22d`GDfA@<;=u#+wUT_@#Xeg$pE-J%ywFq~8GI}$KwbMamX3khm^ z>!fk5mTzo#(x(P(Z`gYeb}xI+Zzc!9I5SRzpb?81iUu@UxCrWScwghGG$zf)^m|D* zDq1T`(Gc{Ur;edbW(oV)o;E3NqL*Nb( z`nO=3BVKG7UMJHaueEmP!NBw(v3`fj@>kZ*Wp)i>Z>Bn_X(Hf+>I`diR+5m;+$C0$ zH!9oTG|S$-<0dS!dpC^?{Fo_I?t}D>dE_*XecB_&rr@ox69Z}??(_z*_~!MGPgtN+ zH(vKQpDHJeYTQut|=lA+_c-nS<3U3Kh7DU`l(RK&qx$<3OA3!jt_B? z@yvciz2YScW#5VjeRl2ehBsw^4j3rg(#5nP5u2Yg{P_c1P)Lk*wiz;G=0^hp<2?pl;{OlIzdCgm%WwS<8&-KmKV^sxY)NQp`VH@D< z^q0+nen_?9A5wRXayJAO`_9}=?phOwvEa5mPTXI3pIuG0 zP=LD1*}6!;<uB3nnNba917kO}y(( z6gFWs>6d2{d#}Y(kwd>P&>$A^z%pd*G379|Y6FMd0r!OoXU%5$bKcl8UNIT-L~AvM zPBF{P`T^RN)&W-Y^_|ulvj*5D*0q6+8Wu`8XjWV?N^;AFkU$ngMpTE9zWcUYD z%f-Q-DqQeX9>IZ+Cod)6#4>Kafa`YHb1ZQSb2#T#^GH zb-Ae#+gtc!_+|mu!qmD#lNGQ~%(z=Y*pRbq{VjMQvv~pW&f%G-;vI)SQA?#9_WeX0 z7tzLX3(ZD{$(JOT*ZhpBgNx#BCEh*{zIS>)k=*tbs}U&@i&w54_A8(3jx-B;To_{n zaO$KfX(x@hEtbcyI4A^wgIpRdb#F^Ze{Jv zo*{Q(38D^%uA+tH~U zPlKsn+v~t%eQFxma&v!T&SoysDxwku+avyh+yh*t{Zp;`b$mK6iNWXx(4|)$eyu6&VOon=p;Vx_y1jV~C zBZ@j^y)SHgh4F#pjyAF1_f`r4joUE5XpGl}6g+$Z&8X}cCQfozwDueQb4 zgWCEMx{^1QsLHW#*xioZ3QLscQp``BUR7s)EmP)+RlY6>V_T@AIg;_wp`gvoAd;;w z;hNL>F)TsXj}^M+sf44m64|Pw7ruAZrd!>=;ysURVP&f?98N-YRqzdRR6BI7M#B|1 zJSTg1yy*O}))HTS5h@er`@RGe7nZP9BGc(MCp>yl%FGFIT2jPbUl6@*ihhln-#6tK z!S=-R1eL|qnuSl+X^orlI!wAb1Fws%0I<^%k}KIQE!oaTl9c=DX}tQQ_&8NVYAUOw zjcW#G$FDilvjf?iJdY?C%Nd4o55+q5GuK$`)9z0^y|MsCSBxM~pm7s8k8Q54 z<>x@|OcPwM_?IUv3Ir)&LULn1Jv9{>uq1x#W9{GXZWDBB5S-JI42;L<$;<ai={1P&0Tf3H6x-4>siAjR{Hqfu^$#$eXGdIQKZ z>_i}N-6~M36Hp6}rMo)*>8Qm!=I%BeZ~q46uwMhekTIs(=J2qx&Gj~zo1Afar~tWW z?;Egvt=j%x`VkF9*L5Ml0Y=Q=)ZyPBw;v;ucqo@NfY- zbpHUpKbTlwJ~+&RV!cHs6qsrv^pMw)1c!BZy+L$r4BgCBSOYHlT{a%irF8|z717ym zHTsXpvx0axW-*n%)@$v~=X~YeHsW?m<9$OAQTC-*+y1~Pr%X)mreho$oB^9KT%43; zzbV}O1`%eG1pPY4yKTGR?)uP#KIXYI58B<*_Lk=meK}|1)xF(-xr=M*jF43%&$CGk zc|{e*(F0U3c<;Tua8OuXnvEt_D;wD~;oJAy-B$%64)X{@(3Q<}c))#Kt?HE(2jzS( zCzHs#`K}%&;3~jZQnp!B>o=vx#RPL4;ZW6mr-ARPu5Ax9l&@3-~ z1+Ipfv!g9kZ9gkuAj@Y4pLTj-3k}4~mU8jBipgGjUf7FQ+UoMq7p-Y$!VGn_Q#BI)b;_#>_`H_a($znp>)%R!DjdA#HZ zqwj$h;bmWfZ%$EWcN`|B)C^s;U0UxJfQ&VGY&35x6vk)Kl$XbS`h%#Jib}Lkwb9iE zjlH~+0f#3DwPn>VI;}nT-kJ$_yI&A2XXOKKFPPQ=JWPt`lzJ1~rG(}i8aN=|W;g9v zomuR=o$T|Ch=INPl9G`8AGQo84<@Uq;C|_Z#cujaTb^Phx4$HyZ!wP9n#i8hQNuLJ zhR-1fAy|fMT`8Lz$=@R$}ZK5*bYqX8$Q zY2gVeT^3)&Ml<1fE1?%r73VAQ8>Q5b0SRyWhIY6rErq>G?>fVi_bnc7D8S?aN7kD2 zrT~UK3Tl1kHO}rfQO7Pp5<>NS^#0RBlxIn=WC+UE4d1NCdRr(|JnL!!se6eYs*pZU?Qez#{4KD1Jhw!8$*9Jt2PgGXIO9Sxydo;l^9D<|WB{oSAe} z1`L%nP7c$%>;}Y_furMc1m!d^A)ck#x(woLt z+Jp`CQw+qJ8%m-LmuAIeQuV4=8}uMR>TYIm-^S;LqiJ)3W#%VUG$2WE_t*6=d8}hP zMwgPV{Ti{LZ(sTh|!76w7}`dXT$C>3pYwwKpCD zXz|q0h!U0czmR@>n-RJ@Tp<7wTma;`8|oD^VT7c^xxFp>B2bM6nyWsK@5iG+_Xj1V zLz}e6vbDbMIU;6f{B@y`EQi41ozp<4377Uf4i7fhZh=oYl9Q*AM?PTo znk(4n4WH{7UoifkFzzEi3n==$(B|(p~5mHU0OP%3Z81pfc1nINd}!bN|kA=((bg` zPBAQjkaIh4FRHfLuW0kFk=O6Ye;?I8n11nTSXYRqG|xPd%1H+)`&ac%#NvQfVpKMW581 zzP@riu&|<`D?%>h*NZVp50i^B*pBh$QBWwqiIbOFB}+L{N-M~8V`StEW0_G(uhMPa z5CGfQv`X>5E2iHji8OI)B93@T^z+n!X>8Yx}(>I&y^Tooz_-S9zD{RY`fk6 za&h^+@vMzj;l(RTs&?fG#34r*1ku3#OI^>Q#-PA2dXDf|ZtJNtQX7c4&Elg}>Nq%? zBx+-4pQ_7e=ND;SIw4^SCaim7h0e4Vb==>Um4Xy!#avPFFHP3Lid?O4s}u}yD`;X5 zgb(OJyG>=4y4Hnt8w*|$V z%rxAW7L`_`>3FAl*`CE!HD}C!TPoj)(0luo$*K5?n6W^&(V>f^eK~u4xE3$JGl>AY z7_3&*_bMhAS3CZ|np7N1CeAZs$73zN3MBTPTkXQk`YSo(^XG$106 z15C*4-f;H|$WY_{5Vk)@Ng1Rz*5%Dh|MDXE#}pO8@vrDO7y3zy3t<;0use9U*m|0y zAxV;8VwOJD99LNft>Q_HrPlM98aesN7*f{DxPBDFwpW?vNVlLQIcl)6c~Dns(bp)# zoEKccZ)&&vgL_6SPO6>!9ZGC1bm^Cy)R7bSnmYNVw#J=qayH<4<^S9?v{nLX zFTM57-ea|NTT}Zxr#I>P>la7~_qpgMXf=I;YAYT6LXumWR9gH^MBnrFZ)f5_;MpbP z+0tm$>ilDNYM^fHAi-1lA78$1$Vqu=J#j}yBE}nn(M3VlqJ2I7*!4 zEhvheR%Fw&2NHrCvnogK!5(Ebmp0nvZ@poULt`jO@(HC(XP)be3YOl@FP7D~tWouTn!b^(vZ|*OGWz`qD!0ygh-`0F?4lb3fRe2CKDChg~=A^lknY-`D%jW7$J zqG}*p(~Kj-UjN(xs2!el#UE~X;iREm7;R0*``)lpPEdI)_+XFS`r1?G&3a{V9{b1X zpAEijG=7Km&GKFD1yM|_m8P+(#pqMKCrQJgbRC~c>)wut&BCPMAexacu zn;r1a+gb{6wu`p)w#_%}3NEivG01}S+gPS8)rh@iQ&6)#MdpS3gDuO(n;VRljlq5K z$)zjXf)z%g_jIA*ACP{Zo&U)%=($7mE5A2HAxmquOhKSyf%s;w~rIo4xzJVyp&8ee?SV z_`@ij{N|=IEsHkyZQ*G3xajh_UuvlIgdV8d%~?#rn=aVLT&yxd5j`SJ;Ks%M>0(a9 z(O;)B3>o4d{;7ZXlMH{pg&b_8k@6Afs=>(D%lS~JGwTW*dmsTl3{1IZETa=HPLS^c zSPqsnW9Bc9jw&5h)|DsX9LnQxx4!Uw|2)nG4xAAkHVuUtt?~KY@wK6fZb_w!IN#myU^yBq z6|FxlD0vNU`2AmApezc8)Vg7zyv_^dQ&VDc$9oVg4R^2b=sP`3!liFlT@E(4*cUtc z-r`DPr5+%SG9cu1X&R!LUS*g~3UYj)9?ZQho>J~Jm#$h4m`w4nFGF?eEC;}yjPUCA zrz36dp3RJp8~EI2Cuk^YTcO|9ROJ)Qa!w_}01D>p@dWCAPT`CT{do3_;hp7?%wid-V0^#$aR`gQo2j*Y zk>_c921>-H#6n-z8~*G`lH<+8u2TXX7>fmX8zrxAjfEYTfpqu4sL#Sd6_yD8lU8^Pe5U=!U9sje`9Mim8bSLc?ki#$Gjx7jKVd$wjo1Q z109B^*qmIg^QUAuTdId7U%$l3b@SE&mtRkXxwLSSGcy1+UvC3g~@u zoxjSh%=l@PxWftq*3+-8s;NB|=?x~&j3n|6z%#2i`RGQb;^IeIBxC3POrcLzh67Mb zqHP~EWIsdFeH(^VT@)O&mE4||$?xG^FB%;++Gc)B6HZo6)QFHd~A<-JagGNgQ0>|$cO1BsHlutS6AK=8VXZ>Zn<(CQ7UP_DKF&TRM zPZbnj+!zgNj<(I+n+^`ETUpU9QTjpKFs$%G}*{R3B)WCM;R$ zY?QE9MzWfxXJ_Yqn(qWk=hd@&^Ds1NoePQ$=*;KEe56u`M)U)(E$Q`IE0CzkaOn z?0lc!?LhP`!S!aYRfm)PBP(EIiG8YNClVg3(}ktB&Z95lJW|-bKMOAqTJh!8O}}A0 zt(Y7gqH=CbVW_HJ+9(V5g5&;pW0ITOOL)HzQa?gKG%5ST2`+Ps4BDhtWeYd#m9*2& zn=1urDIVDZv_8l2(zWJB*{u(rRyxa^hV>tK2(s9hF+zTsy#9^}i^Adr>Yy#F8&s_o z21)Psb5TmmBR&Esf1fwX*~rDUxL;5GSAokBJn^SW8HmZ=QQ#^*e4^`2S|3n=Epnr&=0oi5|d z;>#Qq`Dc>83r}S^A+?uukF}SZ(iYodKRce?ITvu!o6sSCJ!`&SWl!R0>NO-Trb27) zMZK7RBe1vsapX*~4DZ9uzrO{r7vTo0k5KrggJ~QUriiRV5p%wsmz8g zubo0Gi$vOFG%4W$-n0-XbD2)3pnnT}DIzVf$vT_6Z(_4#%A)b!Y2a0$%9mg3t}4Ca zUF@6htNZ9ohnF@41=qyWnS+7VzI~*B!V-^X)O_8Lv!l{0;?DNI=|WRO3|YdZRg-)X z=BS?=_9F_4j#}ro$^s-O&rX{U2nPdr237}cm~dW4@wtgDZi8j#cD`@vGgGwc*x0uQ zvyK$)db=?+F1+TF?}AlQjn1~F_B_#Mv#g3R#@6I+__TE?jx0>{Qof~y!;5XnO}TLFqpA12ZcIF4}v8w1vbe&cD3aPDce{KU}x8;d|nr zjC-We6 zGaqAV%=ET*D-i~G$iA(#BP>Q#wCNNM0A&r)>7qGl87x+WS@9eVD3BCM$XV%(n-bQ$ zTg^+$U?=d6FC+sGN>$CIC{Q#%sor*fpH08)z&bN7V)G(6Ee;4N)?;rDa%)dxQT^zv3f-E71~xW;(P16Ny)W_M@D;FK~}odDYu^-+RJcq8qbp)Ohc=eB2D8Ww%r~A7ryyXIYfW2uFH*nU6*t;@ZE5Nua-4ksq-?eRjU%A8Q5pR3PM04$)^Vvf>6VVzE%2a0^Av;jiP&>6{8uGW~r z(e=iR(N4c{eZ0N_3!Q$SL;k;5&VRlAs%?KaKIqm`K{)hlzyHhKOv1=sxYubXkGw~` zSbmGJ?)b zc7N@F`AkF3{xxP7=(du-rAAe4b)qD^J8gjZYN)hpinp%GULXX0Vf83jks59NT?k;8h=@IZ;PlJgA=b?gzA z+Iz1xLoT<}slq@H*3?sD2g4a&9s*u&RjkpTH)&6UY$jekFx;w z=&xMnqFA`j0|BhlL0TQwT;;dU3@`zCmIT8@BUb=|>7uJ3r@?uG41TSXH4&7nyDE(dW^c(F4NX3o@Y597z9B#4- z!=A~jR`w%-2dyN0WtB19M#fx(?J9%4r`1hP9@bT`HKR-35H2KdQ6^$ev=y+pa=uG> zfZbNJ+L4NCT)&USNUGt8Ll}N0Pe~d=M z#4CH*YuF0ZFx%W^y}x9N0{5UHtyKKWHE%O4nG|pGR0L%UZqp@;{f3~(te~2c(JJzc z7C?wOtb?jbjor|t%HP3}w09tl4KRDVSw|SZJN@x`_1iB5SRH4rwBGrfD;(|7X}O{_ z&sIIK$V-KUuIB82efi^m!tZBa9#kK-xGHq&b)craskE`; zHDlAMUs!RugZJpgO09J>is@uE-xcsd^cJy4?7jPtAUY5>?*h752+=Pr$ee8D48uUL zDh~hp6?MZio=04ao67E(d^jddg-tWN)2)Xnz8Cg+izBOIWUaXGy;-H)w<68-Wi)(yakSZQbukw z-T8+slufN47odS&XM5hQp0va{f`L5*eAYc;5AuT7hAusbd03^nx#K!bc1BdKDXk91 zL-rknV=3deXVe8ou~uP`@^3iNPKqn(!VS5OJWGPelj`!i9L6t)8*mT83%uQO@+QSi zR~<@Ywx32YN(2xWdi@qN#5Zb;q*8Qyb$Qaz#mA?OyQZAdoF&-6SWw|J!JOL!!OWek zwU&{KcGJj;!v#l^05P8CE0t;y#zE3ur8Vs@bx{OpM-vGd%`XMAI0#w_5>%(H19-g; z$1=h{ZCYgo`xFx8=+rp}u&_?6_q35x=D0R%i49+A)Z3kg;I2wk89OQA!`+Q#dvF;h3dpQV z-yZ!a`FQ%|_dDdqVO`JIBfB<8iX_pyHsHYP$h`+`t1j(2JGh~`20(hpQ-MWY^Fe^v zLO_dS&Z@U;rX}y$mAnT~ITdto!8%vWR$9f{LRB8KvjyZ2%rUvGBu||BKYV>-SY}Zh z?qs{E$xWPW*PCtIwkF%QZQC^&lZ`iPvi;^deb;r)zw>>+_V2y-TF-iJJojQcnNCeo zaQzGb@GP>xvf}NLgwbJB zU2Y>dZ9;>)yvT$phz`4?{jS@{@^kWWR`$xC_jMEEVNqhZr~N$`Kq-FyEc`B1{7S!1 zrUQ=N@O_@)?K-`tys9a#drwMxp|~pn#k%ZeqV!p`k6WZ9bDsN_^WW3Bf@Z4zk#IEwyC| zIIm%k1DC1nl~p;L4(W)kG6-zY%1C)LaJiC6I|>i zr-K&uuz^9*P)E^+qIHmqek)t1es_zC>#h#7PEa=pc^S-xiy2QW^yx2Mv!GRRlyxaM zzRwG~G>vs$IDp{b11R}x@UF3P6YzN2wxaK1*SP)VwLMdWUG%p6sLb!O4$7G$Md&0- zt50=r8)27c6v*kYu(ckAZ%b;#fOl+If%LY~YLPqrvm^`TC?`sIGI@MZqV#4sZrUNd zx(v~&ySpw(N0=zed%#ciUuOZ(|6CFl{U3v_v~*wOz#VJurJ_f@-{OZ1__VpN`^t+L z?|HSJ$!pdX@hzmsze!+{jT_s1@5fy9=BrJb&ufWYr&w;qRi|Bdx7UPLXLAvGb=t|_ zRH54k+-8l|p1x3M1s*hAex#rwP= zT=|!I_Oy2?HB5va@6DtVvO2-w+5GXU4fsdvb;iC+qtNQv*;%6UxFC#AbgSYk{s}~r zrfa?IR@=t!x$D^Z`2>)DO%tx4xsX+228})N5_VTGeV#sIIR0+iyq!tOT2?T}Zsnlb zP8z4n_fX6_FO$yK%nLaC>Wl9LdOwDr9ApV;bvxs6dABxM)~?cgMr7Iws3yMG#7k;Z zFxYGSj|m-eiLd5I4u;K>>mt{AMjG{PNEqfk_egFY39 z++NJ(U#4WtfR^6nT)q|aJfHLMZsRjEiE!&bU_vI=`g z&T~&}V4zry*Y(ozB;VtkX>Zt>EKROsoJeCB3i1lydgsN*W?8kl!A4|5Jni!Of>Hyz?<%{>{UkuPrZj$T4OA1IX=5xJH!&u$YN93{DrdD3< zEV-Flag~otKFlO%RzUM0%A1-8LwtmhBXH9$N!u=!kYVlSzTL-XMDq2FGIlize)lY} z^BK1sU1UZ^lHWLVtYrC6s}gdRzsEjq1@l7KO~=GqE3vLush_1PTQW91ulD3VL*4P{ z)WvnsddrGvyK@TA_Ugd{;5O0ZV0mFYCev}BgOaeKsbXUhPP%!V4_1rHly`Ae)TS+# zt9KR>q^I`x_rs^xrMoixWEvZ?#MV4=rmgD8U6$>=Iyj`R=n3o^K#4_MmCfAyA55!s zj6fk7ZHq6RlAqFT<>a~#4D|UP%4Hi2`8QLH-J@uSC>GW^eShibhuOAHdBpWRi&uVm zq9zWVp5e&QrJD;no5XNAs^23iK4G#y+X23DS{t{x_K{JaKvp8NTmj+SBF z#uMCLgXDyuj(#->l^VCX*`ut)ur$5kxjZbye*FDRPyBWlao@=gkAv4jo#IbT^ z*D+?N-_#Fyna2Q$Jfh*<&JOkWWB^@b51Qyj#5aoIm=mm{4%a+r$XcnNwPsRyq5>l( zJdX^wc+{7yIp0~X2A(j-{2F!$D?~qur&J>$SGzujFMiQdF{)NJnU|Up{(FbB!tN{P zhdk$)XxD3Ww$*N$EL*-W#$9+0f4F{11Nll( zfr1dyFYG7%9XRP0S6M$WbR0?yB%E4mHi20hU(NW+&A{}x=2K#IoH*0i9J=dwv{%I z8t5tAFKGYqVX|eWNW+L_%L-!j-goHTynBOt{m9ij!<#&f9qo`}F}%y!=$a*jnIbyVDJO0oaDWVf?8{tm&zIRFh4AbA zIGtbXPH@kSelC7(>Ol1KQ^}*aFBf`E{BQlw7iTH@bsjGeE*(QPU-6OaX>oOn0C?l! z2!H#%kN)JDPm6#IcX}tqjLpqj@2dZA~l?JZj98(RKs5z{1Wu0bh|+*7nJ8X za1|$WWZrcg89#_Ph*ZDpO@?Jdx_V~?!9t2Ll{=u4sVfB@HFc?wA8%6vA1`=K)9P$n zcxum^AF1xI9`LknU6g}>J=F=qXO{JI?H=yhsyQZ%5l%AP8O z<&^W9uhX@5uaik57ZpM8sRYY!3L2|BF4~PpzX_VSqa0HXyVOaT*oqY9d*5exY6J~^ z{VxB+w2q6O%*PQ*p-fedQ4u9xtLRyG*ZMjwkh|Kh*w#{ri86{-B*_yf24HUD>Y&sw3;c zhv+p*LLLjh#~~&>rShdj7+uNu3|ELFz1eNe&ZB~J-ubLndH&zCJ#XG=-7oX5URO(J z7t;vqwtM5l#HCB^8i!xRot+;V|3lVeQx+!KLb|w$RAM%Z;>u!y4Sn#{dt{eM@3T?c z1%EjG5O#U}dFcJ=H`mO95e zbxp~sTsm0G3|PydGB7V{2;^Yy$}X~0UTL(yc--=&Gxv7gk(%ni#*&%f?>YaGCZx>o zSU=NfoJmC3_{ZBfI}2{>R8xtOVsfOxFrwn|vY8!j(oMHT?`OlN$`O-&Oxo(cE9OPz zcUutUmndjwvu)tlct^F)w`i>SNc-Eu*;Y&`4;aa3t<-e!GACN&75r-Bz=Aq79a)8L z$9f|E&E=y*9nqiaC>r6YXUZcHyZUe}Vlw@~L|39F?l5W%&JX;ki;w!hN-o6p^X&W+BB+=%Hqj>QBC0l^sZ%Bu|0oYg@|D%IK`kLS5zh|1Q+mPwm_&+t~SOVDPcM@S0? zn^u%adZx-b6|UJ6>(Hh8@mTO&Y%=G>F{zW)?~R_8yW;n}ve;Epm={}K*z(HBqmke+ zy@CMeKQ*C&;}{XIK|#i#g1Yl~ajO^ro^_4nb|k>|XcK=G6&UvYQVedV*L@DFv&ARJ z=Xw?9yrMR!;gSs{)jg$dmWQi&m^@p^y^n}Ql^BeufZM};xvP4mhADf&kXJXuudCC^ z*p-#V>I;;!I^HWo_Fk-a`Gr$E2r&NF^{=7ypO4L&op8-md|jmPyBq8yww259J8mT% zA?;^#Wh@~(i)3!^ud!`C>pRfDWv)8 z;sk&Kk3xng55kieHie3Z7Ui^;$77|3?Bg`UO;j9^fyuKBi##g{q~Dr!`d}TX>{A3f z)8Dl8`^eSuD(Z4QsOR|{>#Wqr30HzWI%GT|HlMOkTUqg_r53i;iVvoV zqy62~@G^2ki6#XtL}T@N;T6>lcMWOS>!PN zZmij9uV3dQXARow?ObLCh;M`v=Tpq2s%JExJRUNI*pbGQ7Z6Hc}b}1xRqzax{^%XKpn%jXGYYJ9^)CZYA^r zv)R6EtQh_DFyZG8+=TdpEuoQvkVlRg*)Vl3mB?X)%{A1+@ z)KAHGs?n-NK7=sLp2v{OpPfg4l|fSko38m@tXiLo1S>kkS@FMfOrAUMpCtMFJ-Zq$9{VAV|rW@T(QKFbSYyT9k>2jKM_ zL;U=M&xf78a;)e<9#)d@ zGaSlPML|UPX-ch~-I98EPo}cdV=fBXc;{~3vc>(hjqqbw$>jIVK#?8{dN8ysz=u6o`RC->wP| z*L3bA{^{V3*E9Wp1f-0AJ^sb<@*`OWzVA7?5ysDc^nK{PMkAENlk;V!!c=CR}yY2k@UR02&-f@ zJ5|2#xS`fn&EdR~i^ksX*845K`Ls3gu&qrdo9AEGlm#^BcndFMm8Xm~zuzN#Pwx+# zdjNXiP8ars#g)EZ2+wTT$7S=H5v_Vg>9!V5WxijDNH}w*?cs9$54)@UfBNTq9(uH; zWw$pXl=C)9l=~C%-?vIDMD$iUvt19!V=OMFMKyCkmU714q%C9lqKpt4^e)pq0GV#W z@~#EIUjFh5#nAKf^oA5PU2}FW7sAO`{o(EMD(NM5R)riV_2bSQ6Lfz+@dSTd9R)!; z1s_)V^p{0;D-R29&_yreg*nIV{UbUm|1u^8BdVcP12xBT&(l|j+E~ve*j4y1ON|Ng zc$FhIkJXU;JNFj9o&G$2`%#Cn>ryEDREV5cXYdkDOX=n5u%~tMj>ewPvbmv-&}-ju zJD+?0Hh>cBp%ea*H#HUZm~gyd+4^6LtLOA&z?idI2#Y%Wo6BMxOl~5_Mcr1@Ai!gJ zsnumOk|n2R5aO
>%Zy2`$>=byoB7lrTkq+k|(Ebv7~UFrp2SJW%Ul0IiH{-kvK zQIO_y`WM5*b0Nm{&(pa-n|mJ+xr`}$&%FK2n^kczUX|+@{m}`IU#1N?FH`UD#b|EsUE0SqSL-vXq?j zbjoScEEaRH%gL4f{l*2;SfNN-0!SC>x07VEeqQ zFaHV(Kezu^^qMf+7`_=P3x~BhN^vzPlNePHgC54fWd-DBf5(28?4*}^Xw4+hBZ4br zVIy8SOQADz!b|*U#~Jf*y6?!}z0%n<;LzPLU>v3ywsrHe?0oDl0rsR8S0-95+{Ts-fHi+XOb%pmC6@jO_~dz% zDwzq{?E{YF5Y0r?t-FZ&BlvurYNWj#`Edn>Aau>G^lg+vq3`l|8FFwtZI(1^RBg-l z&S+A+D7B1G60Q0weB8wb&BK5864DRM<-`60kMt+`?+<}o;(XkSDE38K3FT|!+2OaS zvh{(5v@6Aog7Oy-x1y*zn zK?*r|4v9&JX9V-6j1*33RHzGOM~1?VgF5NM_fSp72fNL8NEk;NkY&@;ssFfMHyRoI zMTn3o4-(Q|_Y)Q%oboGlE+H$pzcm5nb#W8M;84rYNl)H~#z^`2FORI!o$ZTscUc}z zScGq>mNlQV+^&)~TV~ydEtPAy99gt~$~Q=l^AaZSeLJ>cQAMWS4a`uFHD44OHj9-v z&yJLnmF90gr}1eXo0Kxw3YA$By(=I()A$i3m=8tWfCSbB*wFfbF%v+Z3cAHyDwe*CvX-`x_i`=xL}rImrLlAOmxTf5{?SX0x77 z^rY^bkH}UC4s2;amA;=n^z9jB`7dS5Q<4szju65%Pm4`bUu8f-;HU z`*e3BQf6k;usJo2qyUg&F-rvK@~Qf?KagneG1VtkmW?uh^Qo9&*a#-nhCgmLW_Lg}7w0$;AwltZ5u|;P z)^?S~9fy6H^fQaD4n$w&abNd8R@i@5-#!+2cEvIy3fS}+@_@)mFaA~9=OZNnIz&Z1 zCi>FcsZ<|g?VMOhg=XAx4KDlL_0k}(sDE+T-%8CxFUe^Ih^1^&BqxHPq_XcNE9^Cc z)P`VxNf{m{%!xY5x((6e7Ynr4>$iy*u@aS?O#X0MQD#yM5-~g82{AL#T@(=?p_&oU z9{!yh+z{}O@}`GeRlB$E@|WQ~0*{bdIHDL3O2Cq6J5{!PM8%?T5S;515dpXzay{=i zlTD6k9E5Xi*3@LdaH^){iB4-Je2KNNo_dl?LY`&e8|a> z=&$5Rg!1EsMd49>6IZ-B(9Ea3IyIs@-xbQzK$bVjZ1-9H$9}m`DRmfgiwEGgNpMK%+^WkpKXjd?(2WEuH=CEfpzMK(!CO2q$Bv{5m1?n}=m$4f}fl zuR!1Zs)rNzk<79i^5NgH<-zAArlFhaw?Sw1lEP*U!24m2lKvs|g|X#=rrX<_2u*oc zN<#C?yHd2DTD^QNai?gK0%^+r+C53!xA=C-*MVV%=7d0#^w4Yp10aytENOBlgAGx? zbqQA+n!88dYlJo82KyGFZ`<2F`Wo~~5PIi-Lw?~$AFN9MoJ+;Oq zN#9M`k0MlT)(%E94@UNmhO1#9ZkCfe=s;?ihce)Ap5%t|7|z)T^CRYT8J}co)osjP!DbdA1ofu9N;x($fPWZiMm;>l=B0aXiJz zU7M-D=jEo5UBcO1%LN`qthgYxUUA*dSc`Qqr926 zB5Tk+;&u1g@VQ8wVPY1RqO{*n3uE3pNb9ig!s6U8KNBh=l7p#>Z)cK7w0>uTf1cVh`AUQ+t6fRuXO~%J_5$9{~bNT|J$BM;#l8# zdCqZvJL|3gQ3um!^yQ?|&|l|?C`WFbgGX*`tO4wq;q@xAU~13IRkA(VFyG8lc&Watsc^)Q^5<5mE2+H% z5y9pu``1{}Q2=^H>4H(;aAh=QoxjNY4fzRv`c5LBPJ{k&yXGZJE(TEM3boSx+YVhy zx46OMa`K;Z-Wrt`bu@o}>?vXX{ZK9U zG8hU|T%u_pOBBYejr7NgC7M+ImAE#~h}py$7wYT`BOg_nSlB*?zPpN?O{e6?jBeYg zntq0X6{e3LuHRH-DL$&2sD?FUGywb$McYr*Z~g8ve+toXVr8ZLyoEyR2}YioP%Cz+ zWSoqhc(_woX{<1aQJ)G%o0X-CYh9GT`+ocYNTzy5O65~U#PQXkh*Zjdh3@XBRzh<@ z&evfbadO$lHG2fXiGNBymGy9=rKXKQA8b5@-|qnMpc1%aU#StD&r;2(!xw4!s4uOz zXjMz}D*ELk77tv=>C52_Wq1jY$2$}h{>;|;e9^wbQ$iT1|Gf2oCHHZqs_BPxH5ew@ z+@?qyq_E(S*^BKCA7y~@@vo4i)fuS=i510*+*=Li6SQ=@h~VJ{g-#H(;z~&S{=$&e zj5mA*Ghd}bqlTE6a%e$4dqnm zd8t2M9x!XyS&q;2tg&Ct!t8Am-ebO-6^PRN!8-WBZ=1{Ai|(uFOVA^NFFQ=OGvE&j zc^h@cT5ABi=~VsCe;VXMwYxp>!n*4cE9<&Xdzv`cs3W(ij8D~oSq_8O>G(}^hu7TR z4USRtwz2Ly7dvBSmaFgMH`miT_mnRnW@gd#ki;+^Ri$Xl*f|HKi5jm5R@K}ny2?gP zMMQDpI(-9c4eOaY5D?yvg*|Ee6<)!KrS~=$#}Ela1l8+po~7a;VPHy}-T+{V#LDRhtLF#|aGgb%F~sZbDzrTN61BdHm|KL-w-D zo+*=}gSc?K9t5jEL!X`w>4BSOz)TM@2X~SYJsNWTYjm;xr_YNZC|=+I9Q>Vtq=bUn53Gwa%`gF? zWmFCK>3tijCN1rc@NFF>^nM2oDSH19Ny|R#FYu7)Z_A)%)fZDgWt`^9N`v`7YOVn) zH33+MLi!=rAy6Z4h8AKjMqt9p^z}|;*9*Z(9Vx`h+wFW-quJ|%rLBTTXb_K*Y6T1QR|cq7A109^#oDI`6XI%EEOFHwpeqk8lsK~O5pFU z=Cssx222YmGsvTH(}>cL#ngmzTsTsK&`<@dV<9#jgfIC8zKB4Tpe3Zc)~sCw0f0vZWH9Z=HfRW?`w3b^;$bDT2&pf{^%7J1r5i^ydG4)7+hj+ zYQ{1h%qD8y)@f@tpT!JN8p+JKc9`|UWK1XTnKbl?!$-M;S$8CviwWqd5!dI3_VP-| zG*~ISJ+tSQ3zz~ZXe9DU zi^)RzQmcxWnx+k{w@NhP3SpLHG>Z(7P|)dWfv*Q(g_0p~r2u4!ujR~oF+p$SBhw^b z_eWz&08Il8e9`kV8*z+-d+_#NAg-&^Hq1NUHVwbJnk87S7DbkE3+m#s{0%dBU0tVRi`PBxcOW z@jHSdr~T7(@AGN34tw(&rVjHu$H_J;QyDz~23KxuU-R*!9B3@*iST2EAZtIUjdlDI z+N$8-1%uyMp;}UGk9drbi{;xtS~6W?pQ~+E%C=?|iISNbXJqR@cRMpFW_xtpgzi12 z=Qd@C+k>Od66~lfcfDoGGdogaSbIJH!8?83!?r8$~?tV~t#O`$WOzG{lefW<-V>d-H+C_5No zkIU)a!$Z3Z$J=yByPLBtPcXns6iNm1dm{>C|Cg8TOK3(JcHri=O%YZpsM4oj@Ss@o zXRc!oL!APu^JiWrTzqlP9O~!-z@zx8axOM%nvAgnX*-%&9jW{4av4`sMvKZXsB4Tm z74;w_{W3vZFBgYAG2)6J-^OkmV5xz<8{oupo3^wgiJ!<46GwM`;pRVpTt=0`s|W}^z>oD0JReL zhyn;V`mqm2iqa}uJ#Bhh&dQ{3Pi6*&FG3-w9ofRf(dJhbAFz{NC^+aYn2$K_-;t_B z{83%|MVIqFigkS*tQ3YB)rUMtbDH$X>odJ&QFreAp{by6c%SAGHs@ggm+E?*ns)2S z>ob};1mbSjLI@A7ss3HI_Bq~AG=`&0v@x>Ua{5N4Zv|yLy%}hS`)&Yp5_4q};*Jpx2Mj3g{@*&Wl9 zn8pQ)QG&^@yLanPr+VcH&P8(0#r1@J!&e7`y4VJRP7(m0YrYaCMLK!J-;ywsN}**&*UEW|OC!(A)p%+~iE4|*0C*&;m+u!*vZFKh zYgD%Gc1O4wQlGK+Y;P%`rrVTQ=Xsc#H6hAP_y%Jq#F+k17TIimrdc{a-`(`z!6QRm zq_;)^&^;ZHQql*)+MqG<*7MXXnzNYge<@vavvID=8ZuupdQu(HS<>J|5i^{!5iVvx zOw6~i6POnA8zd>lF&K{q>{JeRIL$+5pUxkkb!zw0h1tBb@M|eG8b$ zxpiNcI{DZSiKdVV8kxEeoM!BBFqZhT+a`Vu=q}zRi zwmDut05@40squ9fZiwX^8+y9QEO15t23h zsfH5Xb_LQ&glS+bLDAZgDm3dzGAVEW(flMI6E~{-L%es*QEhFP|Ie(C$E~EiemT*$ zZ<4qBo!+XhqUoeX9p-#NL@+NHMoyeRB9}KgtaFc4VJ)V#thzhwZ0{kl%)9T2Mcm>m zp8{if8ni{}G?S;Y`;wko426IcudAzwRaeP%iWW&&e)w*IHX{;^n7_%X1sT=R4$<8R zcBtTv*FkOT`1hkSxI2gdjC%MR`?A*q9Ns8PS2Z?8^=&)J=v_X>q&MB27rylS82{w? zFHnyGo_(ZEj1137C?uat~fioDp0h(X-o3(};z4^lXb^tWqx;tAn*>|6F5DExzx8--XyvjQ_ zH@vccEysVMiAi2O_;C(_P<|xYkBM0ky5Hz6kXLyvguUMG6NJylhdzxg3go^|61tYK zL9BfIj8yX3DC5o-f=eR0RPmX$`Di<&g(#U7`oSLEp#l{fO(nQmY4kpia@G&1Z^!^o zM~TyG-3|EpI-JZ-def{0RILyCZCNpkxGgNj)R@n-Wrxj1TCxoL6I= zp%>N-BCE2C>yfbWmLW0ZEGN&LiDd$=+svZm<4qQck%7?&x=_Vb;#a+>I$w}A_Pzcl28t4ek{}j|Cp9vk z$0N>vX;{y`!TUPZ%%bR)Q@DS+60vl0qXb|PB2>f}w5qK{BHZKnYy`9i|InBcLMbD* zb0pBJI%+VwmN-i9!lOyD^h`gZyBn0-KwQ#p(j)3|ZT<5~P! znT?N^{4pkJ96FCHu9jF-stDO+~|(GO#42o3nq)i`d07&?Jrzevrq zU#DB2D0ooq;>O5K3rccoXZc0>GR)YY-hL@v$2hmBMFl<~?(f?-W5E0oW)58fhg-qH z0h9boCMY3qU{hG}c`QY7byMM|n|(zhaQwk%t1Kd|ty8j`3?#&IlTlT~c%(u8DA*w? zy!*4SEiLK&r^l_6!XLrH;Y1o06DnQ{HPs@{!{BLT(ftG#VW$d)!l)GWkWvagdq`RY z943#ajr?=lUxPYcEv$i66D5p((Cld@6PdJ&PP-O+L3thP?5bnhJ7@Lb(#D)<7Y`6*ohgi>oHsS18fkb(XxIknuh^B>nSG>D;H;Ad6Qc#XFXMaj4FaJ@d51<8pF2div zqR)9PpNbRS48}GRWlEb}eLU-?aVF&s2kx9Yv^scc6TQvm_C>aBQetb_6g)$kaavVY zOZT^&ddn+qi&_%PZHl@l-33fjc`%1m=&4tZjORZw*vXv^M?({#+|V%u#ux-~^ScxQ z-OSRgyUGgUc2b5`yAAbMYHQP`I1#_KmJsV2b7#l^NSUoX@QV950`q+Yb-D4cD+N6% zNq$I`&P9V(5=k2?e?$tDqQa8@yH#@NL)9CpcQD|C@DbF@_yGg9Oj>4$%ZIqix=#e& zt(eX|B$QmI&&Lq=;4<205|C8xewfyue3dIu!&`sg{|4Ty$Rb!Mwi|VE-WS|8PY?>% zAN+za(W@;;K1~AdEiFATx3xgbJ$)}8+=KWqWorlFkoY)YAd(AlG9ro#*CNf0o&TLs zbXTdI0GLYH$w`~gj&4H}%v6?yB!Kr2av85#KGPsNc#4)ElGvc3B)3Cwq-pVt_2+Nm z5@{*dJ_NY(&K`G|@$J)JG%o(==Tm%ysL6rD^ki#VIW&q=IwTq+-g*uz!3x?Ysikij|JlVLCf7AMcp=LO=vwT4`~+jIS-?^K>eMN zLyN73zot~^I4ai3vt)u!o6nS3i6!~b#i>ERkRN)WRnz+XPLHvF%PA@()DH0;B)TK)xw7&IZqptN z^#n^~$`YEWOr^SwQ1g#hj*Ixme{FDBRBBZbchrbSck6gAq)*joJ0h7 z>@l_r7q*LuOF_FRu$Y6ejG=XHngiKRtAkF4#{qR}^UCKx$ua&;(X?+tKH|n~c%s-M zIP7*6(55BXtq@rP^P{LGoaV`=*Gw0~J$MYg_i<86$**KdI?-|aV!|mhkavcjHrcHi z{b2G*4tcGTZZA@7{P$Uqz6+eW{cH{mSWGj43CDk0*>Lu7Ay{Nly4XK;UR*Y3kiq46 z45CoR>85-=UZ-)_pIr{uDUlnULNDw_r|v%zD%MEkT`!os98v_GnN$KAv*9RooGH#{ zO@)mmJ+4y(I}SeJo$(r4t$!+(TB(&kqdw-|w0obWfK@?ZS;NL^FYWzUs6XILk?#+; zR;`}ANaa{&)tui;ZrY*Bt87v~(g1E-c#HEX#+0Ok;B<51tx(<$JWF6q%#I zd{|6)aGfDhj2%{OK7Ubm?aCbxy?7vcMMY@ec@5w82L7*A=}mv&#EL zWd7@DDcq2`xNc1P<%@4bN5(77S)1+@giK^;k#t_|2)4%oW&%_cNl$k>=~|^5xP{3# z&%zrj?3{8Ti(`u!);yLsll1?b&QMFp6q8U%Vu{+AI9i|nc973So{@2zou`SEx(<@4;+^UATjR5Qq46dvO`Jtb64rRJ?nFP3ewllF zYC#(%-A*dB@d9Sag&k?9XY*lg7$G^)#S2n4AwK)=Gd#6riyHJ2Y=5kg8qb3XrA6V* z=OV9S2~TKKfZJ3NH%Iuv#~g|q&OhFBPBI6HutlAwW~ldwxb5!hA;>QBIMj7@d*HPq z&4VkkDU(q%T?A0|11zj%$}2{_Pq3)XV~JlHl~Odty#bopM)q+iwQMC~B^Mt?mgUBT5Q_AUcJgJw+`}}c+0)7VK$EZ_T~)DS zmnAoP0>@&9Z{AClJ{qP;s|TwTb{`%5{)8Db3YUZ}016s_y>-_3qr77UE=DY|!|{n6 z7*7OyJ%%RD(=9bCQBh z{|JgwB0pSBEtFziV(QvKQ>VSwtxnuM^V|(I(n{PkbP} zlb8Ll@chmi?pq}5q(-GqwgEvo)7b&aEw-!x%l8`#XijKwYw#h#MFgG*Vzr76sT#HM zTu;#>fH2m68~l9liGsZB#Jktoe(8(9=Fc%~!79;gS4}P*R$Wg3Fa77kkl{%ltmae? zFwe9a@73^|RxHF))HYtb`Nu~G{qtx|Ok>S_@2!;8xam&=q-RDKNZUpG{>ixv`uO5d z5z)aLl29}Z2|MTP{ES=NXsYb5aJ;q&YL{-3CONfLLiGy#U`_p3hi=GuCwu>Il8d95 zZ#XeXV;r6I0(dQV&{X%0Bnm6AvvCLR%oIn$MWK@$YI+%pM#o~ub6t$@)=D(lKsweh z72R9KnQ6Rfu=_S-qAWjQ4G9Ut+6eGiqMs2=rfA&-=;*H11Z=d5Z-J4t4CrRwn167x z4GTak4pPA-eMJd^i5i^1%J}~(7IQ=WK*SrDTADoNJw{K*fE~~|A@(k&VC-?%!Cz&q z#sZUDf+}cvTEgx;c+jpgk*?9-0K(i@H39F>{J*sWPW%~mDd|z9mn9Zy$2DM>;2R}g z#1Ec$gZ~J=q$e1Fk}14>2uVV@5Ad~i3G)7ZN9bWbp$1nC7BQZhFS)IlSpKpl z#rd^GC&c_e9>wxcSmEw^a1TlPjPWubq`7E#DM>gtKt1Ji{71^8v{y8hMxS%fyID0-kM;B3eck9irQT znJnRx0<;MF97NgL_(jOg#UTU7|C!w}O3T z?dGqs5wV>rNNXBuhQ)UBBjeBkNz)2@&?X(m&ovbo;KP^3o;8qTh_4)qZZXKJE_*`hg9IL`#mSD@%Y+7JGVTy9m3K*04I z&S!8ar8toSCqh(9Pmx4L_w%?3=3R49ES=;F;AQzcyCuQ7#qS&cbf1#BRg(R?n$VA& z{&}P*#|eM>JZERIj|*s9ik6-N*G1;HTVmHCuw72xx@y`QW{KJQe`xy3sJNQ0S==20 zA-D&33vR*P-Q6KTaCdiicemg!g9Qc%?(TzIxRdAo?(bP=&044X^sZf1yBY#5hKp`A z<{JZ>aZ0z{XIZOF-L-83x9IkuRA53e!d%F)x>c`$;2h)DRVR7jPlsBZDdj(EHK+j) zG?Ai+SzU06SqCGqA_-16+H&74&fbloTaI+p6D_ae092$buNmPDHCXusj4$Kl zo!MWdyh5r0to%QCB#SBGf>f+&4k}sDpB%b zlVuR^tR4<0Q2>FtZTW*kFm!~ovsFtt5-7o^o2d3P>R_|YWq)LzSgR%y zm#F!>h8>u|Cf5qy=wtu=+E zQucl9|}zb*H)17^9}q^)mO6(r^)Pg|a;C--KGis+%vA|T{V#&#AkY$G`t0wBWi zbZmT)kB-%12M+|<4s_p0Fyo8fNR?SEfr1|xig{C4c5;ajcn#Hpg0M*^ES z1=_?cZ6nV19FYK1xUs)_1Rw1EaQi5unRqa$ZJn$1k9PHG(GS~>NZKK$8$S{6gHbBr@G%0Gvcu`tJ4ShcS-J~G>CIVehY>tSMxQ#NlC>2$lmz#+ zWA^AWE-5S5BXoICz_%Ae0@`h`#&TNNzxDtSN$fa4E|9St-j#83F}+>Hp?rT{o)*jZxd)cSU45IkW2 zAUN9$UfOdirIPgW#7V;%`T#t}Jx&On*QFZzPg6S`)Mdc7}O`+u(H`FN^y z$Hy=JuF_7&@-ABj#WpPY@1q!z*PWKx`>_}zM%mGjDd^%d8syj@>Vp3| z3iHFgOJ=E%2|Dtn(BAt1dkt-zG4+0TGUyd4dV8DRCNMbrehCQ0jRe*0dUvVa-TE9u zBZ8w!G8G-%5=9SudM^iik}+xmQ@v$?=ofSiQ|>Bp1n)Z`K6^jwQNjw|3jZ+yIDt?i z|31nu^Q@7d`k`Ot`D_l#(BF-8HD#mTwU&@jVYo5eE;&@t*Cslzk~aL z)xX52(P%+%7niJupEz4CC+DY0VZYq`p3|0@UGCv!%t=I=y948k`6oYCD@Rz7int*{ z3qp&gLk1#wfN3GQrgz+mYIY;>mtp8u(p}Qv#NUgRpLr+<(CcGU9AEO77PwryT@{(C zO9*5B`HA54bK+64UeA$pW`Nk9B$cjSRtK7xO4YSz!0wsa824?*HF%ztFqKx{DuJf9 z%!Cdqs$T`eiKn=9ctgpVL2xLm)z4nHbiJokhY3~j^8yiEgal0sH;$Za4Z}dz$ERr5 zkpg0YjDXs(L)u7Tk_UUgxu`gv+hj(aB5c-LV?wM2TH@H#ur^ghsYxucEE9DCIhoEI z)+;8+n`3ub(@M=#eFwyTL&Z!0m7pFoql+>kI;CwS4#h0(E%Yo5iJEhbaLi!L7x|7@ zBeqVGJ}_Ech%9j$;nmEbarXW4s`@qEI<_Rvt|lIHC6TX(c*f7G6f=H5&2<78dxQ}V z)8daHh!uFCg)g<}QXw9o#~DJ6&4M+QGrUy!>76rFm)6#JsS&d*O!jw~J*n@XHlR%o zInhOmK;PS^Pg$kn3LAErzYP|_&DeI}cUr2oOEvS<`IrH`rSjQcQpp`tr&Y74nA~on zKN{MjNy*LpSfzKVI?$rseQ-2iV<;jb1H|kCMUJ*h-ZuBr_p7hEq$f`#vu^B)?sce~ zbcWb48?FzFV$!|F_L=h?#-0M*w->U~r_8&OGn`>wyRS}-AF`uWcp}&o1mS>Vq0a8M z60sa!h1QNil}>_7Wk%+4l-MBt2%zcHy#xhYvTd6N60T>hmLM4mD!V^xH^JeVQ;wq6 zQFKkqH;|~4QtyhVL0*vbR^m^TEzF;Ocurpj+n1*o@BdLfo|V5`f)ZW_I>}ZHj*?{(Dv1@QtjDcktWZrz3Dd9gh!d1J3Zqe7ezmIcfOne~ z6O>E|Qt{iJfEzGCkRe+rzZBS*28SegsmMeh{HkAN3ID+c6@KYt$Rk`5rV) z7?OE6Y@E_3+sLir;dPF{FJuZ1yCoax7fsN<@X8ns#w3#|rwG9YaiTDWY#x{7j8M8R zn|<~p8k~>alWmL;Z-`Y4c7h#{!M2kgw~w^H`utH;GX*tJneR#~khKDjamNTCxgmnh zlxgluCRMX`NFPYhU(cclUTlSaU619ec7dnQ=dN_VPLwC}|IFuWqu(r3iR8^4DnDzpC-GY<8 z^aCJ`hEb%7|6GYaHETLq%c$SMbjbLU{Ve`>!rOrCJ1BH|5md>?y(cKf-_lq*?TG1> zBfQYS4>QGxM%XoK0y>do0`wzr$ZwEVt-zri3PROsh!aAe=~xA)@8um8A-;08lOJsv zGFLtP%JQoGx+7k_a099*4rRDGQBul7e-MfG)2q-g{qF=KHwv?Ti!{Z|?eTGNfDu@A zLVyv;669Q`*y`{y$P4Ga8~9Ul(ABJz*x$Skt=ikJwkU?|*0-*@#;`U?KN2ocwSvNd zEKOo^m`a-sYpeZ#^8)xH0eg=(b*!Gf%_>#C4z#v)7|eaKocokeK9@=2zITHWITxXh zY5+4sg~=+>{|0Fquwa^AIh(N%T}kB;#{-rJLJl_8s!Dxv$`HpfODtMlh5Pkz2ByeY zlHdP)R*5?F>S>5qRBZu@mU7w95tC>B1k}X5o69i`z+$EBm$(06^veiaSJj}+zWME^ zs$Sa-M*@=Hbg}+=FGbZNzf#7H_MJz32zQta_02$b)K-HS=x{G0HXDu@Os7i5e5#GM zpx)|Ys6$3(b%SyX&vt(>SCNAdD~0z)o*Nqj;GtHdZd4J7lx`QV(mF4ZN-Q0>#rw_% z7R^MIxC4#I3S};U$_ZJ?38>|@0Y8`N_H*=B)6CE&fDlr7^1*WvVr6mbXql@pE{m~T zm(~FiX zh|B2A3e(|q{Pd?GhlFH=o&KIho8ecUCJYxBlqVmt|zBrD6)c|eVqnf6g&0M zC-~qv)AL#sdKk)x%H{y8SPG%-a_(uG6M2(fo?`mR=ZI7RJQ?Ouc#Ty0)!@rjePFRcSX?pF{k9e)%Tt2rpW#2CE>&*VE}7H$>_7am z7o>|TSJBw+$L_ep5lkk`vKa#@LhRTl}?^5 zkG>(S?|+;rtb+5@z18TfuG7}B!*UbIJ8l7|c1j8ON_s1o)6l5uGU9ww5n6 z2d%nESYTD6-uAD_UTln#1JL)&?0TS-=nPuOft?}3W#pYb54X34 zdlmd{QSvw~)b&}qbm2a}#m>E~8>rJfE;~Z8u8?wzKy$EcnWczDG|JKVpJ(f1nNX9u zo+p3Ejx^S5*;Tk26|KN)=+KkU7p}B7$&46d(q6KpPPYb48uhxMw^%%T6KL2gD#%Yv zOgQs~PNYd1L!I9p2iy%YT*Gi^oBqT!p6px=Oe3tU!5;!5ALSykV*1-5+f-3}SlpphHDzf&y`x#!PWj~gQ%Fqq(d@C}E&M7snc4Tczz&V4x84qd{i{Zy<)_fj z;Hx~6lvz;#cU&mWXw2K247SFR!9j=2!s?0XDbn)yDQ0T#vE>(O+Pvo19ogC$Xp&i! zve`@`*9!k7l0YzE=EXykLD&zdV=?%6P(lQM{G{BN+sTX>5fjZ~J>Z5&7PK(n(ETBC zy{gvHLR|ZJt`Df1Gxe+rjYvstB>V*h&P|tPqZCaQ-=(|tBRnW5mdc>kcFxjUI{#>c?tumGA z%oN1=AH3&qoaH&UGfOjL6J81q3OTlTj0Ov|Yjn;t$}fQ=;@A45AYkY;bR3!#4XUXuAsi}Y5pd^xPi6VCv+DyXs{?n&I*8M!fi{UO5lz-FM&dAH zJbai7*lVxdYDl6vg32p({0h~!bfLk}^|zY%Bk%Jx^NU8QmgK96TIp)U{9`a@gkRyz zWoE`7fVyppwUIDrp2c)Ui-H)-HrM#J`4?@e|nZ7c4=xNZKk~+VSGXzZt?< z97PNZBMryKm_nUjK3Dfc+zxmLk)4Yo%~f_(nO%BEzf-xeqgv>L zBVFJ2^qyZYxZ{+yA{hm78~HGE zz7IEjajFgSRId)gNvr*VOKG#U4})`Gm}{8O3Y7UD==cWn^uocKn&7JS6Lf3@KlSnu zAdX6a-CPae^-xu19!IAa?t{@V#SfrR3uh;Cz- zHx)TVD1e!7`5c-*Sz&j4AdU+93FJyBdr>8erFj=g5|e#@9{oliL0y7w`4!ba`md#? zYQIMjgey$_)e3h=88pxPC0K(Fi!Wp_>6Z2`0wOdkej4Z#LTy8f0BOVjCCCBM?Wilk z^H}AzMT-9k0{v&Vi>q7l`c}8ZCY%so*JRZ;yql>{#uXhpf(rS5T}hjS*IIx(Bz)!!;zRQBLNG+E-jQ_Xf=j1GeaYI-dQf(#Rgw8Q?Tcx zE{I0>i{buz00{?@N+tbIL)aq(V?*&Oh2tLs##v$dR%B{FyqtXllRVf@vEsDT?P)_t zrbI0zHU$^so*GH!vQ$ba#k9@mJ&m^lUWMyYptJ) zGKi6pMSB$$jZ{24U{rMh9s!bT?i1_-BF_enU|lN!@rzZZ0%h`b=djn_tV#s@C@WyN zq4a8%?amy_y!g8Co5Y3Mp@iLZT+eKWoo-Ua$$2bx{F)}B9SgHO6uKziC$N>Xb5Y1{ zkX0R6ZI;+7%O&WxDCjxc>-7~r38-yhM0{S}pZH5RJ`m<(eWjREv^S&+qY`z?IVIy28qhsXX|ZsjxYdc2BTCJhDpv#G!6_F(3B z|Il@nIOlOR!e@?sG^kt_K7YtBmBlv|p4~@rW^|GNA%?Sfi0NF(hVA{%9lvLdWmwP8 zS0_VSKUFFM6&?RVm6mELN)J5aHDJf&U7J;a_W9X)PTp%#CUo?T~TnyRl@` zY3o{gXH+_kqL>wcZ*1uy?Rn@;sj)N_WQt7PkL+5{NxFY}z1O?vk*r(?J({x*3$t$; z!2yDoP|;99YF2UswWVEv2ISYw96ei8Ko=3tPb9yi?c_gKtJmluJ=8Ds39DcRk=yxR zS9|&UR;Ce_3yyC&Kp@jNSh=mm3D5m`2p4) zNm^8;vl8jucKO8R36m+5pZf21iYAb(<%>&@JnxBYGCd3A+wsk8SP=?ITa3#In?mrv zvAF=YbDHCt9`O#K)AKiU>0eJLRlhQn`^YFDLM!d{ac8sRp+mYeDX~Y_CQ^9rAFi1bF$_fUNT71aRb$*2Dsk{yvNb5 zGFHIBhOU|Qh?{gA=sO3@NqzG8Qnud1R3Nz+WLGL}XlbXF?6==S58JTz@J-v`eg6Kp zGMTEtT=6jqr9yOc;Aq!n_ z*Rarlu^7H3Z326K9e`g-uYidhNehuAA-J@@=JVb4TTA0yo&<2HnG9qNHCT7qTu+-N z%o_uAPvj%WEVJyN{eLBdy3}g+hiG+fQIQA%$%%g`mA?S6$H=2!3VZIoY4TH>S;pk* z<26QdCYhMWYDV(n1x8A(EwHvPrqh<1tUYqzHI^ZE1xrILPTBR@&@?_2H``R(V1%cN z1Fh*+#E&rN9eHBKiVvc_VOc_x7eG%3%FhB;MR`~70}ejjQ_y+gFeo3Wb*Z0CPqu#I zw-cz;u@opZV?j)43kI{di7`d>lLbG1$DBzfC8Wfrjd_}i-Uy zaegROmLm=r>^{k^?+0B zJxI!o367h9ojwfII2FjQzTU1&x(YW@(i?(tDV%*;b=`+!vMgGIW&Ca<$r+=qQ_+1a z*D*VmYK=l>1Rd(z9})Q@{=C*9!SGMF?j|%NkSl*n?$-k8X{+|zbLL57a6dyOua)PL zw+~1bJS5_s-3CC1ubLMs445!9vR;Mt&uMdIliFfK+k2_VMH-A5))tzw z`(CI#j*^kq=bTWf`uQ8zz!t6a4=t#+)kz0k;?1uO3|P$aEcgIFAiQcH;=XiI7`G}N z{dc`^h-Oxg?b5e8d3D!_+yfalL~KWYr=5(qjz^@`zFuS_dqv^hwW|}m8jIT3sNXb%;^bv3Up^Z3NesESE zzW+!fNHah>Rp`p1XLW7%XxX>Z2K8u4aLH=}fBUl*dXmv2wVzx#l1z?q9*5i8x$@@* zl?q~QvI|DhC!U7^LRQX$=nZ08=TsSJYEA9|L!DXxJtvQ5s3HE80Ny7uG^quM`BXAP zl5tb?sgCmGa$9}IX@7Hs20$(`x{Os+8c)-2NKNYUGL`?(+))1;nb7H3 z0&;_}$}uIK;nrj$%jtA%HdxX@A3rlDvy@0a!$Uez{`4zTUaKl0`{sU~w@Ak87B@Fca zE0R!&E0Q!rS;MlIBNryE8V<4hM#~B3n*ppRX_tFe>6*hZswd~CF}Rq}R2Y9{MeM6? z5)o{1i}$I3{bK8fRS_ea%Nd=V8Oo(vNd7`6#dtQ)sS`C}QM$C32*2tMSR-f~_jq9j zq}SynFU*CY41`zxCO08qfTIgMk|<`43HEw2zAJQmTngmBr|?b?h=F#^0WW!9?wp(* z?2e8^ex=-GbhLS#EHW%e@}9+7CrEH{ffTFUqAE9h!m&`?)_*4S?{lJum%i%i_m+kudWeoDRmHzw)bUIfjy9R6iarnS&}D`9W>!uQugY5rJAsYyeoH6cc) zDW~blH!~cfu<0b9jZ)gePAPE(nj!bM(C4RC zM*0);oXl67myb}9v?OrtK)=0{z6$o5$DcFQvssW<22KJg_ZYGu{P6%x0%xdwj)fy0 zxR;nhm&<`l?+1*p2aKrM2lS-3y_Qh-fW+Nw2Y(++cek40sl_iy!Tbye$ z(5yjzzF5Uccy$MU?(3n|N~0Z1Y!rDWp~+81XbDoi*R~+;#LKmZs?7R*XqRP>P}zKp zy&M>Vtx(Run6`Hf{nEc22e901k`;8X(5~unpfrYCrkd)Zvy>`22G;0(jvFTtTw&Vs zt6cttZoRy|Z6ucXMfT!0WIX(@82g`3bAJy7I;jV&k?n4_z%k8;^T8nhtsjr`vZmhU zur!ifjVDN?GV(Zt&F)JgRSzADiGzAl0C5jjC`i*LL^F{e7OU26ISI0%|E;D`4+g2R zmzR(D-$M|hXLlcwPNgzgqt5Zp!wasb_ku=eU!(kk=F+aONI4c#&vdKbfAq#aIh6C% zO3o=FAUXZ}{tP%QW56US?3{cK-M@9#BX&q0vln}(P3oV84@{^*LcYIfThp5kKK>i7 zK^afOYs89je>t+TXWIj+fn;R0$9`Bh@nJ(V@e|sLWN-$Emy%2aOZrsREpc;nz1apf zg%Rbe1k7E8rXKyUED9W|tMZ$JR%A{}+K(3MiRviOIt@8KbuutARZqv5q%%HP77-?Hx5L60h$?=P#s$C*8fG9r}DoxD&|P{ z))+Y>Bp^y^JPx4}Xtdy*C$PLPvfzd<@5%9MDNtRqwFufqj41eGn zub-rv`&sNc*V3nG^%MKXx(EdPOopwLTE=bda8IK}R)s?6`fl5Ig3Z+q>A%Q6%*^x| z7RBdMp|QLz>`uRWnmNlag4Os{yQzknCKtzT;{wQ3XvQefR1a2&X2 zSDGVF{Sc){@Lh5!f2KVoWJhbRBfwrkxlj}0*ppL!662dz{rvsKD6JhmLmkbtKU#CTn&VA;abiXbY9m)Mlr8eNu z>`8!xDF~-aAKYU(<1=?yL@}$>{*Sas@Ibg$r{Ygd85$6LemmF$In91L$&L9euXdzP zb4BMy5qP8ZFeo$fu9ff%SaS^GI>lX4Ri=Qi0xlE`#;jtHVoqMlgZPP;2KF9 zNe+=(>^LX%%S+7j%mTbUkm6d^`wOMJNtCMy>?`#T0SK?};EKY<0q$w*I-YCcM+U;! zIxQ_z)znjVSB!=DLGSoUNn4xXKm=E%5=`+df-kfR0*(Ph&q28UrEl|jVxY(RPDQ~6 z>u{KB)|tCg_$#<=^J}&V`o9%0j|LgLFKpw$tv}vfc1-9}v~L7DFj`f=@t88xpQ|Z< zBcgU9W$be9<14sOhc3fy%clNS!>(5PVJUbAy@KR26YQ5>tNo?-l{Em^<*1lzSI@+* zmp)=B5<&IgNsoQzSUt;(e!lLQDZq~3=mLu?<=$=*2s;M9N1u5=#M_oFrchorb$jf1 zjp$WpsR!2mX`iRT40f>q86wrO2ly_Z>H$?zb9eZQDX;Bp1vu&?(Dnr6^R-YPdA|Q) z)v#%s5KxmoF#{7WT)Y-4a?;31Th>nLs~L0f)&jgcaSHchtJcA(p#%iN_499%?C!Q> z5#|4&yHPM%zoaW=W&#-ZzZP$z@qSxp9*X=rIs)}T5W9@Vj62j@R;KKSh8t>t91HYv z%p}uT2F02MyD`?*{SN^4BVjhQs3f}-F_T{md@Xn3FoO&{vF>2_k`=<`BsJJrvYQ7x z)r#=1$0YifLFlaOG)Ra&d;RhFy*O1Tkb=K+I-Tlc|AvNI;z$;u{eaafcOb-)QA#aK zpNh=PK;p~jxnPxT!NQbE zFsu>T576!EU^Wyo1MPd3ieOD=@DfqK+J>U6!H^eaaC15)f52^3xCbK3RMK!0Lv>Gw z?^2==5^NHnL)SKf*T_x}=IJhWT2A^^Tq=+7IElt+rrnzj#r5d`~M*R66UtaERT+>?cV`H+05uozgd~0MRUE_NH{7Q^rp)1^wzP-xwrkv3$ zg7K3O0x@b*wHk;{z6soz2wo!yQ7*YEW!Bg?HtQ65i_r_yQ(bUjZc#Q|`vP%tW%XIu zBWqE|7~idw_<5R$8&E%lqW2YYO6A@-4vm_A%BLr}bh!OD>z6Z99h5}0KJBuNT(8x{ zqesSc*|6_>pgun*u-GZn_cobZ3)<|tRwTHN=xd>DeF{>a?j`0kqsF(>WLDp3ad({= zx%rJ7GP@Yq^|RHScH%CJM{t86u<7~)ag@ce9})R~rXM@P ze+PrS?p|;bN%ld;b)ScBE5Ea|lpmCQ32&5E^@NbF^>j13zZU+bt88*oEa_lH;$*UQ z2$aH~Kx>SZw!(oKpoTIn$)=aEpXFn%3hD>%vgj{X!z5J(%6KRPjG&v0w@3Eb33>|X zQ4*{(1(QE4Wpn-aUHtQKyETaS4uRUs-yM0!N$WX0yXTP@l@ud&Wp-%hqdAyrKwSHD zAS&FF%;&BCAlHJD#(3;9R*lHyz^tZk&n509m@d9(`xCZwXZ!y7{?ZDC4#{^oSK^1! zm``$(X-G4@$TzD6nWv*^SryXX#WyK;Bmu&5L2`TdY+=le#I{;;_kV1i7!}D);0d?j`&108w-|&jyMp{oEsalm z=VQ`ozAu?HD{zt}oNLL=Yophiw3t`r9aA8~iUxQGy&q!94ZU4@41z+_4E{}bfa-cs z55+2=bUsS;QjIQ=>9O4v2LYF(z!}5l>`I|=e_VPPm3Ihlh9rM2K^o0sRZ68~mli(C zEuP;r==nrRH_liU3F$CZL|eG?GW{UCTa_}w8xL&c0?&QS z#G61Kl#W;MuP>8rU7aM*gKbbV?HIwglg3hN3@vZ{WgDQpr3)=9b1C5TxS96OWSdd+cl?^Lv- z+<0%tVwV2$!9F(2WY0zbjmm&B?Hw33jL*Xs7b}^ClOtrh!lq#(T8Xdvj?0)x2`W)85>ZyBB#~BCA zc*oGlCh4EN72>}Q;-B}1&1y$f8&I36+k;YZB94Ty#vUgby`agI!7k%bl<#JldNr^Q zu>h;A^wMQ)kh`q9qpSmhMlUSm%S;$ftVtCS+bND?dR?fKVOD%x>f^c(z$gOwZ0c>g zDA=1YX&#}=P{oT5i9C{%)WolxKa}$-b&*2UQxavRl}Wg0@C?t7n_HrSff{fHZyOZ? zsM>Tcjh1qM3uwp<`R5tyw{&Spc;_g&+f!tz5FCWY9q?WVJ0-oJ2eKght1`S3`PReV z^w>o&77(rQJmSXoSK>(D3eYBg$06*Sdx0J@ZNFWxe`oY>k>hHWWE*M2qd znzOV-=@s|k1lWf%a_P<8w8?w#YqnpqA7oY^eqpltbB&N7^;vd!?#j5HbkT#Gp8@G# zSAxR>MR+NyiU>}~VphEKn0$O)Eyd2NSa{g|Em|X(|6)ETE}*1NBec~9FMg_E4NARH z7x`c735KN!%?Q^7RG+2CJrwUJz*>U2Y%2O$*8NH930(FdpVF9?T3**TBzs^*=Go*A z{H*-{iezV;Fj=GiS(F<@^4p_bZKC9C-1yFQMX6gl^Q^<<1U|G4mWC-?A8iD)bwUO0 zhff4cK8BGGT+X8y^sW80?m@e|SaD_9Zzsf1PIUiTGW|Jf#xSnRn<#O6H8}PzIpGLw z#n04euLWL7s%+z1l;bDcHT;W12qU|pX{QcPM5|CBkQ=AG3%t`Bv`Z^Wma$*DE zr7`Z4&cu`l<4_GD#K5a&I2bw+RwRKLDcX0I11tOveQ_j>OrZw?ERmQcLsxnB)N45} zkuomrzw9gR750|%WNe6*!T1W|BEQ_1$pzbHc$3K}>fIXTRQ%tRqJgj4f&jge831PJ zW6TMG1Zb&@%hMKs=GpMxVw4%G4%3eRHEd;Ht)R6_1?Yn8QDvdMu6C>GY}nT;GM>yMTP2bR*702jW<=f6A0z zI0nWHZ+{f?5hf@1U_M`t7<-;fM(7d2SYa`||o1AAXcXjpi>X*#6k`JS45f6;18yU8l7Fp9R1SwnS50 z*{m?NHs}=p=U4xCs*S_L6y4A=I1qDF8up(hee?h2*6mXvatYwc%t#z#Mgdr7Jb*|0 z)WLbL{gqVzDq?~@xE@ZEZr%~8s@Nr4;ZJ$kH?cK8k7p&H<^ac-BI|H5Oa!f@oJI5H zf-==SsFiXVXDJXPf@FNm`uuMNpB1|t60^6_7c)YKTWt?nkRT!By z@I}QTsRj|l6wIUM4z@XxuYQt7Ej4?A#A-U2-vh=gpyQg%5pC| ztwuix#mS8r7>$bZtcFp_J_jM-WiA)j77xf5(kE{RA9jIDNS&6hsq1h-txHFV!j-Zz zM&unh^oo^@r!~T0Vi%DiaYSW-GLU5YLB4RkaH8a&c*dcj-#f=kJ?1rJaZg3AwOD}5 z=wI3I>m4Y3y8cF_SB?(!T4&}s0=y2Wc~;KP%sRr39d6Pk$;9yGZ+a8>Q0?VWL6jjj zeH}v*U#47<2#B^0x~X6_&)rCBG&n`OoZu((2~p_1)kEd7{%eiSqMg zlLS<)mLI#TqkNw0Wgk`uJg2fRFnv#H>dc++T)S&|CVB7AIr{im8%c`cYOf)er*Yb+ z!UP00RWel?%UO}((E&1_I(@%X0iy{s1(sKcO+h?G%KdAlbVB0uB>xVVc{z(WIf0hX zg%cfpMR3?(m~IBf=!)_12Zvbgvhata7shq4+DW#W@R;)jpU+PyT$p$eTo8xj6I1<_ z(zPe^mV~sJiB;KC`R5@xRLlP~V&or{H*O&}rL28VsATenUBd}05X`tGy496qYUVgF z>N*^#ludkF5B#=baM4_4cAAP$Ht!3$rM*Gw7>c-9ukRz>paw}+ovQE3QBU7*r3od; zY|~Y1Xx1TWhVf1wHo-f@`R$2<>XpQqJE6oh9-F^plx7g;&8hp3AT_fAeyTs+1+I`r z)@A)8;6fgvvX|W=__c-W2OQZKjkR_)PPUO!87#T?bdkl3WWBlo~@s zP(=W)4R#GKOv3@^(XYrB4S&*X+A$`K#cQL1{@ESD52rPUYNeLI%0T_t)+F)UX=WI3 z&IV!hUfQ&b714d^Vo{v;=h z&m~EW9x|48QSJqeS1=`x zYsP7wCwc>uP*-@Eb2~$X&Sfco!|kxA4p&mpCOAdqEN=*tf;gpM)I_Xy2*_1`s({X#~DP4G@ zQ;!C)Aaf&IM8blGm*-^LvfViJJ_0qRAMI%)@<^0bjJ?y*%@Vf-d&i5BLv>wvI_8Fs zOPm4@r5-UPcY6w`3OKn}S=V(a2`^WbE{ORowPM6i+tX-`k4$4mmo^-75+g31rO?eP9g7e2#Zl=q3(Ml*CGk_s zUqf#Lir((VqK46Hs|RFqBk%}xVWLbk?Ekr!m0)<5e&<|mwdN-=8wiq7u)h4Io10u+ zEV_@A|BK{ld#myM2v+|bItXqCruzP8CHg1qkYH|`W^X-hePbns&!YclGe{ZO&$uek~S`Xee|rG5%57OC*;ZMl}(8}x}5wkYwue<6=jSQVOZH|u>jt| zhvk((|K4OB`v7zSRuvMjB}+N=%7)pILus0hMhm6vmzAYemo|$Fx3(h!@1&*XJfb6( z2c6ZMxY8TC)uvAyX+Rt4+L>vGL%AUF8R5bJq^}USACEnn;6cR*wf^eCPP3iT*GqxN zzQ@wrE!P=tueu3`gyk#iRmLl;RkCml#Z(7g-&?6{^+osaDMDmdkdkNY+oFJD(iZP~ z4L>53Qx5xW`l<6R#8|#4i}(*rf#7?xgv!y}YU>;~I{lA%oyF*;Fqua5Gdp-r4Z`H? zBrOuUpo-V)pm!@*UIXFdj4$_V%Rjh=c{Fs$gx39}{_d|MV`b~Y0qoRzSIn~nB}y$A zufxXZ@1+(3mWI4%nchA$rtK(2e5P#-J-u{IOekw1;V5KT^&MTpc&gLh znPh%)g3GSr+M@1r^Zl|zyEXm(fSb0y9>d4q?f3aGo@rd+xT%#S5x}%Mj1&|2mJ3sk zwY{&J1Vt|N|E@G?fPw1navLei=>91q=y_3dFyj;1{oLYh*`ea+wM+(f$w!~VN1pcW zg*5uu^t4I*pWsM>+23vD`H9Ta;4Df?{|`;=0p!mrm!6=dMSa#FCrL#^qQtZ7-ioD) zx=XrcM9JutF3hAeXvwn!2+<#&8#6Dz&_q5(AY2PxmX(N~e)A5dJf+l=%7KgY%I)2a zH6ipzp>3HN#3ZKQmW3yFU_RZjSaK|+OOHNpP!!xqhztrhm3 z74C&uxy~ ze-mv2G9`@tv`-K=Hgh0D=h;#?Ov@s|rxwmB-d`zYi(8)AS5!9)ORdFIFM)ZU4+V}6@cA~jd8;IOm> zr|C%y_vO*&W|+n_N^U$IVfN_6X@qS%qlsZ(LTqYi4+g233H*DdQIp}Y+7Q4U5=I3i zMg@OvGBS$s6zWko?1=pn^(pJ(4tf?b16>k&;FQ0iDWACP{c!evaA z;+D|0VNctCZ+f#Vo@1fM4v3zP!Ri@xHHYoS`JB+1m=SS`INrpHB^R>CS_av*$#Odt2nVs&6fo&ioWz5h2Q97++Z?AJKoBFsb@9$4Pk{)eIx z`p4|~TGt#mR{^I!fyX4`N+4Y1icK3fxl7ELhxVXx*5znT9e?jh%%aIBQ&}L`rI5Z!qvKFROT_uQ^Uk zVWrjCcHXkbVs_VP z6XuEW;Bt0iL(;)J%1P|o3J`&&dkN+R@DnrjI#z0-6%-&gF`1G{?AEF3tL>945-NTi zb-hIxN`~tC*r;Y2Zg!XJJgC7^v0ijH*f%QiYQ6vDr^16PnvYNT!YU{1X1Z(Ng=8#% zCZ?=Q1j;(%ajy@4)UsRO2wVrA|ttjA3_b&nFoeLp+o``Lat zdm@hmI*IMT~!$TL7&gY?0P-rYb?Ybs_dM{(ZL)!(mf#79~2Q>;Z zLbahpWZ7h~%vAS5omUjSt7;WkiR_g^FTSUIQXbcz*@&6e;PNd9oLgYcIhaBhtDbcO zZb0&!TQz!zns(8$2wa~Zy(nd-N&GG}V`xckA~W?#~RAT+O{jMQ%g>T*-?=%PEc&FbgeM zcx~oey1iN}6!VlSCbC24q~SZ&Xh@48p1&aN5!O~MJ~=Ke+(JNtojJ23gnXz+n_;z3 zeqHc&i|7FDDVL6lLr;wRa2wD-VXRgR4p=Yf)c*)u?~CQMV_nmhGY;L9Hex+SVj-b0 z?FtgcB1t@+0s3Rp^{Uf4E=+DH7UT3BG!=wUeu?g}PU_)|=HL_XARIT}%!5cBEn zAO_vfgUidKFIbh1=ZWVtU^Hfh&P-mlw~@A@Ky(^#A>xON^jocWi>2{{WgU&ilapN)a~3*@V|w=o zrJ3813ziiw5}2Wnf!TQY;9GM!8jZGV1{KEN>>Pw;^vXL(V3Gfjy+sfv{W>EM ziZ#98-(^6r_~^-T;dJ1~7Lz1Iip?$I)Vz~zkPMvzt<1nE!mkfM@`T1U7?UlckK+1b zy~rQ${c*G%@eY#dHa?Hb8r1}dJ84U;n?hQLVB)|_kCUK7%(C*&o7g!lCd*JnxJ^VNFaO<+n#h-$W`4x^K9#jyR5WsZef>mVR8ioubI&Uu3qe-s_r7K!%jBxO{qeVm1~UAXX1HBCS;TL*P?1Rv}x=lXOQX^lVN!2CMB%{ zhmGl@i0JW!^DVql(Gr2$tURJ*5A!i#5LJaG`ep7}<8w>INAK<$mjOo_&YTC#Eiv9x zV+$H@2T~a`Dh&BO&0b4>S~efq!^V5XN(BGAz6K;SLdQ>boUjpJD+(@iQ>lxfFW-oa zWQMt`|0!zEq`^He-VpNEbFFDP1!qO61PIznaec{9&E8dh`+Mj~-2~te)GB-sYx#N1 z!cxRNNu^7=X{69@yX2FL({P(XLKaJ+N2!s)Xrq|Q=WkrfA#RBHt+6-1^Xo-eOi#L5 z`+FWp{c5^Q7<=E7)C0en6wAfj852$}MW7QBiYjs9-rO#DY(-O971vmk=bd75wKl^= z63C@vzP4={HbIlcV8^~(kAkyjYx+>QT$<_fIUKl^%O>D4kMA@1*rq&tNoc*wP_G5a z$-lRqqn(($E^L>OS&l^LkM>EDZHd2FL$H1uFPC`Vqgl7RSXIA$R*g!v5iZ8DHNv+j zC3Xo>x+&HMW7`n&E6wSQcsiYXn{DUx*(=)U%dL}a7Ss%W4n*K>7W^l-b3R016iM5AjOC?O7q<_$JzrL;qHj*8C!`EpIi#)#H=1Xa4%e zc`X+&z}+Ramyx%`%B!95%8O*8Z?y!h@N9kvljxulTO>)ljZn7_t$t7(QCe={gpn^0 z^|91a^-5Oy<14vD*x>7vFXE+|8gw#E4m?=x|M387)pT8(fg>+%{(Q_)4H{zviV7HypJ z`dHNZAE}1j3(vJ8;8Li9 zrj%Y5IRJeci}%_hcqcVUG}J^eVvo5ZfIrX~ljrFdZm1cy6mInVkb|L#3boy&;Zt41 zn7xVd^I8@3E_y}eos#Jlwuyx@-3Ss~CuEe`0=y>O9g{9;W66*|VEpW5PEVX>z$jNk z_nOWY|7Ma$kRe@^Q6bR--4-(q6k+K;gq{ts7Tfv5{ZDViUyI=;WEk~~M)%MChsx>U z*gHdJpo(=}YmmEU$af-deqI(+Au;;;PlhH!#NKiFhg$molnrZ0f42wMsV65l4@>#a zqc1+hE3ha|LfJ#M%b~St*-qEz+U%?jUk3 z{<95urW%XaDse_)$gdww4QL;-mM412RjV zQE%)m9m6@h*&<;k#V78=ON)qv^Xa_OZ@+J-w6fCb~Xnlkwsv zlml`;)VZ1EaNR(|AU@!Wl!%A71x|+a_00U@$ipy%{F0O}dSHNMcTc@NTi*qHntH}R zkHcTyAqgXv*2YX*fc7C=+8I9A0Cz#J(&zMj(h7++Z5*hz+JPdyM0BU>d*TkhHr7lK z#20Jqoe>5Ar`MV8xR{^EVmf~8bZSM(S$sjd44kHQPPb~rlbVHq4S?K-Yu%H2F&)mk zc}o&9U)SllD-y%U)_q_}4&{QW@%n4~v@6*4(6{7H3L{*@ zs^FvneY_$q{9PB0x(nhc=UX-C3*>BiUZa`9QgdO822k&icXMpSnEH(?n>k^PpvQe+ z&H=O=$Ip{%bPo^0|JsYb)G{&Oeih@kHptuA8)6K1@CiRgYD_uZoe85uO92nZ-6N9r zpZXn7!VKz)cNf<0nW5*nK-TH`{ZY##qav`~oFKWE1^|-idFk&pk2y!lW(iNc{GUYt zY+?TpMyQN%L8>zGP?s`cjA~A!arJ$h>wEFIP$Qk~7jj1$o z>?(%b!CiONmz?<~;ZIMlH4f4GNh@iQ0v>_qPT_v9D zU_CN7LpoR&NT+Ap7M`DEO-#YkI}>0HT8#cFuz-c@>4^EWLt&uxin$(M1^lhSV>Sq& zigSKF%m?uuhI}$g#g`~k9R#pRH6J?ecN;_Z0h4uCrt{AXfW}Ee*L4VN?0NsAa&W?& z;|HXLVMXS1h+SDT-gqb&R`n{Ck|X6QY1jsd?n;w6;-L8c{U7vDw|`reL`j);PUC2S zFk8>}mM5ZTEJ|@Mdg1IfTUBGsx&Uf?7fn*}TyZSK7b%neSF>r8Crmvy?P1gk3Vh{v z6c6)J1p6@-iGd%e0+9m7st13#<-g4vi7~ByD+YDPkfy`$9W}$~2s^3}o8vK{bD~8q zp_3RHjX!rX`y+9VBu9l$1Yp#mwNP5^-MNShSWS` zyRE)eE<%IE4PonIR0tkA`r<<;<$TDp>K{JITQM?_2OpI5&7birDH8>QjOe9gvguSw)4eRzUL-H^4ZZV@67ZcD%RQJ-dhSD^>DAVV#b`lDByk z-9H>KcxQ-0wJ4H=0e}xevcR1cPQQ-Uz@jno2g42ymM=V~vp*P~rlKS)0aF-k|80sR zDz|Rg;CJ_J4~zk+$a&0RXbHq1dpAYa0ZJa3XvVWL$7Bp9Jt8QGCwXodanM4kYwV?9 zrGV)*Id}d_aMyK^V^DXX*SN&`2j*VwG^xPtjbcX^+0gCW+=6VfW-`3&SoFJQwjinZ z*|Zqo8|sC|38cBI$LM=^-X*KNZ5!j8*)*vi3m%|f;16Y{Zh@J&M2)4*dFibspTJZX zxEW0%4e@nGZ{+iguy9aw`XFMHB;wsH@|(wPBi_0xNs}ALs%n-r*Y(V#h_yCI2|h)E z8!saw)^-^WZ8~^FfMxtJ8bFOboJFW!r0-+auGY7#d+*Lv?TzDSZ#*OF8)RJ@cofz? z*)Q^^DG+l+$oMVYmnY78j*7i@+Ysa<@v=fa>}Uf^h_0&x1FP*!heWk{Ya zUXo0^uc1NQwmJIAhSx2@YRTarZx+}R5ynhDkaJ5lnIpWuN)h6<7ilCXyfl*NE|eCv zv87I}uq5mI#TTCWV@Qej=X2J|R8HUGwMd4rO9=75X(#{ku2~Y~MR@>S*P#W_Q4Vn> zvQnI>_8#)yH=fhUor;3c?6z2gV%A8x_J95-JHoF;%Tj1a7&XYHg+t8r*!c&>8ufY9 zp{hCOXFEAkbR0O`)cQns{IC4&ljMs=#d70hN29^PR=dW=Y-H&Kr`pbY1L(iKgS!UmImP7m+#N+kHr6nWNO7_Ggi?B822^ z6ZKc)2;J~>Y9?Y1P^l0Gj)!HUaw=)dz|nUSoZr5>D98UIil1C%OP;8U$FN8@09j$B zePAeDG%!GXADg2>j{lU*x>lH^vgGPQLIBeuHUtxJXP>kxQMAUBPy2>vSEjAo*hP_Tc19X z*4sl>g7c7IpSb3i@3!P1tQ?rrQd0`wi(XHFiwnq685EnyC-)hamUk+L%M$5j#Lc@Q zF9W?E_{<*?kmAq7^}Y~#-69`dVl5dXt}aG-QB4mUa@>EUv*L?Grr#3v=`xi)SWJZ* zPm~95|JCR>aI{j@Npki;`Dg<#UriAJN4nc;QE{XL7_m3Lc|S=<7MS0Is3$(@rC|={OW9W zg7)uU33E;yAg0xsovNHe^gG(97%BT2vXCiDo|luh$c`rnnXpY=<8%8k%dl-_{I5G- z1%W=TOJhx57?h9GkL-39P)FtxM8c>3D!B`?LYC7HbgMStFz@``I~?OR@gbEgTxsQj z?=q8|8HvSY4HtF8L;6-1;@&lkT7wH@Ixj}oA97=T622ezax?>>7xc&Tckb6{P4q0S zDc8$>Qv(bzF19@6NchI(c>A8}SPQ*JmPnDHS8Zh1phT6fhHf~^c|9LKNt2BR_=5P;ALkEja#9NNy=_#^v^D5{o6Fp`nSHL2#l5+ zB%3qxM_sA#3NTwk3$Ar}!e@dVSWIz+^O7+@!)isz3Z5<#xEbQSx#`k*+FR*ezz7Xk zH6zzgUQy86&`oGfb9%4JjBWo_RzDBfA%+lHY#r7$8(Yc!SquT%7(pymj+|0^w65oml zPO|HNn`9ivbh-AHbto;NrbW9Ny@s-lzN|+qqk_J+y*2f>NGdF$O6Vg0vt_{~iZ09} zVnL+n1E*w5;jiQp<3Goa0lFJ?{3wyEP#L;iW6qWo+ITfMLbK(63vu*+x;>9y?P$i` zoVi))ZoU`BuHTU9`UZb$7C;w7AV?h6W|Y?=n_vK2yK$9J&TYqcwbiM_qJ0I&ryQG@ zin1YrANSn zk*sI5ZiofKK+vJjE2Qkg9y^WEGPCvjnai^#hZ^yEIAyWhwIgGfK9d`b1wHTiOl{^p z$gfT3w5^@I8qqh$TT!q>hrgrjQ$Em`-(58Pz9hEUu>Rj=89z{kA#axcJ?d9QU~9Y7 z`{}8IU)4j|%wZz~EZMHtkp?b(Ry)r{Y6?lP*5#OwQ6oGB!T;kW;s=@#X7)+GT9VW5 zc5%~hAGqhPGxN+hX*h37jHUH+aMH{hk!4iBsp}QHc#5~xF}2u$cl!k9-;HZ7N7x5} z4nT&^cnmW#Z}EsB2e%~XPaX*$+i{-3b}A`pbt#mVer^=Ch>kM26{rN^50_rK_-V2n zd~?`XK``@8oql};$3x2Do1~GZR@yjO1_~Vnvpj;sc|jjuk`CDkfMu|n2_wxZ*-Iq?!M~cO(cBz-fzVS;5khBLhCeS7>m60P)}jv zSk$N9!n=LpITfEzI>_hIACxTMb|wAt9;F|CZufNYQ>QF*PuCxi1gLc=L!Hj?sRbD2 zm;PJ6HT*#M*qo80IvMq@@C2q|eaVYyonFcaEcAZ}K1AggbE4|#^O$b>FxyRo;f9$t z0)0#yX90>DhQi!L+)nIGd5T#WldvaO)6F7LN)HCY5|ZA%dsWkq&$lW1jmZaWvuD2+ z9#rlwK$m028ago3oIyrdxfg}(1y4v%+1aLmKcDUrQK-ujX^y%~O58wHfL73A_hmYg z0ejvE)`dpl*U2|e3~~glR-_vZWQnRg=G=qTf}Xe#dhxJ8$V>2Wp@vIUQ@MQl@PsPP zy5KO#$tA25-S_FimqToHVTzb4BI*G|nu00&Mncwf1DQsQoCxo^njS%$Q3W2CQ(Ph$ z@)V3%3bgCQoe)rkTq{HTKp|#Mv|NU+EIbgoFdIlFSU*u+49y8p-g+7#p4%Iz?jQRY z*VEaj5`6p9eO*^0&J9^+^A(jRMo3FW_g?_Fh6Tf~)`!g@@mW}mKH|q=+gtL_2hC$U z<)VHL+U%Dvax4EUo!#VMwnpG~)Yod~+`ywv%e%f|C3M@ZV0F@Zhz8FL(8ehy^eIPK zY&fh~)Y>I^RrDB$e(y3w17vNI?XEP|j_IE5O2RPjd@LJmdi@cF55y+Pwz3j5|4>%v z9LRsW9$aat41QnjI*bYK@P`3fm0obNRs(Gvd95w z=?>Epa4K)HMY?NalCNUErOw+O$)QC;yn`sSb4=!+-&6k6&LdF>5tueKqaf5!?O7f2m!sQsks+ zlaqb1fybw@i5yOwyvjEx`hIPe$|f*rL@_^7|7ARI!i1=Pnp~_ETJJ6qFMeF}=AygdO5ZBw;?b6-RTMVzM`e=%<*tR>C zDnBzbaa9zg^FE$*ORmXfc~OT&k}0#$ICY%8yAxvcX^_Q@$V!hRd|F}>;S9r9^0Z%f31&f_^)FECun6 zlV+QWy&@P4k1NQA92vZT^mg?1F1?ac(hty%9OFU zy&PnCU}f1CYW_`fSNcoG9kM~SNC#E*Yl`~4^#peRDKjc0n055&JDbioZanP`S&&dOY^QvhBk`7jAMZSx6osZ87|K4nu_0g3oSYP@Gg^t zqH8e3H!I&Zbp8PhEAgSWwRA4+w{W!OKy8f5!ZiNbCZmNw!(@vIEhMaEr3J^YPVFWL z;6qO_A|1rE^}MirS}-Z(ld$NaP_327A`5XK1nIaW*6k)~Al}no4Q?|<{>A1GKs=}( z&~w3?fW~bFKbX)Ux?|H!bxWR5_sN^~YlD3^mhqC%A6RKO*OUi-0L~gFp;(D6iq8=; zr-aI1d`ps~*^x$4bZ3W{oQ<`Chm$M4Q7%A}7(L^CNWF7I*fH;3frPM4%mhX781LG_ zFK36EVOt3hY{!&*h9Y~s;JK;OVxNrD=xa2{MtgsW>vzH=1myhDDRm>A<7Pft$|;B?C}yvC%3-{B4$=s^H!5b;kWxA!KX(E1#S&Sp%V~ew3$H|OE#Wkq;ke4hMALL+GS;|*En6Kytm=QVW@D=S*Jjnyq|fA8eF=j z2NG)y`k+Vkh$Uua58aP9w%WCoJl}p>lAhz*QXElWj=U2sXRNB@)#>{J)GCF1p+_N> z@>9!1EBcH@)Mc_lj^Il{k5Ln|MYCSM9?WLJkUBFjciIX~8TF`627lUQgbz0Q5X>Wc zX95gkhVJ-uvnCj?2Tp!*mP-9q_{E>)Ta4*oLzl_NAw>Pi6-i>P}CFq-a<1TUS+t7Qw$L zJ*+@BG=o?UgX*rjL9rIezqFK@oHWwMddF|&Z6}A zUuPf&7&qcmrtf?bokgZD%1h&V!}jpQ(D@uVL!dP0HKt)ZPt8VgS75pTYE&zkQ0PVTd*BghxjuET}S+vwu*~1LN~=v;(Ewim+(1W`jUH5R-v_vLc{qexzwut$jdS0mFFi;>Ja_D+9)TtXBk>C znlj&V-)x_Z=@IL6U6%18H-^ER87P+B5q*2uNv6@SVZRiNFhIcKZRXy2Y zH^x&d_g-(dlmhLo1nJkvB+T)_Jobq{Y6TOZ(Y(yjQs{!|XCH-mXMq=Ej z5k>nX4eIa|1Zp%kC25-2jO=MY@xTvjB!Ec-2xi?NTD%6oI%lc#tqrTOJ5m+#b-pd8 zkWWBu0X{)1Bfndg#Qs1PKn{$!ZXcQ54%LJ9ijqKVNK#y0eQ)i>|8BiCMHv1N(Y%~2 zT|p`kKOMRil78fd@rw8n;w~qC#8h zBTOMWX8)sQ6ai-dM9S;;sJXAJ=*Cj`wSpy#h^)YqJPirS2Iv){r3w~6n2pQK+q$0u zchlj~n1D&+b;6;(ku(h(w~}@G=cY5-Y)#R^kbc-kQX+{2LQhu}7IA3H+h7MjsjXg# zctP;%l!5?M_V+8t9h%V=X)l5nZGfOHvqZ4VxP*Yv7WC?%dZLxB@+NGfLHT*ZMUy|V z&X?pXiCbC6lt0?by~#7F_IT)j<_j*fA4sY)1bd9wZ~{n5k4Gy>^=eLrN4AXxpg)~} zMq=GpE0Oel9sv0z2Q2F;0ez|i9-8$P%n1}fdkDWL-Et4XE2n9-V~;nsvr`^Rj0CMN z+FMj*4Gy`=RY+de*JU5X9`sQe*&r|-K=GTl+Mt?G>33Obj z?hDPF*k<#YtL(e4{UEvP&igdgFUg-6us4^zvPj(j9})eRIub&l{(2^`%SSP9LY5}N zL$R()G7>3_9m6Pu>!My67;{RTGL0OI7y$QtLBC((Fl7u=vA9=1Fp9TSQ1P9ID1#1c znDM)2lwr-{@sS%krDofIkR4$1;R8%XGz_h__`_&D^y1ifna9t2ccf%<1*xA9z?n~~ z>0=4LE&!L1Y}S8A`YYt&nJau_fcd~q1>c5`8LN(ruB|nB(8`kIM<(ytkk8ttlM{h( z4a9^dO}H5;c||m;x?rkJ4ZsL-5lI`{L3vnkwF9G){$N!LEiWQu*B6WZ@Ku7CW_Wy4 zYZy(Kghq79>#j{kT%sc0c@q*q_TD|PHh{4PA5Xq`QgR^S@@7cJVuR5n%pD)9!|Uie zkRTMW@{tqHC%fS{xMrwn1Wqoqk#%)2=cYjyB( z!s@RmkeLW8Cf6jZa7r~M*HQ}X(;koVF|GF0q`b#HI7Xu>17i!5C(5301u2O&f@LnO ztvj9l3){bod6GcMV}%|iH5+=0LgOBlB8>k!fxuOJsuIG-)(=W0;$^btxq@&HeeXTg z8UjDA8Gmos`qN^3;H5aR)9GB&XLKZj3@*nH&++oi!Sa75j;B71Ive(Y%i5VLF6(xk z))D>{d8#4Ce1!~Zh5BF0$))on@1+scT@xd zlX(~qIaeyO^xCbYX=5jIXI-J<{7tQEtsCOE%Lv!A)Tig6VFv8-5MiAZ(|S0UsQ%0^ zbAcMA9+apol4^HgO(3HWYa-)?L6{x*Z!TA;og9RNq9&T3_8H;nvunDUkye*W%{!+@00##H(SRmhlT{N|^XI1R%gNt>WpmP>mNmqlq_p|45am2#Cq0YAH>S(|C^XhDn@)x5+f2 z`8&6|EN!_tExNti{WwDi*N;cEt)o7SV|FDpRhCJou?Ng50rIfmrq6f> z%OnF-e4EVPP*;RJa1};rmV#}`;*b)A@`Y`yhkW;o1~QWT94lwPGNsqL2M4XQ#r@YQ zLBE!|<@jFPDg$=b!p3vhTthU@)>xaWt&`WRT6!V)n`qEsDMe>lBeheYLYN^55b}pA zr}wB7g^)-~VC8Kz0pB&(!4DHPSQL3p_qg-0?lb6E zGGeP#$P8v-7t?lh*YzO>OAEo|wXg=z-!p{)Ip>8MAYNjFc7a<|y^a%RBp&KBK6zp< zz_CRa;(rtQ8UqeR5F<|xkn=M7#-+e7V~eJe9|mzi{4fYT)tuAu(D?x$IBl#;liW9X zJYvYSU2uT}n4Z57puCFYacE7ic-jJP@cxVAAn_>&+IvZV`I$s*ZZ<<>W983@eY=s- z+w5E5aY0zNuUh?&t02mpkv}KM6C)I!n4Xu?+q&@(x>Z(o#xrOMRBZa~2v}|naV?IxHX5)lev2r$gZZlL7awa!y$)i&QO7IhlJPa-u<0H+IZ|o1( zBfw}N2Ak_0!a_qb+`(GAHs}%cm@}}VDH^)V$EW?lCOHP46rH)r*mWE59ncuZA&ZAG z+xLWZeqSxM_9tJd z;`l;Z?2^Fet-<)OL>^aE&T~qz_4L;#N69YJj!;0u>AK;PkBC5B^H|ZMY%Hm(*5u%- zALXuYQ=_g>w?7+Nr@bc=v`D=Y4;~Jl+z;g!+#n~q?WYjzJujjW=c{Uiz)CHSZ_nF& z4P0up46JD~NnYlj6hGD;VHxJJLBH}VPd@}%?t|web1DC*+DJE_3=_LLMBQ@RfY0_c zg}SQEfQf=njA8OEpTamox2G^XqOOO% z8j5w^vUZmD;O)SJ0m!xuSVZt#;KbJuJZpfBxC~ooD`gk{t4Fo7u$` z*A;qy#{+4;^c~AHW6w>iW88oAF?az8puD7>vAhOs{V~zXN7$^xwsdnB#bc>Y*LM&J zBb3ytWd83gp@waRFxJ#=`-mc=XS=}2yzkt(`}&TIWeJeZe>4Gw$>EoqkI11iU!6|D zNBw!zPV(^aOR9}+D9Z9ut5m?$lu|?ep5UrYjB|s<>ek;$qGK0#w|=0 zS*z>nPJOyl*@G5_T?t!$HN@vxP-qD^!G{QV2LtgT_q zRNs@kL1Sx}o1>}ks{bq$J$2gYuy^t4n~+55_$>mYDDQq{=5I5L8Qp;Wg_N~QwWapS zn2q~AfBNXCHqjTprtW&>L?VzEgiBD&0Y@%x-5p;OoDX1hPxAtO>VQD=N=A8!&rAdC zK=sz@tp2P;^uLnzA_hWAHwipuH+vp^qG;m_aUI*FJ;tM|kdQF{e^~$@qF_&L)$XUg zM?zJGqyV)<6DOZ4um4$OCtPs#BRG*?j`-&%7v7)pFX94X1c2M#2G$M5@K80;Tk9k- zoOyIC9t7QT(vEIUP9f9@J-iut#|lYO*vVz6rcjvd(%*cr)>cc}GG@34{KN1E_7GXn zf8NFf1oR9j?h=8A#D;DBW4Q&N_uT1I?jz30z>U~ACZ~})E4fwF8Dt@n1P`MPBdQCn zq=+a4;p`RHN7WN=$^RgM@d0)a@^h!X5LzJ%X1Mi;5-Y_@|mF2G1gMgGp@kaHV!heGJN0_ZJ zaj4zvc~dd~b~j^bsW3S(Y^yL-SU`kow}+b=-+#;+Yo)EZEqH`Pz8dGBMLZ;hs)A(? zg1cxUIdiXr;PSe5BaEA!c(}PfbShbPtl@JbCwSw1|EQ~$&VazlfS?h#nLu;S2KCjQ zn1lJMwnk+>&nNReq43)aPS`TlM|#tY9w)7S=!xW9}CNDC1r<&&-gPq)l+cXP%96Vr(hbPC)sQe&kjI&%!uma(n& zE^-hLlWSgzF61t!y4`gHRO9xY2*^`7wX{6W2vBjkI6LK_&(ftdx5??8PvU1Lex|pq z0#I13(qP??P|6_o|6G^q2&qoQakPgYvgHmrYxq5R8cBvT5TA%8HIs%)*`yf$kLM!; zpCF=FW+l#Phh_{`!sA2GL}uUz=_+T7Hqp#g&o+|HCHgs$j~m|^=i7~DRs<@4#{f3RlqWxncuMtoU^ddi(OSDPMm=U^X0&U`QXXwn|H-iQp0040QM%u?IS@a* zX}&6~1$z+5(6`x8-8QEcIM<(p8=eEujGhaKBU_KJ$o!N=n~qKFhu3%`%!e80i1nFz z(cI>k%NM_^Fxf0|-V>|ur;bv8o57le1WRIT2yT&434t192=fW3H4qF?n~0D*RONd- z#kP!$rZOn=^E*BC*>q!>%G+Ng!~<{N1hgV^>ARJffDzQ{`JXI%C5KGdc8PiZ&-@%8 zI*su~`*fPm91!22YRNHz>zy0OglP-4*Id-gL2D~Cj@cwh2T=#ZHCy~wN&~-vdSfW( zs^v$k?gUv1)LH2g!qoLBYLnUXV6cM0^3%?;MLR|1<#r>dM*`(w1z(!6c4wxXFJ(SE zDd0XkYr3~V9+grR3`xkD5K?i%OBeW_>%nPUv4n6BR(;EPOu2hPbOA#&AU*Glt;i^N zl)+=eP51L=R!&h!J&PFe4O-vL{mJ}X-@>n|L3(V)Ztm{jKU-XIj82h2`r+tjqr&I;n<+Vv* z>Ty63KQL@@b(`ltN6k%ZB>J2`w1;9aRSp9jdx3)33oRQDw4v8yjRap3@VnFZY(#F~bfWS5T|m zHf>&<%LW&R17>`3*I-0Z8H)4`US3Jj<(K>gAV!k`_6fjU#q*ZSa|$zbrY^)cuVb0c z`l^)-#QrJv*(F?_nQ zF83Ex27n3Qi#Jgq=X@Mcv!;ZIHV-dQg;3%QYN$_nKfrDn{SP>7NtvPT$V;nw%N+9j zih0n2lMr<&b-*!l6Y?L}FhmZ#xM42#lnHjq2xG3xgMq z>~(tx%T~Q)co7$FgMyjAWX|CdZ+@?Rf)g4chtz@k23Q?PtZh1M9qh?Z(=&JgXl!iYSMw8&fsiyo^b+HB*@35d~S%>u^uww3qDiY^3; zi!L?EF%l-sCUR9Y7g4%=%gW2~piTk-}-XVvU zxhIfvtlW5G*36PupJYS<)nlsj7!MrGll7BwMwVaVRBo_fhxyN042A_iKfw^-*|||t zl>5^iO8Am7G2O?m{($V%d&H4M^;R1J3Utttj*f&lIs8Z6lEZ*9}2 zdf=g_*>~{8PR11>`n``;1YXHXM~_*g!!$aim4B2C*pj#UnXw7qUe+S7(tXz|CqA~y zulta#Gx-(7XqjZ~bwN1JXE$TV5kJ5Jb4@9bnUl%pWt?FNY}JVJRa=39iJ9#L+_mv= z@jk(0LzvCp=s9N;kX&R`AVNA_eD*<0IP}IC@x~ahO}LSFmh>>`tKDF)OG2hWRJ#8V zr2Yg$YajS54Ib8jJpI8(d*IHs^t={PkxG7fK^)VCXJrJfZOJY5V=bY4o8tTO3bHvD zjcR@V*q7$EDvpXSPK-=bF|W_W8y3^A`WRhNN2X!KGFwYx!p}KNL(5mYreaKmv8}dg zr`52*62Yp%lB94?->>;UG!@>tl2X?%@eCpDa6*tx=b` zK$d()`L8m{0dd$CzrK&N=UMPcEb!LFJXK_3QX0&hE#Tgsyvy*RKsqD&o0-~u4!kr~ zA0QLdJ;s}$1vm!1MuUm{{g;M!>A`&GIe*RP3NUi({zSTZ0aJ7K5z2?P2k(T2Pnd=i z%wXp3C~t?>Q#3kB&(Aoe;?l1+yvae3rnRSU2frulTz5TUX8%*IEA3&j6ci|4r zVKbxEE408wnJRnH7ncM!KBOLB;|6tQqXACEU$ozmnX*qZwP0U8LaZ}hACsj(m(jd>61U%S zc}i+2y1B*~roWdUY(YXM=a22DX8OI^Kq1g2`~&ZNzs0IeBn-cKko7odJwbQK5~Cx; zCsT&XaTTo!DNSKmb}1`*JN-)iKeL(#R@mha1_9I{Q1Lat4qCOAYNm%{UKpt}dmgt= zOD{W-hND*dedg3(S5RlX_$7Ab(((E{hbVO!yQLk$q5ZtHhU&ennV%pWl($hG`Z#r3 zD9;C5u&B@M?3Y)GF=OPTkiDQ%1|RP+CVMxeb;XNW81tCO4Lj_CAgu#i1EoSNUfeT| z$Qt~;8^#*fk_CZZ3f)Rwjha#o)oW5>-eIM}DW=pL(Aw~~T z;wUfGR?V|VgJ>Fh4tUbIaaqzQ=epOCrCju#IbCAcAxr|idgGVDu~~svl)<{uC9T5b z4=q{KAIvKKLo^BZtDhyRp3QDrKmJn2;9SK&+8lB|ejm~+#Bo;|EyxIP%m=RCZ&?QY z{G!&j5L8?Ffz!Vl9*kFsdVy9KN1zQOy2j4_BvZy^RZsm={;H|2))iN(q}P z79WRi*iOjuo3k!VtT@t5rCJi|q-F$PDT@)Mxmr{;z;VMdKSi84!@qpb|MPCenET{W z!WSO!=B0;U>W(ootx13?ot^n`8jn zx+Jrqwim0O4C$4}i6nRT^0r@p8?h$TV7AmsK%@>unk%zQ{=YU9&CWkZQ2KES&^bfO6bjr_zpCFXVw6miUk_kekMKZv+Tg?lP1tx;~9AR2k8J3IyqCM-+TG zO;hN*%Y7pRV9>cdyR&>36p~oMv^q^+g;iS{v1%V)va1t%Zv^sfrY`p#?2Ocm=$NY=DghwJIr-5vq}O;q4Z5GGbop++`<=vr8jD91Ch#`)Lt0 zi#5r_@UF=}LC7ThIH)!yWt&xxv;?BVZitMBE1peg! zFSEXgOq2#RQ-yI(rI2?(a{b4=Cg5fiA8lSH84VrJjl7*h`t3VrDG60irEJD^wC zTMDT^n42eqIbjO(YTeWOS?(K_f^?6!@aeK33-W1g1&Zp0>i#Y|`DQgvM-pSLjdf$%> zzU4;3!>KV4$N|aW3mo05d%zo{K^DeHtz?da_7@8mQFSi08pmSv;fc7F!Q9WU_ixGS zeuypsY$(bGhYZWV3Z9ji7}_ydsxPTq!qDx;qEJHl8Cx=s+gm`)7%yMG{~nx*yHWQM z(2=jMQv$nQS0cpI3O!i zA|}g0PYkct5-U@aF#La1y>oP3>-Rkzqm6B&v6IHOZQE*W+ji2Zv2EKnPtqif8|!!4 zd%ySdzW<-G$2j}h&%&H@&BaxTHoZswpmQ1Qeci;@RzXjOcuAYKascV0?}8LeEwPlZ z9_bHCOvlKSsJm{~o@T1Qm)cl6QNJzR3LU!Je~)=jm^5OTU?s9kD4q@}{N?PXGCvS( z)Ej~T;{7Z^ycjms_YXSxOzdmfG0n8ea^uWy~k#xN*JxfU9`C)V-qeqi?@tr#BF_Ol9R_}x$ z=DY@Qn``wyQEVw6+&op&CNXUxlIo(M$yfjePpUjh47D3D&*MvD&}+x>iJ+hTt+yMeEHHN1Hlj{3uY;39UKeM)O|k|qZ1(|UeD6rf1K#t^a?o`u zXKt@KA^+x&c_BNp18;(r3bI(K&KKBWo|vh0O?GpE^c!MVum;;{co|7rg!%+GEfh2g z3DTLtSnpZW+2`pfD(zVAkx693TzY4vD8%1gwPH(;G8wx+pG%1SG4m`)=y&jbCUY#Z z9*~DKq*nAi642MAXtjR9k7danAgPf%!V#YnY<&Pgz&=Zno+M9<{akeL6ZW1^=Mpn*L(XMuwtp;>og2(aW!6?)We82J{ z2(aLlAMYb>OQZ9cnhsUp)#$@kYJ+oMV)fP_i?ph*6oK{2)M=ZWjMTw#$PkFnDnKAL z^I|8l90)s^ebbPN@;=JZaa8;hT@eA8dzQuHn2f|4v3pPF2QB{t{0(qPu2OxL82z!6 z7PeT3luNJAGr9HAI6_Mf$8Lk1+=L^}jEd!i8J0P5lF|yK+gH=HmwzlqK?r;&u%Jy@ zIWJFaU}PmqDE|i^;~?3!B1wr;@jSwhkk9g8!!+=C#v_1ZaE%_bcG$zs0_&5gVACMM z24h1tEc6~G6i3eSpfz76?tun*6hy(K+k5G6bVeXNCLUyvjCCMI22$rE)m?Y_Ubd-zcOf7M6gJME zxAxcTIrsxHU(U>WSPvQcIo^qnoyOh>2|q?U?Y=XDV@T9t;W@MkhYrd|-m$*m=AvC| z%h4gW88s3i1vFbf{FN?%?WTD+=cYQ8-7?^_B&FlLV7tsgKwk`U#s6ErH7hGW_Lq^u0ivt5f5yMeT)>#7Q_NMHxt z{L*x{^u8WWpFD~$W&ouP>@KWkB_eF=k$fIWvg``A?o}213m=RM7$`?9a(>yuGiA6N zko&%!mMW~4nPOrkH@siVlOsbieUi+xz)8)ZgPX*Xu8Hb6s(gTxbWD3oe2R9Wl58y< zC?c(*RYbp#8W>2j2HO&QUQn-NT`-=d4uduh-`c_rO78VJ{H>{#;a0Hj8&<Q%MhB}KU`K|09)CIV70Us4UMMPOut7AZNJv1vzXryv`DJ zcaotQL1tELPeQyBT6*ZAk;@l`%~tAF$P6Zg4%$XLo$M%|Mk>#Nl}|?(D|`3-oG<;z z(Wj1*+NJp4wsfT-x&dYk><}p{wqqYg` zjMx(HrI=L#qT<^`F`cnPWSs(~MQ4Bs2YPzDMZs`f1nu(3QMzOAS`-PP4L1>?-L@?b z^hom$t|*r*t@?+s*sM5V*pE_if~20_!F206MGvn7dJk*e29*~(N`(f>h}23L+E@W& zMjIG^b+KoF7%R!x8aYKLt#A8gfALTY1>cEqeo0c&F1A@#(k8sZ?{Z8i|`E7d9ht$|JiSgBS7bGsLLDR#~TCKoQke~Tl7&&8KE4^bHn zXpSxCcVI2XAj1OfuMGRRE~|^jC)OvX@NDG|tSOejPp0TO%4xnNK8RV6$Ek5l*q`&8=>X=ss_v3^A6I+Zm+1jGGucU8tsgIL z4yrsEQj#Gv?Xi`QOy0g+GyQBBHKkNwX9B%(577tdziw!0FG7(fJaH~sQ$2Y##o0W| zN#ea=c7?)w0e2}8Oq`h5Y9wqAo{SYJw|tyI_PYVA$=;ZTjOEwUzf;@UIY0IDc9uK< zDkCdG{v9Fy0;VaP50hJoV4(CkH;jUv9WG6l%Blz3ptc8v0rx_Pogpz!Lqv}&1?Gfx zt<*A4os!yxDflbbpU0;*6);$(BT4WRVM4H+aQG`xot;gIlc;-&GB7GP?}~j$sf%>G`KV0)7ArE}B$fvXyz0~EuO$tV_c?!yXxutDC4abTTe=(% z{Gw$@ohS+wkkAt3nPV%W5H*-UVjO5zpcfYdtRJ?rl57{cEg9E?IMVN}7gmbVN8f)w z(xgrNP-gvn^Fc-`d)3taYZINY;`Noc+ zxjynF(_6!?K|;v==>KbQzBNX?fMTS4WRpbI_JqC}vBlS0@=@ZuQ<5%XhCrcRnTb{# zNiVYdk47zm3fuVY)nJ|As2!MU=Ra48h&jZ3*A%I&bR-Pm6YhMz1RKjm%Z#+kE#*x? z&T$p{I#gZ393MWWP_GN5pvlPHw>wzsy~M!3{c#8So5EH>ARc+=Ab5wss{lkwNz9>F zwmGDA@LxQfBg4tByE(p6?Ob97gw;}WYG7{Hdu$#?l?qcPe?O2>X+PqGIa37HI2hq+sKXip(e%30K9g>9`P~SP7P7I^#XYD+A&DGMMx_ zbr9>ZRvr%aFF?VjU_|SWzH(8}9L{e!9DDExmCSJfts}%i&9_`HQ=~ zL<@^EJ~wYFz;|Ud`5V0aIFMZ#eLpqg+*YHqU(=?#$r17USyF@l4jd zj>YAl-3Y)hAf6D$oypRjlE)JN2jYawPeuzB7U8&H&iE^=Gz(gC`~fD;IbQk>L8#{i z0yK^qP~|JQZ@van^nD~QNEZoI>eXbxDPtH9Q#V-{k#7FRmRRHwCgJAyqv^v8Co)3vXBU3jPW=>rfK0i5&9^R@qgOP~mug4$JHg*z_zh?Z0 zl{HLg>$L=maHj@w?e<}~9O?m6DSr)X`UePLbHU zFgBXIKhQN}W}{nSf+dHoG6$;s*&I%KQ0wNfxmEM-qP~}K|2zJF0|QzfiX-(_k0`PZ z$RvukAz?&<7Qfi?Tod#Y7k zox_l*iP~f<@0_Qvr_0Mkz1OhhFB63W%#j*~DSSN%#>HvvNtZ~;k^tYHh&~Q}h;kp> zS`{;dUDK?UNN17<`xg;LkWWD}y`o$ZQq$VR68UY8Fh#}~*G70Yx99N)o%kd9HE>4$5q5SzQl4pov*PtB$-J9oE5u9I&ZpO&-bibsSpa0F z3yq*Pn$R0`rWxE_&-B`{gN|rwMa(JC5YFVZ2{<%b!V=(?!TBUGp|=*tggV%g6Px9I z?qQquFH0UIpjoGWzZ7cXP%k9cue~>xWh|sBL|>!{Gal~(qojC=N~-@rHEn^tM>};g(fr;7 znWojfjgn3`71)H!7|#Bp$jOPrBzu__>kf$GzyvdtUOzsHPRw`Q;8IU zAAvjCVjext3u=iM)Tp7*r5ovOU_*=g3BfSGB5>4Vy-x4S^=LSgK(S*{E^#)`OWpA1 zIop$~^_GAHm%F&-vzgytp#I_(cCVLv&1$gI+7+qGW8VFmv%G``4d{B5v~3s^=oLZM zzc$q<%N3MqtEkoCjkuGrQ3~B9VB$+kmdMn|NvfTp>1q3D?JO?eZdYecZ7vgs7VJi} zw$1B6+C^W%pic=i#|^Qgt|4W=G1ci&p$0YT>t8wN@Ga`448IA!Sh$iG5`%56|KSxVV$*( zEGbil%7lOVK%gSo=2<<{6xLKz6g!{&L@)_8C(+VV9VWWdJ>pA0X(m>N@x7;*@#&sW z11A#eD`Z5}uQ@X8l+$vOS~4)KN}`(*!Pp%Cp@N7fktjhe@oJ(B9jqhJ!B?9v^g}jH zdV}MmUlXt^v%;GekjlRH=@9aS-<{{e1oTC;YPh4VK5ERGB9ad5t-l$Ms{CT~8Ry&c z)g~y-i`;u|!&)x47q(G--~VaT4%Ur^pL7`qFG(1Vome@ifri1SeB(Yr@Km?#qlTEZ ze>L)Kf4u?7(2|X!n@T~;y00BPs?}(qMzgo6! ziXi8*mfe0#Za4N=nSI1uSu&3wd{UnmWf95Gfx*kEIw_x{=kV-P>@=puyzzpF%512e z(DZu5?>&f%p~b)-T!1Xh|BZC31`zBy60#GBfR>UF7=+e)S^IN@+#Xq})pFRXNNZ9SVLxcqm+NYA@7 z3^}_KN#3=ldA~?9B4KI%n_LL+79%>OOZ3W=0&d5$KJ=DXDRCZ^rR#DM1kTB@oxO%^b6H$k8u`929LEEf9d_ypNxmSq zq)l)zq>X0Cg^G{nph-`Uw=lqsuX*9@mapb5X_R@ZKXRR&#*m0ve?sVSIt#Lv7uL}0 zsSwJX0KXC{y&P#{8CO*T5*Q9hv)pMx!8EBvL61a-aqAMOgf5nSoeGnSPvAol^O(NP5hihwFhU>u5F(^HxvXeUd zyzDm&D|k(z-V;G%+sJ@{IPhEL?>y4s)^(A+&&XGTb3NQT#67i_c2C7!BaE<6HEGjB zo$vci5Td}ju3g*-G?=>SF<6>uFFDDYemgd>XzsU@Smzr|ptG}bVG#3B1l9k(86L5G zEVVy6npN>G@Og`?8eD@|D?|~z#Ij{DeN#Q05($NEZ z+Qfh@`s`hqtU5d6GBfZd0{`HbgiN1kbwzL$2C-`i6!b$RKWTE4>wc|@dE9uE*5$(a z5`)~A>0-IXH*3+}d02C+&1+J3s%z+wZ4ZRk{^ctL@PjO{XNf42|?{JCV z1nDM+HTu7uPt@5n*?(p}F|#9%_wW&P`dzkmK?{e7aS?4>WngAj(<-vfY!At>i0MV)MVjsl8wRs3X^EW_a?o^ zEWzc)<%?l+C>C@b8+Gm-cN9XQv1IpnMhKImCQf4CAIYzsOpr@;th6vd;f795EDh3X zlf$9vQ})yhP|K!>lqPufs8}JWth9f9+wQYSunOg4rcE{16uiqnLzYPT@yn!Zq^-M0 zrPnqFC*)iEw!kS9x+L(0SnD$L6FOzu$}z(!-ZA}?SXRqN}4o? z{XTxmKL+c~^!qrUazkEUur^baE`zI4W z7X?0tM&e_Id>ktqE3Nl;l{_7S+)alH6)Q^0M5?DV%yyG+D8rvN6NJYZ2%qm=I;a5! z%gjG(Q$h_c|Fn1kn<4_|C%Q}a&EFH`_eglKC5yIG-I0Kcb9D~W7-^Nx8|)^^xYa^I zBDlX>#fL;`h~43df;{A27dcKbA<0TvA-JYMHZXHVcCt8dOkg!bxxW@P3v=(W*P5G~ z7g5T!xZa6HbTfzB0m|#Es-_6MR(I53-DV;0oklQuCEfm|Hch}m#cnHpLms-D;+x@O zju+HhJ#8DrW#i$;!*rq*UD*J+@Iw|bZ}Nzl5)&kmRk$5wHo7Q*svZqsFP$)FBGbL* zCd~C6IbvV0ZVOd>8jFGqXw1S<{rxkOBEW?x`%*~eRNH=H1E1Ws2bdFI8Sb~izt+=s zAapXGz@uyzcAk`o~0j~4HaTS8zUBt+*}%FRx3~q zZrxl(k4Z&SFZzKTX3SaA^`N??tK0oa^@>2EXeM~gd3z==MS#2<1TB74WE=YR_2SKZ z+Uw)v;S#7O0tT>EdKd*srk^gO;9@jO3~Ay<@dGI{Un8~3F7-Z7ob09lvc8fY)$Sm^ zv?gKv{B%gu1zcK9M&gXU5vu*v_}Uknzqy4Syj&-3Q2XyLc+)801|jm5Z|<^6&I8L5 zT>-@H@5`3{AEOi&#d`coPAODzU~_5OsZz4j45LF4t=$-WqhT9fPC|f)4258N z(XOyk(4Ps4-PJI%bhhrz>ZZ+fPtFQ!)3_c4RFNzb%iBw4p`(KDS&_ZNBnmO| z$MFl~;#N3CxZTRYw>v&G8l4>{gPU07#!g#U)*8GpR-b9xUH8S(Y6BTGR*adb(~KGR zE}=aV>d2iT7#34pNOpezel%A=LN5m^IZX`O6wB9)hIdlXp9}Q-XoWJ=1O`>H^$l>* z<%E@4X2sq$?-jPJ|L0f(&O<@v zl~Y<$^M?`WH6zIvIW_y!XQXe58N}(eCiqp`q(fjniBL zKM3I1M-waxvokZ|7xrM=FtQRHsOch}I16n>W%;09a_Qo~)%|(*m86 zc)MMn=(mp%BR(J4YmrBQ^ejGRU!z;`qT40*vhz2AV3M+6DVx_!M15~$x+hh4V*l&}+)4-YR51uI2o9NyV@C-3Eb_L{0)0qk7NY_wm|tJ!6fc8+6;ZOoN5H@-R|D%q>F?A$kWH2 ze!M>aUF2_nfi2w~k~}M`B%eNtwX@H?HHFfr`1JARn#|~Tbymqfzc8THrbLlfU16@; z1eAMp+Ic|N2;s(%ALWY6cDCIiGRuKpdp+TA*P0m;S`>R@rA{OWQ|cM~&ivccxTYI@ z<>=Ig5JyB$Bngpr6^1D=t1}T@vl7MZUhu;xd@{X{$gd^Sfg+7zo)t278=|C~gaH{l z4J^uunfwfLySIQ`;36kULphDL*+2c*-&3=JhLC7o^s)j_BD9QmyctH67&G(FC<=MN zmG93I`FJtXKKx#k)b4GwBS~qJH8}z>%i!@lTk6c4mc&TOZhGbk=dP}N*>bTfh|q^m z2?5dG42nR$n`M6JSfVqeYV@TGUsSW0;Mm2kF!X`>;&e7(i&`^|fb`?ULZZM_6FVZB z&HqPPS>J>Ne|8#iWA)YwlYkdszEOmE5Z`uJF0xTxXoEGCZ7a$I){9qSQ84{J=T5jk zs+B4Iu{cAY1i+ia|Vm)7v^g99ygnybA)=I#y#hp41V{HKH8!;gh?pict}_7YsEY0_WL z$m4+^RoyxqU7K*9SX)hj>V>^UQelDEiNC>om^tm4*^)~!f(DA}qRU}PR5%S=krPVOyDlp({o zw>a3+9Rb(K6)S*G1^Je8^D>|E!R`IQK2Q{l;IQP@vNEJ4Eqns%VA$K(&TaX_cd zuLRXPO0juDLtTjT^>xkyZ5!yv`BX-E9jHW#n61-uS&t>YpH>o(IB5rf*RRw}8Tdn0 zDIrXcwzGx^W(JC|9e%l@I7&Wz60B|B)%mnu25V6?JL`=ho++5r>O;Fv4 zP9(7ocIkqU1-69cfGlNX`P{YE7E4BK=5?*N_Y{dtAPKVjp?B??^NKaIDIRbt{h^YYCPA=aj$tC=x z6kZbTf-@c&JQzILJE2 zEs3*$AMigyE*(7?2}*hPKbBARb%4OnPz$^21o{ow2lBkheVDd^z{_iFJIb&qx0S~U9w8|YTV=N#csGDY37Igr&F9$Mxv_Pwc=37I$tL|FKOFPxT0!6HAvlmZYs)vUKFnZasP@6sdtFCRnhDh-*krB!Y8q5MRzRF5YZ=bJ=2`ZC~&j0$x(;8ZFSpHW_?sEs>{d+cRG| zzy)eVya4+fJx1y!f__R5Ve6L_Gr@djiabfiu&m(aS;AajSuj<$n_FHlatQd@@7pM& zlV84&u3)Eky||_E#b4%UE2Vslcjz^2Iw$)?yrF+!>pW1r)_3tTeewHDSe|#eNfX!X zIu7awQv%ghGNGAMldP>cQ37Mb03lJ{szYEI9J;=5#Hh|Gjk&bh-e%lh=Yy2rGsh$o z{)9kr{@Qo45|cODN$D=BmJDcUG?(rwB8S~c6mks5>=!B7;rUj>+R=G)_a4P)%T0VO zGYmJ7CQ{v-^JOykypCPbGMUIao z?CU(Y>GZDj8BB6^GGisVgqseZQKjp^?X0l+6iX;xNabPgpr&`4!>aa0&BZW61I6X++MR;-BkPZ2kGfy9ap8_URV zy3k-~tPt5vLOr%n_mD6IN42&l`j2CRlkmT01Pg5mT4H5$o$`)+ERB{Wb<=5)t;Z8P z-0{h2KBO&d91Y1)mnwb`bb=ISHNb}##*p8i8vyS!pDE$Woh^xhp_O+Kinc&*Adwd< zdfLL(#9dWE#Z8RiBTbZy=0gDTvgQg_83oJ|jEvL!#S6n8yJoWf&uwUCbQa`{Wix1R z_p5^-@JBQX$yYXr>W{rwNgDMezvgT*ncE=$qIMmRf+j}mI;O{hEm&vY=^2HcO;*4vA!-I5k>lKH+>ob8eZ=N3>tyb&@O0tGlu^w<}Wb;2E-U|4;aTJeGyE6YJ+1DjmYHK^V3?J#yLtiiL( zu8;b!@zXP|=Sf6UuXe5idP_u3R7QRtP7?3u;`U>&co(wCBkgJGrwpyLFvjQY20Jof zy?l+k7>3N>uzU?ZP$%EtPjk6}4CpV{#ovEn|50HOc@x-Od?yZ=@8+R3pTsnAsF6Ss zX?0%RkLjZ+vu`+jjt{HKkWYg0oD1Kwe0 zEtZ>E6}!S#wfxh&g9RYHM)7sBkfGKkM~fygqtfIl8sOawH5e;;m=n?J5-DAR^|?3n z7EDTznMy1w&q~m7?EGkATe^ZhOUmnL5eB0##b_n;8_Dt)w)MXBa%FQ5 z!c%|=vB06+KWzKKSBj*kWR%ushT!MtI@&UuJ)(cFyJOq(2dc&Aex;lbmvx)IIE?H=&W_P zR#5K90BKtwOt$a68AOjSyCp)R;Uzmh1}y%@@|G+$0a8vOInV$RpNhJ9#gkQww|_Zp z;MeEc`2njV~O$bo4@>mSn???Wk)9Fk{dk&hm3F}wG#*wj`EHg8~HptJ$ zNYiyNhc1d)tnq@ws*Z|lmXsz8sQXpyac@*dTKQDkqt>i+8PtUg7 z=VtRH-6`rMkWy88wePSnhlH+8fDw3@tp}2z4M(5DjS|Lz-b(%=Uu<>OO z+ZHvZxT6-;nqgBO!;b#ZcjO|08s0>zTeEgmW6x@A5VQve&pNor>{57DOPDnn6hqO0 z`=<<=D9#8%ObbKmQo)pM$zuYZk^m{sQYpP$tkjQ|2hyj#pKKZ7!V8(U2B?V>6Wv8; znHUZ0ArFvzXcQ<7HyfpT%p<-~?;T)7$_2NcU1tpP_V6#muZ;V+sI#N7EsGY^R3Eo% zEr3OLLb=1Pqk2)A^w%3M+BFVrlb3a{ai50ZPbJmR|Gs8Ba8SO)Hh<}*e3)MI@(HA1 zI(kA1i?+k3IbRQ3Ck>Pw^PmgQ|BASh zdBCQ8(>CEVwyvwWA_8piGDD-QWQ15#5~NWTJ%AM?;f5-MxE44~>MP#p!b*+YL`Yrw zpaoq`bRc;5SGnK8hYfD5(xfujXYpOpIPy{Dz;?Alj7Yw}^VFKvND--4RBQtBesiWf z@kfGHqf(SozHRo{_s2cbyR?KyrcpA_u4nZTQGqG<(x+p<2g<-3^d65{l_h33!Df&LQ!{KM7%Fbg3Qc}* zto}q^@YEu)(|{*jos3l1kV%e9)zVPSb4Nl10`o!o8t3zN6E-_K>|;Q-Lc`YkDV(YO zWJLn5zhexD2}7~;$jQjOQdyKdpRl-)2F}5?{E*!aO=#nMDwHN1<*3dT%k1Y>AH#)`U-Go3f5niJIri>^}#hkP4%)QHP3gh|ux^xKYs&8-Ue_ppGk_ z6pluUotY@EYt0UB+YXxY?aWdvFeOgNNQ+8>O~ZhQZ}GB;w>FMZc>jk`NJHEKnKx$Y(ngbli_|WbVKtbto4K}v@7eX% zzSH9PXH1G+KVtZgcpF3!#?Rk=_D(=BEm)a1tfY_OUqLXLO<9oHF-rL=4VMvVEm$ru$a2>r*G1Cw^NN424cFup zsxz?uBuRcMKkpUayY;=}_Hr!y#OX(PhD|-F&wzkG;UkguVS{?m-~ap%Xo3xvy(j^8 zCy>7#P5G0M7tVH)Wlyp&qRkYNCLo$qN-{tOUq%E_TnFkXV6gG?ezZVzQ_BT`(<#R}yPw8Osu-U*cXOTc30Vk*>;%aqtQ3m$X7RnTQDDH;11se9`j>@@ zCnbt$Y0Rn9vV>cgIzkg<5o))=zhO)lc@>1}-ZjK5>QvN&AM*Z|3`7m-{g$zWZE6oZ zBPw)5+yGfaJz%yIdV1$$SJ?}Io8#+ow4eN2P?#+-I|&krRS&81&bM^4rBJT>Tca$G zL(x0+q+O@GhtzcJJL!h$#NyvonW1AlFc=)8Duu;TO6b8>bW`sn z^|lmW(?-u%?4^|iRLk)%XcOAIQDpkaG72ig3;IHEC;I!D{cknz+k#|Bk@g)f=x5<2eB2dskU=`TvQpUu z${udl*S3WBw&xR6j%RCuwL)rmYW*M23-nKtD1pM0TSJF2{9Y&>C>Qpcw(Wd_FbvLu zUY#+1EroLM6EwP`n|qZ8)xqm(=3$3U{4_Ja;Ix%5%=tD;()quF%?=k7^U!|r==Jde z>ZEvQZd zf>hzxJj&H*5ce4X>$R}m>K5P(P=u!YGk^*VWum~PmP;h6lkQ>$_AE${%tut6l_;N` z&8U!s5vxE!56b2&WVJBQ%N|C19#`d_#KpM}g&)7PBFRbOfaeiR`g4QHE^fgSIQBhI z4)|OxTs4fWAELv#=XUYXUFu~eSSK;1Gvy>jD@K6BTcGZn(FPM9iM|jpA*n8^|uCoU>|hTa!mL|Ic)6|qkE90Kv|lq z6S!wkPd%a()ALqd~Fub464I^no?$n1h@QfAF;dM zPmeIlOmH)4X0k;=>$o?AA~S<}Fh)6ptb9^T3~bvnj4$oE-m`Y1;xSCwpcnDozOQqs zN>IRv8st%({a^PJ*{ep{yV8k!5YApB&o{S7mum$b?A~!2}M zAE{2N{+l#cWB+u(PF0a}psE#<^+J0;zK3BtqIH85QqSsx7Hxcmv$1BBI`fzOL@&P; zk4BC-Ue(KnCu$fX z*4(Vz3a@-s%mt}m4z%cj8>D7E-NZsu6Fx}i6Y_w6ceYu8H>Dl|U#cGgBxo6o?Kk`? zS?8dk79W9Idz@<{W3MwtaGr|!C5ej6k7u3F{df3<8r3qs4#p=Hcg%&U`=&xL_C0N3D@iR2F zd?5YpZBC+Q(nyIQ=njJ+=LVyKtnz_pSd|5f|IH!z@Fwte|3di2!8t5gpu{P=`ac-+ z3p7y6(XfPnGy29Yd~@F$-RVcC!+z;D{?%+B+r!V`PwSgV*ikr?zJ2WVeE!b(S}JD3 zoXLcmKVCqmAr?!db+&o4nk#ldJyo^-12F$hOAbi1X;1=> zn9qgr=`7BuuQ|xfZ)#}mQ6nSKQ8%;TlbNBL*V`~AYK!w-r=(+rKlhx$)|cO!!J6!u zy6oqQ;UcDs9z4TN@I&A@>|g;o1bK^rSCbC)P+9hcU>kGao~%xyLI526?rZU-9X^6c z-(j(ZvPuP$hZiid<%djRN`BH)91QE&LmeBrUkoXfXr@Mee#2t9(lc6Ldzt5I4d_|3 zaVMk@IQ{<}qLBYU3X;ql-~1-I3*()1PyP;WE0Fa|pM$8=2NbKG?k9)lv0uxG<$?mq zR_O^iithbR|PfR`}8|4$uX zgp-~3Kn7MzQpPTyXzS7IND8TuOoj9bl}M>Uoi6UjOG`knQR*&hHrzZ@iP&vKC+*xg zt;CNbRDadg%TKL`(&AbGmY1x<-x?yhA*rVm=`OAwWE9^>2tyh?jE7Tgr+R<4PK6X1 zj1x0s1g27ZmpvQYbASjfS(Nqvwue8cUut7qjc~{sxq0xgu@-{QCT~oLCQWsHQ&UW) z4r19H4U~~if1i3@cd1|TfSuZ7GDEl>878~zw5$FcKp&@`ZP5XrQ*OAUU_(JqWc6Zj z^iV;mpX>?4SK2Vc8sIBU7#(>Z#ii~%G&4deeDtBOc7>b4v<%KDy zx4v%H%Ie?u8`%p*vO9?NTST}5!a4`VpMUle@ov0$G`ffpbRyTUDPS!{RmmA-H{}{s z#waUIQpUjRM;UmDFI`Vr7F!@Sjele9zfU5M@h>cq;wnh-w3nCI{u!i7{fI%ACV*`~ z%f@i2lasJg>wOBGtar85k`p3jgR@-jx|)@$l%(FB84Qx*=F-=M+Jg<@+W z2grfAYU9=BM!|f_VY?AfAEi|{^kW=XeoMbRpMV}XTLmQHzBI`R z-y}7hCaK{2L-N_X-}m5mv6 zY9WG5Mr-3O*Sg`*v{UF-JLwbB429(_{o3r54G`|*&DPjd@bnOlDvS94SUa)dZDPWW z@vpgvWE%9}pMhf*bKs<6YqaF=%tj($arAIAku=o4JF*dk);#$*eO>(`>oBHhJ8Xm= z6RW28ugGB#*>JbBSvKbQ4PTY+?mo^iA+gj3Qq@$oSS zQ{JU&F1m3L$`w!7o(GJkj?;s)3dxkxYzk;0(`WYNuT_poaR{Jcaq^(0CS^*eVRQ9c zd)IJUcUfgW*`qvZOvO%rzY}C(gAqzuF9)6lctHHz_(UpcOU#+t7kRx14nt=}S5I)p zilb2~xNR=f*{Vw4=Fy;Za{kX#J&?}wl?t$08nU-cV%bPxE7q$>5LUZ8{Q!svLUDeU ztD0FT+=MLIY&#LHtQ@FDg9B4DC-Vm4xqrYSVEPGA63NE_Pq&K}@2NlVchApwe*Nj6 zosFH;=MHgnN4+7#uWO+bDKkMEZkmbW-Pr_9k-%N8okTSRwZ#12X$gQxmCcJ*siY?t zYi=q#RH)JikW?&ge(!X36nd9VlHWef3T3B8hZ!%1N_DqzOA_bn?$F~p7i}y)w8nRMebO zh*>#5Y?y*3ohr11YGH;smsEn48WC=?PfeIeIg7j^<3`VCkro6{d}VQEuLA}aSbaGR zZYy=ZOCy4PlOV`#N)6o3=GJYGA({tSOqc?ExV_(1e_r>`x^Y2`^=>{MiC0QN%k9V>ey`$J9hm zOc^#K)MJMo6W+*a*|m!AzEm+HeyO@C3dArMf;F^^7{)i%n}C^sVzwC5hWwd zRJ}srs_TN2ZDqn9mMs5>;SEPXh*ml43vP6jdtIyLJtoWfl*?pHpWNX3#-ct<)%q*w z1V44d)UuDNhnZwYhKLTv?t~S<-!nlax8cl`S&OqhR}&Qs0jaEG@8G_QO}8nBRQmz> za{Gr>k==3Vay#&26M!XEFjJe6!WtW=#-ZxtYR&$NY?WYEs*UGBbJev+<78%rLeEaE z&Ef*Olgx5bHY}4U7I)kF` zIWB*h4MC!?$#lI*g2%`{bWo`1B*{^jJ(fuM-9=L@X?-Y)h`(%@nedNPD=YcTP~jxp4$YX=W%`k6q5w>f}h8;l=fVS}Z#bjnJ4wSQ9z(sHOO`k25h6M8VeDxr7T zcmBn`9TSjXYM-*@z~~}ERn*GYdTtKRX4c`;hsxj9j8Sh$-gIqWv+x0WA|4IAkIRy# zqtF3_F1k-ryw27TKS?Waaq#-te<%uO7N=Ot2EkN_omO$j z5B~$7gCp-|<$N#s8cyIdVNX8l~Nc8)lve9m1tL-(yD&TKfu%#NnbxbEH2 zTV5}Aa`d4f=d`RWWyb9yl|Egxx~Yeg`bUk0>i@x%;Gme^#TS(iqrx4yhvOENmEkI% z_ik{Pxcr}xA4|JXO{lL|{%S7-$p3)%Wliimg|7wxXdfsB^2|pUlQA-9`kvnGJ_!EA zD{@OFNx%cjbj_6OPWAXF61d|uRH^hXnos?En;2DF4l(JX+Vvia~ z^~I;Cp)uoKXMYV;kN#)pj|0Y+<&+-&sXLp$k~Pf00XT}QGk$JZqE3FRAp>DiM1|QR-3*!i;X$3XcmCT%l!m{VX-yV|h~G z(S!2+Uw*QT0@Sz0*t}9wm&;6^1+pSai>)9A58*J%rM*9r?{MU1tmVzET+uvC8G7D-p!IyRJBOh@HOI#&5R>&n#OIRm#xSds zSLWx}ZDdS--ND0TGUVQ%x&g8e;u#AsECA}s4hml)_zt$}=vj20Q+Di_1G-*Ht;F}#Rh~iYR9?ai zON0?PAj{Mk`ZbJ2Vu%CCa;Haxwrb4+r}bt7Fj_0}=$<53XF2VZDs|<$x0SVd8J1|x z&Xz^+jdGGGfdTOul%i+0Wcy#KkC^2^|^qPx~lQ?Q`4E{r6DG$B;w2tZN(WUT4rLt@sq}j5^ZQx zzhwEcy!BRBR;K<{c;u5nh*PRew_^MK`Ti|ZB%a&2h?0gPZ{*&+pUvSiIE=*PbKoFf zq?%Q6lGZdra#|9udeVfM83$Tu?NW+o4DQT1@~nGJ4r_pINALE|a+*8c#!f=w<9X0} z5>p1_Q~g^kuwM#t`m)k}KoJQBcA(_J-XyT$1kSp7B!1thcI=5VeQwD=sHb zJ*$KYs(+b!SCL(QRN>4)#;#*=->ZN1eevAMDjgp;N!!>D!+Mmg9^amh{xkUNM3*vg z`j?y;R*%2+num7JO2ir+g8Xv>Q$C6@u)9IfcJqcATx(ql9nYK3!ZZo!}k zI<;!pf|Jm+3XEU{^om~_e194aH;_lqX{h3%Gr(>px(h5si-0NB+g6gD$2_{jwu!jo zLIJ%dMtnQ?xB>%mBkN`BP#Rs1_&Igb&Yi+*0_$9rBW9_e1gK8lKyWxwh&7k&p}Cwe zWRPwPys?-Lp=ZhCo-D(3OBsok<^`qtNZaN3xbo9UdHSJk_(gjBm*!~V2XaViq=eU5 zc%1X!PZRJbxZ?RmXc2u=&&2wC+6(4O0{}y)N4}#<#>n;bdapK)zM_6-c%Ro*;Gm>o zF6FLSvsINu8=GfU`(Q$dzt43_7ouPRd)+g;=Kjg-P1y$y1F^4VzGg|l=ttd483!XO~?@tZ%v!};S6qkwDYuxGT@#v7imeU+g#}R(IF^5CTo}~@2=@L z;|=XB+)W=a6(qK#lPcNez76ncX_h^)aUw?Ja+TjfK+yDRsZRSw3xONMT4Q7q{m&1r z#Sh^l>N1c-ZkuU(r)e`)EfN5+Ml!;O8Z37N)zEt($XpwTJYGD;A@`=yva9Aio;-)( zZ`?$Taf)@QO2~V7oo2s&T8W`>w)B76*AxhdnhiW|6!Bnh_a*6FS3QfLN=#)}@XGkh zOyI49G-8ARR4GvMFP?E>MgH->V=*Zl<`X=e3s`fsbKN>KY~AaYezmg&pM_N3m)fwS zEh#Yx<6MrUtkH11o{bo>4fH<_Qh-Uu#9VO@VU1`!hv z)FtfkepvUBQ$b2@1-;P1WzY4GMk~wkD$P8yL)si@=tecNFJ_;g_YS7@f7VhuwZ;V; zQmN2B!C9zs*P0CXHBcwOQsXl)4n(EfWn*{|LsBRPTY1c&oY~xtzv&CN*On>}oxw{> z&ZUN0^xM$#DVA-U-lgdVSUjhWfk9c~Hg@9y-8MCH1fp{^hdL)^*{7M3V(H3BUm@k}}&D?QHa3IW+ zgD>{s)LD_(o;^hET@*^9tBwKQWqN^`*vzcL`0UwN8p)v8x^E3Ltp4JK<*EO*oLWJZ zQA!i%<_T`mU805@aikz6S!)Sx{fVJt;8)-}MesVbZo}EsRs^UKZfj^^Yk9bNT~7C* zA0O}2Fx;sbL*!0;UPZQmHf)2OB^2&rY2*WzCEjh{5(*n%cw&Oy{d@Z=41-bn(a@9Hjh`wYG0IDobL`;ORuV=}COz zF0WmQUk_pdtOnx@T``qSpjUKJKiZrG!k-)R>`)2@3Ll1F6+ah7V*vSl%I^<)@WRt< zAlN6ct)EwadS^gjoo$}DO2~4C(EbTL)v4(J3a61!!KqD#rK$C>cjrBS;5gYm4fq*m zIDHrPKw5`j>I^2``&nBhSLUa%494&#+1j1aFiwrBP0M=Ex{|)Cx!fJs9(e)A;qmoM z0ACY31!{nhdFSu!2dZLA{n_OpH>Oq1Gfc#)`Py&$sHcW`UWkyy3CFK=v^s6lowZtF zcY2K`CZUQ9;}D)k{%8-Ajy@88zk}0_aY^WU+fEZ)yBI9G$*puB;XKtvMd zQdM|4PrZcFO^gmIJ_i@m{fGQR&v>7cG%8vG@msSel@!Jd-A8HE50-Y{}Z z@OC)PM%mIew2`wyvMkUl6VW_b-x}k*z{7tF<=+omfHqml?iqv`$Wk`0X@Jo#hS$Sq zTViK!&=$t2&fMQM9<2!~!9rXF?(fAQ%}|+BH3xXp<>5@5SUhkR79;G#HLI1f;P0Xu zRs@z67oP;A(T%keJRw+EE9xh2VOycER!oiVtO=ZLgM0o`Xcw?gK#g?rI z8$+Elsu|uVlM6p|PBZRoKW|nn-bq8t9#nOse+&JW9~WZ%l<%+RfE=-8Vpig1_Z4D^ z#@^%Qkevjj;@$DK0;wNj?z{Pp`QQyhI+W^_HeZax%q6OR;JBeyQkLu_IivnU zWTe^u-)gGG0uE~OD>Xm_o^lB}Zf2yp8Ny}xkx6JZgDLlsKBBR&Ww&nb)~4B(_%=x{ zed&aic61!rWhV=!mjbF;Q3-;x8FEw~&!;x*WK$X1Dno#ss!ctd+okwAe04>R8`0BQ zphY`hYC`LC$q{ry1u?RunH3eOYNVI6MSh8m+0<*l$bJ)AmxD}y3zPD5pj@YtxD{j) zD27ML%_c`b2(@w2vCNM`t5ziY2uJ;~htx{w{tSA*<$0IBv|&a2r{vE$u-HM@V-6w{0V`S+(Phv>qaZ8WjIip59+Z-R__o}^ zojI2!V&kMBz+vHA9%dl*8sW?ecy^sO;v(3ca(`M$QH>O9k^C`~GlhFPviZFT52l(( zGM2-1#Za|S@*+$yO45UCjDL=gf6fsjnGS{vpVt;}S*>P!7-eK#r64@`_R)3Q^SXTW zAnzo{p3zY4dIunyZ6=kGCctU?=`=|7v#lR$bc&tmh0n4o$n2moh>EaEzR$@8iC53T zMhNCnacX)pPoIYpL?ykx6H@0-FBs|R-sq*4!$xN;V_VcaeSaw7jwg138LgTC7ClL( zt}~^be66+g`R}z*91TXi*sRW}5`UfGrJic9+-&wmxG$j`M2i_G+0aWZldSCGo}}8& z<4PY*QEpQN0(_!e2uIPcvUwJ$F{~d82BF4Ebv&fvnFD%iAyWZcw>q4E;S3of@Sd=X zaG0Y#JCHEzCwicT4W0})-bxslxH=$VdUu;J2_vJASPih$3|>}`451cw(2x`>A1aO3 z_8G$E^@^rI$wZ7!Yc#v{Jui0)oq40|WqKZFY=2_IVJC2A9VQb@mJlB>yed?S{Gh=PBb2wb z7*Q-dBR^yW^w7x-J>G2$M#BwAvw;esyA+JhpTZ8?gE_E=@pZy$oEg;On^UQVEI5gc ztTi|sPa`@owCVn03ff~!joGFiC7X$0o|fy9iq+WQ8`3s5ByKppKbT{+iM&hxJQ_px z%dw8DwqhL8awzH=L%s`z8|DV2y(>Qo@NXQZpE5yvlCFyf)hidsSQKo(r)$nm^a?)2 zP2tO%bA1JFFcfITy;8u8Mf(*zRUL4x+L`i@=Nx4H)+P|x)nZCGKO^Vh)DPy&i*Noq z0*&E3B`}-q7xE{2S_q=BJ=E*zzS#vmn7Mhk^T2@HU^nUoTmYy9V=DrAYM~y_nLjSs z@{_=k#UF>HD8L)>k_c`}@WQ$(T7rnCo!b|AzIz1+l(RZ1-SDjJXx-f#DB2X7MMUDEeqZatOSEGm70FG&974GQA8Y7DBw~41PP{F@JM-E zt0Zo@8_21A)is!Se_g793!?c$5o)J}Q%T;cQ!#Ji(F|8B8k5~urQaYXXLq-mE`@pT z+^iP6eKxt>Cc3dk(~NkhYu++{YD zMHyo85;L;olo~vQP0+@{15D?H=$bX-^s)cC0Kc^PQy+Qd%_ zJkE$2Zb-)TOw6&|g$8jCP)AaYh6F7uHW%6w zDsjgF3hX*3p#eJhOm;Oi5uLq+Z8ZLl&u)>HW#&i&3-->zZuXXd$loTLtG`VH0O-XO z^0D{)xo*<8rC)PBZjt#>s}HzyCCMnXGsSj}sH`tE$%7yfrPa~PDIVMBTAL7G_%wnk zHAxMc`U4H|AM;?3pU#Y!8H}m_Zj7VZyhG~xkU_2nKedxw(Kc^bVkH+i8WIv&IJi9p zasrP@`36U?_=pGYM6JR@)$|s=`Wwo&zFzcWNFjdxPtr|e4MhNQ-cgxed^T7pULM#+ z0*}MtBT;$?lJl+b4bN*gS* zc_GdLg1Y64aZ|H?!4hcS`MdSokwDz0ZXhQ&jk1!ywaX^6eU|U1+<>vDn@;}u*v$GT zMYJx;y{S-PtNHuXf3k@D!w5B-KUx&CKt*QRpqWBsqEbHa?1%VEn-t})PrF2Llat+x zK%OS`my*2)*0iB2t&!iq32X8Lc(L2`74o`h9AFMb5JDi{ir5Y{N6(bh{e+oBlP{g^ zG&;7fEZaGf+U5M|Faj&SbA`zF1$0gBV5Ft#rE;|80FQm;fly6qf_U*0i0P$=FC^-- z$jM66eP_(gKN}t}n%|HT)4SvQ2GoUJuIAGe%7a%3I8W1D=XLw+(@7zLx5JU(k0LPO zBYk3NyA^yACX6r;=}-i-wUq2)%zkzBLCyTg=#SoeHylvsiz^ATC{3_O_4;>LUOLns zLWsMuk>snv`qG_0D3sEPJBhsAbT#OeAM(JclxrLY_%>GsJk>3XHn0u$(v4?5Pm!pi z&+gpWD8zqXJxd?7OQ`Tc06ByiO_NHiO7&OIpe?XB?;E!S0N9Z|KJ6I+$;NV2#st)_ zj~zk?i>LU%Ule2xo7Vc{9%P>VO~}S5nPbtAJ28>stb;3@zftq+z3gE z;A>XNPpNoX9zU;lZZrA|=8qC3vq5Xv!Dq`0{34b4`EUP|<%0Um;=S>i4MCXivFvY<0SfO#W@(}OxseV7B&C^q4 z2>Of#G{^DZApD(p6V<CqXU4@yn>$Wl!aEvTlsad!@bnjBjc8tnkdwy0mM6 zhk3)1jr4MW59h81(WY=inu4XCB>1I&YpK|eyfCP5G{Jq%mA7I~bcK`=C+5Q8deZmG zLk8jw#qLG80t+SdMC*^%ex;_s#BZ{xd5BR<>hQx=K~Hfi7Bta(w|e8ib6f1xLtf4^ z0MHc+L*R#2S)|PaV>RZ`Z(??s5Zj;TJ`U_NPh;b$${x>!=M>;$Eh+ z)dr_F780}0|U-n#na?%ob*uwnUzFagJqTqH`jFe z77I|hbPS}q?VGwZZ~v4#mG#HaoNSW?Px6bC*0Lvl@)(tox+z&s0=2Gp(I!47`W%nN zdRT0Y`ro9*e2}Cd0O_paW(-3KC%<-Sv93=-^%l=Q4vc$|L{HuqP2=<;ylcX1yLV}t zd8zSVavKZ+pN4gptm%qOFXTQRvg)LtDvzKK=pwm!^eU!j(8fmTpr4btNREaZUlen^ z+XQ$?XPzsLImc4pvskw<1THnnB=JFzDw$f9oMIo1fG@Klke!fQ6)OlWmQTYYGsC<& zH#^SlHj{$IepOI;EGE|nCckVRZqMUB&4IKophWLw+4XXu8oN)YZJ~eqg%! zK)JHSgxq0UM7am$g_q#%u_T-=5VD>Ko>1`=Z)BkMy(43k*qQwOa^O4AA?KU8!~h0X zR+Fi4Q0d-fI z#+mQ8jIE2w^iSVeQyE-~iT>y7Tq6T{xZV0{@2W>dbk&5W-M8Vcs=Z0Hm_N)`jT1r? zo)%?G^E2@(J*U+mvch)P5FfGL1ZTQo8JA;xD!NClu7Q4Y^AjDN-wn$C#j5KRpn@)R zxWR9FYHwiY8Zw7jZgLxO$< zSSw%-IG(}gue<#qCmA&kGXi>c`g}!C{heR@Y+$Ix6x*K%O5BH&qhYE~bXXXdsY)m5 zo9z)9O&8lZBs|Oufgx&E*`AD0>)iVEO8t8gI6u{<7i3{$Ynmp-c^Kf=VebqH=saXR z=sJ6d{^j?rs#BWi!aim4p=t7gId&XXsKNjzYGTY4PfR)0=h(4wDG;-%hQxoUI{Sh5 zm+{a2XQYem%Q*euJj&b4EpzA#{w&{OEOPRC2bXCpT|6zYl+fXE80@jdfJz-5UxoEm ztQ7B#U=qc0uao3&8C0!Nauc|IwgtuQ5!U{zwC232)4IA@W9rT@12wh67^WB(VMgy6)dpt*h}g&=}@bvlzf*v3d4RHKSkSu z5D;KKYr1A=6Cgt~IcY@sU!yNb2L0&z(D(3!;sGWBt?^Q|`|50sf*&o^Im#^9dy~&G zm~#FB&2~q?p^9_#NJ7MD3t`Ykq{Dn!=w*L~` zGy)KqUz(QNkCfZ$cwn(Li2Dc$dh<9jB?2yw6KXYb!+dJVrRW|1Te*Q~H2!3q^d6-a zD9|e_uvvFDMKd~$tF(D*+a&R4Rq4i`$}UkP4u^MWxz~x)Z0B?cWs4|NwORa*8H^Hw zQl$hGbUUXzl(RvhG#s(g!d--E@UOGt7KfM|lNZRG(a}^mGSy7(?QZ%+a5z6k$oQ;f zy!~lNUKF7YsZl+~Y=e`5Z;c1&VIsTa{C5&J4G3>e^7=Cga%6Ay8K8RKu&2eU+i^Ie zn`iEMlL3mpDc?TdB(<)|R+bDhog>!)s7ln5*v}Kv2BjG2GRi3S=cpflj*X~-~m zR4If^pXj!3`t|SJ3JyVPRV>iZX|NNB^!U?C?WeTgr7cZJ0KOLCInwy3k#IH2FW>)k z-3;RSuaQeW?QM(%t?Ln60$WpoRewh0D46oN=O{Gy6z>?V$^;PQmtBI5af1YxX`fZ6 z*MeO1>}pyc(jhdz^J4`(nPx)Hqm9PoBV|n?J9X$YbjDHHG{@{8yiv#%p)_BnSLAYOE31Y zaWiz!6_wyF;d7v8Qbn@3e|zqjQYuW;beV=&{{q<>jgtUiTstD+aLV>YSzkO$=k0(1 zmT$*8H!V|F`=zUQ;YXPv#1<}lsSmThTqQj&>4^8CDlW!6o#~Ch{rMFT**#^-8!8n& z6puZ3MjxjfGwPBF(%J>_%ZsX#LFdia4|6}&zNHII=nJtni0tUSQ%UNzj~EHsw#5bK ze@BXx>x*H6S62m38<`n0U`O@Ff68jO!GS>0JZMKgG9ZsAaX_K=5p&Tfua3-=$Wf`H zo~?PJQ3eszA}v#WplCLbf@wem`fhOc&CfvH!buWdC`; zxLNyZH2;S9aEA3McYXrSb8*LNOV>Wgk56_RbIbZy>^G5y|FX&*hw2f&;?%lLpoBs~ z3z!1j^e~~9GoVhSemD=h!%$3GKl1Uh^TLyTwxGY6#qNI1+P@bMC?cGCANZoycV%|k zDY(XOdgB7jDX7#MbT|SUB~6#e1I}1D2wXx4$!nQ+kJhWah`;BTS*-6Y0 z^_xC)K_%W{t{L+>ife*N5|mZiPqETNk%3mR6#K-kqR`qj2YgVbx5m6kfN`2P>$Rw6xRERqn}vU9(PH7fUV4tcjAg^}oMgxR3aNx9-3Iv`%;# zy7bbgdTZl}Z%pTZ1*u20{fvKuKgs}FNSgQPRp2@B|EK;*{Z!WP&H2Z1T)~jVpr(1u}6Bb~Mdqa-xgb#Iu0jqQL@jhT| z4V$c&>MM2CJ|H;I|6|v^&S|=)SHM;M{iu^!#w<68n-?}H5!oUr6ns=5$gPDXCO0|? znfJUa<8;SNSE$QZ0UJFU3HN^-s!7x!NxpmK^rv~*n~&KPk60f1 z3wo%$-wsg^)dr1K+kKkW{i{f#giDYDxOSKuf?W=7d4H3}++Ht%1Cch6$y$P|_4t0O z-M}xPLSIDFH@2Js4_#3a3jyfpaZ!mq1AuP<8#ScR)=ucw+dZNtAzbiVJgF3_++ zE>7B2QtITzgb1C<`Pdy44nub~dF*L>|DdLLFy>zS$@xNRmA6tO24teGY#zK2#^^(y z(>o$l3iCCR%o#FvejZTMV~QI+IuF$Lx;bg`30N;}ylxVY`4wS+RU?!Eo+!h<0HAT@ zZ&H=#%%B!+QCLx!_y4UFgT3c*wmgpJRZ(2|=gBtg@6AsG0sX13vH%N%17D9QgBclc zo&7`T>2|J!ypL77ag#j0**yvyi{n)xoR58rx-c?=HKG0(nr0s9Y<|&BtyWT4^h!z3lhh9PKp zyJMUlYYRH+%#-0Wix*G7K*yNiK=yrjFAM?e3;5=U-WHQ=ZerZ+9U~|Ph|iH(ep+Sh zH3_7Vtqky8(is--irtn+q#2`#V2kE^m<0q`|B-V7 z5Y2N^Mb@h*@roPUo?mRrq@cdSz^^Pg*zoGFL?ad^Sr%k&U?WaKP$fA*DMtq0{bCV- z&_ueFL&^5?q);Qay$FNx=$`AX``_yt6d2I>Y0tKV|4$-l*K$1lm5i!JzbrH@eaw$+ zFxH9}L-2j0^S`wMfjl2c*H~{Tu=T8ms+JFd?G9)KMchpZZzGg=K+v zfzbV57J%tuhEQE`;4D}Q^hf+&`y4cI_9}#-koNZo_Ka1FMdWV>G7&Hw<8 z0lq5?t43IsNnpKxEO06m{^>rG)%4jTk*c^{B+1Y{8UHlcAWH~WTQb)ngAsFnh-nA) zx+>^HGZ6m4z{}}1fNy&z*!0s(!&vqbcC@D8^uM{YK(Rh#vS$1eebfLEyVw5}f z^?MZMN7=0>-qdGi($bj!pW3F5?H_+#0P=6dFa2_d$=)YN4!igRXWCZGpBFN8_n)V^ zV`dfo0#(R41I;PPJ|~VLcfsEDI4H!9?HhHsRo;cRaRYvo*WiKQZ&&?^2w6|n5_>l; z(){^}Mw>xBFhy!+y5Le_@v>yvJu?JBY-dW8rf$4N#>P15eiVf(8b8?a+aHd15ZHt?iPrp91sr1vf6xGAcnz5KD_6?|0?Oj{`)> z)>$w53PKx4Ci=%8Zf*T!dZHw%=Ps?C*=OdbQ(G#kZl_q4T z@&7}9OUU-Zpfi{ZmE40rEhs;fi0E@g!U`cROx<7r_|bL20;9mCiAQ=P7zf{B=@aD!|=dF2qGjwA%Aydmk0pZ>K4$)Q&9gouq6%8{%=Al1U5I5H4^bYEO2Da zY7o;e3sdF9zkF?3Ytu!1F=F8OJM}3CBg?vY1ZeWCKysc>Cm@=n1s!~lzaW|^=*U>{ zjf#Vzc3Lj*b~M-6Z!O-7O3)}TL?6N|=ej%7zb0bSB8_YUwt9je$7~07S^|YfTaP^$ zEt}!31d8I7ZOYXUCElT6F7|u?&tzIXNhga$nVdoy7Y<6{O3zPAD*-D*`eQ&J0KD<= z7<j@9meu2I1oiC7gAB3vCwPTayx9VNTBmA#7Dbgv==m(q;doP{M{SX2u(N zhiI8L`Y}dHmvcsToZF85PCU9Y5VMar8*UV z!h8j^cr})ca=mHS(cl_gKLn>!q=5%YrViq-O^|^ZQ`v$+TDi%<)W(upS%$r>|3I|heFiJ=gsm3+M?c$Dei7l@t%~0P6(kd-&1>c3uGe{=j6p2YB+kq9i2XF#k($h zAAWdMEADjh0MPDRfb6H{6=BZ6tgjIV#u-o5gn@ZBR`n{2%K{FjMkj6Bf@4!cBgA2J zR<2XBhUgTITns^KBmYN5cPlyc163Vl)vr7!@;S%J0%gRN&NH+O7 zn}Khkl@(H*`ae(} zGmrt|)#ozE?k7gVX_q%ZM}>u5aQ5;U0g*#zZ7*2yAvb{_5F+uvHAR=)s1c(zPTPq! zrb$Pn2Cp4k%tn?_N|xLQ*6dfbN2P%G(}v`ZlU0a#d2zU+91$B7`05j~I_2umM&OxL zo?g^Kt6>Ymld`KW8Vx$cuQ>36;q`}If*vZ%(oyrc+@v&is}jM3aNjH{^}#u)DQjM) z<2`XY?uPyD_~GB~W2LAZpXWWqQojeUH)@W&n8$hIe2UgQ*Hb8wzj70<-gC;(&JAjj zj}x03Zpam6AkT7R_KAO8N%lU}Bs!Y_3BbSP{i1jGwzy%RmBn*3Od+u-df%#de_GAk zpwDYB6lb9S10BHihB;au%8S6}hB9l=BaFNJX}U@mEvOvAFQIhzD9OL+rH*f9&6>@i zNKx~@g-=|Pwuf>y{%+Ef495S8r-nATrdnL>A5=8|?32|k_YwG~2oI0&D|z|1 zA10HOEqH8Jg#*MVk-0qW4w6zq2`>LJ`fZ#L6^R`X5@{o9pQn%*C$4Am3G5Na`Z(l{ zRc!1L8#LS8T~OKmF#~jrl|RXGupXPYBS0lUzq0uHnkX53|C69E(Cb;*JdQ|G93B-A zU_PgLNaiE@2H54!oGxNOw|UA~PDuMY<|iNzP(wO#7PCP^=upA>^lJ+|nR>JybJ1hb zV;|oQ34;u7as~NnortEdO3@5yqYQeQfgEKrB8Vsies|Z?r1f>G0Ly7rE!ahW>N#5U zJIFmtqpTrtO31SKn&}(6knk4p*uP$ddn7V!zFo>{x|kdi{Mo7wE2^>FEo6lDFlyQ$ z&&T{B2RQ3al;nFwYau7M8A|&tldE|ktL^WLh%y!|oE~5w&$qS({!wPL(prRX8;z)m zcq-I6C+TNCtDI2>NaziBOS6=Cp+LfDwLoefKH8i(!!~-dhWXmX%4*i6>hCto`eZlC zWPS4(#dEPEgT&$RKd9g*>D)hTBFYy~2u*~TYQ|4PVHc0DUR^V~^r)BiDZJ|V&7L_5 zF8>6?H%JOm_t(sY93n}*G6eN;&;4N+Ks(*aFt(2fs!mfmKvpbA_LY<NF<$y#c$t)5l^&>8G zDTCj74%tj!gy32#mzjIthKQcl86qd#$$hZq)1$ka@g;&&ii6x4q5wo0TDz#Hki9QM z23|h?0V9WADhL%k?>FQ_MX!L{m`{%(4l)XV;d;g2~bP_7gvEcha6x|tgf8|CgFL1xx_(M3sI zYUT|6*M#psxNL%wu^V#Ms~R@F(lO03igZ4)W90>{m@slMq^EupjW#LT1I`gq;Lwt)EYmx*~GL#e{vkWj2*i*YKc z;dBAxhq1C;xr-nJx`!B1h*!2hhzG$DV6ghJhOok2xl_fFM)6G5>~h?jUJ8(F(6pPH zP*|EsjsWFpPt+3gkoF?<+oHZ+YqJW~{ZJ`MS^r@#_nsB??|9W${b>z#dZ2W}BZ%O} zY?q?7Lu8g7f=r<-`0OtHu<|8-vX}eb$@&2b3WkjX3_K3-BeUssycqTH3n-3=niZTq z_EP)57hqJM1lb%I+nuaOv#=9PLmUG9GLBtS(AW1{kjStw6iE2r2mfZtd`@hD`gqN) z4hwjunIhcU#=x{>v_RS%nu%J)I)+*g`1*P`KU7=Yta%05B_na*s^l74Ld$2RiG>#5TA_mQ(FLgO?k5^Iiwc6c*x4OCOLrBG3p zpFTug;6pd600&C*LRW@-J3yoziE1eXRxmuT7r%$DoqSR3=ww?@J6^eD5CZireL2^7vA0hAm=~U2RsG)7Z{F5V z|J+1PpUMJOhep75&>TfoX(_fbt1O{6@sok)ML~+vHpMClc~IACQb~Dl8+ri}+q@Ws zKvYp*wyZEib*S78hV|ZYA&>;%o@H^q;_zr3?TO1rn9><^GkhaPbsFG?r?V-{BFZ`+ z(v7g-Tw5%?v`Wgy5LuY+`e;m0>U5+bO0_gzNe?-3D_5?+(7}R2Y$W%R0W7KcdDqGD zSwkmz3HnvZzaMw&|8!F_P5)h!*LsjTnbmcOeg9>=R-j9jk4Y|f|19|^@X#IRH&rBZ z?qG-=JH$dYjJ0=HxqgrPBlwfbg8+He7fO>{=%^Ol;W-X^3~+=eQH9#)(#ok{t2(<7 z;63zlTpZ)c0RDyj>+~UCkxi)VJDv_`f^Xk);2eLbRgTgtHo2QNEOi6He9oV9W~v;X z+^9N`nGUYP@}Tz3RCaO+(gZskWqMd0mBTPtC;7*?$_h397ZGD*i3co@L2z7^Z0K)R%8F(>nqOn~ z6eez(^_UJ~TxW>@+L=`xk9~9j)u|l?pIZ{Dsw)x|n=TV}!6TnTfx7Q2x-7l&=|R*9DgF_AQdJIJAZv5+VRhO=pO1%cF!WcqbZZm~(xr37`+5>NC? zZVBrg0I3A91!Ic8i3C%tUPIS;KefR|{~#`N?k$BN9wL(U@zLWUS|CNsj)@Q|vf#l^ zzT>{D@AuGyt@!ZKb7_CLyEpv3bpY$^owe6qYG-4Cw%2bjq^lB;&Y>P2^ChZU=|gup zv5{dY3Es=ENb`MkIDLQnS5+4q7fodQ)GJ~ukAD2;K(J7l9d$N;NK8_Bc@cO=Je(*V zyp+Cg7*_)?Gs10czD6E7XZG-jJBYhY1%#@s^UwDYGqD}kn_BcGP$_*I=xalQXv5H> z{U_UwfsUvA%`8j(wMmZyD^7z_VdhuI&dab!>%_Vw%-JR)<9jcCjjxpaN6%BP< zxKaHRece_UzQ1dR=t%L61n~egSj5IOY2GW$NlPUf*21q`hyFD#639B-@<0!;$uT0` z;{|Jv-wahC&|tgeMDM#Si|DzVr+M3@ZTu!@>ZL(+HZ5#m~My@^>OQNVj;TcU)+9j_YVfy-}XN z&i$_ELqG6YRw%Sir-^H&j$MU+4 zkTz_;6DUikSOU1)2I$)0JA0|o+7jQ}L1onk;R+KVG}YFxDv=@ET)5;HcZ(4HltLkW zARnBP)HZR=AhA^&sCzQP0jNV-*VR#nKkd1LLLf~>!V8zs@P5s z%)B#-YH&Bb7_QxIqnX{4-{IB`m($Z%h7czUu;)h7)XVE}mM|b99`cd+^u3|O#3JX# z6S?>rH{{MV$pOs7mw!|&)e4#xE)vtXU*NpSX6wLH>*AL|~uzp`?{x?zG6pEpy`9E?2)p z^-)(jy#lV;x>|hNPlJ5+H*A3q-J1K>)>}`!g#`Gn7%8+rAEX!=nkrbYYJyg&tmfD% zM105SNwpvy4pKzs8T1|cuH+D2zHdgGWH#JJi74~ zF5q3`+FlJv1Gd@e9Rr^nbLKg@!vj~?p{|ltk-G-PA-B7vQZ&$HP!6{XfFqF}TvO>l%!0n;qL`$L!ed*tTukPCB-&j&0+__KAA( z)YQ~`Q}xdC{=BQspL5^)+SlG|t-ZDlm@|rR?~1p4@4D~2rYIkBk)465S(*Pq^q`P? z{195|=c;D0@unDHRhbaCYnAo3GNK6cOQ^j~r1dBB(N-yaB(T+#(G54DJiEqm{?4S{ zE%FuINzzs9i|RO}69a#`$Or_p3&PH;f(G(R$vU<(!~sF+uBKkX*Vmk3LQH3)g?<)K z&AH?E`W#HF1YwIGaJ@?h@b5g*NJg_9`#0VMJC3o!Pzx`EiuIn^Ozc5P(oNe?Jq<=u z{-&pxpX&=@BOAosO3Db$c$sky6IN_nEyU@)r8&pW6jGR}q1l4tY;P^a!c=Pi8_J$u zMsP~3U$-tK=+ju%28o+mdafpJNWcW^!c#qTn%8*q40(s3fSGH@!k-GzS^u$Oc@LzF zaPdEqWs5W&1o-LWstca%ntE9HZSt>ou4YyJTO2pKtLH{YSf?KWm(yjS}ew!%}aFNUeYE2yj zg4-Gbj)H%26iH`-mYtcpzD_YJ0w?RYJyV1;A9Gio?4ajUK!X53`h*R-ia1Kkdb}74 zt)KNb{@P{L+a~OjLyDB+G_jygnbQ1`vNP&njekT9gx)7sFmlOg18T(|! z{AN$plq$|JU2JasUR)5HWo1N;TB2_goJz_zN})H}pBQJ5kcNNoF~!lIJ!( zqpZ;x)-MbJaabI%<`}Cm)1~n+bp+)hT1@XIM)=Yah*k*DXU`}$*9`clr)k1_mCcul z0Vx3}`pJuUUQP+jJXE(mK=_phkhFlYgUjciO%S!8fp2zqxo>=TlWiM8sVS}QKc;u8 zov=E`FH9-nZ;K3ybM)Ztd!BBaR|YvfHf}aZ2D3wCuM@>Wb{0#9wWxHsO>1zRYBlVj z(!yR2r?Zgg`LG0Nf9s1yfR)acb~*6w05Qkhf;vGMV(_acSE<9@oB`_A7bk0D%{LiP z{;q)s{cUEe&RgU$ufQiI(i6O9MRHyre;XS5bdC3jB7!~rT_!CJWoZYat=T~E*MA&0 znbYY*(3BSlHF&m$d2T{*(SD&^4TA5E$5pW3nyDcjZhY9bfxkFvCk{l>*zs+k34N4J zr7my?kox<6=J?oGNTStF%9Qg0epT}0OBAOhuHIHz6ct2* z2mtBx(oFNQcoziPMpWKh@c*WlCrB~;&E!buwsEC)CrX9^d2)w7V5XTu$w@+)7od~D zwI}!&#zuc}xN4%wN1@d=*Ag6LPnYbbqj*HzC~%EpGS0Pkn03h8HlkkjsZWo3gwydNH0e|p)-)7iQF z%DVqE*u>!5`l2mi)9etxvVGb3yW9 zx&PhtmL+8Nb>^H`ksCucOTY+8yx7!@h;5^uNdAq=fGkm4Qt>z`$zTN)u|gPBjk1qn zyi>3B4wpA6_{V-M{dr-1a)h88mX+xLAu_S={arFrKr|BnC_7oWKhc<-6zVU%L0FMz zE<*cH6dbg$g?{luh}gxo6;m8=)1-XiJ9qZF8WcC*i&UbhRl`tlVnxb9ua*+`{jd+D zm>NUQ?H9h*YosD@wJ?L=ORvFHg`dv1O&abofh!`pAwHJIfByyL91jvr+l~9!Tg-qq zs&i&m$D?i$hJVN}1hqgcIA?JjIz5$|rRU-leF(to9Ugqjsm&vF?Qo4MM<4wO(!d6~ zuv`GKk`4lmp})Gu_`+qEtlgeflKKaVq8=0GwkNQDl}?gH<4JMfv}~8xNA$YZn&lxr z&G{2~X(^M3V>5{KF1=w-X9i)i#zs&R`v!4!Jbno8?a+OT%HHlQh5Ba9i;q)LM!I|N zW#cvBW2Q3*)0WV4T$?2JKgTcP$dSNJr4d+4gdtST5lJU~U-W{hnx zu@%pM&LAJ%4&-zw^ndmibjuu|7mAiai;G8;SNg77g!a=h`v^|A{{Ae$+~OA)9l2p>F&wbG_u@aRc+-r!QSIo}XkZwYE8Fd{ zu$I7G9Y~`~VKZmW?CKW5g49bkzQJ$%Vf@_G_Mo&s!j=;fsH-1r7RLZ;C@Fv6T^3+l zxZ>cpS(oFXxtK!d9!DVz0u2=9nHf)4+*Us{*6_54htJ6ul(WNtR}&1-%v{6lA{9jv zd7ojXi$(G|NtAX{_NBMET7Q_~U(~PoYw5XV-a#ys^4zHN7YUMnkq_i6e(G>iYREm- z>VC#v)IWbrRl$yl6p%rmdJ71u>=WY&xsgzxery3b_e#s|iBA!|eVkxw!*2plH<7UW zyaw#z6sFU)i|Sc>k!x3k94`^?v7#zZi?{lg!SjWIYZ$VWfU52E-vmshw%6qg3vC`a zi@BGTb!mLs`N7;pH`VG!rbJuSnUTZ`C-fRNBkM~ z+d52rZ*PS6a|4u?2+*;=Qf-CnI|){fZPM!CY8x*^{W{#;dwf1>ypKpD21MS9mVYsW z>MOb=`n)_xT!vBjcEBtw&ykm+H)jKPgjU2{%A-Fp%;Y zxb4~qj%>5Va)Xs1V5kDUBoNZZy&FaRaFdU#Tl6PF__?{hHR~Cz!LG#`sk+50teHUzxIn6FEu0t@GWtR{iJ) zQbW&`*A^;1#Z&#@(AgtauA(0^V{uXn12p0&E!hS5`59gv)7R4l;S$iR>||#u`e+ z(6EVM6y67br7FcxFaBEWMTPf>vy;cSDHQ2R0xxGFuP}6#0>~UD2rsU;yB9UQJ21~% zsq9`^+NxKdyXX5!)P$Yvu7gA zhs`NV2Wrbp4(0%H$Gy#M0!cHQ-K77eN@GUW-3M&MbSMKoHv;Ix)@w`9nwV>{K0VoC z&P=*?Y@7lVfvId>F>;tNAzCRU>LB(^n z^cX`gq8J;|BH4UiXiw}f)C5z^p+>fLMHbUp?C+n;#%325+*@6M= zq&E9an!e@>B%TOQ3G#Z7tq3gtoT1dTXpFScWLVGYzN%&tjy4X8eyVz`sq!!4Yaij~ zB{2ZrZpqL>+y3My?8*X4?K=DNN zJx~XID#WL4FDTWa^ZFc&yIdrUuJp`se16Tn44E7Y@hy=sJ;qJOGa{sG!M1SA7@9(C z+#c+NJN6KUaag(HDo_=Qo=yj$bXM)WO{d;r8}vpa=gFFs%zA(s!;Y!q}lXk zykMB9dhoF-+y_Hlyv%Zt1`-7P`J0<-#r_RV36-I>FpZNF& z(YyQJJl!BwV-(61ZuZE>Z2b>uVqWU2uCf_v+1mub{y}~Lr<>TVpL{yl0h@<2H)-b% z_Y7BW@L`#?Rz zD7sCLj+3wNjv+WuJctiFH9 zvQk<4yE*eA>HrYZHo$rvsHar;e(8i@^TMZNb(FumOPcO@^>X;4_1()vKxEzJ02y`d z?T6q+lyK@(OsHp-Q^_l{C)n*jrt*ggAj0ofW@DW?l&w6vyB#^=aToh1X<31Pm1JXj+H5fRz*?M}1vR}8ZK{PbD9P(#Wv<@pz0 z@?Yl*xS0hP+>3p$0Exe)VqCv|W?;L5VJ(izY{a(7J#HD~5EhUvh(U|qZ8w<2dH6)c-K2Ff$U_J6|1|01u@;n%WCMJ8PM;HFKfQ~gI|FQyAOO?To6`@t)-GNU#(zGicTX{w?(xB%z5=={JHThF zQ}5j~h#&6;>`X#)@($s(beIeLRm7I#dO2+sU&354fpcF_+lQTGozaNHYnHsW^?s`-z9$eUPXh{i7+!1un zl$!d@jWwVtII0qr9#MzD;_w^3o3~#1>0yZl5d1_@xY^aBx)EA2uB~!ct$?t9w*S6nhSYB-8Z+x+b8 zMg)B??&z!3z7E^{D6fr`wt;#`tbO_023#Iy;`LwZnCUB|BK3+Xv=NM`%wcn_G(6DQ zV>k}+xfnII9OSR;yX;VTHbf#iZ=hT+omuqNNeVEj)-=*J_eK2##IrD2x%(_Mgp_zM z$~+I`=$pLZ^hy{}2tWv(IBNx`9BXD445Q!kA#=*q>`soEv^|_GmC8j~MFkek)3)QO z!wSv!s*JJ_Z!G$5qt!W_h_SqsY=UswUv{~cM({#M@7qMHdJheK{)RpdjSHAohj(d^ zL-6qs1a;@x^=x2*21P<+tC2d};c`ZwiFAvWk1AiRFOfO`Ky+Hn7eGuT1`w-zxi4`_ zanKh$i=Q8j7ZL&8{<)rI1-&ynBc_;Cw#ez^f%wt+zUE$r{MraikK?`b>7vp#*?PG~ z62A))hEewwJd7-g@xRm9iusb6N>$S%zgcgEXOejX+}x237kgMbRp~I);{3+V{}p+X z{46X*WObH8sYz;Ig*5J5FDQtOKg71b(sX_>o51!tY}T1HSq`3L)l#?6p77Csr{0~Q zOyU1xRi}HI>!g1`pI>Ap&5098vh_k}gER>p-!cd{LbvNtsyOp3m(fAZrKz^k)9$B9 zNfg*HCaGXEl>dl%pX(gV3dYr(BOx(#4s%N+H9vuXb~Zb8ZkTqrP9SPM2oGx?klvZ! z(WQ62qfX^lzCYQz^cb)a#oBjcksZQM1U#tJ=YiMgYtG}$qnvC#(yp&jC_uy3rPD9- z;@T?jLnb@Vy=play3y=_!c>O0$%N~En#@)r-1*gkEqdO@jCx(kNLNz(01ns0*=`t$t@(#<1q{rbC)4h_jI(G;i`wuzxZ6?|$~FHt%-{2619WR@SG zM}cg|b2}!k!cJ|!y?EOFDGG=QpWvNyn%@tn(|eXI8f}69nBp!yCLID|r5Xz0YOZ34 zDLcolANpiI-u;FWlw|oAG_Ti$WFM)<9FhXpirH9B#8mzpei|3V&b$yu{+MxPYJanO ziu?+vy0bhcLEF^Ngw}4=txZ}5nJ%z}MA)$my;jlk4(kr+pjeThQvCVKfHSsqX(jSq z#c)igqTUuMLfsMIhw`2C_8C}_l5qlq8}a)RF_ih3+ql!GGt604Lp@7K8q+Rxj_Im^ z(+!@vt@~PEfun8@K6_)&xSC1s@*}_D>^e1GRa~8+I=>5$TcW96Kl5hP6==(D6dgoI zS7CiT;-sC|l;Qaq9g17rWpE->!JUr6>|n*N<$AeoHDpkKgN>>5uMmH+nMe5vRb#9j zpU2w9LVS;0xLH5(mv8fO;0ePm3I}|Zm-hFdbykwJX5}B01plE7YYavUWRVQNlR^pU zK){2Z$pRwXu9g?{qQTdP(YIlvILIurAir>Pf6wVBU-A8Ou)I^Z0GSc7>19u^$S zvVFIN+-%wOR)_gJ)qb21O=ODxJ770Rrz(Wt0%5<+3YwoN?=og$6N&vV=!e}MZc|P$ zTxFF7VHCCx{43+@-#%*{Rs8{CWSZ`)hU-E2{2ZG--kWq64jxhLzSp0;ypT$hGks&h z5x-~ld4EDeX;2!n6k!JwPMi5p#8qh1vZj z&hgcsiA54tHEkQe!mu-EAtPL&$$K`@U#bj1U(Gpu?$}aU1*e&ejJgf9#2!|aq^A!L z0)U$o=76@-vWJ$-8ZX+xkKId4E5B+n+a}|0T2=|nGn}rh7C03AlB=l#HBr1CV&_)T zEv4W*Dc8p{W?T~AY!1i^5x1hxP2s)UPtrEpWRQ{P`lhB)_X>Pruqp}b?cfi2NF!#S z&zZ}hPtzU_zvpKZjINio*c{mSZrambu1TylDCIq!(dykr7_S5pKjVw246Kfl*fO1@ zUxbJ*AJn|2veyt%q}vJ&u0x(Ip{rTjhR&NIXPY?m^MP|<%WVTEBGYZlo;?e!sX>lQ zhv~!!u9Hna!57#BjQB|%5>ml*@%nxqH!$pmM5Dy71ly==u82!H$x@08u8?u6?Jx-svOd8;o9%ekybyvae9T7}W0-oG9x7iLS) zM@l{36P#cHgv%~d536pm3@xq&meQ01zX#1ibpx||XZ3l&mC9uNdY$uK427e}2JcUf zeiUYBhlBVf)giW zGP{DL`Q=#OUk)=17CRAwP4^>I!_1Xisu^B0U)z>#U6cXnn83Pk@CuhO71nRS9JDaQ zC@fLHNJBrYY`FRlnvv8!pX!Bc9-!p=N>cSPKAW&6nbQP6?poErx1HRDHSPKd>fZ-l z?_1MgJsCKI(y3V5!LaKOGj>eSnC?rM+l~EaWCpD4E7Zv>@V^{uj9ekOQPv_Z0zPve zK8gNRCetNbm~SP|D#rcg&?JqYl9SzPzB}$BRf$914;=(~>-2QfBUBK0=Y;SWSrL^J zfSRu&bk^F2)F@)wAGvqNsqf+EXE$sFB^p?Mg+vPIJY7|le?Bcz8lTd@28t7a7t z@co3THxS={y22?_wjA-XbhnTHJK2+bZ+sKt5>N3PEKO~3{2m1~%j#nR=RO;B|B1ZS zXXdeFDeU5a!Fx{{iYaP$R|)wrK0|jX!*o`Y0PD!oq)>apAUG>v_3op^r{U=@)^|(n z@*IEVaK{sCTKPtC@`dzMxCYr1U?{7J)VB0@ekI{DFqL3C#=g1Vo4P!*sOpio`^d#b zM~o3cZj$bkrGtf&9K0dK@kt>0DB2aj#!Y)CRwf|N`1L@~Heoeuc8aIFjWpzf;YSW@ zQxBzKS@asrlL~3eo_b^Od4?N?z`^jd_Cx)u0A9F=GVXRPH<~dN<(zfL3{V-GMdJ@N z_|w4fLd!{R`iZc44;p@IU-t(qC*2V_;k#ib-cp}@T3KtyODDP2CA4Bc+7o)609E|6 zyL3>R#3fxODwkVcb-z5E%+Pm(frB;Qv^5}TdDy<`=G>D;FO)~dn^gr6>2A@gP1ZgM zrV8M4AFAiftnQa3EYthT4epcq26_&-Oqo40!z7VNdEO%__u8IR~@qBxxCJpHZ`^*4uS<8sibjL|Stv=cMW>)_W~V@GPgAUC`=Qpgd4Y zrNpb||9*=epeS4L=*z^7CHfh2o(P+8bX!RL22rmp_P{l@@xt0E{>IJJh&RuH4&4#y z$@t;sCy`K^d94pzL`8q{(Ld}83PjJX@Fur3bI0O7J#+v&ge~_+YMqk-!rxk~ z+sy2~caIbaXu@wCty#tvcfRgm0!`q&#~5 z7JCCuFtj@-Syb%&k`XD(?mzrvN!q^&!m0*yQQtQAM zI6ro3P>NqW5ZuB`P|;3)>^2HuKJ=AgBhL!$K!9X0nx-rmhD7V(4fXdA1C3D&3S{6p z{Ma!R0+^z>gb5BLH{cZ|zgZ4z@qk0R7n>@M{FVt5y0S_0fzE3xaBMPiIIB z^!)b|z{G3)$4F^9zZ?Xv4-dArr6=+y~{j5 z)&3-^0+;%d`8h2Jy&ZSR(Cd3sCE%k1;nLa^TeHmbuAgK1yBXwVES#u~0b)lyU@nlE zo0e>^;i58g3B7Q;4lwX#4i5U(qUF$HbLwfX89nb$?sd|LGA`xV<_y0(<1x-DdklyN zqkW5^K|emb8P;mC9Fkk&DRwe7AfTw1k^+8sK> zgNm)C0e}I)fZY2!TNqC8Ilh;@PhZtfw!V`-wMN;hz&(Pi$M0cj^8v2!mCaAH21!^Q*_&#KKCY=+vIBf;`OcIMcAo^?E zUh^_8lOdX|xP5<7bZZx$tl;zlDb>rSWFz!)%;9JU0e7fCXU(#&^<@nJ*s*YN;0AxF z7Gic32nWebI4@)$(BQlS4E2cz_|@+!J*aIru))}t-@d6UHw4zwhH7kug1=@2QMmPL z8iMfTdn%mFJ|xtIKG9y?HZbbhK$gK2_fU|Tpr}?Y`WIiAqV@zZGqBHn%(sy>5C{gr z8V3A*(9UUhd=zxpRsY;ECjq6T_4KYcsT`7b(4e?1OHJGr@_T>H+yHtU+D_YZ0{q#pYb z?D<7i_@XFLF{fYi^Fb*Z*2|h#+4L%1GgEf@y?=R4`ND#-s`NF|>_@>3Qr^D%wCn2a zcp9}OI11(J-JMlKTzkG6>vL6$1aTo59I>Q#o zv`I9DGd>5QSLvw3ddOFn8rQ5KH7`cKg&)AZNUuGU1`f6~v4S*QVY?BhL7!)p;xUTb zZo$D>n8x*2Cipc?bJNi>{E}BWeCOSGdH6Pb$T#qW90cOQIP_CHh|>@Gx|xIz-ET4{qd+w+W?ul>1Ntud z-sb4y;3p||zvbwLE&mJWLmqF~@M<$4llLY(PA?c${_jbGI#3Pe7pW`eZKEP#j=}_T zUEFwRp(R3yO8Xkg033q~iavE2s~BmSy2y_Ls5I08-9QrS)xD`0pPq{#1d7A^z5&*! z!ZM`F7txeZ7+FF$a3#ZWd+o=3fis^AJx>#4<2~cMa%oj)g0@k0_&T@N>a}v>v|w{` z?71w1W8CIkzf+n20?b7!{+VE#O^=2X2*RyUERQi0(pb9LuyrKCCr0*ob$$Q)$y22x zkQiG($y$X4IHDI0n3muCA&h-71F4}o=#} z1HpOxW(Hiye_;w{^!nI|3mQZHQ&Ql|r%2{9{$rhKb)Kw(tRj?Hxt{V-@RAiXJ6wvK zpKr0tkPF)BA|Ap()r`@hCzG0EISD5{Qw|s}6a+4fqysj4G$_)XMhbGu+@nl+Lz`s2 zw~H2alARN{*`)Nw`^gM6uw0IqS~EDMSJ$hoBHnNBKMgd|pR>fbk=}PAHPvbmUmFqS zNJbyX^*i=zf>s?~H?+qLo0p}3yfV`LUBl^7dT8$xXxtCh?rIiiw{JUlEK0_ z%*KEFmTi4J%PEd4x?iW7WN@|6GJ%>`4!asi!xqqlqrn2Lb}@kmrA_xow}{d+9anZ6 zbC2gaWF7DA_18YJ6trkV|3a~0ofPQWU5MtV%?ywi0fV z)$K_fxhh(_L{;Vpt#=gj&}2|!vSP_jWPWpEhe%U3-6LT3p5(WJ4n{EQ?_3QdBG<)V zm*ENL`eZ=z2?Z3YCWc!fM3#@rLZUPIVHTmQ_HN=@zjz7V8&DusElcYEbPfL>AyohZ zwoWMtnNwTt(EPD=+x&66(T<=6W~9zqxvyYo!hGd`rvcT~DI9V~*i2k6$HTgZ5rOorms+ zYFRQzd@4sX+{jbBm2i37Ped)Ap;_b~SZ!NTY|l7mZgBg1Y~oH%{AeizlatK5##m1n zXLy>t7i&;X2&nt`nJIqOPjCcT+A)mB)Fw_7RCYjXG|FFda@Q`@jgB6voEW71^LWv*!MZCDuf=Ojs)K4U1TL-dsF^KB9oJ_pCE!ZkS8kT zPB(1N&Mf&wRQ^O!Nv?%gjGNDP^x|q12sd@9*@fEMd9lvmdJ;0K{}-*7UgB=H zk!giVwZKtUwR?wvNI@oijO1O*tin$475^jo{@;^+@;_*>^K!<#G&*$hBr^!Q{s!Nx z@bXOOjv(Oy0j!XSjUTL7Zem=crvJJ2KKk}I+{w}sX7kPyBO!(>W7YjoIBln(oS2yH zDGUM}e!6w7`%W?{0pXY{@&;Hh#JXfSE#B=7@DefTJsXU^_zXA~8YAs*!7))bM!MPA zAXi7Zp3v3vr@$N{uUeQAtBWS0-$*x2cQil!f#+JlTsFl?g9E`y>owTx9?Yv*f4u*( z0{lNij~imC#XgWARzI-esx}g0>n%>^1}8LJoO~CuOqa6UkzNpq#TDf{_++Z%c^Ddl>VFEkfP;H|0oBToXxJI@bjrc zEFEE&=d?>Vo=q__?Uu97Gg%!Uwf!##Stne0 z*T5In;@*#lDI_OycEHS?BO?}i``f{KANRo7csrdFDn zjet(dN71~_f_@ebr{v%gCtloTOc{64prq7~U9=Op7Gc(ju$P*`_OnoDjj$ta{FdnD zSflXwIy3#AT&x>6Z8tF!nt9yTsbuCK^?mIE8v2g=TG-i=iiWgmm-_cxNnGsT9zr0|urw#GdP1+fTZUSr3S({x~CLw?3dsCVw2VqTyD4BH+sUEvREYotGzN+>b>a0 zp?l~Pg9IjB0rG|)3`a_$Cx&_AroY<=X~5 z9$KbSwepI4JLjpk1N9J9rz@V3_Y5hjtN&Y0AW;N6-{4aGB`faT;;)?7AxN1ltx0T) zWxXVhSy~Y!9Nnx%4L&N082lJ}NXDTOS8+bsA@cJ_;(fKfV0eW%?TLS7R zchhb1Cj+4qxAFR)}J8oi1LZj)lHMj>@_ zV(0?lJ=?!GRY+Lv@{A%U1QUvW1sfh_j2ixnKtmor-2ES9me99QX#ITM?8USbcY9fqn5_;w&yd02S_lWzJNye6E30}K5 z^lpq9&~V6o?nG&IrvZY$dP1*TBebzg@p)&7JKXZwuBo8emILv3)GNzw^-ZO%m7D^^ok2bbn6^L>=$;wpsGcB;$pI0=~zf zoWbw&6-oeXvu=|*?#t4DC9727x`B^XTf-9$o2o+%$#n&cQDk3Nbe~SP3C?4U-4TX2 z@9LJ%`>CjKmHg=k&eB0c-bkiAT%6QG1ZS!7S3Jr|IUMN!e>lefeT((0lN5+r1=h%2 zl$0CAa)7<`j*^Yz&N4E$AUb7LlJ}n8H|0B*i^|ysxLPGJ8WT30wQ>Yf=m?kUxFdj@+M^=s-1X&s2BR;{l{&9kMmxqPu;&x-5i|Mb)E=* z8zH}YOWIa*w6^eu`oAdjO4B|S&qq3dK9TqaD};LboBWKle^17JTg&R?sKjqPVzka{ zqmIV4n_Ss-X#dbG7V94MlNATho@m;w6jbi$)Nz4M5PBYxRN|CpJLw(A5MrJPcGG?S zreZ{bb0O8~5y{RIqw6l>M&S6)vj=|vxXQgkf=WAfvrXM>L5lH;*o64i&-;)6JZ566 z=z8BNrwc!*{P*V0Mh1+w^0ErwD! zX}zNs^5l|s=UeElacEenho+q7g5d=iM6b09`u&tcU3aZfc6X_1S5CEcBH@bY^6#@K zO^GrsO>@Dx6S?eDWn8-t-x=MEI5E>cLQ)Ld{^xo>p9U`zGZ}?=qzU)^-WSLAHy=+M z9OLYZR&GX>ttkN`^5Lw z)efi#sApc)QCp_ge4P-PYQ<7>0Nr*v;AOTa02}Sih<+v*rZqRWM$M0WnpbzTS;CCC z-V>(~1t6k-xF`)QCe};$Zd;Xc{(E8(VMkm=ZKb8Il?tZ7)vhEz$W5T$meh;kyHstI;zh6KWeJ~34G(^1I1K+ zur-R2*TK%$Lx=TzKCgH*6cle@wB+%SM>T7Likr?h)Tqx1wGwu(la8qT5ZgcI-qS9} zTTO!)e8tf7Xza2R5wR$g5}8!hmKWsk`FU+?kvGCMN#dJ3MV6({Qg# z$4*BR^zWz<2ArYIqGL0S_`P)dqx@lj3J5;B>`x)s}InkL%`cEPSV)zpVO+;B`#;v&}MBW4e_H zQ=>2?j7+-u@q@Hab^4d72EKab#pJw@;=@yLvs;FNm;m`|Wj_a7v0fO`)SIc_!@qU# zCCJNB{NTSiWQc2e+dX7pHffQY>QImmxd{^k=d}EP&$JFY-@(3vm54qNRY9OF z)N~vA+&PSq1uuo70dcZy2HR}IloRx7Y<-5kaW)B&X9lmyxZ^v97*=OsZeRvpD9Ngr zQJ{Jv|LLfs62l*XP(1Q>W2WlOr22Y(E|wqI#^m?Z^z`u(_<-I{bsjkQ<--=lMgf$h z0>`?{L^W)RwlmIvv8Zy`dR)0-fT?DVAUNpLqZF|&5J_tFv83c=Upr+0u7c4mdY32T z$@YL0E!~Ds8!KK|EOr3SH4ZT}p6-Cx+;JBR&O@XKe)ZL$Z;47WYyPCWIYu8EDwDG! z;o%z@no89GIBV!9_Et(~*12rjGfa;7VMOsUN!^;ldQUge-QqkzRl$GLn%?nxiHd?w zQf@1+^RCCNdWj93#u;*D0L8qsw3{Bz2&8vWJ5_>US`$3Q%Rg?$H zAVDW)&1#W@t2FB9MU;<(=Q!pMhotq9QUP_#9INMngGMHzWb-hu5^{{x!zXzsw4i}N zsUCtqkuuRj)V(sRGI189ybUl4js)rLZAqAyAI1Ex@?h{LK{{OYF<&vhanbrIDepMp zp%OsnlT;J@CS68Ks`NP;U2dze0T8>!wZMyiB{FJU))orp_%0g1-{nZ} zEsLN4EOy$gTkQDy7Y$ol-c4mTR~}*Y$zMSs1kEy%zlcT(;!!R+TrF5ZRw%VBmLg+^ z>hdnd&PEddY|&cRr%%p`?W5kGnwRebb9{RPsa7^1O86N|dN#Y=g^7{Q{eG+$<{COR z0O`O)uL|_Uv$Ng&TyDgFC|?kDb^k#detQNZ{u_Vm--`F=0SnfU3uw&X?+tj-NK}C> zUwSH`5iHMyvPv>EPU&pkpRde1YYt!8oeNEl2+n!>(TYWOD2i00NBr~(Oanc4z_pCW z89C9CBDFf5IXS(lGg<%4k47Y=;o~zKf8niU z6#tX+GN}x0pV+IF6rW;-kYLD{fe~7sWj&U~O1sh`0XV^cL-uMSDwcFZM27bIM-dEG z<0%<&63I2GA=)PRTK}#xllvdK|H1_P)-mB^p{vzi4i=RN6ZPsFi@KOYim&BBt9ZC4e)b`&u<3D*I4qDD0wd?l3 z;m|8DT6V&VhgCOX+Q%fgb&1w1Yk^@Gs|e?YI9NfbMi{yD*$fjCO+`-X=?Er};x{NW zK8+p8)*=1v^FCB)Pom6F)+*+WbqKJl2Ip|1;0U2)gLFVjxzF|uUA4K0Fq!86HkPE- zn>>}{L4h9WiosU!d+AD#dHvVq-Xgsw$@MKr{V^+d)Tk3^#<<(fPyg?ZFZ~EOi6X^T zp=6=ClGT5aU@z$Kb%OORtPdNZJ;9i{=%iOp-7dP#LwWQ%O1$!p;pjd5qg!`OrnK_= zf58L(gR-`r!VuK89)4+z9o`S8ersnWuv>79PWn3qJfhiKA4%a8uq>sgOnDJxIkhWC zna4~Jt2+iYl@l~!B_$zU>u306#x8yE22FGoC@29K$0&$i-6-gvTqaD54;DAMSfn)TdT5Muvv8FADsoq-=wS(;Rh1(R1-Sidmia@SO&pd)%&-==XWj|MnK zlqbJV3D1??4hi3#l(Faj)cM+cWV;pN`!)M@vDkCJD5(uFRGC|zl7YVdEnnPiKIR8J zWHU(;HgLzY?PxV9PBZa8D2HMaX%_i?Y2~ynVtz9Ab>zSk-ccfSw>^2f=zK2{Q8~Tr zKw L8;8^wqEK?n~?h=s%3UtG{vQlpR)TtpyoLfQL%p4Zg*%beQ_NI+x`?9%6MU1 zWNc6wV$9VibJW&VCbF~AJe?AMIz6lWEa1cfvi-jjc9>}2gdLAzEhJj_tYzinU~ERt zIbQm@md&pt*5LKY-0F8kQ0r3D>QgB+Bd8!5Fk)>;s$%>)FC(3B~(i14U2YEzS=`s-|Mn2T_oNpv0P`aec#0i{C)oKmZrF4cc ze*~kdQX_8eqza1Q9*or^u?NLcfpdAOpGHXep3;RR7;@-qd0`jZcRW#LG%)r}te{FR z5Lo3sk+_*bu4ms6G*LCOs9bIt2W_?T;w$%C41*Aw`AMTF3>(`(O)>iV_3#3u5%MPa zWfu)|nS}t-j7mKWr8|1ACm_#|jTO~~R>#LuD*&$Y`#p5TR*ojOIE2sCs$>=-l!_~KFjp^oR2elmSR8j zZv@lltvg)&j*3nfJtv{$8e(mm5ObKYiL24N!={?*cF5lIJ-cGyn}#Rn94soU=X0(6 zp9LEKZz~Cj{)_ZU{gi}Ti4ivd<@WK3rGqyL^l?hcb%o@uW?{~RBw=JCsuaT*?f+`; zEyJR0qkmCAkdhDulnw!Dkdj6akj|mIyF*gxl2E!qas~#58af040SRdsI;ER|8Dh`- z-)Eom<<GJFhJPYHEb zOuBYK^fpn({jVledAf44lViN3nd$5-GuR7YP7`x{dXNgx#C<%41Dqu< zOl1q#rtJk317g%j*LbmG%k7)sIkN2}5D^u+Tl?s;bz%)$y3$gux>+IoH|biFsW2?^5OVp{u?Z)VL*j_#0Cu6rp%`lmFiwkg-L+Pw98Q4J50|liVAIL zYbHD`=<8eSnxG#KDrU)0D!;aE+0-|%So;Wo4r3s+?cq(x`oK?J3T^HVq-*^h-Hr&S0)Ms2I) zr*w(=NVoj2o9}(Tk)i*PP`~j`<2I%jn0j*_L%mg{exuAU`*&>WBMBWU>rdaoo@9O2 z^d88~JZeOlyo330)c(TmDVnEVCmxfTH#Q!~=1vt4T;{W;wfb|YuZ!Bmmf!1j=!}iWFa%>Nv@xL`_fMwQWdKr=ED8>eT%qD`18Jyn7qe! z`yostLyqZ&g2|6&PBLZZQ3lUz)louEErk7;lO*y~(Ug%>0qyI`bG?$_&HaL#Y;2Hu zYRkNj(W~@qpIXOSZoukZktpF0p%(6eOtUnQ;RPbM*sX6n(#vkUl`5ig0LjhN$5eT*5=bX2Lgu3e0jRbGT{wF8}J{Z1@@xd&+Oqyfx)>pN5uW(3o?w1yS_ z9=k!XP#|iHa*?QrY{ZL|K{#vExwt4AdHtiUk9DABSvo26u&-}Ps%)=U#q!o6ir~l$ z#q`d4oeN8t%;$%X!QJgymClqN15)k_c1*K^o9+=$w0L~QEpPj;7HrqeQU|OAYr4IL z1=DP3bmh!`y!8;ozqTQfyOPah&FeEJ@90CMV6b5sKEdrKPGffTZB4p4&v_K$w}9Vq zjIpICzQ7ScNr!2ZP`QW~Az23hDXIf>J3+G8%EvR+vwqYtnBlrt9)#j&GIcan_Ib|{ z@}^?t6lBCQ!EHmeJT80q3GawKl}*E2mubJX+&|_CG?Ul%nKB=lQ@#-};&thieOQ`& zsts=n+*^PI;(xu0c(X90uZ*`|+T@a{|FSBJsj9&ty8OUxTS<2V`u+0t@6j zwE;rAv2BYEffn~>HB;~ek{GpjqUfiFfjV6y!`Qk*s7F^$>H*B-|d483k* z)|DFlylD9~2^;0mNQTF5<^~$c)2Uu?(fHA4l}j(D^)v*~G^Ui;0ns3S8a|8fB)t%1 zx+6=#0v-c1y^zP&Gu>Jey%#ILnu=d$ zcQJ{Ejq+Ej4a8Ih2-C8G)t-NfcF_4*s)dyuQm533PybO4{q9VNd4qBrd&3fb_z`AX zy3S95E9#+3Z@-M7DihU?#%8BoH3BaF)4P`hi_0l$R4sfbe<0vc`<=WxhNo)4nOMtD zJV7?6%6UE0scsPaO{&XyCnoto*@?$s;z3oqZlxQ9Jty^3%HJ#j!lsxv^UyE+T;C=vnal{neUoQCh=R^2< zPuq?M>Ylgq2Hv+;Rzg+D4NGmRTlC+iGs#OA>bvP~AeNTvJV1HdCitt$w4@epVR7;=5MUZf;2L4!?wv zrc|$3%ob)H(<3y= z+kA>kRhR05ssq^s*%Vqb1fQpahE-HtxkQcK8;RCL-ulV$*s^W!4mVv7b5Twk>(yhn z^HZglRiS3Ipn5pJGP1$zBTDKdUQ$24XV6IXFKrggczQ_0EQ^yPXUg<}#+Sjpw{pb% z!@ag*mF3a|Ikv?q>cE5_`r5(Hor1!*DM*b}Npu#ji?H84Z-rPSsX+m>o1}qEX=_z( zD95KYRhjI}CSg;k@$gF&%;(+ry~o^>ww;+_nbK?$UYyEG+FY!87UJRSMhLpRhc)LO zl7j~kcH6juWo|cW5m66#vXhUX!I(JAu<5SV)Haeg% zq0aJCCEt|Tk2@uHXt4r0`JcPlwly}RU^in1snYMDluMea+|_~V!I$&_1J8f;9cbG~ zCcb&Q+Lo+QoA%i%_ts+}k;$3w~MC7Dwt zxKq?sgPWln2g6JUe_3DD=B?UYm{EdzmG;(ln)5+jhcGq_Yc8Gjm`F}GlD?GrB!RGZ z(hxVWjj$X`=2;~Xa^vNe%HkBo=b~ZropSsiRr4Pw!`n2s;qPV*nb0#fbWn3ecUPrz zZEYrArDi2-{PYxw2=IY_|77$$X6X8xeKSt_{!>lt6|O+ClTRQ0Jvnn;fE5Q{@lm3< zA@!D^2V9Md@7E%FYFLm5cz1jOhHd&LNYKG&1hR!)**_<$e3tAPVNwa1jfm~@KGsJX z;%CpDD#u<4M%=`Ht@)3%=bw}5lMLR^O{l&e(&T^sAR_q!$S;Q^*XQQ+{Bp=8f$o5< z{p6F2)G8Lcjf;A;;FAossR!=i(?F9zyP93p2w_CH1l#^E0uhOkN zbR_NrtHlBf6hs_;5wX5cg?Lf}f$8!h@PRTOU65sJOM=Qsusw#(m8J}((aLE_Bz-i{ zy$xrDuI}?fd9>2w)Z4U~mv@QZNls_qN~5rKqg@Oddob>g!*Wvxv)p;IOC`?+d4~rk zz%`>%8wo9r?XBYvCKeE$68uZ9CW5?AC3MAon{0=RPly4@xE)3zPJRi^QI__I(KQBP zIJw;SDEOC1+`ztEkrR>iLg~=Q8tfY#eLB~^*lIs0_Y5_>**}VS3#Ic;^v5QrX6rMn zv8S)Su1t#0*w4Z9(tGnVHQM#n1oz)VsV*`GRQLObjd9}xe+ZYKJHHN)Ov z17p(g5>DigL4zDk?{}JR-=oN^NQu?jiQDZncY78$;w>vVV zf3BRaIu@oFT<+N!81Gr4`l-c&f_)_MfKe&m@zOw*85E!B$tfCeL#Dvt7d^vR-MFQ( zT@qPn(6mV?%>6(wxGj=z=DC)6$Ia`#r_bd}wO+kSP2p>QiDNwP&t9Cx{{6>vZ~|;=8nvMmQz#!hH1$;=4lF~dG2of3Hr=?-JJ%ZO-o!J=>(nV{xs z;V*Sc!TV{dU>?q3MVj_hL;R^u%}=MZ*Aak28L=lbr24E-iq8qLSWiYtIh%+=Fdpd+ zGxus#F>-BlZ{C#Jr$imTk9UhbcskRZ=OC2Q74lg5Yh-`&XR#Jx0Kz*_oFI^@`qjTD8otNn!>WVV8rnY7*Q7!sn(> zW_<^G7WwhhljnA5co-}r-qWf2*BpkmF6dL`E3QU7c*rg-V#tnU{@}Cya_eb{_0>zR zK3*J94lI3oBh$|~Gf`!pY&I>i|9w)vAEYlz;^VM6R%)4! z@1Cxx0{;&HEu{;A${nYiAFvl_s;hK$?=bzWfPW|bnJDk>EYRJJXgpzNMJJ6F3nkTg zo;scX)b%%wFLMPoYhD)7rN-NmV=XMK6htH`Ad?V_3@RxjhjeRf?!l3HZS!5*g^`}I zz=9S_n+DaB2pt|*`F_q)YT4NAGSTb6%+WAP(>BFRX`9R)!_DXsCeKiJ0*QzfoE z&|S#cPM?X{Ir6(Shre>?B5!pU9c;F5-LC1(khxmpk{o;%;zrRMSS~Oz67&)K8Ow5| z3maagD*ekw9r+Di!UfKwWvG1M7PI9m6P0eE&0^92bQR91L?o*xJ#=@>OS2=uUGHHZ z?+=&$8+;n=(E%wTCFL$sW#y`>B=ff_dSMg|n5b(S@eYOMltVMJaOb3P)(f=scml%OY^VuZ*AS&08RX0OAqZU3IPuGJ-@Qb1Kqai z{i?WeIG5SqFOf`>!n!TZk)q9d_G8$NIIqSyqm!cMC%mzeG?`rky)09?j% zC{r^X|Klvm%aC~K0t~W@Jto-{B1R>&u$4N`v(q9+%hT!(1SqE&T_{n`wI^gs&n{I> zrO`~GmSU!fx>qorxv9U2=U~Xv$Twl>CO4oj^P%yktZsDRNtp3r10*ZKk}CH=h6qGtQ&O8+`-*p8FR2Uy})SSgx%k*Y{)5H0U6KlZqb@b$41SFGBxf& zGK9ek9QEF)=OI(l)cfa+d6lsfOQo7RZ#U(1&apyzzjy0S3;XD9VbTnVA2RpJ9v1$* zZmDI!h}{&D?nYyfKP(fm;GDUEOA_mRlcwV`tM?Si>_Cte?UV72(Bh zrRzN#82dQ_yL+$IK*S8Z`}y8ugnN0fog+rSrGu-UgbROy%7sFrc)O=v;1N9^y3!b{ z-f7>-DisJts~pK^8f08=h2j_RY{*#|yBWA<4lb18i!?8`BJ?Q2KPu|>D?D7e!1g6zUq^$CrPQ#I z(M)d(tpNk{_*W7dx0PI>zjb<}Qe)EkPa>}D{3d=5HB^hKJfT45wL+$3Zd29`zJgG- zr(W|KRttaQ?9%)g^Jme(aXj5`riUWjPZV$xE1Jn|gwjd5#GeWqr+2cx99L+XE%j#G zR##cEWsM<{ozmBTpucQy9j zr?~rskGrone3&c4Q3vc5CLWM?94&R|1_#|6kB3}qs0fqC)UqpxYSE#_JpYG4{Zq08 zMI`bb>M?MtndT==EUrH*@Wnnna5qMss8P?uTuFdE=Bz3H-RCFF+N5DvGzKO6_F^cV zlkG*MaH=FyI1_L6KAvBz&h6X)} z*X9vizCpwyGQ>I)#Htfb@Nq0RyfwB)+ZypGBDiNJE1S-z=s~9Wud&l*S7UgoJAjtG zQiH2CKb;@FyPz<`K#wyWzz<3TA5_31MvvXyz+3?+tNuGqggOY<)$%{?Fcj|*1l~__ zx`;3HN*s5HiRj@zx!nFtdtp5qibxzXT{zaSZ7>A`H8|Echs?tHXkL2oFi>tdw6q#eZXETuFW`c`xI$~bIp%L{B_KfP&OC=$m3LMy|U~cMHdF zaH#iV?4!lcZhXfkyW@7jsMp+3>58@RQNRb7G`6)aT7BN784^7)d|Pk7-oeX@RWZ5k z`ok@dY(9%mYqzh(8;vYN@|9Od69Xr)hGgWm#Qni)z~)Gc4I_n!#Rg|n<$2P;ggC3` zazi|B?1m8*Q|ff7iCz|ht~QHkx>-d@dwrhV{@G{JZI8@FG#hGUMx}FOYMST3mG*m$ z9Ja&-(u5Y?oIt^^#;YR~Plaab;(EQttIq*76iP6K`3-4iULbzPT z$T0C&n(CW4YV6Hr+Ge$-**5Ce19i~?hACvt^GS6s-!-;!&)g?90yL+;Y}=N7lTkVt z60t6+aGA3muk&EUo%#NsE%s(wkJV>7GPiQl9iq9aQIm`8^<0w>&zW-a#<3ZX@H9Y5Y|@)xlh7R3T;?5f2|?21;Nx{l9W-29YE!grenIptJ0 zljI<^ic7E#d5SiOde8~4u&9{&+q6yfo~cxG8!k@c6MJFvcun2(oV@=uk=D{u5!x_f zV!iV8$iURoK4b4M?+(7(n&dSLmRf+UiA`rvZPoSICv zM&CPJ($)Ggzi3~xxQKA}@M$rVPY8_Z{&ZDDUvwId1>yeJ*`}^o+$I|Xb_v(y zJP6M&b%^z@=%vtL{}Mz=r07j!hmQCgvDOv^1-pinu0E!&xQXDV*gb*GrWNpUKnVyh zEoFezk33tl-}`3<+fTNok&RK?V%V zHf_Iec3!7@lm1RA&}M$W(oI&|N0i_X+VcNGf{>d30m_l?fo*-JOo>zuU-CQ3ROTIm zs;<`Z3c@yqP&Qw&;1TYa1CN5^Wc}3xz?wi+uwQDc;;eIAPS7_?6i%5NKXvNpI?pKS zC!c)vDcT?QR?sONuSp_bNcvT{cA{_HZZEqlZKENCJpP5PQ9QM1?mfGj+US11llwoTew6Ix%~^D9u)s?jmi?mtN`TFq zZ-NE#bH4qt!&=Jc2stb_<9_m&+}$56`nl!6gO*RYA+t~P7KFO8F+XhToSZ5Xy4m!13!#P ztZ1gaO)}!}AwQB|K+bJni&rn#Rb<|-Adw}N8m%!liyhFI+@I35F%5(f~q~ndDi4pYv=4$T}an-y=wA1k=SR1~)tMsAuL*qf!wqs#4lk+9? zDJ_E6ihpCLV^y%#Bz@g>dmmYyTVwfzPlDR&X?dh~9JZnV2W!SXKezo>Ck%IWG8d&0 z*CRw~fg;OCT|0zlN4OblFtIp5d}gU?Ev72Fx0fk9>y@Lc%I*;+#lil*^m%P{T`h<< ze~BzXS8bOM0<4(3yTF#;A0s5v5V8T-cy`}6K4t_c5cLnAbl}0`8kfY5dfwy0*HhP7 zxVNERMe`*zs(!Md5q71Qc9(J~nBep}?4>X?3>>ffI7s5I_^_a!@~=V;VVvsbTtlW3SqEs*RlNW#m3*YB(Q|#IFC2iI1SPJ=H!SGq4bIXGb(lc5ccKMt9&-1t1tM98SkZmy4cI0#d`%f-R=$bE6q{jk;m zA$2B)o#)ivUR4Tmd+p(+zuCeU+$|LmLYSbX54*4L6CBP3k$ZPJE?;G0mi5wLCB0bL z`eVW=BjLC2pLG9tXgw!F`Q~3G0wpDSMChTDQ=q*l_`};*5(`uEv;F8iK@at% zV;24?P>AIKCP`a&)GlEzul>2Ta#f=!>8o5qwoR2uS5FzS+>VZCC<3=8fX?-ZcBfv{ z#|w17ec;HQTgyV2IQR1tQGA6rmmuc(E*#Yje0%kC>nVPHQ1tQPnnbr7t8cLm=Ujv>tc%Gmwa_jr8G^Rl9q`z zC2?eQwpW7dy-PU10O(q9UHtQ#rN5 zxAfnF0$L8DH;ytIU*!Jau5=QeTXyw1t z+O+4rW7^N}aLv9&gaq$R(msEBZhCWFp1fqTX#TWQ&V!;w@D60SNLg(L<|%DjI$Ex# z(0rY*Y+$Q}x^jlqar*Wxn`O^VrL*F8rCK93=%KTA(tN7&-j`azMOxFG#^n!)#{pjF z1i9HDWloxY()wLa*Lc-ZtcHtWPrlWL+9T8~q7C{#=;}u)YfKzX?r*|~W+QXZX*V^s za0D=$|EKG>yX|Dx`6k`k>%`kT_FMn^*u!CUQsZ}`H-oSz^8ko4gSjpNo75uAG0Mjq z=7LpDLnx3MZo0~I!)bmJ&)PGV4BiRn5{6ZF*)N+_+ztxdUangvUxL%sV3yS?vo$rI zzo4l$Ov^Zbxt)300!hXHN?yZR1j%Hr3-i2(VQ1DoM=(S^@7 zm!Co}*IM)mj^?X8&6N!2T^1KyM)PxOI}vjk;>T;{?SUTiX=y3lf(X^~$oDw*;&{l| zFa96uRh0(N$?NW$JvB12X51sqgKaPT(p02BP#wb>T^rOEc&x;mwe4K_9Zy9Bg2a6d zt%d{yWEY!IBY6Ehj*eB4B?%^uY9*DwdNcr8m&Fs)z$AX);OxVod~4E#Ew7U!XbN@- zqQici3CQhmWO6k6Az;M^d9IZ>`-G;{UlzxT}PrmO?J zJ08^+^QV9@DjIWM+>tTK@2>w&6^?jUG=cB)KCq8wTKM?hj_$espx}U|#r%*3**SyQ0 z;A9VZ`9k;L3=jPIE{(Wg3B23gPA}!GNwP4I88($%2h)y00%47FmzSRimlM-^u?W>v=DCP8(@DQn z_kKM%bU1+C`%JX^DCwGd<&maMVc%hWl{n?jKXwZAJn1;`<=6{S%aL&Dg#To`TqG`a z2rze+m|rAKk1^LS5%H|q%irl1 z9F=ayDlWjv!TybKZjs0vr>MxIc2C=~#k$s^OID}E$fNkJ_O?l5u+6O}9*ev!*}m*> z5uzbKx3;?7Z7?`kF?G-r-$894FqM8ZRt9`!r8i8&Wr>>kV;rHVb)bcwm+y()=sQol zP&aWn*;dU7bWjT5UVlAwmG-jU38HH=I{!oHPWjfi>bGu7@>P!^Vu9^wX!(9@9bO$! z9ppa~>^PlMMq|-mc+1x4@oS3gLj3ystUGctnP@Iil{#M9{KJfFMS@a-%0X<;!ZA6f zh+mer-~BJ=fPE#Qr6(OxMSvS|jz1HeO)ANtFHf|w$!XcoTiSfr9O(#H;p{Uvzmjb~ zPlvP50bR>pe=l+EtN4D-{TO4?2H6D&^6A8=b<}2pfd{*+x*dzA%{5xrvH-3pbInYm zKtC4oIwX{xM5YMMG)+T8l*!!kQPz(~2Oji` zMgf((tEe670_6M;TnLt1+u0r4uRrbBbD9WmZ%@v1;b&=idnulk=Lwf7Zt^1g)YM?` zrDu(v<}`o?fk%rV!S@B5Eb;lUUai7?PIhr{V3qE0`!)N_MO0y6HLXMHkikLIzPx5L zfq#DVX6hU(<@IzvZ2Ww#yqe1{M5iewnB9fXtEqcG(YbZxZt>0#%#ul{CQzwyfe5|4 z_zkqY3;-$L{>tc{jC?;bx%RD13tnA6Gc)lZ2uk5~xc|d<7W-l2T(v7rP4;|%%$4Do z#MjkmZnQP@KSnzQxWvV2;5z<-qP~K2o{PdB)F9X>LRb%XqW#Fb+u1|OK_t;}N81F^ z`LBwKrUan^0#G+kA?-<{y+l7!pmP%&e*RKwII0kkjQ4}$D=)8Z&G2b83E@nlILG6U zx@p4|!)1UHe=mpWJc(DgI%DU>t3VLrX|+DTDx-gQyT>Hb}hXQG!-ltweV zt>#jX!4UY`ezW~#cfXmqm`gcvFXjuHgb97%oUvl(PP~Yz>pslF^!=zYw6$5q@Yrp6 z^jE{ROj78`H`xOpCS^dm1Lz}xk{x33+uPAplc=Kx>s%IG*xa-=?ehf}TB=zK(QFu4 zGMSSw9xSPXvwX`WQ36|$_#-P+a8RtM$-XJ`9VIR$Pw#FkFv&#BvOnqNxiAjS=qwie zts5NiIUbY&1C4Em>0T3eXndT{R_VRG$_cA zaMh#vgSkB(PCET82E+ zAV9VjhimoM_zaIj$;rV0uFfEsAQBuF237HFK^!-%rr?_&T2ouycCZGPQtn^fR2n0} z+;c#_vhygSfiDu{ejDE$%D3@t0_mk_D%IXqf+67>X6Cy(qVJ_$6@ z4l2cqZ@Hk;b2hEmGX0q{^Z4H(3v|pu29y;d>pP>2VFUuNnUILMvV9_238&==6gOy} z1q0x;(=zJnr?W>05|g;YUEeD^)?0cLbPy3^Gnm~LesEAjw14=ED&23-IRRsG7cnJ~ z_P+JOiHzudn=VlGC+9pb;DvWnDk+x?Bn9tGsX%DlAA(Lg^~WgSK)@#TDE3V< z+qGDstE<8s*bsJrO6D4!b|9P-9gTbwPXRv$DoAQ3OQ^q#f{oS70gw~VxJyK)e!yA- zE)sFZvVYQ(EC}@cx^iZX_!dqFAYSdDG2KV&@h2=Bjg1Nb`LxURP<^;`!Lkr-spQ*Z zz3mnJ3J|NrT{e1D;XmPM5-fFv$r=LR9 zxD~c2@<3PUpau?qG`F=ywx?V&b!vcc0w{jD{a4%y742UzNhP(U-2w~DEP{XCoz7_> zi=s?x@~N_8FSpFievsVkg5&beVufkL06q1QXbpUhQJ3fQQT#-^7h#;!*-H8b!oB_@ zA&6<6^v#;KZmLWXFVdIu#UOt*<&n9G{<8g{(Y$(W6EXWIcyrQRT4~hNzva@r?|$FR zE@b{1YZu2;Z*?+tU3w@$>)`mTL-&2}fX$+(vrQ&Pr}4SHIxKEoGC$L}9t9DiAPAO& zYVAUV%5YhLg@vG1A^TKf#o$69)4kF4c9j{nug`p^aD9)9*6*B)Q4O4J-G;(bS}y_@ zUQAH$$6yclZwbSyFg{yVm{4N7gQ?_Q3w37Zr%>&7vb2>wcw1E5$S!u!x4Gqc5Af}; z5Ll&JYVMuKBzcF6HMwiNZm9&wJXrF2qvsM8d#aRv@zL6rs=@G1Ca^WEltBmGUr3)X z?m2I?xnuna>M1b6!qJ?mw-q4D54MBjwQ}EHh2KWW6T;$HN*m;f6vg-1aLLwuuC)9I z@LMBgus|4-;)_0JJgz7b)q$i4dgR!i;x)3<%eI9WbX+ig$#~R;;E#Pt;bhd{#d0I=IOEodJ z;GVI&?8ii1qB!apAUH?9?D^xe0V==R1f3yfGw88l_b1JDMLZzPWmy)vjtn|E_Vn?D zLl&n*on-LTVcGoTPI}!O<yqB4KN9?OeSiEEZazkAZ$@AJ@Gcv<3Hz8E5l zT0&s`xkmkavd7DCCjjufR1}-KulJmM^&<*Fm>ed1w$2na3^<=tGj1I;1h<} z*O;GFV$Fv$x_@_8(_K`I=GM<)sbx7{zpE}d0A(S=kiM{fB0?Yr;Ec~dcz2*xN-Xqz zPfF)JCa*R07H|c=i-}=4|Ggn`KkH;dI$tL6{fK>iTiSc~0r#glEqN9v7dgP-i5SQg zo;l|Bjg^2BnJq+wqA$!sR5V+lx&+EmL{|zoI&KCX^UnM{T;ryb>{XQbfXM%WxANsM zab$FUv~*-P4o|bQq*>lH>siA#vzmM%h)A9jIm}6Yd7XV&(<-3t@;c(esea%KfiCgZ zjo+4M7nkGg+78dXy`dIpHuj7~$6nS2EBi;)AMky2%;`5D=^CVe-|^kl>J6noiC)mwyA9e7W-@}`qT+UWJ@$qqxKq&VD%O_r14g)RVT8*wXYE)E@&iWCD zUm=%Ye0~n5)2*1=Pk`jaf$HAU>FG}^gLORsm)e_jrrY>3A*zfYp zq3#WqWM({7K_B&+9o}%@@3HXi8pJl@ZkOZbj7k^wXBctt9uS`E?2eTm?``)@G$SmDpn} zjAZdWfOLX)5AWKDMt4c8JKoN$a_L%=A4}fb{c>x%W}dp+GT7gpR&50b@PvcpZEg4a zMog3h5C*}76Xe!MXoAsybirgO7hKCz<-$lq{%A|<60ZGRSI+=iE%ssj^1ObQ+Z0BQ zF?p9}RW3y!Bj5LY?%~KYi_rJ8!xcz_N;UemAqomV;ZK~qj^ER#F5V1-Ng2ymaU=F< zX?AL$pCq7JmW}Fm3vM#wh?lji1>So-&l*cfrss3(hMk`WjBfb^oOIQT7xP>UA)Ge( zv3G3=4-zyK(gXEls67ZjObe%yH$!atAn@tT*C zC?cMknQz+ezrI%S4t{&DGpk*|R#bmsA8ank=Qa2={e5pL%Nlu;F*|yYTbK{7A5~7r z#dU)N22A!(=2vP(5mWkwCllpLdpgU?o6smcrsqAI&URt#dwt*9F6`cKK)&NTic^L^ zDK;B$Dq3tUgJmU~*baE`hR8MN5@~Ql1*;kVt-SVso^`&INYBue%(g6+eiHhBqi#LE?JaA=2rZUH}zudi}E}- zL4$$qxRQX-4@d{a5Paz~*WO4;xmi_d@@MFuf+y4}QpWhi#nayK>E&;JZ?;0d>FhZS z*g0(wxFN_WD%tZJ0(^W8xx)J|X3D4@|0yy2M<3PWF(~O`Kyp6wUe_2{tu58P2pC`5 zaSYF1kiJ2!p>G4MYQ)BI9>=qLI^csx;9p)$oex!v6qo?;^OM+(5jQHd`jqz?B*(1% z?(n84ci31f)$TrAoU#P(l*YeG;$;!}>#j&qYXN={@LNn_M1%?eNiJ5*vKwqkiz7=^ zw-q@#V5L=+i!H*gnWYbl=)9+FaoHLE53~Mlhq@e6N&dazP6b~uA}oxzydMk;7OV#C z`Fkxh)ycHBIF$?7u7-iGHTD1Kk^bikHbztq_uTeUlM8*61ORLf!eU86wRIWNI;2XPv^X4OZ~Kh|#l*>8$2%FngsIXSCf;Y}BC)QpYU2@faw z-zG)Ehxx%fH0&Mj)gl+Xf$0=ZGiGrIw03pcUp4gIjp7VX$ZBN%y^PHE_V6h zQau%RS*tuu3i8toniI-stlCm;GL_U7fo^W9hxjkQOY|0Ef2rf`V63w^tybp#;@_zE z|2$KVLh;0V7I+c(j;2tK7c?gzF45*HN1BbNx6a~-Sn(z<+?cN9%ZJK{igIS3C)z!w z{FbN484FZ%vO$QW&wCVVQxTLJ1YATYJgM$PdBq>kMd)>@Z#d8`4*Z4j$JDx}Vxp7~NEC;5qjv(mW8#is7B z%s>+Q;QlVWf_2w*EP(W}tKKpb!v!Y=&NC{)PyOw%k(nc2!)qC|=bF#b%>c3j zQukLu#i>k0r4ERux|vtF-_6(6B}yDd+8SPX{WDjo#RB4F%*op77|>{)ef6#i;)eY< z$gX zFm;+eXbq~vN}>s{X|<4@-)%u5g)W$Q-RzSJOSwn|mY z01;nZIv+s6d=-NRp_6#I{QG}Zu%>bpLu6z03g@vXD7+|;#La{BM0QobMz*}P;|-3@ zUUg`%YdqyBI)-05Q%@%=MW*RkvfDrTo76xaql8+>qIzb6HwvOH0Hi5(5PqG}7?SXb zNepnIIHjIsPyzrdVZkP5NI-VksPpiEdDSTEKeoF3V+aH@P}_XCcibYnz(eKIqbRU# zCSf^qz6~w`0uh7_{47(qk$n$a0X+!9dK)lrAc^?R-xnkHa9u7X z|0QmJ=jM&;s~So2!@Uch^Qvmywzi4&D?iur4=p!eW3`lazbIyuc-t$^LPN*`j;0A6 zNBM!h?m$py4A$QiS*m~nGYeGsxE?*k12_Dad#ch>2e`*%VcE4hKUd3T&dwgyfbu9k zQ~*T5Ux+k;fLHy|>PPVIwg}+}KZIcQ2~{E=O>> zU||YA^*jEUG*HQ3O>clUvfp1tDSt~=yc{C5BWm+kks~e_35g6%138isNUyxY!h=@c zQ#knq&n1j<%Php0lZtpZS2NX%Vn4gwaKhi0C6+4C`A1Gl%IsMF zx?xmUpskD&e~Za|oWC+1rPB_ws2}S1$NK!;YolWS|GUY*v*-U(@^3Q!|CdbON$Qm@ Vbaj5T3qeEuDafk6t&%ni{a^an`_. .. _scikit-learn: https://scikit-learn.org/stable/ .. _getting-started guide: https://scikit-learn.org/stable/getting_started.html -The notebook files can be found `here `_. +.. nbgallery:: + :glob: + + examples/* diff --git a/docs/source/user_guide.rst b/docs/source/user_guide.rst index 228476f03e6..3fa729c618c 100644 --- a/docs/source/user_guide.rst +++ b/docs/source/user_guide.rst @@ -29,4 +29,7 @@ meant to provide a technical manual. user_guide/classification user_guide/regression user_guide/forecasting + user_guide/clustering + user_guide/annotation + user_guide/performance_metrics user_guide/resources diff --git a/docs/source/user_guide/annotation.rst b/docs/source/user_guide/annotation.rst new file mode 100644 index 00000000000..2bb1d03f68b --- /dev/null +++ b/docs/source/user_guide/annotation.rst @@ -0,0 +1,11 @@ +.. _user_guide_annotation: + +Annotation +========== + +.. note:: + + The user guide is under development. We have created a basic + structure and are looking for contributions to develop the user guide + further. For more details, please go to issue `#361 `_ on GitHub. diff --git a/docs/source/user_guide/clustering.rst b/docs/source/user_guide/clustering.rst new file mode 100644 index 00000000000..14a73bf71a7 --- /dev/null +++ b/docs/source/user_guide/clustering.rst @@ -0,0 +1,11 @@ +.. _user_guide_clustering: + +Clustering +========== + +.. note:: + + The user guide is under development. We have created a basic + structure and are looking for contributions to develop the user guide + further. For more details, please go to issue `#361 `_ on GitHub. diff --git a/docs/source/user_guide/learning_tasks.rst b/docs/source/user_guide/learning_tasks.rst index b4e2af41962..f33962837ee 100644 --- a/docs/source/user_guide/learning_tasks.rst +++ b/docs/source/user_guide/learning_tasks.rst @@ -9,15 +9,3 @@ Overview of Learning Tasks structure and are looking for contributions to develop the user guide further. For more details, please go to issue `#361 `_ on GitHub. - -Contents --------- - -* :ref:`classification` -* :ref:`regression` -* :ref:`forecasting` -* Clustering -* Time series annotation -* Panel forecasting -* Supervised forecasting -* Reduction diff --git a/docs/source/user_guide/performance_metrics.rst b/docs/source/user_guide/performance_metrics.rst new file mode 100644 index 00000000000..8cb5de28c83 --- /dev/null +++ b/docs/source/user_guide/performance_metrics.rst @@ -0,0 +1,15 @@ +.. _user_guide_performance_metrics: + +Performance Metrics +=================== + +.. note:: + + The user guide is under development. We have created a basic + structure and are looking for contributions to develop the user guide + further. For more details, please go to issue `#361 `_ on GitHub. + + +Forecasting +----------- diff --git a/docs/source/users.rst b/docs/source/users.rst new file mode 100644 index 00000000000..71fa36e20ec --- /dev/null +++ b/docs/source/users.rst @@ -0,0 +1,15 @@ +.. _user_documentation: + +Documentation +============= + +.. toctree:: + :maxdepth: 1 + + installation + tutorials + user_guide + estimator_overview + glossary + changelog + related_software diff --git a/sktime/annotation/__init__.py b/sktime/annotation/__init__.py index e69de29bb2d..e21bd818606 100644 --- a/sktime/annotation/__init__.py +++ b/sktime/annotation/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Implements time series annotation.""" diff --git a/sktime/annotation/adapters/__init__.py b/sktime/annotation/adapters/__init__.py index 564d7233fb7..dca93c8455f 100644 --- a/sktime/annotation/adapters/__init__.py +++ b/sktime/annotation/adapters/__init__.py @@ -1,4 +1,8 @@ +#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements adapters for time series annotation.""" + __all__ = ["PyODAnnotator"] from sktime.annotation.adapters._pyod import PyODAnnotator diff --git a/sktime/annotation/adapters/_pyod.py b/sktime/annotation/adapters/_pyod.py index 60131e35160..9a529de0801 100644 --- a/sktime/annotation/adapters/_pyod.py +++ b/sktime/annotation/adapters/_pyod.py @@ -1,4 +1,8 @@ +#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements outlier detection from pyOD.""" + import numpy as np from sktime.annotation.base._base import BaseSeriesAnnotator @@ -10,7 +14,7 @@ class PyODAnnotator(BaseSeriesAnnotator): - """Transformer that applies outlier detector from pyOD + """Transformer that applies outlier detector from pyOD. Parameters ---------- @@ -47,11 +51,10 @@ def _fit(self, X, Y=None): ------- self : returns a reference to self - State change - ------------ - creates fitted model (attributes ending in "_") + Notes + ----- + Create fitted model that sets attributes ending in "_". """ - X_np = X.to_numpy() if len(X_np.shape) == 1: @@ -74,7 +77,6 @@ def _predict(self, X): Y : pd.Series - annotations for sequence X exact format depends on annotation type """ - fmt = self.fmt labels = self.labels diff --git a/sktime/annotation/base/__init__.py b/sktime/annotation/base/__init__.py index 3da286f8f7f..1b9d1b0ffce 100644 --- a/sktime/annotation/base/__init__.py +++ b/sktime/annotation/base/__init__.py @@ -1,4 +1,8 @@ +#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements base classes for annotation in sktime.""" + __all__ = ["BaseSeriesAnnotator"] from sktime.annotation.base._base import BaseSeriesAnnotator diff --git a/sktime/annotation/base/_base.py b/sktime/annotation/base/_base.py index 35ed5d99581..82f85992c1b 100644 --- a/sktime/annotation/base/_base.py +++ b/sktime/annotation/base/_base.py @@ -1,6 +1,8 @@ +#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) """ -Base class template for annotator base type for time series stream +Base class template for annotator base type for time series stream. class name: BaseSeriesAnnotator @@ -48,7 +50,7 @@ class BaseSeriesAnnotator(BaseEstimator): Notes ----- Assumes "predict" data is temporal future of "fit" - single time series in both, no meta-data + Single time series in both, no meta-data. The base series annotator specifies the methods and method signatures that all annotators have to implement. @@ -74,17 +76,19 @@ def fit(self, X, Y=None): Parameters ---------- X : pd.DataFrame - training data to fit model to, time series + Training data to fit model to (time series). Y : pd.Series, optional - ground truth annotations for training if annotator is supervised + Ground truth annotations for training if annotator is supervised. + Returns ------- - self : returns a reference to self + self : + Reference to self. - State change - ------------ - creates fitted model (attributes ending in "_") - sets _is_fitted flag to true + Notes + ----- + Creates fitted model that updates attributes ending in "_". Sets + _is_fitted flag to True. """ check_labels(self.labels) check_fmt(self.fmt) @@ -110,14 +114,14 @@ def predict(self, X): Parameters ---------- - X : pd.DataFrame - data to annotate, time series + X : pd.DataFrame + Data to annotate (time series). Returns ------- - Y : pd.Series - annotations for sequence X - exact format depends on annotation type + Y : pd.Series + Annotations for sequence X exact format depends on annotation type. """ - self.check_is_fitted() X = check_series(X) @@ -129,23 +133,24 @@ def predict(self, X): return Y def update(self, X, Y=None): - """update model with new data and optional ground truth annotations + """Update model with new data and optional ground truth annotations. Parameters ---------- X : pd.DataFrame - training data to update model with, time series + Training data to update model with (time series). Y : pd.Series, optional - ground truth annotations for training if annotator is supervised + Ground truth annotations for training if annotator is supervised. + Returns ------- - self : returns a reference to self + self : + Reference to self. - State change - ------------ - updates fitted model (attributes ending in "_") + Notes + ----- + Updates fitted model that updates attributes ending in "_". """ - self.check_is_fitted() X = check_series(X) @@ -163,23 +168,22 @@ def update(self, X, Y=None): return self def update_predict(self, X): - """update model with new data and create annotations for it + """Update model with new data and create annotations for it. Parameters ---------- X : pd.DataFrame - training data to update model with, time series + Training data to update model with, time series. Returns ------- - Y : pd.Series - annotations for sequence X - exact format depends on annotation type + Y : pd.Series + Annotations for sequence X exact format depends on annotation type. - State change - ------------ - updates fitted model (attributes ending in "_") + Notes + ----- + Updates fitted model that updates attributes ending in "_". """ - X = check_series(X) self.update(X=X) @@ -195,16 +199,18 @@ def _fit(self, X, Y=None): Parameters ---------- X : pd.DataFrame - training data to fit model to, time series + Training data to fit model to time series. Y : pd.Series, optional - ground truth annotations for training if annotator is supervised + Ground truth annotations for training if annotator is supervised. + Returns ------- - self : returns a reference to self + self : + Reference to self. - State change - ------------ - creates fitted model (attributes ending in "_") + Notes + ----- + Updates fitted model that updates attributes ending in "_". """ raise NotImplementedError("abstract method") @@ -215,35 +221,37 @@ def _predict(self, X): Parameters ---------- - X : pd.DataFrame - data to annotate, time series + X : pd.DataFrame + Data to annotate, time series. Returns ------- - Y : pd.Series - annotations for sequence X - exact format depends on annotation type + Y : pd.Series + Annotations for sequence X exact format depends on annotation type. """ raise NotImplementedError("abstract method") def _update(self, X, Y=None): - """update model with new data and optional ground truth annotations + """Update model with new data and optional ground truth annotations. core logic Parameters ---------- X : pd.DataFrame - training data to update model with, time series + Training data to update model with time series Y : pd.Series, optional - ground truth annotations for training if annotator is supervised + Ground truth annotations for training if annotator is supervised. + Returns ------- - self : returns a reference to self + self : + Reference to self. - State change - ------------ - updates fitted model (attributes ending in "_") + Notes + ----- + Updates fitted model that updates attributes ending in "_". """ - # default/fallback: re-fit to all data self._fit(self._X, self._Y) diff --git a/sktime/base/_base.py b/sktime/base/_base.py index deba7789466..95e2cd7c813 100644 --- a/sktime/base/_base.py +++ b/sktime/base/_base.py @@ -72,9 +72,10 @@ def get_class_tags(cls): Returns ------- - collected_tags : dictionary of tag names : tag values - collected from _tags class attribute via nested inheritance - NOT overridden by dynamic tags set by set_tags or mirror_tags + collected_tags : dict + Dictionary of tag name : tag value pairs. Collected from _tags + class attribute via nested inheritance. NOT overridden by dynamic + tags set by set_tags or mirror_tags. """ collected_tags = dict() @@ -96,13 +97,16 @@ def get_class_tag(cls, tag_name, tag_value_default=None): Parameters ---------- - tag_name : str, name of tag value - tag_value_default : any type, default/fallback value if tag is not found + tag_name : str + Name of tag value. + tag_value_default : any type + Default/fallback value if tag is not found. Returns ------- - tag_value : value of the tag tag_name in self if found - if tag is not found, returns tag_value_default + tag_value : + Value of the `tag_name` tag in self. If not found, returns + `tag_value_default`. """ collected_tags = cls.get_class_tags() @@ -113,9 +117,10 @@ def get_tags(self): Returns ------- - collected_tags : dictionary of tag names : tag values - collected from _tags class attribute via nested inheritance - then any overrides and new tags from _tags_dynamic object attribute + collected_tags : dict + Dictionary of tag name : tag value pairs. Collected from _tags + class attribute via nested inheritance and then any overrides + and new tags from _tags_dynamic object attribute. """ collected_tags = self.get_class_tags() @@ -129,13 +134,16 @@ def get_tag(self, tag_name, tag_value_default=None): Parameters ---------- - tag_name : str, name of tag value - tag_value_default : any type, default/fallback value if tag is not found + tag_name : str + Name of tag value. + tag_value_default : any type + Default/fallback value if tag is not found. Returns ------- - tag_value : value of the tag tag_name in self if found - if tag is not found, returns tag_value_default + tag_value : + Value of the `tag_name` tag in self. If not found, returns + `tag_value_default`. """ collected_tags = self.get_tags() @@ -146,15 +154,18 @@ def set_tags(self, **tag_dict): Parameters ---------- - tag_dict : dictionary of tag names : tag values + tag_dict : dict + Dictionary of tag name : tag value pairs. Returns ------- - reference to self + Self : + Reference to self. - State change - ------------ - sets tag values in tag_dict as dynamic tags in self + Notes + ----- + Changes object state by settting tag values in tag_dict as dynamic tags + in self. """ self._tags_dynamic.update(deepcopy(tag_dict)) @@ -165,17 +176,20 @@ def clone_tags(self, estimator, tag_names=None): Parameters ---------- - estimator : an estimator inheriting from BaseEstimator - tag_names : list of str, or str; names of tags to clone - default = list of all tags in estimator + estimator : estimator inheriting from :class:BaseEstimator + tag_names : str or list of str, default = None + Names of tags to clone. If None then all tags in estimator are used + as `tag_names`. Returns ------- - reference to self + Self : + Reference to self. - State change - ------------ - sets tag values in tag_set from estimator as dynamic tags in self + Notes + ----- + Changes object state by setting tag values in tag_set from estimator as + dynamic tags in self. """ tags_est = deepcopy(estimator.get_tags()) diff --git a/sktime/base/_meta.py b/sktime/base/_meta.py index 8173c3014a7..73885dcc340 100644 --- a/sktime/base/_meta.py +++ b/sktime/base/_meta.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements meta estimator for estimators composed of other estimators.""" __author__ = ["Markus Löning"] __all__ = ["_HeterogenousMetaEstimator"] diff --git a/sktime/benchmarking/evaluation.py b/sktime/benchmarking/evaluation.py index 37756e4f5fb..68028e7853f 100644 --- a/sktime/benchmarking/evaluation.py +++ b/sktime/benchmarking/evaluation.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +"""Evaluator class for analyzing results of a machine learning experiment.""" __author__ = ["Viktor Kazakov", "Markus Löning", "Aaron Bostrom"] __all__ = ["Evaluator"] @@ -21,9 +22,7 @@ class Evaluator: - """ - Analyze results of machine learning experiments. - """ + """Analyze results of machine learning experiments.""" def __init__(self, results): if not isinstance(results, BaseResults): @@ -43,30 +42,34 @@ def __init__(self, results): @property def metric_names(self): + """Return metric names.""" return self._metric_names @property def metrics(self): + """Return metrics.""" self._check_is_evaluated() return self._metrics @property def metrics_by_strategy(self): + """Return metric by strategy.""" self._check_is_evaluated() return self._metrics_by_strategy @property def metrics_by_strategy_dataset(self): + """Return metrics by strategy and dataset.""" self._check_is_evaluated() return self._metrics_by_strategy_dataset def evaluate(self, metric, train_or_test="test", cv_fold="all"): - """ + """Evaluate estimator performance. + Calculates the average prediction error per estimator as well as the prediction error achieved by each estimator on individual datasets. """ - # check input if isinstance(cv_fold, int) and cv_fold >= 0: cv_folds = [cv_fold] # if single fold, make iterable @@ -135,7 +138,7 @@ def evaluate(self, metric, train_or_test="test", cv_fold="all"): return self._metrics_by_strategy def plot_boxplots(self, metric_name=None, **kwargs): - """Box plot of metric""" + """Box plot of metric.""" self._check_is_evaluated() metric_name = self._validate_metric_name(metric_name) column = self._get_column_name(metric_name, suffix="mean") @@ -152,7 +155,8 @@ def plot_boxplots(self, metric_name=None, **kwargs): return fig, ax def rank(self, metric_name=None, ascending=False): - """ + """Determine estimator ranking. + Calculates the average ranks based on the performance of each estimator on each dataset """ @@ -179,9 +183,7 @@ def rank(self, metric_name=None, ascending=False): return ranked def t_test(self, metric_name=None): - """ - Runs t-test on all possible combinations between the estimators. - """ + """T-test on all possible combinations between the estimators.""" self._check_is_evaluated() metric_name = self._validate_metric_name(metric_name) metrics_per_estimator_dataset = self._get_metrics_per_estimator_dataset( @@ -219,9 +221,8 @@ def t_test(self, metric_name=None): return t_df, values_df_multiindex def sign_test(self, metric_name=None): - """ - Non-parametric test for test for consistent differences between - pairs of observations. + """Non-parametric test for consistent differences between observation pairs. + See ``_ for details about the test and ``_. @@ -295,8 +295,8 @@ def ranksum_test(self, metric_name=None): return ranksum_df, values_df_multiindex def t_test_with_bonferroni_correction(self, metric_name=None, alpha=0.05): - """ - correction used to counteract multiple comparisons + """T-test with correction used to counteract multiple comparisons. + https://en.wikipedia.org/wiki/Bonferroni_correction """ self._check_is_evaluated() @@ -320,13 +320,14 @@ def t_test_with_bonferroni_correction(self, metric_name=None, alpha=0.05): return bonfer_df def wilcoxon_test(self, metric_name=None): - """http://en.wikipedia.org/wiki/Wilcoxon_signed-rank_test + """Wilcoxon signed-rank test. + + http://en.wikipedia.org/wiki/Wilcoxon_signed-rank_test `Wilcoxon signed-rank test `_. Tests whether two related paired samples come from the same - distribution. - In particular, it tests whether the distribution of the differences - x-y is symmetric about zero + distribution. In particular, it tests whether the distribution of the + differences x-y is symmetric about zero """ self._check_is_evaluated() metric_name = self._validate_metric_name(metric_name) @@ -355,7 +356,8 @@ def wilcoxon_test(self, metric_name=None): return wilcoxon_df def friedman_test(self, metric_name=None): - """ + """Friedman test. + The Friedman test is a non-parametric statistical test used to detect differences in treatments across multiple test attempts. The procedure involves @@ -383,7 +385,8 @@ def friedman_test(self, metric_name=None): return friedman_test, values_df def nemenyi(self, metric_name=None): - """ + """Nemenyi test. + Post-hoc test run if the `friedman_test` reveals statistical significance. For more information see `Nemenyi test @@ -406,8 +409,7 @@ def nemenyi(self, metric_name=None): return nemenyi def fit_runtime(self, unit="s", train_or_test="test", cv_fold="all"): - """ - Calculates the average time for fitting the strategy + """Calculate the average time for fitting the strategy. Parameters ---------- @@ -419,7 +421,6 @@ def fit_runtime(self, unit="s", train_or_test="test", cv_fold="all"): run_times: Pandas DataFrame average run times per estimator and strategy """ - # check input if isinstance(cv_fold, int) and cv_fold >= 0: cv_folds = [cv_fold] # if single fold, make iterable @@ -524,11 +525,11 @@ def fit_runtime(self, unit="s", train_or_test="test", cv_fold="all"): # return self._metrics_by_strategy def plot_critical_difference_diagram(self, metric_name=None, alpha=0.1): - """Plot critical difference diagrams + """Plot critical difference diagrams. - References: - ----------- - original implementation by Aaron Bostrom, modified by Markus Löning + References + ---------- + original implementation by Aaron Bostrom, modified by Markus Löning. """ self._check_is_evaluated() metric_name = self._validate_metric_name(metric_name) @@ -752,11 +753,11 @@ def plot_critical_difference_diagram(self, metric_name=None, alpha=0.1): return fig, ax def _get_column_name(self, metric_name, suffix="mean"): - """Helper function to get column name in computed metrics dataframe""" + """Get column name in computed metrics dataframe.""" return f"{metric_name}_{suffix}" def _check_is_evaluated(self): - """Check if evaluator has evaluated any metrics""" + """Check if evaluator has evaluated any metrics.""" if len(self._metric_names) == 0: raise NotEvaluatedError( "This evaluator has not evaluated any metric yet. Please call " @@ -765,7 +766,7 @@ def _check_is_evaluated(self): ) def _validate_metric_name(self, metric_name): - """Check if metric has already been evaluated""" + """Check if metric has already been evaluated.""" if metric_name is None: metric_name = self._metric_names[ -1 @@ -780,7 +781,7 @@ def _validate_metric_name(self, metric_name): return metric_name def _get_metrics_per_estimator_dataset(self, metric_name): - """Helper function to get old format back, to be deprecated""" + """Get old format back, to be deprecated.""" # TODO deprecate in favor of new pandas data frame based data # representation column = f"{metric_name}_mean" @@ -795,7 +796,7 @@ def _get_metrics_per_estimator_dataset(self, metric_name): return d def _get_metrics_per_estimator(self, metric_name): - """Helper function to get old format back, to be deprecated""" + """Get old format back, to be deprecated.""" # TODO deprecate in favor of new pandas data frame based data # representation columns = [ diff --git a/sktime/benchmarking/metrics.py b/sktime/benchmarking/metrics.py index 9230fa9a9ad..b26d49115c0 100644 --- a/sktime/benchmarking/metrics.py +++ b/sktime/benchmarking/metrics.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Implements metrics for pairwise and aggregate comparison.""" __all__ = ["PairwiseMetric", "AggregateMetric"] __author__ = ["Viktor Kazakov", "Markus Löning"] @@ -7,6 +9,16 @@ class PairwiseMetric(BaseMetric): + """Compute metric pairwise. + + Parameters + ---------- + func : function + Function that computes the pairwise metric. + + name : str + Name of the metric + """ def __init__(self, func, name=None, **kwargs): name = func.__name__ if name is None else name @@ -14,27 +26,41 @@ def __init__(self, func, name=None, **kwargs): super(PairwiseMetric, self).__init__(name=name, **kwargs) def compute(self, y_true, y_pred): + """Compute metric and standard error.""" # compute mean mean = self.func(y_true, y_pred) # compute stderr based on pairwise metrics n_instances = len(y_true) pointwise_metrics = np.array( - [self.func([y_true[i]], [y_pred[i]]) for i in range(n_instances)]) + [self.func([y_true[i]], [y_pred[i]]) for i in range(n_instances)] + ) stderr = np.std(pointwise_metrics) / np.sqrt( - n_instances - 1) # sample standard error of the mean + n_instances - 1 + ) # sample standard error of the mean return mean, stderr class AggregateMetric(BaseMetric): + """Compute metric pairwise. + + Parameters + ---------- + func : function + Function that computes the pairwise metric. + + name : str + Name of the metric + """ def __init__(self, func, method="jackknife", name=None, **kwargs): allowed_methods = ("jackknife",) if method not in allowed_methods: raise NotImplementedError( f"Provided method is not implemented yet. " - f"Currently only: {allowed_methods} are implemented") + f"Currently only: {allowed_methods} are implemented" + ) self.method = method name = func.__name__ if name is None else name @@ -43,10 +69,10 @@ def __init__(self, func, method="jackknife", name=None, **kwargs): super(AggregateMetric, self).__init__(name=name, **kwargs) def compute(self, y_true, y_pred): - """Compute metric and standard error + """Compute metric and standard error. - References: - ----------- + References + ---------- .. [1] Efron and Stein, (1981), "The jackknife estimate of variance." .. [2] McIntosh, Avery. "The Jackknife Estimation Method". @@ -72,8 +98,8 @@ def compute(self, y_true, y_pred): # compute metrics on jackknife samples jack_pointwise_metric = np.array( - [self.func(y_true[idx], y_pred[idx], **self.kwargs) - for idx in jack_idx]) + [self.func(y_true[idx], y_pred[idx], **self.kwargs) for idx in jack_idx] + ) # compute standard error over jackknifed metrics jack_stderr = self._compute_jackknife_stderr(jack_pointwise_metric) @@ -81,7 +107,7 @@ def compute(self, y_true, y_pred): @staticmethod def _compute_jackknife_stderr(x): - """Compute standard error of jacknife samples + """Compute standard error of jacknife samples. References ---------- @@ -93,7 +119,7 @@ def _compute_jackknife_stderr(x): @staticmethod def _jackknife_resampling(x): - """Performs jackknife resampling on numpy arrays. + """Perform jackknife resampling on numpy arrays. Jackknife resampling is a technique to generate 'n' deterministic samples diff --git a/sktime/classification/base.py b/sktime/classification/base.py index c5c0eb553fd..deaaca9f6e2 100644 --- a/sktime/classification/base.py +++ b/sktime/classification/base.py @@ -95,12 +95,13 @@ def fit(self, X, y): Returns ------- - self : reference to self. + self : + Reference to self. - State change - ------------ - creates fitted model (attributes ending in "_") - sets is_fitted flag to true + Notes + ----- + Changes state by creating a fitted model that updates attributes + ending in "_" and sets is_fitted flag to True. """ coerce_to_numpy = self.get_class_tag("coerce-X-to-numpy", False) @@ -186,11 +187,13 @@ def _fit(self, X, y): Returns ------- - self : reference to self. + self : + Reference to self. - State change - ------------ - creates fitted model (attributes ending in "_") + Notes + ----- + Changes state by creating a fitted model that updates attributes + ending in "_" and sets is_fitted flag to True. """ raise NotImplementedError("abstract method") diff --git a/sktime/classification/compose/_column_ensemble.py b/sktime/classification/compose/_column_ensemble.py index c4806f02d4b..a0ddc3c55e8 100644 --- a/sktime/classification/compose/_column_ensemble.py +++ b/sktime/classification/compose/_column_ensemble.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -""" ColumnEnsembleClassifier: For Multivariate Time Series Classification. -Builds classifiers on each dimension (column) independently +"""ColumnEnsembleClassifier: For Multivariate Time Series Classification. +Builds classifiers on each dimension (column) independently. """ __author__ = ["Aaron Bostrom"] @@ -57,9 +57,7 @@ def _validate_estimators(self): # this check whether the column input was a slice object or a tuple. def _validate_column_callables(self, X): - """ - Converts callable column specifications. - """ + """Convert callable column specifications.""" columns = [] for _, _, column in self.estimators: if callable(column): @@ -68,10 +66,7 @@ def _validate_column_callables(self, X): self._columns = columns def _validate_remainder(self, X): - """ - Validates ``remainder`` and defines ``_remainder`` targeting - the remaining columns. - """ + """Validate ``remainder`` and defines ``_remainder``.""" is_estimator = hasattr(self.remainder, "fit") or hasattr( self.remainder, "predict_proba" ) @@ -90,15 +85,12 @@ def _validate_remainder(self, X): self._remainder = ("remainder", self.remainder, remaining_idx) def _iter(self, replace_strings=False): - """ - Generate (name, estimator, column) tuples. + """Generate (name, estimator, column) tuples. If fitted=True, use the fitted transformations, else use the user specified transformations updated with converted column names and potentially appended with transformer for remainder. - """ - if self.is_fitted: estimators = self.estimators_ else: @@ -124,7 +116,7 @@ def _iter(self, replace_strings=False): def fit(self, X, y): # the data passed in could be an array of dataframes? - """Fit all estimators, fit the data + """Fit all estimators, fit the data. Parameters ---------- @@ -172,7 +164,7 @@ def _collect_probas(self, X): ) def predict_proba(self, X): - """Predict class probabilities for X in 'soft' voting """ + """Predict class probabilities for X using 'soft' voting.""" self.check_is_fitted() avg = np.average(self._collect_probas(X), axis=0) return avg @@ -185,44 +177,42 @@ def predict(self, X): class ColumnEnsembleClassifier(BaseColumnEnsembleClassifier): """Applies estimators to columns of an array or pandas DataFrame. - This estimator allows different columns or column subsets of the input - to be transformed separately and the features generated by each - transformer - will be ensembled to form a single output. - - Parameters - ---------- - estimators : list of tuples - List of (name, transformer, column(s)) tuples specifying the - transformer objects to be applied to subsets of the data. - - name : string - Like in Pipeline and FeatureUnion, this allows the - transformer and - its parameters to be set using ``set_params`` and searched - in grid - search. - Estimator : estimator or {'drop'} - Estimator must support `fit` and `predict_proba`. Special-cased - strings 'drop' and 'passthrough' are accepted as well, to - indicate to drop the columns - column(s) : string or int, array-like of string or int, slice, \ - boolean mask array or callable - - - remainder : {'drop', 'passthrough'} or estimator, default 'drop' - By default, only the specified columns in `transformations` are - transformed and combined in the output, and the non-specified - columns are dropped. (default of ``'drop'``). - By specifying ``remainder='passthrough'``, all remaining columns - that - were not specified in `transformations` will be automatically passed - through. This subset of columns is concatenated with the output of - the transformations. - By setting ``remainder`` to be an estimator, the remaining - non-specified columns will use the ``remainder`` estimator. The - estimator must support `fit` and `transform`. + This estimator allows different columns or column subsets of the input + to be transformed separately and the features generated by each + transformer + will be ensembled to form a single output. + Parameters + ---------- + estimators : list of tuples + List of (name, estimator, column(s)) tuples specifying the + transformer objects to be applied to subsets of the data. + + name : string + Like in Pipeline and FeatureUnion, this allows the + transformer and + its parameters to be set using ``set_params`` and searched + in grid + search. + estimator : or {'drop'} + Estimator must support `fit` and `predict_proba`. Special-cased + strings 'drop' and 'passthrough' are accepted as well, to + indicate to drop the columns + column(s) : string or int, array-like of string or int, slice, \ + boolean mask array or callable + + remainder : {'drop', 'passthrough'} or estimator, default 'drop' + By default, only the specified columns in `transformations` are + transformed and combined in the output, and the non-specified + columns are dropped. (default of ``'drop'``). + By specifying ``remainder='passthrough'``, all remaining columns + that + were not specified in `transformations` will be automatically passed + through. This subset of columns is concatenated with the output of + the transformations. + By setting ``remainder`` to be an estimator, the remaining + non-specified columns will use the ``remainder`` estimator. The + estimator must support `fit` and `transform`. """ _required_parameters = ["estimators"] @@ -396,10 +386,9 @@ def _get_column_indices(X, key): def _is_empty_column_selection(column): - """ - Return True if the column selection is empty (empty list or all-False - boolean array). + """Check if column selection is empty. + Both an empty list or all-False boolean array are considered empty. """ if hasattr(column, "dtype") and np.issubdtype(column.dtype, np.bool_): return not column.any() diff --git a/sktime/classification/dictionary_based/_boss.py b/sktime/classification/dictionary_based/_boss.py index e170d388ddc..ed0c69d56ac 100644 --- a/sktime/classification/dictionary_based/_boss.py +++ b/sktime/classification/dictionary_based/_boss.py @@ -79,11 +79,13 @@ class BOSSEnsemble(BaseClassifier): See Also -------- - :py:class:`IndividualBOSS`, :py:class:`ContractableBOSS` + IndividualBOSS, ContractableBOSS + Notes + ----- For the Java version, see - `TSML `_. + `TSML `_. References ---------- @@ -91,8 +93,8 @@ class BOSSEnsemble(BaseClassifier): in the presence of noise", Data Mining and Knowledge Discovery, 29(6): 2015 https://link.springer.com/article/10.1007/s10618-014-0377-7 - Example - ------- + Examples + -------- >>> from sktime.classification.dictionary_based import BOSSEnsemble >>> from sktime.datasets import load_italy_power_demand >>> X_train, y_train = load_italy_power_demand(split="train", return_X_y=True) @@ -423,11 +425,13 @@ class IndividualBOSS(BaseClassifier): See Also -------- - :py:class:`BOSSEnsemble`, :py:class:`ContractableBOSS` + BOSSEnsemble, ContractableBOSS + Notes + ----- For the Java version, see - `TSML `_. + `TSML `_. References ---------- diff --git a/sktime/classification/dictionary_based/_cboss.py b/sktime/classification/dictionary_based/_cboss.py index 3659214c71c..f6986348888 100644 --- a/sktime/classification/dictionary_based/_cboss.py +++ b/sktime/classification/dictionary_based/_cboss.py @@ -83,11 +83,13 @@ class ContractableBOSS(BaseClassifier): See Also -------- - :py:class:`BOSSEnsemble`, :py:class:`IndividualBOSS` + BOSSEnsemble, IndividualBOSS + Notes + ----- For the Java version, see - `TSML `_. + `TSML `_. References ---------- diff --git a/sktime/classification/dictionary_based/_muse.py b/sktime/classification/dictionary_based/_muse.py index be32e3fe47d..d0738bb95c3 100644 --- a/sktime/classification/dictionary_based/_muse.py +++ b/sktime/classification/dictionary_based/_muse.py @@ -87,8 +87,8 @@ class MUSE(BaseClassifier): https://github.com/uea-machine-learning/tsml/blob/master/src/main/java/tsml/ classifiers/multivariate/WEASEL_MUSE.java - Example - ------- + Examples + -------- >>> from sktime.classification.dictionary_based import MUSE >>> from sktime.datasets import load_italy_power_demand >>> X_train, y_train = load_italy_power_demand(split="train", return_X_y=True) diff --git a/sktime/classification/dictionary_based/_tde.py b/sktime/classification/dictionary_based/_tde.py index bbb85a17b7f..4d08c6df86c 100644 --- a/sktime/classification/dictionary_based/_tde.py +++ b/sktime/classification/dictionary_based/_tde.py @@ -112,8 +112,8 @@ class TemporalDictionaryEnsemble(BaseClassifier): https://github.com/uea-machine-learning/tsml/blob/master/src/main/java/ tsml/classifiers/dictionary_based/TDE.java - Example - ------- + Examples + -------- >>> from sktime.classification.dictionary_based import TemporalDictionaryEnsemble >>> from sktime.datasets import load_italy_power_demand >>> X_train, y_train = load_italy_power_demand(split="train", return_X_y=True) diff --git a/sktime/classification/dictionary_based/_weasel.py b/sktime/classification/dictionary_based/_weasel.py index 1642ef44a1a..6988918bc7d 100644 --- a/sktime/classification/dictionary_based/_weasel.py +++ b/sktime/classification/dictionary_based/_weasel.py @@ -100,8 +100,8 @@ class WEASEL(BaseClassifier): } https://dl.acm.org/doi/10.1145/3132847.3132980 - Example - ------- + Examples + -------- >>> from sktime.classification.dictionary_based import WEASEL >>> from sktime.datasets import load_italy_power_demand >>> X_train, y_train = load_italy_power_demand(split="train", return_X_y=True) diff --git a/sktime/classification/distance_based/_proximity_forest.py b/sktime/classification/distance_based/_proximity_forest.py index 9c98895f1db..d026f6ace22 100644 --- a/sktime/classification/distance_based/_proximity_forest.py +++ b/sktime/classification/distance_based/_proximity_forest.py @@ -122,7 +122,7 @@ def _derivative_distance(distance_measure, transformer): :param distance_measure: the distance measure to use :param transformer: the transformer to use - :return: a distance measure function with built in transformation + :returns: a distance measure function with built in transformation """ def distance(instance_a, instance_b, **params): @@ -140,7 +140,7 @@ def distance_predefined_params(distance_measure, **params): :param distance_measure: the distance measure to use :param params: the parameters to use in the distance measure - :return: a distance measure with no parameters + :returns: a distance measure with no parameters """ def distance(instance_a, instance_b): @@ -154,7 +154,7 @@ def cython_wrapper(distance_measure): Converts to 1 column per dimension format. :param distance_measure: distance measure to wrap - :return: a distance measure which automatically formats data for cython + :returns: a distance measure which automatically formats data for cython distance measures """ @@ -332,7 +332,7 @@ def dtw_distance_measure_getter(X): """Generate the dtw distance measure. :param X: dataset to derive parameter ranges from - :return: distance measure and parameter range dictionary + :returns: distance measure and parameter range dictionary """ return { "distance_measure": [cython_wrapper(dtw_distance)], @@ -344,7 +344,7 @@ def msm_distance_measure_getter(X): """Generate the msm distance measure. :param X: dataset to derive parameter ranges from - :return: distance measure and parameter range dictionary + :returns: distance measure and parameter range dictionary """ n_dimensions = 1 # todo use other dimensions return { @@ -459,7 +459,7 @@ def erp_distance_measure_getter(X): """Generate the erp distance measure. :param X: dataset to derive parameter ranges from - :return: distance measure and parameter range dictionary + :returns: distance measure and parameter range dictionary """ stdp = _stdp(X) instance_length = max_instance_length(X) # todo should this use the max instance @@ -479,7 +479,7 @@ def lcss_distance_measure_getter(X): """Generate the lcss distance measure. :param X: dataset to derive parameter ranges from - :return: distance measure and parameter range dictionary + :returns: distance measure and parameter range dictionary """ stdp = _stdp(X) instance_length = max_instance_length(X) # todo should this use the max instance @@ -499,7 +499,7 @@ def twe_distance_measure_getter(X): """Generate the twe distance measure. :param X: dataset to derive parameter ranges from - :return: distance measure and parameter range dictionary + :returns: distance measure and parameter range dictionary """ return { "distance_measure": [cython_wrapper(twe_distance)], @@ -523,7 +523,7 @@ def wdtw_distance_measure_getter(X): """Generate the wdtw distance measure. :param X: dataset to derive parameter ranges from - :return: distance measure and parameter range dictionary + :returns: distance measure and parameter range dictionary """ return { "distance_measure": [cython_wrapper(wdtw_distance)], @@ -535,7 +535,7 @@ def euclidean_distance_measure_getter(X): """Generate the ed distance measure. :param X: dataset to derive parameter ranges from - :return: distance measure and parameter range dictionary + :returns: distance measure and parameter range dictionary """ return {"distance_measure": [cython_wrapper(dtw_distance)], "w": [0]} @@ -545,7 +545,7 @@ def setup_wddtw_distance_measure_getter(transformer): Bakes the derivative transformer into the dtw distance measure :param transformer: the transformer to use - :return: a getter to produce the distance measure + :returns: a getter to produce the distance measure """ def getter(X): @@ -564,7 +564,7 @@ def setup_ddtw_distance_measure_getter(transformer): Bakes the derivative transformer into the dtw distance measure :param transformer: the transformer to use - :return: a getter to produce the distance measure + :returns: a getter to produce the distance measure """ def getter(X): @@ -582,7 +582,7 @@ def setup_all_distance_measure_getter(proximity): """All distance measure getter functions from a proximity object. :param proximity: a PT / PF / PS - :return: a list of distance measure getters + :returns: a list of distance measure getters """ transformer = _CachedTransformer(DerivativeSlopeTransformer()) distance_measure_getters = [ @@ -602,7 +602,7 @@ def pick_rand_distance_measure(proximity): :param proximity: proximity object containing distance measures, ranges and dataset - :return: a distance measure with no parameters + :returns: a distance measure with no parameters """ random_state = proximity.random_state X = proximity.X @@ -830,7 +830,7 @@ def _distance_to_exemplars_inst(exemplars, instance, distance_measure): :param instance: the instance to compare to each exemplar :param distance_measure: the distance measure to provide similarity values - :return: list of distances to each exemplar + :returns: list of distances to each exemplar """ n_exemplars = len(exemplars) distances = np.empty(n_exemplars) @@ -853,8 +853,8 @@ def distance_to_exemplars(self, X): ---------- X: the dataset containing a list of instances - Return - ------ + Returns + ------- 2d numpy array of distances from each instance to each exemplar (instance by exemplar) """ @@ -918,8 +918,8 @@ def find_closest_exemplar_indices(self, X): ---------- X: the dataframe containing instances - Return - ------ + Returns + ------- 1d numpy array of indices, one for each instance, reflecting the index of the closest exemplar """ diff --git a/sktime/classification/distance_based/_time_series_neighbors.py b/sktime/classification/distance_based/_time_series_neighbors.py index 87f16dda67e..30735569a17 100644 --- a/sktime/classification/distance_based/_time_series_neighbors.py +++ b/sktime/classification/distance_based/_time_series_neighbors.py @@ -94,8 +94,8 @@ class KNeighborsTimeSeriesClassifier(_KNeighborsClassifier, BaseClassifier): 'wdtw','lcss','erp','msm','twe'}: default ='dtw' distance_params : dictionary for metric parameters: default = None - Example - ------- + Examples + -------- >>> from sktime.classification.distance_based import KNeighborsTimeSeriesClassifier >>> from sktime.datasets import load_basic_motions >>> X_train, y_train = load_basic_motions(return_X_y=True, split="train") diff --git a/sktime/classification/feature_based/_catch22_classifier.py b/sktime/classification/feature_based/_catch22_classifier.py index a80496991bc..7eb03ed469b 100644 --- a/sktime/classification/feature_based/_catch22_classifier.py +++ b/sktime/classification/feature_based/_catch22_classifier.py @@ -46,9 +46,11 @@ class Catch22Classifier(BaseClassifier): See Also -------- - :py:class:`Catch22` + Catch22 - Authors 'catch22ForestClassifier `_. + Notes + ----- + Authors `catch22ForestClassifier `_. For the Java version, see `tsml `_. @@ -59,8 +61,8 @@ class Catch22Classifier(BaseClassifier): Data Mining and Knowledge Discovery 33.6 (2019): 1821-1852. https://link.springer.com/article/10.1007/s10618-019-00647-x - Example - ------- + Examples + -------- >>> from sktime.classification.feature_based import Catch22Classifier >>> from sktime.datasets import load_italy_power_demand >>> X_train, y_train = load_italy_power_demand(split="train", return_X_y=True) diff --git a/sktime/classification/feature_based/_matrix_profile_classifier.py b/sktime/classification/feature_based/_matrix_profile_classifier.py index c3aa7c87f54..8f30f1f3a4e 100644 --- a/sktime/classification/feature_based/_matrix_profile_classifier.py +++ b/sktime/classification/feature_based/_matrix_profile_classifier.py @@ -44,7 +44,7 @@ class MatrixProfileClassifier(BaseClassifier): See Also -------- - :py:class:`MatrixProfile` + MatrixProfile References ---------- @@ -53,8 +53,8 @@ class MatrixProfileClassifier(BaseClassifier): Knowledge Discovery 32.1 (2018): 83-123. https://link.springer.com/article/10.1007/s10618-017-0519-9 - Example - ------- + Examples + -------- >>> from sktime.classification.feature_based import MatrixProfileClassifier >>> from sktime.datasets import load_italy_power_demand >>> X_train, y_train = load_italy_power_demand(split="train", return_X_y=True) diff --git a/sktime/classification/feature_based/_signature_classifier.py b/sktime/classification/feature_based/_signature_classifier.py index f612faebd0d..de1938c5dfd 100644 --- a/sktime/classification/feature_based/_signature_classifier.py +++ b/sktime/classification/feature_based/_signature_classifier.py @@ -82,10 +82,10 @@ class SignatureClassifier(BaseClassifier): See Also -------- - :py:class:`SignatureTransformer` + SignatureTransformer - Example - ------- + Examples + -------- >>> from sktime.classification.feature_based import SignatureClassifier >>> from sktime.datasets import load_italy_power_demand >>> X_train, y_train = load_italy_power_demand(split="train", return_X_y=True) diff --git a/sktime/classification/feature_based/_tsfresh_classifier.py b/sktime/classification/feature_based/_tsfresh_classifier.py index 6a12072a5fe..fe490939875 100644 --- a/sktime/classification/feature_based/_tsfresh_classifier.py +++ b/sktime/classification/feature_based/_tsfresh_classifier.py @@ -56,7 +56,7 @@ class TSFreshClassifier(BaseClassifier): See Also -------- - :py:class:`TSFreshFeatureExtractor`, :py:class:`TSFreshRelevantFeatureExtractor` + TSFreshFeatureExtractor, TSFreshRelevantFeatureExtractor References ---------- @@ -65,8 +65,8 @@ class TSFreshClassifier(BaseClassifier): (2018): 72-77. https://www.sciencedirect.com/science/article/pii/S0925231218304843 - Example - ------- + Examples + -------- >>> from sktime.classification.feature_based import TSFreshClassifier >>> from sktime.datasets import load_italy_power_demand >>> X_train, y_train = load_italy_power_demand(split="train", return_X_y=True) diff --git a/sktime/classification/interval_based/_rise.py b/sktime/classification/interval_based/_rise.py index aa02ffd357e..5d6cd46b38d 100644 --- a/sktime/classification/interval_based/_rise.py +++ b/sktime/classification/interval_based/_rise.py @@ -74,9 +74,7 @@ def _make_estimator(base_estimator, random_state=None): def _select_interval(min_interval, max_interval, series_length, rng, method=3): - """ - private function used to select an interval for a single tree - """ + """Private function used to select an interval for a single tree.""" interval = np.empty(2, dtype=int) if method == 0: interval[0] = rng.randint(series_length - min_interval) @@ -102,9 +100,7 @@ def _select_interval(min_interval, max_interval, series_length, rng, method=3): def _produce_intervals( n_estimators, min_interval, max_interval, series_length, rng, method=3 ): - """ - private function used to produce intervals for all trees - """ + """Private function used to produce intervals for all trees.""" intervals = np.empty((n_estimators, 2), dtype=int) if method == 0: # just keep it as a backup, untested @@ -312,7 +308,9 @@ def fit(self, X, y): return self def predict(self, X): - """Find predictions for all cases in X. Built on top of `predict_proba. + """Find predictions for all cases in X. + + Built on top of `predict_proba`. Parameters ---------- @@ -339,8 +337,8 @@ def predict_proba(self, X): single column (i.e., univariate classification). RISE has no bespoke method for multivariate classification as yet. - Local variables - --------------- + Attributes + ---------- n_instances : int Number of cases to classify. n_columns : int @@ -396,8 +394,8 @@ def acf(x, max_lag): max_lag: int The number of ACF terms to find. - Return - ---------- + Returns + ------- y : array-like shape = [max_lag] """ y = np.empty(max_lag) @@ -499,8 +497,8 @@ def matrix_acf(x, num_cases, max_lag): max_lag: int The number of ACF terms to find. - Return - ---------- + Returns + ------- y : array-like shape = [num_cases,max_lag] """ @@ -542,7 +540,8 @@ def matrix_acf(x, num_cases, max_lag): def ps(x, sign=1, n=None, pad="mean"): - """ + """Power spectrum transformer. + Power spectrum transform, currently calculated using np function. It would be worth looking at ff implementation, see difference in speed to java. @@ -557,8 +556,8 @@ def ps(x, sign=1, n=None, pad="mean"): see numpy.pad for more details https://numpy.org/doc/stable/reference/generated/numpy.pad.html - Return - ---------- + Returns + ------- y : array-like shape = [len(x)/2] """ x_len = x.shape[-1] diff --git a/sktime/classification/shapelet_based/_stc.py b/sktime/classification/shapelet_based/_stc.py index 2e06979f5c8..3ce223dba56 100644 --- a/sktime/classification/shapelet_based/_stc.py +++ b/sktime/classification/shapelet_based/_stc.py @@ -79,6 +79,8 @@ def fit(self, X, y): """Perform a shapelet transform then builds a random forest. Contract default for ST is 5 hours + + Parameters ---------- X : array-like or sparse matrix of shape = [n_instances, series_length] or shape = [n_instances,n_columns] diff --git a/sktime/classification/shapelet_based/mrseql/mrseql.pyx b/sktime/classification/shapelet_based/mrseql/mrseql.pyx index 02c8aac77bc..076cf8d0ffe 100644 --- a/sktime/classification/shapelet_based/mrseql/mrseql.pyx +++ b/sktime/classification/shapelet_based/mrseql/mrseql.pyx @@ -386,11 +386,12 @@ class MrSEQLClassifier(BaseClassifier): return self.seql_clf.predict_proba(mr_seqs) def predict(self, X): - """ - Predict class labels for samples in X. + """Predict class labels for samples in X. + Parameters ---------- X : time series data. + Returns ------- C : array @@ -400,7 +401,8 @@ class MrSEQLClassifier(BaseClassifier): return np.array([self.classes_[np.argmax(prob)] for prob in proba]) def map_sax_model(self, ts): - """ For interpretation. + """For interpretation. + Returns vectors of weights with the same length of the input time series. The weight of each point implies its contribution in the classification decision regarding the class. @@ -412,8 +414,8 @@ class MrSEQLClassifier(BaseClassifier): ------- weighted_ts: ndarray of (number of classes, length of time series) - Note - ------- + Notes + ----- Only supports univariate time series and SAX features. """ self.check_is_fitted() diff --git a/sktime/datasets/__init__.py b/sktime/datasets/__init__.py index 362301b36fd..2fc7692ac29 100644 --- a/sktime/datasets/__init__.py +++ b/sktime/datasets/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Load data functions.""" +"""Functions to load datasets included in sktime.""" __all__ = [ "load_airline", @@ -16,6 +16,7 @@ "load_unit_test", "load_uschange", "load_PBS_dataset", + "load_japanese_vowels", ] from sktime.datasets._data_io import load_airline diff --git a/sktime/datasets/_data_io.py b/sktime/datasets/_data_io.py index bc7a1d64ec0..c356a728dc0 100644 --- a/sktime/datasets/_data_io.py +++ b/sktime/datasets/_data_io.py @@ -224,13 +224,15 @@ def load_gunpoint(split=None, return_X_y=False): The time series data for the problem with m cases and c dimensions y: numpy array The class labels for each case in X - Details - ------- + + Notes + ----- Dimensionality: univariate Series length: 150 Train cases: 50 Test cases: 150 Number of classes: 2 + This dataset involves one female actor and one male actor making a motion with their hand. The two classes are: Gun-Draw and Point: For Gun-Draw the actors @@ -276,8 +278,8 @@ def load_osuleaf(split=None, return_X_y=False): y: numpy array The class labels for each case in X - Details - ------- + Notes + ----- Dimensionality: univariate Series length: 427 Train cases: 200 @@ -319,8 +321,8 @@ def load_italy_power_demand(split=None, return_X_y=False): y: numpy array The class labels for each case in X - Details - ------- + Notes + ----- Dimensionality: univariate Series length: 24 Train cases: 67 @@ -395,8 +397,8 @@ def load_japanese_vowels(split=None, return_X_y=False): y: numpy array The class labels for each case in X - Details - ------- + Notes + ----- Dimensionality: multivariate, 12 Series length: 29 Train cases: 270 @@ -448,8 +450,8 @@ def load_arrow_head(split=None, return_X_y=False): y: numpy array The class labels for each case in X - Details - ------- + Notes + ----- Dimensionality: univariate Series length: 251 Train cases: 36 @@ -496,8 +498,8 @@ def load_acsf1(split=None, return_X_y=False): y: numpy array The class labels for each case in X - Details - ------- + Notes + ----- Dimensionality: univariate Series length: 1460 Train cases: 100 @@ -541,8 +543,8 @@ def load_basic_motions(split=None, return_X_y=False): y: numpy array The class labels for each case in X - Details - ------- + Notes + ----- Dimensionality: univariate Series length: 100 Train cases: 40 @@ -573,8 +575,8 @@ def load_shampoo_sales(): y : pandas Series/DataFrame Shampoo sales dataset - Details - ------- + Notes + ----- This dataset describes the monthly number of sales of shampoo over a 3 year period. The units are a sales count. @@ -616,8 +618,8 @@ def load_longley(y_name="TOTEMP"): X: pandas.DataFrame The exogenous time series data for the problem. - Details - ------- + Notes + ----- This mulitvariate time series dataset contains various US macroeconomic variables from 1947 to 1962 that are known to be highly collinear. @@ -664,8 +666,8 @@ def load_lynx(): y : pandas Series/DataFrame Lynx sales dataset - Details - ------- + Notes + ----- The annual numbers of lynx trappings for 1821–1934 in Canada. This time-series records the number of skins of predators (lynx) that were collected over several years by the Hudson's @@ -678,8 +680,6 @@ def load_lynx(): Frequency: Yearly Number of cases: 1 - Notes - ----- This data shows aperiodic, cyclical patterns, as opposed to periodic, seasonal patterns. @@ -712,8 +712,8 @@ def load_airline(): y : pd.Series Time series - Details - ------- + Notes + ----- The classic Box & Jenkins airline data. Monthly totals of international airline passengers, 1949 to 1960. @@ -722,8 +722,6 @@ def load_airline(): Frequency: Monthly Number of cases: 1 - Notes - ----- This data shows an increasing trend, non-constant (increasing) variance and periodic, seasonal patterns. @@ -755,8 +753,8 @@ def load_uschange(y_name="Consumption"): X : pandas Dataframe columns with explanatory variables - Details - ------- + Notes + ----- Percentage changes in quarterly personal consumption expenditure, personal disposable income, production, savings and the unemployment rate for the US, 1960 to 2016. @@ -769,8 +767,6 @@ def load_uschange(y_name="Consumption"): Frequency: Quarterly Number of cases: 1 - Notes - ----- This data shows an increasing trend, non-constant (increasing) variance and periodic, seasonal patterns. @@ -798,16 +794,15 @@ def load_uschange(y_name="Consumption"): def load_PBS_dataset(): - """ - Load the Pharmaceutical Benefit Scheme univariate time series dataset [1]. + """Load the Pharmaceutical Benefit Scheme univariate time series dataset [1]. Returns ------- y : pd.Series Time series - Details - ------- + Notes + ----- The Pharmaceutical Benefits Scheme (PBS) is the Australian government drugs subsidy scheme. Data comprises of the numbers of scripts sold each month for immune sera @@ -819,8 +814,6 @@ def load_PBS_dataset(): Frequency: Monthly Number of cases: 1 - Notes - ----- The time series is intermittent, i.e contains small counts, with many months registering no sales at all, and only small numbers of items sold in other months. diff --git a/sktime/datasets/setup.py b/sktime/datasets/setup.py index 7d29e0208da..8360b9f9278 100644 --- a/sktime/datasets/setup.py +++ b/sktime/datasets/setup.py @@ -2,11 +2,14 @@ # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) """Set up the datasets included in sktime.""" + __author__ = "Markus Löning" +# The file is adapted from: +# https://github.com/scikit-learn/scikit-learn/blob/master/sklearn/datasets/setup.py + -# adapted from https://github.com/scikit-learn/scikit-learn/blob/master -# /sklearn/datasets/setup.py +___author__ = ["mloning"] def configuration(parent_package="", top_path=None): diff --git a/sktime/datasets/tsc_dataset_names.py b/sktime/datasets/tsc_dataset_names.py index f86b79f81f0..8202f8ee9bd 100644 --- a/sktime/datasets/tsc_dataset_names.py +++ b/sktime/datasets/tsc_dataset_names.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- + """ -Lists of datasets available from the archive on timeseriesclassification.com +List of datasets available from the timeseriesclassification.com archive. There are four main distinctions: univariate/multivariate equal/unequal length. Array univariate lists the 128 UCR problems, as described in [1]. diff --git a/sktime/forecasting/__init__.py b/sktime/forecasting/__init__.py index e69de29bb2d..be217575161 100644 --- a/sktime/forecasting/__init__.py +++ b/sktime/forecasting/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Implements univariate and multivariate forecasting models.""" diff --git a/sktime/forecasting/all/__init__.py b/sktime/forecasting/all/__init__.py index a47faabc2e2..262dabfd750 100644 --- a/sktime/forecasting/all/__init__.py +++ b/sktime/forecasting/all/__init__.py @@ -1,8 +1,9 @@ -#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- -"""Module exports for forecasting module.""" +# !/usr/bin/env python3 -u +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Import all time series forecasting functionality available in sktime.""" -__author__ = ["Markus Löning"] +__author__ = ["mloning"] __all__ = [ "ForecastingHorizon", "load_lynx", diff --git a/sktime/forecasting/arima.py b/sktime/forecasting/arima.py index 658c4a4ec25..ef852980536 100644 --- a/sktime/forecasting/arima.py +++ b/sktime/forecasting/arima.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements autoregressive integrated moving average (ARIMA) models.""" __author__ = ["Markus Löning", "Hongyi Yang"] __all__ = ["AutoARIMA", "ARIMA"] @@ -211,8 +212,8 @@ class AutoARIMA(_PmdArimaAdapter): ---------- https://alkaline-ml.com/pmdarima/modules/generated/pmdarima.arima.AutoARIMA.html - Example - ---------- + Examples + -------- >>> from sktime.datasets import load_airline >>> from sktime.forecasting.arima import AutoARIMA >>> y = load_airline() @@ -386,7 +387,8 @@ class ARIMA(_PmdArimaAdapter): When two out of the three terms are zeros, the model may be referred to based on the non-zero parameter, dropping "AR", "I" or "MA" from the acronym describing the model. For example, ``ARIMA(1,0,0)`` is ``AR(1)``, - ``ARIMA(0,1,0)`` is ``I(1)``, and ``ARIMA(0,0,1)`` is ``MA(1)``. [1] + ``ARIMA(0,1,0)`` is ``I(1)``, and ``ARIMA(0,0,1)`` is ``MA(1)``. [1]_ + See notes for more practical information on the ``ARIMA`` class. Parameters @@ -509,13 +511,14 @@ def foo_loss(y_true, y_pred) References ---------- - https://alkaline-ml.com/pmdarima/modules/generated/pmdarima.arima.ARIMA.html - https://www.statsmodels.org/stable/generated/statsmodels.tsa.statespace.sarimax.SARIMAX.html + ..[1] https://alkaline-ml.com/pmdarima/modules/generated/pmdarima.arima.ARIMA.html + ..[2] https://www.statsmodels.org/stable/generated/ + statsmodels.tsa.statespace.sarimax.SARIMAX.html - Example - ---------- + Examples + -------- >>> from sktime.datasets import load_airline - >>> from sktime.forecasting.arima import AutoARIMA + >>> from sktime.forecasting.arima import ARIMA >>> y = load_airline() >>> forecaster = ARIMA( ... order=(1, 1, 0), diff --git a/sktime/forecasting/base/__init__.py b/sktime/forecasting/base/__init__.py index c92f05921cd..2d6be2fb551 100644 --- a/sktime/forecasting/base/__init__.py +++ b/sktime/forecasting/base/__init__.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements base classes for forecasting in sktime.""" + __all__ = [ "ForecastingHorizon", "BaseForecaster", diff --git a/sktime/forecasting/base/_base.py b/sktime/forecasting/base/_base.py index f3ac91aac57..e24821b0194 100644 --- a/sktime/forecasting/base/_base.py +++ b/sktime/forecasting/base/_base.py @@ -103,10 +103,13 @@ def fit(self, y, X=None, fh=None): Exogeneous data Returns ------- - self : reference to self. + self : + Reference to self. - State change - ------------ + Notes + ----- + Changes state by creating a fitted model that updates attributes + ending in "_" and sets is_fitted flag to True. stores data in self._X and self._y stores fh, if passed updates self.cutoff to most recent time in y @@ -355,11 +358,11 @@ def update(self, y, X=None, update_params=True): ------- self : reference to self - State change - ------------ - updates self._X and self._y with new data - updates self.cutoff to most recent time in y - if update_params=True, updates model (attributes ending in "_") + Notes + ----- + Update self._y and self._X with `y` and `X`, respectively. + Updates self._cutoff to last index seen in `y`. If update_params=True, + updates fitted model that updates attributes ending in "_". """ self.check_is_fitted() @@ -662,9 +665,9 @@ def _set_cutoff(self, cutoff): ---------- cutoff: pandas compatible index element - State change - ------------ - self._cutoff is set to cutoff + Notes + ----- + Set self._cutoff is to `cutoff`. """ self._cutoff = cutoff @@ -676,9 +679,9 @@ def _set_cutoff_from_y(self, y): y: pd.Series, pd.DataFrame, or np.array Target time series to which to fit the forecaster. - State change - ------------ - self._cutoff is set to last index seen in y + Notes + ----- + Set self._cutoff to last index seen in `y`. """ if mtype(y, as_scitype="Series") in ["pd.Series", "pd.DataFrame"]: self._cutoff = y.index[-1] @@ -855,11 +858,11 @@ def _update(self, y, X=None, update_params=True): y_pred_int : pd.DataFrame - only if return_pred_int=True Prediction intervals - State change - ------------ - updates self._X and self._y with new data - updates self.cutoff to most recent time in y - if update_params=True, updates model (attributes ending in "_") + Notes + ----- + Update self._y and self._X with `y` and `X`, respectively. + Updates self._cutoff to last index seen in `y`. If update_params=True, + updates fitted model that updates attributes ending in "_". """ if update_params: # default to re-fitting if update is not implemented diff --git a/sktime/forecasting/base/_fh.py b/sktime/forecasting/base/_fh.py index 1c3c7ae1276..443b62c145d 100644 --- a/sktime/forecasting/base/_fh.py +++ b/sktime/forecasting/base/_fh.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements functionality for specifying forecast horizons in sktime.""" __author__ = ["mloning", "fkiraly"] __all__ = ["ForecastingHorizon"] @@ -46,11 +48,10 @@ def _delegator(method): - """Decorate ForecastingHorizon with pandas.index methods. + """Automatically decorate ForecastingHorizon class with pandas.Index methods. - Helper function to automatically decorate ForecastingHorizon class with + Also delegates method calls to wrapped pandas.Index object. methods from pandas.Index and delegate method calls to wrapped pandas.Index - object. """ def delegated(obj, *args, **kwargs): @@ -60,7 +61,10 @@ def delegated(obj, *args, **kwargs): def _check_values(values): - """Validate forecasting horizon values and coerce to pandas.Index type. + """Validate forecasting horizon values. + + Validation checks validity and also converts forecasting horizon values + to supported pandas.Index types if possible. Parameters ---------- @@ -69,7 +73,8 @@ def _check_values(values): Raises ------ - TypeError : if values type is not supported + TypeError : + Raised if `values` type is not supported Returns ------- @@ -178,15 +183,15 @@ def _new(self, values=None, is_relative=None): Parameters ---------- values : pd.Index, np.array, list or int - Values of forecasting horizon - is_relative : bool, optional (default=same as self.is_relative) + Values of forecasting horizon. + is_relative : bool, default=same as self.is_relative - If None, determined automatically: same as self.is_relative - If True, values are relative to end of training series. - If False, values are absolute. Returns ------- - ForecastingHorizon + ForecastingHorizon : New ForecastingHorizon based on current object """ if values is None: @@ -197,7 +202,7 @@ def _new(self, values=None, is_relative=None): @property def is_relative(self): - """Whether forecasting horizon is relative. + """Whether forecasting horizon is relative to the end of the training series. Returns ------- @@ -206,16 +211,17 @@ def is_relative(self): return self._is_relative def to_pandas(self): - """Return underlying values as pd.Index. + """Return forecasting horizon's underlying values as pd.Index. Returns ------- fh : pd.Index + pandas Index containing forecasting horizon's underlying values. """ return self._values def to_numpy(self, **kwargs): - """Return underlying values as np.array. + """Return forecasting horizon's underlying values as np.array. Parameters ---------- @@ -225,6 +231,7 @@ def to_numpy(self, **kwargs): Returns ------- fh : np.ndarray + NumPy array containg forecasting horizon's underlying values. """ return self.to_pandas().to_numpy(**kwargs) @@ -233,18 +240,18 @@ def to_numpy(self, **kwargs): # calling different methods. @lru_cache(typed=True) def to_relative(self, cutoff=None): - """Return relative values. + """Return forecasting horizon values relative to a cutoff. Parameters ---------- cutoff : pd.Period, pd.Timestamp, int, optional (default=None) - Cutoff value is required to convert a relative forecasting - horizon to an absolute one and vice versa. + Cutoff value required to convert a relative forecasting + horizon to an absolute one (and vice versa). Returns ------- fh : ForecastingHorizon - Relative representation of forecasting horizon + Relative representation of forecasting horizon. """ if self.is_relative: return self._new() @@ -275,18 +282,18 @@ def to_relative(self, cutoff=None): @lru_cache(typed=True) def to_absolute(self, cutoff): - """Convert ForecastingHorizon to absolute and return. + """Return absolute version of forecasting horizon values. Parameters ---------- cutoff : pd.Period, pd.Timestamp, int Cutoff value is required to convert a relative forecasting - horizon to an absolute one and vice versa. + horizon to an absolute one (and vice versa). Returns ------- fh : ForecastingHorizon - Absolute representation of forecasting horizon + Absolute representation of forecasting horizon. """ if not self.is_relative: return self._new() @@ -318,14 +325,14 @@ def to_absolute_int(self, start, cutoff=None): start : pd.Period, pd.Timestamp, int Start value returned as zero. cutoff : pd.Period, pd.Timestamp, int, optional (default=None) - Cutoff value is required to convert a relative forecasting - horizon to an absolute one and vice versa. + Cutoff value required to convert a relative forecasting + horizon to an absolute one (and vice versa). Returns ------- fh : ForecastingHorizon Absolute representation of forecasting horizon as zero-based - integer index + integer index. """ # We here check the start value, the cutoff value is checked when we use it # to convert the horizon to the absolute representation below @@ -349,13 +356,13 @@ def to_in_sample(self, cutoff=None): Parameters ---------- cutoff : pd.Period, pd.Timestamp, int, optional (default=None) - Cutoff value is required to convert a relative forecasting - horizon to an absolute one and vice versa. + Cutoff value required to convert a relative forecasting + horizon to an absolute one (and vice versa). Returns ------- fh : ForecastingHorizon - In-sample values of forecasting horizon + In-sample values of forecasting horizon. """ is_in_sample = self._is_in_sample(cutoff) in_sample = self.to_pandas()[is_in_sample] @@ -368,12 +375,12 @@ def to_out_of_sample(self, cutoff=None): ---------- cutoff : pd.Period, pd.Timestamp, int, optional (default=None) Cutoff value is required to convert a relative forecasting - horizon to an absolute one and vice versa. + horizon to an absolute one (and vice versa). Returns ------- fh : ForecastingHorizon - Out-of-sample values of forecasting horizon + Out-of-sample values of forecasting horizon. """ is_out_of_sample = self._is_out_of_sample(cutoff) out_of_sample = self.to_pandas()[is_out_of_sample] @@ -384,13 +391,12 @@ def _is_in_sample(self, cutoff=None): return self.to_relative(cutoff).to_pandas() <= 0 def is_all_in_sample(self, cutoff=None): - """Whether or not the fh is purely in-sample given cutoff, yes/no. + """Whether the forecasting horizon is purely in-sample for given cutoff. Parameters ---------- - cutoff : pd.Period, pd.Timestamp, int, optional (default=None) - Cutoff value is required to convert a relative forecasting - horizon to an absolute one and vice versa. + cutoff : pd.Period, pd.Timestamp, int, default=None + Cutoff value used to check if forecasting horizon is purely in-sample. Returns ------- @@ -405,13 +411,13 @@ def _is_out_of_sample(self, cutoff=None): return self.to_relative(cutoff).to_pandas() > 0 def is_all_out_of_sample(self, cutoff=None): - """Whether or not the fh is purely out-of-sample given cutoff, yes/no. + """Whether the forecasting horizon is purely out-of-sample for given cutoff. Parameters ---------- cutoff : pd.Period, pd.Timestamp, int, optional (default=None) - Cutoff value is required to convert a relative forecasting - horizon to an absolute one and vice versa. + Cutoff value used to check if forecasting horizon is purely + out-of-sample. Returns ------- @@ -427,7 +433,7 @@ def to_indexer(self, cutoff=None, from_cutoff=True): Parameters ---------- cutoff : pd.Period, pd.Timestamp, int, optional (default=None) - Cutoff value is required to convert a relative forecasting + Cutoff value required to convert a relative forecasting horizon to an absolute one and vice versa. from_cutoff : bool, optional (default=True) - If True, zero-based relative to cutoff. @@ -437,7 +443,7 @@ def to_indexer(self, cutoff=None, from_cutoff=True): Returns ------- fh : pd.Index - Indexer + Indexer. """ if from_cutoff: return self.to_relative(cutoff).to_pandas() - 1 @@ -453,10 +459,19 @@ def __repr__(self): def _check_cutoff(cutoff, index): - """Check whether cutoff is compatible with fh index type. + """Check if the cutoff is valid based on time index of forecasting horizon. - Helper function to check if the cutoff contains all necessary information and is - compatible with the time index of the forecasting horizon + Validates that the cutoff contains necessary information and is + compatible with the time index of the forecasting horizon. + + Parameters + ---------- + cutoff : pd.Period, pd.Timestamp, int, optional (default=None) + Cutoff value is required to convert a relative forecasting + horizon to an absolute one and vice versa. + index : pd.PeriodIndex or pd.DataTimeIndex + Forecasting horizon time index that the cutoff value will be checked + against. """ if cutoff is None: raise ValueError("`cutoff` must be given, but found none.") @@ -491,10 +506,22 @@ def _check_start(start, index): def _coerce_to_period(x, freq=None): - """Coerce compatible index type to pd.PeriodIndex. + """Coerce pandas time index to a alternative pandas time index. - Helper function to coerce pd.Timestamp to pd.Period or pd.DatetimeIndex to - pd.PeriodIndex for more reliable arithmetic operations with time indices + This coerces pd.Timestamp to pd.Period or pd.DatetimeIndex to + pd.PeriodIndex, because pd.Period and pd.PeriodIndex allow more reliable + arithmetic operations with time indices. + + Parameters + ---------- + x : pandas Index + pandas Index to convert. + freq : + + Returns + ------- + index : pd.Period or pd.PeriodIndex + Index coerced to preferred format. """ if freq is None: freq = _get_freq(x) diff --git a/sktime/forecasting/base/_meta.py b/sktime/forecasting/base/_meta.py index 104a4b0017b..e13f70f5911 100644 --- a/sktime/forecasting/base/_meta.py +++ b/sktime/forecasting/base/_meta.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements meta forecaster for forecasters composed of other estimators.""" + __author__ = ["mloning"] __all__ = ["_HeterogenousEnsembleForecaster"] diff --git a/sktime/forecasting/base/_sktime.py b/sktime/forecasting/base/_sktime.py index 88762de8fd7..1a3c65a364f 100644 --- a/sktime/forecasting/base/_sktime.py +++ b/sktime/forecasting/base/_sktime.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- -""" -sktime window forecaster base class - +# !/usr/bin/env python3 -u # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) -""" +"""sktime window forecaster base class.""" __author__ = ["@mloning", "@big-o"] __all__ = ["_BaseWindowForecaster"] @@ -20,7 +18,7 @@ class _BaseWindowForecaster(BaseForecaster): - """Base class for forecasters that use.""" + """Base class for forecasters that use sliding windows.""" def __init__(self, window_length=None): super(_BaseWindowForecaster, self).__init__() diff --git a/sktime/forecasting/base/adapters/__init__.py b/sktime/forecasting/base/adapters/__init__.py index df56a03cfff..179f57f98bb 100644 --- a/sktime/forecasting/base/adapters/__init__.py +++ b/sktime/forecasting/base/adapters/__init__.py @@ -1,5 +1,7 @@ -#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements base classes for adapting other forecasters to sktime framework.""" __author__ = ["Markus Löning"] __all__ = [ diff --git a/sktime/forecasting/base/adapters/_fbprophet.py b/sktime/forecasting/base/adapters/_fbprophet.py index 82066d8bd1b..fb29989c3c5 100644 --- a/sktime/forecasting/base/adapters/_fbprophet.py +++ b/sktime/forecasting/base/adapters/_fbprophet.py @@ -1,6 +1,7 @@ -#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements adapter for Facebook prophet to be used in sktime framework.""" __author__ = ["Markus Löning", "Martin Walter"] __all__ = ["_ProphetAdapter"] @@ -207,6 +208,7 @@ class _suppress_stdout_stderr(object): to stderr just before a script exits, and after the context manager has exited (at least, I think that is why it lets exceptions through). + References ---------- https://github.com/facebook/prophet/issues/223 diff --git a/sktime/forecasting/base/adapters/_pmdarima.py b/sktime/forecasting/base/adapters/_pmdarima.py index acc84fb5bd1..58e0a38561c 100644 --- a/sktime/forecasting/base/adapters/_pmdarima.py +++ b/sktime/forecasting/base/adapters/_pmdarima.py @@ -1,6 +1,7 @@ -#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements adapter for pmdarima forecasters to be used in sktime framework.""" __author__ = ["Markus Löning", "Hongyi Yang"] __all__ = ["_PmdArimaAdapter"] @@ -39,6 +40,7 @@ def _fit(self, y, X=None, fh=None, **fit_params): The forecasters horizon with the steps ahead to to predict. X : pd.DataFrame, optional (default=None) Exogenous variables are ignored + Returns ------- self : returns an instance of self. diff --git a/sktime/forecasting/base/adapters/_statsmodels.py b/sktime/forecasting/base/adapters/_statsmodels.py index c0002f24a63..19b685393f7 100644 --- a/sktime/forecasting/base/adapters/_statsmodels.py +++ b/sktime/forecasting/base/adapters/_statsmodels.py @@ -1,5 +1,7 @@ -#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements adapter for statsmodels forecasters to be used in sktime framework.""" __author__ = ["Markus Löning"] __all__ = ["_StatsModelsAdapter"] @@ -12,7 +14,7 @@ class _StatsModelsAdapter(BaseForecaster): - """Base class for interfacing statsmodels forecasting algorithms""" + """Base class for interfacing statsmodels forecasting algorithms.""" _fitted_param_names = () _tags = { @@ -37,6 +39,7 @@ def _fit(self, y, X=None, fh=None): The forecasters horizon with the steps ahead to to predict. X : pd.DataFrame, optional (default=None) Exogenous variables are ignored + Returns ------- self : returns an instance of self. @@ -49,12 +52,11 @@ def _fit(self, y, X=None, fh=None): return self def _fit_forecaster(self, y_train, X_train=None): - """Internal fit""" + """Log used internally in fit.""" raise NotImplementedError("abstract method") def _predict(self, fh, X=None, return_pred_int=False, alpha=DEFAULT_ALPHA): - """ - Make forecasts. + """Make forecasts. Parameters ---------- @@ -85,7 +87,7 @@ def _predict(self, fh, X=None, return_pred_int=False, alpha=DEFAULT_ALPHA): return y_pred.loc[fh.to_absolute(self.cutoff).to_pandas()] def get_fitted_params(self): - """Get fitted parameters + """Get fitted parameters. Returns ------- @@ -101,7 +103,7 @@ def get_fitted_params(self): return fitted_params def _get_fitted_param_names(self): - """Get names of fitted parameters""" + """Get names of fitted parameters.""" return self._fitted_param_names diff --git a/sktime/forecasting/base/adapters/_tbats.py b/sktime/forecasting/base/adapters/_tbats.py index 80972420919..9afac80677f 100644 --- a/sktime/forecasting/base/adapters/_tbats.py +++ b/sktime/forecasting/base/adapters/_tbats.py @@ -1,5 +1,7 @@ -#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements adapter for using tbats forecasters in sktime framework.""" __author__ = ["Markus Löning", "Martin Walter"] __all__ = ["_TbatsAdapter"] diff --git a/sktime/forecasting/bats.py b/sktime/forecasting/bats.py index fae387d4a08..97efd962615 100644 --- a/sktime/forecasting/bats.py +++ b/sktime/forecasting/bats.py @@ -1,6 +1,12 @@ -#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements BATS algorithm. + +BATS refers to Exponential smoothing state space model with Box-Cox +transformation, ARMA errors, Trend and Seasonal components as described in +De LIvera, Hyndman and Snyder (2011). +""" __author__ = ["Martin Walter"] __all__ = ["BATS"] @@ -53,8 +59,8 @@ class BATS(_TbatsAdapter): context: abstract.ContextInterface, optional (default=None) For advanced users only. Provide this to override default behaviors - Example - ---------- + Examples + -------- >>> from sktime.datasets import load_airline >>> from sktime.forecasting.bats import BATS >>> y = load_airline() diff --git a/sktime/forecasting/compose/__init__.py b/sktime/forecasting/compose/__init__.py index e34f4e05f87..7cbb989ac2c 100644 --- a/sktime/forecasting/compose/__init__.py +++ b/sktime/forecasting/compose/__init__.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- -"""copyright: sktime developers, BSD-3-Clause License (see LICENSE file).""" +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements composite forecasters.""" -__author__ = ["Markus Löning"] +__author__ = ["mloning"] __all__ = [ "ColumnEnsembleForecaster", diff --git a/sktime/forecasting/compose/_ensemble.py b/sktime/forecasting/compose/_ensemble.py index 70abc963921..5e24823fd09 100644 --- a/sktime/forecasting/compose/_ensemble.py +++ b/sktime/forecasting/compose/_ensemble.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements forecaster for creating forecasts from ensembles of other forecasters.""" __author__ = ["Markus Löning"] __all__ = ["EnsembleForecaster"] @@ -30,8 +31,8 @@ class EnsembleForecaster(_HeterogenousEnsembleForecaster): aggfunc : str, {'mean', 'median', 'min', 'max'}, default='mean' The function to aggregate prediction from individual forecasters. - Example - ------- + Examples + -------- >>> from sktime.forecasting.compose import EnsembleForecaster >>> from sktime.forecasting.naive import NaiveForecaster >>> from sktime.forecasting.trend import PolynomialTrendForecaster diff --git a/sktime/forecasting/compose/_multiplexer.py b/sktime/forecasting/compose/_multiplexer.py index 2b086392187..c8c5f00fa0a 100644 --- a/sktime/forecasting/compose/_multiplexer.py +++ b/sktime/forecasting/compose/_multiplexer.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements forecaster for selecting among different model classes.""" from sktime.forecasting.base._meta import _HeterogenousEnsembleForecaster from sktime.forecasting.base._base import DEFAULT_ALPHA @@ -11,7 +12,7 @@ class MultiplexForecaster(_HeterogenousEnsembleForecaster): - """MultiplexForecaster for model selection. + """MultiplexForecaster for selecting among different models. MultiplexForecaster facilitates a framework for performing model selection process over different model classes. diff --git a/sktime/forecasting/compose/_pipeline.py b/sktime/forecasting/compose/_pipeline.py index 72807ea3e94..c1f626ccce3 100644 --- a/sktime/forecasting/compose/_pipeline.py +++ b/sktime/forecasting/compose/_pipeline.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements pipelines for forecasting.""" __author__ = ["Markus Löning", "Martin Walter"] __all__ = ["TransformedTargetForecaster", "ForecastingPipeline"] @@ -130,20 +131,19 @@ def set_params(self, **kwargs): class ForecastingPipeline(_Pipeline): - """Meta-estimator for forecasting with exogenous data. + """Pipeline for forecasting with exogenous data. - ForecastingPipeline is apply transformers to the exogenous serieses. - The given forecaster as last step can also be a TransformedTargetForecaster - containing transformers to transform y. ForecastingPipeline is only applying - the given transformers to X. + ForecastingPipeline is only applying the given transformers + to X. The forecaster can also be a TransformedTargetForecaster containing + transformers to transform y. Parameters ---------- steps : list List of tuples like ("name", forecaster/transformer) - Example - ------- + Examples + -------- >>> from sktime.datasets import load_longley >>> from sktime.forecasting.naive import NaiveForecaster >>> from sktime.forecasting.compose import ForecastingPipeline @@ -187,6 +187,7 @@ def _fit(self, y, X=None, fh=None): The forecasters horizon with the steps ahead to to predict. X : pd.DataFrame, required Exogenous variables are ignored + Returns ------- self : returns an instance of self. @@ -295,8 +296,8 @@ class TransformedTargetForecaster(_Pipeline, _SeriesToSeriesTransformer): steps : list List of tuples like ("name", forecaster/transformer) - Example - ------- + Examples + -------- >>> from sktime.datasets import load_airline >>> from sktime.forecasting.naive import NaiveForecaster >>> from sktime.forecasting.compose import TransformedTargetForecaster @@ -335,6 +336,7 @@ def _fit(self, y, X=None, fh=None): The forecasters horizon with the steps ahead to to predict. X : pd.DataFrame, optional (default=None) Exogenous variables are ignored + Returns ------- self : returns an instance of self. @@ -417,18 +419,19 @@ def _update(self, y, X=None, update_params=True): return self def transform(self, Z, X=None): - """Transform data. - - Returns a transformed version of Z. + """Return transformed version of input series `Z`. Parameters ---------- - Z : pd.Series, pd.DataFrame + Z : pd.Series or pd.DataFrame + A time series to apply the transformation on. + X : pd.DataFrame, default=None + Exogenous data used in transformation. Returns ------- - Z : pd.Series, pd.DataFrame - Transformed time series(es). + Zt : pd.Series or pd.DataFrame + Transformed version of input series `Z`. """ self.check_is_fitted() zt = check_series(Z, enforce_univariate=True) @@ -443,6 +446,8 @@ def inverse_transform(self, Z, X=None): ---------- Z : pd.Series or pd.DataFrame A time series to reverse the transformation on. + X : pd.DataFrame, default=None + Exogenous data used in transformation. Returns ------- diff --git a/sktime/forecasting/compose/_reduce.py b/sktime/forecasting/compose/_reduce.py index 168bd9f79d9..fd22ab7bb96 100644 --- a/sktime/forecasting/compose/_reduce.py +++ b/sktime/forecasting/compose/_reduce.py @@ -566,8 +566,7 @@ def _predict_last_window( class DirectTabularRegressionForecaster(_DirectReducer): - """ - Direct reduction from forecasting to tabular regression. + """Direct reduction from forecasting to tabular regression. For the direct reduction strategy, a separate forecaster is fitted for each step ahead of the forecasting horizon. @@ -585,8 +584,7 @@ class DirectTabularRegressionForecaster(_DirectReducer): class MultioutputTabularRegressionForecaster(_MultioutputReducer): - """ - Multioutput reduction from forecasting to tabular regression. + """Multioutput reduction from forecasting to tabular regression. For the multioutput strategy, a single estimator capable of handling multioutput targets is fitted to all the future steps in the forecasting horizon. @@ -604,8 +602,7 @@ class MultioutputTabularRegressionForecaster(_MultioutputReducer): class RecursiveTabularRegressionForecaster(_RecursiveReducer): - """ - Recursive reduction from forecasting to tabular regression. + """Recursive reduction from forecasting to tabular regression. For the recursive strategy, a single estimator is fit for a one-step-ahead forecasting horizon and then called iteratively to predict multiple steps ahead. @@ -623,8 +620,7 @@ class RecursiveTabularRegressionForecaster(_RecursiveReducer): class DirRecTabularRegressionForecaster(_DirRecReducer): - """ - Dir-rec reduction from forecasting to tabular regression. + """Dir-rec reduction from forecasting to tabular regression. For the hybrid dir-rec strategy, a separate forecaster is fitted for each step ahead of the forecasting horizon and then @@ -645,8 +641,7 @@ class DirRecTabularRegressionForecaster(_DirRecReducer): class DirectTimeSeriesRegressionForecaster(_DirectReducer): - """ - Direct reduction from forecasting to time-series regression. + """Direct reduction from forecasting to time-series regression. For the direct reduction strategy, a separate forecaster is fitted for each step ahead of the forecasting horizon. @@ -664,8 +659,7 @@ class DirectTimeSeriesRegressionForecaster(_DirectReducer): class MultioutputTimeSeriesRegressionForecaster(_MultioutputReducer): - """ - Multioutput reduction from forecasting to time series regression. + """Multioutput reduction from forecasting to time series regression. For the multioutput strategy, a single estimator capable of handling multioutput targets is fitted to all the future steps in the forecasting horizon. @@ -683,8 +677,7 @@ class MultioutputTimeSeriesRegressionForecaster(_MultioutputReducer): class RecursiveTimeSeriesRegressionForecaster(_RecursiveReducer): - """ - Recursive reduction from forecasting to time series regression. + """Recursive reduction from forecasting to time series regression. For the recursive strategy, a single estimator is fit for a one-step-ahead forecasting horizon and then called iteratively to predict multiple steps ahead. @@ -702,8 +695,7 @@ class RecursiveTimeSeriesRegressionForecaster(_RecursiveReducer): class DirRecTimeSeriesRegressionForecaster(_DirRecReducer): - """ - Dir-rec reduction from forecasting to time-series regression. + """Dir-rec reduction from forecasting to time-series regression. For the hybrid dir-rec strategy, a separate forecaster is fitted for each step ahead of the forecasting horizon and then @@ -727,8 +719,7 @@ class DirRecTimeSeriesRegressionForecaster(_DirRecReducer): def ReducedForecaster( estimator, scitype="infer", strategy="recursive", window_length=10, step_length=1 ): - """ - Reduction from forecasting to tabular or time series regression. + """Reduction from forecasting to tabular or time series regression. During fitting, a sliding-window approach is used to first transform the time series into tabular or panel data, which is then used to fit a tabular or @@ -765,8 +756,7 @@ def ReducedForecaster( def ReducedRegressionForecaster( estimator, scitype, strategy="recursive", window_length=10, step_length=1 ): - """ - Reduction from forecasting to tabular or time series regression. + """Reduction from forecasting to tabular or time series regression. During fitting, a sliding-window approach is used to first transform the time series into tabular or panel data, which is then used to fit a tabular or @@ -804,8 +794,7 @@ def make_reduction( window_length=10, scitype="infer", ): - """ - Make forecaster based on reduction to tabular or time-series regression. + """Make forecaster based on reduction to tabular or time-series regression. During fitting, a sliding-window approach is used to first transform the time series into tabular or panel data, which is then used to fit a tabular or @@ -825,6 +814,7 @@ def make_reduction( scitype : str, optional (default="infer") Must be one of "infer", "tabular-regressor" or "time-series-regressor". If the scitype cannot be inferred, please specify it explicitly. + See :term:`scitype`. Returns ------- diff --git a/sktime/forecasting/compose/_stack.py b/sktime/forecasting/compose/_stack.py index c3ee95dce5b..bdd017fab13 100644 --- a/sktime/forecasting/compose/_stack.py +++ b/sktime/forecasting/compose/_stack.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements forecasters for combining forecasts via stacking.""" __author__ = ["Markus Löning"] __all__ = ["StackingForecaster"] @@ -20,7 +21,7 @@ class StackingForecaster(_HeterogenousEnsembleForecaster): """StackingForecaster. - Stacks two or more Forecasters + Stacks two or more Forecasters. Parameters ---------- @@ -55,6 +56,7 @@ def _fit(self, y, X=None, fh=None): The forecasters horizon with the steps ahead to to predict. X : pd.DataFrame, optional (default=None) Exogenous variables are ignored + Returns ------- self : returns an instance of self. @@ -86,8 +88,7 @@ def _fit(self, y, X=None, fh=None): return self def _update(self, y, X=None, update_params=True): - - """Update fitted parameters + """Update fitted parameters. Parameters ---------- diff --git a/sktime/forecasting/croston.py b/sktime/forecasting/croston.py index 5dc48270c84..d7042e4583a 100644 --- a/sktime/forecasting/croston.py +++ b/sktime/forecasting/croston.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) """Croston's Forecasting Method.""" + import numpy as np import pandas as pd from sktime.forecasting.base import BaseForecaster @@ -8,14 +11,14 @@ class Croston(BaseForecaster): - """Croston's Forecasting Method. + """Croston's method for forecasting intermittent demand. - This was designed for forecasting intermittent demand. + Implements method proposed by Croston in [1]_ and described in [2]_. Parameters ---------- smoothing : float, default = 0.1 - Smoothing parameter + Smoothing parameter. Examples -------- @@ -29,10 +32,10 @@ class Croston(BaseForecaster): References ---------- - [1] J. D. Croston. Forecasting and stock control for intermittent demands. - Operational Research Quarterly (1970-1977), 23(3):pp. 289–303, 1972. - [2] Forecasting: Principles and Practice, - Otext book by Rob J Hyndman and George Athanasopoulos + ..[1] J. D. Croston. Forecasting and stock control for intermittent demands. + Operational Research Quarterly (1970-1977), 23(3):pp. 289–303, 1972. + ..[2] Forecasting: Principles and Practice, + Otext book by Rob J Hyndman and George Athanasopoulos """ _tags = { @@ -55,7 +58,8 @@ def _fit(self, y, X=None, fh=None): fh : int, list or np.array, optional (default=None) The forecasters horizon with the steps ahead to to predict. X : pd.DataFrame, optional (default=None) - Exogenous variables are ignored + Exogenous variables are ignored. + Returns ------- self : returns an instance of self. @@ -104,11 +108,12 @@ def _predict( fh : int, list or np.array, optional (default=None) The forecasters horizon with the steps ahead to to predict. X : pd.DataFrame, optional (default=None) - Exogenous variables are ignored + Exogenous variables are ignored. + Returns ------- forecast : pd.series - predicted forecasts + Predicted forecasts. """ len_fh = len(self.fh) f = self._f diff --git a/sktime/forecasting/ets.py b/sktime/forecasting/ets.py index 7eb21a15980..e02a92cc2d9 100644 --- a/sktime/forecasting/ets.py +++ b/sktime/forecasting/ets.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements automatic and manually exponential time series smoothing models.""" + __all__ = ["AutoETS"] __author__ = ["Hongyi Yang"] @@ -10,8 +14,7 @@ class AutoETS(_StatsModelsAdapter): - """ - ETS models with both manual and automatic fitting capabilities. + """ETS models with both manual and automatic fitting capabilities. Manual fitting is adapted from statsmodels' version, while automatic fitting is adapted from R version of ets. @@ -146,12 +149,12 @@ class AutoETS(_StatsModelsAdapter): References ---------- - [1] Hyndman, R.J., & Athanasopoulos, G. (2019) *Forecasting: - principles and practice*, 3rd edition, OTexts: Melbourne, - Australia. OTexts.com/fpp3. Accessed on April 19th 2020. + .. [1] Hyndman, R.J., & Athanasopoulos, G. (2019) *Forecasting: + principles and practice*, 3rd edition, OTexts: Melbourne, + Australia. OTexts.com/fpp3. Accessed on April 19th 2020. - Example - ---------- + Examples + -------- >>> from sktime.datasets import load_airline >>> from sktime.forecasting.ets import AutoETS >>> y = load_airline() @@ -360,9 +363,9 @@ def _fit(error, trend, seasonal, damped): ) def summary(self): - """ - Get a summary of the fitted forecaster, - same as the implementation in statsmodels: + """Get a summary of the fitted forecaster. + + This is the same as the implementation in statsmodels: https://www.statsmodels.org/dev/examples/notebooks/generated/ets.html """ return self._fitted_forecaster.summary() diff --git a/sktime/forecasting/exp_smoothing.py b/sktime/forecasting/exp_smoothing.py index 5d0ba2c7990..247a678825e 100644 --- a/sktime/forecasting/exp_smoothing.py +++ b/sktime/forecasting/exp_smoothing.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements Holt-Winters exponential smoothing.""" + __all__ = ["ExponentialSmoothing"] __author__ = ["Markus Löning", "@big-o"] @@ -8,10 +12,10 @@ class ExponentialSmoothing(_StatsModelsAdapter): - """ - Holt-Winters exponential smoothing forecaster. Default settings use - simple exponential smoothing - without trend and seasonality components. + """Holt-Winters exponential smoothing forecaster. + + Default settings use simple exponential smoothing without trend and + seasonality components. Parameters ---------- @@ -43,8 +47,8 @@ class ExponentialSmoothing(_StatsModelsAdapter): [1] Hyndman, Rob J., and George Athanasopoulos. Forecasting: principles and practice. OTexts, 2014. - Example - ---------- + Examples + -------- >>> from sktime.datasets import load_airline >>> from sktime.forecasting.exp_smoothing import ExponentialSmoothing >>> y = load_airline() diff --git a/sktime/forecasting/fbprophet.py b/sktime/forecasting/fbprophet.py index 0f58c3e1f38..4c1ea8d696d 100644 --- a/sktime/forecasting/fbprophet.py +++ b/sktime/forecasting/fbprophet.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) - -"""Prophet forecaster by wrapping fbprophet.""" +"""Implements Prophet forecaster by wrapping fbprophet.""" __author__ = ["Martin Walter"] __all__ = ["Prophet"] @@ -85,8 +84,8 @@ class Prophet(_ProphetAdapter): https://facebook.github.io/prophet https://github.com/facebook/prophet - Example - ---------- + Examples + -------- >>> from sktime.datasets import load_airline >>> from sktime.forecasting.fbprophet import Prophet >>> # Prophet requires to have data with a pandas.DatetimeIndex diff --git a/sktime/forecasting/hcrystalball.py b/sktime/forecasting/hcrystalball.py index 64d151a1d6d..5d4d131fa3b 100644 --- a/sktime/forecasting/hcrystalball.py +++ b/sktime/forecasting/hcrystalball.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements wrapper for using HCrystalBall forecastsers in sktime.""" + import pandas as pd from sklearn.base import clone @@ -27,7 +31,7 @@ def _check_index(index): def _adapt_y_X(y, X): - """Adapt fit data to HCB compliant format + """Adapt fit data to HCB compliant format. Parameters ---------- @@ -54,7 +58,7 @@ def _adapt_y_X(y, X): def _get_X_pred(X_pred, index): - """Translate forecast horizon interface to HCB native dataframe + """Translate forecast horizon interface to HCB native dataframe. Parameters ---------- @@ -77,9 +81,9 @@ def _get_X_pred(X_pred, index): def _adapt_y_pred(y_pred): - """Translate wrapper prediction to sktime format + """Translate wrapper prediction to sktime format. - From Dataframe to series + From Dataframe to series. Parameters ---------- @@ -94,6 +98,13 @@ def _adapt_y_pred(y_pred): class HCrystalBallForecaster(BaseForecaster): + """Implement wrapper to allow use of HCrystalBall forecasters in sktime. + + Parameters + ---------- + model : + The HCrystalBall forecasting model to use. + """ _tags = { "univariate-only": True, @@ -121,7 +132,6 @@ def _fit(self, y, X=None, fh=None): ------- self : returns an instance of self. """ - y, X = _adapt_y_X(y, X) self.model_ = clone(self.model) self.model_.fit(X, y) @@ -129,7 +139,7 @@ def _fit(self, y, X=None, fh=None): return self def _predict(self, fh=None, X=None, return_pred_int=False, alpha=DEFAULT_ALPHA): - """Make forecasts for the given forecast horizon + """Make forecasts for the given forecast horizon. Parameters ---------- @@ -156,6 +166,7 @@ def _predict(self, fh=None, X=None, return_pred_int=False, alpha=DEFAULT_ALPHA): return _adapt_y_pred(y_pred) def get_fitted_params(self): + """Get fitted parameters.""" raise NotImplementedError() def _compute_pred_err(self, alphas): diff --git a/sktime/forecasting/model_evaluation/__init__.py b/sktime/forecasting/model_evaluation/__init__.py index 565e5135589..68a87478e4f 100644 --- a/sktime/forecasting/model_evaluation/__init__.py +++ b/sktime/forecasting/model_evaluation/__init__.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements functionality to evaluate forecasting models.""" __author__ = ["Martin Walter"] __all__ = ["evaluate"] diff --git a/sktime/forecasting/model_evaluation/_functions.py b/sktime/forecasting/model_evaluation/_functions.py index eb9d9a0281c..53573d342ee 100644 --- a/sktime/forecasting/model_evaluation/_functions.py +++ b/sktime/forecasting/model_evaluation/_functions.py @@ -1,4 +1,7 @@ +#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements functions to be used in evaluating forecasting models.""" __author__ = ["Martin Walter", "Markus Löning"] __all__ = ["evaluate"] @@ -49,13 +52,13 @@ def evaluate( y_pred, y_test. Returns - ---------- + ------- pd.DataFrame DataFrame that contains several columns with information regarding each refit/update and prediction of the forecaster. - Example - ------- + Examples + -------- >>> from sktime.datasets import load_airline >>> from sktime.forecasting.model_evaluation import evaluate >>> from sktime.forecasting.model_selection import ExpandingWindowSplitter diff --git a/sktime/forecasting/model_selection/__init__.py b/sktime/forecasting/model_selection/__init__.py index 71f191ce74c..a6c9fd9ed42 100644 --- a/sktime/forecasting/model_selection/__init__.py +++ b/sktime/forecasting/model_selection/__init__.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements functionality for selecting forecasting models.""" __author__ = ["Markus Löning", "Kutay Koralturk"] __all__ = [ diff --git a/sktime/forecasting/model_selection/_split.py b/sktime/forecasting/model_selection/_split.py index 64e983e1f0a..5ecc900551e 100644 --- a/sktime/forecasting/model_selection/_split.py +++ b/sktime/forecasting/model_selection/_split.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implement dataset splitting for model evaluation and seleciton.""" __all__ = [ "ExpandingWindowSplitter", @@ -34,7 +35,7 @@ def _repr(self): - """Helper function to build repr for splitters similar to estimator objects""" + """Build repr for splitters similar to estimator objects.""" # This is copied from scikit-learn's BaseEstimator get_params method cls = self.__class__ init = getattr(cls.__init__, "deprecated_original", cls.__init__) @@ -96,20 +97,19 @@ def has_changed(k, v): def _check_y(y): - """Check input to `split` function""" + """Check input to `split` function.""" if isinstance(y, pd.Series): y = y.index return check_time_index(y) def _check_fh(fh): - """Check and convert fh to format expected by CV splitters""" + """Check and convert fh to format expected by CV splitters.""" return check_fh(fh, enforce_relative=True) def _get_end(y, fh): - """Compute the end of the last training window for a given and forecasting - horizon.""" + """Compute the end of the last training window for a forecasting horizon.""" # `fh` is assumed to be ordered and checked by `_check_fh` and `window_length` by # `check_window_length`. n_timepoints = y.shape[0] @@ -182,7 +182,7 @@ def split(self, y): yield train[train >= 0], test[test >= 0] def _split(self, y): - """Internal split method implemented by concrete classes""" + """Split method containing internal logic implemented by concrete classes.""" raise NotImplementedError("abstract method") def get_n_splits(self, y=None): @@ -216,7 +216,7 @@ def get_cutoffs(self, y=None): raise NotImplementedError("abstract method") def get_fh(self): - """Return the forecasting horizon + """Return the forecasting horizon. Returns ------- @@ -266,16 +266,16 @@ def _split(self, y): yield training_window, test_window def get_n_splits(self, y=None): - """Return the number of splits""" + """Return the number of splits.""" return len(self.cutoffs) def get_cutoffs(self, y=None): - """Return the cutoff points""" + """Return the cutoff points.""" return check_cutoffs(self.cutoffs) class BaseWindowSplitter(BaseSplitter): - """Base class for sliding and expanding window splitter""" + """Base class for sliding and expanding window splitter.""" def __init__( self, @@ -333,12 +333,11 @@ def _split(self, y): @staticmethod def _split_windows(start, end, step_length, window_length, fh): - """Abstract method implemented by concrete classes for sliding and expanding - windows""" + """Abstract method for sliding/expanding windows.""" raise NotImplementedError("abstract method") def _get_start(self, fh): - """Get the first split point""" + """Get the first split point.""" # By default, the first split point is the index zero, the first # observation in # the data. @@ -369,7 +368,7 @@ def _get_start(self, fh): return start def get_n_splits(self, y=None): - """Return number of splits + """Return number of splits. Parameters ---------- @@ -467,7 +466,7 @@ def __init__( @staticmethod def _split_windows(start, end, step_length, window_length, fh): - """Sliding windows""" + """Generate sliding windows.""" for split_point in range(start, end, step_length): train = np.arange(split_point - window_length, split_point) test = split_point + fh - 1 @@ -526,7 +525,7 @@ def __init__( @staticmethod def _split_windows(start, end, step_length, window_length, fh): - """Expanding windows""" + """Generate expanding windows.""" for split_point in range(start, end, step_length): train = np.arange(start - window_length, split_point) test = split_point + fh - 1 @@ -594,7 +593,8 @@ def get_cutoffs(self, y=None): def temporal_train_test_split(y, X=None, test_size=None, train_size=None, fh=None): - """Split arrays or matrices into sequential train and test subsets + """Split arrays or matrices into sequential train and test subsets. + Creates train/test splits over endogenous arrays an optional exogenous arrays. @@ -648,8 +648,10 @@ def temporal_train_test_split(y, X=None, test_size=None, train_size=None, fh=Non def _split_by_fh(y, fh, X=None): - """Helper function to split time series with forecasting horizon handling both - relative and absolute horizons""" + """Split time series with forecasting horizon. + + Handles both relative and absolute horizons. + """ if X is not None: check_equal_time_index(y, X) fh = check_fh(fh) diff --git a/sktime/forecasting/model_selection/_tune.py b/sktime/forecasting/model_selection/_tune.py index d8102be22b4..6df57153575 100644 --- a/sktime/forecasting/model_selection/_tune.py +++ b/sktime/forecasting/model_selection/_tune.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements grid search functionality to tune forecasters.""" __author__ = ["Markus Löning"] __all__ = ["ForecastingGridSearchCV", "ForecastingRandomizedSearchCV"] @@ -221,6 +222,7 @@ def _fit(self, y, X=None, fh=None, **fit_params): The forecasters horizon with the steps ahead to to predict. X : pd.DataFrame, optional (default=None) Exogenous variables are ignored + Returns ------- self : returns an instance of self. diff --git a/sktime/forecasting/naive.py b/sktime/forecasting/naive.py index 75c747f15e8..1251423f01e 100644 --- a/sktime/forecasting/naive.py +++ b/sktime/forecasting/naive.py @@ -1,6 +1,7 @@ -#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements simple forecasts based on naive assumptions.""" __all__ = ["NaiveForecaster"] __author__ = ["Markus Löning", "Piyush Gade"] @@ -16,7 +17,8 @@ class NaiveForecaster(_BaseWindowForecaster): - """ + """Forecast based on naive assumptions about past trends continuing. + NaiveForecaster is a forecaster that makes forecasts using simple strategies. @@ -47,8 +49,8 @@ class NaiveForecaster(_BaseWindowForecaster): Window length to use in the `mean` strategy. If None, entire training series will be used. - Example - ---------- + Examples + -------- >>> from sktime.datasets import load_airline >>> from sktime.forecasting.naive import NaiveForecaster >>> y = load_airline() @@ -155,7 +157,7 @@ def _fit(self, y, X=None, fh=None): def _predict_last_window( self, fh, X=None, return_pred_int=False, alpha=DEFAULT_ALPHA ): - """Internal predict""" + """Calculate predictions for use in predict.""" last_window, _ = self._get_last_window() fh = fh.to_relative(self.cutoff) diff --git a/sktime/forecasting/online_learning/__init__.py b/sktime/forecasting/online_learning/__init__.py index 38a63f302e8..79f8e03f9a9 100644 --- a/sktime/forecasting/online_learning/__init__.py +++ b/sktime/forecasting/online_learning/__init__.py @@ -1,6 +1,7 @@ -#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implments algorithms for creating online ensembles of forecasters.""" __author__ = ["William Zheng"] diff --git a/sktime/forecasting/online_learning/_online_ensemble.py b/sktime/forecasting/online_learning/_online_ensemble.py index 31ab2526bea..e28b95a7380 100644 --- a/sktime/forecasting/online_learning/_online_ensemble.py +++ b/sktime/forecasting/online_learning/_online_ensemble.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements framework for applying online ensembling algorithms to forecasters.""" + import numpy as np import pandas as pd @@ -10,7 +14,7 @@ class OnlineEnsembleForecaster(EnsembleForecaster): - """Online Updating Ensemble of forecasters + """Online Updating Ensemble of forecasters. Parameters ---------- @@ -47,19 +51,22 @@ def _fit(self, y, X=None, fh=None): The forecasters horizon with the steps ahead to to predict. X : pd.DataFrame, optional (default=None) Exogenous variables are ignored + Returns ------- self : returns an instance of self. """ - names, forecasters = self._check_forecasters() self.weights = np.ones(len(forecasters)) / len(forecasters) self._fit_forecasters(forecasters, y, X, fh) return self def _fit_ensemble(self, y, X=None): - """Fits the ensemble by allowing forecasters to predict and - compares to the actual parameters. + """Fit the ensemble. + + This makes predictions with individual forecasters and compares the + results to actual values. This is then used to update ensemble + weights. Parameters ---------- @@ -87,7 +94,6 @@ def _update(self, y, X=None, update_params=False): ------- self : an instance of self """ - if len(y) >= 1 and self.ensemble_algorithm is not None: self._fit_ensemble(y, X) diff --git a/sktime/forecasting/online_learning/_prediction_weighted_ensembler.py b/sktime/forecasting/online_learning/_prediction_weighted_ensembler.py index 0cbd4b99884..1c2d5eaaf11 100644 --- a/sktime/forecasting/online_learning/_prediction_weighted_ensembler.py +++ b/sktime/forecasting/online_learning/_prediction_weighted_ensembler.py @@ -1,13 +1,18 @@ # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements online algorithms for prediction weighted ensembles.""" + import numpy as np from scipy.optimize import bisect from scipy.optimize import nnls class _PredictionWeightedEnsembler: - """Wrapper class to handle ensemble algorithms that use multiple forecasters - for prediction. We implement default methods for setting uniform weights, - updating and prediction. + """Wrapper class to handle ensemble algorithms that use multiple forecasters. + + This implements default methods for setting uniform weights, updating + and prediction. Parameters ---------- @@ -30,8 +35,7 @@ def __init__(self, n_estimators=10, loss_func=None): super(_PredictionWeightedEnsembler, self).__init__() def _predict(self, y_pred): - """Performs prediction by taking a weighted average of the estimator - predictions w.r.t the weights vector + """Make predictions by taking weighted average of forecaster predictions. Parameters ---------- @@ -48,8 +52,7 @@ def _predict(self, y_pred): return prediction def _modify_weights(self, new_array): - """Performs a pointwise multiplication of the current - weights with a new array of weights. + """Multiply pointwise the current weights with a new array of weights. Parameters ---------- @@ -60,8 +63,10 @@ def _modify_weights(self, new_array): self.weights /= np.sum(self.weights) def _update(self, y_pred, y_true): - """Resets the weights over the estimators by passing previous observations - to the weighting algorithm + """Update fitted paramters and performs a new ensemble fit. + + Resets the weights over the estimators by passing previous + observations to the weighting algorithm. Parameters ---------- @@ -73,7 +78,7 @@ def _update(self, y_pred, y_true): raise NotImplementedError() def _uniform_weights(self, n_estimators): - """Resets weights for n estimator to uniform weights + """Reset weights for n estimator to uniform weights. Parameters ---------- @@ -85,8 +90,10 @@ def _uniform_weights(self, n_estimators): class HedgeExpertEnsemble(_PredictionWeightedEnsembler): - """Wrapper class to set parameters for hedge-style ensemble algorithms with - a forecasting horizon and normalizing constant. + """Use hedge-style ensemble algorithms. + + Wrapper for hedge-style ensemble algorithms with a forecasting horizon and + normalizing constant. Parameters ---------- @@ -115,7 +122,9 @@ def __init__(self, n_estimators=10, T=10, a=1, loss_func=None): class NormalHedgeEnsemble(HedgeExpertEnsemble): - """Implementation of A Parameter-free Hedging Algorithm, + """Parameter free hedging algorithm. + + Implementation of A Parameter-free Hedging Algorithm, Kamalika Chaudhuri, Yoav Freund, Daniel Hsu (2009) as a hedge-style algorithm. @@ -142,8 +151,10 @@ def __init__(self, n_estimators=10, a=1, loss_func=None): self.R = np.zeros(n_estimators) def update(self, y_pred, y_true, low_c=0.01): - """Resets the weights over the estimators by passing previous observations - and updating based on Normal Hedge. + """Update forecaster weights. + + The weights are updated over the estimators by passing previous + observations and updating based on Normal Hedge. Parameters ---------- @@ -170,7 +181,9 @@ def update(self, y_pred, y_true, low_c=0.01): self._update_weights(low_c=low_c) def _update_weights(self, low_c=0.01): - """Updates the weights on each of the estimators by performing a potential + """Update forecaster weights. + + Update the weights on each of the estimators by performing a potential function update with a root-finding search. low_c represents the lower bound on the window that the root finding is occuring over. @@ -179,7 +192,6 @@ def _update_weights(self, low_c=0.01): low_c : float lowest value that c can take """ - # Calculating Normalizing Constant R_plus = np.array(list(map(lambda x: 0 if 0 > x else x, self.R))) normalizing_R = np.max(R_plus) @@ -190,7 +202,7 @@ def _update_weights(self, low_c=0.01): high_c = (max(R_plus) ** 2) / 2 def _pot(c): - """Internal Potential Function + """Calculate algorithm's potential Function. Parameters ---------- @@ -206,7 +218,7 @@ def _pot(c): c_t = bisect(_pot, low_c, high_c) def _prob(r, c_t): - """Internal Probability Function + """Calculate algorithm's probability Function. Parameters ---------- @@ -227,7 +239,9 @@ def _prob(r, c_t): class NNLSEnsemble(_PredictionWeightedEnsembler): - """Ensemble class that performs a non-negative least squares to fit to the + """Ensemble forecasts with Non-negative least squares based weighting. + + Ensemble class that performs a non-negative least squares to fit to the estimators. Keeps track of all observations seen so far and fits to it. Parameters @@ -250,6 +264,15 @@ def __init__(self, n_estimators=10, loss_func=None): self.total_y_true = np.empty(0) def update(self, y_pred, y_true): + """Update the online ensemble with new data. + + Parameters + ---------- + y_pred : np.array(), shape=(time_axis,estimator_axis) + array with predictions from the estimators + y_true : np.array(), shape=(time_axis) + array with actual values for predicted quantity + """ self.total_y_pred = np.concatenate((self.total_y_pred, y_pred), axis=1) self.total_y_true = np.concatenate((self.total_y_true, y_true)) weights, loss = nnls(self.total_y_pred.T, self.total_y_true) diff --git a/sktime/forecasting/tbats.py b/sktime/forecasting/tbats.py index 041b74be849..298d083e543 100644 --- a/sktime/forecasting/tbats.py +++ b/sktime/forecasting/tbats.py @@ -1,6 +1,12 @@ -#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# !/usr/bin/env python3 -u # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements TBATS algorithm. + +TBATS refers to Exponential smoothing state space model with Box-Cox +transformation, ARMA errors, Trigonometric Trend and Seasonal components as +described in De LIvera, Hyndman and Snyder (2011). +""" __author__ = ["Martin Walter"] __all__ = ["TBATS"] @@ -53,8 +59,8 @@ class TBATS(_TbatsAdapter): context: abstract.ContextInterface, optional (default=None) For advanced users only. Provide this to override default behaviors - Example - ---------- + Examples + -------- >>> from sktime.datasets import load_airline >>> from sktime.forecasting.tbats import TBATS >>> y = load_airline() diff --git a/sktime/forecasting/theta.py b/sktime/forecasting/theta.py index 19a36544d8c..ef1eec9103d 100644 --- a/sktime/forecasting/theta.py +++ b/sktime/forecasting/theta.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- -"""Theta forecaster from statsmodels.""" + +"""Theta forecaster.""" + __all__ = ["ThetaForecaster"] __author__ = ["big-o", "mloning"] @@ -20,21 +22,16 @@ class ThetaForecaster(ExponentialSmoothing): """Theta method for forecasting. The theta method as defined in [1]_ is equivalent to simple exponential - smoothing - (SES) with drift. This is demonstrated in [2]_. + smoothing (SES) with drift (as demonstrated in [2]_). The series is tested for seasonality using the test outlined in A&N. If - deemed - seasonal, the series is seasonally adjusted using a classical - multiplicative - decomposition before applying the theta method. The resulting forecasts - are then - reseasonalised. + deemed seasonal, the series is seasonally adjusted using a classical + multiplicative decomposition before applying the theta method. The + resulting forecasts are then reseasonalised. In cases where SES results in a constant forecast, the theta forecaster - will revert - to predicting the SES constant plus a linear trend derived from the - training data. + will revert to predicting the SES constant plus a linear trend derived + from the training data. Prediction intervals are computed using the underlying state space model. @@ -66,21 +63,17 @@ class ThetaForecaster(ExponentialSmoothing): References ---------- - .. [1] `Assimakopoulos, V. and Nikolopoulos, K. The theta model: a - decomposition - approach to forecasting. International Journal of Forecasting 16, - 521-530, - 2000. - `_ + .. [1] Assimakopoulos, V. and Nikolopoulos, K. The theta model: a + decomposition approach to forecasting. International Journal of + Forecasting 16, 521-530, 2000. + https://www.sciencedirect.com/science/article/pii/S0169207000000662 .. [2] `Hyndman, Rob J., and Billah, Baki. Unmasking the Theta method. - International J. Forecasting, 19, 287-290, 2003. - `_ + International J. Forecasting, 19, 287-290, 2003. + https://www.sciencedirect.com/science/article/pii/S0169207001001431 - Example - ---------- + Examples + -------- >>> from sktime.datasets import load_airline >>> from sktime.forecasting.theta import ThetaForecaster >>> y = load_airline() @@ -120,6 +113,7 @@ def _fit(self, y, X=None, fh=None): The forecasters horizon with the steps ahead to to predict. X : pd.DataFrame, optional (default=None) Exogenous variables are ignored + Returns ------- self : returns an instance of self. @@ -227,8 +221,7 @@ def _update(self, y, X=None, update_params=True): def _zscore(level: float, two_tailed: bool = True) -> float: - """ - Calculate a z-score from a confidence level. + """Calculate a z-score from a confidence level. Parameters ---------- diff --git a/sktime/forecasting/trend.py b/sktime/forecasting/trend.py index 2276e0b73db..10c2b3fa168 100644 --- a/sktime/forecasting/trend.py +++ b/sktime/forecasting/trend.py @@ -1,6 +1,7 @@ -#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- -"""copyright: sktime developers, BSD-3-Clause License (see LICENSE file).""" +# !/usr/bin/env python3 -u +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements trend based forecaster.""" __author__ = ["Anthony Jancso", "mloning"] __all__ = ["TrendForecaster", "PolynomialTrendForecaster"] @@ -18,7 +19,7 @@ class TrendForecaster(BaseForecaster): - """Forecast time series data. + """Trend based forecasts of time series data. Default settings train a linear regression model. @@ -127,8 +128,8 @@ class PolynomialTrendForecaster(BaseForecaster): zero. (i.e. a column of ones, acts as an intercept term in a linear model) - Example - ---------- + Examples + -------- >>> from sktime.datasets import load_airline >>> from sktime.forecasting.trend import PolynomialTrendForecaster >>> y = load_airline() diff --git a/sktime/performance_metrics/base/_base.py b/sktime/performance_metrics/base/_base.py index 9b14afa13f7..3dd51c02253 100644 --- a/sktime/performance_metrics/base/_base.py +++ b/sktime/performance_metrics/base/_base.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements base class for defining performance metric in sktime.""" __author__ = ["Ryan Kuhns"] __all__ = ["BaseMetric"] diff --git a/sktime/performance_metrics/forecasting/_classes.py b/sktime/performance_metrics/forecasting/_classes.py index aae102b99bb..4e084fba85c 100644 --- a/sktime/performance_metrics/forecasting/_classes.py +++ b/sktime/performance_metrics/forecasting/_classes.py @@ -1,4 +1,13 @@ +#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Metrics classes to assess performance on forecasting task. + +Classes named as ``*Score`` return a value to maximize: the higher the better. +Classes named as ``*Error`` or ``*Loss`` return a value to minimize: +the lower the better. +""" + from sktime.performance_metrics.base import BaseMetric from sktime.performance_metrics.forecasting._functions import ( relative_loss, diff --git a/sktime/performance_metrics/forecasting/_functions.py b/sktime/performance_metrics/forecasting/_functions.py index 316587c0c94..e3cdd3f6b40 100644 --- a/sktime/performance_metrics/forecasting/_functions.py +++ b/sktime/performance_metrics/forecasting/_functions.py @@ -1,15 +1,13 @@ +#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- -"""Metrics to assess performance on forecasting task. +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Metrics functions to assess performance on forecasting task. -Functions named as ``*_score`` return a scalar value to maximize: the higher -the better. -Function named as ``*_error`` or ``*_loss`` return a scalar value to minimize: +Functions named as ``*_score`` return a value to maximize: the higher the better. +Function named as ``*_error`` or ``*_loss`` return a value to minimize: the lower the better. """ -# !/usr/bin/env python3 -u -# -*- coding: utf-8 -*- -# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) import numpy as np from scipy.stats import gmean from sklearn.utils.stats import _weighted_percentile diff --git a/sktime/registry/__init__.py b/sktime/registry/__init__.py index 37d160e6708..89ea2344494 100644 --- a/sktime/registry/__init__.py +++ b/sktime/registry/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Sktime registry module exports.""" +"""Implements registry for sktime estimator base classes and tags.""" from sktime.registry._tags import ( ESTIMATOR_TAG_REGISTER, diff --git a/sktime/registry/_base_classes.py b/sktime/registry/_base_classes.py index dd81ba5ecea..21a52a4cea4 100644 --- a/sktime/registry/_base_classes.py +++ b/sktime/registry/_base_classes.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -""" -Register of estimator base classes corresponding to sktime scitypes. +"""Register of estimator base classes corresponding to sktime scitypes. This module exports the following: diff --git a/sktime/registry/_tags.py b/sktime/registry/_tags.py index 87261bdfe5f..ef829518fe2 100644 --- a/sktime/registry/_tags.py +++ b/sktime/registry/_tags.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -""" -Register of estimator and object tags. +"""Register of estimator and object tags. Note for extenders: new tags should be entered in ESTIMATOR_TAG_REGISTER. No other place is necessary to add new tags. diff --git a/sktime/regression/__init__.py b/sktime/regression/__init__.py index 5fc57ca4930..0b57ec8e6a6 100644 --- a/sktime/regression/__init__.py +++ b/sktime/regression/__init__.py @@ -1,4 +1,2 @@ # -*- coding: utf-8 -*- -__all__ = ["ComposableTimeSeriesForestRegressor"] - -from sktime.regression.compose._ensemble import ComposableTimeSeriesForestRegressor +"""Implements sktime estimators for time series regression.""" diff --git a/sktime/regression/all/__init__.py b/sktime/regression/all/__init__.py index aae8534583e..046c7ee435f 100644 --- a/sktime/regression/all/__init__.py +++ b/sktime/regression/all/__init__.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +"""Import all time series regression functionality available in sktime.""" __author__ = ["Markus Löning"] __all__ = [ diff --git a/sktime/regression/base.py b/sktime/regression/base.py index e0588c97ece..850a1fba1f6 100644 --- a/sktime/regression/base.py +++ b/sktime/regression/base.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements base class for time series regression estimators in sktime.""" __author__ = ["Markus Löning"] __all__ = ["BaseRegressor"] @@ -9,17 +10,55 @@ class BaseRegressor(BaseEstimator): - """ - Base class for regressors, for identification. - """ + """Base class for regressors, for identification.""" def fit(self, X, y): + """Fit regressor to training data. + + Parameters + ---------- + X : pd.DataFrame, optional (default=None) + Exogeneous data + y : pd.Series, pd.DataFrame, or np.array + Target time series to which to fit the regressor. + + Returns + ------- + self : + Reference to self. + """ raise NotImplementedError("abstract method") def predict(self, X): + """Predict time series. + + Parameters + ---------- + X : pd.DataFrame, shape=[n_obs, n_vars] + A2-d dataframe of exogenous variables. + + Returns + ------- + y_pred : pd.Series + Regression predictions. + """ raise NotImplementedError("abstract method") def score(self, X, y): + """Scores regression against ground truth, R-squared. + + Parameters + ---------- + X : pd.DataFrame, shape=[n_obs, n_vars] + A2-d dataframe of exogenous variables. + y : pd.Series + Target time series to which to compare the predictions. + + Returns + ------- + score : float + R-squared score. + """ from sklearn.metrics import r2_score return r2_score(y, self.predict(X)) diff --git a/sktime/regression/compose/__init__.py b/sktime/regression/compose/__init__.py index 5fc57ca4930..e495edf384c 100644 --- a/sktime/regression/compose/__init__.py +++ b/sktime/regression/compose/__init__.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +"""Implement composite time series regression estimators.""" + __all__ = ["ComposableTimeSeriesForestRegressor"] from sktime.regression.compose._ensemble import ComposableTimeSeriesForestRegressor diff --git a/sktime/regression/compose/_ensemble.py b/sktime/regression/compose/_ensemble.py index 09e491f6ee9..bb1243e4134 100644 --- a/sktime/regression/compose/_ensemble.py +++ b/sktime/regression/compose/_ensemble.py @@ -1,4 +1,8 @@ +#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements a composite Time series Forest Regressor that accepts a pipeline.""" + __author__ = ["Markus Löning", "Ayushmaan Seth"] __all__ = ["ComposableTimeSeriesForestRegressor"] @@ -285,14 +289,17 @@ def _validate_estimator(self): def predict(self, X): """Predict regression target for X. + The predicted regression target of an input sample is computed as the mean predicted regression targets of the trees in the forest. + Parameters ---------- X : array-like or sparse matrix of shape = [n_samples, n_features] The input samples. Internally, its dtype will be converted to ``dtype=np.float32``. If a sparse matrix is provided, it will be converted into a sparse ``csr_matrix``. + Returns ------- y : array of shape = [n_samples] or [n_samples, n_outputs] @@ -314,8 +321,7 @@ def predict(self, X): return np.sum(y_hat, axis=0) / len(self.estimators_) def _set_oob_score(self, X, y): - """ - Compute out-of-bag scores.""" + """Compute out-of-bag scores.""" X, y = check_X_y(X, y, enforce_univariate=True) n_samples = y.shape[0] diff --git a/sktime/regression/interval_based/__init__.py b/sktime/regression/interval_based/__init__.py index fed0e429f51..5c4643097d8 100644 --- a/sktime/regression/interval_based/__init__.py +++ b/sktime/regression/interval_based/__init__.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +"""Implement interval based time series regression estimators.""" __author__ = ["Markus Löning"] __all__ = ["TimeSeriesForestRegressor"] diff --git a/sktime/regression/interval_based/_tsf.py b/sktime/regression/interval_based/_tsf.py index 677f9681fb9..1ee860a823e 100644 --- a/sktime/regression/interval_based/_tsf.py +++ b/sktime/regression/interval_based/_tsf.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -""" - Time Series Forest Regressor (TSF). -""" +"""Time Series Forest Regressor (TSF).""" __author__ = ["Tony Bagnall", "kkoziara", "luiszugasti", "kanand77", "Markus Löning"] __all__ = ["TimeSeriesForestRegressor"] @@ -36,10 +34,10 @@ class TimeSeriesForestRegressor(BaseTimeSeriesForest, ForestRegressor, BaseRegre intervals with replacement and does not use the splitting criteria tiny refinement described in [1]. This is an intentionally stripped down, non configurable version for use as a hive-cote component. For a configurable - tree based ensemble, see sktime.classifiers.ensemble.TimeSeriesForestClassifier + tree based ensemble, see sktime.classifiers.ensemble.TimeSeriesForestClassifier. - Parameters - ---------- + Parameters + ---------- n_estimators : int, ensemble size, optional (default = 200) min_interval : int, minimum width of an interval, optional (default to 3) @@ -48,14 +46,14 @@ class TimeSeriesForestRegressor(BaseTimeSeriesForest, ForestRegressor, BaseRegre ``-1`` means using all processors. random_state : int, seed for random, optional (default = none) - Attributes - ---------- + Attributes + ---------- n_classes : int n_intervals : int classes_ : List of classes for a given problem - References - ---------- + References + ---------- .. [1] H.Deng, G.Runger, E.Tuv and M.Vladimir, "A time series forest for classification and feature extraction",Information Sciences, 239, 2013 Java implementation @@ -67,7 +65,7 @@ class TimeSeriesForestRegressor(BaseTimeSeriesForest, ForestRegressor, BaseRegre _base_estimator = DecisionTreeRegressor() def predict(self, X): - """Predict + """Predict. Parameters ---------- diff --git a/sktime/series_as_features/base/estimators/_ensemble.py b/sktime/series_as_features/base/estimators/_ensemble.py index b2984e4de88..aabf6983489 100644 --- a/sktime/series_as_features/base/estimators/_ensemble.py +++ b/sktime/series_as_features/base/estimators/_ensemble.py @@ -1,4 +1,8 @@ +#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements base class for time series forests.""" + __author__ = ["Markus Löning", "Ayushmaan Seth"] __all__ = ["BaseTimeSeriesForest"] @@ -42,8 +46,7 @@ def _parallel_build_trees( class_weight=None, n_samples_bootstrap=None, ): - """ - Private function used to fit a single tree in parallel.""" + """Private function used to fit a single tree in parallel.""" if verbose > 1: print("building tree %d of %d" % (tree_idx + 1, n_trees)) # noqa: T001 @@ -80,9 +83,7 @@ def _parallel_build_trees( class BaseTimeSeriesForest(BaseForest): - """ - Base class for forests of trees. - """ + """Base class for forests of trees.""" @abstractmethod def __init__( @@ -113,6 +114,7 @@ def __init__( def _make_estimator(self, append=True, random_state=None): """Make and configure a copy of the `estimator_` attribute. + Warning: This method should be used to properly instantiate new sub-estimators. """ @@ -128,8 +130,8 @@ def _make_estimator(self, append=True, random_state=None): return estimator def fit(self, X, y, sample_weight=None): - """ - Build a forest of trees from the training set (X, y). + """Build a forest of trees from the training set (X, y). + Parameters ---------- X : array-like or sparse matrix of shape (n_samples, n_features) @@ -145,6 +147,7 @@ def fit(self, X, y, sample_weight=None): ignored while searching for a split in each node. In the case of classification, splits are also ignored if they would result in any single class carrying a negative weight in either child node. + Returns ------- self : object @@ -270,9 +273,14 @@ def fit(self, X, y, sample_weight=None): return self def apply(self, X): + """Abstract method that is implemented by concrete estimators.""" raise NotImplementedError() def decision_path(self, X): + """Decision path of decision tree. + + Abstract method that is implemented by concrete estimators. + """ raise NotImplementedError() def _validate_X_predict(self, X): @@ -288,7 +296,7 @@ def _validate_X_predict(self, X): @property def feature_importances_(self): - """Compute feature importances for time series forest""" + """Compute feature importances for time series forest.""" # assumes particular structure of clf, # with each tree consisting of a particular pipeline, # as in modular tsf diff --git a/sktime/transformations/series/__init__.py b/sktime/transformations/series/__init__.py index e69de29bb2d..b35aa918a13 100644 --- a/sktime/transformations/series/__init__.py +++ b/sktime/transformations/series/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Module :mod:`sktime.transformations.series` implements series transformations.""" diff --git a/sktime/transformations/series/acf.py b/sktime/transformations/series/acf.py index 52609eecd0f..f0cdda1dde9 100644 --- a/sktime/transformations/series/acf.py +++ b/sktime/transformations/series/acf.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) -""" -Auto-correlation transformations. +"""Auto-correlation transformations. Module :mod:`sktime.transformations.series` implements auto-correlation transformers. @@ -20,11 +20,10 @@ class AutoCorrelationTransformer(_SeriesToSeriesTransformer): - """ - Auto-correlation transformer. + """Auto-correlation transformer. - Example - ------- + Examples + -------- >>> from sktime.transformations.series.acf import PartialAutoCorrelationTransformer >>> from sklearn.preprocessing import MinMaxScaler >>> from sktime.datasets import load_airline @@ -83,8 +82,7 @@ def transform(self, Z, X=None): class PartialAutoCorrelationTransformer(_SeriesToSeriesTransformer): - """ - Partial auto-correlation transformer. + """Partial auto-correlation transformer. Parameters ---------- @@ -99,8 +97,8 @@ class PartialAutoCorrelationTransformer(_SeriesToSeriesTransformer): - ld or ldunbiased : Levinson-Durbin recursion with bias correction - ldb or ldbiased : Levinson-Durbin recursion without bias correction - Example - ------- + Examples + -------- >>> from sktime.transformations.series.acf import AutoCorrelationTransformer >>> from sklearn.preprocessing import MinMaxScaler >>> from sktime.datasets import load_airline diff --git a/sktime/transformations/series/adapt.py b/sktime/transformations/series/adapt.py index 04a29018f56..cba0ec1d0cb 100644 --- a/sktime/transformations/series/adapt.py +++ b/sktime/transformations/series/adapt.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements adaptor for applying Scikit-learn-like transformers to time series.""" __author__ = ["Markus Löning"] __all__ = ["TabularToSeriesAdaptor"] @@ -32,8 +33,7 @@ def _from_2d_numpy_to_series(x, index=None): class TabularToSeriesAdaptor(_SeriesToSeriesTransformer): - """Adaptor for scikit-learn-like tabular transformations to series - setting. + """Adapt scikit-learn-like tabular transformations to series setting. This is useful for applying scikit-learn transformations to series, but only works with transformations that do not require multiple @@ -44,8 +44,8 @@ class TabularToSeriesAdaptor(_SeriesToSeriesTransformer): transformer : Estimator scikit-learn-like transformer to fit and apply to series - Example - ---------- + Examples + -------- >>> from sktime.transformations.series.adapt import TabularToSeriesAdaptor >>> from sklearn.preprocessing import MinMaxScaler >>> from sktime.datasets import load_airline @@ -82,6 +82,7 @@ def fit(self, Z, X=None): def transform(self, Z, X=None): """Transform data. + Returns a transformed version of y. Parameters diff --git a/sktime/transformations/series/boxcox.py b/sktime/transformations/series/boxcox.py index 720f915e6fb..192bbeaa1cf 100644 --- a/sktime/transformations/series/boxcox.py +++ b/sktime/transformations/series/boxcox.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- -"""copyright: sktime developers, BSD-3-Clause License (see LICENSE file).""" +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file). +"""Implmenents Box-Cox and Log Transformations.""" __author__ = ["Markus Löning"] -__all__ = ["BoxCoxTransformer"] +__all__ = ["BoxCoxTransformer", "LogTransformer"] import numpy as np import pandas as pd @@ -26,8 +27,8 @@ class BoxCoxTransformer(_SeriesToSeriesTransformer): """Box-Cox power transform. - Example - ------- + Examples + -------- >>> from sktime.transformations.series.boxcox import BoxCoxTransformer >>> from sktime.datasets import load_airline >>> y = load_airline() @@ -109,14 +110,53 @@ def inverse_transform(self, Z, X=None): class LogTransformer(_SeriesToSeriesTransformer): + """Log transformation. + + Examples + -------- + >>> from sktime.transformations.series.boxcox import LogTransformer + >>> from sktime.datasets import load_airline + >>> y = load_airline() + >>> transformer = LogTransformer() + >>> y_hat = transformer.fit_transform(y) + """ + _tags = {"transform-returns-same-time-index": True} def transform(self, Z, X=None): + """Transform data. + + Parameters + ---------- + Z : pd.Series + Series to transform. + X : pd.DataFrame, optional (default=None) + Exogenous data used in transformation. + + Returns + ------- + Zt : pd.Series + Transformed series. + """ self.check_is_fitted() Z = check_series(Z) return np.log(Z) def inverse_transform(self, Z, X=None): + """Inverse transform data. + + Parameters + ---------- + Z : pd.Series + Series to transform. + X : pd.DataFrame, optional (default=None) + Exogenous data used in transformation. + + Returns + ------- + Zt : pd.Series + Transformed data - the inverse of the Box-Cox transformation. + """ self.check_is_fitted() Z = check_series(Z) return np.exp(Z) @@ -181,7 +221,7 @@ def _all(x): def _guerrero(x, sp, bounds=None): - r"""Return lambda estimated by the Guerrero method [Guerrero]. + """Estimate lambda using the Guerrero method as described in [1]_. Parameters ---------- @@ -201,8 +241,8 @@ def _guerrero(x, sp, bounds=None): References ---------- - [Guerrero] V.M. Guerrero, "Time-series analysis supported by Power - Transformations ", Journal of Forecasting, vol. 12, pp. 37-48, 1993. + .. [1] V.M. Guerrero, "Time-series analysis supported by Power + Transformations ", Journal of Forecasting, vol. 12, pp. 37-48, 1993. """ if sp is None or not is_int(sp) or sp < 2: raise ValueError( diff --git a/sktime/transformations/series/compose.py b/sktime/transformations/series/compose.py index 1140a5c3a1b..c0302c020cb 100644 --- a/sktime/transformations/series/compose.py +++ b/sktime/transformations/series/compose.py @@ -1,11 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) - -"""Series-to-Series Transformers: OptionalPassthrough and Columnwisetransformer.""" - -__author__ = ["Martin Walter", "Svea Meyer"] -__all__ = ["OptionalPassthrough", "ColumnwiseTransformer"] +"""Meta-transformers for building composite transformers.""" import pandas as pd from sktime.transformations.base import _SeriesToSeriesTransformer @@ -14,12 +10,14 @@ from sklearn.base import clone from sklearn.utils.metaestimators import if_delegate_has_method +__author__ = ["Martin Walter", "Svea Meyer"] +__all__ = ["OptionalPassthrough", "ColumnwiseTransformer"] + class OptionalPassthrough(_SeriesToSeriesTransformer): - """ - Tune implicit hyperparameter. + """Wrap an existing transformer to tune whether to include it in a pipeline. - A transformer to tune the implicit hyperparameter whether or not to use a + Allows tuning the implicit hyperparameter whether or not to use a particular transformer inside a pipeline (e.g. TranformedTargetForecaster) or not. This is achived by having the additional hyperparameter "passthrough" which can be added to a grid then (see example). @@ -32,8 +30,8 @@ class OptionalPassthrough(_SeriesToSeriesTransformer): This arg decides whether to apply the given transformer or to just passthrough the data (identity transformation) - Example - ------- + Examples + -------- >>> from sktime.datasets import load_airline >>> from sktime.forecasting.naive import NaiveForecaster >>> from sktime.transformations.series.compose import OptionalPassthrough @@ -82,7 +80,7 @@ def __init__(self, transformer, passthrough=False): super(OptionalPassthrough, self).__init__() def fit(self, Z, X=None): - """Fit data. + """Fit the model. Parameters ---------- @@ -102,7 +100,7 @@ def fit(self, Z, X=None): return self def transform(self, Z, X=None): - """Transform data. + """Apply transformation. Parameters ---------- diff --git a/sktime/transformations/series/cos.py b/sktime/transformations/series/cos.py index 8394253f4d3..e41b659a434 100644 --- a/sktime/transformations/series/cos.py +++ b/sktime/transformations/series/cos.py @@ -1,4 +1,8 @@ +#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements cosine transformation.""" + import numpy as np from sktime.transformations.base import _SeriesToSeriesTransformer @@ -9,9 +13,10 @@ class CosineTransformer(_SeriesToSeriesTransformer): - """ - Example - ---------- + """Cosine transformation. + + Examples + -------- >>> from sktime.transformations.series.cos import CosineTransformer >>> from sktime.datasets import load_airline >>> y = load_airline() @@ -22,6 +27,20 @@ class CosineTransformer(_SeriesToSeriesTransformer): _tags = {"transform-returns-same-time-index": True, "fit-in-transform": True} def transform(self, Z, X=None): + """Transform data. + + Parameters + ---------- + Z : pd.Series + Series to transform. + X : pd.DataFrame, optional (default=None) + Exogenous data used in transformation. + + Returns + ------- + Zt : pd.Series + Transformed series. + """ self.check_is_fitted() Z = check_series(Z) return np.cos(Z) diff --git a/sktime/transformations/series/detrend/_deseasonalize.py b/sktime/transformations/series/detrend/_deseasonalize.py index f21b41deeac..7f3b7ba46ca 100644 --- a/sktime/transformations/series/detrend/_deseasonalize.py +++ b/sktime/transformations/series/detrend/_deseasonalize.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements transformations to deseasonalize a timeseries.""" __author__ = ["Markus Löning"] __all__ = [ @@ -22,8 +23,7 @@ class Deseasonalizer(_SeriesToSeriesTransformer): - """A transformer that removes seasonal components from time - series. + """A transformer that removes seasonal components from time series. Parameters ---------- @@ -32,8 +32,8 @@ class Deseasonalizer(_SeriesToSeriesTransformer): model : str {"additive", "multiplicative"}, optional (default="additive") Model to use for estimating seasonal component - Example - ---------- + Examples + -------- >>> from sktime.transformations.series.detrend import Deseasonalizer >>> from sktime.datasets import load_airline >>> y = load_airline() @@ -59,7 +59,7 @@ def _set_y_index(self, y): self._y_index = y.index def _align_seasonal(self, y): - """Align seasonal components with y's time index""" + """Align seasonal components with y's time index.""" shift = ( -_get_duration( y.index[0], @@ -115,6 +115,7 @@ def _inverse_transform(self, y, seasonal): def transform(self, Z, X=None): """Transform data. + Returns a transformed version of y. Parameters @@ -134,6 +135,7 @@ def transform(self, Z, X=None): def inverse_transform(self, Z, X=None): """Inverse transform data. + Returns a transformed version of y. Parameters @@ -152,7 +154,7 @@ def inverse_transform(self, Z, X=None): return self._inverse_transform(z, seasonal) def update(self, Z, X=None, update_params=False): - """Update fitted parameters + """Update fitted parameters. Parameters ---------- @@ -171,8 +173,7 @@ def update(self, Z, X=None, update_params=False): class ConditionalDeseasonalizer(Deseasonalizer): - """A transformer that removes seasonal components from time - series, conditional on seasonality test. + """Remove seasonal components from time series, conditional on seasonality test. Parameters ---------- @@ -192,8 +193,7 @@ def __init__(self, seasonality_test=None, sp=1, model="additive"): super(ConditionalDeseasonalizer, self).__init__(sp=sp, model=model) def _check_condition(self, y): - """Check if y meets condition""" - + """Check if y meets condition.""" if not callable(self.seasonality_test_): raise ValueError( f"`func` must be a function/callable, but found: " @@ -219,7 +219,6 @@ def fit(self, Z, X=None): ------- self : an instance of self """ - z = check_series(Z, enforce_univariate=True) self._set_y_index(z) sp = check_sp(self.sp) diff --git a/sktime/transformations/series/detrend/_detrend.py b/sktime/transformations/series/detrend/_detrend.py index 833812df5c4..cb6066f0ceb 100644 --- a/sktime/transformations/series/detrend/_detrend.py +++ b/sktime/transformations/series/detrend/_detrend.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements transformations to detrend a time series.""" __all__ = ["Detrender"] __author__ = ["Markus Löning", "Svea Meyer"] @@ -15,8 +16,8 @@ class Detrender(_SeriesToSeriesTransformer): - """ - Remove a trend from a series. + """Remove a trend from a series. + This transformer uses any forecaster and returns the in-sample residuals of the forecaster's predicted values. @@ -49,8 +50,8 @@ class Detrender(_SeriesToSeriesTransformer): forecaster_ : estimator object Model that defines the trend in the series - Example - ---------- + Examples + -------- >>> from sktime.transformations.series.detrend import Detrender >>> from sktime.forecasting.trend import PolynomialTrendForecaster >>> from sktime.datasets import load_airline @@ -68,8 +69,7 @@ def __init__(self, forecaster=None): super(Detrender, self).__init__() def fit(self, Z, X=None): - """ - Compute the trend in the series + """Compute the trend in the series. Parameters ---------- @@ -101,8 +101,7 @@ def fit(self, Z, X=None): return self def transform(self, Z, X=None): - """ - Remove trend from the data. + """Remove trend from the data. Parameters ---------- @@ -142,8 +141,7 @@ def transform(self, Z, X=None): return z - z_pred def inverse_transform(self, Z, X=None): - """ - Add trend back to a time series + """Add trend back to a time series. Parameters ---------- @@ -183,8 +181,7 @@ def inverse_transform(self, Z, X=None): return z + z_pred def update(self, Z, X=None, update_params=True): - """ - Update the parameters of the detrending estimator with new data + """Update the parameters of the detrending estimator with new data. Parameters ---------- diff --git a/sktime/transformations/series/exponent.py b/sktime/transformations/series/exponent.py index be3273bf9d7..e5fb0d8579f 100644 --- a/sktime/transformations/series/exponent.py +++ b/sktime/transformations/series/exponent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- # copyright: sktime developers, BSD-3-Clause License (see LICENSE file) -"""Classes to raise timeseries to a user provied exponent.""" +"""Implements transformers raise time series to user provided exponent.""" __author__ = ["Ryan Kuhns"] __all__ = ["ExponentTransformer", "SqrtTransformer"] @@ -42,8 +42,8 @@ class ExponentTransformer(_SeriesToSeriesTransformer): offset : int or float User supplied offset value. - Example - ------- + Examples + -------- >>> from sktime.transformations.series.exponent import ExponentTransformer >>> from sktime.datasets import load_airline >>> y = load_airline() @@ -76,7 +76,6 @@ def _fit(self, Z, X=None): ------- self """ - if not isinstance(self.power, (int, float)): raise ValueError( f"Expected `power` to be int or float, but found {type(self.power)}." @@ -216,8 +215,8 @@ class SqrtTransformer(ExponentTransformer): offset : int or float User supplied offset value. - Example - ------- + Examples + -------- >>> from sktime.transformations.series.exponent import SqrtTransformer >>> from sktime.datasets import load_airline >>> y = load_airline() diff --git a/sktime/transformations/series/impute.py b/sktime/transformations/series/impute.py index a7306bd8ecf..120d9007f71 100644 --- a/sktime/transformations/series/impute.py +++ b/sktime/transformations/series/impute.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) """Utilities to impute series with missing values.""" + __author__ = ["Martin Walter"] __all__ = ["Imputer"] @@ -44,8 +46,8 @@ class Imputer(_SeriesToSeriesTransformer): random_state : int/float/str, optional Value to set random.seed() if method="random", default None - Example - ---------- + Examples + -------- >>> from sktime.transformations.series.impute import Imputer >>> from sktime.datasets import load_airline >>> y = load_airline() diff --git a/sktime/transformations/series/matrix_profile.py b/sktime/transformations/series/matrix_profile.py index 2186595e80d..a0d36843d2c 100644 --- a/sktime/transformations/series/matrix_profile.py +++ b/sktime/transformations/series/matrix_profile.py @@ -1,4 +1,7 @@ +#!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements matrix profile transformation.""" __author__ = ["Markus Löning"] __all__ = ["MatrixProfileTransformer"] @@ -16,7 +19,8 @@ class MatrixProfileTransformer(_SeriesToSeriesTransformer): - """ + """Calculate the matrix profile of a time series. + Takes as input a single time series dataset and returns the matrix profile for that time series dataset. @@ -24,10 +28,10 @@ class MatrixProfileTransformer(_SeriesToSeriesTransformer): ---------- window_length : int - Example - ---------- - # noqa: - >>> from sktime.transformations.series.matrix_profile import MatrixProfileTransformer + Examples + -------- + >>> from sktime.transformations.series.matrix_profile import \ + MatrixProfileTransformer >>> from sktime.datasets import load_airline >>> y = load_airline() >>> transformer = MatrixProfileTransformer() @@ -41,14 +45,15 @@ def __init__(self, window_length=3): super(MatrixProfileTransformer, self).__init__() def transform(self, Z, X=None): - """ + """Tranform data. + Parameters ---------- Z: pandas.Series Time series dataset(lets say of length=n) Returns - ---------- + ------- Z: pandas.Series Matrix Profile of time series as output with length as (n-window_length+1) """ diff --git a/sktime/transformations/series/outlier_detection.py b/sktime/transformations/series/outlier_detection.py index 6d91aa05234..133e19d93cc 100644 --- a/sktime/transformations/series/outlier_detection.py +++ b/sktime/transformations/series/outlier_detection.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements transformers for detecting outliers in a time series.""" __author__ = ["Martin Walter"] __all__ = ["HampelFilter"] @@ -14,8 +16,9 @@ class HampelFilter(_SeriesToSeriesTransformer): - """HampelFilter to detect outliers based on a sliding window. Correction - of outliers is recommended by means of the sktime.Imputer, + """Use HampelFilter to detect outliers based on a sliding window. + + Correction of outliers is recommended by means of the sktime.Imputer, so both can be tuned separately. Parameters @@ -31,13 +34,17 @@ class HampelFilter(_SeriesToSeriesTransformer): If True, outliers are filled with True and non-outliers with False. Else, outliers are filled with np.nan. + Notes + ----- + Implementation is based on [1]_. + References ---------- - Hampel F. R., "The influence curve and its role in robust estimation", - Journal of the American Statistical Association, 69, 382–393, 1974 + .. [1] Hampel F. R., "The influence curve and its role in robust estimation", + Journal of the American Statistical Association, 69, 382–393, 1974 - Example - ---------- + Examples + -------- >>> from sktime.transformations.series.outlier_detection import HampelFilter >>> from sktime.datasets import load_airline >>> y = load_airline() @@ -61,6 +68,7 @@ def __init__(self, window_length=10, n_sigma=3, k=1.4826, return_bool=False): def transform(self, Z, X=None): """Transform data. + Returns a transformed version of Z. Parameters @@ -86,7 +94,8 @@ def transform(self, Z, X=None): return Z def _transform_series(self, Z): - """ + """Logic internal to the algorithm for transforming the input series. + Parameters ---------- Z : pd.Series @@ -161,7 +170,7 @@ def _hampel_filter(Z, cv, n_sigma, half_window_length, k): def _compare(value, cv_median, cv_sigma, n_sigma): - """Function to identify an outlier + """Identify an outlier. Parameters ---------- diff --git a/sktime/transformations/series/summarize.py b/sktime/transformations/series/summarize.py index ce16c4a11f2..9cd812778b7 100644 --- a/sktime/transformations/series/summarize.py +++ b/sktime/transformations/series/summarize.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- +# copyright: sktime developers, BSD-3-Clause License (see LICENSE file) +"""Implements transformers for summarizing a time series.""" __author__ = ["Markus Löning"] __all__ = ["MeanTransformer"] @@ -11,10 +13,10 @@ class MeanTransformer(_SeriesToPrimitivesTransformer): - """Get mean value of time series + """Get mean value of time series. - Example - ---------- + Examples + -------- >>> from sktime.transformations.series.summarize import MeanTransformer >>> from sktime.datasets import load_airline >>> y = load_airline() @@ -23,7 +25,8 @@ class MeanTransformer(_SeriesToPrimitivesTransformer): """ def transform(self, Z, X=None): - """ + """Transform series. + Parameters ---------- Z : pd.Series diff --git a/sktime/transformations/series/theta.py b/sktime/transformations/series/theta.py index 27257df242e..cb2d31fab18 100644 --- a/sktime/transformations/series/theta.py +++ b/sktime/transformations/series/theta.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -u # -*- coding: utf-8 -*- -"""copyright: sktime developers, BSD-3-Clause License (see LICENSE file).""" +# License: copyright: sktime developers, BSD-3-Clause License (see LICENSE file). +"""Implements Theta-lines transformation for use with automatic theta forecasting.""" __author__ = ["Guzal Bulatova", "Markus Löning"] __all__ = ["ThetaLinesTransformer"] @@ -17,19 +18,23 @@ class ThetaLinesTransformer(_SeriesToSeriesTransformer): """Decompose the original data into two or more Theta-lines. - Example - ------- + Notes + ----- + Implements decomposition as described in [1]_. + + References + ---------- + .. [1] E.Spiliotis et al., "Generalizing the Theta method for + automatic forecasting ", European Journal of Operational + Research, vol. 284, pp. 550-558, 2020. + + Examples + -------- >>> from sktime.transformations.series.theta import ThetaLinesTransformer >>> from sktime.datasets import load_airline >>> y = load_airline() >>> transformer = ThetaLinesTransformer([0, 0.25, 0.5, 0.75]) >>> y_thetas = transformer.fit_transform(y) - - References - ---------- - [1] E.Spiliotis et al., "Generalizing the Theta method for - automatic forecasting ", European Journal of Operational - Research, vol. 284, pp. 550-558, 2020. """ _tags = {