├── .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 | [](https://github.com/DHI/tsod/actions/workflows/test.yml)
6 | [](https://badge.fury.io/py/tsod)
7 | 
8 |
9 | 
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) [](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 |
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": "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RakkRBq5Jovu3buzaNEi9uzZQ15eHt26daOgoIB58+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": "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\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++HYC3336b7t2707lzZ2644QZKS0spLS1l+PDhdOjQgY4dO/LMM8/ErQM7tCHWaDQajcH0h6D4kP+x4kPG8Th5/fXXWbhwIQUFBUyYMIEdO3Zw4MABevTowZIlS+jTpw+vvPIKALfddhtXXnklS5cu5dJLL2XEiBHefHbt2sW8efN45plnOOecc7jjjjtYvnw5P//8M4sXLwaw3FbRjl9++YUpU6Ywd+5cFi9eTEZGBpMnT2bx4sX8+eefLFu2jJ9//jmh85C1IdZoNBqNwZ6N0R2PggkTJnh7vhs2bGDVqlVkZ2czaNAgwNgqcd26dQDMmzePYcOGAXD55ZczZ84cbz6DBw9GROjYsSMNGzakY8eOuFwu2rdv770+Pz+fLl26cPzxx7N8+XJWrFhhK9f06dNZuHAhJ5xwAp07d2b69OmsWbOGli1bsmbNGm677TamTZtGzZo149aBHXqMWKPRaDQGtZqYbmmL43Hw7bff8s033zBv3jyqVq1Kv379OHz4MFlZWYgIABkZGZSUlITNKycnBwCXy+X92/N/SUkJa9eujWhbRQ9KKa688koee+yxoHNLlizhyy+/ZOLEieTn5/P6669H++gRoXvEGo1GozHoPwayqvgfy6piHI+DPXv2UKdOHapWrcrKlSv54YcfQqY/6aSTeO+99wCYPHkyvXv3jvheobZVtKJ///5MnTqVv/76C4CdO3fyxx9/sH37dtxuNxdccAHjxo1j0aJFEcsQLbpHrNFoNBoDT3S0w1HTAwYMYOLEibRt25bWrVvTo0ePkOmff/55rrrqKp544gkaNGjAG2+8EfG9fLdVbNq0qd+2ila0a9eOcePGccYZZ+B2u8nKyuKFF16gSpUqXHXVVbjdbgDLHrNT6G0Q40RvVadJFPrb0jiB3gYx+ehtEDWaisDSfOZkj2BNzjB4poMj00c0Gk16ol3TGk26Yc7lbOIyp5F45nJChVxYQaOp7OgesUaTbiRwLqemcpKKIcjKSiy61j3iOPnhnv5kuCTVYmgqEgmcy6mpfOTm5rJjxw7q1avnnSqkSQxKKXbs2EFubm5U12lDHCdH1opO4RpNWBI0l1NTOWnSpAkbN25k27ZtqRalUpCbm0uTJtGVVW2INZp0o/8YY0zY1z3twFxOTeUkKyuLFi1apFoMTQj0GLFGk27kDYHBE6BWU0CM34Mn6EAtjaaConvEGk06kjdEG16NppKge8QajUaj0aSQlKysJSLbgD8iTF4f2J5AccojWifWaL0Eo3VijdZLMFon1jill6OVUg2sTqTEEEeDiBTYLQtWWdE6sUbrJRitE2u0XoLROrEmGXrRrmmNRqPRaFKINsQajUaj0aSQ8mCIX061AGmI1ok1Wi/BaJ1Yo/USjNaJNQnXS9qPEWs0Go1GU5EpDz1ijUaj0WgqLNoQazQajUaTQrQh1mjKOaK31NFoyjVpYYhF5Ejzt65QTESkvYjorZ0CEJGTRaRVquVIM6qkWoB0REQyzN+6XjHROrFHRFJmD1NqiEXkeBGZDjwMoHTkGCKSJyJzgHFAvVTLky6ISBcR+QqYAdRKtTzpgIj0EJH3gRdE5AxPJVvZEZGeIvIKcIeI1ND1ircB+x/gPhGpq3ViICLdRWQEgFLKnSo5UmKIxeAZ4E3gP0qp61IhR5pyHzBVKfU3pdSfULlbryKSJSL/xphCMAH4EuhnnksLj04qEJF+wIvAB8CvwGVAnRSKlBaISF/gXxgNtsbAvSJyZmqlSi0i0hLjW5kJHA08LCIDUytV6hGRvwMfYjROzjKPpaQxm5KKzGyNVQd+Ukq9CSAirSp5xeoyC8x+pdSz5rHTRaQ2UJndSTnALKC3UupT4H2grYhkprIFmwZ0BBYopSYDbwFZwP7UipQWdAHmKqXexfC0NQQu9gx/VVJOAH5RSk0C7gIWA4NEpGkqhUoD1gCDgJuAewCUUqWpqGeTZvhMN9pxPofuAk4UkftFZC7wBDBJRLomS6ZU46sT06hsB3qLyEAR+R/wfxi9wLvNNJXCnRTwrRxQSr2jlDpk/p8JlCqlSipTw82i/MwGLhKRMcAioBHwoohclBIBU4SFXn4DaotII6XULozGSTZwXirkSwUiMlhEbhWRHuahBUBTEWlq6mQusBs4P1UypgILvXwGLDV/7/e4qDE7Pskk4RWZiNQWkc+Ar4EhIlINQCm1F3gBuBCjNXIJsBm4QEQsd6ioKITRyRsYLfnXlVJnAq8CPXw+ngqLlV6UUsocyvB8q7OAv4lIncrQI7bQSXUApdRiYADQHLhZKdUPo4IdICJtUyNt8rDTC4Yh3gv8xxw/bwr8hOGBq9BeJRFpJCKfACMxhineEJEzlVJrgHmAZ4PrX4EVQN3KEBAaQi+lgFspdRh4CrhGROorpUqSLWMyehTVMMb1bjP/7uM5oZSaAPRTSn2nlCoE/gd0Aw4mQa5UYqsT4FOMytUz3lcAbAUKkyhfqrDUizJwm8Z4nZmmb6qETDKBOuntOaGUmg80wNAJGOOiNYADyRUxJdh9K6uAO4HHgP8qpf4GLMeMK6jgXqVuwGylVG+l1MPAc4An/mY20FFEupsG6E/gZNMIVXQC9fIscCP4fQ/fAj9gfE+ISPdkCpgQQywiV4hIXxGpaQYcvQzkA4cx3NGNPWlNV4mHrsBGoDQRcqWSCHRyFIBSaimGK/pWEamPEYTTAdiRItETSqTfioiI2QPOMS897DmeCrkTSRQ6yQG+B24xL+2PEWlfISvXMHrp7tGLUqpIKTVTKfWeeWlXYFpqpE4spk76md/CdIx4AQ87gFXm3z9ieAaeMb0H7YH1IlI1qQIniTB62Qn8YqZzgTE2jDFT5R8isgfoksy6xTFDbLoPG4nITOBK4FLgJbOrf1gpdRD4BqOnd6rPdTmmwgqAM4HxFaWVFqtOlFKvAe8CDwAXANcqpdYn/QESRCx6MV3UGUqpAxjfbQ/P8dQ8hbNEqZP+AKYX6WOguoh8hzG8c6tS6q/UPIXzxFqGzGt7ichCDC/Cp8mWPVFY6GQY8DpQVSm1WUSyzKSNMD1rSqktSqnnMAzy6xgN/H+a+qsQxKgXt3ndMcA7GMM7vZRSE5Natyil4v4BMszfxwFve44BzwMfBKS9A6PlUQuoYh47CTjPCVnS5ScOndTwOZ6V6udII71U9TmenernSAOd1PYpP1WAlql+jjTRSy2gmnmsMXB2qp8j2TrxSfMJcJr59xHm70zfOqai/MShl7oe/QCnpEr+TOJAjDlXDwMZIvI5UBPTrayMMPDbgU0i0lcpNcu87BWzwHwDNBOR45VS38cjRzoRp06+Bo42dbJJKVWcgkdICA7rpSgFj+A4DuikmYh0UYabdk3ynyAxOPStdFVKbQQ2Jf8JnCdanYhINrAN+E1EHsGYrtRPGUOB+1L0GI7jkF5OUYYXKWWepJhd02JMnF+I0cVfjaGMYuAUMQe6lTGm94D542EgcDPGXLaOSqkKUVDAEZ0soYLpBLRerHCw/PyZNKGTgIPfysbkSZ1YotTJg+ZlucBwjPHRGhg9wF1UIBzUy86kCm5BPD1iN/CUUuotMJarBFoAY4CXgK5iDIT/DzhVRJorpdZhBFacppT6Lh7B0xStE2u0XoLROrFG6yWYaHXSBMMt/zbwtDKmulVEKoxe4gnWWgjkS9mSYHOBZspYvSVDRG4zWyNNMBZgWAeglPqoghYW0DqxQ+slGK0Ta7RegolGJ26l1Eal1Hyl1BXpZGwSQIXRS8yGWCl1UClVqIywb4DTMXzvAFdhLEP4KUb07yKomFNNfNE6sUbrJRitE2u0XoKJUicLoeLrBCqWXuIK1gLvYLnCWNP1Y/PwPuBejPmvaz3jWEqpCjHVJBxaJ9ZovQSjdWKN1kswWifWVAS9ODGP2I2x4Px2IM9sgdyP4QqYU9GCSSJE68QarZdgtE6s0XoJRuvEmnKvF3GigSDGOsjfmz9vKGNBikqN1ok1Wi/BaJ1Yo/USjNaJNeVdL04Z4ibA5RiRaJVhTeSwaJ1Yo/USjNaJNVovwWidWFPe9eKIIdZoNBqNRhMblWY/V41Go9Fo0hFtiDUajUajSSHaEGs0Go1Gk0K0IdZoNBqNJoVoQ6zRaDQaTQrRhlij0Wg0mhSiDbFGo9FoNCnk/wHFkcZWFZvtTAAAAABJRU5ErkJggg==\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 |
--------------------------------------------------------------------------------