├── .gitattributes ├── .github └── workflows │ ├── docs.yml │ ├── python-publish.yml │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── docs ├── Makefile ├── api.rst ├── conf.py ├── design.rst ├── getting_started.rst ├── index.rst └── make.bat ├── images ├── active_learning_app.png ├── anomaly.png └── logo │ ├── tsod.eps │ ├── tsod.png │ └── tsod.svg ├── notebooks ├── Example Water Level.ipynb ├── Getting started.ipynb ├── SMHI_hydrology.ipynb └── cmems.ipynb ├── pyproject.toml ├── tests ├── __init__.py ├── data │ ├── BO_TS_MO_FINO2.nc │ ├── Ballen_20150218-20201222.csv │ ├── combined.joblib │ └── example.csv ├── data_generation.py ├── test_detectors.py └── test_persistence.py └── tsod ├── __init__.py ├── base.py ├── custom_exceptions.py ├── detectors.py ├── features.py └── hampel.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ipynb linguist-language=Python -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: 3.12 19 | 20 | - name: Install package 21 | run: | 22 | pip install .[dev] 23 | 24 | - name: Sphinx Build 25 | run: | 26 | cd docs 27 | make html 28 | 29 | - name: Publish to GitHub Pages 30 | uses: peaceiris/actions-gh-pages@v3.6.1 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: docs/_build/html -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | workflow_dispatch: 8 | 9 | release: 10 | types: [created] 11 | 12 | jobs: 13 | deploy: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: '3.x' 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install setuptools wheel twine 27 | - name: Build and publish 28 | env: 29 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 30 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 31 | run: | 32 | python setup.py sdist bdist_wheel 33 | twine upload dist/* 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Full test 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest] 19 | python-version: ["3.10", "3.13"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: chartboost/ruff-action@v1 # Fail fast if there are any linting errors 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install wheel pytest pytest-cov 32 | 33 | - name: Install tsod 34 | run: | 35 | pip install .[test] 36 | - name: Test with pytest 37 | run: | 38 | pytest --cov=tsod tests 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # PyCharm 2 | .idea/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # Sphinx 135 | /doc/_build/ 136 | 137 | data/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "tests" 4 | ], 5 | "python.formatting.provider": "none", 6 | "editor.formatOnSave": true, 7 | "python.testing.unittestEnabled": false, 8 | "python.testing.nosetestsEnabled": false, 9 | "python.testing.pytestEnabled": true, 10 | "restructuredtext.confPath": "${workspaceFolder}\\doc", 11 | "[python]": { 12 | "editor.defaultFormatter": "ms-python.black-formatter" 13 | } 14 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 DHI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # tsod: Anomaly Detection for time series data. 4 | 5 | [![Full test](https://github.com/DHI/tsod/actions/workflows/test.yml/badge.svg)](https://github.com/DHI/tsod/actions/workflows/test.yml) 6 | [![PyPI version](https://badge.fury.io/py/tsod.svg)](https://badge.fury.io/py/tsod) 7 | ![Python version](https://img.shields.io/pypi/pyversions/tsod.svg) 8 | 9 | ![univariate](https://raw.githubusercontent.com/DHI/tsod/main/images/anomaly.png) 10 | 11 | Sensors often provide faulty or missing observations. These anomalies must be detected automatically and replaced with more feasible values before feeding the data to numerical simulation engines as boundary conditions or real time decision systems. 12 | 13 | This package aims to provide examples and algorithms for detecting anomalies in time series data specifically tailored to DHI users and the water domain. It is simple to install and deploy operationally and is accessible to everyone (open-source). 14 | 15 | ## Getting Started 16 | 17 | * [Documentation](https://dhi.github.io/tsod/getting_started.html) 18 | * [Notebook](https://github.com/DHI/tsod/blob/main/notebooks/Getting%20started.ipynb) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](http://colab.research.google.com/github/DHI/tsod/blob/main/notebooks/Getting%20started.ipynb) 19 | 20 | 21 | 22 | ## Installation 23 | 24 | `tsod` is a pure Python library and runs on Windows, Linux and Mac. 25 | 26 | From PyPI: 27 | 28 | `pip install tsod` 29 | 30 | Or development version: 31 | 32 | `pip install https://github.com/DHI/tsod/archive/main.zip` 33 | 34 | 38 | 39 | 40 | ## Vision 41 | * A simple and consistent API for anomaly detection of timeseries 42 | * The computational speed will be good for typical timeseries data found in the water domain, to support realtime detection 43 | * It will have a suite of different algorithms ranging from simple rule-based to more advanced based on e.g. neural networks 44 | 45 | ## Definitions 46 | Note that we distinguish between [two types of anomaly detection](https://scikit-learn.org/stable/modules/outlier_detection.html) 47 | 48 | - Outlier detection (unsupervised anomaly detection) 49 | The training data may contain outliers, i.e. observations far from most other observations. Outlier detectors try to concentrate on the observations in the training data that similar and close together, and ignores observations further away. 50 | 51 | - Novelty detection (semi-supervised anomaly detection) 52 | The training data is considered "normal" and is not polluted by outliers. New test data observations can be categorized as an outlier and is in this context called a novelty. 53 | 54 | 55 | ## Contribute to `tsod` 56 | [Open in Visual Studio Code](https://open.vscode.dev/DHI/tsod) 57 | - Follow PEP8 code style. This is automatically checked during Pull Requests. 58 | 59 | - If citing or re-using other code please make sure their license is also consistent with our policy. 60 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API Reference 4 | ================= 5 | 6 | * :class:`tsod.RangeDetector` 7 | * :class:`tsod.ConstantValueDetector` 8 | * :class:`tsod.ConstantGradientDetector` 9 | * :class:`tsod.GradientDetector` 10 | * :class:`tsod.DiffDetector` 11 | * :class:`tsod.CombinedDetector` 12 | 13 | Generic 14 | ------- 15 | .. autoclass:: tsod.RangeDetector 16 | :members: 17 | :undoc-members: 18 | :inherited-members: 19 | 20 | .. autoclass:: tsod.ConstantValueDetector 21 | :members: 22 | :undoc-members: 23 | :inherited-members: 24 | 25 | .. autoclass:: tsod.ConstantGradientDetector 26 | :members: 27 | :undoc-members: 28 | :inherited-members: 29 | 30 | .. autoclass:: tsod.GradientDetector 31 | :members: 32 | :undoc-members: 33 | :inherited-members: 34 | 35 | .. autoclass:: tsod.DiffDetector 36 | :members: 37 | :undoc-members: 38 | :inherited-members: 39 | 40 | .. autoclass:: tsod.CombinedDetector 41 | :members: 42 | :undoc-members: 43 | :inherited-members: 44 | 45 | Hampel 46 | ------- 47 | .. autoclass:: tsod.hampel.HampelDetector 48 | :members: 49 | :undoc-members: 50 | :inherited-members: 51 | 52 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "tsod" 21 | copyright = ( 22 | "2021, Henrik Andersson, Rasmus Halvgaard, Laura Frøhlich, Jesper Mariegaard" 23 | ) 24 | author = "Henrik Andersson, Rasmus Halvgaard, Laura Frøhlich, Jesper Mariegaard" 25 | 26 | # The full version, including alpha/beta/rc tags 27 | release = "" 28 | 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon"] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ["_templates"] 39 | 40 | # List of patterns, relative to source directory, that match files and 41 | # directories to ignore when looking for source files. 42 | # This pattern also affects html_static_path and html_extra_path. 43 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 44 | 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | 48 | # The theme to use for HTML and HTML Help pages. See the documentation for 49 | # a list of builtin themes. 50 | # 51 | html_theme = "sphinx_book_theme" 52 | 53 | # Add any paths that contain custom static files (such as style sheets) here, 54 | # relative to this directory. They are copied after the builtin static files, 55 | # so a file named "default.css" will overwrite the builtin "default.css". 56 | # html_static_path = ['_static'] 57 | 58 | # autoclass_content = 'both' 59 | -------------------------------------------------------------------------------- /docs/design.rst: -------------------------------------------------------------------------------- 1 | .. _design: 2 | 3 | Design philosophy 4 | ================= 5 | 6 | * Easy to use 7 | * Easy to install 8 | * Easy to get started 9 | * Open Source​ 10 | * Easy to collaborate​ 11 | * Reproducible 12 | * Easy access to new features 13 | 14 | 15 | Easy to use 16 | ----------- 17 | Common operations such as reading a file should only need a few lines of code. 18 | 19 | Make extensive use of existing standard libraries for scientific computing such as numpy, matplotlib and pandas. 20 | 21 | 22 | Easy to install 23 | --------------- 24 | 25 | From PyPI:: 26 | 27 | pip install tsod 28 | 29 | 30 | Easy to get started 31 | ------------------- 32 | By providing many examples to cut/paste from. 33 | 34 | Examples are available in two forms: 35 | 36 | * `Unit tests `_ 37 | * `Jupyter notebooks `_ 38 | 39 | Open Source​ 40 | ------------ 41 | 42 | tsod is an open source project licensed under the `MIT license `_. 43 | The software is provided free of charge with the source code available for inspection and modification. 44 | 45 | 46 | Easy to collaborate​ 47 | -------------------- 48 | 49 | By developing `tsod` on GitHub along with a completely open discussion, we believe that the collaboration between developers and end-users results in a useful library. 50 | 51 | Reproducible 52 | ------------ 53 | 54 | By providing the historical versions of `tsod`` on PyPI it is possible to reproduce the behaviour of an older existing system, based on an older version. 55 | 56 | Install specific version:: 57 | 58 | pip install tsod==0.1.2 59 | 60 | 61 | Install development version:: 62 | 63 | pip install https://github.com/DHI/tsod/archive/main.zip 64 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | .. _getting_started: 2 | 3 | Getting started 4 | =============== 5 | 6 | `tsod` is library for timeseries data. The format of a timeseries is always a :py:class:`~pandas.Series` and in some cases with a :py:class:`~pandas.DatetimeIndex` 7 | 8 | 1. Get data in the form of a a :py:class:`~pandas.Series` (see Data formats below) 9 | 2. Select one or more detectors e.g. :class:`RangeDetector ` or :class:`ConstantValueDetector ` 10 | 3. Define parameters (e.g. min/max, max rate of change) or... 11 | 4. Fit parameters based on normal data, i.e. without outliers 12 | 5. Detect outliers in any dataset 13 | 14 | .. image:: https://colab.research.google.com/assets/colab-badge.svg 15 | :target: http://colab.research.google.com/github/DHI/tsod/blob/main/notebooks/Getting%20started.ipynb 16 | 17 | Example 18 | ------- 19 | 20 | >>> import pandas as pd 21 | >>> from tsod import RangeDetector 22 | >>> rd = RangeDetector(max_value=2.0) 23 | >>> data = pd.Series([0.0, 1.0, 3.0]) # 3.0 is out of range i.e. an anomaly 24 | >>> anom = rd.detect(data) 25 | >>> anom 26 | 0 False 27 | 1 False 28 | 2 True 29 | dtype: bool 30 | >>> data[anom] # get anomalous data 31 | 2 3.0 32 | dtype: float64 33 | >>> data[~anom] # get normal data 34 | 0 0.0 35 | 1 1.0 36 | dtype: float64 37 | >>> 38 | 39 | 40 | Saving and loading 41 | ------------------ 42 | .. code-block:: python 43 | 44 | # save a configured detector 45 | cd = CombinedDetector([ConstantValueDetector(), RangeDetector()]) 46 | cd.fit(normal_data) 47 | cd.save("detector.joblib") 48 | 49 | # ... and then later load it from disk 50 | my_detector = tsod.load("detector.joblib") 51 | my_detector.detect(some_data) 52 | 53 | Data formats 54 | ------------ 55 | 56 | Converting data to a :py:class:`~pandas.Series` 57 | 58 | .. code-block:: python 59 | 60 | import pandas as pd 61 | df = pd.read_csv("mydata.csv", parse_dates=True, index_col=0) 62 | my_series = df['water_level'] 63 | 64 | from mikeio import Dfs0 65 | dfs = Dfs0('simple.dfs0') 66 | df = dfs.to_dataframe() 67 | my_series_2 = df['rainfall'] 68 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. tsod documentation master file, created by 2 | sphinx-quickstart on Thu Mar 25 08:11:16 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. image:: https://raw.githubusercontent.com/DHI/tsod/main/images/logo/tsod.png 7 | :width: 600 8 | 9 | tsod: Anomaly Detection for time series data. 10 | ============================================= 11 | 12 | 13 | .. image:: https://raw.githubusercontent.com/DHI/tsod/main/images/anomaly.png 14 | 15 | Sensors often provide faulty or missing observations. These anomalies must be detected automatically and replaced with more feasible values before feeding the data to numerical simulation engines as boundary conditions or real time decision systems. 16 | 17 | This package aims to provide examples and algorithms for detecting anomalies in time series data specifically tailored to DHI users and the water domain. It is simple to install and deploy operationally and is accessible to everyone (open-source). 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | :caption: Contents: 22 | :hidden: 23 | 24 | getting_started 25 | design 26 | api 27 | 28 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /images/active_learning_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DHI/tsod/07669cfb4d92daf28e6ee1495f0090ae4046a57e/images/active_learning_app.png -------------------------------------------------------------------------------- /images/anomaly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DHI/tsod/07669cfb4d92daf28e6ee1495f0090ae4046a57e/images/anomaly.png -------------------------------------------------------------------------------- /images/logo/tsod.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DHI/tsod/07669cfb4d92daf28e6ee1495f0090ae4046a57e/images/logo/tsod.eps -------------------------------------------------------------------------------- /images/logo/tsod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DHI/tsod/07669cfb4d92daf28e6ee1495f0090ae4046a57e/images/logo/tsod.png -------------------------------------------------------------------------------- /images/logo/tsod.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 16 | 24 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /notebooks/Example Water Level.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Load water level data from DMI" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import os\n", 17 | "import pandas as pd\n", 18 | "import numpy as np" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 2, 24 | "metadata": {}, 25 | "outputs": [ 26 | { 27 | "name": "stderr", 28 | "output_type": "stream", 29 | "text": [ 30 | "C:\\Users\\JAN\\AppData\\Local\\Temp/ipykernel_21332/396824514.py:3: DeprecationWarning: `set_matplotlib_formats` is deprecated since IPython 7.23, directly use `matplotlib_inline.backend_inline.set_matplotlib_formats()`\n", 31 | " set_matplotlib_formats('png')\n" 32 | ] 33 | } 34 | ], 35 | "source": [ 36 | "from IPython.display import set_matplotlib_formats\n", 37 | "set_matplotlib_formats('png')" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 3, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "from tsod.detectors import CombinedDetector, RangeDetector, DiffDetector, RollingStandardDeviationDetector\n", 47 | "from tsod.hampel import HampelDetector" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 4, 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "file_path = os.path.join(\"..\", \"tests\", \"data\", \"Ballen_20150218-20201222.csv\")\n", 57 | "df = pd.read_csv(file_path, index_col=0, parse_dates=True)" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 5, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "data = df.water_level" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 6, 72 | "metadata": {}, 73 | "outputs": [ 74 | { 75 | "data": { 76 | "text/plain": [ 77 | "" 78 | ] 79 | }, 80 | "execution_count": 6, 81 | "metadata": {}, 82 | "output_type": "execute_result" 83 | }, 84 | { 85 | "data": { 86 | "image/png": "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\n", 87 | "text/plain": [ 88 | "
" 89 | ] 90 | }, 91 | "metadata": { 92 | "needs_background": "light" 93 | }, 94 | "output_type": "display_data" 95 | } 96 | ], 97 | "source": [ 98 | "data.plot()" 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "metadata": {}, 104 | "source": [ 105 | "# Detect anomalies outside manually set range" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": 7, 111 | "metadata": {}, 112 | "outputs": [], 113 | "source": [ 114 | "range_anomalies = RangeDetector(-1, 1.3).detect(data)" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 8, 120 | "metadata": {}, 121 | "outputs": [ 122 | { 123 | "data": { 124 | "text/plain": [ 125 | "" 126 | ] 127 | }, 128 | "execution_count": 8, 129 | "metadata": {}, 130 | "output_type": "execute_result" 131 | }, 132 | { 133 | "data": { 134 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAeIAAADNCAYAAABkZHP0AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAABKeElEQVR4nO2dZ5gVRdaA3zPDzJAzIghIUMkDAiIoSTEgQV1XwbAq5tVVdtVvXRTFhMqaUNRd1siuosKy5ixRRBQHBAREUURAguTMMKG+H933zg19c98wM+d9nnnm3u7q6tPnVtWpOnWqWowxKIqiKIqSHrLSLYCiKIqiVGbUECuKoihKGlFDrCiKoihpRA2xoiiKoqQRNcSKoiiKkkbUECuKoihKGlFDrChJRETuEZFX7M8tRGSfiGQn+Z5rReS0ZN7DbUSkpYgYEamSblkUJdWoIVbKNSIyR0R2ikheumWJhDFmnTGmpjGmJN2ygLvGz7fDkQpE5EYRKRCRQhGZHHCul4h8KiI7RGSriPxXRJoEyFpkd4o8f60d7nGZrZ+rU/BISiVGDbFSbhGRlkBfwABnp1caJcVsBMYBLzqcqwc8C7QEjgb2Ai8FpJlqd4o8f2t8T4pIPeAOYIXbgitKIGqIlfLMZcCXwGTgct8TIjJZRJ4RkfdFZK+IfCUibXzOnyQiX4vIbvv/ST7n5ojIOBH5wh4tvSsiDURkiojssdO39En/pIist88tEpG+TsIGjkBFpI6IvCAim0TkV/ue2fa5Y0Rkri3fNhGZGkoJInKpiPwiIttFZEzAuSwRGS0iP9nnp4lIffv0Z/b/XfZz9ravuVJEvrM9DR+LyNE++XX0GW1uEZE7RGQQltEaYeezNIrnyxaRR+1nWwMMCfV8Thhj3jDGvAVsdzj3oTHmv8aYPcaYA8DTwMmx5A88BEwEtsV4naLEjBpipTxzGTDF/jtTRBoHnL8QuBdrhPQj8ACAbYjex2poGwCPA++LSIOAay8FjgLaAAuwRlX1ge+Au33Sfg10tc+9CvxXRKpGIf9koBg4BjgeOAPwuEHvBz6xZW8GPOWUgYh0AP5py9rUfp5mPkluAs4F+tvndwLP2Of62f/r2qPCBSJyDpZRPQ9oBMwDXrPvVQuYAXxk53UMMNMY8xHwIGWjzC5RPN81wFD7eA/g/IDnGi0i74XQW6z0I3hkO8zuTKwQkesD7t3TlmmSS/dXlPAYY/RP/8rdH9AHKAIa2t9XATf7nJ8MPO/zfTCwyv58KbAwIL8FwEj78xxgjM+5x4APfb4PA5aEkW0n0MX+fA/wiv25JZYbvQrQGCgEqvlcdxEw2/78Hyz3arMIehgLvO7zvQZwGDjN/v4dMNDnfBNbb1V85fE5/yFwlc/3LOAAlov3IuCbEHJ4n9P+Hun5ZgF/9Dl3RqAsUZaDccDkMOfzgR1AX59jHbA6EtnAScAm4CL7XDZQAPTyKQtXp7u861/F/tMRsVJeuRz4xBjjcR2+SoB7Gtjs8/kAUNP+3BT4JSDtL1ijXw9bfD4fdPjuyQsR+T/blbtbRHYBdYCGEeQ/GsgBNonILvu6fwFH2OdvAwRYaI/argyRT1NgveeLMWY//u7ao4E3fe7xHVCCZShDyfWkT/odthxHAc2BnyI8V7TP5yc3wb9HwojIMVgdiz8bY+Z5jhtjVhpjNhpjSowxXwBPUjYivwFYZoz50m15FCUUulRAKXeISDVgOJAtIh5jmwfUFZEuxpilEbLYiGUofGmB5XKNVZa+WEZzILDCGFMqIjuxjFc41mONGBsaY4oDTxpjNmO5bxGRPsAMEfnMGPNjQNJNQHsfeapjuad973OlMWa+g+yBOvCkf8AYMyVE+gtDPE/ga9zCPp8td3Of7y1C5BsXtqwzgPuNMS9HSG4o+70GAv1FZLD9vT5wvIh0Ncbc6KaMiuJBR8RKeeRcrFFdB6y52a5Yxmge1rxxJD4AjhORi0WkioiMsPOKZ06yFtY86FagioiMBWpHusgYswlrDvgxEaltB1W1EZH+ACJygYh45np3YhmLUoespgNDRaSPiOQC9+FfrycBD3iMrog0sueBsWUuBVoHpL9dRDra6euIyAX2ufeAJiLyFxHJE5FaInKifW4L0FJEsqJ5PmAaMEpEmokVoTw6ks58sX+3qliu5GwRqeoTBHcUluv7aWNM0DyviJwjIvXEoicwCnjbPj0Sqyx1tf8KsOIMxgTmoyhuoYZYKY9cDrxkrHW5mz1/WNGxl0iEdbHGmO1YgUK3YrlxbwOG+ri5Y+FjrJH0D1ju1UP4u1zDcRmQC6zEMrbTseZwAU4AvhKRfcA7WO7VNYEZGGNWAH/Ccs1vsvPZ4JPkSfv6T0RkL1aU+Yn2tQewAtjm2+7jXsaYN4G/A6+LyB5gOXCWnX4vcDrWHPlmYDVwin2f/9r/t4vI4iie7zlbd0uBxcAbvs9lR2N/GEZ3d2JNEYwG/mB/vtM+dzVW5+Ie8Vkr7HPthVjBe3ux5uL/boz5t/2MuwLK1GFgjzFmdxhZFCUhxJhAj5KiKIqiKKlCR8SKoiiKkkbUECuKoihKGlFDrCiKoihpRA2xoiiKoqQRNcSKoiiKkkbSsqFHw4YNTcuWLdNxa0VRFEVJOYsWLdpmjGnkdC4thrhly5YUFBSk49aKoiiKknJEJOQ2ruqaVhRFUZQ0ooZYURRFicyyaTChE9xT1/q/bFq6Jaow6EsfFEVRlPAsmwbvjoKig9b33eut7wD5w9MnVwVBR8SKoihKeGbeV2aEPRQdtI4rCaOGWFEURQnP7g2xHVdiQg2xoiiKEp46zWI7rsSEGmJFURQlPAPHQk41/2M51azjSsKoIVYURVHCkz8chk1ke5UjKDXCgWpNYdhEDdRyCTXEipKp6HIRJZPIH86Yo1+ndeEU5gyepUbYRXT5kqJkIrpcRMlgJN0CVDB0RKwomYguF1EyEINJtwgVEjXEipKJ6HIRJYMRHRK7ihpiRclEdLmIkoEYHRAnBTXESvmjMgQx6XIRRak0qCFWyheeIKbd6wFTFsRU0YyxvVxkQ2lDSo1Anea6XERJLpWhg5uhqCFWyheVKYgpfzh9Dk+kdeEUuHm5GmEleTh1cN+4Fu6pE8Iol5NJ4nLSudDlS0r5IpODmJZNszoEuzdYc7kDx6rxVMoHTh1cT4S0z9I5Q+vUypUI5WgJoI6IlfJFpgYxVRaXuVIxidSRDfA6lYuo6XLkPVNDrJQvMjWIKdFKX05caEoFJZqO7O4N5StqOpO9ZwEkbIhFpLmIzBaRlSKyQkT+7IZgSgXBbQOTqUFMiVR6HU0r6WbgWEqzq4VP42Osy8OAOGO9Zw64MSIuBm41xnQAegF/EpEOLuSrlHeSZWAyMYgpkUpfjlxolY7K4qnIH86GvuOtDi4QZGq9XqdyNCQeOJairKr+xzLBe+ZAwobYGLPJGLPY/rwX+A44KtF8lQpAZTIwibjMQ46m13N21ueJy6bERyXzVOw65lz6HJ7I2Q3eg/OeDet1kvIwSZw/nBnHjGFDaUMMGeQ9c8DVqGkRaQkcD3zlcO5a4FqAFi1auHlbJVNJdI4mk6KQI8lif9761hgalGzlcM0mVD3z3ujkrdPMbuyDGZ/zPBQBDEn8GZTYCNeRzMDGPFHEdxScP5w+r9YAYO3NKSx7Ltf51Y3P4vplbbjp1GO49Yy2LgrqLq4Fa4lITeB/wF+MMXsCzxtjnjXG9DDG9GjUqJFbt1WShRsuuZBuWRM5z0wajUQrS/5wbmz8H1oXTuGb8z6PvgFxGk3bVJfD3FalYo7AMp4wnopK4a5ONZlU51OMK4ZYRHKwjPAUY8wbbuSppBG3KkQYAxMxz0xyaydbFjsALdTsW1PZ7s59lOhZNg0kVPMoFdpYhIuMTmrUdBLrWaZHe7sRNS3AC8B3xpjHExdJSTtuVQifCGfHihAuz0xaehBuZBRiVBTz6+Lyh7NFnD1FG02D2PJSEsPTETUlDieFoIClooPw5h/L/Qg5lmnfoKRueNCSUOfLwUw24M6I+GTgUuBUEVli/w12IV8lXbhZIewI55BmKVSembT0IOw9/UdFicSwPJ97KQdMrt+xAyaXh4sr3nxkUknUKDjuMgVGsgkZNWxKyMgRsstR345P75YHLZPqfIpxI2r6c2OMGGPyjTFd7b8P3BBOSRNJqBAbTcPY8syUjTuWTYPD+yOnc8GFNiu3P6OLrvaLVh1ddDXvlPZJKN9KRaxGwclQheocmlIr8jYSmbIyIIlzrn4dTrc8aJlS59OA7qylBJOECvFw8fDY8syEjTs8DdnBHd5DYeeafBvwOOek3int47dGWo1wjMRiFEIZqmr1HLMuqXVU+LgHXzJh96Y4DWT4OWKHk2550JJY52OeKkoxaogrMqHcUpHcVXaF2J17JKVG2JN3ZMIV4p3SPjBsIr8aq5KZOs0i55nujTscGjIRQLKd09dp5r8EJEbKy3xWRhOLUQhlqCDI2B4wuew+6fZgYxGmLKSdJMZZ+I2I3fSguVzny8NyZ1BDXHEJ1dt/75aol+JM6vY2rQun8HKv990xgvnD6V/0FK0Lp1B007eZvxYzxNpeTEnqXWiVZYenRInFKIQySAd3+hnbzTRidNHVHGx3nnXe11j8blLqykKsZSAJU0yO48qBYynOLh87WGUqaogrKqF6+4smR+2u8nih3OxVevLKdFcREHq0I9kwbCKbpRGlRiiuFTy6d/Ppzs76HN64JuBdsdeoMXYilmmVcIbKx9j+vuqzoacIUjWFEs98b6rmXPOH83XnezJ6B6sKv3xJyVBCBpw4LclwTp8MY+lx3WZ6xQBC68qUQP5wLqj2LK0Lp/DryIXeRieRTkuobQOfyPmH8wVvXBP/zSoqsRjGGA1VyDKbiimUeOZ74+wkxFM11x81lD6HJ3Jbx7mZs/875WQrTlze4lLJIEJtmyjZzgbGaXTgGRG7OXtZPuqFRZ3mzjq0I2dd1UsYypPKMoJot2e0jcXu9+6iVuEW9lVtTO0h92eMEfEj3vneGLaqjG0dsZZKN9ERcUVl4FhMFYfefveRMburXHVNu5dV8nEYMR0wuUG6choplYsRv5KcWIhkkCFrbLVcJwc1xBWV/OEUDXki2C019PGo3VWeOpcM41kuKnSAa29DaUNGF10d5Ib2fZSEXNPxX6okQLnQewpf6ee4RCmQcqG0MjK9uVHXdAWmtNMF9Jlai9zsLH64+ayyE1G6q0pLreJbaYO1wE9XHiba/8tZW6TYPD9vDb9sP8D953byOx6VAUoX+cOZvXILHVY+wVFZ25EkvI0sGndzBmuoXKOGuAKTqAEtGxG7Z3KSEaxVWFxCcYmhRl56irNTA15uOhqVkHHvfwfAuh0H+Nel3cvNWtPVjc/i2iWtuX5AG/42qF1K771h5wEKi0u938uJysoNaogrAYkaheSMiN3jnKfns2rzXtaOT+07ez0RmX6u6UQ29Ahx6U5TjfoSvPcxeXXivpcCc3/Yypdryt5sFapzmGmGOh0D9z5/nw1A32NDbFWb4WSyswN0jrhCk+joMxmF19OmuekGXLV5r2t5xULZsyT3Pt0OvwB5dTAG7x95deD2dcm9cSXAUH4igDOpQ1BelgWVEzF1RFwZiNdOlBrPHLGLrunyUjOiIcyjxGOcwxqE29fRavT73q9r703t6L/CYhw/ZjTJmvaoSFWzvKEjYiUiSYmaTkKeC3/ewda9hUnIORJlT6ONWfnCYMrNb+btqJWXHkM5YNeBwxw4XJxuMdQQZyKrNu9h8+5DruWXSdGgyXTnDv/XAs59Zr77GYcgVa5pJXmUlsUfRfwd0/07l5cOQyYSyovQ9b5POWPCZymWJhg1xBnIoCfm0euhmQAcKirxLiOKl3ivNl7XdEK398drvdzJLrCT8esuh6CmJBHOzR7P42lDG5p2d33IM7N/jPm6lqPf561vfg15/ur/FPDbnvBelEz7XQyelQKlEdPGlX8FWkYczfz/hp2pazNCUeEN8Z5DRfyyPYoXu2cgi9ftpN1dH/HgB98llE/cwVr2/+TY4fiEWv7rbj/j67ukwkO/h2fHlXe86IA4+RwqKuWRj78POv7kjNUs+mVn2Gsnzlwd9vziddb1ocrkYbuMbd2XjmmPMnzrYds7P2LY0+56fybPXxsxTbq9AhWVCm+Iz/vHF/R/ZE66xYiL8/7xBQDTCqz9jl/58hcW/LQ93CWuUvb2JfeDteKp0B+v2MzQpz7n2c/WeI9lOci2bseBuOWLhVS6phOdXigqKeWNxRsyaprCDSbM+IHf//OL8IkSLL5b7BHzfe+tTCwjl/D8ht9t2hPztQ9/tIrrX1nkeG5qQYjXfjqQaV6CiGR4sa/whvjH3/al5b6FxSX8ttedeV5PGbrzreVc9NyXjmnWbN3Hio27Xblf2X2TubNW7Ez5ylqu89CHq7zHsrOchUuFwQmnl1Tcf+Oug0ycuTrsvb74aRvvLN3IpDk/ccu0pbyzdGPS5co0IhVfb5nM8MbajXr4jzk/8eHyzQx+ch4Fa3fEfH1526imvHQYKrwhThc3vLKYng/MDJvmjje/5Zg7PgCs7SQf//QHPlq+Oa77nfrYXIZM/DyuayORDNd0PAxsdwQAF5/YwnsshB12dFknC+MXNZ26mv/HVxbx+Kc/8NPW0FMvFz/3FaNe+4YdBw4DsG3f4VSJlzH8tHV/XPPLQWSIpXZDjJWb9nDPuyuc8w8wtu8tK+u87dxfBFhzr4eLS/nD81/x7QZ3BwDpYPmv6X0GNcRJYuaq34DwI6NXv1pHsR2INXPVb0ycuZo/OriN0tWpS2a7E8+I0WPjDhWVvcbxp63OHo9dB4ocj988dQlnPTkv5ns7yuPydp1hg78C7rF6y17vRiae9d4bdx2k5ej3HacvcrKtql6UpACfTOfxT3+ImCYzzGxovOXN55gxhpaj3+fZz36KOb/lvzq7tn/Yso+vfHYcu/HVb7yfV9rucIPh+817+fzHbdz+5rKY751pbNnj3iqVeKg0hjhda8WKo4x4LiwO8RL6NOIN1krGHHECebyxuCwKNpR+x7z5rePxN7/5Na65NSfCuqZdyN+3lz59sf97Z0+f8Jk3iOipWdZo76ufrcZz/EerCMSzHO7rn3fQ6e6PmfZ19POB6ca307Zu+wF2HYh9VF8Sw8qDLvd+wn8WrI35HsnGqbx5nuvBD4J/82iY/f1vjsdHPOs8BeaEr0EvMSbhVR7JIJJEWyJEziebCmOI/zZ9GX+bHrpntq8wPYb41yhD48ONqvYcKmZ/GPk37S67h6eB3bDzAPNWb41OyAgyJeN9xNGMIv85x7+X77SVZXYI4bbtT50L1vdZ3PReDH2qbKrhtjBl+92lG9m+r9Arx9L1u/htzyE/g9WsnvVe5SNqV2VfYTH3vruC7fsK6XzPxyxdv8tFqd3HV7/9HpnNqY/NTdqNjDHsPljE2Led3baZgK8+ou3oh+Lrn2OfJwYoNVYbE8gbi3+ltT3dlg7OmDCXV778xespi7Y+fr95T0oDYQOpMIZ4asF6phasj2tkuXP/YfYecnZl+rJp90G+jjHAIZq5yle/irxncMe7P/Z+Xr3F3yDtPlgm+23/W8a+wmL6/H02V/27IAZJnUisku89VMRDAUuvYnkN4t8DRnZOesoKMUkcaY1luCVt+wuL2R7DUpVEAlj2FRazw+40JGLEDxWX4tsm93xwJl3v+9T7vYrtmq6Zlw3A/sMl/OGFhew9VOwXhZ5pXPrCV/xjjv/87o79h1m/4wADH5vjPZbIHN8PW8qmNzwjzFCxB5nAyk1lz3o4RDk/7x/z6ezTZoQiVLBjJEpLDddPWRzy/O6DRfR9eJZfe3y4uDSmjYo+X73N+9kYw/ooVkP8sGUfd761nIGPzWXd9uhXT/x7wS8hA2FTQYUxxB7GvhWiJxumrTz+/k8jBlYB9H5oFhdMWgDA1r2FtBz9fsQo1P1RuMT/u2h9TE35OQG7RwXOh3ZyqIDxzMl6R8RxmognZ6zmX0GNvJXXwx99T8HaHfz59W+4++3lQe6sHuNm+H2f/+M2nAg1Ii4uCf+8ns7L0KfmMfQp/znjgY/Npbt9/+teDt2ZCbsUy1g6n73qt7Cuum73f0q3+z8NeT5acrKF//vv0piu8bjo3/92U8L3TxbzVm/j0U+C53ffXbbRL0gtnMfIw4HDxcwJ4YoFq4nwjDA9HRdfDhW5P79eWFzChE9/8It7iMSXa8oGA8+F6EQtXreLvVHoxBj4aPmmqPTnyxc/OddHD13u/YT1Ow5y2QsLvcf+On0pvR6aGfVg6Q8vfOX9/MpX6+j78Gw/740xhisnf81nP1iev8DO9/b9ZZ3pWJ8v1VSIlz74zndNLVjP38/P54kZPwQt5N+w8wC7DxZRWgrPzVvDhBFdefhja9R10K4I+wuL+fG3fXRpXjfk/Ywx/GCPSl9fuI6S0lJ+3naAW04/Lijt7f/7lo9v7hdW/pysrJh2yTlw2L8gXxjFfE6s87wlpcbrzo/VNb33UBE186oEeQOue7nAm9f0RRuYvqhs3jMnO4s7h3agqKSU4hLDtoAR6SXPf4UToXr0RaXh9XnJc1/x7b1nBgWsLPplB5t9Ajc+XrElbD6+HDxcwly7UXjjm1/ZureQ2/63jHvP7sjlJ7WkYO0OXlu4nkcvyPf+Hp553u8372W9g6svWrbsjm4EH2ou7ODhEqrlWqPlQ0UlZImQWyVz++kPf+S/uUc0O6p1vffTkCNID55gttzsLO9v4+H7Le6/5WvSnDU8OXM1WSL8+bRj/c6t3rKX0yd8xrs39qFzM+dXXq7YWFZ+F/y0neNb1PVbsnnJ8+HbhqfjjCZ/bt7Pft9v+5/z1MlGn2mzD+wO3/7CEvKqZMd0P89Sq+Ubd3vb5sLiUmat+o35P25j5q39uezFhX7XZIl425spX60jr0o2RSWlNKtXjWv7teabEFMyBw4Xk5ud5dgZSxYVwhAHFoLbpi9lWoF/cEupKXunZm4Vq5Id17gm/5pb1qNsOfp9GtbMZdu+w3wx+lQOFZXQulFNVm/Z6+e+anX7B7RpVAOwetA3T7VGIk6G+Pste1m6fhfnPDOfWbf2p3WjmkFpsrLglmmxjWZa2m/ieeqi42O6LhJTv15H1ZxsPl+9jfeWWRUnkh02xlBUYsitksW67Qfo98hs7j+nIy9/+YtfunBG7Rfb7XTxc1/y9drwOyX5sjFEA+w7Ij5cXEpOtvh1RvYWFvuNsr/4aRsntWnI7/+5IOp7++rFGEP7sR95v7+7dCPv2t4Sj4zn296Uced2YuWmPXQ6qrY3/ZlPJLbf7bCnwy9d83RKQ3lwiktLAatxbHfXRxxzRE1m3NIfsDqnY99ewV1D21O3em7UMq3fcYB1Ow7Q/eh6VM0J3fDuLyym490fM/evAzi6QY2o8/fllmlLKS41DO/RPGSaSEbYGCgq8YyI3fNNl5Rac8+BDfuiX3YwYYY12j9kjxI37z7EVz9v55yuR/Hpd1Z9efCD73jt2l4sdVgmNGtV2Qjf41r1fWfw/B/TN+8JUOJTDz267Xb/pzx3WQ8GtG3kjeaPhGdEO+bN5Vxy4tEcLi71eqIKi0u9bbsvN09bwoUnlJWHF+eXdR5WbNwTsi50GPsxA9sdwQsjT4hKNjeoEIY4kEAjDGVLPKBsFOLk8vKsszxp/Kyw9/C4xRb6BDu8veRXzul6VJAB+ut0y8ie+thcx5fXL10f//zWTa99EzkRVmPw295DfLdpD92Prh90/sDhYjqMdZ5TijQi/vcXa7nn3ZVMGNGFWnk5ANwVY7DLpyutRicWIwyhozs9HoZfdx3k5PGzuG1QW24YcIxfGt9R9sXPfRX021zzH2e3dI9xnzL3r6d4R7DGwKjXl4SUce4PW/mvz+jf12BnCh8u38xt05ex4PZTAf+NcF5buI7/Ld7Alj2HeOXqE1m1eQ8ffLuZW04/jjVb97FlTyG92zQIyrOvz1ajTuXegyf+of8jczipTQMmjOhK49pVY36G26YvY/EvOxl9VruYr/XgKTehlr9F4rc9hzgiQPY2dvDS2vFDWLV5D20b10JE/FzM/5zzE6e1P4KbXv2GjbsPMajTkTxut08L1mzno+WbvB27SMxb7ew2Xrst9Vv9FpcaNu0+SO+H/NvTa/5TwMiTWnLP2R3DXj971W80qpXHjO/KOhzPzP6RRz7+nv9c2TPstWu27uf9Zc7TLpGmFGeuCj2FkQzKvSGOdu7z8gC3RTJ4YsZqzul6FHe9tdzvuO9o2jOS9W2YDsYwPxQvz81bw3h7R6ozOzb2Hi8sttxE173svO0d+M8Rf756G22PrMV1LxeweN0uv3Q3T11KnWo5ccuYf0/k4JJoOVhUwn3vrvT2gh/+6PsgQxwJT+cgkG37DvPIx9+z95DVS7/25QI2hQlCcYr2zjQ8Udm+DebYt5dzTd/W3u+f/7iNb9bt5LIXFrK3sJjr+7fxRjCHM7RgdQR9pxH6PjyL9TsO8urVJ/ql++Kn7Zz44Ez+74zj+PeCXwKzicjrX6/n9TiXZj09+8e4XbUeej44008XvoGVnro/6Q/dGNSpidfj5MHXG9P2Tv/O2h9fCR0YFS0DHp2TcB6x8tveQoY95eytWbphF0vW7+KB91fyzz90p2HNvKA0V0z+OuiYZ8/xQFe08z1iG+TEMlfvJuXeEEe7/mt1Cra6/Hnbfm9li8S4FO9b+/rCsojjjbvKjMakOWu87rFQ3Pa/ZWzbX8g1fVv7BVA44RvBHSt7DrkXULHzQJGfKwqsedBwBM4JhuNbnyjdcEa4PPOfBb/wnwW/0LFpmQv92193e4OAdoZYz/v95r3kBcwvj3xpIY9e0MU70l2/w3LXXxxi7t/JW5Up7IywNK7l6PeZfMUJDGh7BKc7vGLPDaNangi1m9s363Z5X1saGJyZLnw3L7n9jW956LzOKbmvpGMT+B49epiCgkSX1ljM/WFrSka7SsXnr2e2dXzDj1LG7We189vr28PCMQM5opZlZKPtjJZH1o4fwssL1nqnXr4fN4hLnvuKAoc3QA3p3CSjI9KVyETy9MSCiCwyxvRwOpe5YZFRErimVlHiRY1wZHaF8Hj0fGBm2tx6qaSk1H/VeI9xMxyNMGT2sjAls3DFNS0ig4AnscIunzfGjHcj32gY935i7+pVFCV6Anc786XdXZkXhOY2RSWlfpG+e12cTlEqLwmPiEUkG3gGOAvoAFwkIh0SzVdRFCXTECHqJTeKEi1ulKiewI/GmDXGmMPA68A5LuSrKIqSURgDVTJ5/0ulXOKGIT4K8F0vsME+piiKUuFIZG9xRXEiZT4WEblWRApEpGDr1sTeCqQoipIOStOwykSp+LhhiH8FfPeVa2Yf88MY86wxpocxpkejRo1cuK2iKEpqMSa6V3gqSiy4YYi/Bo4VkVYikgtcCLzjQr6VkgkjuqRbBEVRQmBQQ6y4T8KG2BhTDNwIfAx8B0wzxmTuW7UVpRzTu3Xwns5K6ig1OkOsuI8rc8TGmA+MMccZY9oYYx5wI89M4rnLyjZDOe/44Di0xrXzmHfbKa7ca1h+U1fySSWntT8i6rQ/jDuLyVecwKr7BzFq4LFh07ZuGN+beNKJ7xuVnBjeo1lC+bcshzqpSBgT37u9wzHr1v5c1acVJzm8OCMe5v51AKe2i75OZipN6gS/+OOvZ7ZNgyTJp8ItiGvfJHxDGIqaeaH3Njm9g/WShIt6tuDv5+cHna9XPZfm9avz7o19HK+//5zwbxjxJZXvwIxEm0Y1+PGBsyKmmzCia8g33uQGPE9ulSwGtD2CqjnZDMtvEjbfdk1qRS3r3L8OiDptMjmydlXO7uLcmWrfpDYPn1829XDX0A7ed/4e3aB6VPl3Oqq29zV3qdoHV/HB4PqIOCc7i7uGduDKk1sB/h3/cDx9sfMrUGvmVeHFBF/hd+eQ9gld7wZ/Hngsa8cP8dtm8k+nRPfiFs9e557X1UZiQNvguKVFd54W1bVukDmtvku8c+PJQcdOaFmPhXcM9H6/8ITmrB0/xK8h+9LnvC8dbMMemN4Xz3tuOzerw9rxQxh3bie/85f2bsnfBvkbqn87vMLri9GnOuafLl64/ISoOga1quZwyYktIqa7P0AvxzYuM7QFDoX+uMbRG+J432PrJk9e2JXHLuhKrar+nbrARu26fq059oiaXNWnlffYK1f5v4UoFKWlhpevOpG144dwUc9gnXc+yvkF8ummYc1czutmeZNifYe203u+3cJp1BWOUmPIjvReUB+q5mTRsGZ073A+rUNj1o4f4u34h2Pmrf0Zmt+Uf1zSLWpZPKx5cHDENIFl2A26H13P8fhtg8pGuQvHlLXDRRHeHw1w9zDnvaMW3D6QteOHMPPWAVzXv7VjGl/O6nRk0LEGDm+DShbl3hC/9Sd/w+tUSZrWreb3QnOPofRtyAJHxBee0JyCO08LMuzRVMH+xwX3rq7t518Y2jauxctX9fTr1TatWw0oGx31O64Rqx84y1vYEnnFYDx43KAt6kc3WovEpb2ODjp2arsjOKNDY8dXoFXJEscGIdpebqDO3SbQTXZO16OoUz3HO2K65fTjuGNwO3rZ87qesnP74PZ8ekt/64uduFGt6Cp9aZjh2LOXdufdm5y9Mh5836SUSgruPJ3HLujCGzecxND8Jsy6tX/U157YKvj92U6MGdye41vUjSrtoI5Hsnb8EB6M0auwcfdBHvqwbFvddkcGdxZ/8jF0t57eluhajcj4dt7bNKoJwODO/l6ly3ofTf0a4Q1/VpgNSXq2tHQdrpxFQ4cmtVl1/yBv+9m+SW2vS/8ou53zcHHPFjx5YVe+GH2q98Uh4L+D2TFH1HS8z3ndnKd6fHVw+1n+HWEn9/bwHs2DjqWScm+Iuzav6/fdqbNaWFTqdQGCv/t34kXH8/Ff+gHwxg0neY+P/30+DWvmxeUqbl6/etBbO7KzhLXjh/DzQ4NZevcZHFmnKn2PbcTxLaxeYlOfnvkZdo/4yRFdycnO4jJ7RB04ovTF1yVfNSe8zNlR7Az0/bhB3s+zbu3P9QPaAPC/63sDlsu5fZPaMc0PO/HiyBN41nbFBfZKG9euysxb/BvsFfeeGdV7he87pyN3DI7OvXZZ7+AOgi9N61TlziHtefOGk/yM+8iTWoa9rn6NXK7t1yYqGaIdZIVbx+r7QvqjG1Rn2T1nBKX5fYiGKxncMdjfCyQidGtRz+tBipZmAR3B7CzxKyu52Vn894+9uaZfa16+6sSgVzA68fAF1hTTKW2dy2+ot+5c9/IiDhWVjdReufpEv6mnWnlV/OpXXoS6GA2eAL2cLAnbYXv0gi7cd06nsPp9+0/BHkNfcqpY10YzDd67dQOv4fZFxGpLq+Zkk+UjiyfLcb/rxB2D23n1VCOvCud0Pco7EFl052ncevpxnN+9rKx+MKov391X1iaFos8xDf3aLieyRPjoL339PDOxlkm3KfeG2JenLjoeEQlyA/do6ewSATi7S1Pa2r3abi1Cp/Pg9IPF8hOKiN/ItmmdqtwxuB3/vb6sEzD6rPYU3Hka9exeXXaWcP2ANgx0CMCYaBemOtXKRo7L7zkzrAyvXdPL+9kzt5qTXfYUNfOqkFcl2/u9SnYWfxvUjrXjh1DD9hy0aliDD//cl+cvjzAXFYNyWvuMdJ+/rAfnd28W5B6qkVeF87odRQNbN+PO7cSTF3YNyuuy3i2jvm+nCO7cL24fyNV9W3N8i3rcMbh9xI5OMjmpTcOQ5zyqXnr3GXz0535+janHBRvthhS+HSzf5z23a1Nm3dqfGbf080v/yc3+38HyEIQiGinuHtaBy3ofzVF1q/lN2/z04GD+z2dU07BmLifYBqFmXhWWjD2DP/YP3wEKVywDXagNa+Z5je2GnQd59IIu9LDT1Myrwh96HR1y2WGi+1IHTmF9MKqv34DBF1/DFQone+PrOr+4p9UpbXuk/wj0qj6t+Cpg+q5dk1rU9mnLjqpbjdsGteWr2wdSNSfbL61QZtxrV83h2n5t6G63t4EiNaiZx00Dj/UbBOVWyaJabjZBBBSkqjnZfm2XEyLQ7sjaDAsRy5EOKpQh9ij2un6tva6JJy/s6jcXl2mICNf2a+PnrsnOEkdXbQ2HgLKm3ga27JjTKH58CBdcNbvC+BbeK09uGVpeu9okYxFHlawyuU/r0BgRcRy9i5SNDLq1qBfU4C8Ze3rM9w41Avr8b8HR8OLTdPSM0m0K0Y96A/nLaWVBK20DXKHv3dTH+xt6qFMtx2q07J+oVtUq3pFJ9dyyMhQ4d+kb8f3PP3TnPdvN3axeda++rx9wDK0b1eSYI/zlaOkwR28MXNSzOX86JTqvQCBXnNyK+86xvEDhRoKBneNqudn0OSZ0hyUUt9pz0Z559nvPLhvpzlu9DbA8CoM6Hcmr1/Tii9GnUjUnGxFhYHvLi+WpFed2tdoiX8n+dEobx3odjsBprka18qIaMETL48O78MnNZV6nIflNWDt+iJ+LGKzAwsa1A44N6eAt05P+0J35o0/lhgHH+HlmnPBc8/LVPVlw+6kpCVAd4+Md8/1N5t12Cv/9Y++k3z8SFcIQX3iCv38/K0u8EbldmtWNye3w0V/6et2vTmTadu/dWtTjqj6tmDCia9h0F/ZswatXn0jtqlVo7xCN7Ptct5wReomAR5WBAys3zHKvCGtko31Jt288wGd/PYWXr+oZNC8VLc3qBc+Pe3UATLsusUocTYdGwpS6TkfV8c6fhSrmQlngi+8SmcB5t6v7OM+p52Zn8fWY0xw7At40VbJ48Heduf2sdjSuXWZsHjovn7+e6RxRnwhuriB6cWQPxp/XmZp2PIJnLtPj/haBE+2y+Tc7sCi3SpbXlQrB7YJnKkykzFt1xcmteP7y6CKi3WTt+CHeTlZgWTqucS2vh+7h3wevCAlHlu2pq5lXJaYOqYe8Ktk0qRNfvYyVa/q15mp7QOZbT5rXr+71pqQT90Pj0sCDv+scNH86ZkgHLujRPOZ1l+2OjD2YJZ3TC1lZwl1Do3vr5EnHNGRZCLd1tM/gGaAmY8/d3iHWUU76Q3fW7dgfV54tGlSnRYPqPHdZD/74yiLW7TgQnCjGR/GoKtr1pNEkC2dsI+Yf4gF8j/+2txCAg0Ul3mPhYgXikeZiO3L+xfk/h5ULIutkSEAQkttVzLdzfmo7azQ72ZZ7b2HwO4avPLkll/Y62i/WxAlPmfB9vpdG9mTBmm00rJnHhp0HHWSJWfyYCdT39+MGsXLjHu+0TGAnN9rq3a1FPZbfG34qzJtndFkmnUTqWrKoECPirCwJmovJrZIVce4vHtI8p580wkVS+uJpwFK5zd+gTkeGDHqK9vfo0LQ2DzusAY8Hrw5CnA+lm1jKzg0Dyp432utCNTAiwrzbTuGins39gvqys5JT/b3TF2HLiP/JwLX2fY6N5Fp2vwB6ltMNsAO4fO8gImGNcKgyIQgtGlRnxAlWJ8XtzUBixVOW8qpkewNFU4b97G42obFMkWVKR8CJCjEiTjcVwThH+wiedNGOiMuDamKd7y4bEUdIl8DD+wb0RcomlBy+x5vXr85D5/l3RMK9V9d3xBjrc8Tz3Ik0ktHcr92Rtdi46yB7DgWPdj2cfExDFo4ZGDQ/Gs3jBKbJtEY/E/bH9oiQrghljw4ysb2uECPiysQ1fVslZX1sVpSlMyvCaDBVuNmwxJyX1xJHmX8CLlqIYUQcao44xPFA13Q6G6jInRp/4SKlD+xjfPSXfkF7DjgRaIRjJUiuADnSXW/SidcQpuv+Gax9HRHHSLrXm40ZYs0HP/vZGlfzjbWxz5T3sqbj54j3luHmpsI9R6QyF3JEHEGesCPiCNdGQ7j7RzS8MdzHST2Rplrcn3P2/54h1cNLJomTria0bESceUNiHRG7QCZO/sdOjCPiwKjpTKrpMRKv6MnsYbvRVpgIc3KeEXGoe8X7m0YjemDWicydOtW/aDatiUQ8IgWWiSCXtUOeqTQM0d4qGWU7KXnGkWUmttY6Io6V925hTd4Lfj/mnu01gI3pkighPOU41nYr2gqQrDYmnW6mWAPWkt1JiZR9qIb+khOP5r1lm+jeoh4Fv+wMuMb5c0xyxfDgEZ8hxvTdJ7fiZ98lu/eA3BhfHY3m+QM7A5nmBs2EILEy13QmmsL0oiPiWHjvFih4gSyxCpbnrzb74aHILz3IRDwu5qiXL2V5jFBmNDRuVOpYH8XTaQkz8+t41EnH0dw63jniSHn3btOAteOH0CTEGut4jYkbkfWxGP+gtPfUQfCvowBHP9009DUuEfjMwZ2gNBvENBlB6/WRtgxpc03H1talEjXEsVDwouNhASjcnVJR3MJTOaIP1rL+J7opfCYRc9S0ratI8+SxNHqBKX2vjZRPpE5Rou1OMhrvWKc2Yp2DdZI41qeIpVxIYOcsQ+tHWgPy0pynN2o7CXIkihrimIjws793S2rEcBGPMYk1ajrdwVrpjJqOvUFPDUHu0YRGpBLT9W57SBLpxLhNIh2RTGn0Y/153K7ebkx1JIoGa1UWFk1OtwQxE68RypQOfyYvuQkk7mjruF3T6XHFhdoG1ZfA0Was5SnROdikuWiN37/g007BWsmRxJH01pc0d97TVB+iQQ2xm5iSyGkyDK9rOtqSEEUj6588A0t9ADEvI/a6IRNvWEI1Tn4jiIh5RLpLfK7tWBos3yyi0U+wazqCez2MMJkwwol1DXeqyaTgsXS3CRnyk/ihhthNJPzrtzIRby8x5uVLgS2pq2JFxNXbxbujh4tChDc00eYRcCDWEXuMm2aEzMeNpi5CFmFl63GV4+G9nS6PSYREli+F6lik2xym2gj6qiEZwVoxReanW/lhUEPsJlm5kdNkGLFWjkyZI/aQjt5tUGBOALGO9iLeL9KINkLznmjDF//ypcTuGy1B4g193DLGko0xUGyyoMdV7DplfNk1iURlO8rgnChjRsQZUF3T7Rr2qiBTfhQf1BC7SclBePrEdEsRE7EGa3lSVaSo6VjxzpPHGOkbSwPQetMHfJ47ijV5F3Ph/MGwbFoUcgWuZU2MeCJSIXJHBRLfEMYvvZNahz4Od++gVeGrHFP4ivU9BUR6jnQbxOg39Kh4pHuLzXDohh5us21VuiWIidIYC2dI13QIMrDzGURa54gdjp2d9Tl9V71ITtYhAGoVboJ3R1kn84cH5xFCjGgbnlg303Ci1Biy7ZTRpI84io8ij1iJtSzG8usGdj7iuTaZpNuwJm9jn9hTZ2KbJOmIZOvRo4cpKChI7U2XTWPD9NtpKtvIqtscBo51bNTCck+dKNPtdud+oXDKG8qOVa/PjgOHqcs+Sskim1LEVwaf60ur1mXPoWLqyj5KjUPaAArf/gvZi/9tpcvKhu4jYejjHCh4jR3v3ElT2YbB39VSipCFCZtvyOdySrtsGpvfvIMjSrdSUusocs64x//5Q1y76dUbOOL7KUFuICNZZLXqy/afFlFf9oWXedk0Nr1xB43NVkprN6NK2zPZsPDtsvseewY7l75HncNbOFitCTUG38fP2/eTM3scTbO2k1Wnmf/v8L/baco2pHp99h/YTw2s9wY7NhZ1msPNy4N0seXNO2jkqwtb1l1fTmHfB2P97xuop2PPYPuS96hXtIWi3LrsP1xMXfaRVbc5W5sMoHDlh8Fy2/fdPv1mr76oXh856+8Awb+Nw2+4fu5kqs8c472+RHLIMkVeA1xcpTo55zzpdz9fubc3HQAr3rSuF5Bq9eGsv/vfK+Cag7VakrN+PtmUQlY2YpfdUOz48hUOfHC38/MHULJkKpveuMNb//YVFlO9ZA+Hqjeh+ln3ea/7ceaL1Jt7V5neACQL6XFFmSy+clevz55DxdQs3cPhmk2peua9YZ/RqY5nVa/P7oNF1DJ7w/4mvvw2/2WyPx7tlVOq14eOv2P7wqn+xwJ1HsC2qTdSd+UUsqXUOmCgOEoZHAkodyanBruKpKytk1KkjkP9XzaNne/e6VcvQ+nRsc10ARFZZIzp4XiuUhjiZdOsEUWRz0u5c6rBsImxKTlaQ3zec+7czwmnZ8nKsVruksPhr82pBl0uhqWv+l8fKm2gvO/dgil4IXi00qo/Zv1CpDhCnqHyheh/o2ifP/DaULJHgycvCL53JLJyKAWySov884v2dwjkHp+NY8LpDCh9ZxRZxXGUExtDwMjUVw9v3QC+zwRWsGJWdvjfwZa79M0byDIB1weSlQ3nTrI+BzxnkGwA2blwzjNeQxTVNT2ucjbGy6YF6y9M2TXvjkJC/Za+v0m45+5xFbToFb6M+coQ6vePVLYitUXR/j7gr/NAwtW5eNrDZdOcy12k/CO1LU7nE5EzBGqIJ3SC3euDjzuNMMIRrSGu09yd+zkR6lmiRbKjX2YVKO+99d1ZouWkh2h/o1ie3/faRGWv09z6n4jufYnld/C95u4dZd/D6QzckzXRvBP9DWO5n+de0d4jUKeRZIyl7AZeB+HTSTbUbhpdXuGeMZqyFa4tirWNCZVXpDoXa3sYr1yRfstI+brRbhPeEFeOOeLdG2I7nsn3SzSPWBr/wHu5tU7a6Rmi1Vksz++bNlHZ3S4r8cgTeE2qy3W8ebv1G8aSPtrrQv0Oseg2mntFk8aUxJZXqLTRlK1w94lX57HK4dZ9IqWP9FtGyjeZ9cmmckRN12kW2/FMvl+iecSy1jnwXm6tk3Z6hmh1Fsvz+6ZNVPY6zdwtL/HI4xlVeb+H0Vkyy3aseSf6G8bzm0d7TajfIZY6HM29onkOyY4+r3D3jaZshbtPor9vtHK4dZ9I6SP9lpHyTVZd8qFyGOKBYy1fvy851bzBK1HTqn9q7xdt3lk51lxNJHKqWcFVgdeHShsob/eRzmlb9Y8uz1D5QvQ6i/b5A68NJXs0ePJyunckQskW7e8QKIMv4XSWSDmJJMPAsVZegUh25N/BI7fT9YFkZcem9+zcsntFe02ochFLHY50r0h685Ul2rzCyRipbEVqi6L9fcBf54GEq3PxtIexyBWNnqIpK2612xGoHIY4f7g14V6nOSDW/3gm4C9/J7Ixvme3e/dzwinvc/9hBUx4jlWrb/1BWa/UI8PQx/2vD5c2UF6fjRK86XtcZenFmycEhcRIVvh8Qz2XU9pont/pWo/sTqEjkm39rh49hJLZ6d49rgr/PZRsTr9DTo2y++fUYIepSamJURfhZHWSxVdeb1kI8Sy+eZ/7D399VasPv5sU+XfwyB14feBmOLk1rECtcHoPvL9v0JDTNa36B5fdUFHTsdThwLSBegynN7DKmkeWaPMKJ2PIOh5lWxTq942k80AC2wsP8baHTnLl1IjcfkX6Lf3Oh8kniVSOYK1ksGwazLyP0l0b2Gga0Oz8h1LygymVh5aj3wdg7fghaZZEUZRE0WCtZGD3cFt7Gst8bSwVRVGU2KkcrmlFURRFyVASMsQi8oiIrBKRZSLypojUdUkuRVEURakUJDoi/hToZIzJB34Abk9cJEVRFEWpPCRkiI0xnxhjiu2vXwLJX3ClKIqiKBUIN+eIrwQ+DHVSRK4VkQIRKdi6dauLt1UURVGU8kvEqGkRmQEc6XBqjDHmbTvNGKAYmBIqH2PMs8CzYC1fiktaRVEUJSaKiorYsGEDhw4dSrcolYKqVavSrFkzcnKi3HyEKAyxMea0cOdFZCQwFBho0rEoWVEURQnJhg0bqFWrFi1btkQy8WW8FQhjDNu3b2fDhg20atUq6usSjZoeBNwGnG2MOZBIXoqiKIr7HDp0iAYNGqgRTgEiQoMGDWL2PiQ6R/w0UAv4VESWiMikBPNTFEVRXEaNcOqIR9cJ7axljDkmkesVRVEUpbKjO2spiqIoGckTTzzBgQPuzHqOHDmS6dOnu5KX23mqIVYURVEykngMcUlJSZKkSR760gdFUZRKwr3vrmDlxj2u5tmhaW3uHtYxbJpHHnmEvLw8Ro0axc0338zSpUuZNWsWs2bN4oUXXqB27dp8/fXXHDx4kPPPP597772XiRMnsnHjRk455RQaNmzI7Nmz+eSTT7j77rspLCykTZs2vPTSS9SsWZOWLVsyYsQIPv30U2677TYuvPDCsPIsWrSIW265hX379tGwYUMmT57M7t27ueyyy1i4cCEAa9euZdiwYXz77beO6Zs0aeKaDnVErCiKoiSVvn37Mm/ePAAKCgrYt28fRUVFzJs3j379+vHAAw9QUFDAsmXLmDt3LsuWLWPUqFE0bdqU2bNnM3v2bLZt28a4ceOYMWMGixcvpkePHjz+eNn7pBs0aMDixYsjGuGioiJuuukmpk+fzqJFi7jyyisZM2YM7dq14/Dhw/z8888ATJ06lREjRoRM7yY6IlYURakkRBq5Jovu3buzaNEi9uzZQ15eHt26daOgoIB58+YxceJEpk2bxrPPPktxcTGbNm1i5cqV5Ofn++Xx5ZdfsnLlSk4++WQADh8+TO/evb3nR4wYEZUs33//PcuXL+f0008HLFe2Z3Q7fPhwpk6dyujRo5k6dSpTp04Nm94t1BAriqIoSSUnJ4dWrVoxefJkTjrpJPLz85k9ezY//vgj1apV49FHH+Xrr7+mXr16jBw50nEdrjGG008/nddee83xHjVq1IhKFmMMHTt2ZMGCBUHnRowYwQUXXMB5552HiHDsscfy7bffhkzvFuqaVhRFUZJO3759efTRR+nXrx99+/Zl0qRJHH/88ezZs4caNWpQp04dtmzZwocflr2yoFatWuzduxeAXr16MX/+fH788UcA9u/fzw8//BCzHG3btmXr1q1ew1pUVMSKFSsAaNOmDdnZ2dx///3eEXa49G6hhlhRFEVJOn379mXTpk307t2bxo0bU7VqVfr27UuXLl04/vjjadeuHRdffLHX9Qxw7bXXMmjQIE455RQaNWrE5MmTueiii8jPz6d3796sWrUqZjlyc3OZPn06f/vb3+jSpQtdu3bliy++8J4fMWIEr7zyCsOHD48qvRtIOraH7tGjhykoKEj5fZNBy9HvA7B2/JA0S6JUNLRsKW7w3Xff0b59+3SLUalw0rmILDLG9HBKryNiRVEURUkjGqylKIqiVBj+9Kc/MX/+fL9jf/7zn7niiivSJFFk1BAriqIoFYZnnnkm3SLEjLqmFUVRFCWNqCFWFEVRlDSihlhRFEVR0ogaYkVRFKVSMGDAADxLZwcPHsyuXbvSK5CNBmspiqIoZSybBjPvg90boE4zGDgW8oenWyrX+eCDD9ItghcdESuKoigWy6bBu6Ng93rAWP/fHWUdT5Bzzz2X7t2707FjR5599lkAatasyZgxY+jSpQu9evViy5YtgPUKwlNPPZX8/HwGDhzIunXrABg5ciTXX389vXr1onXr1syZM4crr7yS9u3bM3LkSO+9rr/+enr06EHHjh25++67HeVp2bIl27ZtA+CVV16hZ8+edO3aleuuu46SkhJKSkoYOXIknTp1onPnzkyYMCFhHYRCDbGiKIpiMfM+KDrof6zooHU8QV588UUWLVpEQUEBEydOZPv27ezfv59evXqxdOlS+vXrx3PPPQfA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\n", 135 | "text/plain": [ 136 | "
" 137 | ] 138 | }, 139 | "metadata": { 140 | "needs_background": "light" 141 | }, 142 | "output_type": "display_data" 143 | } 144 | ], 145 | "source": [ 146 | "detected = data.to_frame()\n", 147 | "detected[\"anomalies\"] = data[range_anomalies.values]\n", 148 | "detected.plot(style=['-', 'o'], figsize=(8,3), title=f\"Anomalies detected: {range_anomalies.sum()}\")" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": 9, 154 | "metadata": {}, 155 | "outputs": [], 156 | "source": [ 157 | "data_clean = data.copy()\n", 158 | "data_clean[range_anomalies.values] = np.nan" 159 | ] 160 | }, 161 | { 162 | "cell_type": "markdown", 163 | "metadata": {}, 164 | "source": [ 165 | "# Detect anomalies outside automatically set range" 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": 10, 171 | "metadata": {}, 172 | "outputs": [], 173 | "source": [ 174 | "N = 1000\n", 175 | "normal_data, test_data = data[:N], data[N:]" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": 11, 181 | "metadata": {}, 182 | "outputs": [], 183 | "source": [ 184 | "anomaly_detector = CombinedDetector([RangeDetector(), DiffDetector()])\n", 185 | "anomaly_detector.fit(normal_data)\n", 186 | "detected_anomalies = anomaly_detector.detect(test_data)" 187 | ] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "execution_count": 12, 192 | "metadata": {}, 193 | "outputs": [ 194 | { 195 | "data": { 196 | "text/plain": [ 197 | "2015-02-25 12:30:00 False\n", 198 | "2015-02-25 12:40:00 False\n", 199 | "2015-02-25 12:50:00 False\n", 200 | "2015-02-25 13:00:00 False\n", 201 | "2015-02-25 13:20:00 False\n", 202 | "dtype: bool" 203 | ] 204 | }, 205 | "execution_count": 12, 206 | "metadata": {}, 207 | "output_type": "execute_result" 208 | } 209 | ], 210 | "source": [ 211 | "detected_anomalies.head()" 212 | ] 213 | }, 214 | { 215 | "cell_type": "markdown", 216 | "metadata": {}, 217 | "source": [ 218 | "# Detect peaks" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": 13, 224 | "metadata": {}, 225 | "outputs": [], 226 | "source": [ 227 | "detector = RollingStandardDeviationDetector(10, 0.1)\n", 228 | "std_anomalies = detector.detect(data)" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": 14, 234 | "metadata": {}, 235 | "outputs": [ 236 | { 237 | "data": { 238 | "text/plain": [ 239 | "" 240 | ] 241 | }, 242 | "execution_count": 14, 243 | "metadata": {}, 244 | "output_type": "execute_result" 245 | }, 246 | { 247 | "data": { 248 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAeIAAAC9CAYAAABvRn41AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAABQAUlEQVR4nO2dd5gURdrAfzW7s0vOiEQJBjIIiKgkASOgngEUFTGcnqiYTgyogKJiAsVwHCpG8IPjDJiPJAJiWJAsAgLCkuOSd3d26vujpyd2z3TP9OzM7tbvefbZ3Q7Vb79dVW/VW29VCSklCoVCoVAoUoMr1QIoFAqFQlGWUYZYoVAoFIoUogyxQqFQKBQpRBlihUKhUChSiDLECoVCoVCkEGWIFQqFQqFIIZmpeGitWrVk48aNU/FohUKhUCiKnSVLluyVUtY2OpcSQ9y4cWNycnJS8WiFQqFQKIodIcRfZueUa1qhUCgUihSiDLFCoVAoYrNiOoxvDaOqab9XTE+1RKWGlLimFQqFQlGCWDEdvhgGhce1//O2av8DtB2QOrlKCapHrFAoFIrozHkqYIR1Co9rxxUJowyxQqFQKKKTl2vvuMIWyhArFAqFIjpVG9g7rrCFMsQKhUKhiE7vJ8FdPvSYu7x2XJEwyhArFAqFIjptB0D/CezLPAmvFBwrXw/6T1CBWg6hDLFCka6o6SKKdKLtAEac8n80zZ/C95fOVUbYQdT0JYUiHVHTRRRpjEi1AKUM1SNWKNIRNV1EkYZIZKpFKJUoQ6xQpCNquogijRGqS+woyhArFOmImi6iSEOk6hAnBWWIFSWPshDEpKaLKBRlBmWIFSULPYgpbysgA0FMpc0Y+6aL5Hpr4ZUCqjZU00UUyaUsNHDTFGWIFSWLshTE1HYAXQsm0DR/Cty/ShlhRfIwauB+cjuMqmpilEvIIHEJaVyo6UuKkkU6BzGtmK41CPJytbHc3k8q46koGRg1cPUI6aCpc5KmxStXIpSgKYCqR6woWaRrEFNZcZkrSiexGrJhXqcSETVdgrxnyhArShbpGsSUaKEvIS40RSnFSkM2L7dkRU2ns/csjIQNsRCioRBinhBijRBitRDiXicEU5QSnDYw6RrElEihV71pRarp/STejPLRrwky1iWhQ5y23jMDnOgRe4AHpZQtgS7AXUKIlg6kqyjpJMvApGMQUyKFvgS50MocZcVT0XYAud3Gag1cIMLU+r1OJahL3PtJCl3lQo+lg/fMgIQNsZRyh5Ryqe/vw8DvQP1E01WUAsqSgUnEZW7am97KZa6FicumiI8y5qk4eOoVdC2YwGU1v4QrJ0X1OomSMEjcdgCzTx1BrrcWkjTynhngaNS0EKIxcCbws8G524HbARo1auTkYxXpSqJjNOkUhRxLFt/fez4bQc2iPRRUqku5i0Zbk7dqA19lH8lY99tQCNA38XdQ2CNaQzINK/NEEcG94LYD6Dq1IgCb7y/GvOdwmV9f5xLuXNGMe3qdyoMXnuGgoM7iWLCWEKIS8F/gPinlofDzUspJUspOUspOtWvXduqximThhEvO1C0rY6eZTr0Rq7K0HcDddT6gaf4UfrtyofUKxKg37aOCKGB4ZunsgaU9UTwVZcJdXdykU5kvZhwxxEIIN5oRniKl/MSJNBUpxKkCEcXAxEwzndzayZbFF4BmNvpWT+xz5jkK66yYDsKsehSl2lhEi4xOatR0EstZukd7OxE1LYB3gN+llOMSF0mRcpwqEEERzoYFIVqa6TT1IFrPyKRXZHu7uLYD2CWMPUXbZU17aSkSQ2+IyiKDk4KIgKXC4/DpP0p8D9nOsG/EpU540JJQ5kvASDbgTI/4POBGoJcQYpnv51IH0lWkCicLhC/C2dQsmaWZTlMPoj4ztFeUSAzL21k3ckxmhRw7JrN4wVP6xiOTSqJGwXCVKZAiA9OoYVlEWvaQHY76Nnx7pzxo6VTmixknoqYXSimFlLKtlLK97+drJ4RTpIgkFIjtspa9NNNl4Y4V06HgaOzrHHChzc3qwSOFt4VEqz5SeBszvV0TSrdMYdcoGBkqs8ah9GqRt7FIl5kBSRxzDWlwOuVBS5cynwLUylqKSJJQIF7wDLCXZjos3KFXZMf3+w9FHWsKrsDjHJOa6e0aMkdaGWGb2DEKZoaqfHXDpIsq148e9xBMOqzeFKeBjD5GbHDSKQ9aEsu87aGiYkYZ4tKMmVsqlrvKVyDysk7GKwWHsk9OuEDM9HaF/hPYJrVCJqs2iJ1mqhfuMKjIhABEhvH1VRuETgGxSUkZz0pr7BgFM0MFEcb2mMwi79xHI41FlLyQcpIYZxHSI3bSg+ZwmS8J051BGeLSi1lr/8sHLE/Fmdjhc5rmT+HDLl85YwTbDqBH4Ws0zZ9C4T0r038upsncXmRRmXWhpT12jIKZQTp+IMTY7qQ2jxTexvHmV2rng43F3yYWX16wO96bhCEmw35l7yfxZJSMFazSFWWISytmrf0l71l2V+leKCdblXpa6e4qAsx7OyID+k9gp6iNVwo8lSN79yXg7UondoZVohmqIGN7VblJ5kMExTWEEs94b3GNubYdwK9tRqX1ClalfvqSIk0xDTgxmpJhfH0yjKXuuk33ggGY60oWQdsBXFN+Ek3zp7BtyC/+SieRRkusZQM9RV4W/6nmFEfFjmG0aahM82xxDKHEM94bZyMhnqK5tX4/uhZMYHir+emz/jslZClOHF7iUpFGmC2bKDKMDYxR70DvETs5elkyyoVG1YbGOvRFzjqqFwtc+MoPbNxzlJevacdVHdNgDDJdsbo8o89Y5H35BJXzd3GkXB2q9H06bYxICPGO99pYqtLePOKSVJDTH9UjLq30fhKZadDa7zjEtrvKUde0c0klH4Me0zGZFaEro55SMnr8G/do06ge/M9y5xMvqyQjFiIZpMkc2xLhySqBKENcWmk7gMK+r0S6pfqNs+yu0stcMoxniSjQYa69XG8tHim8LcINHfwqCbmmo51cMZ2FWcPYmD2IhVlptGBEKaBENA6LcUs/wylK4ZQIpQVI9+pGuaZLMd7W19B1WmWyMlysu/+SwAmL7iqvV8u+ZTZYC0J0pTPB97u46qLLXAvhs3/TwKUNKTQQe+Gzf/jlU9jj7QUb+WvfMZ6+onXIcUsGKFW0HcC8NbtoueYV6rv2IZKwG5kVd3Maa6hEowxxKSZRAxroETtncpIRrJXvKcJTJKmYnZrsbFSBO9nQeNY9Gbxh4/reIvjyPmWI42DMV78DsGX/Mf59Y8cSM9d0fZ1LuH1ZU+7s2YyHL25erM/OPXCMfI/X/38JUVmJQRniMkCiRiE5PWLnuPz1RazdeZjNY4t3z149IjPENZ3Igh4mt1bkhPEJK0tvKkyZv24PP20MRKGbNQ7TzVCnouPe9fl5AHQ7zWSp2jQnnZ0doMaISzWJ9j6TkXn1Os1JN+DanYcdS8sOgXdJyeMVDiApORHA6dQgKCnTgkqImMoQlwXitRNeqY8RO+iaLiklwwpRXiUe41xSDEKpQhr+mdYkK76iNBXNkoYyxIqYJCVqOglp/rJpP3sO5ych5VgE3kZVZiULiSwx38zfUCspLYYSwMFjBRwr8KRaDGWI05G1Ow+xM89kXDAO0ikaNJnu3AH/XswVbyxyPmETlGu65OMNxB/F/I6p/s4lpcGQjph5Edo/NYsLx/9QzNJEogxxGnLxKwvo8twcAE4UFvmnEcVLvHdLv2s6oceH4rdeziQX3sjYdjByQ/dkEc3NHs/rqYrWnOZPfMMb8zbYvq/xI1/x2W/bTM/f9kEOuw9F96Kk23eR6DMFvDGvjSv9UjSNOOpwz5cPsCH7BhYc+xuMrqFtiJMiSr0hPnSikL/2lczo0qVbDtD8iW959uvfE0on7mAt3+/k2OH4hFq1LS/E+AZPqdDp/sK8uNKOF9UhTj4nCr28+N0fEcdfnb2eJX8diHrvhDnro55fukW73yxPFvjy2J4jqRj2CBBcDs94/Fv6v+6s9+e9RZujnl+adSsfbL2QTdmD6Db1VHiukaPPL1a+fABy3iFTeLWGliyCnHdSZoxLvSG+8s0f6fHi96kWIy6ufPNHAKbnaOsdf/TTX8W66H9g9yXng7XiaRx8t3on/V5byKQfNvqPuQxk27L/WNzy2aEkuaYLi7x8sjQ3rYYpnGD87HVc9a8fo1+UYPbd5esxP/XlmsQScgj9G/6+45Dte1/4di13frTE8Ny0HJNtP9GMcHVxHIHmIRAA+XklxxiHZ/sl7xpfZ3Y8yZR6Q7xh95GUPDffU8Tuw86M8+p56PHPVnHdWz8ZXrNxzxFWb89z5HmB5yZzZS37TPl5CwDPfbPWfyzDZSxccRicaHopjudLtN5etGf9+OdeZi7fzsTv/+SB6cuZuXx70uVKN2JlX3+eTPM2ihPl8M3v/+SbVTu59NUF5Gzeb/m+6uK48fPzna1znMZUZ9LErW92PMmUekOcKoZ+tJTOz8yJes1jn67k1Me+BrTlJMfNWse3q3YyOnMyG7JvYFP2IDZk38BjvB3zeb1enk/fCQsdkT2cZLim46F385MAGHR2oBVuYocNXdbJQoZETRfv6Nm4Wev4c4/50Mugt35m2Me/sf9YAQB7jxQUl2hpw597jsY1vhxBmlhqJ8RYs+MQo75YbZx+WDP5yxXmjTcJrMxNb2NcElCGOEnMWbsbiN4zmvrzFjy+QKw5a3czYc56Tpnei8EZs/1jF5nCy7X8LyVjF8msd+LpMeo27kRhYLnHP/cYezwOHis0PH7/tGVc8uoC2882lMfh5TojjLhvowdTfM/V53tvP3icxo98ZTh84c7QinphkgJ80p1xs9bFvCY9zKw5/vwWdExKSeNHvmLSD3/aTm/VNmPX9rpdR/g5aMWxu6f+Zp6IhEc/XWH72YpQyowhTtVcMY/FiOd8TxGjMyfTXGyLcKcISMnYhT9YKxljxAmk8cnSQBSsmX5HfLoy8uCo6oxb04Ov9/eDUVVhVPUEpIjhmk4oZW2jB/nJ7TRw7Y3pknxtrtbb+3mTVnmO/XZtxDX6dLhfN+2n9cjvmP6r+XhguiGDdp7yvNyKozlTbadRZGPmQbvR/+ODxZttPyPZGOUD/b2e/Trym1th3h+7DY8PnGQ8BGZEsEEvkjLhWR7JIP0kCqXUGOKHZ6zg4RnmLbMj+akxxNsOWJtOIyVcnzHXtNKVUcYuduQFnqFXsLkHjrFg/R7rgprIBMnZj9hKL/Jf34e28o2WsswwEW7v0TAX7KjqgOZlCNziTdgYQ+i7OKWqV9xvIixWH18s386+I/l+OZZvPcjuQyc4eCyggwbVtX2VT6pSjiP5HkZ/sZp9R/JpM+o7lm896JDUSWDFdPj8Lhq49uISkHk4F/eX9yRnG0gpkVKSd7yQJz83dtumA8H5zWpD34xfN1kbJ17gbRVRZqXveDCfLN1GU99wWyq4cPx8PvrpL7+nzLQ8lq9h73iSKTWGeFrOVqblbCXfUxT74jAOHC3g8AljV2YwO/KO86uNAAewNlY51ReElEGUa4MKwfpdoQYp73hA9uH/XcGRfA9dn5/Hre/n2JI16kPj4PCJQp4Lm3plZxvE58N6drqegnGZDBJHzrE0023k8aP5HvbZmKqSyJKDR/I97Pc1GvQ3+SbrIdsG/YTHS3Cd3PnZORwdexqbsgexKXsQD/10NouyhlIpOwOAowVF3PDOLxw+4QmJQk83Dn32IKIotFGVhYeir4fT++Xv/cdWbYt/nHLdrsDwht7DNIs9SAfW7Ai8a4HJUMOVby6izcjvQg+G7Wl9mWuhabBjOIMLR/iNsZQ+I1zUisGFIwyvzzteSLcX5obUxwUer62Fihau3+v/W0rJVguzIdbtOsLjn62i98vz2bIvyvWXPA8iI+SQRwrteAooNYZY58nPTFqyUerKM5+eFTOwCuCc5+ZyzcTFABz79D48I6sjR1WNOhn8qAWX+H+WbEUCRRY/x+Vhq0eFj4e2Di+AxDcm6+8Rx9nPe3X2ev4dUclrab3w7R/kbN7Pvf/3GyM/XxXhzuo0ZnbI/4s27MUIsx6xp8ja+0qg32uhY8a9X55PR9/z7/jQvDETdSqW1HQ+b+3uqK66Dk/PosPTs0KOGQ1PGJEftHmaO0Pwz/8s9/+/KGso9cRBvwdAAPXEQe5e2t9/jT795auVO2I/LEVULjIex3SdOBASpHbUgsfrWIGH701csaDlBb2HmZkRWRZPFDo/vp7vKWL8rHUhcQ+x+GljoDOw/81L/I0tRlWF9y8DYOmWgxwO0om2p/U//J6FBq69jHNP5Izd3/Ltqh2G+vvA/Yw/7U3Zg+jmWk0RLj4o6sOzZy02NcKgufe37j/O4Hd+8R97aMZyujw3x3Jn6YZ3fvb//dHPW+j2wrwQ742Uklve+5Uf1mmev/DG976jgca0Yf4Qod/Yav2bDErFNojB413Tcrby/NVteWX2uoiJ/LkHjpF3vBCvF95asJHxA9vzwndar+u4ryAczfewYfcR2jWsZvo8+eUDlF/+bqCylEXInHc0E9NvXMi1j/53Jd/d3z2q/G6XC0+RlylFWqBWrEr4WEFoRr7WwniO3XHeIq/0u/PtuqYPnyikUnZmhDfgjg9z/GnNWJLLjCW5/nPuDBeP92tJYZEXT5Fkb1iP9Pq3A4VSNzIAvA6LsqpxXsGbIdcXei1WmjJ0jGvJX/vZeSjQav9u9S5r6QDHC4qY76sUPvltG3sO5zP8vysYfVkrbjq3MTmb9/PxL1t56Zq2/u+hLxZR8PwZfHV8J2Rbe1aRhOGFt/v/35UXqi/dCAcjBFQpMm7QHC8oonyW1kM4UViESwiyMtO4nS7RjA9aBbp5/jXQdFLUW9qPnmXag9TRg9myMlz+b6Pzxy7nd/ma+P1GXp2zHpcQ3NvntJBz63cd5oLxP/DF3V1p06BqxL0fuJ/hlEOrQ76z3DSfI5MuBe4C4Pq3tbrBaE/rTOGl5/pnab3qVMO0u7lWR+ShTLwMzpjNB4sBbvEfH/5f42HB7UHDZl/7GnxH84vIzswwvN4MfarVqu15/ro53+Nl7trdLNqwlzkP9mDw5F9C7nEJ4Zd/ys9byM7MoLDIS4Pq5bl96VMIb2gHJlsUwZynONb8SrIyXIaNsWRRKgxxeCYYPmM503NyQ47V+Hc7Mo7spL7v/4dlNf71/Wz+PT/QY2v8yFfUqpTF3iMF/PhIL04UFtG0diXW7zoc4r6Sv74T4boSoK3MEmaI/9h1mOVbD3L5G4uY+2APmtauFCG/ywUPTF8O3MKNGbMN+59FwMKsYdQTe9kuazHssYXM9HbltevOBGBd1iDcQTcWSvjY24frM+ZqLu/RGdBxSIR84Uz7dQvl3BksXL+XL1fsCLxbFKSUFBZJsjJdbNl3jO4vzuPpy1vx4U9/hVwXzaj95XM7DXrrJ37dbL5SUnBPT6eeOMiirKEhxji4R1zg8eLGhQhzQ0s0PYE23/bcZrW46l+LY7xtgGC9SClp8eS3/v+/WL6dL3xzdrf7lt282udNGXNFa9bsOETr+lX87+Q+5nunGMqWErbJWrzgGcBMb1f/8f6vW5y6ZtI593i9gFY5Nn/iW049qRKzH+gBaI3TJz9fzRP9WlCtQpa15wBb9x9jy/5jdDylOuXc5hXv0XwPrUZ+x/yHenJKzYqW0g7+/pl4abZ5GkeenUelx8xX0fo840GaZwYC/dbK+lxS8KL/fymhsEjvETvnmy7yamPP4RX7kr/2M362Fs19wtdL3Jl3gp837ePy9vWZ9btWXp79+nc+vr0Ly8OmCXVzrY7ILgKotH0RuiFetEEL4DPb0zr4+DdZD9FcBPRj1gAXQotnGem5xfiCIIqCyqGu2w5Pz+KtwZ3oeUZtfzR/LPQe7YhPV3H92adQ4PH6PVH5Hq9/v+Rg7p++jGvPauj/f/KiTf6/by+XG3E9AHlbKf9MzUA5rFQX/hlfIJwdSoUhDifcCC/KGkrG0bDKm4P87fs+vERoT0qfZ3nu2Lmm6ZvOEfedCzdAD83QXIa9Xp5vuHn98q1aAbvMtdA07Qw0dxJAA7GXl9yToBDu+ThghIPfzw2hvWtfrz33wDGqXP1aRPrHCjy0fDLSpQ2xe8Tv/7iZUV+sYfzAdlTOdgPwRFCwy2WuhQzPnO5vRIQbEYBZa7RKJ5oRBvOeXj0OhhzT3VTbDh7nvLFzGX7xrwz9/qyQoLdCCacXaBG4g976OeLb/P0DY7d0pzGzmP/Q+Ww9oDUepIRh/7fMVOb56/bwn6Def7DBNnsnI6SMNB7BBOvZLt+s2snwGStY/GgvIHQhnI9/2cJ/l+ay69AJPrrtbNbuPMTXK3fywAWns3HPEXYdyuecZjUj0uwWtNSoUb7XaeUbSunx4vec26wm4we2p06VcrbkFwIq5u9m3Qu9OOnubyPO60YmWM/N2cY3WQ+F6FPPN2bT32Kx+9AJTgqTvZkveGnz2L6s3XmIM+pURjxVmw6ykE0+D0j+jy6WtPiTe6b+xva8E1zc+mTG/U8z0os37uPbVTv8DbuYxBGyYKSfaESNZwnC45XsyDvOOc+F1qd//yCHIec2ZtRlrUzu1Ji3dje1K2cz+/fAkMIb8zbw4nd/8MEtnaPeu3HPUW6e25G/Zwc1Bnxlfpu3pr8+DSdEB0d2wEvNk26MS7whtjL2WU8cjGw5GlTeVvgm66GY1zzx2aqQ/4N7040f+QoIrZh0t/jwzOlRW6HBZAkPo9wfMDO/a4QRNroetEZCgw0f848Zt/mP5Xs0N9EdHxove6fdF0hs4fq9nHFyZe74MIelWw6GXHf/tOVULa8Z4hD3cZA8DcReXnb/CwqJMMZtRxk3BOLheGERT32xxt8KfuHbPxg69gBNfPq3gt44CGfvkQJe/O4PDp/QWum3f5jDjrwThl6J0wumGkZ7x0u4EQ43vvEGGQ33zTgIrjCf/HwVf+/W1P//wg17+W3LAQa/8wuH8z3c2aMZvV6eD0Q3tKD1CoMDg7q9MJet+48z9bazQ6778c99nP3sHP554em8v/gvfglPKApCwGlHl9DkqVkR5wynBQrNGOu8Pm8Drye48EfnZ+eE6CI4sFIv+xvK3UgmRVqp8smUjZdW7zRje8FHgLaWNASWluQ/sCkbDsjydCh4JyEZjbBjhMH6eOruw/n0f83YW7M89yDLth7kma/W8K8bOlKrUuS4zM3v/RpxTF9zPNgVvSrrJiqKQOPpqHSTRSGZMrKDsi5rEP/0DGWs+20qiEAgoJQmnY4jyY+hKPGGeFeMnVOcJGarUQYKWyzGGKxba7cnU534lu/cfjDgjpr4/Ua/e8yM4f9dwd6j+fy9W9OQAAoj8o4XGrqPg3ELyTOZ7zCzINQQHzrh3BSzA8cKQ1xRoI2DRuude9+7jE3Z8/3XL/BGRoWOzpysufuXenk828WUol6MzLvF1CuxLmuQv9ftNJe5FjLO/SaZNo1v8BBH8PuHuybXLqlPt8Uv0qpeFf+xldvy/EFAB44Zr9L1x87DZIeNLw959xdeuqadv6e7db/mrh/09s8BneKlCJ9O/+dze1ocMy9ODoRPjSO04SlHwolyJ1H+0fVcYLDFXoYsinCrCaEZY4gcZgrOU9U5ztKsW5NijK0iJayXdS1fb7aa229bDvq3LQ0PzrSDboSD9VQRzSgbNb7c+DoBhfjqgn1slzWpH4cnySlKvCFORgCFGbFajfk21Pn2wk0Rx7bLWjQohsywMmiqRywjrPPCt3/wwreRu9+Ec5lroSVXayVR/DvZPDzqcca5J5IptAqvgdAiRymEqzPmIzaHBqd0c63mA/czfmM8OnNyiLs/U2iBK4MztErErNAni+cz/23bCEPoEMcr7jehEO7M/Nzcdbs90As/HhQoGDx8s/vwCU6qrBnZi16JND4L1u/l7GcjZyZE6NQXDNTftYgOBe+wX1aipkjNevEQ2Tjh9eZ82XGa/998TxGHxpwakefLndjNvtFNgchhoGgYNeiCEUIzxqAF7Bl9//AJA6MzJ5s+L55IYd2TMDpzsqVx4mQTboRBkzGWs3Smt2tIZ0AP/ksFJd4Qh8+pTSWeBNX5gmcAr7rftB2lXCi1Cr84ljk2c79CoIfmlBxGPaW1sj7NCTUY+thpLJ51T/YbYZ1M4eVVtxYnYDR80c0VGOu+0SCi3cq7bswaFHJdsM6sYvSO5YT9OfPh8roEvJL5ZthCJ4Frg123AAePG4+ddn5mDmufvjhqUFY4l7kWGs4S0I3N2qwbGO75R1xlwg5mBsXIAyb3rqXvgr/xBGMArSe3Qu43fIcaXvs7pUUzwuE84BnKK+43Q4YjvFI7rhPe0AlGSphS1Mu2jKDJeEPGnJiGeEPWIIzi3oy8TWUZRwyxEOJi4FW0mKK3pZRjnUjXCmO+ir1Xrx5EZXTcScwiE60y09uVV8OCx2KxMGsY//QM5aXMN/29r2RVWtHcr3uoZjnoSCe4t6n/H2z4gIieUr40bsHXIfZCK2bfJ1brWTcI8ahVl99snGp4ZvQVonS5ogVpJUqsb7Ype1DAZfy9VvGGxwBsl9Vo/sSbLMoayqbs0OPhU8tAM8Ivu/8VteeXjZc7Mz+3+zq2iBYBbLbcbPVjgZkWh094bLvPzcYiTccoTTByr4YHQho1HvVnfVDUx//ehVKQJezViK4YNahuhI2eH+5tskN4Az0a4TqVMjBTIhg99sfwG9RqbltGuyRsiIUQGcAbwAVALvCrEGKmlDI9Nu8EPizqE9EqlFI7XpIRIuBevK9wKDO9XaO6V6SMXJIueMw0OP8VSSgkI6LXZeZ+rYc9I6z3NvXCaDZvMfyebN8yleHHdXddvMQyCIn0ysx09or7X7hiVH56IyFZRjgW4Q2hqzIWUiBFxLZ49ThoWPHWI3JqGcAzme/gtvDuzdlm6AUxItxrEOFWNsEoAjiaOxcSc2NaDci0Qrh7VcfKu/d2LePG7EFsl7X42NubG8Rsw95rNKIZUzMjDJHeJp1oHjeInL6Yide0ES0BjwwdHjLyRhl6PvTFjGo3h7ujx8U4gRMzljsDG6SUG6WUBcD/AZc7kK5jjPTcwgdFffBIF1KCR7pCWoPpRDy9dJeAl9wTLV0bXGj0Xom+2o4I+skQUN5VFHIsGvFUIsGFMZYRTiVW3j8eYhnhdEMIqCROGO5Nq+cZw6llQT1nHTsxApcUvMhaWd+/vKIRUmoRxXqDJbhy1X/M7g3vVUVz5+pYLRfhxJp1URjlHcG4MW30jOB3NyN4la1rMn7go6I+5HprWd5NzMyYxkuwx03/cQvtOGhG32z6YsQ62L6O1ukFU2mSH/gxGhIyi6gHisUIgzOu6fpA8FYuucDZJtemjJGeW5JuePfLyMU67BJvfe/2terN3PBGROuVGBXgdDWUVojVwwH7rsFUYzrkEmWzDiNXnd13TlRHVr5FOJcUvMjCrGGGcz/D3axgXrkavX/4OGlMIxz+v8m1J2ToeLmVubqnF0zVDJLJeStjq1amIoWfryAK6O1aRteCCXH19o16snYxm4ap6yJaY10/ruf98PxgZS2DVFJswVpCiNuB2wEaNWoU4+qSh5Qw2jM41WJwjHJUMhgLNQr2SUXkcqqwsnQoxG+M7Ro/JzAbcvnAdzyWrGAur9k5J7D6LcIxm94nESGVrt6DMiP4/bfLaiGR77F6m1aREh723BFyzOpc3WRNd4tFPWE/uAzMY0dSgd7YCjfCwcNA2nCe8VoGqcIJ1/Q2oGHQ/w18x0KQUk6SUnaSUnaqXbu2A49NP0Zlvuv/+zLXwoidThLBauVdwSQgSZK6cUanMXJDrZX1E9J3osYxvEUenK7X527Uf+z0FqJFhMc75NIkfyr3Fg6Nek06IKVmKHW2y1qG122XgVW9rEz/CXbZhmyMYcHVaiebvOJ+03JelBLTQESnMcvrwXq0g9UFhcJlOCDLx/U8u7zk/nfEMJBLSF5y/7tYnm8FJ3rEvwKnCSGaoBnga4HUTchKEXrA0KKsoTzvGRQS3NNA7NWmyFhogRVkVSW7MC/i+BGZTZ6sTH0RfaN4s7nIAvwLARhFJ1vBqKckpbaRoL0l3BNjgbdViPxrZX3OENsTbvE60QP8oKhP5OIUBobRjvsvWgPKbMilCBeZBkFI+nhotFXcIPoQR5HUWvDh+aBIavkg/LiVqWVmBAd5veAZELEa0jGZxQueAf7/7Uz/MRt+ccJjIYSmvwZiLy+6/w1RVszUjXBz36paxcExmRVVj1aJpS+vDF3tTb/WaIUwo2mYdhuuRrgxnuZndjwVJNwEk1J6gLuB74DfgelSyvTdVTuJ6IEprxhE2AqhtZBjsaLNY+SHjS3lywxGeG6la8EE0xa5fnyOt71hwRACqovjrMsa5B9rMauwjIJi9MoiZE9SX+DI/YVD8cRZWPTbwsfTol0/uHBESABGM7Ez4RZvNBet1eOgGcZT8z+iSf5UTs3/yLR3avV9wzmnqbVey5SiXobfUB8PjbWK24dFfQzvPyLLcX/hUA7I8hEu7gyB/7j+4+S0q5nerjxSeBu53lp4pSDXW4tHCm9LinsxasCUzbSyRREjMz+Ies00b+z5vGuzbgjZlnBt1g2G122X1WI2JqLpMfzbJsJ9hUNDnnNv4VCa5E81XBns9IKp/mC1YO9Rsl31ep0WjJWgOCdxZIxYSvk18LUTaaUjbw3uZLoBgBFmDXIrDfUz+97OAz/9ZTo3MNZUrN6uZVHdclYX/tguq4WsxW02HxSgT4uTeOAPTNc8NhuTlATkfthzR8RyjeG9MrMpZ1ZavEelm4pErsDjNK3rVwnZVjGcAZ0aMD0n1/h9DcaSj8rQ0bbGtSqyeGPssTy9AWDWO4+1ilv4/ToVOcFLmZrcRo3NahynSX70itPKtzDrSZtN1ylOwvOklTxVQxzxlykjvd2YOZvvTnmIH/80/rZrs24gW4RO3cvGy9qsGyJ60p5717Dv9TbU9O4zlS2aHjsUvKOtcR00JdCKq9moJ2v3ewUb3bpVy7EjLzDUtsDbim4ZkTtOhcth1wMTPH0y+FmDC0ew2VZK8ZPGG47GR4u6VWJfZEClbPM2yQUt62h/FENEbWaGi5nernQtmEDT/Cl0LZgQ0uKPNS4Yz847wegZ+byCN+lT5XOKnjxAk/yppkYYYPzA9rS86DafzFNDWsHbZKAV/EFRHxC+nqDI4GDLwX65Z3q78kBY6/n3ch0cm3LWuuD9uO4zw6wFfXKVclzWrp7hPS3qVuGFq9sB2vsuaP0s26T2vjtFbfJ976r/HJXuCLlb169Ct9O0sdLnrmwTVcZovfMXPANi9npGem7B69utKnxKSSKNzdYF73NUukPeNXwcPd6etNH0n2jTnuxiFDFtNe1oZcgF3HJeE0Br+IcTboT1Z2eLyOGHStmZ1Bq50Z+fgrE6Fv3JBT/6PU5WSEZP9t7ep7F5bF//JhqDC0cgmvSwlG/0tc6b1ba2rebkpq+EeNkGF45gyePFt85EiV/iMpyZd5/HaSO+CTl2VuPqvDGoA519a91ee1ZDxl7Vlo9/2cKjn6wE4KfHetN6ZOTuPy19hn3z2L4wKvbzReW6SAu7dbx/S2duCtvI+sdHrC03F20qVrzrVesFNrgCfOemsyxtjl25nJvrz27E2G+0rcKCW8FZGS4KvFpl4eo3Drqc4r+vOsBSbZOMnMf70GkMIa3n+7qfxqWzzfeXtYs+tuxErzh8nHqBtxUHrvoPL59+Ei98F7pl2uN9W4SsAHdH96bMXbub86+5i9OXN6XA42XB8PNpHrRtoBler+TDWwOzA/X8q9OmftWQtcTNmOntyp3yc8PlQoNdcmaBOHYNW61KWXQ/vTafLN3Ga9edSeuPrTeMHrjgdMbNsrYmutH0n0IJf2K8NKpTHhKPdPk9B/Gm2adlnZi7WEUQ5VnNCz7SetJBHo186SJr1H54LLoDs3K5gGmwst63FaPb8ZTqLPnrQMTx4Ref4V/H/pcRven8jFZPFxYZbLV408yQHdRG9m/J6C8i145a/GhvalTU9s72jjKZ5hf09yWtT+b7P/aEnK9psBtUsijxPeLP7jov5P8Mg1JQr1r5kA3Nx1zRGoDrOgemUYX3iK89qyE5j/dh5t2h6cfkn2uRZEa2RAltiZ5RpzIf3tqZ1wedGSInwCk1KwDQ/fTarH/mEkb2bwng32IwGi94BnBMGm/eHqvybJI/NaQX0riW1ppsVKNCzOda4cYgI6zTq/lJXNiyjuEWaJkuEVIh6Fht5YaXvsGFI0LGuIOJ9X84gwtH8GbPJSEt6Mvb16dqBbe/gD9wwek8dmlzuvjGdXVxHr20BbMe6OF7kPardmVrhd4bRa5JN3bki3uiuwGDd1K6pOBFwzH/ZKwBnPP4Bbx8TTs+GXou/drWZe6DPSzfe3aTGpauG3FpC85sVM1wEYfwRUEkIDLLR4z36jrQrw0/Z4bueShuwmu7weec4jdAoBnjYF00L/gIV5S9Mjs31nQdnM9GewZHxK3omI2ltqxbhbVPX+yvP1vUrYK+ZW39aqHR0oM6N+LVa9vz4yO9/BuHALiDOgGnnmS8RsOVHRoYHg/WgavTrYbfMniYa0CnhqSSEm+I2zesFvK/UWs0v9BLVtC2bMG9vAnXncl393UH4JOh5/qPj72qLbUqZVvqEYbjGrUP4XKHVPhCuCn31AE2PXcpy0deyMlVy9HttNqc2ag6APWqBjLghT5X+KsD2+POcDH4nMY8fHFznvY1IIzQXfIzvV15Qt7uc/GGum2iBWGEH/9jzMX+v+c+2IM7ezYD4L93ngNoPd0WdavQp8VJlvVixOQhZzHJ54q7pPXJIefqVCnHnAdCK+zVoy9iaM9TQ44ZjQlJQBisEasHeoVXyuGuteBpM2YMObdx1PM1KmZxe/dmMdMB670obxRrELwh/Sk1K7Bi1IUR11wVVnGFB745aYQfuzRU/0IIOjSqjrDZZWwQ1hDMcImQvJKV4eI//ziHv3dvyoe3nh2xBaPOJQUv+t/z8CN74YmdEa5OvSHS4qk1oXkkWsMsyUNW5zStaSlA7KVr2vHU5a2j6vfzu6J3LNy+oIXg953p7cpDhXdE1inAcnf7iDwjhFaXlnNn4AqSRU9yzN9a89ilzf37U1fMzuTy9vX9HZElj/fhwQtO5+qOgbz69bBu/P5UoE4yo+uptULqLgD6jQsZzvPi4kCrG6lxTWBnLLt50mlKvCEO5rXrzkQIwcMXh1YAnRpXN73nsnb1OOPkygB0aGR+nSXKB7XcR+4NjLGMyoORmrtYCBHSs61XtRyPXdqc/9wZaAQ8ckkLch7vQ3Vfqy7DJbizZzN6N480ehOu03rUVcsHeo7Pj3zaP14bXMl2KHgn5so0oHkHsjMDLeDMDBcPX9yczWP7UtHnOWhSqyLf3NuNt286K7pObOTvpkE93bcHd+Lqjg0i3EMVszO5skN9avp0M+aK1qy7chbUah5SaYpa0deIDa6U/9NvVUQv6ryCN/nAIHIYYIq8wPpLOcy5zYzn0kJA1ctHXsi393YPkb2ur6EXzZAHozewDMdcAWngng4PlLm8vXnQjBUpRvZvyeBzTqF+tfIhwzZ/Pnsp/7zoDP//tSplcZavJ1cpO5NlT17IP3pEbwD5s+VNMyMaIh1P0eqBSwpe9MU1mC9nKQGa9GD8QG3sfzGRjWVJoNdod6j6/Vs6A1r0sYGjFq58y/9nsOEyw+gdalUK9B4Hdda8VmecHNoDrX3ujbj/uTqkTnmq44+83vBl/zX1q5Vn+MVn8POjvSN24RIE8kuVcm5u796Mjr76NlykmpWyuaf3aSGdoKxMF+WzDHrlYQot584Iqbt0guMlJp2fQ40Br9PfJJYjFZQqQ6wr9o7uTf2uiVevbc+tXZsUjwCXPG/7FiEEt3dvFuKuyXAJQ1dtRYOAsnr+CjZwzKgXPzZGYE8wt5zX2FxeX7GRju9dBZmugNx9WtZBCOFvNYfIIITflduhUXWtwr/7Z38FkffwHttrxBqNzY303MKRtjeFBJjR6Vaekbf5r+ls0W2qyW1LJD/39QkEreiNRp0v7+lK+bBKr2p5t1Zp+T5R5XKZ/p5JhaxAHgqugEELBNP51w0d+fKerpxeMBWPEJrx1V26uHCNyovwKoQHWEkJ13VuyF3nW/MKhHPzeU146nLNsEVz3Yf3ZspnZdD1VPMGixkPXnA6oI2zA4y+rBXXZ8w1HV+UgGjSA3HTTHq30LxYt/MkNOkRoq+faMPgwhHcdX4zZoiLDBs2dLrVUKYep2uLH830dmXDeeOgakNAaL+vfAva2p/7G8y4Ae343/0Br1PftnXZPLZviIsY4Il+LalTJexY35b+PD3xho4seqQXQ3ueGuKZMUK/58PbOrP40V5xeR3tMuLSFoHnBx1fMPx8/vOPc5L+/FiUCkN87Vmh/n2XS9C/bV0A2jWoZsvt8O193fzu15JAh0bVubVrE8YPbB/1ums7N2LqbWdb6qE+cOEZpudMV5CKnWxMusSYI2s1kCU4HuCHh87nw1s7R4xLWaXyVRNg5H6fV2M/9BsX0AEw/Y7E8oqVBo2I8tFa16/qHz8z9XYQCHw5t1lAx+Hjbrd1bWp4/+XVv0CMykOMzkOMyoNRWsBNsFdBjMrjt77f8uglzalTJWA0n7uyLQ9d5Pw2ck5FQQNMHtKJsVe2oZIvHkEfy7yk9cmGOzOBplMxKg9umun/389NM3m49Q80yZ/K9L4rebyytnfxzec14fRbJ0XMejjS5iboNy6mnHubXA73r4JRB7XfFo3w5rF9/Y2s8Lx0ep3Kfg/dC1e1tZSejsvnqauUnWmrQaqTnZlB3arFs7rW37s35TZfhyy4nDSsUcHvTUklpSJq+tm/tYkYPx3RtyXXdGroDziySvOT45v+BMCcpxJuodrF5RI80a+lpWvPPbUWCDfI0GV+pIR8Ya1NpndQrbo47XBOM2NDPPGGjmzZfzSuNBvVrECjmhV4a3An/vHRErbsPxZ5kc1X0cuxtKgDK5dFM7Yx0zd5geDjuw9r64ofLwzMrTbyNgTksc+gs7Xgx8mLNkWVC2LrpG+bugnLE43gxnmv5lpv9j2f3IfzPf5zZiuU+b0kYeh5Ivj93h3SmcUb91KrUja5B45HzHpY2Ot8Kocn5DBGMSBrth+ita/3H97ItVq8OzSqzqrRF1mTwVqSSSeRspYsSkWP2OUSIRF2oI0p6Jms2MjLLd7nxcPIvSDcIS7FfOmii2uapdv1CiwJdtiUi1ufbBr0ZNXZ0bJeFV642l6L3wy/DkzOm+nGjmt6aM/A+1pestGkghFCsGD4+VzXuWHIPPsMV3KKv3/4ImoeCT359OWhkbddT4vlWnY+A55WRzOHPc84yf+EKUW9jJ/UcUjIv2Z5QiBoVLMCA8/SGilWG2/JQs9L2ZkZ/kDRYsP37k6aQTtDZOnSEDCiVBjitKFq7GCJtCAokGzPA7toXvCR5cKhX2e1R5x+bc9I7I53B3rEMa5L4OWDA/piJWMlEr5hjQo8d2VoQyQzWo84SPji2CYxkUrSyvOan1yZKgZT4YI579Ra/DKid8iCLCM9tzCDiyLiBMJdyeEipFuln2L7r8ng+52qCOXADJaUPD4qpcI1nTb0fjLpj/h7tyYIIZj0w0ZH03VZzJ2uGL3B4sLJisV2Wn5LbDH9BFy0YKNHbCEiPphw13QqK6jYjZpQ4WJdH97G+Pa+7mzcc4ReL8+Pel94kBLAi+7buWbE9OgPNJMrTI5Ul5tU4jeEqXp+Gmtf9Yid5Lfk754yom9LHguKAHQKu5V9MsaI4yEVxiPeR0Ybm4r2HrF6EKY94hjyRO0Rx7jXCtGeH9Pw2niOkXqiLVphN/14ZEiT4uEnncRJVYMv0CNOvy6xMsR2yIoR+LUpems7vbHZIzaYQ1pSiVf0ZLawnagrZIwxOb1HHG0XrniwInp40omMnRo1cKIFolklHpHC80SEy9ogzeI0DFYflYy8nZQ040gy/cywMsT2KMHGxgz9lezWW1YLQLLqmFS6mewGrCW7kRIrebOK/vqztcUbOhoE7QTfEu83tGNcY76DzeuN8nO8Bs/KbeGNgXRzg6ZDkFjANZ2OpjC1KENsh8L4ptCkM7qL2Wodpbv8Ul2wdZwo1HZfRa/ko4z8Gh41XZkpBvGOEcdK+5xmNdk8ti91TeZYx2tMnIist2MzDV3TMRJIWgMx7J0jjX+KDWKKjGDocr8pESHgIUrDdoAyxGUcvXBYD9bSfkfbfKCkYTtq2qerWOPkdiq98CuD742VTqxGUaL1TjIqb7tDG3bHYA1XZLMgV8gzbOQLEd44S9PykdKAvBSn6Y/aToIciaIMsZO47S0ekg7oxsRu1HSqg7VSGTVtv0IvHiLcown1SIWt+532kCTSiAGoYLAucbxGKJGGSLpU+nY/j9PF24mhjkRRwVplBc/xVEtgm3iNULo0+NN5yk04cUdbx+2aTo0rzmwZ1GDCe5t281Os3uqpJ1Xm5WvamZ5PmotWhvyKPG0UrJUcSQxJbXlJceM9ReXBCsoQO4k0Xpc2nfG7pq3mBAuVbOjlaZjrw7A9jdjvhky8YjGrnEJ6EDHTiPWU+Fzbdiqs4CSs6CfSNR3DvR5FGLNzV1nYjcgp7M7hLm7SKXgs1XVCmnySEJQhdhKT9WfTGX8r0fb0pfCa1FGxYuLo4+Jd0cNBIaIbGqtphB2w22O3uWiGaTpOVHUxkiiOzlUi05fMGhapNofFbQSD1ZCMYC1bkfmpVn4UlCF2ksZdUy2BbewWjnQZI9ZJRes2IjAnDLu9vZjPi9WjjVG9J1rxxT99KbHnWsWqeCEBcAlGZUdL2+69xUE6FNdUu4b9KkiXjxKEWuLSSXauTLUEtrEbrKVfVZqipu3iHye3GelrpwJouuNrFmaNp57Yy9FFdaFa7J29IueyJkY8EakQu6ECiS8IE3J9GtWrsd4j1QbR+oIepY9UL7EZDWWIneT4/lRLYBuvzcxp6po2IQ0bnxGkdIzY4NhlroV0WzsZt+sEAJXzd8AXw7STBsY41qYPMceYY5y38gm9UpLhu9LK9TF78RbSsIvdvGjn64Y3PuK5N5mk2rAmb2Ef+1enY50kUhHJ1qlTJ5mTk1O8D10xndwZj1JP7MVVraG2QYPdvYNHVbVwTZ5zzzPDKG0IHKtQg/3HCqjGEby4yMCLCJYh6H5vuWocOuGhmjiCVxpcG0b+5/eRsfR97TpXhrYdXL9xHMv5mP0zH6ee2IskdMzDi8CFjJquLZ2tmM7OTx/jJO8eiirXx33hqND3N7l3x9ShnPTHlIjxGClcuJp0Y9+fS6ghjkSXecV0dnzyGHXkHrxVGpB5xkXk/vJ54LmnXciB5V9StWAXx8vXpeKlT7Fp31Hc88ZQz7UPV9UGod/hv49Sj72ICjU4euwoFdH2DTasLKo21DaED9PFrk8fo3awLnyyHvxpCke+fjL0ueF6Ou1C9i37kuqFuyjMqsbRAg/VOIKrWkP21O1J/ppvIuX2PXffjPv9+qJCDcQlzwNEfhuDb7h1/ntUmDPCf3+RcOOShX4D7MmsgPvyV0OeFyz3vno9YfWn2v0CRPkacMnzoc8Ku+d45ca4ty4iAy+4MhC+vGvG/p8+4tjXI43fP4yiZdPY8clj/vJ3JN9DhaJDnKhQlwqXBLwZG+ZMpvr8JwJ6AxAuRKebA7IEy12hBodOeKjkPURBpXqUu2h01Hc0KuOuCjXIO15IZXk46jcJZveiD8n47hG/nKJCDWj1N/b9Mi30WLjOw9g77W6qrZlChvAFskrwWJTBkLB8J90VOVgoAnWd8CKqGpT/FdM58MXjIeXSTI+GdaYDCCGWSCk7GZ4rE4Z4xXStR1EYNL3IXR76T7CnZCuGuFZz6P6gM88zwuhdXG6t5i4qiH6vuzy0GwTLp4beb3ZtuLxfPoDMeSeyt9KkB3LrLwgr07fM9GD1G1l9//B7zWS3gp4WRD47Fi43XsDlLQxNz+p3CEdv6EF0nQHemcNweeLIJz4kYT3TYD18NhSC3wm0YEVXRvTv4JPb++lQXDLs/nBcGXDFRO3vsPeMkA0gIwsuf8NviCzdY7CloV/GcP1Fybvyi2EIs28Z/E2ivXenW6FRl+h5LFgGs+8fK2/Fqousfh8I1Xk40cpcPPXhiunG+S5W+rHqFqPzichpgjLE41tD3tbI40Y9jGhYMcR6uk48zwizd7GKyABZZO3acHlH17B+r510wfo3svP+wfcmKnvVhtrvRHQfjJ3vEHzPyKDhj2g6A+dkTTTtRL+hnefpz7L6jHCdxpLRTt4Nvw+iXycyoEo9a2lFe0creStaXWS3jjFLK1aZs1sfxitXrG8ZK10n6m2iG+KyMUacl2vveDo/L9E07FT+4c9ywggbpWt2zOi4nfcPvjZR2Z3OK/HIE35PcefreNN26hvaud7qfWbfwY5urTzLyjWyyF5aZtdayVvRnhOvzu3K4dRzYl0f61vGSjeZ5clH2Zi+VNVkYr/Z8XR+XqJp2JnrHP4sp+ZJG72DVZ3Zef/gaxOVvWoDZ/NLPPLovSr//1F0lsy8bTftRL9hPN/c6j1m38FOGbbyLCvvITKspxXtuVbyVrTnJPp9rcrh1HNiXR/rW8ZKN1llKYiyYYh7P6n5+oNxl/cHr1imSY/Y19Rq7tzzjDBK2+XWxmpi4S6vBVeF3292bbi8HYcYX9ukh7U0zdIF6zqz+v7h95rJbgU9LaNnx8JMNqvfIVyGYKLpLJF8EkuG3k9qaYUjMmJ/B11uo/vDcWXY03tGVuBZVu8xyxd2ynCsZ8XSW7AsVtOKJmOsvBWrLrL6fSBU5+FEK3Px1Id25LKiJyt5xal6OwZlwxC3HaANuFdtCAjtdzwD8DfNjDDGwdt7Uas53P2zc8+z+i5XvKkFTOjHytfQfiDQKtVl6Dcu9P5o14bL22+cFlCiXycytP9vmhmUJkSExAhX9HTN3svoWivvb3SvLrtR6IjI0L6rrgczmY2e3enW6P+byWb0HYI3DXFXZL+shFfa1EU0WY1kCZbXnxdM3iU47SveDNVX+Rrwt4mxv4Mud/j9rjADnlVRC9SKpvfw5wcHDRnd06RHZN41i5q2U4bDrw3XYzS9gZbXdFmsphVNRtMybrEuMvu+sXQeTnh9oRNvfWgkl7ti7Por1rcMOR8lnSRSNoK1kkjjR74CYPPYvimWRFHaUHlLoSg9RAvWKhs9YoVCoVAo0hRliBUKhUKhSCEJGWIhxItCiLVCiBVCiE+FENUckkuhUCgUijJBoj3iWUBrKWVbYB3waOIiKRQKhUJRdkjIEEsp/yel9Pj+/Qkovp24FQqFQqEoBTg5RnwL8I3ZSSHE7UKIHCFEzp49exx8rEKhUCgUJZeYS1wKIWYDJxucGiGl/Nx3zQjAA0wxS0dKOQmYBNr0pbikVSgUCoUtCgsLyc3N5cSJE6kWpUxQrlw5GjRogNttcfERLBhiKWWfaOeFEEOAfkBvmYpJyQqFQqEwJTc3l8qVK9O4cWNEOm7GW4qQUrJv3z5yc3Np0qSJ5fsSjZq+GBgOXCalPJZIWgqFQqFwnhMnTlCzZk1lhIsBIQQ1a9a07X1IdIz4daAyMEsIsUwIMTHB9BQKhULhMMoIFx/x6DqhbRCllKcmcr9CoVAoFGUdtbKWQqFQKNKSV155hWPHnBn1HDJkCDNmzHAkLafTVIZYoVAoFGlJPIa4qKgoSdIkj4Rc0wqFQqEoOYz+YjVrth9yNM2W9aowsn+rqNe8+OKLZGdnM2zYMO6//36WL1/O3LlzmTt3Lu+88w5VqlTh119/5fjx41x99dWMHj2aCRMmsH37ds4//3xq1arFvHnz+N///sfIkSPJz8+nWbNmvPvuu1SqVInGjRszcOBAZs2axfDhw7n22mujyrNkyRIeeOABjhw5Qq1atXjvvffIy8tj8ODB/PLLLwBs3ryZ/v37s3LlSsPr69at65gOVY9YoVAoFEmlW7duLFiwAICcnByOHDlCYWEhCxYsoHv37jzzzDPk5OSwYsUK5s+fz4oVKxg2bBj16tVj3rx5zJs3j7179zJmzBhmz57N0qVL6dSpE+PGBfaTrlmzJkuXLo1phAsLC7nnnnuYMWMGS5Ys4ZZbbmHEiBE0b96cgoICNm3aBMC0adMYOHCg6fVOonrECoVCUUaI1XNNFh07dmTJkiUcOnSI7OxsOnToQE5ODgsWLGDChAlMnz6dSZMm4fF42LFjB2vWrKFt27Yhafz000+sWbOG8847D4CCggLOOecc//mBAwdakuWPP/5g1apVXHDBBYDmytZ7twMGDGDatGk88sgjTJs2jWnTpkW93imUIVYoFApFUnG73TRp0oT33nuPc889l7Zt2zJv3jw2bNhA+fLleemll/j111+pXr06Q4YMMZyHK6Xkggsu4OOPPzZ8RsWKFS3JIqWkVatWLF68OOLcwIEDueaaa7jyyisRQnDaaaexcuVK0+udQrmmFQqFQpF0unXrxksvvUT37t3p1q0bEydO5Mwzz+TQoUNUrFiRqlWrsmvXLr75JrBlQeXKlTl8+DAAXbp0YdGiRWzYsAGAo0ePsm7dOttynHHGGezZs8dvWAsLC1m9ejUAzZo1IyMjg6efftrfw452vVMoQ6xQKBSKpNOtWzd27NjBOeecQ506dShXrhzdunWjXbt2nHnmmTRv3pxBgwb5Xc8At99+OxdffDHnn38+tWvX5r333uO6666jbdu2nHPOOaxdu9a2HFlZWcyYMYOHH36Ydu3a0b59e3788Uf/+YEDB/LRRx8xYMAAS9c7gUjF8tCdOnWSOTk5xf7cZND4ka8A2Dy2b4olUZQ2VN5SOMHvv/9OixYtUi1GmcJI50KIJVLKTkbXqx6xQqFQKBQpRAVrKRQKhaLUcNddd7Fo0aKQY/feey8333xziiSKjTLECoVCoSg1vPHGG6kWwTbKNa1QKBQKRQpRhlihUCgUihSiDLFCoVAoFClEGWKFQqFQlAl69uyJPnX20ksv5eDBg6kVyIcK1lIoFApFgBXTYc5TkJcLVRtA7yeh7YBUS+U4X3/9dapF8KN6xAqFQqHQWDEdvhgGeVsBqf3+Yph2PEGuuOIKOnbsSKtWrZg0aRIAlSpVYsSIEbRr144uXbqwa9cuQNuCsFevXrRt25bevXuzZcsWAIYMGcKdd95Jly5daNq0Kd9//z233HILLVq0YMiQIf5n3XnnnXTq1IlWrVoxcuRIQ3kaN27M3r17Afjoo4/o3Lkz7du354477qCoqIiioiKGDBlC69atadOmDePHj09YB2YoQ6xQKBQKjTlPQeHx0GOFx7XjCTJ58mSWLFlCTk4OEyZMYN++fRw9epQuXbqwfPlyunfvzltvvQXAPffcw0033cSKFSu4/vrrGTZsmD+dAwcOsHjxYsaPH89ll13G/fffz+rVq1m5ciXLli0DMNxW0Yzff/+dadOmsWjRIpYtW0ZGRgZTpkxh2bJlbNu2jVWrVrFy5cqkzkNWhlihUCgUGnm59o7bYMKECf6e79atW1m/fj1ZWVn069cP0LZK3Lx5MwCLFy9m0KBBANx4440sXLjQn07//v0RQtCmTRvq1KlDmzZtcLlctGrVyn//9OnT6dChA2eeeSarV69mzZo1pnLNmTOHJUuWcNZZZ9G+fXvmzJnDxo0badq0KRs3buSee+7h22+/pUqVKgnrwAw1RqxQKBQKjaoNfG5pg+MJ8P333zN79mwWL15MhQoV6NmzJydOnMDtdiOEACAjIwOPxxMzrezsbABcLpf/b/1/j8fDpk2bLG2rqCOl5KabbuK5556LOLd8+XK+++47Jk6cyPTp05k8ebLdV7eE6hErFAqFQqP3k+AuH3rMXV47ngB5eXlUr16dChUqsHbtWn766aeo15977rn83//9HwBTpkyhW7dulp8VbVtFI3r37s2MGTPYvXs3APv37+evv/5i7969eL1errrqKsaMGcPSpUsty2AX1SNWKBQKhYYeHe1w1PTFF1/MxIkTadGiBWeccQZdunSJev1rr73GzTffzIsvvkjt2rV59913LT8reFvFhg0bhmyraETLli0ZM2YMF154IV6vF7fbzRtvvEH58uW5+eab8Xq9AIY9ZqdQ2yAmiNqqTpEsVN5SOIHaBrH4UdsgFicrprMwaxgbswfB+NaOhPgrFIDKWwpFGUK5puPFN9+ugcsX6q/Pt4NSOfldUYyovKVQlClUjzhekjjfTlHGUXlL4TCpGIIsq8Sja2WI4yWJ8+0UZRyVtxQOUq5cOfbt26eMcTEgpWTfvn2UK1fO1n3KNR0vSZpvp1CovKVwkgYNGpCbm8uePXtSLUqZoFy5cjRoYK+sKkMcL72f1Mbtgl2IDsy3UyhU3lI4idvtpkmTJqkWQxEF5ZqOl7YDoP8EqNoQENrv/hNUMI0icVTeUijKFGoesUKhUCgUSUbNI1YoFAqFIk1JSY9YCLEH+Mvi5bWAvUkUpySidGKM0kskSifGKL1EonRijFN6OUVKWdvoREoMsR2EEDlm3fmyitKJMUovkSidGKP0EonSiTHFoRflmlYoFAqFIoUoQ6xQKBQKRQopCYZ4UqoFSEOUToxReolE6cQYpZdIlE6MSbpe0n6MWKFQKBSK0kxJ6BErFAqFQlFqUYZYoVAoFIoUogyxQlHCEUKIVMugUCjiJy0MsRDiZN9vVaH4EEK0EkLY20urDCCEOE8I0SzVcqQZ5VMtQDoihMjw/Vb1ig+lE3OEECmzhyk1xEKIM4UQc4CnAaSKHEMI0VYIsRAYA9RMtTzpghCigxDif8BcoGqq5UkHhBBdhBD/Bd4QQlyoV7JlHSHEOUKIt4D7hRCVVb3ib8C+DzwuhKihdKIhhOgshBgGIKX0pkqOlBhioTEe+AB4X0r591TIkaY8DsyQUv5NSrkNynbrVQjhFkL8G20KwQTgO6Cn71xaeHRSgRCiJ/Am8AnwB3ADUD2FIqUFQogewOtoDbZ6wGNCiItSK1VqEUI0Rcsr84BTgKeFEH1TK1XqEULcB3yK1ji5xHcsJY3ZlFRkvtZYJeA3KeUHAEKIZmW8YnX5CswRKeUrvmMXCCGqAWXZnZQNzAe6SSm/BP4LtBBCZKayBZsGtAF+lVJOAT4E3MCR1IqUFnQAFkkpP0bztNUBrtWHv8ooZwG/SynfAx4ElgH9hBANUylUGrAR6AfcCTwKIKUsSkU9W2yGz+dGOz3o0IPA2UKIJ4QQi4AXgfeEEB2LS6ZUE6wTn1HZC3QTQvQVQnwG/BOtF/iQ75oy4U4KyytHpZRTpZTHff9nAkVSSk9ZargZlJ8FwDVCiCeBpUBd4E0hxDUpETBFGOhlHVBNCFFXSnkArXGSBVyRCvlSgRCivxDibiFEF9+hX4GGQoiGPp0sAg4CV6ZKxlRgoJevgBW+30d0FzW+jk9xkvSKTAhRTQjxFTALGCCEqAggpTwEvAFcjdYauQ7YAVwlhDDcoaK0EEMn76K15CdLKS8C3ga6BGWeUouRXqSU0jeUoefV+cDfhBDVy0KP2EAnlQCklMuAi4HGwFApZU+0CvZiIUSL1EhbfJjpBc0QHwLe942fNwR+Q/PAlWqvkhCirhDiC2A42jDFu0KIi6SUG4HFwADfpX8Aa4AaZSEgNIpeigCvlPIE8DJwqxCilpTSU9wyFkePoiLauN49vr+76yeklBOAnlLKH6SU+cBnQCfgWDHIlUpMdQJ8iVa56uN9OcAuIL8Y5UsVhnqRGl6fMd7su6ZHqoQsZsJ10k0/IaX8BaiNphPQxkUrA0eLV8SUYJZX1gMPAM8B/5FS/g1YjS+uoJR7lToBC6SU3aSUTwOvAnr8zQKgjRCis88AbQPO8xmh0k64Xl4B/gEh+eF74Ce0/IQQonNxCpgUQyyEGCyE6CGEqOILOJoETAdOoLmj6+nX+lwlOh2BXKAoGXKlEgs6qQ8gpVyB5oq+WwhRCy0IpzWwL0WiJxWreUUIIXw94GzfrSf046mQO5nY0Ek28CNwl+/W3miR9qWyco2hl866XqSUBVLKeVLK//Pd2hH4NjVSJxefTnr68sIctHgBnX3Aet/fP6N5Bsb7vAetgC1CiArFKnAxEUMv+4Hffde5QBsbRpup8rAQIg/oUJx1i2OG2Oc+rCuEmAfcBFwP/MvX1T8hpTwGzEbr6fUKui/bp7Ac4CJgbGlppcWrEynlO8DHwCjgKuA2KeWWYn+BJBGPXnwu6gwp5VG0fNtFP56at3AWmzrpDeDzIs0EKgkhfkAb3rlbSrk7NW/hPPGWId+9XYUQS9C8CF8Wt+zJwkAng4DJQAUp5Q4hhNt3aV18njUp5U4p5atoBnkyWgP/eZ/+SgVx6sXru+9UYCra8E5XKeXEYq1bpJQJ/wAZvt+nAx/px4DXgE/Crr0freVRFSjvO3YucIUTsqTLTwI6qRx03J3q90gjvVQIOp6V6vdIA51UCyo/5YGmqX6PNNFLVaCi71g94NJUv0dx6yTomi+APr6/T/L9zgyuY0rLTwJ6qaHrBzg/VfJnkgBCm3P1NJAhhPgaqILPrSy1MPB7ge1CiB5Syvm+297yFZjZQCMhxJlSyh8TkSOdSFAns4BTfDrZLqUsTMErJAWH9VKQgldwHAd00kgI0UFqbtqNxf8GycGhvNJRSpkLbC/+N3AeuzoRQmQBe4B1Qohn0KYr9ZTaUODhFL2G4zikl/Ol5kVKmScpbte00CbOL0Hr4m9AU0YhcL7wDXRLbUxvlO9Hpy8wFG0uWxspZakoKOCITpZTynQCSi9GOFh+thWb0MWAg3klt/ikTi42dTLad1s5YAja+GhltB7gAUoRDuplf7EKbkAiPWIv8LKU8kPQlqsEmgBPAv8COgptIPwzoJcQorGUcjNaYEUfKeUPiQiepiidGKP0EonSiTFKL5HY1UkDNLf8R8A4qU11K42UGr0kEqy1BJguAkuCLQIaSW31lgwhxD2+1kgDtAUYNgNIKT8vpYUFlE7MUHqJROnEGKWXSOzoxCulzJVS/iKlHJxOxiYJlBq9xG2IpZTHpJT5Ugv7BrgAzfcOcDPaMoRfokX/LoXSOdUkGKUTY5ReIlE6MUbpJRKbOlkCpV8nULr0klCwFvgHyyXamq4zfYcPA4+hzX/dpI9jSSlLxVSTWCidGKP0EonSiTFKL5EonRhTGvTixDxiL9qC83uBtr4WyBNoroCFpS2YxCJKJ8YovUSidGKM0kskSifGlHi9CCcaCEJbB/lH38+7UluQokyjdGKM0kskSifGKL1EonRiTEnXi1OGuAFwI1okWllYEzkmSifGKL1EonRijNJLJEonxpR0vThiiBUKhUKhUMRHmdnPVaFQKBSKdEQZYoVCoVAoUogyxAqFQqFQpBBliBUKhUKhSCHKECsUCoVCkUKUIVYoFAqFIoUoQ6xQKBQKRQr5f9xR9Kk5wt6fAAAAAElFTkSuQmCC\n", 249 | "text/plain": [ 250 | "
" 251 | ] 252 | }, 253 | "metadata": { 254 | "needs_background": "light" 255 | }, 256 | "output_type": "display_data" 257 | } 258 | ], 259 | "source": [ 260 | "detected = data.to_frame()\n", 261 | "detected[\"anomalies\"] = data[std_anomalies.values]\n", 262 | "detected.plot(style=['-', 'o'], figsize=(8,3))" 263 | ] 264 | }, 265 | { 266 | "cell_type": "markdown", 267 | "metadata": {}, 268 | "source": [ 269 | "# Hampel filter" 270 | ] 271 | }, 272 | { 273 | "cell_type": "markdown", 274 | "metadata": {}, 275 | "source": [ 276 | "The default threshold of the HampelDetector is 3, which means that a sample that deviates by more than three times of the rolling window's standard deviation is marked as an anomaly. **Increasing** the threshold marks **more** samples as anomalies, **decreasing** the threshold marks **fewer**." 277 | ] 278 | }, 279 | { 280 | "cell_type": "code", 281 | "execution_count": 16, 282 | "metadata": {}, 283 | "outputs": [], 284 | "source": [ 285 | "detector = HampelDetector(window_size=20, threshold=3)" 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": 17, 291 | "metadata": {}, 292 | "outputs": [], 293 | "source": [ 294 | "anomalies = detector.detect(data)" 295 | ] 296 | }, 297 | { 298 | "cell_type": "code", 299 | "execution_count": 18, 300 | "metadata": {}, 301 | "outputs": [ 302 | { 303 | "data": { 304 | "text/plain": [ 305 | "" 306 | ] 307 | }, 308 | "execution_count": 18, 309 | "metadata": {}, 310 | "output_type": "execute_result" 311 | }, 312 | { 313 | "data": { 314 | "image/png": "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N27MzJkzmTlzJtu3b2fcuHF88803LFq0iG7duvH002X7SderV49FixaFNcLFxcXcdtttTJ06lYULF3L11VczevRo2rRpQ1FREWvXrgVgypQpDB061Da9k+gesUaj0VQSwvVcE0XXrl1ZuHAhe/fuJScnhy5dulBQUMDs2bOZMGEC+fn5vPzyy5SUlLB582ZWrFhBXl6eXx4//PADK1as4OSTTwagqKiInj17es8PHTo0Ill+/fVXli1bxumnnw4YrmxP73bIkCFMmTKFUaNGMWXKFKZMmRIyvVNoQ6zRaDSahJKVlUWLFi2YNGkSJ510Enl5ecycOZPVq1dTpUoVnnzySRYsWECdOnUYPny45TxcpRSnn3467777ruU9qlWrFpEsSinat2/PvHnzgs4NHTqUiy66iPPPPx8R4dhjj+Xnn3+2Te8U2jWt0Wg0moTTu3dvnnzySfr06UPv3r2ZOHEixx9/PHv37qVatWrUqlWLrVu38sUXZVsW1KhRg3379gHQo0cP5s6dy+rVqwE4cOAAv/32W9RytG7dmm3btnkNa3FxMcuXLwegVatWZGRk8PDDD3t72KHSO4U2xBqNRqNJOL1792bz5s307NmThg0bkpubS+/evenUqRPHH388bdq0YdiwYV7XM8D111/PgAEDOOWUU2jQoAGTJk3ikksuIS8vj549e7Jy5cqo5cjOzmbq1Kn84x//oFOnTnTu3Jnvv//ee37o0KG8/fbbDBkyJKL0TiCpWB66W7duqqCgIOn3TQTNR30GwLrxA1Msiaaiob8tjRP88ssvtG3bNtViVCqsdC4iC5VS3azS6x6xRqPRaDQpRAdraTQajabCcMsttzB37ly/Y7fffjtXXXVViiQKjzbEGo1Go6kwvPDCC6kWIWq0a1qj0Wg0mhSiDbFGo9FoNClEG2KNRqPRaFKINsQajUajqRT069cPz9TZs88+m927d6dWIBMdrKXRaDSaMpbmw/SHYM9GqNUE+o+BvCGplspxPv/881SL4EX3iDUajUZjsDQfPhkBezYAyvj9yQjjeJycd955dO3alfbt2/Pyyy8DUL16dUaPHk2nTp3o0aMHW7duBYwtCE899VTy8vLo378/69evB2D48OHcdNNN9OjRg5YtW/Ltt99y9dVX07ZtW4YPH+6910033US3bt1o3749Y8eOtZSnefPmbN++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\n", 315 | "text/plain": [ 316 | "
" 317 | ] 318 | }, 319 | "metadata": { 320 | "needs_background": "light" 321 | }, 322 | "output_type": "display_data" 323 | } 324 | ], 325 | "source": [ 326 | "detected = data.to_frame()\n", 327 | "detected[\"anomalies\"] = data[anomalies]\n", 328 | "detected.plot(style=['-', 'o'], figsize=(8,3), title=f'Anomalies detected: {sum(anomalies)}')" 329 | ] 330 | }, 331 | { 332 | "cell_type": "code", 333 | "execution_count": null, 334 | "metadata": {}, 335 | "outputs": [], 336 | "source": [] 337 | } 338 | ], 339 | "metadata": { 340 | "kernelspec": { 341 | "display_name": "Python 3 (ipykernel)", 342 | "language": "python", 343 | "name": "python3" 344 | }, 345 | "language_info": { 346 | "codemirror_mode": { 347 | "name": "ipython", 348 | "version": 3 349 | }, 350 | "file_extension": ".py", 351 | "mimetype": "text/x-python", 352 | "name": "python", 353 | "nbconvert_exporter": "python", 354 | "pygments_lexer": "ipython3", 355 | "version": "3.9.6" 356 | } 357 | }, 358 | "nbformat": 4, 359 | "nbformat_minor": 4 360 | } 361 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [tool.hatch.build] 6 | include = [ 7 | "tsod/**/*", 8 | ] 9 | 10 | [project] 11 | name = "tsod" 12 | version = "0.3.dev0" 13 | description = "Time series anomaly detection." 14 | authors = [{ name = "Henrik Andersson", email = "jan@dhigroup.com" }] 15 | license = { text = "MIT" } 16 | readme = "README.md" 17 | requires-python = ">=3.10" 18 | dependencies = [ 19 | "pandas>=1.0.0", 20 | "joblib", 21 | "numba", 22 | ] 23 | 24 | classifiers = [ 25 | "License :: OSI Approved :: MIT License", 26 | "Development Status :: 2 - Pre-Alpha", 27 | "Intended Audience :: Science/Research", 28 | "Programming Language :: Python", 29 | "Programming Language :: Python :: 3", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | "Programming Language :: Python :: 3.12", 33 | "Operating System :: OS Independent", 34 | "Topic :: Scientific/Engineering", 35 | ] 36 | 37 | [project.optional-dependencies] 38 | dev = [ 39 | "ruff", 40 | "pytest>=6", 41 | "pytest-cov>=4", 42 | "sphinx<7,>=4", 43 | "sphinx-book-theme", 44 | ] 45 | 46 | [project.urls] 47 | "Homepage" = "https://github.com/DHI/tsod" 48 | "Bug Tracker" = "https://github.com/DHI/tsod/issues" 49 | 50 | 51 | 52 | [tool.ruff] 53 | lint.ignore = ["E501", "E741"] -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DHI/tsod/07669cfb4d92daf28e6ee1495f0090ae4046a57e/tests/__init__.py -------------------------------------------------------------------------------- /tests/data/BO_TS_MO_FINO2.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DHI/tsod/07669cfb4d92daf28e6ee1495f0090ae4046a57e/tests/data/BO_TS_MO_FINO2.nc -------------------------------------------------------------------------------- /tests/data/combined.joblib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DHI/tsod/07669cfb4d92daf28e6ee1495f0090ae4046a57e/tests/data/combined.joblib -------------------------------------------------------------------------------- /tests/data/example.csv: -------------------------------------------------------------------------------- 1 | datetime,value 2 | 2000-01-01 00:00,0 3 | 2000-01-01 01:00,1.152781471 4 | 2000-01-01 02:00,1.450555612 5 | 2000-01-01 03:00,0.958642996 6 | 2000-01-01 04:00,1.174894688 7 | 2000-01-01 05:00,1.873848691 8 | 2000-01-01 06:00,1.84439191 9 | 2000-01-01 07:00,1.042026059 10 | 2000-01-01 08:00,0.787750065 11 | 2000-01-01 09:00,0.748739534 12 | 2000-01-01 10:00,0.821560744 13 | 2000-01-01 11:00,1.225795469 14 | 2000-01-01 12:00,0.957734013 15 | 2000-01-01 13:00,3 16 | 2000-01-01 14:00,3 17 | 2000-01-01 15:00,0.41953078 18 | 2000-01-01 16:00,0.612788648 19 | 2000-01-01 17:00,0.960619748 20 | 2000-01-01 18:00,0.922445351 21 | 2000-01-01 19:00,0.317372902 22 | 2000-01-01 20:00,0.683232022 23 | 2000-01-01 21:00,1.59095915 24 | 2000-01-01 22:00,1.284410756 25 | 2000-01-01 23:00,0.506858535 26 | 2000-01-02 00:00,1 27 | 2000-01-02 01:00,1 28 | 2000-01-02 02:00,1 29 | 2000-01-02 03:00,1 30 | 2000-01-02 04:00,1 31 | 2000-01-02 05:00,1 32 | 2000-01-02 06:00,1 33 | 2000-01-02 07:00,1 34 | 2000-01-02 08:00,1 35 | 2000-01-02 09:00,1 36 | 2000-01-02 10:00,1 37 | 2000-01-02 11:00,1 38 | 2000-01-02 12:00,1 39 | 2000-01-02 13:00,1.586141486 40 | 2000-01-02 14:00,1.314504228 41 | 2000-01-02 15:00,1.201552756 42 | 2000-01-02 16:00,1.445814101 43 | 2000-01-02 17:00,1.162387057 44 | 2000-01-02 18:00,1.066841287 45 | 2000-01-02 19:00,1.00248802 46 | 2000-01-02 20:00,0.503450841 47 | 2000-01-02 21:00,0.872922217 48 | 2000-01-02 22:00,1.580114567 49 | 2000-01-02 23:00,1.045514877 50 | 2000-01-03 00:00,1.012183073 51 | 2000-01-03 01:00,1.425633166 52 | 2000-01-03 02:00,1.191899682 53 | 2000-01-03 03:00,1.267666114 54 | 2000-01-03 04:00,1.061485161 55 | 2000-01-03 05:00,0.665546206 56 | 2000-01-03 06:00,0.424668666 57 | 2000-01-03 07:00,2.5 58 | 2000-01-03 08:00,0.4 59 | 2000-01-03 09:00,1.295534815 60 | 2000-01-03 10:00,1.514240194 61 | 2000-01-03 11:00,1.083260333 62 | 2000-01-03 12:00,1.1584635 63 | 2000-01-03 13:00,1.679222803 64 | 2000-01-03 14:00,1.081591441 65 | 2000-01-03 15:00,0.454411928 66 | 2000-01-03 16:00,0.937110802 67 | 2000-01-03 17:00,1.020471646 68 | 2000-01-03 18:00,1.285019944 69 | 2000-01-03 19:00,1.450649173 70 | 2000-01-03 20:00,0.937287208 71 | 2000-01-03 21:00,0.506385868 72 | 2000-01-03 22:00,0.921499469 73 | 2000-01-03 23:00,1.081260008 74 | 2000-01-04 00:00,1.015525661 75 | 2000-01-04 01:00,1.319109975 76 | 2000-01-04 02:00,1.187151435 77 | 2000-01-04 03:00,1.267982362 78 | 2000-01-04 04:00,0.896385507 79 | 2000-01-04 05:00,1 80 | 2000-01-04 06:00,0.9 81 | 2000-01-04 07:00,0.8 82 | 2000-01-04 08:00,0.7 83 | 2000-01-04 09:00,0.6 84 | 2000-01-04 10:00,0.5 85 | 2000-01-04 11:00,0.4 86 | 2000-01-04 12:00,0.3 87 | 2000-01-04 13:00,0.2 88 | 2000-01-04 14:00,0.1 89 | 2000-01-04 15:00,0 90 | 2000-01-04 16:00,0.768838193 91 | 2000-01-04 17:00,1.156824441 92 | 2000-01-04 18:00,1.399798202 93 | 2000-01-04 19:00,1.112722702 94 | 2000-01-04 20:00,1.221512379 95 | 2000-01-04 21:00,0.859352212 96 | 2000-01-04 22:00,0.247665553 97 | 2000-01-04 23:00,0.784977135 98 | 2000-01-05 00:00,0.634419463 99 | 2000-01-05 01:00,0.239960571 100 | 2000-01-05 02:00,0.422304927 101 | 2000-01-05 03:00,0.606980415 102 | 2000-01-05 03:59,0.776914226 103 | 2000-01-05 04:59,0.96975411 104 | 2000-01-05 05:59,0.774306839 105 | 2000-01-05 06:59,0.967961138 106 | 2000-01-05 07:59,1.145144565 107 | 2000-01-05 08:59,0.464866706 108 | 2000-01-05 09:59,0.13530199 109 | 2000-01-05 10:59,0.738101172 110 | 2000-01-05 11:59,0.832450078 111 | 2000-01-05 12:59,1.082938156 112 | 2000-01-05 13:59,0.948400949 113 | 2000-01-05 14:59,0.65301421 114 | 2000-01-05 15:59,1.526724574 115 | 2000-01-05 16:59,1.842639141 116 | 2000-01-05 17:59,1.869540157 117 | 2000-01-05 18:59,1.697764564 118 | 2000-01-05 19:59,0.784910422 119 | 2000-01-05 20:59,1.040359074 120 | 2000-01-05 21:59,1.802531081 121 | 2000-01-05 22:59,0.834522078 122 | 2000-01-05 23:59,0.179125049 123 | 2000-01-06 00:59,1.064403211 124 | 2000-01-06 01:59,1.859795992 125 | 2000-01-06 02:59,1.58075584 126 | 2000-01-06 03:59,1.058748553 127 | 2000-01-06 04:59,0.503596224 128 | 2000-01-06 05:59,0.503728592 129 | 2000-01-06 06:59,0.917595552 130 | 2000-01-06 07:59,1.322622102 131 | 2000-01-06 08:59,0.968904598 132 | 2000-01-06 09:59,1.080349742 133 | 2000-01-06 10:59,1.401408437 134 | 2000-01-06 11:59,1.44165518 135 | 2000-01-06 12:59,1.745179237 136 | -------------------------------------------------------------------------------- /tests/data_generation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def create_random_walk_with_outliers( 5 | n_steps, t0=0, outlier_fraction=0.1, outlier_scale=10, seed=42 6 | ): 7 | """ 8 | Generate a random walk time series with random outlier peaks. 9 | 10 | Parameters 11 | ------------ 12 | n_steps : int 13 | Length of the time series to be generated. 14 | t0 : int 15 | Time series initial value. 16 | outlier_fraction : float 17 | Fraction of outliers to be generated in series [0-1]. 18 | outlier_scale : float 19 | Scalar by which to multiply the RW increment to create an outlier. 20 | seed : int 21 | Random seed 22 | 23 | Returns 24 | ------- 25 | random_walk : np.ndarray 26 | The generated random walk time series with outliers. 27 | outlier_indices : np.ndarray 28 | The indices of the introduced outliers. 29 | """ 30 | assert 0 <= outlier_fraction <= 1 31 | n_outliers = int(outlier_fraction * n_steps) 32 | 33 | # Simulate random walk 34 | np.random.seed(seed) 35 | possible_steps = [-1, 1] 36 | random_steps = np.random.choice(a=possible_steps, size=n_steps) 37 | random_walk = np.append(t0, random_steps[:-1]).cumsum(axis=0) 38 | 39 | # Add outliers 40 | random_walk_with_outliers = random_walk.copy() 41 | outlier_indices = np.random.randint(0, n_steps, n_outliers) 42 | random_walk_with_outliers[outlier_indices] += ( 43 | random_steps[outlier_indices] * outlier_scale 44 | ) 45 | 46 | return random_walk_with_outliers, sorted(outlier_indices), random_walk 47 | -------------------------------------------------------------------------------- /tests/test_detectors.py: -------------------------------------------------------------------------------- 1 | from tsod.base import Detector 2 | import pytest 3 | import numpy as np 4 | import pandas as pd 5 | import os 6 | 7 | from tsod.custom_exceptions import WrongInputDataTypeError 8 | from tsod.detectors import ( 9 | RangeDetector, 10 | DiffDetector, 11 | CombinedDetector, 12 | RollingStandardDeviationDetector, 13 | ConstantValueDetector, 14 | ConstantGradientDetector, 15 | GradientDetector, 16 | ) 17 | 18 | from tsod.features import create_dataset 19 | from tsod.hampel import HampelDetector 20 | 21 | 22 | from tests.data_generation import create_random_walk_with_outliers 23 | 24 | 25 | @pytest.fixture 26 | def data_series(): 27 | n_steps = 100 28 | ( 29 | time_series_with_outliers, 30 | outlier_indices, 31 | random_walk, 32 | ) = create_random_walk_with_outliers(n_steps) 33 | time = pd.date_range(start="2020", periods=n_steps, freq="1h") 34 | return ( 35 | pd.Series(time_series_with_outliers, index=time), 36 | outlier_indices, 37 | pd.Series(random_walk, index=time), 38 | ) 39 | 40 | 41 | @pytest.fixture 42 | def range_data(): 43 | normal_data = np.array([0, np.nan, 1, 0, 2, np.nan, 3.14, 4]) 44 | abnormal_data = np.array([-1.0, np.nan, 2.0, np.nan, 1.0, 0.0, 4.1, 10.0]) 45 | expected_anomalies = np.array([True, False, False, False, False, False, True, True]) 46 | assert len(expected_anomalies) == len(abnormal_data) 47 | return normal_data, abnormal_data, expected_anomalies 48 | 49 | 50 | @pytest.fixture 51 | def range_data_series(range_data): 52 | normal_data, abnormal_data, expected_anomalies = range_data 53 | time = pd.date_range(start="2020", periods=len(normal_data), freq="1h") 54 | return ( 55 | pd.Series(normal_data, index=time), 56 | pd.Series(abnormal_data, index=time), 57 | expected_anomalies, 58 | ) 59 | 60 | 61 | @pytest.fixture 62 | def constant_gradient_data_series(range_data): 63 | normal_data = np.array([0, np.nan, 1, 1.1, 1.4, 1.5555, 3.14, 4]) 64 | abnormal_data = np.array([-1, 2.0, 2.1, 2.2, 2.3, 2.4, 4, 10]) 65 | expected_anomalies = np.array([False, True, True, True, True, True, False, False]) 66 | time = pd.date_range(start="2020", periods=len(normal_data), freq="1h") 67 | return ( 68 | pd.Series(normal_data, index=time), 69 | pd.Series(abnormal_data, index=time), 70 | expected_anomalies, 71 | ) 72 | 73 | 74 | @pytest.fixture 75 | def constant_data_series(range_data): 76 | normal_data = np.array([0, np.nan, 1, 1.1, 1.4, 1.5555, 3.14, 4]) 77 | abnormal_data = np.array([-1, np.nan, 1, 1, 1, 1, 4, 10]) 78 | expected_anomalies = np.array([False, False, True, True, True, True, False, False]) 79 | time = pd.date_range(start="2020", periods=len(normal_data), freq="1h") 80 | return ( 81 | pd.Series(normal_data, index=time), 82 | pd.Series(abnormal_data, index=time), 83 | expected_anomalies, 84 | ) 85 | 86 | 87 | def test_base_detector_exceptions(range_data, range_data_series): 88 | data, _, _ = range_data 89 | data_series, _, _ = range_data_series 90 | 91 | detector = RangeDetector() 92 | pytest.raises(WrongInputDataTypeError, detector.fit, data) 93 | 94 | 95 | def test_range_detector(range_data_series): 96 | data, _, _ = range_data_series 97 | 98 | detector = RangeDetector(0, 2) 99 | anomalies = detector.detect(data) 100 | expected_anomalies = [False, False, False, False, False, False, True, True] 101 | assert len(anomalies) == len(data) 102 | assert sum(anomalies) == 2 103 | assert all(expected_anomalies == anomalies) 104 | 105 | 106 | def test_range_detector_autoset(range_data_series): 107 | data, _, _ = range_data_series 108 | 109 | anomalies = RangeDetector(min_value=3).detect(data) 110 | assert sum(anomalies) == 4 111 | 112 | anomalies = RangeDetector(max_value=3).detect(data) 113 | assert sum(anomalies) == 2 114 | 115 | 116 | def test_combined_fit(range_data_series): 117 | normal_data, abnormal_data, labels = range_data_series 118 | cd = CombinedDetector([ConstantValueDetector(), RangeDetector()]) 119 | cd.fit(normal_data) 120 | 121 | anomalies = cd.detect(abnormal_data) 122 | assert all(anomalies == labels) 123 | 124 | 125 | def test_combined_wrong_type(): 126 | with pytest.raises(ValueError): 127 | CombinedDetector([ConstantValueDetector, RangeDetector()]) # 128 | 129 | 130 | def test_combined_access_items(): 131 | 132 | cd = CombinedDetector([ConstantValueDetector(), RangeDetector()]) 133 | 134 | assert isinstance(cd[0], Detector) 135 | assert isinstance(cd[0], ConstantValueDetector) 136 | assert isinstance(cd[1], RangeDetector) 137 | assert isinstance(cd[-1], RangeDetector) 138 | 139 | 140 | def test_range_detector_quantile(): 141 | np.random.seed(42) 142 | train = np.random.normal(size=1000) 143 | test = np.random.normal(size=1000) 144 | 145 | train[42] = -6.5 146 | train[560] = 10.5 147 | 148 | test[142] = -4.5 149 | test[960] = 5.5 150 | 151 | normal_data_incl_two_outliers = pd.Series(train) 152 | test_data = pd.Series(test) 153 | 154 | # all test data is within range of train data, no anomalies detected 155 | nqdetector = RangeDetector().fit(normal_data_incl_two_outliers) 156 | detected_anomalies = nqdetector.detect(test_data) 157 | assert sum(detected_anomalies) == 0 158 | 159 | # exclude extreme values 160 | detector = RangeDetector(quantiles=[0.001, 0.999]).fit( 161 | normal_data_incl_two_outliers 162 | ) 163 | detected_anomalies = detector.detect(test_data) 164 | assert sum(detected_anomalies) == 2 165 | assert detector._min > normal_data_incl_two_outliers.min() 166 | assert detector._max < normal_data_incl_two_outliers.max() 167 | 168 | 169 | def test_diff_detector_autoset(range_data_series): 170 | normal_data, abnormal_data, expected_anomalies = range_data_series 171 | 172 | detector = DiffDetector().fit(normal_data) 173 | detected_anomalies = detector.detect(abnormal_data) 174 | assert sum(detected_anomalies) == 2 175 | 176 | 177 | def test_combined_detector(): 178 | path_to_tests_super_folder = os.path.abspath(__file__).split("tests")[0] 179 | df = pd.read_csv( 180 | os.path.join(path_to_tests_super_folder, "tests", "data", "example.csv"), 181 | parse_dates=True, 182 | index_col=0, 183 | ) 184 | combined = CombinedDetector( 185 | [ 186 | ConstantValueDetector(), 187 | RangeDetector(max_value=2.0), 188 | ] 189 | ) 190 | 191 | series = df.value 192 | res = combined.detect(series) 193 | 194 | assert isinstance(res, pd.Series) 195 | 196 | 197 | def test_rollingstddev_detector(): 198 | 199 | np.random.seed(42) 200 | normal_data = pd.Series(np.random.normal(scale=1.0, size=1000)) + 10.0 * np.sin( 201 | np.linspace(0, 10, num=1000) 202 | ) 203 | abnormal_data = pd.Series(np.random.normal(scale=2.0, size=100)) 204 | 205 | all_data = pd.concat([normal_data, abnormal_data]) 206 | 207 | detector = RollingStandardDeviationDetector() 208 | anomalies = detector.detect(normal_data) 209 | assert sum(anomalies) == 0 210 | 211 | detector.fit(normal_data) 212 | anomalies = detector.detect(normal_data) 213 | assert sum(anomalies) == 0 214 | 215 | anomalies = detector.detect(all_data) 216 | assert sum(anomalies) > 0 217 | 218 | # Manual specification 219 | detector = RollingStandardDeviationDetector(max_std=2.0) 220 | anomalies = detector.detect(normal_data) 221 | assert sum(anomalies) == 0 222 | 223 | anomalies = detector.detect(all_data) 224 | assert sum(anomalies) > 0 225 | 226 | 227 | def test_hampel_detector(data_series): 228 | data_with_anomalies, expected_anomalies_indices, _ = data_series 229 | detector = HampelDetector() 230 | anomalies = detector.detect(data_with_anomalies) 231 | anomalies_indices = np.array(np.where(anomalies)).flatten() 232 | # Validate if the found anomalies are also in the expected anomaly set 233 | # NB Not necessarily all of them 234 | assert all(i in expected_anomalies_indices for i in anomalies_indices) 235 | 236 | 237 | 238 | def test_constant_value_detector(constant_data_series): 239 | good_data, abnormal_data, _ = constant_data_series 240 | 241 | detector = ConstantValueDetector(2, 0.0001) 242 | anomalies = detector.detect(good_data) 243 | 244 | assert len(anomalies) == len(good_data) 245 | assert sum(anomalies) == 0 246 | 247 | detector = ConstantValueDetector(3, 0.0001) 248 | anomalies = detector.detect(abnormal_data) 249 | 250 | assert len(anomalies) == len(abnormal_data) 251 | assert sum(anomalies) == 4 252 | 253 | 254 | def test_constant_gradient_detector(constant_gradient_data_series): 255 | good_data, abnormal_data, _ = constant_gradient_data_series 256 | 257 | detector = ConstantGradientDetector(3) 258 | anomalies = detector.detect(good_data) 259 | 260 | assert len(anomalies) == len(good_data) 261 | assert sum(anomalies) == 0 262 | 263 | detector = ConstantGradientDetector(3) 264 | anomalies = detector.detect(abnormal_data) 265 | 266 | assert len(anomalies) == len(abnormal_data) 267 | assert sum(anomalies) == 5 268 | 269 | 270 | def test_gradient_detector_constant_gradient(constant_gradient_data_series): 271 | good_data, _, _ = constant_gradient_data_series 272 | 273 | detector = GradientDetector(1.0) 274 | anomalies = detector.detect(good_data) 275 | 276 | assert len(anomalies) == len(good_data) 277 | assert sum(anomalies) == 0 278 | 279 | 280 | def test_gradient_detector_sudden_jump(): 281 | 282 | normal_data = np.array( 283 | [ 284 | -0.5, 285 | -0.6, 286 | 0.6, 287 | 0.6, 288 | 0.1, 289 | 0.6, 290 | 0.4, 291 | 0.8, 292 | 0.7, 293 | 1.5, 294 | 1.6, 295 | 1.1, 296 | 0.3, 297 | 2.1, 298 | 0.7, 299 | 0.3, 300 | -1.7, 301 | -0.3, 302 | 0.0, 303 | -1.0, 304 | ] 305 | ) 306 | abnormal_data = np.array( 307 | [ 308 | -0.5, 309 | -1.5, 310 | 1.5, 311 | 0.6, 312 | 0.1, 313 | 0.6, 314 | 0.4, 315 | 0.8, 316 | 0.7, 317 | 1.5, 318 | 1.6, 319 | 1.1, 320 | 0.3, 321 | 2.1, 322 | 0.7, 323 | 0.3, 324 | -1.7, 325 | -0.3, 326 | 0.0, 327 | -1.0, 328 | ] 329 | ) 330 | 331 | expected_anomalies = np.repeat(False, len(normal_data)) 332 | expected_anomalies[2] = True 333 | time = pd.date_range(start="2020", periods=len(normal_data), freq="1h") 334 | 335 | normal_data = pd.Series(normal_data, index=time) 336 | abnormal_data = pd.Series(abnormal_data, index=time) 337 | 338 | detector = GradientDetector() 339 | 340 | anomalies = detector.detect(normal_data) 341 | assert sum(anomalies) == 0 342 | 343 | # Default is to accept any gradient 344 | anomalies = detector.detect(abnormal_data) 345 | assert sum(anomalies) == 0 346 | 347 | # Max gradient 2.0/h 348 | detector.fit(normal_data) 349 | anomalies = detector.detect(abnormal_data) 350 | 351 | assert sum(anomalies) == 1 352 | 353 | 354 | def test_create_dataset(data_series): 355 | data_with_anomalies, _, _ = data_series 356 | data_with_anomalies.name = "y" 357 | data = data_with_anomalies.to_frame() 358 | time_steps = 2 359 | predictors, y = create_dataset(data[["y"]], data.y, time_steps) 360 | assert len(y) == len(data) - time_steps 361 | assert predictors.shape[0] == len(data) - time_steps 362 | assert predictors.shape[1] == time_steps 363 | -------------------------------------------------------------------------------- /tests/test_persistence.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tsod 3 | from tsod import RangeDetector, ConstantValueDetector, CombinedDetector 4 | 5 | 6 | def test_save_and_load(tmp_path): 7 | 8 | combined = CombinedDetector( 9 | [ 10 | ConstantValueDetector(), 11 | RangeDetector(max_value=2.0), 12 | ] 13 | ) 14 | 15 | path = tmp_path / "combined.joblib" 16 | combined.save(path) 17 | 18 | loaded = tsod.load(path) 19 | 20 | assert isinstance(loaded, CombinedDetector) 21 | 22 | 23 | def test_load(): 24 | path_to_tests_super_folder = os.path.abspath(__file__).split("tests")[0] 25 | filename = os.path.join( 26 | path_to_tests_super_folder, "tests", "data", "combined.joblib" 27 | ) 28 | 29 | loaded = tsod.load(filename) 30 | 31 | assert isinstance(loaded, CombinedDetector) 32 | 33 | 34 | def test_save_and_load_filename(tmpdir): 35 | 36 | combined = CombinedDetector( 37 | [ 38 | ConstantValueDetector(), 39 | RangeDetector(max_value=2.0), 40 | ] 41 | ) 42 | 43 | filename = os.path.join(tmpdir, "combined.joblib") 44 | combined.save(filename) 45 | 46 | loaded = tsod.load(filename) 47 | 48 | assert isinstance(loaded, CombinedDetector) 49 | -------------------------------------------------------------------------------- /tsod/__init__.py: -------------------------------------------------------------------------------- 1 | from .detectors import ( 2 | RangeDetector, 3 | DiffDetector, 4 | ConstantGradientDetector, 5 | GradientDetector, 6 | ConstantValueDetector, 7 | CombinedDetector, 8 | RollingStandardDeviationDetector, 9 | ) 10 | 11 | 12 | from .base import load 13 | 14 | __version__ = "0.2.0" 15 | 16 | __all__ = [ 17 | "RangeDetector", 18 | "DiffDetector", 19 | "ConstantGradientDetector", 20 | "GradientDetector", 21 | "ConstantValueDetector", 22 | "CombinedDetector", 23 | "RollingStandardDeviationDetector", 24 | "load", 25 | ] 26 | -------------------------------------------------------------------------------- /tsod/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Union 3 | 4 | from pathlib import Path 5 | import joblib 6 | 7 | import pandas as pd 8 | 9 | 10 | from .custom_exceptions import WrongInputDataTypeError 11 | 12 | 13 | def load(path: Union[str, Path]): 14 | """Load a saved model from disk saved with `Detector.save` 15 | 16 | Parameters 17 | ========== 18 | path: str or Path 19 | file-like object to load detector from 20 | """ 21 | 22 | return joblib.load(path) 23 | 24 | 25 | class Detector(ABC): 26 | """Abstract base class for all detectors""" 27 | 28 | def __init__(self): 29 | pass 30 | 31 | def fit(self, data: pd.Series): 32 | """Set detector parameters based on data. 33 | 34 | Parameters 35 | ---------- 36 | data: pd.Series 37 | Normal time series data. 38 | """ 39 | data = self.validate(data) 40 | self._fit(data) 41 | return self 42 | 43 | def _fit(self, data: pd.Series): 44 | # Default implementation is a NoOp 45 | return self 46 | 47 | def detect(self, data: pd.Series) -> pd.Series: 48 | """Detect anomalies 49 | 50 | Parameters 51 | ---------- 52 | data: pd.Series 53 | Time series data with possible anomalies 54 | 55 | Returns 56 | ------- 57 | pd.Series 58 | Time series with bools, True == anomaly 59 | """ 60 | data = self.validate(data) 61 | 62 | pred = self._detect(data) 63 | return self._postprocess(pred) 64 | 65 | def _postprocess(self, pred: pd.Series) -> pd.Series: 66 | # TODO implement 67 | return pred 68 | 69 | @abstractmethod 70 | def _detect(self, data: pd.Series) -> pd.Series: 71 | """Detect anomalies""" 72 | pass 73 | 74 | def validate( 75 | self, data: Union[pd.Series, pd.DataFrame] 76 | ) -> Union[pd.Series, pd.DataFrame]: 77 | """Check that input data is in correct format and possibly adjust""" 78 | if not (isinstance(data, pd.Series) or isinstance(data, pd.DataFrame)): 79 | raise WrongInputDataTypeError() 80 | return data 81 | 82 | def _gradient( 83 | self, data: Union[pd.Series, pd.DataFrame], periods: int = 1 84 | ) -> pd.Series: 85 | dt = data.index.to_series().diff().dt.total_seconds() 86 | if dt.min() < 1e-15: 87 | raise ValueError("Index must be monotonically increasing") 88 | 89 | gradient = data.diff(periods=periods) / dt 90 | return gradient 91 | 92 | def __str__(self): 93 | return f"{self.__class__.__name__}" 94 | 95 | def save(self, path: Union[str, Path]) -> None: 96 | """Save a detector for later use 97 | 98 | Parameters 99 | ========== 100 | path: str or Path 101 | file-like object to load detector from 102 | """ 103 | 104 | joblib.dump(self, path) 105 | -------------------------------------------------------------------------------- /tsod/custom_exceptions.py: -------------------------------------------------------------------------------- 1 | class WrongInputDataTypeError(Exception): 2 | def __init__(self, message="Input data must be a pandas.Series."): 3 | self.message = message 4 | super().__init__(self.message) 5 | 6 | 7 | class NotFittedError(Exception): 8 | def __init__(self, message="Please call fit() before detect().", tip=""): 9 | self.message = " ".join([message, tip]) 10 | super().__init__(self.message) 11 | 12 | 13 | class NoRangeDefinedError(NotFittedError): 14 | def __init__( 15 | self, message="Or specify min/max range when instantiating detector object." 16 | ): 17 | super().__init__(message) 18 | 19 | 20 | class InvalidArgumentError(Exception): 21 | def __init__(self, argument_name, requirement): 22 | self.message = f"{argument_name} must be {requirement}." 23 | super().__init__(self.message) 24 | 25 | 26 | class NotIntegerError(InvalidArgumentError): 27 | def __init__(self, argument_name): 28 | super().__init__(argument_name, "an integer") 29 | 30 | 31 | class NonUniqueTimeStampsError(Exception): 32 | def __init__(self, message="Found multiple values at the same time stamp."): 33 | self.message = message 34 | super().__init__(self.message) 35 | 36 | 37 | class WrongInputSizeError(ValueError): 38 | pass 39 | -------------------------------------------------------------------------------- /tsod/detectors.py: -------------------------------------------------------------------------------- 1 | """Simple univariate anomaly detectors""" 2 | 3 | from collections.abc import Sequence 4 | import pandas as pd 5 | import numpy as np 6 | 7 | from .base import Detector 8 | 9 | 10 | class CombinedDetector(Detector, Sequence): 11 | """Combine detectors. 12 | 13 | It is possible to combine several anomaly detection strategies into a combined detector. 14 | 15 | Examples 16 | -------- 17 | >>> normal_data = pd.Series(np.random.normal(size=100)) 18 | >>> abnormal_data = pd.Series(np.random.normal(size=100)) 19 | >>> abnormal_data[[2, 6, 15, 57, 60, 73]] = 5 20 | 21 | >>> anomaly_detector = CombinedDetector([RangeDetector(), DiffDetector()]) 22 | >>> anomaly_detector.fit(normal_data) 23 | >>> detected_anomalies = anomaly_detector.detect(abnormal_data) 24 | """ 25 | 26 | def __init__(self, detectors): 27 | super().__init__() 28 | 29 | for detector in detectors: 30 | if not isinstance(detector, Detector): 31 | raise ValueError( 32 | f"""{detector} is not a Detector. 33 | Did you forget to create an instance, e.g. ConstantValueDetector()?""" 34 | ) 35 | 36 | self._detectors = detectors 37 | 38 | def _fit(self, data): 39 | for detector in self._detectors: 40 | detector.fit(data) 41 | return self 42 | 43 | def _detect(self, data: pd.Series) -> pd.Series: 44 | all_anomalies = [] 45 | for detector in self._detectors: 46 | anom = detector.detect(data) 47 | all_anomalies.append(anom) 48 | data_frame = pd.DataFrame(all_anomalies).T 49 | return data_frame.any(axis=1) 50 | 51 | def __getitem__(self, index): 52 | return self._detectors[index] 53 | 54 | def __len__(self): 55 | return len(self._detectors) 56 | 57 | 58 | class RangeDetector(Detector): 59 | """ 60 | Detect values outside range. 61 | 62 | Parameters 63 | ---------- 64 | min_value : float 65 | Minimum value threshold. 66 | max_value : float 67 | Maximum value threshold. 68 | quantiles : list[2] 69 | Default quantiles [0, 1]. Same as min and max value. 70 | 71 | Examples 72 | --------- 73 | >>> normal_data = pd.Series(np.random.normal(size=100)) 74 | >>> abnormal_data = pd.Series(np.random.normal(size=100)) 75 | >>> abnormal_data[[2, 6, 15, 57, 60, 73]] = 5 76 | >>> normal_data_with_some_outliers = pd.Series(np.random.normal(size=100)) 77 | >>> normal_data_with_some_outliers[[12, 13, 20, 90]] = 7 78 | 79 | >>> detector = RangeDetector(min_value=0.0, max_value=2.0) 80 | >>> anomalies = detector.detect(abnormal_data) 81 | 82 | >>> detector = RangeDetector() 83 | >>> detector.fit(normal_data) # min, max inferred from normal data 84 | >>> anomalies = detector.detect(abnormal_data) 85 | 86 | >>> detector = RangeDetector(quantiles=[0.001,0.999]) 87 | >>> detector.fit(normal_data_with_some_outliers) 88 | >>> anomalies = detector.detect(abnormal_data)""" 89 | 90 | def __init__(self, min_value=-np.inf, max_value=np.inf, quantiles=None): 91 | super().__init__() 92 | 93 | self._min = min_value 94 | 95 | self._max = max_value 96 | 97 | if quantiles is None: 98 | self._quantiles = [0.0, 1.0] 99 | else: 100 | assert 0.0 <= quantiles[0] <= 1.0 101 | assert 0.0 <= quantiles[1] <= 1.0 102 | self._quantiles = quantiles 103 | 104 | def _fit(self, data): 105 | """Set min and max based on data. 106 | 107 | Parameters 108 | ---------- 109 | data : pd.Series 110 | Normal time series data. 111 | """ 112 | super().validate(data) 113 | 114 | quantiles = np.nanquantile(data, self._quantiles) 115 | self._min = quantiles.min() 116 | self._max = quantiles.max() 117 | 118 | assert self._max >= self._min 119 | return self 120 | 121 | def _detect(self, data: pd.Series) -> pd.Series: 122 | """Detect anomalies outside range""" 123 | 124 | if self._max is None: 125 | return data < self._min 126 | 127 | if self._min is None: 128 | return data > self._max 129 | 130 | return (data < self._min) | (data > self._max) 131 | 132 | def __str__(self): 133 | return f"{super.__str__(self)}{self._min}, {self._max})" 134 | 135 | def __repr__(self): 136 | return f"{self.__class__.__name__}(min: {self._min:.1e}, max: {self._max:.1e})" 137 | 138 | 139 | class DiffDetector(Detector): 140 | """Detect sudden shifts in data. Irrespective of time axis. 141 | 142 | Parameters 143 | ---------- 144 | max_diff : float 145 | Maximum change threshold. 146 | direction: str 147 | positive, negative or both, default='both' 148 | 149 | See also 150 | -------- 151 | GradientDetector: similar functionality but considers actual time between data points 152 | """ 153 | 154 | def __init__(self, max_diff=np.inf, direction="both"): 155 | super().__init__() 156 | self._max_diff = max_diff 157 | 158 | valid_directions = ("both", "positive", "negative") 159 | if direction in valid_directions: 160 | self._direction = direction 161 | else: 162 | raise ValueError( 163 | f"Selected direction, '{direction}' is not a valid direction. Valid directions are: {valid_directions}" 164 | ) 165 | 166 | def _fit(self, data): 167 | data_diff = data.diff() 168 | 169 | self._max_diff = data_diff.max() 170 | return self 171 | 172 | def _detect(self, data: pd.Series) -> pd.Series: 173 | if self._direction == "both": 174 | return np.abs(data.diff()) > self._max_diff 175 | elif self._direction == "positive": 176 | return data.diff() > self._max_diff 177 | else: 178 | return data.diff() < -self._max_diff 179 | 180 | def __str__(self): 181 | return ( 182 | f"{self.__class__.__name__}({self._max_diff}, direction:{self._direction})" 183 | ) 184 | 185 | 186 | class RollingStandardDeviationDetector(Detector): 187 | """Detect large variations 188 | 189 | 190 | ---------- 191 | window_size: int 192 | Number of data points to evaluate over, default=10 193 | max_std: float 194 | Maximum standard deviation to accept as normal, default=np.inf 195 | center: bool 196 | Center rolling window, default=True 197 | """ 198 | 199 | def __init__(self, window_size=10, max_std=np.inf, center=True): 200 | super().__init__() 201 | self._window_size = window_size 202 | self._max_std = max_std 203 | self._center = center 204 | 205 | def _fit(self, data): 206 | self._max_std = data.rolling(self._window_size).std().max() 207 | 208 | return self 209 | 210 | def _detect(self, data: pd.Series) -> pd.Series: 211 | anomalies = ( 212 | data.rolling(self._window_size, center=self._center).std() > self._max_std 213 | ) 214 | # anomalies = anomalies.astype(int).diff() > 0 # only take positive edges 215 | anomalies[0] = False # first element cannot be determined by diff 216 | return anomalies 217 | 218 | def __str__(self): 219 | return f"{self.__class__.__name__}(window_size:{self._window_size}, max_std:{self._max_std})" 220 | 221 | 222 | class ConstantValueDetector(Detector): 223 | """ 224 | Detect constant values over a longer period. 225 | 226 | Commonly caused by sensor failures, which get stuck at a constant level. 227 | """ 228 | 229 | def __init__(self, window_size: int = 3, threshold: float = 1e-7): 230 | super().__init__() 231 | self._threshold = threshold 232 | self._window_size = window_size 233 | 234 | def _fit(self, data): 235 | return self 236 | 237 | def _detect(self, data: pd.Series) -> pd.Series: 238 | rollmax = data.rolling(self._window_size, center=True).apply(np.nanmax) 239 | rollmin = data.rolling(self._window_size, center=True).apply(np.nanmin) 240 | anomalies = np.abs(rollmax - rollmin) < self._threshold 241 | anomalies.iloc[0] = False # first element cannot be determined 242 | anomalies.iloc[-1] = False 243 | idx = np.where(anomalies)[0] 244 | if idx is not None: 245 | # assuming window size = 3 246 | # remove also points before and after each detected anomaly 247 | anomalies.iloc[idx[idx > 0] - 1] = True 248 | maxidx = len(anomalies) - 1 249 | anomalies.iloc[idx[idx < maxidx] + 1] = True 250 | 251 | return anomalies 252 | 253 | def __str__(self): 254 | return f"{self.__class__.__name__}({self._window_size}, {self._threshold})" 255 | 256 | 257 | class ConstantGradientDetector(ConstantValueDetector): 258 | """Detect constant gradients. 259 | 260 | Typically caused by linear interpolation over a long interval. 261 | 262 | Parameters 263 | ========== 264 | window_size: int 265 | Minium window to consider as anomaly, default 3 266 | """ 267 | 268 | def __init__(self, window_size: int = 3): 269 | super().__init__(window_size=window_size) 270 | 271 | def _detect(self, data: pd.Series) -> pd.Series: 272 | gradient = self._gradient(data, periods=1) 273 | s1 = super()._detect(gradient) 274 | gradient = self._gradient(data, periods=-1) 275 | s2 = super()._detect(gradient) 276 | return s1 | s2 277 | 278 | def __str__(self): 279 | return f"{self.__class__.__name__}({self._window_size})" 280 | 281 | 282 | class GradientDetector(Detector): 283 | """Detects abrupt changes 284 | 285 | Parameters 286 | ========== 287 | max_gradient: float 288 | Maximum rate of change per second, default np.inf 289 | direction: str 290 | positive, negative or both, default='both' 291 | """ 292 | 293 | def __init__(self, max_gradient=np.inf, direction="both"): 294 | super().__init__() 295 | self._max_gradient = max_gradient 296 | valid_directions = ("both", "positive", "negative") 297 | if direction in valid_directions: 298 | self._direction = direction 299 | else: 300 | raise ValueError( 301 | f"""Selected direction, '{direction}' is not a valid direction. 302 | Valid directions are: {valid_directions}""" 303 | ) 304 | 305 | def _fit(self, data: pd.Series): 306 | """Set max gradient based on data.""" 307 | 308 | self._max_gradient = np.max(np.abs(self._gradient(data))) 309 | return self 310 | 311 | def _detect(self, data: pd.Series) -> pd.Series: 312 | gradient = self._gradient(data) 313 | if self._direction == "negative": 314 | return gradient < -self._max_gradient 315 | elif self._direction == "positive": 316 | return gradient > self._max_gradient 317 | else: 318 | return np.abs(gradient) > self._max_gradient 319 | 320 | def __str__(self): 321 | max_grad_hr = self._max_gradient * 3600.0 322 | return ( 323 | f"{self.__class__.__name__}({max_grad_hr}/hr, direction:{self._direction})" 324 | ) 325 | -------------------------------------------------------------------------------- /tsod/features.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | 4 | 5 | def lag_time_series(time_series: pd.Series, lags): 6 | """Create lagged time series features. 7 | 8 | Parameters 9 | ---------- 10 | time_series : pd.Series 11 | lags : list[int] 12 | List of lags 13 | 14 | Returns 15 | ------- 16 | pd.DataFrame 17 | Lagged time series features. 18 | """ 19 | lagged_time_series = {} 20 | for lag in lags: 21 | lagged_time_series[str(lag)] = time_series.shift(lag) 22 | 23 | return pd.concat(lagged_time_series, axis=1) 24 | 25 | 26 | def create_dataset(X, y, time_steps=1): 27 | Xs, ys = [], [] 28 | for i in range(len(X) - time_steps): 29 | v = X.iloc[i : (i + time_steps)].values 30 | Xs.append(v) 31 | ys.append(y.iloc[i + time_steps]) 32 | return np.array(Xs), np.array(ys) 33 | -------------------------------------------------------------------------------- /tsod/hampel.py: -------------------------------------------------------------------------------- 1 | """Hampel detector""" 2 | 3 | import numpy as np 4 | from numba import jit 5 | 6 | from tsod.custom_exceptions import NotIntegerError, InvalidArgumentError 7 | from tsod.detectors import Detector 8 | 9 | 10 | # GAUSSIAN_SCALE_FACTOR = k = 1/Phi^(-1)(3/4) 11 | # Choosing 3/4 as argument makes +-MAD cover 50% of the standard normal cumulative distribution function. 12 | 13 | GAUSSIAN_SCALE_FACTOR = 1.4826 14 | 15 | 16 | def _validate_arguments(window_size, threshold): 17 | if not isinstance(window_size, int): 18 | raise NotIntegerError("window_size") 19 | else: 20 | if window_size <= 0: 21 | raise InvalidArgumentError("window_size", "nonnegative") 22 | 23 | if threshold < 0: 24 | raise InvalidArgumentError("threshold", "positive") 25 | 26 | 27 | @jit(nopython=True) 28 | def _detect(time_series, window_size, threshold=3, k=GAUSSIAN_SCALE_FACTOR): 29 | """ 30 | Hampel filter implementation that works on numpy arrays, implemented with numba. 31 | 32 | Parameters 33 | ---------- 34 | time_series: numpy.ndarray 35 | window_size: int 36 | The window range is from [(i - window_size):(i + window_size)], so window_size is half of the 37 | window, counted in number of array elements (as opposed to specify a time span, which is not 38 | supported by this implementation) 39 | threshold: float 40 | The threshold for marking an outlier. A low threshold "narrows" the band within which values are deemed as 41 | outliers. n_sigmas 42 | k : float 43 | Constant scale factor dependent on distribution. Default is normal distribution. 44 | """ 45 | 46 | # time_series_clean = time_series.copy() 47 | # outlier_indices = [] 48 | is_outlier = [False] * len(time_series) 49 | 50 | for t in range(window_size, (len(time_series) - window_size)): 51 | time_series_window = time_series[(t - window_size) : (t + window_size)] 52 | median_in_window = np.nanmedian(time_series_window) 53 | mad_in_window = k * np.nanmedian(np.abs(time_series_window - median_in_window)) 54 | absolute_deviation_from_median = np.abs(time_series[t] - median_in_window) 55 | is_outlier[t] = absolute_deviation_from_median > threshold * mad_in_window 56 | # if is_outlier[t]: 57 | # outlier_indices.append(t) 58 | # time_series_clean[t] = median_in_window 59 | 60 | return is_outlier 61 | 62 | 63 | class HampelDetector(Detector): 64 | """ 65 | Hampel filter implementation that works on numpy arrays, implemented with numba. 66 | 67 | Parameters 68 | ---------- 69 | window_size: int 70 | The window range is from [(i - window_size):(i + window_size)], so window_size is half of the 71 | window, counted in number of array elements (as opposed to specify a time span, which is not 72 | supported by this implementation) 73 | threshold: float 74 | The threshold for marking an outlier. A low threshold "narrows" the band within which values are deemed as 75 | outliers. n_sigmas, default=3.0 76 | """ 77 | 78 | def __init__(self, window_size=5, threshold=3): 79 | super().__init__() 80 | _validate_arguments(window_size, threshold) 81 | self._threshold = threshold 82 | self._window_size = window_size 83 | 84 | def _detect(self, data): 85 | anomalies = _detect(data.values, self._window_size, self._threshold) 86 | 87 | return anomalies 88 | 89 | def __str__(self): 90 | return f"{self.__class__.__name__}({self._window_size}, {self._threshold})" 91 | --------------------------------------------------------------------------------