diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..4f43e04 --- /dev/null +++ b/.flake8 @@ -0,0 +1,41 @@ +# Unfortunately, flake8 does not support pyproject.toml configuration. +# https://github.com/PyCQA/flake8/issues/234 +[flake8] +per-file-ignores = + __init__.py:F401 +show-source = True +count= True +statistics = True +# https://www.flake8rules.com +# E203 = Whitespace before ‘:' +# E265 = comment blocks like @{ section, which it can't handle +# E266 = too many leading '#' for block comment +# E731 = do not assign a lambda expression, use a def +# W293 = Blank line contains whitespace +# W503 = Line break before binary operator +# E704 = multiple statements in one line - used for @override +# TC002 = move third party import to TYPE_CHECKING +# ANN = flake8-annotations +# TC, TC2 = flake8-type-checking +# B = flake8-bugbear +# S = flake8-bandit +# D = flake8-docstrings +# S = flake8-bandit +# F are errors reported by pyflakes +# E and W are warnings and errors reported by pycodestyle +# C are violations reported by mccabe +# BLK = flake8-black +# DAR = darglint +# SC = flake8-spellcheck +ignore = E203, E211, E265, E501, E999, F401, F821, W503, W505, SC100, SC200, C400, C401, C402, B008, E800, E741, F403, F405, C901, B028, E226 +max-line-length = 120 +max-doc-length = 120 +import-order-style = google +docstring-convention = google +inline-quotes = " +strictness=short +dictionaries=en_US,python,technical,pandas +min-python-version = 3.7.0 +exclude = .git,.tox,.nox,venv,.venv,.venv-docs,.venv-dev,.venv-note,.venv-dempy,docs,test +max-complexity = 10 +#spellcheck-targets=comments diff --git a/.gitignore b/.gitignore index 1d3b9e2..f63985f 100644 --- a/.gitignore +++ b/.gitignore @@ -121,6 +121,4 @@ notebooks/CorrelationReport.html notebooks/orange_input_test.csv notebooks/orange_input_train.csv notebooks/UCI HAR Dataset.zip - - - +tests/datasets/cached_datasets/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..9b32ae5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,338 @@ +--- +########################################################################################## +# # +# Pre-commit configuration file # +# # +# # +# See https://pre-commit.com for more information # +# See https://pre-commit.com/hooks.html for more hooks # +# # +# To install the git pre-commit hook run: # +# pre-commit install # +# pre-commit autoupdate # +# To update the pre-commit hooks run: # +# pre-commit install --install-hooks -t pre-commit -t commit-msg # +# To run all hooks against current changes in your repository # +# pre-commit run --all-files # +# If you wish to execute an individual hook use pre-commit run . Example: # +# pre-commit run black # +# # +########################################################################################## +default_language_version: + python: python3 +default_stages: [commit, push] +fail_fast: false +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: fix-byte-order-marker + name: fix-byte-order-marker + description: removes UTF-8 byte order marker + - id: trailing-whitespace + name: trailing-whitespace + description: Trims trailing whitespace + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer + name: end-of-file-fixer + description: Makes sure files end in a newline and only a newline + - id: check-json + name: check-json + description: Attempts to load all json files to verify syntax + - id: check-toml + name: check-toml + description: Attempts to load all TOML files to verify syntax + - id: check-symlinks + name: check-symlinks + description: Checks for symlinks which do not point to anything + - id: check-added-large-files + name: check-added-large-files + description: Prevent files larger than 1 MB from being committed + args: [ "--maxkb=1024", '--enforce-all' ] + - id: check-case-conflict + name: check-case-conflict + description: Check for files with names that would conflict on a case-insensitive filesystem like MacOS HFS+ or Windows FAT + - id: end-of-file-fixer + name: end-of-file-fixer + description: Makes sure files end in a newline and only a newline + - id: mixed-line-ending + name: mixed-line-ending + description: Replaces or checks mixed line ending + - id: check-ast + name: check-ast + description: Simply check whether files parse as valid python + - id: debug-statements + name: debug-statements + description: Check for debugger imports and py37+ breakpoint() calls in python source + - id: detect-aws-credentials + name: detect-aws-credentials + description: Checks for the existence of AWS/Minio secrets that you have set up + args: [--allow-missing-credentials] + - id: detect-private-key + name: detect-private-key + description: Checks for the existence of private keys. + - id: requirements-txt-fixer + name: requirements-txt-fixer + description: Sorts entries in requirements.txt and removes incorrect entries + #- id: no-commit-to-branch + # name: no-commit-to-master-branch + # description: Prevent commits to master/main branch + # language: python + # args: ["-b", master, "-b", main] + # pass_filenames: false + - id: check-merge-conflict + name: check-merge-conflict + description: Check for files that contain merge conflict strings + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: python-check-mock-methods + name: check-mock-methods + description: Prevent common mistakes of assert mck.not_called(), assert mck.called_once_with(...) and mck.assert_called. + - id: python-use-type-annotations + name: python-use-type-annotations + description: Enforce that python3.6+ type annotations are used instead of type comments + - id: python-check-blanket-noqa + name: python-check-blanket-noqa + description: Enforce that noqa annotations always occur with specific codes. + # - id: python-no-eval + # name: python-no-eval + # description: A quick check for the eval() built-in function + # - repo: https://github.com/pre-commit/mirrors-mypy + # rev: v1.5.1 + # hooks: + # - id: mypy + # name: mypy - static type checker for Python + # description: Static type checker for Python + # files: ^src/ + # exclude: ^tests/ + # args: [--ignore-missing-imports] + # additional_dependencies: [types-all] + # not working really well + # - repo: https://github.com/asottile/yesqa + # rev: v1.4.0 + # hooks: + # - id: yesqa + # name: yesqa - remove unnecessary `# noqa` comments + # description: Automatically remove unnecessary `# noqa` comments + - repo: https://github.com/asottile/add-trailing-comma + rev: v3.1.0 + hooks: + - id: add-trailing-comma + name: add-trailing-comma + description: Automatically add trailing commas to calls and literals. + - repo: https://github.com/PyCQA/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + name: flake8 - check the style and quality of some python code + description: Python tool that glues together pycodestyle, pyflakes, mccabe, and third-party plugins to check the style and quality of some python code + additional_dependencies: + - flake8-bugbear + # - flake8-variables-names + # - pep8-naming + # - flake8-print + - flake8-quotes + - flake8-broken-line + - flake8-comprehensions + - flake8-spellcheck # ignored by now + - flake8-eradicate + #- flake8-walrus==1.1.0 + - flake8-typing-imports==1.12.0 + #- flake8-match==1.0.0 + - repo: https://github.com/psf/black + rev: 24.1.1 + hooks: + - id: black + name: black - consistent Python code formatting + description: The uncompromising Python code formatter + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort - sort Python imports + description: Library to sort imports + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade + name: pyupgrade - upgrade syntax for newer versions of the language + description: Automatically upgrade syntax for newer versions of the language + args: [--py36-plus] + # - repo: https://github.com/jendrikseipp/vulture + # rev: v2.9.1 + # hooks: + # - id: vulture + # name: vulture - finds unused code in Python programs + # description: Finds unused code in Python programs +########################################################################################## +# Notebooks +########################################################################################## + - repo: https://github.com/nbQA-dev/nbQA + rev: 1.7.1 + hooks: +# - id: nbqa-flake8 +# name: nbqa-flake8 - Python linting (notebooks) +# additional_dependencies: [flake8] + #- id: nbqa-mypy + # name: nbqa-mypy - Static type checker for Python (notebooks) + # additional_dependencies: [mypy] + # args: [--ignore-missing-imports] + - id: nbqa-isort + name: nbqa-isort - Sort Python imports (notebooks) + additional_dependencies: [isort] + - id: nbqa-pyupgrade + name: nbqa-pyupgrade - Upgrade syntax for newer versions of Python (notebooks) + additional_dependencies: [pyupgrade] + args: [--py36-plus] + - id: nbqa-black + name: nbqa-black - consistent Python code formatting (notebooks) + additional_dependencies: [black] +# - id: nbqa-pydocstyle +# additional_dependencies: [pydocstyle, toml==0.10.2] + - repo: https://github.com/kynan/nbstripout + rev: 0.6.1 + hooks: + - id: nbstripout + name: nbstripout - strip outputs from notebooks + description: Strip output from Jupyter and IPython notebooks + args: + - --extra-keys + - "metadata.colab metadata.kernelspec cell.metadata.colab cell.metadata.executionInfo cell.metadata.id cell.metadata.outputId" + files: .ipynb +########################################################################################## +# Shell Scripting +########################################################################################## + - repo: local + hooks: + - id: shellcheck + name: shellcheck - static analysis tool for shell scripts + description: A static analysis tool for shell scripts + language: script + entry: scripts/shellcheck.sh + types: [shell] + args: [-e, SC1091] + - repo: https://github.com/lovesegfault/beautysh + rev: v6.2.1 + hooks: + - id: beautysh + name: beautysh - Autoformat shell scripts + description: Autoformat shell scripts +########################################################################################## +# Tests +########################################################################################## + - repo: local + hooks: + - id: pytest + name: pytest + description: Run pytest + entry: pytest -sv test + language: system + always_run: true + types: [python] + stages: [push] + pass_filenames: false +########################################################################################## +# Security +########################################################################################## +# - repo: local +# hooks: +# - id: safety +# name: safety +# description: Analyze your Python requirements for known security vulnerabilities +# entry: safety check --short-report -r +# language: system +# files: requirements/*.txt + - repo: https://github.com/PyCQA/bandit + rev: 1.7.7 + hooks: + - id: bandit + args: ["-c", "pyproject.toml"] + additional_dependencies: [".[toml]"] + # - repo: https://github.com/PyCQA/bandit + # rev: 1.7.5 + # hooks: + # - id: bandit + # name: bandit - find common security issues in Python code. + # description: Tool designed to find common security issues in Python code + # args: ["-c", "pyproject.toml"] + # additional_dependencies: [toml==0.10.2] +########################################################################################## +# Git +########################################################################################## + # - repo: https://github.com/commitizen-tools/commitizen + # rev: 3.6.0 + # hooks: + # - id: commitizen + # stages: [commit-msg] + # additional_dependencies: [git+https://bitbucket.fraunhofer.pt/scm/is2020/mlops-commit-drafter.git] +########################################################################################## +# Documentation +########################################################################################## + - repo: https://github.com/executablebooks/mdformat + rev: 0.7.17 + hooks: + - id: mdformat + name: mdformat - Markdown formatter that can be used to enforce a consistent style in Markdown files + description: Markdown formatter that can be used to enforce a consistent style in Markdown files + additional_dependencies: + - mdformat-black + - mdformat-beautysh + exclude: CHANGELOG.md + - repo: https://github.com/myint/docformatter + rev: v1.7.5 + hooks: + - id: docformatter + name: docformatter - formats docstrings to follow PEP 257 + description: Formats docstrings to follow PEP 257 + args: [--in-place] + - repo: https://github.com/terrencepreilly/darglint + rev: v1.8.1 + hooks: + - id: darglint + name: darglint - Python documentation linter + description: A python documentation linter which checks that the docstring description matches the definition. + args: ["-z", long] +# - repo: https://github.com/econchick/interrogate +# rev: 1.5.0 +# hooks: +# - id: interrogate +# name: interrogate - interrogate a codebase for docstring coverage +# description: Interrogate a codebase for docstring coverage +# WIP +# - repo: https://github.com/PyCQA/prospector +# rev: 1.5.3.1 +# hooks: +# - id: prospector +########################################################################################## +# DVC +########################################################################################## +# https://dvc.org/doc/command-reference/install#--use-pre-commit-tool +# - repo: https://github.com/iterative/dvc +# hooks: +# - id: dvc-pre-commit +# language_version: python3 +# stages: +# - commit +# - id: dvc-pre-push +# # use s3/gs/etc instead of all to only install specific cloud support +# additional_dependencies: ['.[all]'] +# language_version: python3 +# stages: +# - push +# - always_run: true +# - id: dvc-post-checkout +# language_version: python3 +# stages: +# - post-checkout +########################################################################################## +# Docker +########################################################################################## + - repo: local + hooks: + - id: hadolint + name: hadolint - Lint Dockerfile for errors and enforce best practices + description: Lint Dockerfile for errors and enforce best practices + language: script + entry: scripts/hadolint.sh + files: Dockerfile diff --git a/.readthedocs.yml b/.readthedocs.yml index 0de0b4a..7957d04 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -21,4 +21,4 @@ python: - method: pip path: . extra_requirements: - - docs \ No newline at end of file + - docs diff --git a/CHANGELOG.rst b/CHANGELOG.rst index df53638..0f5acd6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,22 @@ Changelog ========= + +Version 0.1.7 +============= +- New features + - Implemented the Lempel-Ziv-Complexity in the temporal domain (`#146 `_) + - Added the fractal domain with the following features (`#144 `_): + - Detrended fluctuation analysis (DFA) + - Higuchi fractal dimension + - Hurst exponent + - Maximum fractal length + - Multiscale entropy (MSE) + - Petrosian fractal dimension + +- Changes + - Changed the ``autocorrelation`` logic. It now measures the first lag below (1/e) from the ACF (`#142 `_). + Version 0.1.6 ============= - Changes @@ -15,7 +31,7 @@ Version 0.1.6 - Improvements - Correlated features are now computed using absolute value - Unit tests improvements - - Refactoring of some code sections and overall improved stability + - Refactoring of some code sections and overall improved stability\ Version 0.1.5 diff --git a/README.md b/README.md index 7b8368d..2d4ac44 100644 --- a/README.md +++ b/README.md @@ -6,30 +6,43 @@ [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/fraunhoferportugal/tsfel/blob/master/notebooks/TSFEL_HAR_Example.ipynb) # Time Series Feature Extraction Library + ## Intuitive time series feature extraction -This repository hosts the **TSFEL - Time Series Feature Extraction Library** python package. TSFEL assists researchers on exploratory feature extraction tasks on time series without requiring significant programming effort. + +This repository hosts the **TSFEL - Time Series Feature Extraction Library** python package. TSFEL assists researchers +on exploratory feature extraction tasks on time series without requiring significant programming effort. Users can interact with TSFEL using two methods: + ##### Online + It does not requires installation as it relies on Google Colabs and a user interface provided by Google Sheets ##### Offline + Advanced users can take full potential of TSFEL by installing as a python package + ```python -pip install tsfel +pip +install +tsfel ``` ## Includes a comprehensive number of features -TSFEL is optimized for time series and **automatically extracts over 60 different features on the statistical, temporal and spectral domains.** + +TSFEL is optimized for time series and **automatically extracts over 65 different features on the statistical, temporal, +spectral and fractal domains.** ## Functionalities -* **Intuitive, fast deployment and reproducible**: interactive UI for feature selection and customization -* **Computational complexity evaluation**: estimate the computational effort before extracting features -* **Comprehensive documentation**: each feature extraction method has a detailed explanation -* **Unit tested**: we provide unit tests for each feature -* **Easily extended**: adding new features is easy and we encourage you to contribute with your custom features + +- **Intuitive, fast deployment and reproducible**: interactive UI for feature selection and customization +- **Computational complexity evaluation**: estimate the computational effort before extracting features +- **Comprehensive documentation**: each feature extraction method has a detailed explanation +- **Unit tested**: we provide unit tests for each feature +- **Easily extended**: adding new features is easy and we encourage you to contribute with your custom features ## Get started + The code below extracts all the available features on an example dataset file. ```python @@ -37,7 +50,7 @@ import tsfel import pandas as pd # load dataset -df = pd.read_csv('Dataset.txt') +df = pd.read_csv("Dataset.txt") # Retrieves a pre-defined feature configuration file to extract all available features cfg = tsfel.get_features_by_domain() @@ -49,87 +62,110 @@ X = tsfel.time_series_features_extractor(cfg, df) ## Available features #### Statistical domain -| Features | Computational Cost | -|----------------------------|:------------------:| -| Absolute energy | 1 | -| Average power | 1 | -| ECDF | 1 | -| ECDF Percentile | 1 | -| ECDF Percentile Count | 1 | -| Entropy | 1 | -| Histogram | 1 | -| Interquartile range | 1 | -| Kurtosis | 1 | -| Max | 1 | -| Mean | 1 | -| Mean absolute deviation | 1 | -| Median | 1 | -| Median absolute deviation | 1 | -| Min | 1 | -| Root mean square | 1 | -| Skewness | 1 | -| Standard deviation | 1 | -| Variance | 1 | +| Features | Computational Cost | +|---------------------------|:------------------:| +| Absolute energy | 1 | +| Average power | 1 | +| ECDF | 1 | +| ECDF Percentile | 1 | +| ECDF Percentile Count | 1 | +| Entropy | 1 | +| Histogram | 1 | +| Interquartile range | 1 | +| Kurtosis | 1 | +| Max | 1 | +| Mean | 1 | +| Mean absolute deviation | 1 | +| Median | 1 | +| Median absolute deviation | 1 | +| Min | 1 | +| Root mean square | 1 | +| Skewness | 1 | +| Standard deviation | 1 | +| Variance | 1 | #### Temporal domain -| Features | Computational Cost | -|----------------------------|:------------------:| -| Area under the curve | 1 | -| Autocorrelation | 1 | -| Centroid | 1 | -| Mean absolute diff | 1 | -| Mean diff | 1 | -| Median absolute diff | 1 | -| Median diff | 1 | -| Negative turning points | 1 | -| Peak to peak distance | 1 | -| Positive turning points | 1 | -| Signal distance | 1 | -| Slope | 1 | -| Sum absolute diff | 1 | -| Zero crossing rate | 1 | -| Neighbourhood peaks | 1 | +| Features | Computational Cost | +|-------------------------|:------------------:| +| Area under the curve | 1 | +| Autocorrelation | 2 | +| Centroid | 1 | +| Lempel-Ziv-Complexity\* | 2 | +| Mean absolute diff | 1 | +| Mean diff | 1 | +| Median absolute diff | 1 | +| Median diff | 1 | +| Negative turning points | 1 | +| Peak to peak distance | 1 | +| Positive turning points | 1 | +| Signal distance | 1 | +| Slope | 1 | +| Sum absolute diff | 1 | +| Zero crossing rate | 1 | +| Neighbourhood peaks | 1 | + +\* Disabled by default due to its longer execution time compared to other features. #### Spectral domain -| Features | Computational Cost | -|-----------------------------------|:------------------:| -| FFT mean coefficient | 1 | -| Fundamental frequency | 1 | -| Human range energy | 2 | -| LPCC | 1 | -| MFCC | 1 | -| Max power spectrum | 1 | -| Maximum frequency | 1 | -| Median frequency | 1 | -| Power bandwidth | 1 | -| Spectral centroid | 2 | -| Spectral decrease | 1 | -| Spectral distance | 1 | -| Spectral entropy | 1 | -| Spectral kurtosis | 2 | -| Spectral positive turning points | 1 | -| Spectral roll-off | 1 | -| Spectral roll-on | 1 | -| Spectral skewness | 2 | -| Spectral slope | 1 | -| Spectral spread | 2 | -| Spectral variation | 1 | -| Wavelet absolute mean | 2 | -| Wavelet energy | 2 | -| Wavelet standard deviation | 2 | -| Wavelet entropy | 2 | -| Wavelet variance | 2 | - -#### Fractal domain -:sparkles: **Coming Soon!** :hourglass_flowing_sand: +| Features | Computational Cost | +|----------------------------------|:------------------:| +| FFT mean coefficient | 1 | +| Fundamental frequency | 1 | +| Human range energy | 1 | +| LPCC | 1 | +| MFCC | 1 | +| Max power spectrum | 1 | +| Maximum frequency | 1 | +| Median frequency | 1 | +| Power bandwidth | 1 | +| Spectral centroid | 2 | +| Spectral decrease | 1 | +| Spectral distance | 1 | +| Spectral entropy | 1 | +| Spectral kurtosis | 2 | +| Spectral positive turning points | 1 | +| Spectral roll-off | 1 | +| Spectral roll-on | 1 | +| Spectral skewness | 2 | +| Spectral slope | 1 | +| Spectral spread | 2 | +| Spectral variation | 1 | +| Wavelet absolute mean | 2 | +| Wavelet energy | 2 | +| Wavelet standard deviation | 2 | +| Wavelet entropy | 2 | +| Wavelet variance | 2 | + +#### Fractal domain + +| Features | Computational Cost | +|--------------------------------------|:------------------:| +| Detrended fluctuation analysis (DFA) | 3 | +| Higuchi fractal dimension | 3 | +| Hurst exponent | 3 | +| Maximum fractal length | 3 | +| Multiscale entropy (MSE) | 1 | +| Petrosian fractal dimension | 1 | + +_Fractal domain features are typically applied to relatively longer signals to capture meaningful patterns, and it's +usually +unnecessary to previously divide the signal into shorter windows. Therefore, this domain is disabled in the default +feature +configuration files._ ## Citing + When using TSFEL please cite the following publication: -Barandas, Marília and Folgado, Duarte, et al. "*TSFEL: Time Series Feature Extraction Library.*" SoftwareX 11 (2020). [https://doi.org/10.1016/j.softx.2020.100456](https://doi.org/10.1016/j.softx.2020.100456) +Barandas, Marília and Folgado, Duarte, et al. "*TSFEL: Time Series Feature Extraction Library.*" SoftwareX 11 ( +2020). [https://doi.org/10.1016/j.softx.2020.100456](https://doi.org/10.1016/j.softx.2020.100456) ## Acknowledgements -We would like to acknowledge the financial support obtained from the project Total Integrated and Predictive Manufacturing System Platform for Industry 4.0, co-funded by Portugal 2020, framed under the COMPETE 2020 (Operational Programme Competitiveness and Internationalization) and European Regional Development Fund (ERDF) from European Union (EU), with operation code POCI-01-0247-FEDER-038436. + +We would like to acknowledge the financial support obtained from the project Total Integrated and Predictive +Manufacturing System Platform for Industry 4.0, co-funded by Portugal 2020, framed under the COMPETE 2020 (Operational +Programme Competitiveness and Internationalization) and European Regional Development Fund (ERDF) from European Union ( +EU), with operation code POCI-01-0247-FEDER-038436. diff --git a/__init__.py b/__init__.py index 89d7e5c..a51f059 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,5 @@ -from tsfel import * from pkg_resources import get_distribution -__version__ = get_distribution('tsfel').version +from tsfel import * + +__version__ = get_distribution("tsfel").version diff --git a/docs/Makefile b/docs/Makefile index a76ca5d..34c1340 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -17,4 +17,4 @@ help: # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py index 2a96da7..304b30e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # TSFEL documentation build configuration file, created by # sphinx-quickstart on Tue Nov 5 08:17:41 2019. @@ -16,13 +15,18 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. +import datetime import os -if os.environ.get('READTHEDOCS', None) == 'True': +if os.environ.get("READTHEDOCS", None) == "True": import inspect + from sphinx.ext.apidoc import main - __location__ = os.path.join(os.getcwd(), os.path.dirname(inspect.getfile(inspect.currentframe()))) + __location__ = os.path.join( + os.getcwd(), + os.path.dirname(inspect.getfile(inspect.currentframe())), + ) output_dir = os.path.join(__location__, "../docs/descriptions/modules") module_dir = os.path.join(__location__, "../tsfel") @@ -39,39 +43,45 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', - 'sphinx.ext.autosummary', 'sphinx.ext.viewcode', 'sphinx.ext.coverage', - 'sphinx.ext.doctest', 'sphinx.ext.ifconfig', 'sphinx.ext.imgmath', - 'sphinx.ext.napoleon' +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.autosummary", + "sphinx.ext.viewcode", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.ifconfig", + "sphinx.ext.imgmath", + "sphinx.ext.napoleon", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -import datetime now = datetime.datetime.today() -project = u'TSFEL' -copyright = u'2024, Fraunhofer AICOS' -author = u'Fraunhofer AICOS' +project = "TSFEL" +copyright = "2024, Fraunhofer AICOS" +author = "Fraunhofer AICOS" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = u'0.1.6' +version = "0.1.6" # The full version, including alpha/beta/rc tags. -release = u'0.1.6' +release = "0.1.6" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -83,7 +93,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # If true, keep warnings as "system message" paragraphs in the built documents. keep_warnings = True @@ -93,14 +103,14 @@ autosummary_generate = True # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'default' +pygments_style = "default" # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -116,7 +126,7 @@ # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'TSFELdoc' +htmlhelp_basename = "TSFELdoc" # -- Options for LaTeX output --------------------------------------------- @@ -125,15 +135,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -143,8 +150,13 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'TSFEL.tex', u'TSFEL Documentation', - u'Fraunhofer AICOS', 'manual'), + ( + master_doc, + "TSFEL.tex", + "TSFEL Documentation", + "Fraunhofer AICOS", + "manual", + ), ] @@ -153,8 +165,13 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'tsfel', u'TSFEL Documentation', - [author], 1) + ( + master_doc, + "tsfel", + "TSFEL Documentation", + [author], + 1, + ), ] @@ -164,9 +181,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'TSFEL', u'TSFEL Documentation', - author, 'TSFEL', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "TSFEL", + "TSFEL Documentation", + author, + "TSFEL", + "One line description of project.", + "Miscellaneous", + ), ] @@ -188,8 +211,8 @@ # epub_uid = '' # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} diff --git a/docs/descriptions/faq.rst b/docs/descriptions/faq.rst index e1c227f..13a22aa 100644 --- a/docs/descriptions/faq.rst +++ b/docs/descriptions/faq.rst @@ -35,4 +35,4 @@ FAQ * **How can I give the authors credit for their work?** - If you used TSFEL we would be appreciated if you star our `GitHub `_ repository. In case you use TSFEL during your research, you would be happy if you can `cite our work `_. \ No newline at end of file + If you used TSFEL we would be appreciated if you star our `GitHub `_ repository. In case you use TSFEL during your research, you would be happy if you can `cite our work `_. diff --git a/notebooks/TSFEL_HAR_Example.ipynb b/notebooks/TSFEL_HAR_Example.ipynb index 818a5a5..6a8750b 100644 --- a/notebooks/TSFEL_HAR_Example.ipynb +++ b/notebooks/TSFEL_HAR_Example.ipynb @@ -3,8 +3,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "FMKUENZnk_fq" + "colab_type": "text" }, "source": [ "# Human Activity Recognition using TSFEL\n", @@ -22,33 +21,33 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "cellView": "both", - "colab": {}, - "colab_type": "code", - "id": "QC8NanicBDIP" + "colab_type": "code" }, "outputs": [], "source": [ - "#@title Import Time Series Feature Extraction Library\n", + "# @title Import Time Series Feature Extraction Library\n", "import warnings\n", - "warnings.filterwarnings('ignore')\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", "!pip install tsfel >/dev/null 2>&1\n", "from sys import platform\n", + "\n", "if platform == \"linux\" or platform == \"linux2\":\n", " !wget http://archive.ics.uci.edu/ml/machine-learning-databases/00240/UCI%20HAR%20Dataset.zip >/dev/null 2>&1\n", "else:\n", " !pip install wget >/dev/null 2>&1\n", " import wget\n", - " wget.download('http://archive.ics.uci.edu/ml/machine-learning-databases/00240/UCI HAR Dataset.zip')" + "\n", + " wget.download(\"http://archive.ics.uci.edu/ml/machine-learning-databases/00240/UCI HAR Dataset.zip\")" ] }, { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "oO5DRSFMnd0X" + "colab_type": "text" }, "source": [ "To check if everything was correctly imported, access \"Files\" (on the left side of the screen) and press \"Refresh\". If UCI HAR Dataset folder does not appear run Import Time Series Features library again.\n", @@ -58,33 +57,31 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { - "colab": {}, - "colab_type": "code", - "id": "TL0cXPIXW1Nl" + "colab_type": "code" }, "outputs": [], "source": [ - "# Import libraries\n", - "import tsfel\n", "import glob\n", "import zipfile\n", "\n", "import numpy as np\n", "import pandas as pd\n", "import seaborn as sns\n", - "\n", "from matplotlib import pyplot as plt\n", "from sklearn import preprocessing\n", "from sklearn.ensemble import RandomForestClassifier\n", - "from sklearn.metrics import classification_report\n", "from sklearn.feature_selection import VarianceThreshold\n", - "from sklearn.metrics import confusion_matrix, accuracy_score\n", + "from sklearn.metrics import accuracy_score, classification_report, confusion_matrix\n", + "\n", + "# Import libraries\n", + "import tsfel\n", + "\n", "sns.set()\n", "\n", "# Unzip dataset\n", - "zip_ref = zipfile.ZipFile(\"UCI HAR Dataset.zip\", 'r')\n", + "zip_ref = zipfile.ZipFile(\"UCI HAR Dataset.zip\", \"r\")\n", "zip_ref.extractall()\n", "zip_ref.close()" ] @@ -92,8 +89,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "wdbIZYZ3lznS" + "colab_type": "text" }, "source": [ "# Dataset\n", @@ -114,16 +110,14 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "cellView": "both", - "colab": {}, - "colab_type": "code", - "id": "LmMjwZ-CBS5J" + "colab_type": "code" }, "outputs": [], "source": [ - "#@title Data Preparation\n", + "# @title Data Preparation\n", "\n", "# Load data\n", "x_train_sig = list(np.loadtxt('UCI HAR Dataset/train/Inertial Signals/total_acc_x_train.txt', dtype='float32'))\n", @@ -138,48 +132,28 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 458 - }, - "colab_type": "code", - "id": "EK66Oc_jNzKc", - "outputId": "b08781af-b9d5-4583-f244-abf7d8d85dfc" + "colab_type": "code" }, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "#@title Signal Preview\n", "%matplotlib inline\n", "plt.figure()\n", "plt_size = 10\n", - "plt.plot(np.concatenate(x_train_sig[0:plt_size],axis=0))\n", + "plt.plot(np.concatenate(x_train_sig[0:plt_size], axis=0))\n", "plt.xlabel(\"time (s)\")\n", "plt.ylabel(\"Acceleration (m/s²)\")\n", "plt.title(\"Accelerometer Signal\")\n", - "plt.legend('x axis')\n", + "plt.legend(\"x axis\")\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "1xVXlzMbKave" + "colab_type": "text" }, "source": [ "# Feature Extraction\n", @@ -189,93 +163,14 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 221 - }, - "colab_type": "code", - "id": "uf_qta_DJ4xD", - "outputId": "401ddf5a-8ec3-46f3-9ebe-5cb9ff0028cc" + "colab_type": "code" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "*** Feature extraction started ***\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "

\n", - " Progress: 100% Complete\n", - "

\n", - " \n", - " 7352\n", - " \n", - "\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "*** Feature extraction finished ***\n", - "*** Feature extraction started ***\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "

\n", - " Progress: 100% Complete\n", - "

\n", - " \n", - " 2947\n", - " \n", - "\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "*** Feature extraction finished ***\n" - ] - } - ], + "outputs": [], "source": [ - "#@title Feature Extraction\n", - "cfg_file = tsfel.get_features_by_domain() # All features \n", + "# @title Feature Extraction\n", + "cfg_file = tsfel.get_features_by_domain() # All features\n", "# cfg_file = tsfel.get_features_by_domain('statistical') # Only statistical features\n", "# cfg_file = tsfel.get_features_by_domain('temporal') # Only temporal features\n", "# cfg_file = tsfel.get_features_by_domain('spectral') # Only spectral features\n", @@ -288,8 +183,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "i_i0SNNGXWdp" + "colab_type": "text" }, "source": [ "# Feature Selection\n", @@ -299,11 +193,9 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": { - "colab": {}, - "colab_type": "code", - "id": "50e9L9k-W1Ny" + "colab_type": "code" }, "outputs": [], "source": [ @@ -326,8 +218,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "5PlVaejpK9LQ" + "colab_type": "text" }, "source": [ "# Classification\n", @@ -337,38 +228,11 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 252 - }, - "colab_type": "code", - "id": "E3o5jF_sOwDI", - "outputId": "5f15ae3f-8190-43be-ad91-fc9f0ef9c906" + "colab_type": "code" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " precision recall f1-score support\n", - "\n", - " WALKING 0.85 0.97 0.90 496\n", - " WALKING_UPSTAIRS 0.84 0.85 0.85 471\n", - "WALKING_DOWNSTAIRS 0.92 0.76 0.84 420\n", - " SITTING 0.75 0.48 0.58 491\n", - " STANDING 0.64 0.86 0.73 532\n", - " LAYING 1.00 1.00 1.00 537\n", - "\n", - " accuracy 0.82 2947\n", - " macro avg 0.84 0.82 0.82 2947\n", - " weighted avg 0.83 0.82 0.82 2947\n", - "\n", - "Accuracy: 82.38887003732609%\n" - ] - } - ], + "outputs": [], "source": [ "classifier = RandomForestClassifier()\n", "# Train the classifier\n", @@ -380,45 +244,26 @@ "# Get the classification report\n", "accuracy = accuracy_score(y_test, y_test_predict) * 100\n", "print(classification_report(y_test, y_test_predict, target_names=activity_labels))\n", - "print(\"Accuracy: \" + str(accuracy) + '%')" + "print(\"Accuracy: \" + str(accuracy) + \"%\")" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 380 - }, - "colab_type": "code", - "id": "6lhdbQnCQxSV", - "outputId": "cfc76407-fe6d-4254-e5b9-4cbd78c6934e" + "colab_type": "code" }, - "outputs": [ - { - "data": { - "image/png": "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UWCo37dAeay6/03gui0M9SvyfGGfYEP92mKFiPXA2+Ecq4rzPqzLyfUPqktKS9etT3QIcdXimqRqnYuplbfMSnQIcVcpK7PQUqEnf1cmePJ3ycqTf9mVzsnf2/ydc865NOPJ3znnnEsznvydc865NOPJ3znnnEsznvydc865NOPJ3znnnEsznvydc865NOPJ3znnnEsznvydc865NOPJ3znnnEsznvydc865NOPJ3znnnEszvrCPKxN8YR/nXKxV7t4k0SHEXd5bC31hH+ecc8558nfOOefSjid/55xzLs148nfOOefSjCd/55xzLs148nfOOefSjCd/55xzLs148nfOOefSjCd/55xzLs148nfOOefSjCd/55xzLs148nfOOefSjCd/55xzLs148nfOOefSjCd/56K8P20avXv2oFe3bjz0wAOJDifm/PrKtlS+vlS5tp8e/5Av7n+b2fe9ycwxbwBw3b+G8vnYt5h935u8OepJdqyTDcAJXfsy+743839y3lzA3s32KJU4M/LyfJn0dCAi84BeqjqnkMeeAzoDDVV1g4iMA5aq6uVRx7wNvAhMBGapat2o8/4J7KWquYW9logcD1wC1ABWheNvU9XXixv/upzcuH9Qc3Jy6N2zB2MffIjs7GxOGDiAUbeNplnz5vF+6VLh11e2pfL1JeraKndvEvNz/vT4h7Q7tyfLV63I31e9SjVWr/kTgPP7nM4eO7dgyF1XbPa81k1b8sp1D9HslPYxjSfvrYUZhe33O/80JyK1gcOAH4Cjwu4LgQEickA45iwgD7iviNNUA04u4vxnAFcBJ6lqC1XdF7gIaBWra4iVOV9+QeMmTWjUuDHlK1Sge4+eTHn33USHFTN+fWVbKl9fKl8bkJ/4AapWqkxhN93Hdz2apye/UmoxefJ3JwFvAGOA0wFUdRVwJvCwiOwGjAAGqWpRd98jgZEiUqGIxy5S1bmRHar6qareErMriJEli5fQoEGD/O36DbJZvGRxAiOKLb++si2Vry+Vri0vL4//jXqKWWMmMLjnifn7bzjtMhY8OYMTu/bl6kdH/+N5Azsd5cnflarTgIexKv2DRGQnAFV9C5gKzASuUdUFWzjHrPAzJHqniNQHGgIfxyHumCusNJ5BoTVmZZJfX9mWyteXStfW/qK+7HtOD3oMP5lze/+LjnseAMCIh2+lyYn7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\n", - "text/plain": [ - "

" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "#@title Confusion Matrix\n", + "# @title Confusion Matrix\n", "cm = confusion_matrix(y_test, y_test_predict)\n", "df_cm = pd.DataFrame(cm, index=[i for i in activity_labels], columns=[i for i in activity_labels])\n", "plt.figure()\n", - "ax= sns.heatmap(df_cm, cbar=False, cmap=\"BuGn\", annot=True, fmt=\"d\")\n", + "ax = sns.heatmap(df_cm, cbar=False, cmap=\"BuGn\", annot=True, fmt=\"d\")\n", "plt.setp(ax.get_xticklabels(), rotation=45)\n", "\n", - "plt.ylabel('True label', fontweight='bold', fontsize = 18)\n", - "plt.xlabel('Predicted label', fontweight='bold', fontsize = 18)\n", + "plt.ylabel(\"True label\", fontweight=\"bold\", fontsize=18)\n", + "plt.xlabel(\"Predicted label\", fontweight=\"bold\", fontsize=18)\n", "bottom, top = ax.get_ylim()\n", "plt.show()" ] @@ -426,8 +271,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "DDm-JuND4BK1" + "colab_type": "text" }, "source": [ "# Conclusion\n", @@ -437,17 +281,6 @@ } ], "metadata": { - "colab": { - "collapsed_sections": [], - "name": "TSFEL_HAR_Example.ipynb", - "provenance": [], - "toc_visible": true - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { "name": "ipython", diff --git a/notebooks/TSFEL_SMARTWATCH_HAR_Example.ipynb b/notebooks/TSFEL_SMARTWATCH_HAR_Example.ipynb index eff0f3f..7cff71a 100644 --- a/notebooks/TSFEL_SMARTWATCH_HAR_Example.ipynb +++ b/notebooks/TSFEL_SMARTWATCH_HAR_Example.ipynb @@ -3,8 +3,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "0xhqQ_JO8KCC" + "colab_type": "text" }, "source": [ "# Smartwatch Activity Recognition using TSFEL\n", @@ -22,28 +21,30 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "#@title Import Time Series Feature Extraction Library\n", + "# @title Import Time Series Feature Extraction Library\n", "import warnings\n", - "warnings.filterwarnings('ignore')\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", "!pip install tsfel >/dev/null 2>&1\n", "from sys import platform\n", + "\n", "if platform == \"linux\" or platform == \"linux2\":\n", " !wget http://archive.ics.uci.edu/ml/machine-learning-databases/00507/wisdm-dataset.zip >/dev/null 2>&1\n", "else:\n", " !pip install wget >/dev/null 2>&1\n", " import wget\n", - " wget.download('http://archive.ics.uci.edu/ml/machine-learning-databases/00507/wisdm-dataset.zip')" + "\n", + " wget.download(\"http://archive.ics.uci.edu/ml/machine-learning-databases/00507/wisdm-dataset.zip\")" ] }, { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "efUBgAg-JDRu" + "colab_type": "text" }, "source": [ "# Imports" @@ -52,8 +53,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "OIFaOYbDJTOI" + "colab_type": "text" }, "source": [ "To check if everything was correctly imported, access \"Files\" (on the left side of the screen) and press \"Refresh\". If UCI HAR Dataset folder does not appear run Import Time Series Features library again." @@ -61,34 +61,32 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { - "colab": {}, - "colab_type": "code", - "id": "uHCWyijP6u75" + "colab_type": "code" }, "outputs": [], "source": [ "# Import libraries\n", "import glob\n", - "import tsfel\n", + "import itertools\n", "import secrets\n", "import zipfile\n", - "import itertools\n", "\n", + "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", "import seaborn as sns\n", - "import matplotlib.pyplot as plt\n", - "\n", "from scipy import interpolate\n", "from sklearn import preprocessing\n", - "from sklearn.feature_selection import VarianceThreshold\n", - "from sklearn.metrics import classification_report, confusion_matrix, accuracy_score\n", "from sklearn.ensemble import RandomForestClassifier\n", + "from sklearn.feature_selection import VarianceThreshold\n", + "from sklearn.metrics import accuracy_score, classification_report, confusion_matrix\n", + "\n", + "import tsfel\n", "\n", "# Unzip dataset\n", - "zip_ref = zipfile.ZipFile(\"wisdm-dataset.zip\", 'r')\n", + "zip_ref = zipfile.ZipFile(\"wisdm-dataset.zip\", \"r\")\n", "zip_ref.extractall()\n", "zip_ref.close()" ] @@ -96,8 +94,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "VU4C5Vhg0nCe" + "colab_type": "text" }, "source": [ "# Auxiliary Methods" @@ -106,8 +103,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "pzEwY5rz0A2o" + "colab_type": "text" }, "source": [ "**Data pre-processing**\n", @@ -117,16 +113,14 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { - "colab": {}, - "colab_type": "code", - "id": "C3K28ULAezRz" + "colab_type": "code" }, "outputs": [], "source": [ "def pre_process_data(data_acc, data_gyro, fs):\n", - " \"\"\"This function interpolates the accelerometer and gyroscope data to \n", + " \"\"\"This function interpolates the accelerometer and gyroscope data to\n", " the same time interval.\n", "\n", " Parameters\n", @@ -143,23 +137,27 @@ " Interpolated data (nd-array)\n", "\n", " \"\"\"\n", - " time_acc = (data_acc[:, 2])/1e9 - data_acc[0, 2]/1e9\n", + " time_acc = (data_acc[:, 2]) / 1e9 - data_acc[0, 2] / 1e9\n", " data_act_acc = data_acc[:, 3:]\n", "\n", - " time_gyro = (data_gyro[:, 2])/1e9 - data_gyro[0, 2]/1e9\n", + " time_gyro = (data_gyro[:, 2]) / 1e9 - data_gyro[0, 2] / 1e9\n", " data_act_gyro = data_gyro[:, 3:]\n", "\n", " # time interval for interpolation\n", " t0 = np.max([time_acc[0], time_gyro[0]])\n", " tn = np.min([time_acc[-1], time_gyro[-1]])\n", - " time_new = np.linspace(t0, tn, int((tn - t0) / ((1 / fs))))\n", + " time_new = np.linspace(t0, tn, int((tn - t0) / (1 / fs)))\n", "\n", " # interpolation\n", - " acc_data = np.array([interpolate.interp1d(time_acc, data_act_acc[:, ax])(time_new) for ax in range(np.shape(data_act_acc)[1])]).T\n", - " gyro_data = np.array([interpolate.interp1d(time_gyro, data_act_gyro[:, ax])(time_new) for ax in range(np.shape(data_act_gyro)[1])]).T\n", + " acc_data = np.array(\n", + " [interpolate.interp1d(time_acc, data_act_acc[:, ax])(time_new) for ax in range(np.shape(data_act_acc)[1])]\n", + " ).T\n", + " gyro_data = np.array(\n", + " [interpolate.interp1d(time_gyro, data_act_gyro[:, ax])(time_new) for ax in range(np.shape(data_act_gyro)[1])]\n", + " ).T\n", "\n", " # concatenate interpolated data\n", - " data = np.concatenate((acc_data, gyro_data), axis = 1)\n", + " data = np.concatenate((acc_data, gyro_data), axis=1)\n", "\n", " return data" ] @@ -167,8 +165,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "v4ZXPFiQ9GKB" + "colab_type": "text" }, "source": [ "# Dataset\n", @@ -202,8 +199,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "HP8TVkfS_JfZ" + "colab_type": "text" }, "source": [ "# Load Data\n", @@ -213,24 +209,21 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { - "colab": {}, - "colab_type": "code", - "id": "VuHASTlb8Cir" + "colab_type": "code" }, "outputs": [], "source": [ "# Loading smartwatch data files\n", - "watch_files_acc = np.sort(glob.glob('wisdm-dataset/raw/watch/accel' + \"*/**.txt\", recursive=True))\n", - "watch_files_gyro = np.sort(glob.glob('wisdm-dataset/raw/watch/gyro' + \"*/**.txt\", recursive=True))" + "watch_files_acc = np.sort(glob.glob(\"wisdm-dataset/raw/watch/accel\" + \"*/**.txt\", recursive=True))\n", + "watch_files_gyro = np.sort(glob.glob(\"wisdm-dataset/raw/watch/gyro\" + \"*/**.txt\", recursive=True))" ] }, { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "dwDtDMuPdnrq" + "colab_type": "text" }, "source": [ "# Defining train and test\n", @@ -242,37 +235,38 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { - "colab": {}, - "colab_type": "code", - "id": "aOXhXZX78PFU" + "colab_type": "code" }, "outputs": [], "source": [ - "all_acc_data = [np.array(pd.read_csv(acc_file, header = None, delimiter = ',', comment=';')) for acc_file in watch_files_acc]\n", - "all_gyro_data = [np.array(pd.read_csv(gyro_file, header = None, delimiter = ',', comment=';')) for gyro_file in watch_files_gyro]\n", + "all_acc_data = [\n", + " np.array(pd.read_csv(acc_file, header=None, delimiter=\",\", comment=\";\")) for acc_file in watch_files_acc\n", + "]\n", + "all_gyro_data = [\n", + " np.array(pd.read_csv(gyro_file, header=None, delimiter=\",\", comment=\";\")) for gyro_file in watch_files_gyro\n", + "]\n", "\n", "x_test = []\n", "x_train = []\n", "y_test = []\n", "y_train = []\n", - "activities = np.unique(np.vstack(all_acc_data)[:,1]).astype(str)\n", + "activities = np.unique(np.vstack(all_acc_data)[:, 1]).astype(str)\n", "ntrain = 45\n", - "fs = 20 # According to dataset information\n", + "fs = 20 # According to dataset information\n", "overlap = 0\n", - "ws = 200 # 10 second windows\n", + "ws = 200 # 10 second windows\n", "\n", "for acq, (acc_data, gyro_data) in enumerate(zip(all_acc_data, all_gyro_data)):\n", - "\n", " windows = []\n", " labels = []\n", "\n", " for act in activities:\n", " act_acc, act_gyro = np.where(acc_data == act)[0], np.where(gyro_data == act)[0]\n", " acc_data_act, gyro_data_act = acc_data[act_acc, :], gyro_data[act_gyro, :]\n", - " ids_act_acc = np.append(0, np.where(np.diff(act_acc)>1)[0]+1)\n", - " ids_act_gyro = np.append(0, np.where(np.diff(act_gyro)>1)[0]+1)\n", + " ids_act_acc = np.append(0, np.where(np.diff(act_acc) > 1)[0] + 1)\n", + " ids_act_gyro = np.append(0, np.where(np.diff(act_gyro) > 1)[0] + 1)\n", "\n", " if len(act_acc) == 0 or len(act_gyro) == 0:\n", " continue\n", @@ -280,36 +274,36 @@ " # Only one acquisition of act\n", " if (len(ids_act_gyro) == 1) and (len(ids_act_acc) == 1):\n", " data = pre_process_data(acc_data_act, gyro_data_act, fs)\n", - " w = tsfel.signal_window_splitter(data.astype(float), ws, overlap) \n", + " w = tsfel.signal_window_splitter(data.astype(float), ws, overlap)\n", " windows.append(w)\n", " labels.append(np.repeat(act, len(w)))\n", " else:\n", " # More than one acquisition of act\n", - " acc_data_acts = [acc_data_act[ids_act_acc[i]:ids_act_acc[i+1], :] for i in range(len(ids_act_acc)-1)]\n", - " gyro_data_acts = [gyro_data_act[ids_act_gyro[i]:ids_act_gyro[i+1], :] for i in range(len(ids_act_gyro)-1)]\n", + " acc_data_acts = [acc_data_act[ids_act_acc[i] : ids_act_acc[i + 1], :] for i in range(len(ids_act_acc) - 1)]\n", + " gyro_data_acts = [\n", + " gyro_data_act[ids_act_gyro[i] : ids_act_gyro[i + 1], :] for i in range(len(ids_act_gyro) - 1)\n", + " ]\n", "\n", " for acc_data_act, gyro_data_act in zip(acc_data_acts, gyro_data_acts):\n", " data = pre_process_data(acc_data_act, gyro_data_act, fs)\n", - " w = tsfel.signal_window_splitter(data.astype(float), ws, overlap) \n", + " w = tsfel.signal_window_splitter(data.astype(float), ws, overlap)\n", " windows.append(w)\n", " labels.append(np.repeat(act, len(w)))\n", "\n", " # Consider ntrain acquisitions for train and the remaining for test\n", - " if acq<=ntrain:\n", + " if acq <= ntrain:\n", " x_train.append(windows)\n", " y_train.append(np.hstack(labels))\n", " else:\n", " x_test.append(windows)\n", - " y_test.append(np.hstack(labels))\n" + " y_test.append(np.hstack(labels))" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": { - "colab": {}, - "colab_type": "code", - "id": "nlntr1ea88KA" + "colab_type": "code" }, "outputs": [], "source": [ @@ -322,8 +316,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "EwsNz0kwdhzq" + "colab_type": "text" }, "source": [ "# Feature Extraction\n", @@ -335,112 +328,31 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 221 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 482236, - "status": "ok", - "timestamp": 1586778157434, - "user": { - "displayName": "Leticia Fernandes", - "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14Gj3U_hSW1M2-Ab0tHYcZEiOzvFIfJrkA-pccFhU=s64", - "userId": "17109198128714142667" - }, - "user_tz": -60 - }, - "id": "NHr4OvvXC8ax", - "outputId": "a4b80acf-92f2-4d63-af62-d3348798cdd0" + "colab_type": "code" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "*** Feature extraction started ***\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "

\n", - " Progress: 100% Complete\n", - "

\n", - " \n", - " 13890\n", - " \n", - "\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "*** Feature extraction finished ***\n", - "*** Feature extraction started ***\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "

\n", - " Progress: 100% Complete\n", - "

\n", - " \n", - " 1548\n", - " \n", - "\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "*** Feature extraction finished ***\n" - ] - } - ], + "outputs": [], "source": [ - "cfg_file = tsfel.get_features_by_domain('temporal')\n", - "x_train_feat = tsfel.time_series_features_extractor(cfg_file, x_train, fs = fs, header_names = ['accx', 'accy', 'accz', 'gyrox', 'gyroy', 'gyroz'])\n", - "x_test_feat = tsfel.time_series_features_extractor(cfg_file, x_test, fs = fs, header_names = ['accx', 'accy', 'accz', 'gyrox', 'gyroy', 'gyroz'])" + "cfg_file = tsfel.get_features_by_domain(\"temporal\")\n", + "x_train_feat = tsfel.time_series_features_extractor(\n", + " cfg_file,\n", + " x_train,\n", + " fs=fs,\n", + " header_names=[\"accx\", \"accy\", \"accz\", \"gyrox\", \"gyroy\", \"gyroz\"],\n", + ")\n", + "x_test_feat = tsfel.time_series_features_extractor(\n", + " cfg_file,\n", + " x_test,\n", + " fs=fs,\n", + " header_names=[\"accx\", \"accy\", \"accz\", \"gyrox\", \"gyroy\", \"gyroz\"],\n", + ")" ] }, { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "gR09UN0c4mTu" + "colab_type": "text" }, "source": [ "# Feature Selection\n", @@ -450,7 +362,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -473,8 +385,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "0x1DJRJYjcWZ" + "colab_type": "text" }, "source": [ "# Classification\n", @@ -484,68 +395,34 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 796 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 14133, - "status": "ok", - "timestamp": 1586778235659, - "user": { - "displayName": "Leticia Fernandes", - "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14Gj3U_hSW1M2-Ab0tHYcZEiOzvFIfJrkA-pccFhU=s64", - "userId": "17109198128714142667" - }, - "user_tz": -60 - }, - "id": "dxQJwbedb2wQ", - "outputId": "87c0e05a-13bb-4b83-a2db-043587a9f38f" + "colab_type": "code" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " precision recall f1-score support\n", - "\n", - " walking 0.84 0.60 0.70 85\n", - " jogging 1.00 0.93 0.96 85\n", - " stairs 0.71 0.71 0.71 85\n", - " sitting 0.63 0.58 0.60 85\n", - " standing 0.58 0.84 0.69 85\n", - " typing 0.84 0.95 0.90 85\n", - " brushing teeth 0.86 0.87 0.87 85\n", - " eating soup 0.67 0.44 0.53 85\n", - " eating chips 0.40 0.52 0.45 85\n", - " eating pasta 0.72 0.65 0.68 85\n", - " drinking 0.63 0.55 0.59 85\n", - "eating sandwich 0.29 0.24 0.26 85\n", - " kicking 0.71 0.71 0.71 103\n", - " playing catch 0.79 0.92 0.85 85\n", - " dribblinlg 0.93 0.95 0.94 85\n", - " writing 0.95 0.93 0.94 85\n", - " clapping 0.87 0.92 0.89 85\n", - "folding clothes 0.75 0.85 0.80 85\n", - "\n", - " accuracy 0.73 1548\n", - " macro avg 0.73 0.73 0.73 1548\n", - " weighted avg 0.73 0.73 0.73 1548\n", - "\n", - "Accuracy: 72.9328165374677%\n" - ] - } - ], + "outputs": [], "source": [ - "classifier = RandomForestClassifier(n_estimators = 20, min_samples_split=10)\n", - "\n", - "activities = ['walking', 'jogging', 'stairs', 'sitting', 'standing', 'typing', \n", - " 'brushing teeth', 'eating soup', 'eating chips', 'eating pasta', \n", - " 'drinking', 'eating sandwich', 'kicking', 'playing catch', \n", - " 'dribblinlg', 'writing', 'clapping', 'folding clothes']\n", + "classifier = RandomForestClassifier(n_estimators=20, min_samples_split=10)\n", + "\n", + "activities = [\n", + " \"walking\",\n", + " \"jogging\",\n", + " \"stairs\",\n", + " \"sitting\",\n", + " \"standing\",\n", + " \"typing\",\n", + " \"brushing teeth\",\n", + " \"eating soup\",\n", + " \"eating chips\",\n", + " \"eating pasta\",\n", + " \"drinking\",\n", + " \"eating sandwich\",\n", + " \"kicking\",\n", + " \"playing catch\",\n", + " \"dribblinlg\",\n", + " \"writing\",\n", + " \"clapping\",\n", + " \"folding clothes\",\n", + "]\n", "\n", "# Train The Classifier\n", "classifier.fit(X_train, y_train.ravel())\n", @@ -554,46 +431,32 @@ "y_predict = classifier.predict(X_test)\n", "\n", "# Get the Classification Report\n", - "accuracy = accuracy_score(y_test, y_predict)*100\n", - "print(classification_report(y_test, y_predict, target_names = activities))\n", - "print('Accuracy: ' + str(accuracy) + '%')" + "accuracy = accuracy_score(y_test, y_predict) * 100\n", + "print(classification_report(y_test, y_predict, target_names=activities))\n", + "print(\"Accuracy: \" + str(accuracy) + \"%\")" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "

" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Confusion Matrix\n", "cm = confusion_matrix(y_test, y_predict)\n", - "df_cm = pd.DataFrame(cm, index=[i for i in activities], columns = [i for i in activities])\n", + "df_cm = pd.DataFrame(cm, index=[i for i in activities], columns=[i for i in activities])\n", "plt.figure()\n", - "ax = sns.heatmap(df_cm, cbar = True, cmap=\"BuGn\", annot = True, fmt = 'd')\n", - "plt.setp(ax.get_xticklabels(), rotation = 90)\n", - "plt.ylabel('True label', fontweight = 'bold', fontsize = 18)\n", - "plt.xlabel('Predicted label', fontweight = 'bold', fontsize = 18)\n", + "ax = sns.heatmap(df_cm, cbar=True, cmap=\"BuGn\", annot=True, fmt=\"d\")\n", + "plt.setp(ax.get_xticklabels(), rotation=90)\n", + "plt.ylabel(\"True label\", fontweight=\"bold\", fontsize=18)\n", + "plt.xlabel(\"Predicted label\", fontweight=\"bold\", fontsize=18)\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "KWIZUEj-LUlO" + "colab_type": "text" }, "source": [ "# Conclusion" @@ -602,8 +465,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "mQMArVOn2YKb" + "colab_type": "text" }, "source": [ "The final accuracy obtained by feeding a Random Forest classifier with only temporal domain features is around 75%. \n", @@ -615,16 +477,6 @@ } ], "metadata": { - "colab": { - "collapsed_sections": [], - "name": "TSFEL_SMARTWATCH_HAR_Exampleold.ipynb", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { "name": "ipython", diff --git a/notebooks/TSFEL_predicting_NormalVsPathologicalknee.ipynb b/notebooks/TSFEL_predicting_NormalVsPathologicalknee.ipynb index b4c6505..fdbce3c 100644 --- a/notebooks/TSFEL_predicting_NormalVsPathologicalknee.ipynb +++ b/notebooks/TSFEL_predicting_NormalVsPathologicalknee.ipynb @@ -3,8 +3,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "4n_c2DADj-6y" + "colab_type": "text" }, "source": [ "# TSFEL predicting Normal Vs. Pathological knee\n", @@ -20,78 +19,62 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 1000 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 25663, - "status": "ok", - "timestamp": 1586777469202, - "user": { - "displayName": "Leticia Fernandes", - "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14Gj3U_hSW1M2-Ab0tHYcZEiOzvFIfJrkA-pccFhU=s64", - "userId": "17109198128714142667" - }, - "user_tz": -60 - }, - "id": "yQblfSnCkD19", - "outputId": "53de2f4b-e4c9-465f-c6e9-26f978962fa2" + "colab_type": "code" }, "outputs": [], "source": [ - "#@title Import Time Series Feature Extraction Library\n", + "# @title Import Time Series Feature Extraction Library\n", "import warnings\n", - "warnings.filterwarnings('ignore')\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", "!pip install tsfel >/dev/null 2>&1\n", "!pip install patool >/dev/null 2>&1\n", "from sys import platform\n", + "\n", "if platform == \"linux\" or platform == \"linux2\":\n", " !wget http://archive.ics.uci.edu/ml/machine-learning-databases/00278/SEMG_DB1.rar >/dev/null 2>&1\n", "else:\n", " !pip install wget >/dev/null 2>&1\n", " import wget\n", - " wget.download('http://archive.ics.uci.edu/ml/machine-learning-databases/00278/SEMG_DB1.rar')" + "\n", + " wget.download(\"http://archive.ics.uci.edu/ml/machine-learning-databases/00278/SEMG_DB1.rar\")" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { - "colab": {}, - "colab_type": "code", - "id": "2eDU5SCbkHqn" + "colab_type": "code" }, "outputs": [], "source": [ "# Import libraries\n", "import glob\n", "import itertools\n", - "import patoolib\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", - "import seaborn as sns\n", + "import patoolib\n", "import scipy.interpolate as interp\n", - "import tsfel\n", + "import seaborn as sns\n", "from sklearn import preprocessing\n", "from sklearn.feature_selection import VarianceThreshold\n", - "from sklearn.metrics import classification_report, accuracy_score, confusion_matrix\n", + "from sklearn.metrics import accuracy_score, classification_report, confusion_matrix\n", "from sklearn.tree import DecisionTreeClassifier\n", "\n", + "import tsfel\n", + "\n", "# Unzip dataset\n", - "patoolib.extract_archive(\"SEMG_DB1.rar\") " + "patoolib.extract_archive(\"SEMG_DB1.rar\")" ] }, { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "BJwehz1elgs5" + "colab_type": "text" }, "source": [ "## Auxiliary Methods\n", @@ -103,35 +86,37 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { - "colab": {}, - "colab_type": "code", - "id": "B_eVbl_Yk5IQ" + "colab_type": "code" }, "outputs": [], "source": [ "def preprocess(data):\n", " \"\"\"Interpolating the EMG and goniometer to the same sample size\n", "\n", - " Parameters\n", - " ----------\n", - " data list of pandas DataFrame\n", - " EMG and gonomioter signals for a given activity\n", - " Returns\n", - " -------\n", - " Interpolated data (list of nd-array)\n", + " Parameters\n", + " ----------\n", + " data list of pandas DataFrame\n", + " EMG and gonomioter signals for a given activity\n", + " Returns\n", + " -------\n", + " Interpolated data (list of nd-array)\n", "\n", " \"\"\"\n", - " data = [interp.interp1d(np.arange(len(x[0].dropna())), x[0].dropna(), axis=0, kind='nearest')(np.linspace(0, len(x[0].dropna()) - 1, len(x[0].iloc[:, 0].dropna()))) for x in data]\n", + " data = [\n", + " interp.interp1d(np.arange(len(x[0].dropna())), x[0].dropna(), axis=0, kind=\"nearest\")(\n", + " np.linspace(0, len(x[0].dropna()) - 1, len(x[0].iloc[:, 0].dropna()))\n", + " )\n", + " for x in data\n", + " ]\n", " return data" ] }, { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "igDQuS4alnmr" + "colab_type": "text" }, "source": [ "## Dataset\n", @@ -146,11 +131,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { - "colab": {}, - "colab_type": "code", - "id": "wIbkHfvEllG5" + "colab_type": "code" }, "outputs": [], "source": [ @@ -158,21 +141,22 @@ "normal_files = glob.glob(\"*/N_TXT/*.txt\")\n", "patholo_files = glob.glob(\"*/A_TXT/*.txt\")\n", "\n", - "normalfl = [[pd.read_csv(fl, sep='\\t', skiprows=7, usecols=(0, 4), header=None).dropna()] for fl in\n", - " normal_files if\n", - " 'Npie' in fl]\n", + "normalfl = [\n", + " [pd.read_csv(fl, sep=\"\\t\", skiprows=7, usecols=(0, 4), header=None).dropna()] for fl in normal_files if \"Npie\" in fl\n", + "]\n", "\n", - "patholofl = [[pd.read_csv(fl, sep='\\t', skiprows=7, usecols=(0, 4), header=None).dropna()] for fl in\n", - " patholo_files if 'Apie' in fl]" + "patholofl = [\n", + " [pd.read_csv(fl, sep=\"\\t\", skiprows=7, usecols=(0, 4), header=None).dropna()]\n", + " for fl in patholo_files\n", + " if \"Apie\" in fl\n", + "]" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { - "colab": {}, - "colab_type": "code", - "id": "6v7igkKoltCl" + "colab_type": "code" }, "outputs": [], "source": [ @@ -180,7 +164,7 @@ "# dataset sampling frequency, according to dataset information\n", "fs = 1000\n", "# window size for window splitter method (each window has 1 seconds)\n", - "window_size = 1000 \n", + "window_size = 1000\n", "\n", "# Interpolating data\n", "normalfl = preprocess(normalfl)\n", @@ -188,84 +172,75 @@ "\n", "# Dividing into train and test sets. Splitting signal in windows\n", "# Using 2 normal files and 2 pathological files for test set\n", - "x_train = list(itertools.chain(*[tsfel.signal_window_splitter(signal[i], window_size, overlap=0) for signal in\n", - " [normalfl, patholofl] for i in range(len(normalfl) - 2)]))\n", + "x_train = list(\n", + " itertools.chain(\n", + " *[\n", + " tsfel.signal_window_splitter(signal[i], window_size, overlap=0)\n", + " for signal in [normalfl, patholofl]\n", + " for i in range(len(normalfl) - 2)\n", + " ]\n", + " )\n", + ")\n", "\n", - "x_test = list(itertools.chain(*[tsfel.signal_window_splitter(signal[i], window_size, overlap=0) for signal in\n", - " [normalfl, patholofl] for i in [len(normalfl) - 2, len(normalfl) - 1]]))\n", + "x_test = list(\n", + " itertools.chain(\n", + " *[\n", + " tsfel.signal_window_splitter(signal[i], window_size, overlap=0)\n", + " for signal in [normalfl, patholofl]\n", + " for i in [len(normalfl) - 2, len(normalfl) - 1]\n", + " ]\n", + " )\n", + ")\n", "\n", "y_train = np.concatenate(\n", - " (np.repeat(0, np.cumsum([int(len(normalfl[i]) / window_size) for i in range(len(normalfl) - 2)])[-1]),\n", - " np.repeat(1, np.cumsum([int(len(patholofl[i]) / window_size) for i in range(len(patholofl) - 2)])[-1])))\n", + " (\n", + " np.repeat(\n", + " 0,\n", + " np.cumsum([int(len(normalfl[i]) / window_size) for i in range(len(normalfl) - 2)])[-1],\n", + " ),\n", + " np.repeat(\n", + " 1,\n", + " np.cumsum([int(len(patholofl[i]) / window_size) for i in range(len(patholofl) - 2)])[-1],\n", + " ),\n", + " )\n", + ")\n", "\n", - "y_test = np.concatenate((np.repeat(0, np.cumsum([int(len(normalfl[i]) / window_size) for i in [len(normalfl) - 2, len(normalfl) - 1]])[-1]),\n", - " np.repeat(1, np.cumsum([int(len(patholofl[i]) / window_size) for i in [len(patholofl) - 2, len(patholofl) - 1]])[-1])))\n" + "y_test = np.concatenate(\n", + " (\n", + " np.repeat(\n", + " 0,\n", + " np.cumsum([int(len(normalfl[i]) / window_size) for i in [len(normalfl) - 2, len(normalfl) - 1]])[-1],\n", + " ),\n", + " np.repeat(\n", + " 1,\n", + " np.cumsum([int(len(patholofl[i]) / window_size) for i in [len(patholofl) - 2, len(patholofl) - 1]])[-1],\n", + " ),\n", + " )\n", + ")" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 599 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 2152, - "status": "ok", - "timestamp": 1586777487184, - "user": { - "displayName": "Leticia Fernandes", - "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14Gj3U_hSW1M2-Ab0tHYcZEiOzvFIfJrkA-pccFhU=s64", - "userId": "17109198128714142667" - }, - "user_tz": -60 - }, - "id": "Byr434P7lvkm", - "outputId": "feed2286-9334-4a52-e3a0-1564f3ce8450" + "colab_type": "code" }, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAX8AAAEjCAYAAADaCAHrAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8vihELAAAACXBIWXMAAAsTAAALEwEAmpwYAABT9UlEQVR4nO2deXxU1fn/309CIAECARL2Jewisu+ggKKCirjUldalrVq1rdrFamurVr9+f636dbe1ti61buBOxSpuuGFlUUD2RbawbyEBkpDl/P44czOTySz3TiYMmXner1de985dzj3nZuZzznnOc54jxhgURVGU1CIt0RlQFEVRjj4q/oqiKCmIir+iKEoKouKvKIqSgqj4K4qipCAq/oqiKCmIir+ixICIzBWRq8Kcu1NEnj/aeVIUL6j4K8ckIrJRRHaKSLOAY1eJyNwEZktRkgYVf+VYphFwY10TEYt+1xUlAP1BKMcy9wG/FpGcUCdFZKyILBCRA77t2IBzc0XkHhH5AjgM9BARIyLXi8haESkWkbtFpKeIfCkiRSIyU0Qa++5vJSJvi8huEdnv2+/stQAikiEiL4nIayLS2GcSmikiz/nysFxEhgdc39F37W4R2SAiNwScSxORW0VkvYjs9aXT2mueFAVU/JVjm4XAXODXwSd8ojcbeARoAzwAzBaRNgGXXQZcA2QDm3zHpgDDgNHAb4Ange8DXYATgEt916UBzwDdgK5ACfCYl8yLSBbwJlAGXGSMOeI7NQ14GcgBZjnp+non/waWAJ2AScBNIjLZd98NwLnABKAjsB943EueFMVBxV851rkd+LmI5AUdPwtYa4z5lzGmwhjzErAKODvgmmeNMct958t9x/5sjCkyxiwHlgFzjDHfGWMOAP8BhgAYY/YaY14zxhw2xhQD92BF1y0tgHeB9cAPjTGVAec+N8a84zv2L2CQ7/gIIM8Yc5cx5ogx5jvg78AlvvM/AW4zxhQYY8qAO4ELRKSRh3wpCmBtqopyzGKMWSYibwO3AisDTnXE35p32IRtMTtsCZHkzoD9khCf2wOISFPgQWxPoZXvfLaIpAcJeThGAxnApaZ29MQdAfuHgUyfgHcDOopIYcD5dOAz33434A0RqQo4Xwm0A7a6yJOiVKMtf6UhcAdwNTWFfRtWDAPpSk0RrEvI2l8BfYFRxpgWwHjfcXF5/xzg/wEfikg7l/dsATYYY3IC/rKNMWcGnD8j6HymMUaFX/GMir9yzGOMWQfMwNq8Hd4B+ojIdBFpJCIXA8cDb8fpsdnYnkChb3zhDq8JGGPuBV7EVgC5Lm6ZDxSJyC0ikiUi6SJygoiM8J1/ArhHRLoBiEieiJzjNV+KAir+SsPhLqDa598YsxeYim2h78UO3k41xuyJ0/MeArKAPcB/sfZ7zxhj7sYO+n4QzTPHZ046GxgMbPA9+x9AS98lD2MHiOeISLEvX6NiyZeiiC7moiiKknpoy19RFCUFUfFXFEVJQVT8FUVRUhAVf0VRlBRExV9RFCUFUfFXFEVJQVT8FUVRUhAVf0VRlBRExV9RFCUFUfFXFEVJQVT8FUVRUhAVf0VRlBRExV9RFCUFUfFXFEVJQVT8FUVRUhAVf0VRlBRExV9RFCUFaZToDLglNzfX5OfnJzobiqIoDYpFixbtMcbkBR9vMOKfn5/PwoULE50NRVGUBoWIbAp1XM0+iqIoKYiKv6IoSgqi4u+Wr38N8y5PdC4URVHiQoOx+SecVf9nt2OfS2w+lJSnvLycgoICSktLE50V5RgiMzOTzp07k5GR4ep6FX9FaWAUFBSQnZ1Nfn4+IpLo7CjHAMYY9u7dS0FBAd27d3d1j5p9FKWBUVpaSps2bVT4lWpEhDZt2njqDar4K0oDRIVfCcbrd0LFX1EUJQVR8XfDtncTnQNFSRrefPNNVqxYEZe0XnnlFfr168fJJ58cl/TcMGvWLP70pz+5vn779u1MnTo1Ls++5JJLWLt2bVzSUvF3Q8nWROdAUY5JjDFUVVV5uiee4v/UU0/xl7/8hY8//jgu6UWjoqKCadOmceutt7q+54EHHuDqq6+Oy/Ovu+467r333rikpeKvKIonNm7cSL9+/bj++usZOnQoW7Zs4b777mPEiBEMHDiQO+64o/ra5557joEDBzJo0CAuu+wy5s2bx6xZs7j55psZPHgw69evZ+LEidWhW/bs2YMTw2v58uWMHDmSwYMHM3DgwFot3rvuuovPP/+ca6+9lptvvpnKykpuvvnm6nz87W9/A2Du3LlMmDCBiy66iD59+nDrrbfywgsvMHLkSAYMGMD69esB2LRpE5MmTWLgwIFMmjSJzZs3A3DllVfyy1/+kpNPPplbbrmFZ599lp/97GeA7XmccMIJDBo0iPHjx4d8X6+99hpTpkwB4Nlnn+Xcc8/l7LPPpnv37jz22GM88MADDBkyhNGjR7Nv3z5WrlzJyJEja7zvgQMHAnDSSSfxwQcfUFFRUaf/Iairp6I0bBbdBPsXxzfNVoNh2EMRL1m9ejXPPPMMf/nLX5gzZw5r165l/vz5GGOYNm0an376KW3atOGee+7hiy++IDc3l3379tG6dWumTZvG1KlTueCCCyI+44knnuDGG2/k+9//PkeOHKGysrLG+dtvv52PPvqI+++/n+HDh/Pkk0/SsmVLFixYQFlZGePGjeP0008HYMmSJaxcuZLWrVvTo0cPrrrqKubPn8/DDz/Mo48+ykMPPcTPfvYzLr/8cq644gqefvppbrjhBt58800A1qxZwwcffEB6ejrPPvtsdR7uuusu3nvvPTp16kRhYWGtMmzYsIFWrVrRpEmT6mPLli3jm2++obS0lF69evHnP/+Zb775hl/84hc899xz3HTTTRw5coTvvvuOHj16MGPGDC666CIA0tLS6NWrF0uWLGHYsGER3180tOWvKIpnunXrxujRowGYM2cOc+bMYciQIQwdOpRVq1axdu1aPvroIy644AJyc3MBaN26tadnjBkzhv/93//lz3/+M5s2bSIrKyvi9XPmzOG5555j8ODBjBo1ir1791b3FkaMGEGHDh1o0qQJPXv2rK4UBgwYwMaNGwH48ssvmT59OgCXXXYZn3/+eXXaF154Ienp6bWeOW7cOK688kr+/ve/16qcwNr78/JqBtQ8+eSTyc7OJi8vj5YtW3L22WfXystFF13EzJkzAZgxYwYXX3xx9f1t27Zl27ZtEd+FG7TlrygNmSgt9PqiWbNm1fvGGH7729/yk5/8pMY1jzzyiCv3w0aNGlWPGwT6qU+fPp1Ro0Yxe/ZsJk+ezD/+8Q9OOeWUsOkYY3j00UeZPHlyjeNz586t0fJOS0ur/pyWlhbWhBKY98DyBvLEE0/w1VdfMXv2bAYPHszixYtp06ZN9fmsrKxavvdu8nLxxRdz4YUXcv755yMi9O7du/qe0tLSqBWhG7TlryhKnZg8eTJPP/00Bw8eBGDr1q3s2rWLSZMmMXPmTPbu3QvAvn37AMjOzqa4uLj6/vz8fBYtWgTAq6++Wn3cMXvccMMNTJs2jaVLl0bNx1//+lfKy8sBa6o5dOiQ63KMHTuWl19+GYAXXniBE088Meo969evZ9SoUdx1113k5uayZcuWGuf79OlT3Zr3Qs+ePUlPT+fuu++u0eoHW67+/ft7TjMYbfkrilInTj/9dFauXMmYMWMAaN68Oc8//zz9+/fntttuY8KECaSnpzNkyBCeffZZLrnkEq6++moeeeQRXn31VX79619z0UUX8a9//atGy37GjBk8//zzZGRk0L59e26//faI+bjqqqvYuHEjQ4cOxRhDXl5etc3eDY888gg/+tGPuO+++8jLy+OZZ56Jes/NN9/M2rVrMcYwadIkBg0aVON8s2bN6NmzJ+vWraNXr16u8wK29X/zzTezYcOG6mM7d+4kKyuLDh06eEorFGKMqXMiR4Phw4ebhC3msv4p+Ooquz+9YbwvJXlZuXIl/fr1S3Q2FJe88cYbLFq0iP/5n/+pc1oPPvggLVq04Mc//nHI86G+GyKyyBgzPPhabfkriqLUI+edd1616auu5OTkcNlll8UlLRV/RVGUeuaqq66KSzo//OEP45IO6ICvojRIGoq5Vjl6eP1OqPgrSgMjMzOTvXv3agWgVOPE88/MzHR9T9zNPiIyBXgYSAf+YYz5U9D544BngKHAbcaY++OdB0VJZjp37kxBQQG7d+9OdFaUYwhnJS+3xFX8RSQdeBw4DSgAFojILGNMYBSnfcANwLnxfLaipAoZGRmuV2tSlHDE2+wzElhnjPnOGHMEeBk4J/ACY8wuY8wCoDzOz1YURVFcEm/x7wQETnEr8B2LCRG5RkQWishC7eIqiqLEj3iLf6hAHjGPShljnjTGDDfGDA8OjqQoiqLETrzFvwDoEvC5M1D38HOKoihKXIm3+C8AeotIdxFpDFwCzIrzMxRFUZQ6EldvH2NMhYj8DHgP6+r5tDFmuYhc6zv/hIi0BxYCLYAqEbkJON4YUxTPvCiKoijhibufvzHmHeCdoGNPBOzvwJqDFEVRlAShM3wVRVFSEBV/RVGUFETF3ytVOjct6Vn7V5jZHDR2TvzYvxReFCham+icKD5U/L2y8YVE50CpbxZcDxXul/9TXLDhn3a79a3E5kOpRsXfK9ryVxTvVPeioi/orhwdVPzdUHE40TlQlAaOT/xFJedYQf8TbjCVic6BkggObYh+jeIOU2W3u79IbD6UalT8XRHngb+NL9rBr/Li+KarxJf3RiU6B0mE7ze05bXEZkOpRsU/ESy/x24PbU5sPpQw+OzSZXsSm42kQj2njjVU/N2gLn8phv6/445j9lGOGVT8XdGAvrjlxfDByVC8LtE5URTlGEbF3w0NqeW/9W3YNReW/iHROVGUABrQbyhFUPGvC0cKvd9jDBxYEf06RUkm1OxzzBH3qJ7JSUCrZc+XkNne+it/MhUmfQztJnpIqiLuuQt6QD2nryixoN/LY43kb/mbKig/WPc0HL57Bj6dZoUf4MOT65JwnbJV76grKhzcmOgcJAeBptN9ixKXD6Wa5Bf/pXfAK9kwewBUxTpZq55EOtDV8+BGqDxSt/T2/Be2z6mddix89xy80gIKl9ctnYbOrO6JzkGSENCAend44rKhVJP8Zh8nENuBZXBkP2TmJjY/gXxyFnS7FDa9ZD93vwLGPBt7enPG+Pf3zLOtLYkxlsq22XZ7YBnk9I89T4oCavM/Bkn+ln9gqz1WIaxP84wj/ADb340tjcNb7YzheLH8/8HmmXa/IXk6Kccu3z2T6BwoQSR/y7+G+MdY1214Lj5ZiUqMQrv2r/F5/NI7Yc2jcGRffNJriJTuSnQOko/KskTnQAlBCrT8A4mxuEWr45uNcMS9le0xvWV/TLzwvyh2klqieL194p6drCz8eaJzoIQg+cXfxKHlf9SIUfzrtVwJMPvsmnv0n1lNiPLO6nX0s5FMhPLueVGsuVJJGMe6GtadqkAPmmNgIYmKSG6nMQpt4behjx/ZrwNt8eDg+kTnIDnZ9k6ic5DSJL/4l+7w78c84BtHlv9v+HOxmn0K3gx9/LVcWPY/saWpKEpSk/ziX4OjUNzNr8DGl8Of3780ws0RxN9UQVUMs4ML3nB3XfEx0LqNeR6GUoOqStjzFRzalOicWPZ/negc1B1j7OLzSTRBLbXEP5xtfNfnMHeq9ZoJbn0XrfH2jM8vgnmX2v3S3VCyE1Y/5hfufQvD3xup5f/RqfByhre8AEi6rTiK1sKim8L3BPbMC5cp78+MlVey/fs7Pqp57vDW6DO1C5fD5td0nYQPxsOc0fBWfqJzEoUG5Ea88j54u09STVBLLfEPxYEV8MFJdlLTguth67/957a+A2/3jS3dOePg9bbwRntY9HNY+xd7PKI3TYgfQ/lBO3N358chLjcuegNi/fbf7gOrHw4d7fPwVvjy8jBZqsOYwdq/wq7P3F9fWeLf/2hSzbWT3+xs/xcLbwztOmiq4J0T4PML4K1u4Z9hDCy93VaGXghV8eyeBxWHah/f9w2suNdb+vEkbEUeBw6shENb3JsoIy3bGCrAoZN+MEcKbeMlUW6jgfMU4u2VV7IDCpcd9Tk1KSb+ATb/qgrr0z07aPZq6Q4oL7L7B8IMpLoh+AdY5tGF8uB39sfxSnbNmbuBvJQWvTdQdQR2RxHgSL7tX14WufdTfhA+PhMOrLJlLNlujx9YYSvTD8ZHfnYkZjazFdPHU+znkm2w5hH47tma11WWwUvp7tIs2QrL7oa5Z3jLy8ogMd/5Cbw/DmY2tz/eQM+Vd4fC4lu8pQ+2HJ9Ms0JQsiP69W5w1nWoLAvvGOCF2cfDW11hxZ9rn9v8ChzeVvPYmsfCp7X64ZqfK0r86VccthWBw5Lb7PUb/uU9z1//yuatZHvsAhu4jve6v/n3931dd9F+owO8MwD+M6T2uV2f15upKbXEf+9//fsfngKvt6t9zfyfwCstfR88DBDvngcfTAx/3lU0TwNL/mDd4Gb1rF0xgRUdLzGACpfC9vdqHntR4D9D/Z83Ph85jbf72jGBAyttL2L/Ynt89xew7gnY/h+Y3Q9eawNvdLTpB+b98Lbwi8t88X0rduFiCL3ZuXb+A/O7e17o0NovCpT6lmE0xvZAjPH/iKtCvMNIk+WW3V2zpfrhRP/+Gx1sPoP5YEL49ILZ9h+YkWl7nu8MsGk6FWldWPl/VqhnZMI7A+sWqG7PV/799U/VPFdZZk2ewYEON78aOc31T/v3P/uef39mM1sROJVgla/FP/9q971RZ+b7qgds3t7oGPtM48Ae9oLrbO/xm5vh3WHw+YWxpRlM4ZLaxz44qd5MTWIayPT94cOHm4ULI9jLwxEc9qDXtdD5XJg7JS75AuCUD6xNPhIZLfw9ingw9iX/2EKsnPqJN4EKZPC9sPg33u65YD80zvF/3vo2fHK23W/WzfsAZd5JkXs1jbKhIiAy6UmvQXZvK4IAZy6DJb+DTlNtr+LbOyM/r/9tMPAu28N5Pa/2+YsOQ6Osmt+5sS9A/vToZQkVnqPNKPu8DqeHvufQZtjxAXS7BBo1jZxWIMMesWVu7gtaV1VuI7g2ae2/ZsubkHMCZPvmOJgq+PrXsPrB2ul1mAwnvQEzm4I0gtHPWEeDLa9HzofDdGN7n6EaYyP+Cr2vrVmm3LFwegRzkkO49zD0ITjuxtDnSvfAzo8gd5T9Tjq82QUOF4R/Vp+fQdeLoe2JAWntgsy2/s97vrLvOvCaAyttJefQJA/Kdtv96cZfhumx67SILDLG1KpBUk/8lcTR5wZrtgHIaAnlBxKbH6/kDArdOnPLhNnQ8QzbY/rqx9DrJ5A3zg7ORuKk1+Gz8/2fW54Anaf53YZzBtr3OeJxOza0KIywBdO8Z805DO1Phb0Lav5f+v0GjvuF7YlE4rQvrBmsPuh8XnivtWEPQ48fWXPtguuseA57KHRFEsiEt/1h2R3O+Kam6SX/+9BtOpRuh6+ucpfXzHZQutP/ueXxcOSANTc69Pk59Pyx7RUtj+CKPekja6GAhiH+IjIFeBhIB/5hjPlT0HnxnT8TOAxcaYyJ6gum4q8oSspSD+IfV5u/iKQDjwNnAMcDl4rI8UGXnQH09v1dA8QpKpmiKEqSUlka9yTjPeA7ElhnjPnOGHMEeBk4J+iac4DnjOW/QI6IROlTKoqipDLx982Jd4qdgEAn3QLfMa/XACAi14jIQhFZuHv37rhmVFEUpcEgLl2ZPRBv8Q9lYA82Vrm5xh405kljzHBjzPC8vBDeFYqiKKlAPUTujXeKBUCXgM+dgW0xXKMoytEk78To1yiJox6CUsZb/BcAvUWku4g0Bi4BZgVdMwu4XCyjgQPGmDjMZlGOebp8r6bXwhmLE5YVADr45npktrWuf9GYugYaNfP2jBNuh9bDah47cylMW2/fxXTj7tkjn4S2E2Cib6nPRr44SGlNoONZ1m105N/C3x9Ijyth/CxbnlPet26ap31m3RIzfBMcu10KZ6+z/vvRcMox8u/284VFcKbL2cQ9fwyD7nF3bSBdzq/5+dytMHm+Py8XunAjnvC2fadjX4QWfa3rq0OPH0KHM6y7pVsm/gfOXmvf6YR/w6VV1v/fISMHTv8KzvO1dQdHCAHilOPSSrik3H0ePFAfrp5nAg9hXT2fNsbcIyLXAhhjnvC5ej4GTMG6ev7QGBPVhzMurp69rrX+toHxe44mzbrDoQ2JeXas9L7OiuTB72DrLH+Moamrvcc9urgE0jPt5KRGzaBJG///Z9LHtWeHBpPdG4p9MXkuOmzXRti7AD45K/w97U+HnR/amb1dL7QzlYtWwAl/gP6/q3nta3lQtid8WtONjZh5YJkV3dn9ap7PGQCnfAT/7umf0HfRQX+FcWiLLX9mCBNmKJfki8tseXNCzPTeNBO+uBi6XAAnvRI9reByeKXyCMxoUvNY/9tgUAQ/9TV/gYU/jZzumUvte9u/1P5/Zzatfc207+xSqs4kvAF3woA77P6R/XaGtzNhLZAPJ9kJW4Gc/J6t7HMGhW5Nh5tU9cEE2PVp9HJ4JdT/qg5unaEI5+oZ9zV8jTHvAO8EHXsiYN8AUb4R9USva6D1EDvp4tWc8NedudRODc8dBe+Ptz/2SAy4E7I6wvxrwl9zaZX9ssVj3sG4l6HT2bDrE5h7ZuzpXFhcM5JmKEp22AlFAMfdBBtftAHYWvTxXzPqH/5JMGevhawOtpJof6qd8bngeiu06Zn2mmZdaz+ncavo+c3q5Bf/Rln2r9OZViTLC2HdkzZwnTOTePv79n+YlmnDazQKISyBjP5n5IoEIC0dWg0KfW7iO5CZG3R9gGA260JYTpsH74+teSy9cWjhh9jNAGeFCaMRjfTGtqfwdsD/vdvFke/pcz30uMLGPwqH+CSo1cDQ50/5sLawNw6Yidy4VfjvzqQPa//eWg0JXflGIyMn/LnuV8Qm/AAX7INXA8oz7JHY0omB1Irt40xVb9wSztkE54WwNp34qv1Hdpxiv1Rnuei+tuwPva6GZvnhr3F+rJHMBulRxAnsjMNuF1shazMq+vXpWeHPZTS3YSIi0X5Szc/5021XPZCeP/b/OLJ72TJ2mmrFPq0RjHoSmueHTv/8nTD0ATtLNRpD7rPbYPNGemPbmjvh97bV5ISQ6HCaDauR3ji68IOtSLzQvIfd9rkBzi2ApkHxfYbcb8vvhryg4H2DQwROC0mIVuIZ34S/3KvZKpAWvf37o55yJ3jRnhcc8+oEX4u+zUiYsgjan1LzvKTbcA+xkNkuNuEHW95wOKayWAiuuPoevfWO497yP6YJ/CKGan0CZMWwgHdHX4TIs9fBy0GvtHmPmtPle15VO5qhQ79f2QBiYFuzlYf8rYLWI2DfAugVMM08MBZLWMK0EFsNttv8S6z5JbMd/CdEi7bL92ofcxhyH9Xth7O+tQHavJLZ1oYPiMZJr0Gb4fC9vS7LfRQ4a4WN1ZIR1LJ1TKk9XYYECIVTsYQlQsvf+d/WB027wuHNtrXrln6/qR0V1SE4JPmA223cnWBRdN5p/9sgLYZ1LQByw0THdUNwjy6eDH0Avv6lreyOIqkl/qF+MK2G1lxpKDB0q1ucSiUt3Q7m7JwL83zBvKYFrZB13K9qin/Xi2DzTLs/8C6/+Kc3tn8jnrDmjMn/hb3zITcoDkxwV7y6XENg/ze2x9EsHw5thPwfWGHo96ua13Y4zW6DTQ9nrYhcGfb7tX+/aefaLV+vRAru5vz4jxXhB0hvYv/C4dU0c9ZK2zItWlP7/xwWl/bhnIE2wmtGC295CubcGFYHi+SmGNwSl7QoJkCP7/TcrTYiaPkByA7xO6kLTuDAXBc98Egc9wt3DaA4k/xmn/Fv2W16Vugf42lBUSFDLY4SyXQSTFYHyL/U2npbhwjFGvxDiNa97/0TOGORvS+UIAR2xUc+6f/hVD9HIK2x3R1wZ23hr5G3oIkkWUd54nUkd8NwdvZ4M25G6OPtT6v/Z7c8zvbC8sZErzg6TIG242FQmDWhT/uipjfVhLetacrN2Eq86XtD+HN1bTBEo2lHO2bQarA701/3K+DEV0KfO3ud39sKYNTfrfnYTdTWY5Dkb/l3nhZ59DxY2JuGmGycnllzlSk3XBIuFkc9BprrdbX9on90Ws3nTHgb1v8juikhUOy7XlQz/PLRoEkYe+xJr/vHa+qbbhdZL5pgmnjp9h+FSLkZzW1I7nDk+XpwUxZC0y41QwsfbeLSiDhK0YfHPBv+XHZP+zdtvTX3tYhxlb9jhOQX/2iI2BaRNLK2zJD/0CDBPuEO2DwDilbF9rxITHzXtv7ihtjewRAXA4jNusA0nytqVsc45sElA++C1Q/Z/U5n2yUSg131EkWnqdGvqcUxEFE2eI5Bouj1k5orYIFtYHilHiY7eSbqeEzDQMUf/K39rDAxwAO/cCe+Cl2/Z+OgxyL+mVEGlDtO9p7m1FD58LWUvP5YwnnlHA0a+QZO258KE2bB9jlW/NuMTFyeAIY96q1r334SFLwV+8BkMhLsCZfRwk6ucktLX3DgFv0iX6e4RsXfFQEC6sZuGDGpIDFOz7R+8uv+EXuagb2VWpP2joGWkltE7OQxpzLucHrcJ7zEhNf/+diX7MC1M69BoZbZpnEr6yDhlm4X2wHb1iHWuVViIvkHfONB4EBosLgOj7BAdTgu2Gf928fPst40PX8Mk7+sWx4dnNZm9ZyBBiT+YCeP1cUXPR70/33d7m+UZQdvFT8dgnq0jSJM/AqHCn9cUfF3Q+MQ7oXH/dJO7uhyQQzptbIDcJ3Prnvegmk7Hgb80fYmlNgYdLeNTdPVGfhtYBXosUjroXDORrveNaDvNPGo+Luh9dAQx4bAhYXhxwkShaTZiTLOoPGxMEDWEMk5wT9ZKpQHmOKdZt1s/B6o34loiivU5u+GbpfCxhd8H44BG7Qbqs1TKv4x0+9mO4GnXZSAc4p7mnWF0z63kyuVhKLi75mGIqYxevsoftLSVfjrg7xxic6Bgpp9vNPh9ETnwB1OsKlBf0psPhRFOSbRlr9X3EZpTDTpjY8NN0lFUY5JtOXvChVRRVGSCxV/RVGUFETFX1EUJQVR8VcURUlBVPwVRVFSEBV/N9QKlqYoitKwUfFXFEVJQVT8FUVRUhAVfzc4i4b3+Vli86EoihInGsh01QSTNw5OegM6Tkl0ThRFUeKCir9bupyb6BwoiqLEDTX7KIqipCAq/oqiKCmImAbiwy4iu4FNMd6eC+yJY3YaAlrm1CDVypxq5YW6l7mbMSYv+GCDEf+6ICILjTHDE52Po4mWOTVItTKnWnmh/sqsZh9FUZQURMVfURQlBUkV8X8y0RlIAFrm1CDVypxq5YV6KnNK2PwVRVGUmqRKy19RFEUJQMVfURQlBUlq8ReRKSKyWkTWicitic5PfSEiT4vILhFZFnCstYi8LyJrfdtWicxjPBGRLiLysYisFJHlInKj73gylzlTROaLyBJfmf/oO560ZQYQkXQR+UZE3vZ9TuryAojIRhH5VkQWi8hC37G4lztpxV9E0oHHgTOA44FLReT4xOaq3ngWCI46dyvwoTGmN/Ch73OyUAH8yhjTDxgN/NT3v03mMpcBpxhjBgGDgSkiMprkLjPAjcDKgM/JXl6Hk40xgwP8++Ne7qQVf2AksM4Y850x5gjwMnBOgvNULxhjPgX2BR0+B/inb/+fwLlHM0/1iTFmuzHma99+MVYcOpHcZTbGmIO+jxm+P0MSl1lEOgNnAf8IOJy05Y1C3MudzOLfCdgS8LnAdyxVaGeM2Q5WLIG2Cc5PvSAi+cAQ4CuSvMw+E8hiYBfwvjEm2cv8EPAboCrgWDKX18EAc0RkkYhc4zsW93Inc0hnCXFM/VqTCBFpDrwG3GSMKRIJ9S9PHowxlcBgEckB3hCRExKcpXpDRKYCu4wxi0RkYoKzc7QZZ4zZJiJtgfdFZFV9PCSZW/4FQJeAz52BbQnKSyLYKSIdAHzbXQnOT1wRkQys8L9gjHnddzipy+xgjCkE5mLHeZK1zOOAaSKyEWuyPUVEnid5y1uNMWabb7sLeANrwo57uZNZ/BcAvUWku4g0Bi4BZiU4T0eTWcAVvv0rgLcSmJe4IraJ/xSw0hjzQMCpZC5znq/Fj4hkAacCq0jSMhtjfmuM6WyMycf+dj8yxvyAJC2vg4g0E5FsZx84HVhGPZQ7qWf4isiZWLthOvC0MeaexOaofhCRl4CJ2NCvO4E7gDeBmUBXYDNwoTEmeFC4QSIiJwKfAd/itwf/Dmv3T9YyD8QO9KVjG20zjTF3iUgbkrTMDj6zz6+NMVOTvbwi0gPb2gdrln/RGHNPfZQ7qcVfURRFCU0ym30URVGUMKj4K4qipCAq/oqiKClIg/Hzz83NNfn5+YnOhqIoSoNi0aJFe0Kt4dtgxD8/P5+FCxcmOhuKoigNChHZFOq4mn0URVFSEBV/pe5UHIald4Cpin5tfVNedGzkQ1GOcRqM2Uc5Rqksg5nN7P6yu/zHJ/wbOp4FRzPezvb34ePT/Z/HPAfdLzt6z1eUBkSDmeQ1fPhwozb/Y5AXXYh749bwvT31XxFEysukudD2JBDt7CqphYgsClgXoBr9JSixU17k359u/H85A2ped2QfvJQG2+fUX172L66Zl/an1zz/4UR4Kd1WEA2kwRMXqirg4EY1hcUTY+DQZqg8kuic1AkVfyV2Fv7cbse/WfP4mUv9FcHUgGi0H0+uvx/Mf4bY7Ymv2O0p7/nz0O3Smte+dAx/7SsOw/qnrGDXFWPsO5/VHT4+A6rK655m6R6Yfx2sfszmtSFgDGyaCXvmxye9pb+Ht7rB28dByfa6p1dVCUt+b/9Kj16Q0rj8CryuISsiv/Wtq7taRCbHIw9KAtjwnN12mhb+mhZ9rQA7zGgS/5b37i/9+10vqH1+3Is2DxeXxfe5gVTGKe2lt8NXV8HbfeHjM2Hbu7GntfND2PkRtJ0IO+bAG53g0/NsqzVWVt4H656ART+H90+KT4USjni904I34IuLYc4o+OBkWPVw7GmV7YOV90PrEVC6E/7dB94fD7u/iD3NbW/D8nvs39vHwaEt0e+JA/FqAj2LyzVkfWutXgL0993zF996u0pDwvmCNmruzpYfWAF8dVV88/L+WLuduibydemN7SA0wNZ34vf8r66CGVnW46mubHoZmnaF7D6w6xOYN72mec0L29+DtMZw8n9g3AzocDrs+BDmnmVbm7GwbTa0Pw1GPQX7v4aCeoqovO5JmNkUPjmn7pXAxpcgPRPyToTCJfD1TbH3AnZ9AlVHYNjDMOlDyJ8OhzbAx1PgcEFsaW59GzJawuQFUHEIVj0YWzoeiYv4e1xD9hzgZWNMmTFmA7AOu1iB0pB4q6vdjvfw47+4xG6/ezp++ThS6N9v0Tv69YP/ZLefnBWf5+/72pppmnax3k4F/449rZKdULIVjrsJzvoWJn0MR/bDxhdjS2//EmjZ3wpft4tg7PMw+mk4sAwKXo9+fzAVh+DACsgbB92vsAP5W9+OLW8Rn1MC3/wGsjrB1lnw7Z11S2//19DpbDjtMzhnM2S0gLWPx5jWEus00GoQ5I6GkX+DUz+FylJY+UD0+0Oxb5FNq81waDfJVrBHgfo0foZbc9L12roico2ILBSRhbt3767HrCqeKC/277c/xf196Zn+/XiZft4fZ7fdL3d3fU6cVz7c9DKkZcCUhdC8J6y8N/a0CpfYbavBdttmhDWbbX41xvSWWpEKpPN5kNURNs2IIb1vAWPzl5YO7SbalnC82f4elB+wFVW3S2HN47GPL5QXwcHv/O80ozl0Od/2WGIZfypcCtm9oVFT/7Hm3aHTVNg80/v3uvIIHFjuz1/7U6B4DZTs8J43jyRi5Mv12rrGmCeNMcONMcPz8mqFplASxbzv222PK73fO+IJuz24Pj55ObDCbkc95f6ethPtti62b4ft71lzQmYe9Pgh7P489u7/fp/45/gEW8S2WHd/ZlvdXijZaW3SOUHin5YOHc+EHe97t9c7HlWOUOWOg0Mb7bPiyfZ3oVE2tJ0APa+CimLYFqOZbv9Suw18D53OtpXL3q+8p1e4BHIG1j7e6Wzbaytc6i29olXWjJQz2H7OO9Fu93wZ9pZ4UZ/iH27NyVRfW7fhs9Vn2hgVg/kmd4zd7p5X93wE2q3TPMxXHHKf3QZOSouFsr32x95ukv3c7WK7/e6f4e+JxP7F0LQzNGntP9buVCsOXt+XI0KhhKrDFNsi3rvAe/4ycuyYBEBrn4eV02OJF7vmQtvxtkfVdjxkdYDvno0treoKK0D8250MCOz82Fta5cW2FxFcoYJ9pwA7Pqhb/nIGWrPS/ji/0xDUp/iHW3NyFnCJiDQRke5AbyBOPlhKveN0lTPbxjZpq2V/u/3qx3XPy8bn7banx7Ra+URrvYfeQih2fWq3bSfYbXYvOxi69i/Wv94rhUv8LUCHvHEgjazXjqe0Ioh/25Psdvdn3tLcv8S2+p3/uyOCgXMs6krJDiha7X+naY2g59XWDl4cQ2+xcAk0aWPHDxwat7LfAc/v9Fu7DfVOm3a0Zj+v77RwCaRn2QF+sOak7D5QuNhbOjEQL1fPl4Avgb4iUiAiPwb+BJwmImuB03yfMcYsx65FuQJ4F/ipMSZG1wPlqLNrrt0OuT+2+9PSIb0pNGpW97w49vUBHlvwaXFyLts51/5w2wT4K/S+Dkq2Wa8aL1SWWhNAsI0+o7m1/cci/lkdITO39rnMtlZgvLgnmqraYwhNWtteQDzFf+dcu2030X+s1zWAwIZ/eU9v/xJbSQU3VNqdbE0rXsYSnAq1VQjxB1up7v7Cm91//2JoeULN72TOoIbT8jfGXGqM6WCMyTDGdDbGPGWM2WuMmWSM6e3b7gu4/h5jTE9jTF9jzH/ikQflKLHva7ttPSL2NPreaG2udZ3QktUJmuTaVpdXMtvbbWVp7M/fNRdyx1oXUoeOZ9iB7e0e/fOL14Kp9PeMAml/OuxbaH3M3eKIXjjyxsGeee6F6vAWqDwMLY+vebzV4PiK/6651hvH6Z0BNO1kvWG8vlNjoGhFmHd6mjWn7fLQUt+/xLpkOmavYHLHQtke+790S9HK0O/00Maanmz1wDE81VE5Jjm00XabW/SNPQ1nwHCHx9ZsMIVL/X77Xhn6f740vo3t/iP77b2BLVSwwp871t9DcosjGC361D7X4TTb8nZro66qsKISHGYjkNzRdszi4Hfu0izyzaHIDspfq8FQvDp+s313fWoHkoPHcNqdYivAQE+zaJTusAPl2SFcgNueZOdA7HjffXoHvrXvNJy50xnP2uvSil1ebGcIB/+WnN9HPbf+VfwVb6z7mxW+ugRp63gmIFY0YqV0t8+bJUwXPBqthtpt0arI14Vj/xLA1DT5OOSOsRWDl8lJxevstnmv2ufajLRmMrcVysENtlXbsl/4a1r6XF7dlr/YEf8gIW091FZM+79xl04kKg7b/IR7p6bSWy/DeaehxL9RU++VdNEqaBHhnbboC5Lu4Z2GyV9r33dzn8cBeY+o+CvucUwETbtEvi4aGc2tMG2rg8WvevAtQus2Etk97UBqrOIfaUC1RT8rVG5b1WCFoEkeNG5Z+1xahhVEtx4/xWFa6YE454pcVsDFa+1YTVaQia3NaLuNh2vigeXYeQQh3qlTkTllc0O1uIaoUAHyxtrKxI0bbdk+21OK1ONNy4DmPby9U6gt/pltbTr17O6p4q+4x7HR9/t13dM64vOzjnWy1wFfGKlYxT8tw1YAdRH/JnmQ2a72OefH7IiPGw6uC91Cdcgdaz1Ayg9GT8uN+Gfm2hm6bsW0eK0V0eAeX1Y7K1R1iW3jEKlCbdrV/s+8vlNpBM3yQ5/PHWsraTdmGjfvFCC7r7d3CqErp9yx3gePPaLir7inaKXdRur6uqX1MLstjXEmY+G3drA3lPi6pcVxdTD7LPX5ZIcwfzk/Zi8Df8XrwrdQwWf2qLKhAKKmtdaOyzRpE/m6Fn29tVLDVU5tx1sXx7oK1f6ltnfRvEftc2mNoFl37++0WX74OSC5Tq/FxWSvcK30YFr08Q3euwihXbzW9qRCeb61PcmaNb2U1yMq/op7HPGPZEt2S98b7TbWAdfCKINvbsjua39cXn3yqyptzyPceEOT1r5WtcsfbmWpnRUcSvQc2oyy273/jZ5e0RrbQo32brL7uGulVlXYcYRwwtd6hDWJlNRxrmbhUt//NIwsZff2JoYH10d+p03a2Hfg9p1KWuT0wKZXWeJulnek3p7jTed1xrAHVPwV9xxYaafdB06YiZVqj4YYBgpNlRXfljGafBxaHGdDHBza6O2+g+vtDzycvzdYkXCb7qFNgIksLJm5dhKR21ZqtBYq2JZ/ybboHjSHC8BU2OeHSwfc9yJCYYxP/OP0TsFWWNHEus0o+06j9VqK19qeR6Bbbyi8vIvi9RHeqTMmE2PP1AUq/op7ilZawYzHcoxNWkOzbrAvBvE/tNEO0sVq73docZzdev2BRbJNOzTr6hN1Fzit2XBC4NBmVPR4NJVl1ic/kgmpOo/5dhutlXpogy9/3UOfj/U9BlKyza74Fu2dlhfZ8aJolO2z6WVHe6cjrenxcJQY+gcjCHWNPObbbbT0Kkrsc8O900bNrGNFXSrUKKj4K+45sCI+Jh+HVkNia/lXe/rUMUJndSvNq/h/a00AwZNzAmnqE383dvADLs1pbYZbkYwUSK1kK2BsxRqNLN9Et2jjLs6qYuGEKqujXdehLkLl/E8j9aacyVVuKlW341NtfOaVaN/Dw5u9vdNoK3w5ZQg3GA2+MRlt+SuJ5sgBKzzxGOx1aJJrbc5uWnKBOEIRauamp+e3tm51sbT8s/vWDFEdTHYvOyO2ZGv09IpW2oHrxq0iX+e0ig9EGCdxIpW6ccfN7GC3roRKIKtz6PMivvGDuoi/05uK0JurHkh38Ry3FarzHdofwbZeWWo93ZqFmdkbSHqmDX4XrUKtFv8IFUqL42yFWk8ePyr+ijscgYxny9+ZzOI1KmTht9b+mpFd9zyU7vIe4G3/4ugmJ0eo3czSPLDSXaXqPDPSILkX8a9upUYRqsObbWTNSPbuunhOgX1PTbtErgBbHm8nUbl5p0UrbdylplFa6xnNrTknUoXqmMXczm/J6hC9Qj3s+z9FqlCy+9pw1rF6xEVBxV9xRzzdPB06nWO3XqexH1hWd3u/Q5pP0Ny2ro4csGMOwQHYgnHEP5q3hjGh47uEIrOt7SFESvOwB/HPaGlbqqXRhGpL9PRa9LUVT6xhHgqjxCICm9cWfV1WqCvstW6C+OUMiPxOD7kQ6kCy2rsQ/y3Y3lSEuFQt4zCWEgEVf8UdB1ZaoYzmPeGFrA5WzPZ/7f6eyjLbFY6X+DvRSd2YZyBgvCGKUDVuae250YSqZLsNcue2Us0ZEL3l3yQPGmVFT0vEBrhzI1RuxB8Tm196ZVnoiKahyBnkrqdY5LI3BbaiLl5rB2FDUV2huhT/zA7RW+uHt9jvf1pG+Guy4+BFFQEVf8UdRSut+6CXRVOiIWJdJr0s1FG0ys7KjJf4V8dRcTnw7MbTx6GVC6HyOnciZ6ANgxBuAfbDW9y3UMFnooggVKbKVijRxN/xhPES0sLhwArf/9TlOz28JXKE04pD1qbupUI1VTYCaCgO+Tx3moYZ8wjGMftE6k0e3Bi9MmnayTa4YnmnLlDxV9zh1i7tFSeQWXmRu+vX+1YPc2MmcUPOIEDczZwFK/4ZOe6EIGeQHdAO16IE/8CkF6GqLLUThELhppUeSGb7yGafw1tsBe2YIMLh9AhjESovFarT44rU+6ken3L5HYk2lnJ4izW5pTdxl15We/vOIn2ni1dHf6eSZnuPKv5Kwqgss37O8RzsdTjhNrtd/4y763f5who7XeK6ktEcMLDsj+6u37/Etj7dzHXIGeibkLY8/DVFK238+qwO7p4fbSzBq/hHG5w84BPSaO+7cSs7hnBwg/tnOxQutfZ8NxPTqssfoUfl1tPHoXkv+/xwHj+eK9QoXlRHDthzbr7DzXvE9k5doOKvRGffQsBAk7bxT7vdKXYbrssdjNM6izbTMhaiDfqaKl9Md5dhpJ01bvdECB9wYAW0ON79xLkW/WyLMFQrtbzI/nkRqmbdbIjucO62Tiu6RbRWqviEKoZW6v4ltVezCkdWB9sKj/ROi1Zar6BQ4bFDkZZuXT7DefwcdmH2CsQxu4WbjezY8KO9U4j9nbpAxV+JjhNSoC4LuISjcQ60Hu7eoyE9y19hxIu8cXbrRAoNx8ENvpnFLsW/WXcrKhtfCH9N0UpvPapGvvVeQ7X8I0WJDIcjQAfCVL7Fq62ZK9NFxd+8p+0hesEYn6ePy3cqAp2mwdZZ4SOcHlhp34GXBkLOwNDvtKrShmGI9zsNvC4SzXtCxUHvc2FcoOKvRGft43abd2L9pN9mpB1wjRYJsWSntaV2Oju+z+9+ud2u+3vk67zYpsEKVY8f2sBhoWLyHNlvIzd6HUsJ5/FT3aL0UEk7AeN2fxr6fNEq9yE9nNg74QajQ1G60y596GVRnh4/tJXw+n+EPl/k6015IWeAnfMRPHv68GaoKvNmZszMs+9i1ydh8rfKhpqOFnoCoM/1cHFp6HUe6oiKvxIdp9vpxn0wFloPtZNZonVvnUFZx0MnXuR/324j2ebBhgCQdG9hJXpdY2cyO4vNB+LVNu3QarB9V2V7ax4vWu2LPOlCVByy2tnw2htfCm32KloVfWDSIbunXUGsxEVESwcnrIIT6M8NuWPsAuwr769d0VQesaGcY3mnUHvg31m+0muvt+OZsP290F5JRavsu4rk5umQnunOHBYDKv5K4nEW63YWhw/H8nt818dZ/Bs1g7yToke33LvAmnEaNXWfdkY29LgSCmb5XQYdYp0413aC3e6cG5Teausd4tYrxaHn1db0svvzmse9DEyCv7UdKVRCMHsXAOKtQheBPj+zczO2zqp57uA66zbq9Z22GWXdKncGrSvtmGiiLeISTM+rbY8hVO+kaFX8HBbqgIq/EpnDvhjtQx+qv2c48VVWPxz5OsfUkdE8/nloPdyumXqkMPR5Y+z51sO9p937p0BV7fIdWGFbdpGCe4WizUjrWRMsfMWrYxOV7pfZiWGrHqh53MvAJNgeRFpG7UokEnsX2PS9huroNM0OVq+8v2aPxbGze235N2pqF6XZOqtmekWr7bt2M+YRSKuB0P5U+z8PXC+iqsKOzbh9p/WIir8SmS2v2m1dI2hGwmmpRlpUo/ygNQ31u7l+8pCZZ7f/vTL0+aLV1szirP7kheb5kP8DWPV/8N8f+cWgcJkVa6/d+rQM6HoBbHnNH3fGVFkTRQuPLVSwwtfjh1b4NjzvP+7YrN22yhtlQd542Pi8PyRCJEwV7JkX2ztNawTH/9beP2c0lO6xxwt9g/axiGu3S6wwb3/Xf6xolW9h9hjCmPf5mQ2G+M2v/eapvV/ZNSTibbqMgThO11SSEsfmmTumfp/TaqgN81BZFtpsseLPdhttacJYOe4XsOR3UPBW6PM7P7TbthNjS3/Yw1bsvnsGdnxgffsPLI99PeR+v7F2+n/3tt44R/ZbM0Pu2NjSO+E2uxTjl5fBkt9agSrdaXs6XmYMD/4TfHiyFeR+N0Pn8+z9oVbn2r/YxtyP9Z32utrmcdldMPs4G3W0aJX13gq1NGI0uk2HFffC3LPsRK2Kwzb0Rt+bYstfp2nW/LP6YdjwnM3T4a12QaT2p8WWZhwRU48LBMeT4cOHm4ULFyY6G6nHi2J/uJd68OCIhbVPwILrrGAMCTE4+qKv5XXRIW82dy84zxjyf9Dvl/7jxsD746xXytTVdVvMZuNLsHmG3W9xPPT/bezRSfcvhTWP2fykN7Wmjp5Xx56/ylL7f9i30LrUNu8JPX/k3eSx+0v4+pf+nlxaBvS7BQbdXfO6r38Nqx+E87Z7f0bw89Y86p/jcPzNscegKt0Fy/+fda9Mb2oHxPvcELup0RjYNAN2vGc/Z3W2PYycOoYj94CILDLG1LJXqvgr4akqh5d9vtLT6/l7UroHXveZXia+Ax3P8J+rOAwzm9V/Pja8AF/+oGZld2Q/zLsMts2G4Y9b1zvFHYXfwu55sPoh+x7P98UQMlXwzc2w6kHInw5jn4+YjFI3wom/2vyV8GyfY7d9bqj/Z2Xm+vfnnlnznCP8jj9+fdH9+3bwuf3p/mMLfgo75kD/31u3TcU9OQOg90/s+6ws8x9f87gdXO52KQx9MHH5S3ESJv4iMkVEVovIOhG5NVH5UCLgrBc78M6j87zAVv3Gl+z2xQATxmiX8X/qQuMc66sO1tNp8wzo+wtrsohnRNNUIr0JVJXafVMFqx+xrrVjn/cPtCtHnYSIv4ikA48DZwDHA5eKSJzCNCpxo2SbXWwi2vKC8eRMn7fGvOk1hX/sS6EHDeNNWqadyfvJNJjjm/3a6+r6f24yk9bEjid8ej68M9D64ve+tm5jJ0qdSVTLfySwzhjznTHmCPAycE6C8qKEo7LM/nCPJqEGwia+A/mXHJ3ntz/VemMc2mQnn42f5S2ui1KbvLF2ALZ4jR3YHfaoNfkoCSVR/dhOQOB0xwJgVPBFInINcA1A164e3M2U2DDG+o4DdDnfug56nS0aD6YbWPl/1v/bCbp2tOh/q/1T4kfHM2Cax4BvSr2TKPEP1d+r5cZhjHkSeBKst099ZyrlWfUgfPMru996hJ3RGm25wvqi368S81xFSRESJf4FQGCA7M7AtgTlpeFTshMW/8baxJ2p/41b2YlZjXPcpWGMnYzSdrwNdLb6UTuzsb49bBRFSQiJEv8FQG8R6Q5sBS4BpicoL97Y+jYsvcMKbXqWnanZ56c1/dLdUrbPzlBMz7ITlxq3iS2C34LroOANm5fA9XCzOsI5G91FD9z/tQ1fO/CPNhCZujUqSlKTEPE3xlSIyM+A94B04GljTJR4uscAe/4Ln18EjZrbae+VJdYz5NNz4dytNX3Vo6Y1Hz4Yb+3qDmmNbaz6cS+7dyss2QEFb1pf/GEP2iiMFQftAiLL7rahf92Eo93li+d+DEw7VxSl/kmY47Ix5h3gnUQ93zObX4PPL7Ct6TO+8U9H3/EBfHSaXQIu82SXab0Kn19oY8OPftb6PlccsgtqbH7FhtdtPcxdWoXfAgY6TrG9kaad7PGOZ1nxL1rjTvyL10Lj1v77FUVJapJ71oqpsjFGdn1qW+sdJvsXDPfK5xfYbbdLa8YhcULoFq22C0y44csr7DbvJOhxhf94u4lW/IvWuBf/0l12GxzLxFkMu3iNu3Qcn35FUVKC5A7vIGl23dUmbezyckt/D7u/qFuaHSbX/Ny0kw0A5cQ+d0PlYbttO77m8exegLgXbLBjBlA7MFaT1nYFKdfiv13FX1FSiOQWf4Dxb8Ip78NZKyCrEyy6KfRydW4JbpFLmo2h7kX824y02xN+X/N4eqZdoKLIg/iX7bIDuhk5tc9le8hXyTZoquKvKKlC8ou/M4U8ozkMuNOGq939mbc0nIUYulxgW9TBZPf1L/fmhrJ9NqxrKC+c7D4eW/67oEnb0FPls3vb9UyjYarswHFmB/fPVRSlQZP84h9I/nS7oMKmGd7uO+JbhNlZOzWYFn2tWSkwcmEkyvZYk0zItHytdbe9k9Kd4WOhN+0MpTtqL3IdKj+mQs0+ipJCpJb4N2pqB1md5enc4gyqhhPZ7D629Xzwu+hpVVVAeaH16Q9Fs+52ucLyQvd5y2wX+lzTTnYx67JdkdMo2W63WdryV5RUIbXEH6DVIGtWCVxUORplUcS/eb7dHtoUPa0j++023HKETTvbrbM2azRKd4XPl9OSP7w1chplu+22LqspKYrSoEg98W9xnF2h6uAG9/eUOB41YVrYzbrZrRvxL9trt/EQf2NsxRRW/H0++yVRImdEy5OiKElH6ol/tV/+Kvf3RGv5Z3YAaeRS/PfYbTibvxfxryi2cdLDVUpOy78kWsvfJ/6NQwxmK4qSlKSe+DuzXb1455TusrNxwy1qkpZuhdaNYB9xWtlhxD+rAyBweEvo88H5AuvtE4rMdtYV9bDLlr+Kv6KkDKkn/k1aQ5M8by3/0l32nkgrSTXtDCUuxD9ayz8tA7Lau6tIog1Ep6XbiqF0R+R0juyzC5ikN47+TEVRkoLUE3+wrX8vk7Ii2dUdMtv5xThiWo74R7CvZ3W0fvfRKI0yFgE239HyVbY39PwFRVGSlhQV/+O8t/xdif/O6GmV7bEzedOb1j2taC1/t2kd2Rve9VRRlKQkNcU/u7cV4fIid9c7s2gjkdnWtqCjuZCW7bUmn0iLV3sV/yZ5UdKK1vLfA5kR0lAUJelITfGv9oJxYVoB3yzaCKYV8J03frNOOCLN7q1Oq601NUWb5Vu60w5CR7LVZ7aNXpG4yZOiKElFiop/e7uNNhAKUHHYLo4S1ezjO++mlR3NxJLZzs5FiDbL1+1YROVhKD8YOU8q/oqSUqSm+Gf6xN9Ny9/t7FenZxCPVraTVkmUtFyZo3xphQvxUHnEmr9U/BUlpUht8XfT8nczqAoexH+ve/GPllakoG7VafnOh6tIos07UBQlKUlN8W/S2s7IdeVOGWUilYMbs09VhY3t48bmD9EDskUK6ladVpSKJNq8A0VRkpLUFH9J83nBxLHln9HSLsAeqbV+ZD9gosfQcWP2qSq3k7PctvzDVSQq/oqSkqSm+IMd9PU0kSqKyIr4vXTC4VZoG7exFVSkiqTU5VhEkyhmHxV/RUlJUlf8M9u7b/k3am7XAoiaZrvIrfUyl/b1tHR7TSTxrw42F8Xsk97YuoOq2UdRlABSV/yzXIp/2a7Ik6gCaRLFp95NaAeHzHaRexFuxyIgco+k1EOeFEVJGholOgMJI7O9FdCqStvSDoebCV4OWe3gwLfhzztuo25a2dF6EW7NUU5akVr+GS1DryesJJzy8nIKCgooLS1NdFaUY5zMzEw6d+5MRoa733Jqi7+ptOKXFUHcS3ZAdk93aTbxBVEzJnT4hmrxd9GTaNIWiteHP1/q0uzjXFO4NPQ5neB1TFNQUEB2djb5+flIpJAgSkpjjGHv3r0UFBTQvXt3V/ekrtmnWVe7Pbw58nWl2+1iLW7IbAdVR6D8QJi0dvvGD7LcpRXN7JPWGDJaRE/LqZRCoeJ/TFNaWkqbNm1U+JWIiAht2rTx1ENMYfHPt9tDG8NfU3nEDtK6Xdg8mk+9m+igDlntoOKQ/QuZlm+ClxtRyGxn3Uwrj9Q+p+J/zKPCr7jB6/ekTuIvIheKyHIRqRKR4UHnfisi60RktYhMDjg+TES+9Z17RBL1zXYWXT/4XfhrnAFh1+IfZaKXl8FjVxWJy7GISL7+Kv7KUWLhwoXccMMN9fqMZ599lm3boqxc54I333yTu+66K+S55s2b1zn9WHnsscd45pln4pJWXVv+y4DzgU8DD4rI8cAlQH9gCvAXEXFGVf8KXAP09v1NqWMeYiOjhRW9SOJfst1u49XyL9nhDyoXjaj++S7i+tTKl4q/kjiGDx/OI488Uq/PiEX8Kypqh2G/9957uf766+OVLdfPjcaPfvSjuL3DOom/MWalMSbUkljnAC8bY8qMMRuAdcBIEekAtDDGfGmMMcBzwLl1yUOdyO4deVEXz+LvtPzDif82fzjpaGRFCcjmJq5Pdb7CVEoVh23ETxV/JQJ33303xx13HKeddhqXXnop999/PwCLFy9m9OjRDBw4kPPOO4/9+/cDMHHiRG655RZGjhxJnz59+OyzzwCYO3cuU6dOBWDfvn2ce+65DBw4kNGjR7N0qXVIuPPOO7niiis4/fTTyc/P5/XXX+c3v/kNAwYMYMqUKZSXlwOwaNEiJkyYwLBhw5g8eTLbt2/n1VdfZeHChXz/+99n8ODBlJSUhLzOyePvfvc7JkyYwMMPP1yjvGvWrKFJkybk5trfxYYNGxgzZgwjRozgD3/4Q41r77vvPkaMGMHAgQO54447or6z4OeGy9/69euZMmUKw4YN46STTmLVKqtTTZs2JT8/n/nz59f5/1pf3j6dgP8GfC7wHSv37QcfD4mIXIPtJdC1a9f45zJnIGyaEd47xzH7uB3wbZILSOgWdkWJDceQFba4NYnUizAmNrNPcL50glfDYtFNsH9xfNNsNRiGPRT29MKFC3nttdf45ptvqKioYOjQoQwbNgyAyy+/nEcffZQJEyZw++2388c//pGHHrJpVVRUMH/+fN555x3++Mc/8sEHH9RI94477mDIkCG8+eabfPTRR1x++eUsXmzLtn79ej7++GNWrFjBmDFjeO2117j33ns577zzmD17NmeddRY///nPeeutt8jLy2PGjBncdtttPP300zz22GPcf//9DB8+nPLy8rDXARQWFvLJJ5/UKvMXX3zB0KFDqz/feOONXHfddVx++eU8/vjj1cfnzJnD2rVrmT9/PsYYpk2bxqeffkrTpk3DvrPA55aXlzNhwoSQ+bvmmmt44okn6N27N1999RXXX389H330EWB7UJ999hkjR450/W8ORVTxF5EPgFC2ituMMW+Fuy3EMRPheEiMMU8CTwIMHz48ysomMdBqEKz7Gxze4vf+CaRkOyDuW9hpjexkqVCCXeLrijZ1Kf7O2EAos095kfUqqmvL/+AGu23WzV06Ssrx+eefc84555CVZT3Uzj77bAAOHDhAYWEhEyZMAOCKK67gwgsvrL7v/PPPB2DYsGFs3LgxZLqvvfYaAKeccgp79+7lwAHrJXfGGWeQkZHBgAEDqKysZMoUaxkeMGAAGzduZPXq1SxbtozTTjsNgMrKSjp0qN1Ai3bdxRdfHLLM27dvJy/PPzb3xRdfVOf1sssu45ZbbgGs+M+ZM4chQ4YAcPDgQdauXUtxcXHIdxb83HD5O3jwIPPmzavxPsvKyqr327ZtW90TqAtRxd8Yc2oM6RYAXQI+dwa2+Y53DnE8MeQMtNvCpWHEf6sVzjQPHaRwyyaWbLVbty3/9CaQkRPa7OM22JxDRnO7ZnAt8V9nt27nMSiJJUILvb4w0VaTC0OTJk0ASE9PD2nbDpWu4/vh3JuWlkZGRkb18bS0NCoqKjDG0L9/f7788suoeY90XbNmzUIez8rKqq6IgvMWnP5vf/tbfvKTn9Q4/uCDD0bMl/PccPkrKioiJyenuicUTGlpaXXFUhfqy9VzFnCJiDQRke7Ygd35xpjtQLGIjPZ5+VwOhOs91D+O+O9fEvp88VrI7uUtzXCzaQ/7xN9tyx+s3T9kL8JnOXM7FgG+5RyDKpLi9Ta0ddN6MKkpScGJJ57Iv//9b0pLSzl48CCzZ88GoGXLlrRq1aranv+vf/2ruhfghvHjx/PCCy8AdiwgNzeXFi1czFkB+vbty+7du6tFs7y8nOXLlwOQnZ1NcXFx1Osi0a9fP9atW1f9edy4cbz88ssA1XkGmDx5Mk8//TQHD9pV8rZu3cquXbvCvjO35WjRogXdu3fnlVdeAWwlsWSJX6PWrFnDCSec4OJNRaaurp7niUgBMAaYLSLvARhjlgMzgRXAu8BPjTGVvtuuA/6BHQReD/ynLnmoExnZ0LwHFIYR/6LVkN3HW5rh1sx1vIq8mFgy2/vNRYEUr7Xb7N4e0gpRkRQutZWbl56NklKMGDGCadOmMWjQIM4//3yGDx9Oy5YtAfjnP//JzTffzMCBA1m8eDG3336763TvvPNOFi5cyMCBA7n11lv55z//6frexo0b8+qrr3LLLbcwaNAgBg8ezLx58wC48sorufbaaxk8eDCVlZVhr4vE+PHj+eabb6p7Jw8//DCPP/44I0aMqNEjOP3005k+fTpjxoxhwIABXHDBBRQXF0d8Z27L8cILL/DUU08xaNAg+vfvz1tv+dvIX3zxBaeeGotBJghjTIP4GzZsmKkXPjnXmH/3rX28rNCYFzBm+Z+9pbfwRmNmZNc+Pu8KY17v6C2teVcY80bn2scX/cqYl5oYU1XpPq2504yZPcD/ubLCmJk5xvz3Km95Uo4qK1asSHQWTHFxsTHGmEOHDplhw4aZRYsWJThH9c8NN9xg3n///Zjvr6939vXXX5sf/OAHYc+H+r4AC00ITU3dGb4OOYNsSzp4gfPiNXbrueXfDiqKa8/MPbjOW0sd7Czkw1trz8wtXmvTEg//vmbd4OBG6ykEtrdTXghtJ3rLk5JyXHPNNQwePJihQ4fyve99r4YnTLLyu9/9jsOHD8d8f329sz179nD33XfHJS3t7+eOAlMFe+dD+1P8x4t84t/Co/i3OM5uC5fZtB2K10Kns0PfE47snoCBQxugRd+AtNZAy+O9pdW8h62UyvZCZi7s+NAebzfRWzpKyvHiiy8mOgtHnXbt2jFt2rSY76+vd+Z4BsUDbfnnjgEEdvkmKZfthVUPweoH7WBoc4+eMK19Nfy+Rf5jh7bYwdaWHgdpnF7HgZX+Y0cOQPE6fyXjluY97Hb7e7B1Nmx6yfZ6vAxAK4qSNGjLv3EOtBkFK/4XCl63wlpZYl0yh9xvXS690LSr9fXft8D66DdqBuuetOfaexykyRloK6C986HLubaHsuxuMBXQ5XxvaTk9hy9/4D82Oj4xQpT6xRijwd2UqBiPbrkq/gADbof519rQC7ljoNe10HpIbGmJQOsR8N2z9s+hwxTI8djyb5RlJ6Kt+H+w7gn/AvE9fwyth0W/P5DAHkyns6HzOdD9Cm9pKEedzMxM9u7dq2GdlYgYXzz/zMxM1/eo+AN0PAPO3RS/9Ab9rx2QzWxrB2VbHAcdz4wtrd7XwdI77DR8DHT5HvS40ns6aY1gxF+tCWnYg94Gi5WE0blzZwoKCti9e3eis6Ic4zgreblFvHYVEsXw4cPNwoULE50NRVGUBoWILDLGDA8+rs0/RVGUFETFX1EUJQVR8VcURUlBGozNX0R2A7GOyuYCe+KYnYaAljk1SLUyp1p5oe5l7maMqbV+bIMR/7ogIgtDDXgkM1rm1CDVypxq5YX6K7OafRRFUVIQFX9FUZQUJFXE/8lEZyABaJlTg1Qrc6qVF+qpzClh81cURVFqkiotf0VRFCWApBZ/EZkiIqtFZJ2I3Jro/NQXIvK0iOwSkWUBx1qLyPsista3bZXIPMYTEekiIh+LyEoRWS4iN/qOJ3OZM0Vkvogs8ZX5j77jSVtmABFJF5FvRORt3+ekLi+AiGwUkW9FZLGILPQdi3u5k1b8RSQdeBw4AzgeuFREPK6A0mB4FpgSdOxW4ENjTG/gQ9/nZKEC+JUxph8wGvip73+bzGUuA04xxgwCBgNTRGQ0yV1mgBuBgAUtkr68DicbYwYHuHjGvdxJK/7ASGCdMeY7Y8wR4GXgnATnqV4wxnwK7As6fA7grIr9T+Dco5mn+sQYs90Y87VvvxgrDp1I7jIbY4yz1miG78+QxGUWkc7AWcA/Ag4nbXmjEPdyJ7P4dwK2BHwu8B1LFdoZY7aDFUugbYLzUy+ISD4wBPiKJC+zzwSyGNgFvG+MSfYyPwT8BqgKOJbM5XUwwBwRWSQi1/iOxb3cyRzPP9TKF+ralESISHPgNeAmY0xRsi92YoypBAaLSA7whoh4XB2o4SAiU4FdxphFIjIxwdk52owzxmwTkbbA+yKyqj4ekswt/wKgS8DnzsC2BOUlEewUkQ4Avu2uBOcnrohIBlb4XzDGvO47nNRldjDGFAJzseM8yVrmccA0EdmINdmeIiLPk7zlrcYYs8233QW8gTVhx73cySz+C4DeItJdRBoDlwCzEpyno8kswFmn8QrgrQTmJa6IbeI/Baw0xjwQcCqZy5zna/EjIlnAqcAqkrTMxpjfGmM6G2Pysb/dj4wxPyBJy+sgIs1EJNvZB04HllEP5U7qSV4icibWbpgOPG2MuSexOaofROQlYCI2+t9O4A7gTWAm0BXYDFxojAkeFG6QiMiJwGfAt/jtwb/D2v2TtcwDsQN96dhG20xjzF0i0oYkLbODz+zza2PM1GQvr4j0wLb2wZrlXzTG3FMf5U5q8VcURVFCk8xmH0VRFCUMKv6KoigpiIq/oihKCqLiryiKkoKo+CuKoqQgKv6KoigpiIq/knKISBtfuNzFIrJDRLb69g+KyF/q6Zk3icjlEc5PdcI0K8rRQP38lZRGRO4EDhpj7q/HZzQCvgaGGmMqwlwjvmvGGWMO11deFMVBW/6K4kNEJgYsGnKniPxTROb4Ftc4X0Tu9S2y8a4vthAiMkxEPvFFYHzPib8SxCnA147wi8gNIrJCRJaKyMtgQzZj4/VMPSqFVVIeFX9FCU9PbDz5c4DngY+NMQOAEuAsXwXwKHCBMWYY8DQQKoTIOGBRwOdbgSHGmIHAtQHHFwInxb0UihKCZA7prCh15T/GmHIR+RYbU+dd3/FvgXygL3ACNuwuvmu2h0inAzVXo1oKvCAib2JjMDnsAjrGL/uKEh4Vf0UJTxmAMaZKRMqNf4CsCvvbEWC5MWZMlHRKgMyAz2cB44FpwB9EpL/PJJTpu1ZR6h01+yhK7KwG8kRkDNg1BkSkf4jrVgK9fNekAV2MMR9jV6nKAZr7ruuDDd+rKPWOir+ixIhvbegLgD+LyBJgMTA2xKX/wbb0wZqGnveZkr4BHvQtzgJwMjC7PvOsKA7q6qkoRwEReQP4jTFmbZjz7bCx2ycd3ZwpqYqKv6IcBUSkL3YR7k/DnB8BlBtjFh/VjCkpi4q/oihKCqI2f0VRlBRExV9RFCUFUfFXFEVJQVT8FUVRUhAVf0VRlBTk/wOd0vFZLUkyeQAAAABJRU5ErkJggg==\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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IS4ujjlz5rB27VpSUlK49dZbCQsL46+//mLHjh3l+tWtW5e+fftyxRVXsHbtWoYPH84jjzxinO+ePXsICQkhLs7gaXbw4EFuueUWioqKuPrqq0t9Nm+88QYLFy4kPz+f6667jueff97qZ1b2un379jUr3/79+7n//vs5c+YM4eHhfPzxx1x++eWEh4eTmJjIhg0b6NKlYokR1Mrfy2hdp6nxtSdWxoczTgBQOyLWbddQG5xVh5SUFL799lv++ecfvvvuO0xjbW677TZee+01/v33X9q2bWtUfGBY1W7YsIHp06eXatd59tln6dChA//++y+vvPIKt912m/HY/v37WbJkCT/++CNjxoyhX79+bN26lbCwMJYsWUJhYSEPPvgg33zzDampqdxxxx089dRTjBw5kuTkZObNm8fmzZsJCgoy208nMzOTP//8s5TiB1i7di0dO3Y0vn/ooYe499572bhxI3Xq1DG2L1u2jL1797JhwwY2b95Mamoqq1atsvqZmV534sSJFuWbMGEC7777LqmpqUydOrXUU0hycjKrV6+2+29oCbXy90J6N+nIqgObWLb7b666vHulXvtI5kk6N2hdKQpa2fxLY2uF7g7WrFnDiBEjCAsLA2DYsGEAnD9/nszMTPr06QPAuHHjuPHGG43nXX/99QB06tSJQ4cOmR3322+/BaB///6kp6dz/vx5AAYPHkxwcDBt27aluLjYuNpu27Ythw4dYvfu3Wzbto0rr7wSgOLiYurWrVvuGrb6jRo1yuycT5w4QXz8pSSYa9euNco6duxYHnvsMcCg/JctW0aHDh0AyMnJYe/evWRnZ5v9zMpe15J8OTk5rFu3rtTnmZ+fb3xdq1Ytdu3aZVZ2R1DK3wt5rP84Vh3YxKOL365U5X/2QiYARSWO2yodQbf5K19Pz+Ps02VIiCHdSWBgoFnbtrlx9QWFfm5AQADBwcHG9oCAAIqKipBS0rp1a/76y2pSAJv9qlevbrY9LCzMeCMqK1vZ8Z944gnuvvvuUu1vvfWWVbn061qSLysri5o1a7J582az5+fl5RlvLBVBmX28kISatY2vK9P088aKuQA81PsWt15HWX2qDj179uSnn34iLy+PnJwclixZAkBUVBTR0dFG88Pnn39ufAqwh969ezNv3jwAVq5cSVxcHJGRkXad26JFC86cOWNUmoWFhWzf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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# @title Visualizing signals\n", "nsignal = np.concatenate(x_train[:50])\n", "asignal = np.concatenate(x_train[-50:])\n", - "time = np.linspace(0, len(nsignal)* 1/fs, len(nsignal))\n", - "c = ['orange', 'seagreen']\n", - "title = ['Normal knee', 'Pathological knee']\n", + "time = np.linspace(0, len(nsignal) * 1 / fs, len(nsignal))\n", + "c = [\"orange\", \"seagreen\"]\n", + "title = [\"Normal knee\", \"Pathological knee\"]\n", "\n", "for i, signal in enumerate([nsignal, asignal]):\n", " plt.figure(i)\n", " plt.subplot(211)\n", - " plt.plot(time, signal[:, 0], color=c[i], label='rectus femoris (mv)')\n", + " plt.plot(time, signal[:, 0], color=c[i], label=\"rectus femoris (mv)\")\n", " plt.legend()\n", " plt.subplot(212)\n", - " plt.plot(time, signal[:, 1], color=c[i], label='goniometer (degree)')\n", + " plt.plot(time, signal[:, 1], color=c[i], label=\"goniometer (degree)\")\n", " plt.legend()\n", " plt.suptitle(title[i])\n", "\n", @@ -281,106 +256,16 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 221 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 40167, - "status": "ok", - "timestamp": 1586777529356, - "user": { - "displayName": "Leticia Fernandes", - "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14Gj3U_hSW1M2-Ab0tHYcZEiOzvFIfJrkA-pccFhU=s64", - "userId": "17109198128714142667" - }, - "user_tz": -60 - }, - "id": "Z5-x4VIEmj_O", - "outputId": "84b95291-3077-4380-85e5-c1bf83a61b4d" + "colab_type": "code" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "*** Feature extraction started ***\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "

\n", - " Progress: 100% Complete\n", - "

\n", - " \n", - " 748\n", - " \n", - "\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "*** Feature extraction finished ***\n", - "*** Feature extraction started ***\n" - ] - }, - { - "data": { - "text/html": [ - "\n", - "

\n", - " Progress: 100% Complete\n", - "

\n", - " \n", - " 68\n", - " \n", - "\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "*** Feature extraction finished ***\n" - ] - } - ], + "outputs": [], "source": [ "# Extract all features' domains (spectral, statistical and temporal)\n", "cfg_file = tsfel.get_features_by_domain()\n", - "X_train = tsfel.time_series_features_extractor(cfg_file, x_train, fs=fs, header_names = np.array(['emg', 'gon']))\n", - "X_test = tsfel.time_series_features_extractor(cfg_file, x_test, fs=fs, header_names = np.array(['emg', 'gon']))" + "X_train = tsfel.time_series_features_extractor(cfg_file, x_train, fs=fs, header_names=np.array([\"emg\", \"gon\"]))\n", + "X_test = tsfel.time_series_features_extractor(cfg_file, x_test, fs=fs, header_names=np.array([\"emg\", \"gon\"]))" ] }, { @@ -394,11 +279,9 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": { - "colab": {}, - "colab_type": "code", - "id": "_51u2ioEmm2I" + "colab_type": "code" }, "outputs": [], "source": [ @@ -421,8 +304,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "whXQ6hI1m2bM" + "colab_type": "text" }, "source": [ "# Classification\n", @@ -432,45 +314,11 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 185 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 833, - "status": "ok", - "timestamp": 1586777540937, - "user": { - "displayName": "Leticia Fernandes", - "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14Gj3U_hSW1M2-Ab0tHYcZEiOzvFIfJrkA-pccFhU=s64", - "userId": "17109198128714142667" - }, - "user_tz": -60 - }, - "id": "82aqGd22mrZ_", - "outputId": "2eb49883-706e-47f8-e67b-a1d7644fef54" + "colab_type": "code" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " precision recall f1-score support\n", - "\n", - " Normal 0.90 1.00 0.95 28\n", - "Pathological 1.00 0.92 0.96 38\n", - "\n", - " micro avg 0.95 0.95 0.95 66\n", - " macro avg 0.95 0.96 0.95 66\n", - "weighted avg 0.96 0.95 0.95 66\n", - "\n", - "Accuracy: 95.45454545454545%\n" - ] - } - ], + "outputs": [], "source": [ "classifier = DecisionTreeClassifier()\n", "\n", @@ -480,60 +328,30 @@ "# Predict on test data\n", "y_predict = classifier.predict(nX_test)\n", "\n", - "condition_labels = ['Normal', 'Pathological']\n", + "condition_labels = [\"Normal\", \"Pathological\"]\n", "\n", "# Get the classification report\n", "accuracy = accuracy_score(y_test, y_predict) * 100\n", - "print(classification_report(y_test, y_predict, target_names = condition_labels))\n", - "print(\"Accuracy: \" + str(accuracy) + '%')" + "print(classification_report(y_test, y_predict, target_names=condition_labels))\n", + "print(\"Accuracy: \" + str(accuracy) + \"%\")" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 338 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 1111, - "status": "ok", - "timestamp": 1586777544492, - "user": { - "displayName": "Leticia Fernandes", - "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14Gj3U_hSW1M2-Ab0tHYcZEiOzvFIfJrkA-pccFhU=s64", - "userId": "17109198128714142667" - }, - "user_tz": -60 - }, - "id": "LqQAkWDsm8hw", - "outputId": "9d2b8e80-d5b5-49ca-8668-a2b511b649e8" + "colab_type": "code" }, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "

" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "#@title Confusion Matrix\n", + "# @title Confusion Matrix\n", "cm = confusion_matrix(y_test, y_predict)\n", - "df_cm = pd.DataFrame(cm, index=[i for i in condition_labels], columns = [i for i in condition_labels])\n", + "df_cm = pd.DataFrame(cm, index=[i for i in condition_labels], columns=[i for i in condition_labels])\n", "plt.figure()\n", - "ax = sns.heatmap(df_cm, cbar = True, cmap=\"BuGn\", annot = True, fmt = 'd')\n", - "plt.setp(ax.get_xticklabels(), rotation = 90)\n", - "plt.ylabel('True label', fontweight = 'bold', fontsize = 18)\n", - "plt.xlabel('Predicted label', fontweight = 'bold', fontsize = 18)\n", + "ax = sns.heatmap(df_cm, cbar=True, cmap=\"BuGn\", annot=True, fmt=\"d\")\n", + "plt.setp(ax.get_xticklabels(), rotation=90)\n", + "plt.ylabel(\"True label\", fontweight=\"bold\", fontsize=18)\n", + "plt.xlabel(\"Predicted label\", fontweight=\"bold\", fontsize=18)\n", "bottom, top = ax.get_ylim()\n", "ax.set_ylim(bottom + 0.5, top - 0.5)\n", "plt.show()" @@ -542,8 +360,7 @@ { "cell_type": "markdown", "metadata": { - "colab_type": "text", - "id": "WDvM1J2unCDr" + "colab_type": "text" }, "source": [ "All features domains were used in the feature extraction step. Accordingly, the Decision Tree classifier obtained high accuracy and was able to distinguish between normal and pathological knee condition during the extension of the leg from the sit position activity." @@ -551,16 +368,6 @@ } ], "metadata": { - "colab": { - "collapsed_sections": [], - "name": "TSFEL_predicting_NormalVsPathologicalkneeold.ipynb", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { "name": "ipython", diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..76aab6f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,130 @@ +[tool.black] +line-length = 120 +color = true +target-version = ['py37', 'py38'] +include = '\.pyi?$' +exclude = ''' +( + \.egg + | \.eggs + | \.git + | \.hg + | \.dvc + | \.mypy_cache + | \.pytest_cache + | \.nox + | \.tox + | \.venv + | \.venv-docs + | \.venv-dev + | \.venv-note + | \.venv-dempy + | _build + | build + | dist + | setup.py +) +''' + +[tool.isort] +# https://github.com/timothycrosley/isort +py_version = 38 +profile = "black" +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +line_length = 120 +skip_gitignore = true +color_output = true +#known_typing = ["typing", "types", "typing_extensions", "mypy", "mypy_extensions"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: nocover", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "raise AssertionError", +] +show_missing = true +ignore_errors = true +skip_covered = true +#fail_under = 100 +#precision = 1 +omit = [ + "test/*", + ".venv*", +] + +# `pytest` configurations +[tool.pytest.ini_options] +minversion = "6.0" +addopts = ["-vv", "--doctest-modules"] +doctest_optionflags = "NORMALIZE_WHITESPACE" +testpaths = ["test"] +filterwarnings = ["ignore::DeprecationWarning"] + +[tool.mypy] +# https://mypy.readthedocs.io/en/latest/config_file.html +python_version = 3.8 +pretty = true +show_traceback = true +color_output = true +warn_return_any = true +warn_no_return = true +warn_unused_configs = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_unreachable = true + +[tool.vulture] +paths = ["src"] +min_confidence = 65 + +[tool.pydocstyle] +convention = "google" +#ignore = "D205,D415" + +[tool.interrogate] +# https://github.com/econchick/interrogate#configuration +ignore-init-method = true +fail-under = 95 +color = true +# possible values: 0 (minimal output), 1 (-v), 2 (-vv) +verbose = 0 +quiet = false +exclude = ["setup.py", "docs", "build"] + +[tool.nbqa.config] +black = "pyproject.toml" +isort = "pyproject.toml" + +[tool.nbqa.mutate] +isort = 1 +black = 1 +pyupgrade = 1 + +[tool.nbqa.addopts] +pyupgrade = ["--py36-plus"] + +[tool.nbqa.files] +isort = "^notebooks/" +black = "^notebooks/" +flake8 = "^notebooks/" +mypy = "^notebooks/" +pydocstyle = "^notebooks/" +pyupgrade = "^notebooks/" + +[tool.bandit] +targets = ["src"] +# (optional) list included test IDs here, eg '[B101, B406]': +tests = ["B201", "B301"] +# (optional) list skipped test IDs here, eg '[B101, B406]': +skips = ["B101", "B601"] + +[tool.bandit.assert_used] +exclude = ["*_test.py", "test_*.py"] + +[tool.cruft] +skip = [".git"] diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index 410dbbb..b043998 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -1 +1 @@ -pre-commit==2.17.0 \ No newline at end of file +pre-commit==2.17.0 diff --git a/requirements/requirements-docs.txt b/requirements/requirements-docs.txt index f48ee10..1e64fb3 100644 --- a/requirements/requirements-docs.txt +++ b/requirements/requirements-docs.txt @@ -1,2 +1,2 @@ Sphinx >= 7.2.6 -sphinx-rtd-theme >= 1.3.0 \ No newline at end of file +sphinx-rtd-theme >= 1.3.0 diff --git a/requirements/requirements-test.txt b/requirements/requirements-test.txt index 3a12ad6..7e598a2 100644 --- a/requirements/requirements-test.txt +++ b/requirements/requirements-test.txt @@ -1,2 +1,8 @@ +colorednoise==2.2.0 +matplotlib==3.8.2 nose==1.3.7 -pytest==7.1.1 \ No newline at end of file +pyarrow==15.0.0 +pytest==7.1.1 +requests==2.31.0 +seaborn==0.13.2 +tqdm==4.66.1 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 188cebe..9ce48c2 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -14,6 +14,9 @@ numpy >= 1.18.5 # tsfel/utils/signal_processing.py: 2 pandas >= 1.5.3 +# tsfel/feature_extraction/features_utils.py: 1 +scikit-learn >= 0.21.3 + # tsfel/feature_extraction/features.py: 1 # tsfel/feature_extraction/features_utils.py: 1 # tsfel/utils/calculate_complexity.py: 4 @@ -23,3 +26,6 @@ scipy >= 1.7.3 # tsfel/__init__.py: 2 # tsfel/setup.py: 1 setuptools >= 47.1.1 + +# tsfel/feature_extraction/features.py: 39 +statsmodels >= 0.12.0 diff --git a/setup.py b/setup.py index 39a7e73..4ddbba6 100644 --- a/setup.py +++ b/setup.py @@ -1,19 +1,16 @@ -import setuptools from pathlib import Path +import setuptools + ROOT = Path(__file__).parent -with open("README.md", "r") as fh: +with open("README.md") as fh: long_description = fh.read() def find_requirements(filename): with (ROOT / "requirements" / filename).open() as f: - return [ - s - for s in [line.strip(" \n") for line in f] - if not s.startswith("#") and s != "" - ] + return [s for s in [line.strip(" \n") for line in f] if not s.startswith("#") and s != ""] install_reqs = find_requirements("requirements.txt") @@ -21,14 +18,14 @@ def find_requirements(filename): setuptools.setup( name="tsfel", - version="0.1.6", + version="0.1.7", author="Fraunhofer Portugal", description="Library for time series feature extraction", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/fraunhoferportugal/tsfel/", package_data={ - "tsfel": ["feature_extraction/features.json"] + "tsfel": ["feature_extraction/features.json"], }, packages=setuptools.find_packages(), classifiers=[ diff --git a/tests/benchmark.py b/tests/benchmark.py new file mode 100644 index 0000000..f0b9a13 --- /dev/null +++ b/tests/benchmark.py @@ -0,0 +1,160 @@ +import time + +import matplotlib.pyplot as plt +import pandas as pd +import seaborn as sns +from datasets.empirical1000_dataset import Empirical1000Dataset + +import tsfel + +DOMAINS = [ + "statistical", + "temporal", + "spectral", +] # It specifies the domains included in the analysis. + +emp_dataset = Empirical1000Dataset() + +######################################################################################################### +# Experiment 1 - Overall executing time benchmarking per domain # +######################################################################################################### +execution_times_global = pd.DataFrame( + { + "Domain": pd.Series(dtype="str"), + "Execution Time (s)": pd.Series(dtype="int"), + "Normalized Execution Time (s/feature)": pd.Series(dtype="float32"), + }, +) + +for domain in DOMAINS: + n_features_domain = len(tsfel.get_features_by_domain(domain)[domain]) + + init_time = time.perf_counter() + for _id, ts in emp_dataset.raw.items(): + _ = tsfel.time_series_features_extractor( + tsfel.get_features_by_domain(domain), + ts.sig, + fs=100, + verbose=False, + ) + end_time = time.perf_counter() + total_time = end_time - init_time + + execution_times_global = pd.concat( + [ + execution_times_global, + pd.DataFrame( + { + "Domain": [domain], + "Execution Time (s)": [int(total_time)], + "Normalized Execution Time (s/feature)": [ + total_time / n_features_domain, + ], + }, + ), + ], + ignore_index=True, + ) + +######################################################################################################### +# Experiment 2 - Detailed time benchmarking as function of time series length grouped by feature domain # +######################################################################################################### +execution_times_individual = pd.DataFrame( + { + "Domain": pd.Series(dtype="str"), + "ID": pd.Series(dtype="int"), + "Execution Time (s)": pd.Series(dtype="float32"), + "Length": pd.Series(dtype="int"), + }, +) + +for domain in ["statistical", "temporal", "spectral"]: + for id, ts in emp_dataset.raw.items(): + init_time = time.perf_counter() + _ = tsfel.time_series_features_extractor( + tsfel.get_features_by_domain(domain), + ts.sig, + fs=100, + verbose=False, + ) + end_time = time.perf_counter() + total_time = end_time - init_time + + execution_times_individual = pd.concat( + [ + execution_times_individual, + pd.DataFrame( + { + "Domain": [domain], + "ID": [id], + "Execution Time (s)": [total_time], + "Length": [ts.len], + }, + ), + ], + ignore_index=True, + ) + +# Some lengths have a low number of time series for statistical analysis (< 25 samples). We only consider lengths that +# have at least 25 samples for the analysis. For each length, we calculate the average and standard deviation and plot +# them on normal and log-log scales. +length_counts = execution_times_individual["Length"].value_counts() +valid_lengths = length_counts[length_counts >= 25].index +filtered_df = execution_times_individual[execution_times_individual["Length"].isin(valid_lengths)] +grouped_stats = filtered_df.groupby(["Length", "Domain"])["Execution Time (s)"].agg(["mean", "std"]).reset_index() + + +fig, (ax0, ax1) = plt.subplots(ncols=2) +for domain, marker in zip(DOMAINS, ["o", "v", "^"]): + _data_slice = grouped_stats[grouped_stats["Domain"] == domain] + ax0.errorbar( + _data_slice["Length"], + _data_slice["mean"], + yerr=_data_slice["std"], + label=domain, + marker=marker, + capsize=4, + ) + ax1.errorbar( + _data_slice["Length"], + _data_slice["mean"], + yerr=_data_slice["std"], + label=domain, + marker=marker, + capsize=4, + ) + +[ax.set(xlabel="Length (#)", ylabel="Execution Time (s)") for ax in [ax0, ax1]] +[ax.spines[loc].set_visible(False) for ax in [ax0, ax1] for loc in ["top", "right"]] +[ax.legend() for ax in [ax0, ax1]] +ax1.set_xscale("log") +ax1.set_yscale("log") + +######################################################################################################### +# Experiment 4 - Measure the overall execution time per feature # +######################################################################################################### +execution_times_feature = pd.DataFrame(columns=["Domain", "Feature_Name", "Execution_Time"]) +cfg = tsfel.get_features_by_domain() +for domain in DOMAINS: + for feature in cfg[domain]: + print(domain, feature) + init_time = time.perf_counter() + for _, ts in emp_dataset.raw.items(): + _ = tsfel.time_series_features_extractor( + {domain: {feature: cfg[domain][feature]}}, + ts.sig, + fs=100, + verbose=False, + ) + execution_time = time.perf_counter() - init_time + + execution_times_feature.loc[len(execution_times_feature)] = { + "Domain": domain, + "Feature_Name": feature, + "Execution_Time": execution_time, + } + +fig, ax = plt.subplots(1, 1) +sns.barplot(execution_times_feature, x="Feature_Name", y="Execution_Time", hue="Domain", ax=ax) +ax.tick_params(axis="x", rotation=90) +plt.show(block=False) diff --git a/tests/complexity_measures_intuition.py b/tests/complexity_measures_intuition.py new file mode 100644 index 0000000..e891505 --- /dev/null +++ b/tests/complexity_measures_intuition.py @@ -0,0 +1,93 @@ +"""This module offers helper functions designed to facilitate testing and +enhance understanding of features that quantify repetitiveness in dynamic +systems.""" + +import matplotlib.pyplot as plt +import neurokit2 as nk +import numpy as np +from statsmodels.graphics.tsaplots import plot_acf +from statsmodels.tsa.stattools import acf + +from tests.tests_tools.test_data.complexity_datasets import load_complexities_datasets + + +def plot_dfa(info, scale, fluctuations, ax): + """Plot log-log visualization of the detrended fluctuation analysis + (DFA).""" + + polyfit = np.polyfit(np.log2(scale), np.log2(fluctuations), 1) + fluctfit = 2 ** np.polyval(polyfit, np.log2(scale)) + ax.loglog(scale, fluctuations, "o", c="#90A4AE") + ax.loglog( + scale, + fluctfit, + c="#E91E63", + label=r"$\alpha$ = {:.3f}".format(info["Alpha"]), + ) + + return ax + + +def plot_multiscale_entropy(info, ax): + """Plots the entropy values for each scale factor.""" + + ax.plot( + info["Scale"][np.isfinite(info["Value"])], + info["Value"][np.isfinite(info["Value"])], + color="#FF9800", + ) + + return ax + + +def get_first_acf_time_constant(signal): + """Captures the approximate scale of the autocorrelation function by + measuring the first time lag at which the autocorrelation function (acf) + drops below 1/e = 0.3679.""" + n = signal.size + maxlag = int(n / 4) + threshold = 0.36787944117144233 # 1 / np.exp(1) + + acf_vec = acf(signal, adjusted=True, fft=n > 400, nlags=maxlag)[ + 1: + ] # n > 400 empirically selected based on performance tests + idxs = np.where(acf_vec < threshold)[0] + first_lag = idxs[0] + 1 if idxs.size > 0 else maxlag + + return first_lag + + +dataset = load_complexities_datasets() + +f, ax = plt.subplots(4, len(dataset), figsize=(15, 5)) +for i, (k, _v) in enumerate(dataset.items()): + signal = dataset[k] + + # First row (raw data) + ax[0, i].plot(signal, "k") + ax[0, i].set_title(k) + ax[0, i].spines[["left", "top", "right"]].set_visible(False) + ax[0, i].tick_params(left=False, right=False, labelleft=False) + + # Second row (first_acf_time_constant) + plot_acf(signal, ax[1, i], adjusted=True, fft=len(signal) > 1250, title="") + ax[1, i].axhline(y=(1 / np.e), color="r", linestyle="--") + ax[1, i].spines[["top", "right"]].set_visible(False) + ax[1, i].set_title("$\\tau = %d$" % get_first_acf_time_constant(signal)) + + # Third row (dfa) + # TODO: Change to our own implementation instead of relying on neurokit2 + _, info = nk.fractal_dfa(signal) + plot_dfa(info, info["scale"], info["Fluctuations"], ax[2, i]) + ax[2, i].set_title("$\\alpha = %0.2f$" % info["Alpha"]) + ax[2, i].set_axis_off() + + # Forth row (mse) + mse, info = nk.entropy_multiscale(signal) + plot_multiscale_entropy(info, ax[3, i]) + ax[3, i].set_title("mse = %0.2f" % mse) + ax[3, i].set_ylim([0, 2.5]) + ax[3, i].spines[["top", "right"]].set_visible(False) + +f.tight_layout() +plt.show(block=False) diff --git a/tests/datasets/__init__.py b/tests/datasets/__init__.py new file mode 100644 index 0000000..f21d41c --- /dev/null +++ b/tests/datasets/__init__.py @@ -0,0 +1,4 @@ +"""A utility module providing simplified access to time series datasets used +for benchmarking and testing.""" + +from .empirical1000_dataset import Empirical1000Dataset diff --git a/tests/datasets/empirical1000_dataset.py b/tests/datasets/empirical1000_dataset.py new file mode 100644 index 0000000..3c5a29f --- /dev/null +++ b/tests/datasets/empirical1000_dataset.py @@ -0,0 +1,188 @@ +import os +import shutil +import zipfile +from pathlib import Path +from urllib.parse import urlparse + +import numpy as np +import pandas as pd +import requests +import scipy.io as sio +from tqdm import tqdm + +module_path = os.path.dirname(__file__) +data_path = os.path.join(module_path, "cached_datasets") + + +class Empirical1000Dataset: + """A convenience class to access the Empirical1000 dataset. + + When using one this dataset, please cite [1]_. + + Parameters + ---------- + use_cache : bool (default: True) + Whether a cached version of the dataset should be used, if one is found. + The dataset is always cached upon loading, and this parameter only determines + whether the cached version shall be refreshed upon loading. + + extract_features : bool (default: False) + Whether a feature extraction routine should be called after loading the data files. + TODO: Implement this functionality. + + Notes + ----- + To optimize running times it is recommended to use `use_cache=True` (default) in order + to only experience a time once downloading and work on a cached version of the dataset afterward. + + + References + ---------- + .. [1] Fulcher, B. D., Lubba, C. H., Sethi, S. S., & Jones, N. S. (2020). + A self-organizing, living library of time-series data. Scientific data, 7(1), 213. + """ + + def __init__(self, use_cache=True, extract_features=False): + self.use_cache = use_cache + + self.raw = {} + self.features = {} # An add-on to this class will contain a featurized data representation. + + self.__dataset_url = "https://figshare.com/ndownloader/articles/5436136/versions/10" + self.cache_folder_path = data_path + self.__empirical1000_folder_path = os.path.join( + self.cache_folder_path, + "Empirical1000", + ) + + if not os.path.exists(self.cache_folder_path) or not os.listdir(self.cache_folder_path) or not use_cache: + print("Cache folder is empty. Downloading the Empirical1000 dataset...") + Path(os.path.join(self.cache_folder_path, "Empirical1000")).mkdir( + parents=True, + exist_ok=True, + ) + self.__download_dataset() + + self.__data_files = sio.loadmat( + os.path.join(self.__empirical1000_folder_path, "INP_1000ts.mat"), + ) + self.metadata = pd.read_csv( + os.path.join(self.__empirical1000_folder_path, "hctsa_timeseries-info.csv"), + ) + + for i, name in enumerate(self.metadata["Name"]): + self.raw[i] = EmpiricalTimeSeries( + name, + self.__data_files["timeSeriesData"][0][i].flatten(), + ) + if extract_features: + tqdm_iterator = tqdm( + total=len(self.metadata["Name"]), + desc="Extracting features.", + ) + tqdm_iterator.update(1) + + def __download_dataset(self): + try: + # Send a request to the dataset URL with allow_redirects=False to handle redirection + response = requests.get( + self.__dataset_url, + stream=True, + allow_redirects=False, + ) + + # Check if the request was successful (status code 200) + if response.status_code == 200: + total_size = int(response.headers.get("content-length", 0)) or None + filename = self.__extract_filename(response) + + with tqdm( + total=total_size, + unit="B", + unit_scale=True, + desc="Downloading Empirical1000 dataset", + dynamic_ncols=True, + ) as pbar: + with open( + os.path.join(self.cache_folder_path, filename), + "wb", + ) as out_file: + shutil.copyfileobj(response.raw, out_file) + pbar.update(os.path.getsize(out_file.name)) + + zip_file_path = os.path.join(self.cache_folder_path, filename) + with zipfile.ZipFile(zip_file_path, "r") as zip_ref: + zip_ref.extractall(self.__empirical1000_folder_path) + + # A sanitizing routine that deletes non-relevant files. + os.remove(zip_file_path) + extracted_files = os.listdir(self.__empirical1000_folder_path) + timeseries_files = [ + file for file in extracted_files if "timeseries-info" in file.lower() or "inp" in file.lower() + ] + + for file in extracted_files: + if file not in timeseries_files: + os.remove(os.path.join(self.__empirical1000_folder_path, file)) + + print(f"Dataset downloaded and saved to: {self.cache_folder_path}") + else: + print( + f"Failed to download dataset. Status code: {response.status_code}", + ) + + except Exception as e: + print(f"An error occurred: {e}") + + @staticmethod + def __extract_filename(response): + # Try to extract the filename from the Content-Disposition header. + # If the header is not present, extract the filename from the URL + content_disposition = response.headers.get("Content-Disposition") + if content_disposition and "filename=" in content_disposition: + filename = content_disposition.split("filename=")[1].strip("\";'") + else: + filename = os.path.basename(urlparse(response.url).path) + + return filename + + def get_by_len(self, l): + query = np.nonzero(self.metadata["Length"] == l)[0] + filtered_data = {key: self.raw[key] for key in query if key in self.raw} + + return filtered_data + + +class EmpiricalTimeSeries: + """The object representing a time series from the Empirical1000 dataset. + + Attributes: + name (str): The name of the time series. + sig (array-like): The time series data. + len (int): The length of the time series. + + Methods: + __init__(self, name, sig): + Initializes a new EmpiricalTimeSeries object. + """ + + def __init__(self, name, sig): + """Initializes a new EmpiricalTimeSeries object. + + Parameters: + name (str): The name or identifier of the time series. + sig (list or array-like): The time series data. + """ + self.name = name + self.sig = sig + self.len = len(sig) + + +class FeaturizedEmpiricalTimeSeries(EmpiricalTimeSeries): + def __init__(self, empirical_time_series): + super().__init__(empirical_time_series.name, empirical_time_series.sig) + self.example_feature = self.get_feature() + + @staticmethod + def get_feature(self): + return -1 diff --git a/tests/performance_tests_acf.py b/tests/performance_tests_acf.py new file mode 100644 index 0000000..183f392 --- /dev/null +++ b/tests/performance_tests_acf.py @@ -0,0 +1,46 @@ +import timeit + +import matplotlib.pyplot as plt +import numpy as np +from mpl_toolkits.axes_grid1.inset_locator import inset_axes +from statsmodels.tsa.stattools import acf + +use_fft = [False, True] +signal_lengths = np.hstack((np.arange(15, 515, 15), np.arange(500, 5500, 500))) +results = [[], []] + +for i, use_fft in enumerate([False, True]): + for length in signal_lengths: + signal = np.random.randn(length) + + def measure_acf(signal, use_fft): + return acf(signal, adjusted=True, fft=use_fft, nlags=int(len(signal) / 4))[1:] + + cpu_time = timeit.timeit("measure_acf()", globals=globals(), number=10) + mean_cpu_time = cpu_time / 10 # Average time per execution + + results[i].append(mean_cpu_time * 1000) +results = np.array(results) + +fig, ax = plt.subplots(1, 1) +ax.plot(signal_lengths, results[0], "o-", label="full convolution") +ax.plot(signal_lengths, results[1], "o-", label="fft") +ax.set_ylabel("CPU time / ms") +ax.set_xlabel("Signal size / #") +ax.spines[["top", "right"]].set_visible(False) +ax.legend() + +ax_inset = inset_axes(ax, width="30%", height="30%", loc=1) # loc=2 is for upper left +ax_inset.plot(signal_lengths, results[0], "o-", label="full convolution") +ax_inset.plot(signal_lengths, results[1], "o-", label="FFT") +ax_inset.set_xlim(0, 500) +ax_inset.set_ylim(0, 0.10) +ax_inset.yaxis.set_visible(False) +ax_inset.yaxis.set_ticks([]) +ax_inset.axvline( + x=signal_lengths[np.where(results[0] > results[1])[0][0]], + color="r", + linestyle="--", +) + +plt.show(block=False) diff --git a/tests/test_calc_features.py b/tests/test_calc_features.py index 74b4ea4..38de910 100644 --- a/tests/test_calc_features.py +++ b/tests/test_calc_features.py @@ -1,13 +1,16 @@ -import os -import json import glob +import json +import os +from pathlib import Path + # import tsfel import pandas as pd -from pathlib import Path + +from tsfel.feature_extraction.calc_features import dataset_features_extractor, time_series_features_extractor from tsfel.feature_extraction.features_settings import get_features_by_domain, get_features_by_tag -from tsfel.feature_extraction.calc_features import time_series_features_extractor, dataset_features_extractor -from tsfel.utils.signal_processing import merge_time_series, signal_window_splitter from tsfel.utils.add_personal_features import add_feature_json +from tsfel.utils.signal_processing import merge_time_series, signal_window_splitter + # Example of user preprocess sensor data def pre_process(sensor_data): @@ -22,7 +25,11 @@ def pre_process(sensor_data): # JSON DIR # tsfel_path_json = tsfel.__path__[0] + os.sep + "feature_extraction" + os.sep + "features.json" personal_path_json = os.path.join("tests", "tests_tools", "test_features.json") -personal_features_path = os.path.join("tests", "tests_tools", "test_personal_features.py") +personal_features_path = os.path.join( + "tests", + "tests_tools", + "test_personal_features.py", +) # DEFAULT PARAM for testing time_unit = 1e9 # seconds @@ -34,7 +41,7 @@ def pre_process(sensor_data): sensor_data = {} key = "Accelerometer" -folders = [f for f in glob.glob(main_directory + "**/", recursive=True)] +folders = list(glob.glob(main_directory + "**/", recursive=True)) sensor_data[key] = pd.read_csv(folders[-1] + key + ".txt", header=None) @@ -43,7 +50,8 @@ def pre_process(sensor_data): settings2 = get_features_by_domain("statistical") settings3 = get_features_by_domain("temporal") settings4 = get_features_by_domain("spectral") -settings5 = get_features_by_domain() +settings5 = get_features_by_domain("fractal") +settings6 = get_features_by_domain() settings8 = get_features_by_tag("inertial") settings10 = get_features_by_tag() @@ -57,30 +65,75 @@ def pre_process(sensor_data): # multi windows and multi axis # input: list -features0 = time_series_features_extractor(settings5, windows, fs=resample_rate, n_jobs=n_jobs) +features0 = time_series_features_extractor( + settings6, + windows, + fs=resample_rate, + n_jobs=n_jobs, +) # multiple windows and single axis # input: np.array -features1 = time_series_features_extractor(settings5, data_new.values[:, 0], fs=resample_rate, n_jobs=n_jobs, window_size=window_size, overlap=overlap) +features1 = time_series_features_extractor( + settings6, + data_new.values[:, 0], + fs=resample_rate, + n_jobs=n_jobs, + window_size=window_size, + overlap=overlap, +) # input: pd.series -features2 = time_series_features_extractor(settings5, data_new.iloc[:, 0], fs=resample_rate, n_jobs=n_jobs, window_size=window_size, overlap=overlap) +features2 = time_series_features_extractor( + settings6, + data_new.iloc[:, 0], + fs=resample_rate, + n_jobs=n_jobs, + window_size=window_size, + overlap=overlap, +) # single window and multi axis # input: pd.DataFrame -features3 = time_series_features_extractor(settings5, data_new, fs=resample_rate, n_jobs=n_jobs) +features3 = time_series_features_extractor( + settings6, + data_new, + fs=resample_rate, + n_jobs=n_jobs, +) # input: np.array -features4 = time_series_features_extractor(settings4, data_new.values, fs=resample_rate, n_jobs=n_jobs) +features4 = time_series_features_extractor( + settings4, + data_new.values, + fs=resample_rate, + n_jobs=n_jobs, +) # single window and single axis # input: pd.Series -features5 = time_series_features_extractor(settings2, data_new.iloc[:, 0], fs=resample_rate, n_jobs=n_jobs) +features5 = time_series_features_extractor( + settings2, + data_new.iloc[:, 0], + fs=resample_rate, + n_jobs=n_jobs, +) # input: np.array -features6 = time_series_features_extractor(settings4, data_new.values[:, 0], fs=resample_rate, n_jobs=n_jobs) +features6 = time_series_features_extractor( + settings5, + data_new.values[:, 0], + fs=resample_rate, + n_jobs=n_jobs, +) # personal features settings1 = json.load(open(personal_path_json)) add_feature_json(personal_features_path, personal_path_json) -features7 = time_series_features_extractor(settings1, data_new.values[:, 0], fs=resample_rate, n_jobs=n_jobs, features_path=personal_features_path) +features7 = time_series_features_extractor( + settings1, + data_new.values[:, 0], + fs=resample_rate, + n_jobs=n_jobs, + features_path=personal_features_path, +) # Dataset features extractor @@ -95,4 +148,4 @@ def pre_process(sensor_data): pre_process=pre_process, output_directory=output_directory, ) -print("-----------------------------------OK-----------------------------------") \ No newline at end of file +print("-----------------------------------OK-----------------------------------") diff --git a/tests/test_features.py b/tests/test_features.py index 3692251..76e6783 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -1,10 +1,10 @@ import unittest +import colorednoise as cn import numpy as np from tsfel.feature_extraction.features import * - # Unit testing for Linux OS # Implementing signals for testing features @@ -23,17 +23,51 @@ noiseWave = wave + np.random.normal(0, 0.1, 1000) offsetWave = wave + 2 +duration = 5 +samples = duration * Fs +whiteNoise = cn.powerlaw_psd_gaussian(0, samples, random_state=10) +pinkNoise = cn.powerlaw_psd_gaussian(1, samples, random_state=10) +brownNoise = cn.powerlaw_psd_gaussian(2, samples, random_state=10) + + class TestFeatures(unittest.TestCase): # ############################################### STATISTICAL FEATURES ############################################### # def test_hist(self): - np.testing.assert_almost_equal(hist(const0, 10, 5), (0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 0.0, 0.0, 0.0)) - np.testing.assert_almost_equal(hist(const1, 10, 5), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 0.0, 0.0)) - np.testing.assert_almost_equal(hist(constNeg, 10, 5), (0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 0.0, 0.0, 0.0, 0.0)) - np.testing.assert_almost_equal(hist(constF, 10, 5), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 0.0)) - np.testing.assert_almost_equal(hist(lin, 10, 5), (0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 2)) - np.testing.assert_almost_equal(hist(wave, 10, 5), (0.0, 0.0, 0.0, 0.0, 499, 496, 5, 0.0, 0.0, 0.0), decimal=5) - np.testing.assert_almost_equal(hist(offsetWave, 10, 5), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 499, 496, 5, 0.0), decimal=5) - np.testing.assert_almost_equal(hist(noiseWave, 10, 5), (0.0, 0.0, 0.0, 48, 446, 450, 56, 0.0, 0.0, 0.0), decimal=5) + np.testing.assert_almost_equal( + hist(const0, 10, 5), + (0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 0.0, 0.0, 0.0), + ) + np.testing.assert_almost_equal( + hist(const1, 10, 5), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 0.0, 0.0), + ) + np.testing.assert_almost_equal( + hist(constNeg, 10, 5), + (0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 0.0, 0.0, 0.0, 0.0), + ) + np.testing.assert_almost_equal( + hist(constF, 10, 5), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 0.0), + ) + np.testing.assert_almost_equal( + hist(lin, 10, 5), + (0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 2), + ) + np.testing.assert_almost_equal( + hist(wave, 10, 5), + (0.0, 0.0, 0.0, 0.0, 499, 496, 5, 0.0, 0.0, 0.0), + decimal=5, + ) + np.testing.assert_almost_equal( + hist(offsetWave, 10, 5), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 499, 496, 5, 0.0), + decimal=5, + ) + np.testing.assert_almost_equal( + hist(noiseWave, 10, 5), + (0.0, 0.0, 0.0, 48, 446, 450, 56, 0.0, 0.0, 0.0), + decimal=5, + ) def test_skewness(self): self.assertTrue(np.isnan(skewness(const0))) @@ -41,10 +75,26 @@ def test_skewness(self): self.assertTrue(np.isnan(skewness(constNeg))) self.assertTrue(np.isnan(skewness(constF))) np.testing.assert_almost_equal(skewness(lin), 0) - np.testing.assert_almost_equal(skewness(lin0), -1.0167718723297815e-16, decimal=5) - np.testing.assert_almost_equal(skewness(wave), -2.009718347115232e-17, decimal=5) - np.testing.assert_almost_equal(skewness(offsetWave), 9.043732562018544e-16, decimal=5) - np.testing.assert_almost_equal(skewness(noiseWave), -0.0004854111290521465, decimal=5) + np.testing.assert_almost_equal( + skewness(lin0), + -1.0167718723297815e-16, + decimal=5, + ) + np.testing.assert_almost_equal( + skewness(wave), + -2.009718347115232e-17, + decimal=5, + ) + np.testing.assert_almost_equal( + skewness(offsetWave), + 9.043732562018544e-16, + decimal=5, + ) + np.testing.assert_almost_equal( + skewness(noiseWave), + -0.0004854111290521465, + decimal=5, + ) def test_kurtosis(self): self.assertTrue(np.isnan(kurtosis(const0))) @@ -54,8 +104,16 @@ def test_kurtosis(self): np.testing.assert_almost_equal(kurtosis(lin), -1.206015037593985, decimal=2) np.testing.assert_almost_equal(kurtosis(lin0), -1.2060150375939847, decimal=2) np.testing.assert_almost_equal(kurtosis(wave), -1.501494077162359, decimal=2) - np.testing.assert_almost_equal(kurtosis(offsetWave), -1.5014940771623597, decimal=2) - np.testing.assert_almost_equal(kurtosis(noiseWave), -1.4606204906023366, decimal=2) + np.testing.assert_almost_equal( + kurtosis(offsetWave), + -1.5014940771623597, + decimal=2, + ) + np.testing.assert_almost_equal( + kurtosis(noiseWave), + -1.4606204906023366, + decimal=2, + ) def test_mean(self): np.testing.assert_almost_equal(calc_mean(const0), 0.0) @@ -63,10 +121,22 @@ def test_mean(self): np.testing.assert_almost_equal(calc_mean(constNeg), -1.0) np.testing.assert_almost_equal(calc_mean(constF), 2.5) np.testing.assert_almost_equal(calc_mean(lin), 9.5) - np.testing.assert_almost_equal(calc_mean(lin0), -3.552713678800501e-16, decimal=5) - np.testing.assert_almost_equal(calc_mean(wave), 7.105427357601002e-18, decimal=5) + np.testing.assert_almost_equal( + calc_mean(lin0), + -3.552713678800501e-16, + decimal=5, + ) + np.testing.assert_almost_equal( + calc_mean(wave), + 7.105427357601002e-18, + decimal=5, + ) np.testing.assert_almost_equal(calc_mean(offsetWave), 2.0, decimal=5) - np.testing.assert_almost_equal(calc_mean(noiseWave), -0.0014556635615470554, decimal=5) + np.testing.assert_almost_equal( + calc_mean(noiseWave), + -0.0014556635615470554, + decimal=5, + ) def test_median(self): np.testing.assert_almost_equal(calc_median(const0), 0.0) @@ -74,10 +144,22 @@ def test_median(self): np.testing.assert_almost_equal(calc_median(constNeg), -1.0) np.testing.assert_almost_equal(calc_median(constF), 2.5) np.testing.assert_almost_equal(calc_median(lin), 9.5) - np.testing.assert_almost_equal(calc_median(lin0), -3.552713678800501e-16, decimal=5) - np.testing.assert_almost_equal(calc_median(wave), 7.105427357601002e-18, decimal=5) + np.testing.assert_almost_equal( + calc_median(lin0), + -3.552713678800501e-16, + decimal=5, + ) + np.testing.assert_almost_equal( + calc_median(wave), + 7.105427357601002e-18, + decimal=5, + ) np.testing.assert_almost_equal(calc_median(offsetWave), 2.0, decimal=5) - np.testing.assert_almost_equal(calc_median(noiseWave), 0.013846093997438328, decimal=5) + np.testing.assert_almost_equal( + calc_median(noiseWave), + 0.013846093997438328, + decimal=5, + ) def test_max(self): np.testing.assert_almost_equal(calc_max(const0), 0.0) @@ -87,7 +169,11 @@ def test_max(self): np.testing.assert_almost_equal(calc_max(lin), 19) np.testing.assert_almost_equal(calc_max(lin0), 10.0, decimal=5) np.testing.assert_almost_equal(calc_max(wave), 1.0, decimal=5) - np.testing.assert_almost_equal(calc_max(noiseWave), 1.221757617217142, decimal=5) + np.testing.assert_almost_equal( + calc_max(noiseWave), + 1.221757617217142, + decimal=5, + ) np.testing.assert_almost_equal(calc_max(offsetWave), 3.0, decimal=5) def test_min(self): @@ -98,7 +184,11 @@ def test_min(self): np.testing.assert_almost_equal(calc_min(lin), 0) np.testing.assert_almost_equal(calc_min(lin0), -10.0, decimal=5) np.testing.assert_almost_equal(calc_min(wave), -1.0, decimal=5) - np.testing.assert_almost_equal(calc_min(noiseWave), -1.2582533627830566, decimal=5) + np.testing.assert_almost_equal( + calc_min(noiseWave), + -1.2582533627830566, + decimal=5, + ) np.testing.assert_almost_equal(calc_min(offsetWave), 1.0, decimal=5) def test_variance(self): @@ -110,7 +200,11 @@ def test_variance(self): np.testing.assert_almost_equal(calc_var(lin0), 36.84210526315789, decimal=5) np.testing.assert_almost_equal(calc_var(wave), 0.5, decimal=5) np.testing.assert_almost_equal(calc_var(offsetWave), 0.5, decimal=5) - np.testing.assert_almost_equal(calc_var(noiseWave), 0.5081167177369529, decimal=5) + np.testing.assert_almost_equal( + calc_var(noiseWave), + 0.5081167177369529, + decimal=5, + ) def test_std(self): np.testing.assert_almost_equal(calc_std(const0), 0.0) @@ -120,8 +214,16 @@ def test_std(self): np.testing.assert_almost_equal(calc_std(lin), 5.766281297335398) np.testing.assert_almost_equal(calc_std(lin0), 6.069769786668839, decimal=5) np.testing.assert_almost_equal(calc_std(wave), 0.7071067811865476, decimal=5) - np.testing.assert_almost_equal(calc_std(offsetWave), 0.7071067811865476, decimal=5) - np.testing.assert_almost_equal(calc_std(noiseWave), 0.7128230620125536, decimal=5) + np.testing.assert_almost_equal( + calc_std(offsetWave), + 0.7071067811865476, + decimal=5, + ) + np.testing.assert_almost_equal( + calc_std(noiseWave), + 0.7128230620125536, + decimal=5, + ) def test_interq_range(self): np.testing.assert_almost_equal(interq_range(const0), 0.0) @@ -131,8 +233,16 @@ def test_interq_range(self): np.testing.assert_almost_equal(interq_range(lin), 9.5) np.testing.assert_almost_equal(interq_range(lin0), 10.0, decimal=5) np.testing.assert_almost_equal(interq_range(wave), 1.414213562373095, decimal=5) - np.testing.assert_almost_equal(interq_range(offsetWave), 1.414213562373095, decimal=5) - np.testing.assert_almost_equal(interq_range(noiseWave), 1.4277110228590328, decimal=5) + np.testing.assert_almost_equal( + interq_range(offsetWave), + 1.414213562373095, + decimal=5, + ) + np.testing.assert_almost_equal( + interq_range(noiseWave), + 1.4277110228590328, + decimal=5, + ) def test_mean_abs_diff(self): np.testing.assert_almost_equal(mean_abs_diff(const0), 0.0) @@ -140,10 +250,26 @@ def test_mean_abs_diff(self): np.testing.assert_almost_equal(mean_abs_diff(constNeg), 0.0) np.testing.assert_almost_equal(mean_abs_diff(constF), 0.0) np.testing.assert_almost_equal(mean_abs_diff(lin), 1.0) - np.testing.assert_almost_equal(mean_abs_diff(lin0), 1.0526315789473684, decimal=5) - np.testing.assert_almost_equal(mean_abs_diff(wave), 0.019988577818740614, decimal=5) - np.testing.assert_almost_equal(mean_abs_diff(noiseWave), 0.10700252903161511, decimal=5) - np.testing.assert_almost_equal(mean_abs_diff(offsetWave), 0.019988577818740614, decimal=5) + np.testing.assert_almost_equal( + mean_abs_diff(lin0), + 1.0526315789473684, + decimal=5, + ) + np.testing.assert_almost_equal( + mean_abs_diff(wave), + 0.019988577818740614, + decimal=5, + ) + np.testing.assert_almost_equal( + mean_abs_diff(noiseWave), + 0.10700252903161511, + decimal=5, + ) + np.testing.assert_almost_equal( + mean_abs_diff(offsetWave), + 0.019988577818740614, + decimal=5, + ) def test_mean_abs_deviation(self): np.testing.assert_almost_equal(mean_abs_deviation(const0), 0.0) @@ -151,10 +277,26 @@ def test_mean_abs_deviation(self): np.testing.assert_almost_equal(mean_abs_deviation(constNeg), 0.0) np.testing.assert_almost_equal(mean_abs_deviation(constF), 0.0) np.testing.assert_almost_equal(mean_abs_deviation(lin), 5.0) - np.testing.assert_almost_equal(mean_abs_deviation(lin0), 5.263157894736842, decimal=5) - np.testing.assert_almost_equal(mean_abs_deviation(wave), 0.6365674116287157, decimal=5) - np.testing.assert_almost_equal(mean_abs_deviation(noiseWave), 0.6392749078483896, decimal=5) - np.testing.assert_almost_equal(mean_abs_deviation(offsetWave), 0.6365674116287157, decimal=5) + np.testing.assert_almost_equal( + mean_abs_deviation(lin0), + 5.263157894736842, + decimal=5, + ) + np.testing.assert_almost_equal( + mean_abs_deviation(wave), + 0.6365674116287157, + decimal=5, + ) + np.testing.assert_almost_equal( + mean_abs_deviation(noiseWave), + 0.6392749078483896, + decimal=5, + ) + np.testing.assert_almost_equal( + mean_abs_deviation(offsetWave), + 0.6365674116287157, + decimal=5, + ) def test_calc_median_abs_deviation(self): np.testing.assert_almost_equal(median_abs_deviation(const0), 0.0) @@ -162,10 +304,26 @@ def test_calc_median_abs_deviation(self): np.testing.assert_almost_equal(median_abs_deviation(constNeg), 0.0) np.testing.assert_almost_equal(median_abs_deviation(constF), 0.0) np.testing.assert_almost_equal(median_abs_deviation(lin), 5.0) - np.testing.assert_almost_equal(median_abs_deviation(lin0), 5.2631578947368425, decimal=5) - np.testing.assert_almost_equal(median_abs_deviation(wave), 0.7071067811865475, decimal=5) - np.testing.assert_almost_equal(median_abs_deviation(offsetWave), 0.7071067811865475, decimal=5) - np.testing.assert_almost_equal(median_abs_deviation(noiseWave), 0.7068117164205888, decimal=5) + np.testing.assert_almost_equal( + median_abs_deviation(lin0), + 5.2631578947368425, + decimal=5, + ) + np.testing.assert_almost_equal( + median_abs_deviation(wave), + 0.7071067811865475, + decimal=5, + ) + np.testing.assert_almost_equal( + median_abs_deviation(offsetWave), + 0.7071067811865475, + decimal=5, + ) + np.testing.assert_almost_equal( + median_abs_deviation(noiseWave), + 0.7068117164205888, + decimal=5, + ) def test_rms(self): np.testing.assert_almost_equal(rms(const0), 0.0) @@ -179,18 +337,75 @@ def test_rms(self): np.testing.assert_almost_equal(rms(noiseWave), 0.7128245483240299, decimal=5) def test_ecdf(self): - np.testing.assert_almost_equal(ecdf(const0), (0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5)) - np.testing.assert_almost_equal(ecdf(const1), (0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5)) - np.testing.assert_almost_equal(ecdf(constNeg), (0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5)) - np.testing.assert_almost_equal(ecdf(constF), (0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5)) - np.testing.assert_almost_equal(ecdf(lin), (0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5)) - np.testing.assert_almost_equal(ecdf(lin0), (0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5)) - np.testing.assert_almost_equal(ecdf(wave), (0.001, 0.002, 0.003, 0.004, 0.005, 0.006, 0.007, 0.008, 0.009, - 0.01)) - np.testing.assert_almost_equal(ecdf(offsetWave), (0.001, 0.002, 0.003, 0.004, 0.005, 0.006, 0.007, 0.008, 0.009, - 0.01)) - np.testing.assert_almost_equal(ecdf(noiseWave), (0.001, 0.002, 0.003, 0.004, 0.005, 0.006, 0.007, 0.008, 0.009, - 0.01)) + np.testing.assert_almost_equal( + ecdf(const0), + (0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5), + ) + np.testing.assert_almost_equal( + ecdf(const1), + (0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5), + ) + np.testing.assert_almost_equal( + ecdf(constNeg), + (0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5), + ) + np.testing.assert_almost_equal( + ecdf(constF), + (0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5), + ) + np.testing.assert_almost_equal( + ecdf(lin), + (0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5), + ) + np.testing.assert_almost_equal( + ecdf(lin0), + (0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5), + ) + np.testing.assert_almost_equal( + ecdf(wave), + ( + 0.001, + 0.002, + 0.003, + 0.004, + 0.005, + 0.006, + 0.007, + 0.008, + 0.009, + 0.01, + ), + ) + np.testing.assert_almost_equal( + ecdf(offsetWave), + ( + 0.001, + 0.002, + 0.003, + 0.004, + 0.005, + 0.006, + 0.007, + 0.008, + 0.009, + 0.01, + ), + ) + np.testing.assert_almost_equal( + ecdf(noiseWave), + ( + 0.001, + 0.002, + 0.003, + 0.004, + 0.005, + 0.006, + 0.007, + 0.008, + 0.009, + 0.01, + ), + ) def test_ecdf_percentile(self): np.testing.assert_almost_equal(ecdf_percentile(const0), (0, 0)) @@ -198,10 +413,20 @@ def test_ecdf_percentile(self): np.testing.assert_almost_equal(ecdf_percentile(constNeg), (-1, -1)) np.testing.assert_almost_equal(ecdf_percentile(constF), (2.5, 2.5)) np.testing.assert_almost_equal(ecdf_percentile(lin), (3, 15)) - np.testing.assert_almost_equal(ecdf_percentile(lin0), (-6.8421053, 5.7894737), decimal=7) + np.testing.assert_almost_equal( + ecdf_percentile(lin0), + (-6.8421053, 5.7894737), + decimal=7, + ) np.testing.assert_almost_equal(ecdf_percentile(wave), (-0.809017, 0.809017)) - np.testing.assert_almost_equal(ecdf_percentile(offsetWave), (1.1909830056250523, 2.809016994374947)) - np.testing.assert_almost_equal(ecdf_percentile(noiseWave), (-0.8095410722491809, 0.796916231269631)) + np.testing.assert_almost_equal( + ecdf_percentile(offsetWave), + (1.1909830056250523, 2.809016994374947), + ) + np.testing.assert_almost_equal( + ecdf_percentile(noiseWave), + (-0.8095410722491809, 0.796916231269631), + ) def test_ecdf_percentile_count(self): np.testing.assert_almost_equal(ecdf_percentile_count(const0), (0, 0)) @@ -214,7 +439,6 @@ def test_ecdf_percentile_count(self): np.testing.assert_almost_equal(ecdf_percentile_count(offsetWave), (200, 800)) np.testing.assert_almost_equal(ecdf_percentile_count(noiseWave), (200, 800)) - # ################################################ TEMPORAL FEATURES ################################################# # def test_distance(self): np.testing.assert_almost_equal(distance(const0), 19.0) @@ -224,8 +448,16 @@ def test_distance(self): np.testing.assert_almost_equal(distance(lin), 26.87005768508881) np.testing.assert_almost_equal(distance(lin0), 27.586228448267438, decimal=5) np.testing.assert_almost_equal(distance(wave), 999.2461809866238, decimal=5) - np.testing.assert_almost_equal(distance(offsetWave), 999.2461809866238, decimal=5) - np.testing.assert_almost_equal(distance(noiseWave), 1007.8711901383033, decimal=5) + np.testing.assert_almost_equal( + distance(offsetWave), + 999.2461809866238, + decimal=5, + ) + np.testing.assert_almost_equal( + distance(noiseWave), + 1007.8711901383033, + decimal=5, + ) def test_negative_turning(self): np.testing.assert_almost_equal(negative_turning(const0), 0.0) @@ -252,13 +484,28 @@ def test_positive_turning(self): def test_centroid(self): np.testing.assert_almost_equal(calc_centroid(const0, Fs), 0.0) np.testing.assert_almost_equal(calc_centroid(const1, Fs), 0.009499999999999998) - np.testing.assert_almost_equal(calc_centroid(constNeg, Fs), 0.009499999999999998) + np.testing.assert_almost_equal( + calc_centroid(constNeg, Fs), + 0.009499999999999998, + ) np.testing.assert_almost_equal(calc_centroid(constF, Fs), 0.0095) np.testing.assert_almost_equal(calc_centroid(lin, Fs), 0.014615384615384615) np.testing.assert_almost_equal(calc_centroid(lin0, Fs), 0.0095, decimal=5) - np.testing.assert_almost_equal(calc_centroid(wave, Fs), 0.5000000000000001, decimal=5) - np.testing.assert_almost_equal(calc_centroid(offsetWave, Fs), 0.47126367059427926, decimal=5) - np.testing.assert_almost_equal(calc_centroid(noiseWave, Fs), 0.4996034303128802, decimal=5) + np.testing.assert_almost_equal( + calc_centroid(wave, Fs), + 0.5000000000000001, + decimal=5, + ) + np.testing.assert_almost_equal( + calc_centroid(offsetWave, Fs), + 0.47126367059427926, + decimal=5, + ) + np.testing.assert_almost_equal( + calc_centroid(noiseWave, Fs), + 0.4996034303128802, + decimal=5, + ) def test_mean_diff(self): np.testing.assert_almost_equal(mean_diff(const0), 0.0) @@ -267,10 +514,21 @@ def test_mean_diff(self): np.testing.assert_almost_equal(mean_diff(constF), 0.0) np.testing.assert_almost_equal(mean_diff(lin), 1.0) np.testing.assert_almost_equal(mean_diff(lin0), 1.0526315789473684, decimal=5) - np.testing.assert_almost_equal(mean_diff(wave), -3.1442201279407477e-05, decimal=5) - np.testing.assert_almost_equal(mean_diff(offsetWave), -3.1442201279407036e-05, decimal=5) - np.testing.assert_almost_equal(mean_diff(noiseWave), -0.00010042477181949707, decimal=5) - + np.testing.assert_almost_equal( + mean_diff(wave), + -3.1442201279407477e-05, + decimal=5, + ) + np.testing.assert_almost_equal( + mean_diff(offsetWave), + -3.1442201279407036e-05, + decimal=5, + ) + np.testing.assert_almost_equal( + mean_diff(noiseWave), + -0.00010042477181949707, + decimal=5, + ) def test_median_diff(self): np.testing.assert_almost_equal(median_diff(const0), 0.0) @@ -279,10 +537,21 @@ def test_median_diff(self): np.testing.assert_almost_equal(median_diff(constF), 0.0) np.testing.assert_almost_equal(median_diff(lin), 1.0) np.testing.assert_almost_equal(median_diff(lin0), 1.0526315789473684, decimal=5) - np.testing.assert_almost_equal(median_diff(wave), -0.0004934396342684, decimal=5) - np.testing.assert_almost_equal(median_diff(offsetWave), -0.0004934396342681779, decimal=5) - np.testing.assert_almost_equal(median_diff(noiseWave), -0.004174819648320949, decimal=5) - + np.testing.assert_almost_equal( + median_diff(wave), + -0.0004934396342684, + decimal=5, + ) + np.testing.assert_almost_equal( + median_diff(offsetWave), + -0.0004934396342681779, + decimal=5, + ) + np.testing.assert_almost_equal( + median_diff(noiseWave), + -0.004174819648320949, + decimal=5, + ) def test_calc_mean_abs_diff(self): np.testing.assert_almost_equal(mean_abs_diff(const0), 0.0) @@ -290,11 +559,26 @@ def test_calc_mean_abs_diff(self): np.testing.assert_almost_equal(mean_abs_diff(constNeg), 0.0) np.testing.assert_almost_equal(mean_abs_diff(constF), 0.0) np.testing.assert_almost_equal(mean_abs_diff(lin), 1.0) - np.testing.assert_almost_equal(mean_abs_diff(lin0), 1.0526315789473684, decimal=5) - np.testing.assert_almost_equal(mean_abs_diff(wave), 0.019988577818740614, decimal=5) - np.testing.assert_almost_equal(mean_abs_diff(offsetWave), 0.019988577818740614, decimal=5) - np.testing.assert_almost_equal(mean_abs_diff(noiseWave), 0.10700252903161508, decimal=5) - + np.testing.assert_almost_equal( + mean_abs_diff(lin0), + 1.0526315789473684, + decimal=5, + ) + np.testing.assert_almost_equal( + mean_abs_diff(wave), + 0.019988577818740614, + decimal=5, + ) + np.testing.assert_almost_equal( + mean_abs_diff(offsetWave), + 0.019988577818740614, + decimal=5, + ) + np.testing.assert_almost_equal( + mean_abs_diff(noiseWave), + 0.10700252903161508, + decimal=5, + ) def test_median_abs_diff(self): np.testing.assert_almost_equal(median_abs_diff(const0), 0.0) @@ -302,11 +586,26 @@ def test_median_abs_diff(self): np.testing.assert_almost_equal(median_abs_diff(constNeg), 0.0) np.testing.assert_almost_equal(median_abs_diff(constF), 0.0) np.testing.assert_almost_equal(median_abs_diff(lin), 1.0) - np.testing.assert_almost_equal(median_abs_diff(lin0), 1.0526315789473681, decimal=5) - np.testing.assert_almost_equal(median_abs_diff(wave), 0.0218618462348652, decimal=5) - np.testing.assert_almost_equal(median_abs_diff(offsetWave), 0.021861846234865645, decimal=5) - np.testing.assert_almost_equal(median_abs_diff(noiseWave), 0.08958750592592835, decimal=5) - + np.testing.assert_almost_equal( + median_abs_diff(lin0), + 1.0526315789473681, + decimal=5, + ) + np.testing.assert_almost_equal( + median_abs_diff(wave), + 0.0218618462348652, + decimal=5, + ) + np.testing.assert_almost_equal( + median_abs_diff(offsetWave), + 0.021861846234865645, + decimal=5, + ) + np.testing.assert_almost_equal( + median_abs_diff(noiseWave), + 0.08958750592592835, + decimal=5, + ) def test_sum_abs_diff(self): np.testing.assert_almost_equal(sum_abs_diff(const0), 0.0) @@ -315,10 +614,21 @@ def test_sum_abs_diff(self): np.testing.assert_almost_equal(sum_abs_diff(constF), 0.0) np.testing.assert_almost_equal(sum_abs_diff(lin), 19) np.testing.assert_almost_equal(sum_abs_diff(lin0), 20.0, decimal=5) - np.testing.assert_almost_equal(sum_abs_diff(wave), 19.968589240921872, decimal=5) - np.testing.assert_almost_equal(sum_abs_diff(offsetWave), 19.968589240921872, decimal=5) - np.testing.assert_almost_equal(sum_abs_diff(noiseWave), 106.89552650258346, decimal=5) - + np.testing.assert_almost_equal( + sum_abs_diff(wave), + 19.968589240921872, + decimal=5, + ) + np.testing.assert_almost_equal( + sum_abs_diff(offsetWave), + 19.968589240921872, + decimal=5, + ) + np.testing.assert_almost_equal( + sum_abs_diff(noiseWave), + 106.89552650258346, + decimal=5, + ) def test_zerocross(self): np.testing.assert_almost_equal(zero_cross(const0), 0.0) @@ -331,18 +641,16 @@ def test_zerocross(self): np.testing.assert_almost_equal(zero_cross(offsetWave), 0.0, decimal=5) np.testing.assert_almost_equal(zero_cross(noiseWave), 38, decimal=5) - def test_autocorr(self): - np.testing.assert_almost_equal(autocorr(const0), 0.0) - np.testing.assert_almost_equal(autocorr(const1), 20.0) - np.testing.assert_almost_equal(autocorr(constNeg), 20.0) - np.testing.assert_almost_equal(autocorr(constF), 125.0) - np.testing.assert_almost_equal(autocorr(lin), 2470.0) - np.testing.assert_almost_equal(autocorr(lin0), 736.8421052631579, decimal=0) - np.testing.assert_almost_equal(autocorr(wave), 500.5, decimal=0) - np.testing.assert_almost_equal(autocorr(offsetWave), 4500.0, decimal=0) - np.testing.assert_almost_equal(autocorr(noiseWave), 508.6149018530489, decimal=0) - + np.testing.assert_almost_equal(autocorr(const0), 1) + np.testing.assert_almost_equal(autocorr(const1), 1) + np.testing.assert_almost_equal(autocorr(constNeg), 1) + np.testing.assert_almost_equal(autocorr(constF), 1) + np.testing.assert_almost_equal(autocorr(lin), 6) + np.testing.assert_almost_equal(autocorr(lin0), 6) + np.testing.assert_almost_equal(autocorr(wave), 40) + np.testing.assert_almost_equal(autocorr(offsetWave), 40) + np.testing.assert_almost_equal(autocorr(noiseWave), 39) def test_auc(self): np.testing.assert_almost_equal(auc(const0, Fs), 0.0) @@ -355,7 +663,6 @@ def test_auc(self): np.testing.assert_almost_equal(auc(offsetWave, Fs), 1.998015705379539) np.testing.assert_almost_equal(auc(noiseWave, Fs), 0.6375702578824347) - def test_abs_energy(self): np.testing.assert_almost_equal(abs_energy(const0), 0.0) np.testing.assert_almost_equal(abs_energy(const1), 20.0) @@ -367,7 +674,6 @@ def test_abs_energy(self): np.testing.assert_almost_equal(abs_energy(offsetWave), 4500.0) np.testing.assert_almost_equal(abs_energy(noiseWave), 508.11883669335725) - def test_pk_pk_distance(self): np.testing.assert_almost_equal(pk_pk_distance(const0), 0.0) np.testing.assert_almost_equal(pk_pk_distance(const1), 0.0) @@ -379,7 +685,6 @@ def test_pk_pk_distance(self): np.testing.assert_almost_equal(pk_pk_distance(offsetWave), 2.0) np.testing.assert_almost_equal(pk_pk_distance(noiseWave), 2.4800109800001993) - def test_slope(self): np.testing.assert_almost_equal(slope(const0), 0.0) np.testing.assert_almost_equal(slope(const1), -8.935559365603017e-18) @@ -391,7 +696,6 @@ def test_slope(self): np.testing.assert_almost_equal(slope(offsetWave), -0.00038194082891805853) np.testing.assert_almost_equal(slope(noiseWave), -0.00040205425841671337) - def test_entropy(self): np.testing.assert_almost_equal(entropy(const0), 0.0) np.testing.assert_almost_equal(entropy(const1), 0.0) @@ -400,10 +704,9 @@ def test_entropy(self): np.testing.assert_almost_equal(entropy(lin), 1.0) np.testing.assert_almost_equal(entropy(lin0), 1.0) np.testing.assert_almost_equal(entropy(wave), 0.9620267810255854) - np.testing.assert_almost_equal(entropy(offsetWave), 0.8890012261845581) + np.testing.assert_almost_equal(entropy(offsetWave), 0.8891261649211666) np.testing.assert_almost_equal(entropy(noiseWave), 1.0) - def test_neighbourhood_peaks(self): np.testing.assert_almost_equal(neighbourhood_peaks(const0), 0.0) np.testing.assert_almost_equal(neighbourhood_peaks(const1), 0.0) @@ -415,6 +718,16 @@ def test_neighbourhood_peaks(self): np.testing.assert_almost_equal(neighbourhood_peaks(offsetWave), 5.0) np.testing.assert_almost_equal(neighbourhood_peaks(noiseWave), 14.0) + def test_lempel_ziv(self): + np.testing.assert_almost_equal(lempel_ziv(const0), 0.25) + np.testing.assert_almost_equal(lempel_ziv(const1), 0.25) + np.testing.assert_almost_equal(lempel_ziv(constNeg), 0.25) + np.testing.assert_almost_equal(lempel_ziv(constF), 0.25) + np.testing.assert_almost_equal(lempel_ziv(lin), 0.4) + np.testing.assert_almost_equal(lempel_ziv(lin0), 0.4) + np.testing.assert_almost_equal(lempel_ziv(wave), 0.066) + np.testing.assert_almost_equal(lempel_ziv(offsetWave), 0.066) + np.testing.assert_almost_equal(lempel_ziv(noiseWave), 0.079) # ################################################ SPECTRAL FEATURES ################################################# # def test_max_fre(self): @@ -438,7 +751,11 @@ def test_med_fre(self): np.testing.assert_almost_equal(median_frequency(lin0, Fs), 150.0, decimal=1) np.testing.assert_almost_equal(median_frequency(wave, Fs), 5.0, decimal=1) np.testing.assert_almost_equal(median_frequency(offsetWave, Fs), 0.0, decimal=1) - np.testing.assert_almost_equal(median_frequency(noiseWave, Fs), 146.0, decimal=1) + np.testing.assert_almost_equal( + median_frequency(noiseWave, Fs), + 146.0, + decimal=1, + ) np.testing.assert_almost_equal(median_frequency(x, Fs), 4.0, decimal=1) def test_fund_fre(self): @@ -448,20 +765,51 @@ def test_fund_fre(self): np.testing.assert_almost_equal(fundamental_frequency(constF, Fs), 0.0) np.testing.assert_almost_equal(fundamental_frequency(lin, Fs), 50.0) np.testing.assert_almost_equal(fundamental_frequency(lin0, Fs), 50.0, decimal=1) - np.testing.assert_almost_equal(fundamental_frequency(wave, Fs), 5.0100200400801596, decimal=1) - np.testing.assert_almost_equal(fundamental_frequency(offsetWave, Fs), 5.0100200400801596, decimal=1) - np.testing.assert_almost_equal(fundamental_frequency(noiseWave, Fs), 5.0100200400801596, decimal=1) + np.testing.assert_almost_equal( + fundamental_frequency(wave, Fs), + 5.0100200400801596, + decimal=1, + ) + np.testing.assert_almost_equal( + fundamental_frequency(offsetWave, Fs), + 5.0100200400801596, + decimal=1, + ) + np.testing.assert_almost_equal( + fundamental_frequency(noiseWave, Fs), + 5.0100200400801596, + decimal=1, + ) def test_power_spec(self): np.testing.assert_almost_equal(max_power_spectrum(const0, Fs), 0.0) np.testing.assert_almost_equal(max_power_spectrum(const1, Fs), 0.0) np.testing.assert_almost_equal(max_power_spectrum(constNeg, Fs), 0.0) np.testing.assert_almost_equal(max_power_spectrum(constF, Fs), 0.0) - np.testing.assert_almost_equal(max_power_spectrum(lin, Fs), 0.004621506382612649) - np.testing.assert_almost_equal(max_power_spectrum(lin0, Fs), 0.0046215063826126525, decimal=5) - np.testing.assert_almost_equal(max_power_spectrum(wave, Fs), 0.6666666666666667, decimal=5) - np.testing.assert_almost_equal(max_power_spectrum(offsetWave, Fs), 0.6666666666666667, decimal=5) - np.testing.assert_almost_equal(max_power_spectrum(noiseWave, Fs), 0.6570878541643916, decimal=5) + np.testing.assert_almost_equal( + max_power_spectrum(lin, Fs), + 0.004621506382612649, + ) + np.testing.assert_almost_equal( + max_power_spectrum(lin0, Fs), + 0.0046215063826126525, + decimal=5, + ) + np.testing.assert_almost_equal( + max_power_spectrum(wave, Fs), + 0.6666666666666667, + decimal=5, + ) + np.testing.assert_almost_equal( + max_power_spectrum(offsetWave, Fs), + 0.6666666666666667, + decimal=5, + ) + np.testing.assert_almost_equal( + max_power_spectrum(noiseWave, Fs), + 0.6570878541643916, + decimal=5, + ) def test_average_power(self): np.testing.assert_almost_equal(average_power(const0, Fs), 0.0) @@ -469,66 +817,207 @@ def test_average_power(self): np.testing.assert_almost_equal(average_power(constNeg, Fs), 1052.6315789473686) np.testing.assert_almost_equal(average_power(constF, Fs), 6578.9473684210525) np.testing.assert_almost_equal(average_power(lin, Fs), 130000.0) - np.testing.assert_almost_equal(average_power(lin0, Fs), 38781.16343490305, decimal=5) - np.testing.assert_almost_equal(average_power(wave, Fs), 500.5005005005005, decimal=5) - np.testing.assert_almost_equal(average_power(offsetWave, Fs), 4504.504504504504, decimal=5) - np.testing.assert_almost_equal(average_power(noiseWave, Fs), 508.6274641575148, decimal=5) + np.testing.assert_almost_equal( + average_power(lin0, Fs), + 38781.16343490305, + decimal=5, + ) + np.testing.assert_almost_equal( + average_power(wave, Fs), + 500.5005005005005, + decimal=5, + ) + np.testing.assert_almost_equal( + average_power(offsetWave, Fs), + 4504.504504504504, + decimal=5, + ) + np.testing.assert_almost_equal( + average_power(noiseWave, Fs), + 508.6274641575148, + decimal=5, + ) def test_spectral_centroid(self): np.testing.assert_almost_equal(spectral_centroid(const0, Fs), 0.0) - np.testing.assert_almost_equal(spectral_centroid(const1, Fs), 2.7476856540265033e-14) - np.testing.assert_almost_equal(spectral_centroid(constNeg, Fs), 2.7476856540265033e-14) - np.testing.assert_almost_equal(spectral_centroid(constF, Fs), 2.4504208511457478e-14) - np.testing.assert_almost_equal(spectral_centroid(lin, Fs), 96.7073273343592, decimal=5) - np.testing.assert_almost_equal(spectral_centroid(lin0, Fs), 186.91474845748346, decimal=5) - np.testing.assert_almost_equal(spectral_centroid(wave, Fs), 5.000000000003773, decimal=5) - np.testing.assert_almost_equal(spectral_centroid(offsetWave, Fs), 1.0000000000008324, decimal=5) - np.testing.assert_almost_equal(spectral_centroid(noiseWave, Fs), 181.0147784750644, decimal=5) + np.testing.assert_almost_equal( + spectral_centroid(const1, Fs), + 2.7476856540265033e-14, + ) + np.testing.assert_almost_equal( + spectral_centroid(constNeg, Fs), + 2.7476856540265033e-14, + ) + np.testing.assert_almost_equal( + spectral_centroid(constF, Fs), + 2.4504208511457478e-14, + ) + np.testing.assert_almost_equal( + spectral_centroid(lin, Fs), + 96.7073273343592, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_centroid(lin0, Fs), + 186.91474845748346, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_centroid(wave, Fs), + 5.000000000003773, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_centroid(offsetWave, Fs), + 1.0000000000008324, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_centroid(noiseWave, Fs), + 181.0147784750644, + decimal=5, + ) def test_spectral_spread(self): np.testing.assert_almost_equal(spectral_spread(const0, Fs), 0.0, decimal=5) - np.testing.assert_almost_equal(spectral_spread(const1, Fs), 2.811883163207112e-06, decimal=5) - np.testing.assert_almost_equal(spectral_spread(constNeg, Fs), 2.811883163207112e-06, decimal=5) - np.testing.assert_almost_equal(spectral_spread(constF, Fs), 2.657703172211011e-06, decimal=5) - np.testing.assert_almost_equal(spectral_spread(lin, Fs), 138.77058121011598, decimal=5) - np.testing.assert_almost_equal(spectral_spread(lin0, Fs), 142.68541769470383, decimal=5) - np.testing.assert_almost_equal(spectral_spread(wave, Fs), 3.585399057660381e-05, decimal=5) - np.testing.assert_almost_equal(spectral_spread(offsetWave, Fs), 2.0000000000692015, decimal=5) - np.testing.assert_almost_equal(spectral_spread(noiseWave, Fs), 165.48999545678365, decimal=5) + np.testing.assert_almost_equal( + spectral_spread(const1, Fs), + 2.811883163207112e-06, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_spread(constNeg, Fs), + 2.811883163207112e-06, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_spread(constF, Fs), + 2.657703172211011e-06, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_spread(lin, Fs), + 138.77058121011598, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_spread(lin0, Fs), + 142.68541769470383, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_spread(wave, Fs), + 3.585399057660381e-05, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_spread(offsetWave, Fs), + 2.0000000000692015, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_spread(noiseWave, Fs), + 165.48999545678365, + decimal=5, + ) def test_spectral_skewness(self): np.testing.assert_almost_equal(spectral_skewness(const0, Fs), 0.0, decimal=5) np.testing.assert_almost_equal(spectral_skewness(const1, Fs), 0.0, decimal=5) np.testing.assert_almost_equal(spectral_skewness(constNeg, Fs), 0.0, decimal=5) np.testing.assert_almost_equal(spectral_skewness(constF, Fs), 0.0, decimal=5) - np.testing.assert_almost_equal(spectral_skewness(lin, Fs), 1.4986055403796703, decimal=5) - np.testing.assert_almost_equal(spectral_skewness(lin0, Fs), 0.8056481576984844, decimal=5) - np.testing.assert_almost_equal(spectral_skewness(wave, Fs), 10746623.828906002, decimal=1) - np.testing.assert_almost_equal(spectral_skewness(offsetWave, Fs), 1.5000000137542306, decimal=1) - np.testing.assert_almost_equal(spectral_skewness(noiseWave, Fs), 0.4126776686583098, decimal=1) + np.testing.assert_almost_equal( + spectral_skewness(lin, Fs), + 1.4986055403796703, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_skewness(lin0, Fs), + 0.8056481576984844, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_skewness(wave, Fs), + 10757350.436568316, + decimal=1, + ) + np.testing.assert_almost_equal( + spectral_skewness(offsetWave, Fs), + 1.5000000137542306, + decimal=1, + ) + np.testing.assert_almost_equal( + spectral_skewness(noiseWave, Fs), + 0.4126776686583098, + decimal=1, + ) def test_spectral_kurtosis(self): np.testing.assert_almost_equal(spectral_kurtosis(const0, Fs), 0.0, decimal=5) np.testing.assert_almost_equal(spectral_kurtosis(const1, Fs), 0.0, decimal=5) np.testing.assert_almost_equal(spectral_kurtosis(constNeg, Fs), 0.0, decimal=5) np.testing.assert_almost_equal(spectral_kurtosis(constF, Fs), 0.0, decimal=5) - np.testing.assert_almost_equal(spectral_kurtosis(lin, Fs), 4.209140226148914, decimal=0) - np.testing.assert_almost_equal(spectral_kurtosis(lin0, Fs), 2.378341102603641, decimal=5) - np.testing.assert_almost_equal(spectral_kurtosis(wave, Fs), 123297212118194.1, decimal=1) - np.testing.assert_almost_equal(spectral_kurtosis(offsetWave, Fs), 3.2500028252333513, decimal=5) - np.testing.assert_almost_equal(spectral_kurtosis(noiseWave, Fs), 1.7248024846010621, decimal=5) - + np.testing.assert_almost_equal( + spectral_kurtosis(lin, Fs), + 4.209140226148914, + decimal=0, + ) + np.testing.assert_almost_equal( + spectral_kurtosis(lin0, Fs), + 2.378341102603641, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_kurtosis(wave, Fs), + 123562213974218.03, + decimal=1, + ) + np.testing.assert_almost_equal( + spectral_kurtosis(offsetWave, Fs), + 3.2500028252333513, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_kurtosis(noiseWave, Fs), + 1.7248024846010621, + decimal=5, + ) def test_spectral_slope(self): np.testing.assert_almost_equal(spectral_slope(const0, Fs), 0.0) - np.testing.assert_almost_equal(spectral_slope(const1, Fs), -0.0009090909090909091) - np.testing.assert_almost_equal(spectral_slope(constNeg, Fs), -0.0009090909090909091) - np.testing.assert_almost_equal(spectral_slope(constF, Fs), -0.0009090909090909091) + np.testing.assert_almost_equal( + spectral_slope(const1, Fs), + -0.0009090909090909091, + ) + np.testing.assert_almost_equal( + spectral_slope(constNeg, Fs), + -0.0009090909090909091, + ) + np.testing.assert_almost_equal( + spectral_slope(constF, Fs), + -0.0009090909090909091, + ) np.testing.assert_almost_equal(spectral_slope(lin, Fs), -0.0005574279006023302) - np.testing.assert_almost_equal(spectral_slope(lin0, Fs), -0.00023672490168659717, decimal=1) - np.testing.assert_almost_equal(spectral_slope(wave, Fs), -2.3425149700598465e-05, decimal=5) - np.testing.assert_almost_equal(spectral_slope(offsetWave, Fs), -2.380838323353288e-05, decimal=5) - np.testing.assert_almost_equal(spectral_slope(noiseWave, Fs), -6.586047565550932e-06, decimal=5) + np.testing.assert_almost_equal( + spectral_slope(lin0, Fs), + -0.00023672490168659717, + decimal=1, + ) + np.testing.assert_almost_equal( + spectral_slope(wave, Fs), + -2.3425149700598465e-05, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_slope(offsetWave, Fs), + -2.380838323353288e-05, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_slope(noiseWave, Fs), + -6.586047565550932e-06, + decimal=5, + ) def test_spectral_decrease(self): np.testing.assert_almost_equal(spectral_decrease(const0, Fs), 0.0) @@ -536,10 +1025,26 @@ def test_spectral_decrease(self): np.testing.assert_almost_equal(spectral_decrease(constNeg, Fs), 0.0) np.testing.assert_almost_equal(spectral_decrease(constF, Fs), 0.0) np.testing.assert_almost_equal(spectral_decrease(lin, Fs), -2.2331549790804033) - np.testing.assert_almost_equal(spectral_decrease(lin0, Fs), 0.49895105698521264, decimal=5) - np.testing.assert_almost_equal(spectral_decrease(wave, Fs), 0.19999999999999687, decimal=5) - np.testing.assert_almost_equal(spectral_decrease(offsetWave, Fs), -26.97129371996163, decimal=5) - np.testing.assert_almost_equal(spectral_decrease(noiseWave, Fs), 0.06049066756953234, decimal=5) + np.testing.assert_almost_equal( + spectral_decrease(lin0, Fs), + 0.49895105698521264, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_decrease(wave, Fs), + 0.19999999999999687, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_decrease(offsetWave, Fs), + -26.97129371996163, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_decrease(noiseWave, Fs), + 0.06049066756953234, + decimal=5, + ) def test_spectral_roll_on(self): np.testing.assert_almost_equal(spectral_roll_on(const0, Fs), 0.0) @@ -560,8 +1065,16 @@ def test_spectral_roll_off(self): np.testing.assert_almost_equal(spectral_roll_off(lin, Fs), 450.0) np.testing.assert_almost_equal(spectral_roll_off(lin0, Fs), 450.0, decimal=5) np.testing.assert_almost_equal(spectral_roll_off(wave, Fs), 5.0, decimal=5) - np.testing.assert_almost_equal(spectral_roll_off(offsetWave, Fs), 5.0, decimal=5) - np.testing.assert_almost_equal(spectral_roll_off(noiseWave, Fs), 465.0, decimal=5) + np.testing.assert_almost_equal( + spectral_roll_off(offsetWave, Fs), + 5.0, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_roll_off(noiseWave, Fs), + 465.0, + decimal=5, + ) def test_spectral_distance(self): np.testing.assert_almost_equal(spectral_distance(const0, Fs), 0.0) @@ -569,10 +1082,26 @@ def test_spectral_distance(self): np.testing.assert_almost_equal(spectral_distance(constNeg, Fs), -110.0) np.testing.assert_almost_equal(spectral_distance(constF, Fs), -275.0) np.testing.assert_almost_equal(spectral_distance(lin, Fs), -1403.842529396485) - np.testing.assert_almost_equal(spectral_distance(lin0, Fs), -377.7289783120891, decimal=5) - np.testing.assert_almost_equal(spectral_distance(wave, Fs), -122750.0, decimal=1) - np.testing.assert_almost_equal(spectral_distance(offsetWave, Fs), -623750.0, decimal=5) - np.testing.assert_almost_equal(spectral_distance(noiseWave, Fs), -125372.23803384512, decimal=5) + np.testing.assert_almost_equal( + spectral_distance(lin0, Fs), + -377.7289783120891, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_distance(wave, Fs), + -122750.0, + decimal=1, + ) + np.testing.assert_almost_equal( + spectral_distance(offsetWave, Fs), + -623750.0, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_distance(noiseWave, Fs), + -125372.23803384512, + decimal=5, + ) def test_spect_variation(self): np.testing.assert_almost_equal(spectral_variation(const0, Fs), 1.0) @@ -580,10 +1109,26 @@ def test_spect_variation(self): np.testing.assert_almost_equal(spectral_variation(constNeg, Fs), 1.0) np.testing.assert_almost_equal(spectral_variation(constF, Fs), 1.0) np.testing.assert_almost_equal(spectral_variation(lin, Fs), 0.04330670010243309) - np.testing.assert_almost_equal(spectral_variation(lin0, Fs), 0.3930601189429277, decimal=5) - np.testing.assert_almost_equal(spectral_variation(wave, Fs), 0.9999999999999997, decimal=5) - np.testing.assert_almost_equal(spectral_variation(offsetWave, Fs), 0.9999999999999999, decimal=5) - np.testing.assert_almost_equal(spectral_variation(noiseWave, Fs), 0.9775800433849368, decimal=5) + np.testing.assert_almost_equal( + spectral_variation(lin0, Fs), + 0.3930601189429277, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_variation(wave, Fs), + 0.9999999999999997, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_variation(offsetWave, Fs), + 0.9999999999999999, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_variation(noiseWave, Fs), + 0.9775800433849368, + decimal=5, + ) def test_spectral_positive_turning(self): np.testing.assert_almost_equal(spectral_positive_turning(const0, Fs), 0.0) @@ -591,10 +1136,26 @@ def test_spectral_positive_turning(self): np.testing.assert_almost_equal(spectral_positive_turning(constNeg, Fs), 0.0) np.testing.assert_almost_equal(spectral_positive_turning(constF, Fs), 0.0) np.testing.assert_almost_equal(spectral_positive_turning(lin, Fs), 0.0) - np.testing.assert_almost_equal(spectral_positive_turning(lin0, Fs), 1.0, decimal=5) - np.testing.assert_almost_equal(spectral_positive_turning(wave, Fs), 161, decimal=0) - np.testing.assert_almost_equal(spectral_positive_turning(offsetWave, Fs), 160, decimal=1) - np.testing.assert_almost_equal(spectral_positive_turning(noiseWave, Fs), 173.0, decimal=1) + np.testing.assert_almost_equal( + spectral_positive_turning(lin0, Fs), + 1.0, + decimal=5, + ) + np.testing.assert_almost_equal( + spectral_positive_turning(wave, Fs), + 161, + decimal=0, + ) + np.testing.assert_almost_equal( + spectral_positive_turning(offsetWave, Fs), + 161, + decimal=1, + ) + np.testing.assert_almost_equal( + spectral_positive_turning(noiseWave, Fs), + 173.0, + decimal=1, + ) def test_human_range_energy(self): np.testing.assert_almost_equal(human_range_energy(const0, Fs), 0.0) @@ -603,52 +1164,173 @@ def test_human_range_energy(self): np.testing.assert_almost_equal(human_range_energy(constF, Fs), 0.0) np.testing.assert_almost_equal(human_range_energy(lin, Fs), 0.0) np.testing.assert_almost_equal(human_range_energy(lin0, Fs), 0.0) - np.testing.assert_almost_equal(human_range_energy(wave, Fs), 2.838300923247935e-33) - np.testing.assert_almost_equal(human_range_energy(offsetWave, Fs), 1.6194431630448383e-33) - np.testing.assert_almost_equal(human_range_energy(noiseWave, Fs), 4.5026865350839304e-05) + np.testing.assert_almost_equal( + human_range_energy(wave, Fs), + 2.838300923247935e-33, + ) + np.testing.assert_almost_equal( + human_range_energy(offsetWave, Fs), + 1.6194431630448383e-33, + ) + np.testing.assert_almost_equal( + human_range_energy(noiseWave, Fs), + 4.5026865350839304e-05, + ) def test_mfcc(self): - np.testing.assert_almost_equal(mfcc(const0, Fs), (-1e-08, -2.5654632210061364e-08, -4.099058125255727e-08, - -5.56956514302075e-08, -6.947048992011573e-08, - -8.203468073398136e-08, - -9.313245317896842e-08, -1.0253788861142992e-07, - -1.1005951948899701e-07, - -1.1554422709759472e-07, -1.1888035860690259e-07, - -1.2000000000000002e-07)) - np.testing.assert_almost_equal(mfcc(const1, Fs), (0.14096637144714785, 0.4029720554090289, 0.2377457745400458, - 0.9307791929462678, -0.8138023913445843, -0.36127671623673, - 0.17779314470940918, 1.5842014538963525, -5.868875380858009, - -1.3484207382203723, -1.5899059472962034, 2.9774371742123975)) - np.testing.assert_almost_equal(mfcc(constNeg, Fs), (0.14096637144714785, 0.4029720554090289, 0.2377457745400458, - 0.9307791929462678, -0.8138023913445843, -0.36127671623673, - 0.17779314470940918, 1.5842014538963525, -5.868875380858009, - -1.3484207382203723, -1.5899059472962034, 2.9774371742123975)) - np.testing.assert_almost_equal(mfcc(constF, Fs), (0.1409663714471363, 0.40297205540906766, 0.23774577454002216, - 0.9307791929463864, -0.8138023913445535, -0.3612767162368284, - 0.17779314470931407, 1.584201453896316, -5.868875380858139, - -1.3484207382203004, -1.589905947296293, 2.977437174212552)) - np.testing.assert_almost_equal(mfcc(lin, Fs), (63.41077963677539, 42.33256774689686, 22.945623346731722, - -9.267967765468333, -30.918618746635172, -69.45624761250505, - -81.74881720705784, -112.32234611356338, -127.73335353282954, - -145.3505024599537, -152.08439229251312, -170.61228411241296)) - np.testing.assert_almost_equal(mfcc(lin0, Fs), (4.472854975902669, 9.303621966161266, 12.815317252229947, - 12.65260020301481, 9.763110307405048, 3.627814979708572, - 1.0051648150842092, -8.07514557618858, -24.79987026383853, - -36.55749714126207, -49.060094200797785, -61.45654150658956)) - np.testing.assert_almost_equal(mfcc(wave, Fs), (115.31298449242963, -23.978080415791883, 64.49711308839377, - -70.83883973188331, -17.4881594184545, -122.5191336465161, - -89.73379214517978, -164.5583844690884, -153.29482394321641, - -204.0607944643521, -189.9059214788022, -219.38937674972897)) - np.testing.assert_almost_equal(mfcc(offsetWave, Fs), (0.02803261518615674, 0.21714705316418328, 0.4010268706527706, - 1.0741653432632032, -0.26756380975236493, - -0.06446520044381611, 1.2229170142535633, 2.2173729990650166, - -5.161787305125577, -1.777027230578585, -2.2267834681371506, - 1.266610194040295)) - np.testing.assert_almost_equal(mfcc(noiseWave, Fs), (-59.93874366630627, -20.646010360067542, -5.9381521505819, - 13.868391975194648, 65.73380784148053, 67.65563377433688, - 35.223042940942214, 73.01746718829553, 137.50395589362876, - 111.61718917042731, 82.69709467796633, 110.67135918512074)) - + np.testing.assert_almost_equal( + mfcc(const0, Fs), + ( + -1e-08, + -2.5654632210061364e-08, + -4.099058125255727e-08, + -5.56956514302075e-08, + -6.947048992011573e-08, + -8.203468073398136e-08, + -9.313245317896842e-08, + -1.0253788861142992e-07, + -1.1005951948899701e-07, + -1.1554422709759472e-07, + -1.1888035860690259e-07, + -1.2000000000000002e-07, + ), + ) + np.testing.assert_almost_equal( + mfcc(const1, Fs), + ( + 0.14096637144714785, + 0.4029720554090289, + 0.2377457745400458, + 0.9307791929462678, + -0.8138023913445843, + -0.36127671623673, + 0.17779314470940918, + 1.5842014538963525, + -5.868875380858009, + -1.3484207382203723, + -1.5899059472962034, + 2.9774371742123975, + ), + ) + np.testing.assert_almost_equal( + mfcc(constNeg, Fs), + ( + 0.14096637144714785, + 0.4029720554090289, + 0.2377457745400458, + 0.9307791929462678, + -0.8138023913445843, + -0.36127671623673, + 0.17779314470940918, + 1.5842014538963525, + -5.868875380858009, + -1.3484207382203723, + -1.5899059472962034, + 2.9774371742123975, + ), + ) + np.testing.assert_almost_equal( + mfcc(constF, Fs), + ( + 0.1409663714471363, + 0.40297205540906766, + 0.23774577454002216, + 0.9307791929463864, + -0.8138023913445535, + -0.3612767162368284, + 0.17779314470931407, + 1.584201453896316, + -5.868875380858139, + -1.3484207382203004, + -1.589905947296293, + 2.977437174212552, + ), + ) + np.testing.assert_almost_equal( + mfcc(lin, Fs), + ( + 63.41077963677539, + 42.33256774689686, + 22.945623346731722, + -9.267967765468333, + -30.918618746635172, + -69.45624761250505, + -81.74881720705784, + -112.32234611356338, + -127.73335353282954, + -145.3505024599537, + -152.08439229251312, + -170.61228411241296, + ), + ) + np.testing.assert_almost_equal( + mfcc(lin0, Fs), + ( + 4.472854975902669, + 9.303621966161266, + 12.815317252229947, + 12.65260020301481, + 9.763110307405048, + 3.627814979708572, + 1.0051648150842092, + -8.07514557618858, + -24.79987026383853, + -36.55749714126207, + -49.060094200797785, + -61.45654150658956, + ), + ) + np.testing.assert_almost_equal( + mfcc(wave, Fs), + ( + 115.31298449242963, + -23.978080415791883, + 64.49711308839377, + -70.83883973188331, + -17.4881594184545, + -122.5191336465161, + -89.73379214517978, + -164.5583844690884, + -153.29482394321641, + -204.0607944643521, + -189.9059214788022, + -219.38937674972897, + ), + ) + np.testing.assert_almost_equal( + mfcc(offsetWave, Fs), + ( + 0.02803261518615674, + 0.21714705316418328, + 0.4010268706527706, + 1.0741653432632032, + -0.26756380975236493, + -0.06446520044381611, + 1.2229170142535633, + 2.2173729990650166, + -5.161787305125577, + -1.777027230578585, + -2.2267834681371506, + 1.266610194040295, + ), + ) + np.testing.assert_almost_equal( + mfcc(noiseWave, Fs), + ( + -59.93874366630627, + -20.646010360067542, + -5.9381521505819, + 13.868391975194648, + 65.73380784148053, + 67.65563377433688, + 35.223042940942214, + 73.01746718829553, + 137.50395589362876, + 111.61718917042731, + 82.69709467796633, + 110.67135918512074, + ), + ) def test_power_bandwidth(self): np.testing.assert_almost_equal(power_bandwidth(const0, Fs), 0.0) @@ -662,86 +1344,241 @@ def test_power_bandwidth(self): np.testing.assert_almost_equal(power_bandwidth(noiseWave, Fs), 2.0) def test_fft_mean_coeff(self): - np.testing.assert_almost_equal(fft_mean_coeff(const0, Fs, nfreq=10), - (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)) - np.testing.assert_almost_equal(fft_mean_coeff(const1, Fs, nfreq=10), - (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)) - np.testing.assert_almost_equal(fft_mean_coeff(constNeg, Fs, nfreq=10), - (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)) - np.testing.assert_almost_equal(fft_mean_coeff(constF, Fs, nfreq=10), - (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)) - np.testing.assert_almost_equal(fft_mean_coeff(lin, Fs, nfreq=10), (0.00408221375370652, 0.29732082717207287, - 0.04400486791011177, 0.006686945426272411, - 0.00027732608206304087, 0.0003337183893114616, - 0.0008722727267959805, 0.0007221373313148659, - 0.00024061479410220662, 2.1097101108186473e-07)) - np.testing.assert_almost_equal(fft_mean_coeff(lin0, Fs, nfreq=10), (0.004523228535962903, 0.3294413597474491, - 0.04875885641009613, 0.007409357813044217, - 0.00030728651752137475, 0.0003697710684891545, - 0.0009665071765052403, 0.0008001521676618994, - 0.00026660919014094884, 2.337628931654879e-07)) - np.testing.assert_almost_equal(fft_mean_coeff(wave, Fs, nfreq=10), (2.0234880089914443e-06, 0.0001448004568848076, - 2.1047578415647817e-05, 3.2022732210152474e-06, - 1.52158292419209e-07, 1.7741879185514087e-07, - 4.2795757073284126e-07, 3.5003942541628605e-07, - 1.1626895252132188e-07, 1.6727906953620535e-10)) - np.testing.assert_almost_equal(fft_mean_coeff(offsetWave, Fs, nfreq=10), (2.0234880089914642e-06, - 0.00014480045688480763, - 2.104757841564781e-05, - 3.2022732210152483e-06, - 1.5215829241920897e-07, - 1.7741879185514156e-07, - 4.27957570732841e-07, - 3.500394254162859e-07, - 1.1626895252132173e-07, - 1.6727906953620255e-10)) - np.testing.assert_almost_equal(fft_mean_coeff(noiseWave, Fs, nfreq=10), (3.2947755935395495e-06, - 0.00014466702099241778, - 3.838265852158549e-05, - 1.6729032217627548e-05, - 1.6879950037320804e-05, - 1.571169205601392e-05, - 1.679718723715948e-05, - 1.810371503556574e-05, - 2.0106126483830693e-05, - 8.91285109135437e-06)) - + np.testing.assert_almost_equal( + fft_mean_coeff(const0, Fs, nfreq=10), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + ) + np.testing.assert_almost_equal( + fft_mean_coeff(const1, Fs, nfreq=10), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + ) + np.testing.assert_almost_equal( + fft_mean_coeff(constNeg, Fs, nfreq=10), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + ) + np.testing.assert_almost_equal( + fft_mean_coeff(constF, Fs, nfreq=10), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + ) + np.testing.assert_almost_equal( + fft_mean_coeff(lin, Fs, nfreq=10), + ( + 0.00408221375370652, + 0.29732082717207287, + 0.04400486791011177, + 0.006686945426272411, + 0.00027732608206304087, + 0.0003337183893114616, + 0.0008722727267959805, + 0.0007221373313148659, + 0.00024061479410220662, + 2.1097101108186473e-07, + ), + ) + np.testing.assert_almost_equal( + fft_mean_coeff(lin0, Fs, nfreq=10), + ( + 0.004523228535962903, + 0.3294413597474491, + 0.04875885641009613, + 0.007409357813044217, + 0.00030728651752137475, + 0.0003697710684891545, + 0.0009665071765052403, + 0.0008001521676618994, + 0.00026660919014094884, + 2.337628931654879e-07, + ), + ) + np.testing.assert_almost_equal( + fft_mean_coeff(wave, Fs, nfreq=10), + ( + 2.0234880089914443e-06, + 0.0001448004568848076, + 2.1047578415647817e-05, + 3.2022732210152474e-06, + 1.52158292419209e-07, + 1.7741879185514087e-07, + 4.2795757073284126e-07, + 3.5003942541628605e-07, + 1.1626895252132188e-07, + 1.6727906953620535e-10, + ), + ) + np.testing.assert_almost_equal( + fft_mean_coeff(offsetWave, Fs, nfreq=10), + ( + 2.0234880089914642e-06, + 0.00014480045688480763, + 2.104757841564781e-05, + 3.2022732210152483e-06, + 1.5215829241920897e-07, + 1.7741879185514156e-07, + 4.27957570732841e-07, + 3.500394254162859e-07, + 1.1626895252132173e-07, + 1.6727906953620255e-10, + ), + ) + np.testing.assert_almost_equal( + fft_mean_coeff(noiseWave, Fs, nfreq=10), + ( + 3.2947755935395495e-06, + 0.00014466702099241778, + 3.838265852158549e-05, + 1.6729032217627548e-05, + 1.6879950037320804e-05, + 1.571169205601392e-05, + 1.679718723715948e-05, + 1.810371503556574e-05, + 2.0106126483830693e-05, + 8.91285109135437e-06, + ), + ) def test_lpcc(self): - np.testing.assert_almost_equal(lpcc(const0), (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) - np.testing.assert_almost_equal(lpcc(const1), ((0.04123300285294014, 1.0090886040206166, 0.5477967308058846, - 0.38934442309407374, 0.3182978464880446, 0.28521488787857235, - 0.2753824147772057, 0.2852148878785723, 0.3182978464880446, - 0.3893444230940738, 0.5477967308058846, 1.0090886040206166))) - np.testing.assert_almost_equal(lpcc(constNeg), (0.04123300285294014, 1.0090886040206166, 0.5477967308058846, - 0.38934442309407374, 0.3182978464880446, 0.28521488787857235, - 0.2753824147772057, 0.2852148878785723, 0.3182978464880446, - 0.3893444230940738, 0.5477967308058846, 1.0090886040206166)) - np.testing.assert_almost_equal(lpcc(constF), (0.04123300285293459, 1.0090886040206097, 0.54779673080588, - 0.3893444230940775, 0.31829784648804227, 0.2852148878785703, - 0.2753824147772089, 0.28521488787857036, 0.31829784648804227, - 0.3893444230940775, 0.54779673080588, 1.0090886040206097)) - np.testing.assert_almost_equal(lpcc(lin), (0.0008115287870079275, 0.949808211511519, 0.469481269387107, - 0.31184557567242355, 0.24105584503772784, 0.2081345982990198, - 0.198356078000162, 0.2081345982990197, 0.24105584503772784, - 0.31184557567242355, 0.469481269387107, 0.9498082115115191)) - np.testing.assert_almost_equal(lpcc(lin0), (0.14900616258072136, 0.7120654174490035, 0.28220640800360736, - 0.13200549895670105, 0.0674236160580709, 0.03817662578918231, - 0.029619974765142276, 0.03817662578918224, 0.0674236160580709, - 0.13200549895670105, 0.28220640800360736, 0.7120654174490035)) - np.testing.assert_almost_equal(lpcc(wave), (0.3316269831953852, 2.2936534454791397, 1.3657365894469182, - 1.0070201818573588, 0.8468783257961441, 0.8794963357759213, - 0.6999667686345086, 0.8794963357759211, 0.8468783257961441, - 1.0070201818573588, 1.3657365894469182, 2.2936534454791397)) - np.testing.assert_almost_equal(lpcc(offsetWave), (0.6113077446051783, 1.650942970269406, 1.191078896704431, - 1.0313503136863278, 0.9597088652698047, 0.9261323880465244, - 0.9160979308646922, 0.9261323880465244, 0.9597088652698047, - 1.031350313686328, 1.1910788967044308, 1.650942970269406)) - np.testing.assert_almost_equal(lpcc(noiseWave), (0.3907899246825849, 0.6498327698888337, 0.7444466184462464, - 0.7833967114468317, 0.7517838305481447, 0.7739966761714876, - 0.8210271929385791, 0.7739966761714876, 0.7517838305481447, - 0.7833967114468317, 0.7444466184462464, 0.6498327698888338)) - + np.testing.assert_almost_equal( + lpcc(const0), + (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + ) + np.testing.assert_almost_equal( + lpcc(const1), + ( + ( + 0.04123300285294014, + 1.0090886040206166, + 0.5477967308058846, + 0.38934442309407374, + 0.3182978464880446, + 0.28521488787857235, + 0.2753824147772057, + 0.2852148878785723, + 0.3182978464880446, + 0.3893444230940738, + 0.5477967308058846, + 1.0090886040206166, + ) + ), + ) + np.testing.assert_almost_equal( + lpcc(constNeg), + ( + 0.04123300285294014, + 1.0090886040206166, + 0.5477967308058846, + 0.38934442309407374, + 0.3182978464880446, + 0.28521488787857235, + 0.2753824147772057, + 0.2852148878785723, + 0.3182978464880446, + 0.3893444230940738, + 0.5477967308058846, + 1.0090886040206166, + ), + ) + np.testing.assert_almost_equal( + lpcc(constF), + ( + 0.04123300285293459, + 1.0090886040206097, + 0.54779673080588, + 0.3893444230940775, + 0.31829784648804227, + 0.2852148878785703, + 0.2753824147772089, + 0.28521488787857036, + 0.31829784648804227, + 0.3893444230940775, + 0.54779673080588, + 1.0090886040206097, + ), + ) + np.testing.assert_almost_equal( + lpcc(lin), + ( + 0.0008115287870079275, + 0.949808211511519, + 0.469481269387107, + 0.31184557567242355, + 0.24105584503772784, + 0.2081345982990198, + 0.198356078000162, + 0.2081345982990197, + 0.24105584503772784, + 0.31184557567242355, + 0.469481269387107, + 0.9498082115115191, + ), + ) + np.testing.assert_almost_equal( + lpcc(lin0), + ( + 0.14900616258072136, + 0.7120654174490035, + 0.28220640800360736, + 0.13200549895670105, + 0.0674236160580709, + 0.03817662578918231, + 0.029619974765142276, + 0.03817662578918224, + 0.0674236160580709, + 0.13200549895670105, + 0.28220640800360736, + 0.7120654174490035, + ), + ) + np.testing.assert_almost_equal( + lpcc(wave), + ( + 0.3316269831953852, + 2.2936534454791397, + 1.3657365894469182, + 1.0070201818573588, + 0.8468783257961441, + 0.8794963357759213, + 0.6999667686345086, + 0.8794963357759211, + 0.8468783257961441, + 1.0070201818573588, + 1.3657365894469182, + 2.2936534454791397, + ), + ) + np.testing.assert_almost_equal( + lpcc(offsetWave), + ( + 0.6113077446051783, + 1.650942970269406, + 1.191078896704431, + 1.0313503136863278, + 0.9597088652698047, + 0.9261323880465244, + 0.9160979308646922, + 0.9261323880465244, + 0.9597088652698047, + 1.031350313686328, + 1.1910788967044308, + 1.650942970269406, + ), + ) + np.testing.assert_almost_equal( + lpcc(noiseWave), + ( + 0.3907899246825849, + 0.6498327698888337, + 0.7444466184462464, + 0.7833967114468317, + 0.7517838305481447, + 0.7739966761714876, + 0.8210271929385791, + 0.7739966761714876, + 0.7517838305481447, + 0.7833967114468317, + 0.7444466184462464, + 0.6498327698888338, + ), + ) def test_spectral_entropy(self): np.testing.assert_almost_equal(spectral_entropy(const0, Fs), 0.0) @@ -750,9 +1587,18 @@ def test_spectral_entropy(self): np.testing.assert_almost_equal(spectral_entropy(constF, Fs), 0.0) np.testing.assert_almost_equal(spectral_entropy(lin, Fs), 0.5983234852309258) np.testing.assert_almost_equal(spectral_entropy(lin0, Fs), 0.5745416630615365) - np.testing.assert_almost_equal(spectral_entropy(wave, Fs), 1.5228376718814352e-29) - np.testing.assert_almost_equal(spectral_entropy(offsetWave, Fs), 1.783049297437309e-29) - np.testing.assert_almost_equal(spectral_entropy(noiseWave, Fs), 0.0301141821499739) + np.testing.assert_almost_equal( + spectral_entropy(wave, Fs), + 1.5228376718814352e-29, + ) + np.testing.assert_almost_equal( + spectral_entropy(offsetWave, Fs), + 1.783049297437309e-29, + ) + np.testing.assert_almost_equal( + spectral_entropy(noiseWave, Fs), + 0.0301141821499739, + ) def test_wavelet_entropy(self): np.testing.assert_almost_equal(wavelet_entropy(const0), 0.0) @@ -765,138 +1611,594 @@ def test_wavelet_entropy(self): np.testing.assert_almost_equal(wavelet_entropy(offsetWave), 1.7965939302139549) np.testing.assert_almost_equal(wavelet_entropy(noiseWave), 2.0467527462416153) - def test_wavelet_abs_mean(self): - np.testing.assert_almost_equal(wavelet_abs_mean(const0), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)) - np.testing.assert_almost_equal(wavelet_abs_mean(const1), - (0.081894185676901, 0.24260084511769256, 0.4653470776794248, - 0.8500400580778283, 1.3602249381214044, 1.8378460432593602, - 2.2080039502231164, 2.4676456085810874, 2.638131856418627)) - np.testing.assert_almost_equal(wavelet_abs_mean(constNeg), - (0.081894185676901, 0.24260084511769256, 0.4653470776794248, - 0.8500400580778283, 1.3602249381214044, 1.8378460432593602, - 2.2080039502231164, 2.4676456085810874, 2.638131856418627)) - np.testing.assert_almost_equal(wavelet_abs_mean(constF), - (0.20473546419225214, 0.6065021127942314, 1.1633676941985622, - 2.1251001451945712, 3.4005623453035114, 4.5946151081484015, - 5.5200098755577915, 6.169114021452717, 6.5953296410465665)) - np.testing.assert_almost_equal(wavelet_abs_mean(lin), (0.7370509925842613, 2.183416725919023, 4.1974435700809565, - 7.744819422931153, 12.504051331233388, 16.982183932901865, - 20.46332353598833, 22.91143100556329, 24.52363151471446)) - np.testing.assert_almost_equal(wavelet_abs_mean(lin0), - (0.0430987066803135, 0.12767505547269026, 0.23510912407745171, - 0.3479590829560181, 0.4400900851788993, 0.5024773453284851, - 0.5396989380329178, 0.5591602904810937, 0.5669696013289379)) - np.testing.assert_almost_equal(wavelet_abs_mean(wave), (5.138703105035948e-05, 0.00015178141653400073, - 0.00027925117450851024, 0.0004278724786267016, - 0.0005932191214607947, 0.0007717034331954587, - 0.0009601854175466062, 0.0011557903088208192, - 0.0013558175034366186)) - np.testing.assert_almost_equal(wavelet_abs_mean(offsetWave), (0.0032504208945027323, 0.009623752088931016, - 0.017761411181034453, 0.027372614777691914, - 0.03826512918833778, 0.050306487368868114, - 0.06339897203822373, 0.07746693331944604, - 0.09244971907566273)) - np.testing.assert_almost_equal(wavelet_abs_mean(noiseWave), (4.631139377647647e-05, 7.893225282164063e-05, - 0.00033257747958655794, 0.0005792253883615155, - 0.0007699898255271558, 0.0009106252575513913, - 0.0010387197644970154, 0.0011789334866018457, - 0.0013341945911985783)) - + np.testing.assert_almost_equal( + wavelet_abs_mean(const0), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + ) + np.testing.assert_almost_equal( + wavelet_abs_mean(const1), + ( + 0.081894185676901, + 0.24260084511769256, + 0.4653470776794248, + 0.8500400580778283, + 1.3602249381214044, + 1.8378460432593602, + 2.2080039502231164, + 2.4676456085810874, + 2.638131856418627, + ), + ) + np.testing.assert_almost_equal( + wavelet_abs_mean(constNeg), + ( + 0.081894185676901, + 0.24260084511769256, + 0.4653470776794248, + 0.8500400580778283, + 1.3602249381214044, + 1.8378460432593602, + 2.2080039502231164, + 2.4676456085810874, + 2.638131856418627, + ), + ) + np.testing.assert_almost_equal( + wavelet_abs_mean(constF), + ( + 0.20473546419225214, + 0.6065021127942314, + 1.1633676941985622, + 2.1251001451945712, + 3.4005623453035114, + 4.5946151081484015, + 5.5200098755577915, + 6.169114021452717, + 6.5953296410465665, + ), + ) + np.testing.assert_almost_equal( + wavelet_abs_mean(lin), + ( + 0.7370509925842613, + 2.183416725919023, + 4.1974435700809565, + 7.744819422931153, + 12.504051331233388, + 16.982183932901865, + 20.46332353598833, + 22.91143100556329, + 24.52363151471446, + ), + ) + np.testing.assert_almost_equal( + wavelet_abs_mean(lin0), + ( + 0.0430987066803135, + 0.12767505547269026, + 0.23510912407745171, + 0.3479590829560181, + 0.4400900851788993, + 0.5024773453284851, + 0.5396989380329178, + 0.5591602904810937, + 0.5669696013289379, + ), + ) + np.testing.assert_almost_equal( + wavelet_abs_mean(wave), + ( + 5.138703105035948e-05, + 0.00015178141653400073, + 0.00027925117450851024, + 0.0004278724786267016, + 0.0005932191214607947, + 0.0007717034331954587, + 0.0009601854175466062, + 0.0011557903088208192, + 0.0013558175034366186, + ), + ) + np.testing.assert_almost_equal( + wavelet_abs_mean(offsetWave), + ( + 0.0032504208945027323, + 0.009623752088931016, + 0.017761411181034453, + 0.027372614777691914, + 0.03826512918833778, + 0.050306487368868114, + 0.06339897203822373, + 0.07746693331944604, + 0.09244971907566273, + ), + ) + np.testing.assert_almost_equal( + wavelet_abs_mean(noiseWave), + ( + 4.631139377647647e-05, + 7.893225282164063e-05, + 0.00033257747958655794, + 0.0005792253883615155, + 0.0007699898255271558, + 0.0009106252575513913, + 0.0010387197644970154, + 0.0011789334866018457, + 0.0013341945911985783, + ), + ) def test_wavelet_std(self): - np.testing.assert_almost_equal(wavelet_std(const0), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)) - np.testing.assert_almost_equal(wavelet_std(const1), (0.1767186264889806, 0.28069306259219023, 0.3235061868750311, - 0.3115893726751135, 0.31446140614407014, 0.3582016825631658, - 0.4133090941627322, 0.4598585090675407, 0.4935514064162697)) - np.testing.assert_almost_equal(wavelet_std(constNeg), (0.1767186264889806, 0.28069306259219023, 0.3235061868750311, - 0.3115893726751135, 0.31446140614407014, 0.3582016825631658, - 0.4133090941627322, 0.4598585090675407, 0.4935514064162697)) - np.testing.assert_almost_equal(wavelet_std(constF), (0.44179656622245145, 0.7017326564804757, 0.8087654671875778, - 0.7789734316877838, 0.7861535153601755, 0.8955042064079146, - 1.0332727354068305, 1.1496462726688517, 1.2338785160406742)) - np.testing.assert_almost_equal(wavelet_std(lin), (2.721791561180164, 5.325234998810811, 8.137581399111415, - 10.529795250703716, 11.836525442245224, 12.296195571788726, - 12.315744378517108, 12.135259348389042, 11.869294506387352)) - np.testing.assert_almost_equal(wavelet_std(lin0), (2.239406940011677, 4.7878443746478245, 7.797954379287043, - 10.418506686200207, 11.746946049852674, 12.045972295386465, - 11.828477896749822, 11.408150997410496, 10.932763618021895)) - np.testing.assert_almost_equal(wavelet_std(wave), (0.001939366875349316, 0.009733675496927717, 0.025635801097107388, - 0.05125305898778544, 0.08783649118731567, 0.13636963970273208, - 0.197613166916789, 0.2721306670702481, 0.360305525758368)) - np.testing.assert_almost_equal(wavelet_std(offsetWave), - (0.05459142980660159, 0.10410347082332229, 0.155831467554863, - 0.2101395066938644, 0.268489203478025, 0.33264452641566, - 0.4044076212671741, 0.4854392072251105, 0.5771385517659353)) - np.testing.assert_almost_equal(wavelet_std(noiseWave), - (0.08974931069698587, 0.09625674025798765, 0.10445386849293256, - 0.11395751571203461, 0.13232763520967267, 0.16659967802754122, - 0.2187573594673847, 0.2877270278501564, 0.3722670641715661)) - + np.testing.assert_almost_equal( + wavelet_std(const0), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + ) + np.testing.assert_almost_equal( + wavelet_std(const1), + ( + 0.1767186264889806, + 0.28069306259219023, + 0.3235061868750311, + 0.3115893726751135, + 0.31446140614407014, + 0.3582016825631658, + 0.4133090941627322, + 0.4598585090675407, + 0.4935514064162697, + ), + ) + np.testing.assert_almost_equal( + wavelet_std(constNeg), + ( + 0.1767186264889806, + 0.28069306259219023, + 0.3235061868750311, + 0.3115893726751135, + 0.31446140614407014, + 0.3582016825631658, + 0.4133090941627322, + 0.4598585090675407, + 0.4935514064162697, + ), + ) + np.testing.assert_almost_equal( + wavelet_std(constF), + ( + 0.44179656622245145, + 0.7017326564804757, + 0.8087654671875778, + 0.7789734316877838, + 0.7861535153601755, + 0.8955042064079146, + 1.0332727354068305, + 1.1496462726688517, + 1.2338785160406742, + ), + ) + np.testing.assert_almost_equal( + wavelet_std(lin), + ( + 2.721791561180164, + 5.325234998810811, + 8.137581399111415, + 10.529795250703716, + 11.836525442245224, + 12.296195571788726, + 12.315744378517108, + 12.135259348389042, + 11.869294506387352, + ), + ) + np.testing.assert_almost_equal( + wavelet_std(lin0), + ( + 2.239406940011677, + 4.7878443746478245, + 7.797954379287043, + 10.418506686200207, + 11.746946049852674, + 12.045972295386465, + 11.828477896749822, + 11.408150997410496, + 10.932763618021895, + ), + ) + np.testing.assert_almost_equal( + wavelet_std(wave), + ( + 0.001939366875349316, + 0.009733675496927717, + 0.025635801097107388, + 0.05125305898778544, + 0.08783649118731567, + 0.13636963970273208, + 0.197613166916789, + 0.2721306670702481, + 0.360305525758368, + ), + ) + np.testing.assert_almost_equal( + wavelet_std(offsetWave), + ( + 0.05459142980660159, + 0.10410347082332229, + 0.155831467554863, + 0.2101395066938644, + 0.268489203478025, + 0.33264452641566, + 0.4044076212671741, + 0.4854392072251105, + 0.5771385517659353, + ), + ) + np.testing.assert_almost_equal( + wavelet_std(noiseWave), + ( + 0.08974931069698587, + 0.09625674025798765, + 0.10445386849293256, + 0.11395751571203461, + 0.13232763520967267, + 0.16659967802754122, + 0.2187573594673847, + 0.2877270278501564, + 0.3722670641715661, + ), + ) def test_wavelet_var(self): - np.testing.assert_almost_equal(wavelet_var(const0), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)) - np.testing.assert_almost_equal(wavelet_var(const1), (0.031229472948151833, 0.07878859538738324, 0.10465625294642253, - 0.09708793716407076, 0.09888597595410582, 0.128308445391083, - 0.17082440731761822, 0.21146984836182142, 0.24359299077547786)) - np.testing.assert_almost_equal(wavelet_var(constNeg), - (0.031229472948151833, 0.07878859538738324, 0.10465625294642253, - 0.09708793716407076, 0.09888597595410582, 0.128308445391083, - 0.17082440731761822, 0.21146984836182142, 0.24359299077547786)) - np.testing.assert_almost_equal(wavelet_var(constF), (0.19518420592594893, 0.49242872117114533, 0.654101580915141, - 0.6067996072754422, 0.6180373497131617, 0.8019277836942689, - 1.0676525457351138, 1.3216865522613839, 1.5224561923467361)) - np.testing.assert_almost_equal(wavelet_var(lin), (7.408149302511555, 28.35812779255958, 66.22023102716409, - 110.87658802174253, 140.10333454491848, 151.19642553967668, - 151.67755959697575, 147.26451945266362, 140.88015207935698)) - np.testing.assert_almost_equal(wavelet_var(lin0), (5.014943442972464, 22.923453755846815, 60.808092501441976, - 108.5452815703984, 137.99074149814933, 145.10544854121827, - 139.91288935389912, 130.1459091797181, 119.5253203275432)) - np.testing.assert_almost_equal(wavelet_var(wave), - (3.761143877202169e-06, 9.474443867949103e-05, 0.0006571942978904524, - 0.0026268760556054137, 0.007715249184099382, 0.018596678632652963, - 0.03905096373888271, 0.07405509996009821, 0.12982007189201397)) - np.testing.assert_almost_equal(wavelet_var(offsetWave), - (0.0029802242083291084, 0.010837532637462314, 0.02428344628030232, - 0.044158612273540676, 0.07208645238426431, 0.11065238095429873, - 0.16354552413897414, 0.2356512239113438, 0.33308890793448115)) - np.testing.assert_almost_equal(wavelet_var(noiseWave), - (0.008054938770584103, 0.0092653600450937, 0.01091061064313885, - 0.012986315387258616, 0.017510603040184203, 0.027755452718880403, - 0.04785478232114257, 0.08278684255548469, 0.1385827670669169)) - + np.testing.assert_almost_equal( + wavelet_var(const0), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + ) + np.testing.assert_almost_equal( + wavelet_var(const1), + ( + 0.031229472948151833, + 0.07878859538738324, + 0.10465625294642253, + 0.09708793716407076, + 0.09888597595410582, + 0.128308445391083, + 0.17082440731761822, + 0.21146984836182142, + 0.24359299077547786, + ), + ) + np.testing.assert_almost_equal( + wavelet_var(constNeg), + ( + 0.031229472948151833, + 0.07878859538738324, + 0.10465625294642253, + 0.09708793716407076, + 0.09888597595410582, + 0.128308445391083, + 0.17082440731761822, + 0.21146984836182142, + 0.24359299077547786, + ), + ) + np.testing.assert_almost_equal( + wavelet_var(constF), + ( + 0.19518420592594893, + 0.49242872117114533, + 0.654101580915141, + 0.6067996072754422, + 0.6180373497131617, + 0.8019277836942689, + 1.0676525457351138, + 1.3216865522613839, + 1.5224561923467361, + ), + ) + np.testing.assert_almost_equal( + wavelet_var(lin), + ( + 7.408149302511555, + 28.35812779255958, + 66.22023102716409, + 110.87658802174253, + 140.10333454491848, + 151.19642553967668, + 151.67755959697575, + 147.26451945266362, + 140.88015207935698, + ), + ) + np.testing.assert_almost_equal( + wavelet_var(lin0), + ( + 5.014943442972464, + 22.923453755846815, + 60.808092501441976, + 108.5452815703984, + 137.99074149814933, + 145.10544854121827, + 139.91288935389912, + 130.1459091797181, + 119.5253203275432, + ), + ) + np.testing.assert_almost_equal( + wavelet_var(wave), + ( + 3.761143877202169e-06, + 9.474443867949103e-05, + 0.0006571942978904524, + 0.0026268760556054137, + 0.007715249184099382, + 0.018596678632652963, + 0.03905096373888271, + 0.07405509996009821, + 0.12982007189201397, + ), + ) + np.testing.assert_almost_equal( + wavelet_var(offsetWave), + ( + 0.0029802242083291084, + 0.010837532637462314, + 0.02428344628030232, + 0.044158612273540676, + 0.07208645238426431, + 0.11065238095429873, + 0.16354552413897414, + 0.2356512239113438, + 0.33308890793448115, + ), + ) + np.testing.assert_almost_equal( + wavelet_var(noiseWave), + ( + 0.008054938770584103, + 0.0092653600450937, + 0.01091061064313885, + 0.012986315387258616, + 0.017510603040184203, + 0.027755452718880403, + 0.04785478232114257, + 0.08278684255548469, + 0.1385827670669169, + ), + ) def test_wavelet_energy(self): - np.testing.assert_almost_equal(wavelet_energy(const0), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)) - np.testing.assert_almost_equal(wavelet_energy(const1), (0.19477199643643478, 0.3710037269882903, 0.56674875884399, - 0.9053485723747671, 1.3961009484422982, 1.8724279756816202, - 2.2463539016634275, 2.510128422593423, 2.683902509896041)) - np.testing.assert_almost_equal(wavelet_energy(constNeg), (0.19477199643643478, 0.3710037269882903, 0.56674875884399, - 0.9053485723747671, 1.3961009484422982, - 1.8724279756816202, - 2.2463539016634275, 2.510128422593423, 2.683902509896041)) - np.testing.assert_almost_equal(wavelet_energy(constF), (0.48692999109108687, 0.9275093174707258, 1.4168718971099752, - 2.263371430936918, 3.4902523711057456, 4.6810699392040505, - 5.615884754158569, 6.275321056483556, 6.709756274740101)) - np.testing.assert_almost_equal(wavelet_energy(lin), (2.819821531264169, 5.7554701277638936, 9.156350995411767, - 13.071297407509103, 17.21785800380053, 20.966425462405052, - 23.883575313078858, 25.926785187819767, 27.244974853151422)) - np.testing.assert_almost_equal(wavelet_energy(lin0), (2.2398216316238173, 4.789546395603321, 7.8014978562880115, - 10.424315665491429, 11.75518697346929, 12.056447736534448, - 11.84078393931808, 11.421846147193937, 10.947455177180416)) - np.testing.assert_almost_equal(wavelet_energy(wave), - (0.0019400475520363772, 0.00973485882167256, 0.025637321995655413, - 0.051254844946242696, 0.08783849436907175, 0.13637182318514984, - 0.19761549963228792, 0.2721331214889804, 0.3603080766970352)) - np.testing.assert_almost_equal(wavelet_energy(offsetWave), - (0.054688110630378595, 0.10454735406375197, 0.15684040935755078, - 0.21191477606176637, 0.27120227229148447, 0.3364269959823273, - 0.4093469845918956, 0.49158147815928066, 0.584496243351187)) - np.testing.assert_almost_equal(wavelet_energy(noiseWave), - (0.08974932264551803, 0.09625677262091348, 0.10445439794914707, - 0.11395898775133596, 0.13232987540429264, 0.16660216672432593, - 0.2187598255162308, 0.2877294431226156, 0.37226945502166053)) - - -if __name__ == '__main__': + np.testing.assert_almost_equal( + wavelet_energy(const0), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + ) + np.testing.assert_almost_equal( + wavelet_energy(const1), + ( + 0.19477199643643478, + 0.3710037269882903, + 0.56674875884399, + 0.9053485723747671, + 1.3961009484422982, + 1.8724279756816202, + 2.2463539016634275, + 2.510128422593423, + 2.683902509896041, + ), + ) + np.testing.assert_almost_equal( + wavelet_energy(constNeg), + ( + 0.19477199643643478, + 0.3710037269882903, + 0.56674875884399, + 0.9053485723747671, + 1.3961009484422982, + 1.8724279756816202, + 2.2463539016634275, + 2.510128422593423, + 2.683902509896041, + ), + ) + np.testing.assert_almost_equal( + wavelet_energy(constF), + ( + 0.48692999109108687, + 0.9275093174707258, + 1.4168718971099752, + 2.263371430936918, + 3.4902523711057456, + 4.6810699392040505, + 5.615884754158569, + 6.275321056483556, + 6.709756274740101, + ), + ) + np.testing.assert_almost_equal( + wavelet_energy(lin), + ( + 2.819821531264169, + 5.7554701277638936, + 9.156350995411767, + 13.071297407509103, + 17.21785800380053, + 20.966425462405052, + 23.883575313078858, + 25.926785187819767, + 27.244974853151422, + ), + ) + np.testing.assert_almost_equal( + wavelet_energy(lin0), + ( + 2.2398216316238173, + 4.789546395603321, + 7.8014978562880115, + 10.424315665491429, + 11.75518697346929, + 12.056447736534448, + 11.84078393931808, + 11.421846147193937, + 10.947455177180416, + ), + ) + np.testing.assert_almost_equal( + wavelet_energy(wave), + ( + 0.0019400475520363772, + 0.00973485882167256, + 0.025637321995655413, + 0.051254844946242696, + 0.08783849436907175, + 0.13637182318514984, + 0.19761549963228792, + 0.2721331214889804, + 0.3603080766970352, + ), + ) + np.testing.assert_almost_equal( + wavelet_energy(offsetWave), + ( + 0.054688110630378595, + 0.10454735406375197, + 0.15684040935755078, + 0.21191477606176637, + 0.27120227229148447, + 0.3364269959823273, + 0.4093469845918956, + 0.49158147815928066, + 0.584496243351187, + ), + ) + np.testing.assert_almost_equal( + wavelet_energy(noiseWave), + ( + 0.08974932264551803, + 0.09625677262091348, + 0.10445439794914707, + 0.11395898775133596, + 0.13232987540429264, + 0.16660216672432593, + 0.2187598255162308, + 0.2877294431226156, + 0.37226945502166053, + ), + ) + + # ################################################ FRACTAL FEATURES ################################################# # + + def test_dfa(self): + np.testing.assert_almost_equal(dfa(const0), np.nan) + np.testing.assert_almost_equal(dfa(const1), np.nan) + np.testing.assert_almost_equal(dfa(constNeg), np.nan) + np.testing.assert_almost_equal(dfa(constF), np.nan) + np.testing.assert_almost_equal(dfa(wave), 2.0354620960383225) + np.testing.assert_almost_equal(dfa(offsetWave), 2.0354620960383234) + np.testing.assert_almost_equal(dfa(noiseWave), 1.5878329458221712) + np.testing.assert_almost_equal(dfa(whiteNoise), 0.512887549688051) + np.testing.assert_almost_equal(dfa(pinkNoise), 1.0162533608512214) + np.testing.assert_almost_equal(dfa(brownNoise), 1.5183298484325374) + + def test_hurst_exponent(self): + np.testing.assert_almost_equal(hurst_exponent(const0), np.nan) + np.testing.assert_almost_equal(hurst_exponent(const1), np.nan) + np.testing.assert_almost_equal(hurst_exponent(constNeg), np.nan) + np.testing.assert_almost_equal(hurst_exponent(constF), np.nan) + np.testing.assert_almost_equal(hurst_exponent(wave), 0.998709262523381) + np.testing.assert_almost_equal(hurst_exponent(offsetWave), 0.9987092625233801) + np.testing.assert_almost_equal(hurst_exponent(noiseWave), 1.080805529048927) + np.testing.assert_almost_equal(hurst_exponent(whiteNoise), 0.5705064906877406) + np.testing.assert_almost_equal(hurst_exponent(pinkNoise), 0.9225990076703923) + np.testing.assert_almost_equal(hurst_exponent(brownNoise), 0.9996474734595799) + + def test_higuchi_fractal_dimension(self): + np.testing.assert_almost_equal( + higuchi_fractal_dimension(wave), + 1.1116648974914232, + ) + np.testing.assert_almost_equal( + higuchi_fractal_dimension(offsetWave), + 1.1116648974914232, + ) + np.testing.assert_almost_equal( + higuchi_fractal_dimension(noiseWave), + 1.2787337809642858, + ) + np.testing.assert_almost_equal( + higuchi_fractal_dimension(whiteNoise), + 1.9999190166166754, + ) + np.testing.assert_almost_equal( + higuchi_fractal_dimension(pinkNoise), + 1.9303823647578682, + ) + np.testing.assert_almost_equal( + higuchi_fractal_dimension(brownNoise), + 1.581020130301515, + ) + + def test_maximum_fractal_length(self): + np.testing.assert_almost_equal(maximum_fractal_length(wave), 1.4082260937055102) + np.testing.assert_almost_equal( + maximum_fractal_length(offsetWave), + 1.40822609370551, + ) + np.testing.assert_almost_equal( + maximum_fractal_length(noiseWave), + 1.6957110038772847, + ) + np.testing.assert_almost_equal( + maximum_fractal_length(whiteNoise), + 3.7493320152840734, + ) + np.testing.assert_almost_equal( + maximum_fractal_length(pinkNoise), + 3.542078306170788, + ) + np.testing.assert_almost_equal( + maximum_fractal_length(brownNoise), + 2.4274067988328794, + ) + + def test_petrosian_fractal_dimension(self): + np.testing.assert_almost_equal(petrosian_fractal_dimension(const0), 1.0) + np.testing.assert_almost_equal(petrosian_fractal_dimension(const1), 1.0) + np.testing.assert_almost_equal(petrosian_fractal_dimension(constNeg), 1.0) + np.testing.assert_almost_equal(petrosian_fractal_dimension(constF), 1.0) + np.testing.assert_almost_equal(petrosian_fractal_dimension(lin), 1.0) + np.testing.assert_almost_equal(petrosian_fractal_dimension(lin0), 1.0) + np.testing.assert_almost_equal( + petrosian_fractal_dimension(wave), + 1.000578238436128, + ) + np.testing.assert_almost_equal( + petrosian_fractal_dimension(offsetWave), + 1.000578238436128, + ) + np.testing.assert_almost_equal( + petrosian_fractal_dimension(noiseWave), + 1.0343688500384227, + ) + np.testing.assert_almost_equal( + petrosian_fractal_dimension(whiteNoise), + 1.0285596700513615, + ) + np.testing.assert_almost_equal( + petrosian_fractal_dimension(pinkNoise), + 1.0247825195115237, + ) + np.testing.assert_almost_equal( + petrosian_fractal_dimension(brownNoise), + 1.0193455287912367, + ) + + def test_mse(self): + np.testing.assert_almost_equal(mse(const0), np.nan) + np.testing.assert_almost_equal(mse(const1), np.nan) + np.testing.assert_almost_equal(mse(constNeg), np.nan) + np.testing.assert_almost_equal(mse(constF), np.nan) + np.testing.assert_almost_equal(mse(wave), 0.08721585110301311) + np.testing.assert_almost_equal(mse(offsetWave), 0.08721585110301311) + np.testing.assert_almost_equal(mse(noiseWave), 0.11937683012271358) + + +if __name__ == "__main__": unittest.main() diff --git a/tests/test_features_settings.py b/tests/test_features_settings.py index 1b1e3a5..943cbda 100644 --- a/tests/test_features_settings.py +++ b/tests/test_features_settings.py @@ -1,27 +1,29 @@ import tsfel -FEATURES_JSON = tsfel.__path__[0] + '/feature_extraction/features.json' +FEATURES_JSON = tsfel.__path__[0] + "/feature_extraction/features.json" settings0 = tsfel.load_json(FEATURES_JSON) -settings1 = tsfel.get_features_by_domain('statistical') +settings1 = tsfel.get_features_by_domain("statistical") -settings2 = tsfel.get_features_by_domain('temporal') +settings2 = tsfel.get_features_by_domain("temporal") -settings3 = tsfel.get_features_by_domain('spectral') +settings3 = tsfel.get_features_by_domain("spectral") + +settings12 = tsfel.get_features_by_domain("fractal") settings4 = tsfel.get_features_by_domain(None) # settings5 = tsfel.extract_sheet('Features') -settings6 = tsfel.get_features_by_tag('audio') +settings6 = tsfel.get_features_by_tag("audio") -settings7 = tsfel.get_features_by_tag('inertial') +settings7 = tsfel.get_features_by_tag("inertial") -settings8 = tsfel.get_features_by_tag('ecg') +settings8 = tsfel.get_features_by_tag("ecg") -settings9 = tsfel.get_features_by_tag('eeg') +settings9 = tsfel.get_features_by_tag("eeg") -settings10 = tsfel.get_features_by_tag('emg') +settings10 = tsfel.get_features_by_tag("emg") settings11 = tsfel.get_features_by_tag(None) diff --git a/tests/tests_tools/test_data/complexity_datasets.py b/tests/tests_tools/test_data/complexity_datasets.py new file mode 100644 index 0000000..11423c8 --- /dev/null +++ b/tests/tests_tools/test_data/complexity_datasets.py @@ -0,0 +1,214 @@ +"""This module provides functions for creating or downloading representative +data from several dataset sources. These functions are designed to enhance the +understanding of complexity measures related to dynamical systems. + +The implementation for generating colored noise is sourced from the +'colorednoise' PyPI package, with credit to Felix Patzelt. +""" + +import os +from typing import Iterable, Optional, Union + +import numpy as np +import pandas as pd +from numpy import integer, newaxis, sqrt +from numpy import sum as npsum +from numpy.fft import irfft, rfftfreq +from numpy.random import Generator, RandomState, default_rng + + +def powerlaw_psd_gaussian( + exponent: float, + size: Union[int, Iterable[int]], + fmin: float = 0.0, + random_state: Optional[Union[int, Generator, RandomState]] = None, +): + """Gaussian (1/f)**beta noise. + + Based on the algorithm in: + Timmer, J. and Koenig, M.: + On generating power law noise. + Astron. Astrophys. 300, 707-710 (1995) + + Normalised to unit variance + + Parameters: + ----------- + + exponent : float + The power-spectrum of the generated noise is proportional to + + S(f) = (1 / f)**beta + flicker / pink noise: exponent beta = 1 + brown noise: exponent beta = 2 + + Furthermore, the autocorrelation decays proportional to lag**-gamma + with gamma = 1 - beta for 0 < beta < 1. + There may be finite-size issues for beta close to one. + + shape : int or iterable + The output has the given shape, and the desired power spectrum in + the last coordinate. That is, the last dimension is taken as time, + and all other components are independent. + + fmin : float, optional + Low-frequency cutoff. + Default: 0 corresponds to original paper. + + The power-spectrum below fmin is flat. fmin is defined relative + to a unit sampling rate (see numpy's rfftfreq). For convenience, + the passed value is mapped to max(fmin, 1/samples) internally + since 1/samples is the lowest possible finite frequency in the + sample. The largest possible value is fmin = 0.5, the Nyquist + frequency. The output for this value is white noise. + + random_state : int, numpy.integer, numpy.random.Generator, numpy.random.RandomState, + optional + Optionally sets the state of NumPy's underlying random number generator. + Integer-compatible values or None are passed to np.random.default_rng. + np.random.RandomState or np.random.Generator are used directly. + Default: None. + + Returns + ------- + out : array + The samples. + + + Examples: + --------- + + # generate 1/f noise == pink noise == flicker noise + """ + + # Make sure size is a list so we can iterate it and assign to it. + if isinstance(size, (integer, int)): + size = [size] + elif isinstance(size, Iterable): + size = list(size) + else: + raise ValueError("Size must be of type int or Iterable[int]") + + # The number of samples in each time series + samples = size[-1] + + # Calculate Frequencies (we asume a sample rate of one) + # Use fft functions for real output (-> hermitian spectrum) + f = rfftfreq(samples) # type: ignore # mypy 1.5.1 has problems here + + # Validate / normalise fmin + if 0 <= fmin <= 0.5: + fmin = max(fmin, 1.0 / samples) # Low frequency cutoff + else: + raise ValueError("fmin must be chosen between 0 and 0.5.") + + # Build scaling factors for all frequencies + s_scale = f + ix = npsum(s_scale < fmin) # Index of the cutoff + if ix and ix < len(s_scale): + s_scale[:ix] = s_scale[ix] + s_scale = s_scale ** (-exponent / 2.0) + + # Calculate theoretical output standard deviation from scaling + w = s_scale[1:].copy() + w[-1] *= (1 + (samples % 2)) / 2.0 # correct f = +-0.5 + sigma = 2 * sqrt(npsum(w**2)) / samples + + # Adjust size to generate one Fourier component per frequency + size[-1] = len(f) + + # Add empty dimension(s) to broadcast s_scale along last + # dimension of generated random power + phase (below) + dims_to_add = len(size) - 1 + s_scale = s_scale[(newaxis,) * dims_to_add + (Ellipsis,)] + + # prepare random number generator + normal_dist = _get_normal_distribution(random_state) + + # Generate scaled random power + phase + sr = normal_dist(scale=s_scale, size=size) + si = normal_dist(scale=s_scale, size=size) + + # If the signal length is even, frequencies +/- 0.5 are equal + # so the coefficient must be real. + if not (samples % 2): + si[..., -1] = 0 + sr[..., -1] *= sqrt(2) # Fix magnitude + + # Regardless of signal length, the DC component must be real + si[..., 0] = 0 + sr[..., 0] *= sqrt(2) # Fix magnitude + + # Combine power + corrected phase to Fourier components + s = sr + 1j * si + + # Transform to real time series & scale to unit variance + y = irfft(s, n=samples, axis=-1) / sigma + + return y + + +def _get_normal_distribution( + random_state: Optional[Union[int, Generator, RandomState]], +): + normal_dist = None + if isinstance(random_state, (integer, int)) or random_state is None: + random_state = default_rng(random_state) + normal_dist = random_state.normal + elif isinstance(random_state, (Generator, RandomState)): + normal_dist = random_state.normal + else: + raise ValueError( + "random_state must be one of integer, numpy.random.Generator, " "numpy.random.Randomstate", + ) + return normal_dist + + +def _get_data_from_url_column(url: str): + return pd.read_csv(url, usecols=[1]).values.flatten() + + +def _get_data_from_url_ucr(url: str): + return pd.read_csv(url, header=None).iloc[0, :][1:].values + + +def _get_data_from_url_plux(url: str): + return np.loadtxt(url)[1] + + +COLORED_NOISE_SAMPLES = 2**12 + + +def load_complexities_datasets(): + metadata = { + "white": {"exponent": 0}, + "pink": {"exponent": 1}, + "brownian": {"exponent": 2}, + "ecg": { + "url": "https://raw.githubusercontent.com/hgamboa/novainstrumentation/master/novainstrumentation/data/cleanecg.txt", + "func": _get_data_from_url_plux, + }, + "airpax": { + "url": "https://raw.githubusercontent.com/AileenNielsen/TimeSeriesAnalysisWithPython/master/data/AirPassengers.csv", + "func": _get_data_from_url_column, + }, + "earthquake": { + "url": "https://raw.githubusercontent.com/AileenNielsen/TimeSeriesAnalysisWithPython/master/data/Earthquakes.csv", + "func": _get_data_from_url_ucr, + }, + "50words": { + "url": "https://raw.githubusercontent.com/AileenNielsen/TimeSeriesAnalysisWithPython/master/data/50words_TEST.csv", + "func": _get_data_from_url_ucr, + }, + } + + dataset = { + key: ( + powerlaw_psd_gaussian(metadata[key]["exponent"], COLORED_NOISE_SAMPLES) + if "exponent" in metadata[key] + else metadata[key]["func"](metadata[key]["url"]) + ) + for key in metadata + } + + return dataset diff --git a/tests/tests_tools/test_features.json b/tests/tests_tools/test_features.json index b68f34d..5682510 100644 --- a/tests/tests_tools/test_features.json +++ b/tests/tests_tools/test_features.json @@ -1,8 +1,62 @@ { + "fractal": { + "Detrended fluctuation analysis": { + "complexity": "nlog", + "description": "Computes the Detrended Fluctuation Analysis (DFA) of the signal.", + "function": "tsfel.dfa", + "n_features": 1, + "parameters": "", + "use": "no" + }, + "Higuchi fractal dimension": { + "complexity": "squared", + "description": "Computes the fractal dimension of a signal using Higuchi's method (HFD).", + "function": "tsfel.higuchi_fractal_dimension", + "n_features": 1, + "parameters": "", + "use": "no" + }, + "Hurst exponent": { + "complexity": "squared", + "description": "Computes the Hurst exponent of the signal through the Rescaled range (R/S) analysis.", + "function": "tsfel.hurst_exponent", + "n_features": 1, + "parameters": "", + "use": "no" + }, + "Maximum fractal length": { + "complexity": "squared", + "description": "Computes the Maximum Fractal Length (MFL) of the signal, which is the average length at the smallest scale, measured from the logarithmic plot determining FD. The Higuchi's method is used.", + "function": "tsfel.maximum_fractal_length", + "n_features": 1, + "parameters": "", + "use": "no" + }, + "Multiscale entropy": { + "complexity": "linear", + "description": "Computes the Multiscale entropy (MSE) of the signal, that performs the entropy analysis over multiple time scales.", + "function": "tsfel.mse", + "n_features": 1, + "parameters": { + "m": 3, + "maxscale": null, + "tolerance": null + }, + "use": "no" + }, + "Petrosian fractal dimension": { + "complexity": "log", + "description": "Computes the Petrosian Fractal Dimension of a signal.", + "function": "tsfel.petrosian_fractal_dimension", + "n_features": 1, + "parameters": "", + "use": "no" + } + }, "new_domain": { "new_feature_with_multiple_tag": { "complexity": "constant", - "description": "A new feature with multiple tags", + "description": "A new feature with multiple tags.", "function": "new_feature_with_multiple_tag", "parameters": "", "tag": [ @@ -13,7 +67,7 @@ }, "new_feature_with_tag": { "complexity": "constant", - "description": "A new feature with a tag", + "description": "A new feature with a tag.", "function": "new_feature_with_tag", "parameters": "", "tag": "inertial", @@ -494,14 +548,14 @@ }, "new_feature": { "complexity": "constant", - "description": "Computes a new feature", + "description": "Computes a new feature.", "function": "new_feature", "parameters": "", "use": "yes" }, "new_feature_with_parameter": { "complexity": "constant", - "description": "A new feature", + "description": "A new feature.", "function": "new_feature_with_parameter", "parameters": { "weight": 0.5 @@ -539,6 +593,16 @@ }, "use": "yes" }, + "Lempel-Ziv complexity": { + "complexity": "linear", + "description": "Computes the Lempel-Ziv's (LZ) complexity index, normalized by the signal's length.", + "function": "tsfel.lempel_ziv", + "n_features": 1, + "parameters": { + "threshold": null + }, + "use": "no" + }, "Mean absolute diff": { "complexity": "constant", "description": "Computes mean absolute differences of the signal.", diff --git a/tests/tests_tools/test_personal_features.py b/tests/tests_tools/test_personal_features.py index bed0704..9328d3a 100644 --- a/tests/tests_tools/test_personal_features.py +++ b/tests/tests_tools/test_personal_features.py @@ -1,10 +1,12 @@ -from tsfel.feature_extraction.features_utils import set_domain import numpy as np +from tsfel.feature_extraction.features_utils import set_domain + @set_domain("domain", "statistical") def new_feature(signal): - """Computes a new feature + """Computes a new feature. + Parameters ---------- signal : nd-array @@ -15,12 +17,13 @@ def new_feature(signal): float new feature """ - return np.mean(signal)-np.std(signal) + return np.mean(signal) - np.std(signal) @set_domain("domain", "statistical") def new_feature_with_parameter(signal, weight=0.5): - """A new feature + """A new feature. + Parameters ---------- signal : nd-array @@ -32,13 +35,14 @@ def new_feature_with_parameter(signal, weight=0.5): float new feature with parameter """ - return np.mean(signal)-np.std(signal)*weight + return np.mean(signal) - np.std(signal) * weight @set_domain("domain", "new_domain") @set_domain("tag", "inertial") def new_feature_with_tag(signal): - """A new feature with a tag + """A new feature with a tag. + Parameters ---------- signal : nd-array @@ -50,13 +54,14 @@ def new_feature_with_tag(signal): float new feature with parameter """ - return np.mean(signal)-np.std(signal) + return np.mean(signal) - np.std(signal) @set_domain("domain", "new_domain") @set_domain("tag", ["inertial", "emg"]) def new_feature_with_multiple_tag(signal): - """A new feature with multiple tags + """A new feature with multiple tags. + Parameters ---------- signal : nd-array @@ -68,5 +73,4 @@ def new_feature_with_multiple_tag(signal): float new feature with parameter """ - return np.mean(signal)-np.std(signal) - + return np.mean(signal) - np.std(signal) diff --git a/tsfel/__init__.py b/tsfel/__init__.py index d4f58f1..f33dc94 100644 --- a/tsfel/__init__.py +++ b/tsfel/__init__.py @@ -1,2 +1,3 @@ from tsfel.utils import * from tsfel.feature_extraction import * +from tsfel.constants import * diff --git a/tsfel/constants.py b/tsfel/constants.py new file mode 100644 index 0000000..8001a08 --- /dev/null +++ b/tsfel/constants.py @@ -0,0 +1 @@ +FEATURES_MIN_SIZE = 160 diff --git a/tsfel/feature_extraction/calc_features.py b/tsfel/feature_extraction/calc_features.py index 49420a9..92e2427 100644 --- a/tsfel/feature_extraction/calc_features.py +++ b/tsfel/feature_extraction/calc_features.py @@ -11,7 +11,6 @@ import numpy as np import pandas as pd - from IPython import get_ipython from IPython.display import display @@ -20,7 +19,7 @@ def dataset_features_extractor(main_directory, feat_dict, verbose=1, **kwargs): - """Extracts features from a dataset. + r"""Extracts features from a dataset. Parameters ---------- @@ -77,35 +76,36 @@ def dataset_features_extractor(main_directory, feat_dict, verbose=1, **kwargs): ------- file csv file with the extracted features - """ - search_criteria = kwargs.get('search_criteria', None) - time_unit = kwargs.get('time_unit', 1e9) - resample_rate = kwargs.get('resample_rate', 30) - window_size = kwargs.get('window_size', 100) - overlap = kwargs.get('overlap', 0) - pre_process = kwargs.get('pre_process', None) - output_directory = kwargs.get('output_directory', str(Path.home()) + '/tsfel_output') - features_path = kwargs.get('features_path', None) - names = kwargs.get('header_names', None) + search_criteria = kwargs.get("search_criteria", None) + time_unit = kwargs.get("time_unit", 1e9) + resample_rate = kwargs.get("resample_rate", 30) + window_size = kwargs.get("window_size", 100) + overlap = kwargs.get("overlap", 0) + pre_process = kwargs.get("pre_process", None) + output_directory = kwargs.get( + "output_directory", + str(Path.home()) + "/tsfel_output", + ) + features_path = kwargs.get("features_path", None) + names = kwargs.get("header_names", None) # Choosing default of n_jobs by operating system - if sys.platform[:-2] == 'win': + if sys.platform[:-2] == "win": n_jobs_default = None else: n_jobs_default = -1 # Choosing default of n_jobs by python interface - if get_ipython().__class__.__name__ == 'ZMQInteractiveShell' or \ - get_ipython().__class__.__name__ == 'Shell': + if get_ipython().__class__.__name__ == "ZMQInteractiveShell" or get_ipython().__class__.__name__ == "Shell": n_jobs_default = -1 - n_jobs = kwargs.get('n_jobs', n_jobs_default) + n_jobs = kwargs.get("n_jobs", n_jobs_default) if main_directory[-1] != os.sep: main_directory = main_directory + os.sep - folders = [f for f in glob.glob(main_directory + "**/", recursive=True)] + folders = list(glob.glob(main_directory + "**/", recursive=True)) if folders: for fl in folders: @@ -113,18 +113,20 @@ def dataset_features_extractor(main_directory, feat_dict, verbose=1, **kwargs): if search_criteria: for c in search_criteria: if os.path.isfile(fl + c): - key = c.split('.')[0] + key = c.split(".")[0] sensor_data[key] = pd.read_csv(fl + c, header=None) else: - all_files = np.concatenate((glob.glob(fl + '/*.txt'), glob.glob(fl + '/*.csv'))) + all_files = np.concatenate( + (glob.glob(fl + "/*.txt"), glob.glob(fl + "/*.csv")), + ) for c in all_files: - key = c.split(os.sep)[-1].split('.')[0] + key = c.split(os.sep)[-1].split(".")[0] try: data_file = pd.read_csv(c, header=None) except pd.io.common.CParserError: continue - if np.dtype('O') in np.array(data_file.dtypes): + if np.dtype("O") in np.array(data_file.dtypes): continue sensor_data[key] = pd.read_csv(c, header=None) @@ -139,28 +141,45 @@ def dataset_features_extractor(main_directory, feat_dict, verbose=1, **kwargs): windows = signal_window_splitter(data_new, window_size, overlap) if features_path: - features = time_series_features_extractor(feat_dict, windows, fs=resample_rate, verbose=0, - features_path=features_path, header_names=names, n_jobs=n_jobs) + features = time_series_features_extractor( + feat_dict, + windows, + fs=resample_rate, + verbose=0, + features_path=features_path, + header_names=names, + n_jobs=n_jobs, + ) else: - features = time_series_features_extractor(feat_dict, windows, fs=resample_rate, verbose=0, - header_names=names, n_jobs=n_jobs) + features = time_series_features_extractor( + feat_dict, + windows, + fs=resample_rate, + verbose=0, + header_names=names, + n_jobs=n_jobs, + ) - fl = '/'.join(fl.split(os.sep)) - invalid_char = '<>:"\|?* ' + fl = "/".join(fl.split(os.sep)) + invalid_char = r'<>:"\|?* ' for char in invalid_char: - fl = fl.replace(char, '') + fl = fl.replace(char, "") pathlib.Path(output_directory + fl).mkdir(parents=True, exist_ok=True) - features.to_csv(output_directory + fl + '/Features.csv', sep=',', encoding='utf-8') + features.to_csv( + output_directory + fl + "/Features.csv", + sep=",", + encoding="utf-8", + ) if verbose == 1: - print('Features files saved in: ', output_directory) + print("Features files saved in: ", output_directory) else: raise FileNotFoundError("There is no folder(s) in directory: " + main_directory) def calc_features(wind_sig, dict_features, fs, **kwargs): - """Extraction of time series features. + r"""Extraction of time series features. Parameters ---------- @@ -181,19 +200,30 @@ def calc_features(wind_sig, dict_features, fs, **kwargs): ------- DataFrame Extracted features - """ - features_path = kwargs.get('features_path', None) - names = kwargs.get('header_names', None) - feat_val = calc_window_features(dict_features, wind_sig, fs, features_path=features_path, header_names=names) + features_path = kwargs.get("features_path", None) + names = kwargs.get("header_names", None) + feat_val = calc_window_features( + dict_features, + wind_sig, + fs, + features_path=features_path, + header_names=names, + ) feat_val.reset_index(drop=True) return feat_val -def time_series_features_extractor(dict_features, signal_windows, fs=None, verbose=1, **kwargs): - """Extraction of time series features. +def time_series_features_extractor( + dict_features, + signal_windows, + fs=None, + verbose=1, + **kwargs, +): + r"""Extraction of time series features. Parameters ---------- @@ -231,31 +261,32 @@ def time_series_features_extractor(dict_features, signal_windows, fs=None, verbo ------- DataFrame Extracted features - """ if verbose == 1: print("*** Feature extraction started ***") - window_size = kwargs.get('window_size', None) - overlap = kwargs.get('overlap', 0) - features_path = kwargs.get('features_path', None) - names = kwargs.get('header_names', None) + window_size = kwargs.get("window_size", None) + overlap = kwargs.get("overlap", 0) + features_path = kwargs.get("features_path", None) + names = kwargs.get("header_names", None) # Choosing default of n_jobs by operating system - if sys.platform[:-2] == 'win': + if sys.platform[:-2] == "win": n_jobs_default = None else: n_jobs_default = -1 # Choosing default of n_jobs by python interface - if get_ipython().__class__.__name__ == 'ZMQInteractiveShell' or \ - get_ipython().__class__.__name__ == 'Shell': + if get_ipython().__class__.__name__ == "ZMQInteractiveShell" or get_ipython().__class__.__name__ == "Shell": n_jobs_default = -1 - n_jobs = kwargs.get('n_jobs', n_jobs_default) + n_jobs = kwargs.get("n_jobs", n_jobs_default) if fs is None: - warnings.warn('Using default sampling frequency set in configuration file.', stacklevel=2) + warnings.warn( + "Using default sampling frequency set in configuration file.", + stacklevel=2, + ) if names is not None: names = list(names) @@ -270,21 +301,27 @@ def time_series_features_extractor(dict_features, signal_windows, fs=None, verbo signal_windows = signal_window_splitter(signal_windows, window_size, overlap) if len(signal_windows) == 0: - raise SystemExit('Empty signal windows. Please check window size input parameter.') + raise SystemExit( + "Empty signal windows. Please check window size input parameter.", + ) features_final = pd.DataFrame() if isinstance(signal_windows, list) and isinstance(signal_windows[0], numbers.Real): signal_windows = np.array(signal_windows) + if len(np.shape(signal_windows)) > 2: + signal_windows = list(signal_windows) + # more than one window if isinstance(signal_windows, list): # Starting the display of progress bar for notebooks interfaces - if (get_ipython().__class__.__name__ == "ZMQInteractiveShell") or ( - get_ipython().__class__.__name__ == "Shell" - ): + if (get_ipython().__class__.__name__ == "ZMQInteractiveShell") or (get_ipython().__class__.__name__ == "Shell"): - out = display(progress_bar_notebook(0, len(signal_windows)), display_id=True) + out = display( + progress_bar_notebook(0, len(signal_windows)), + display_id=True, + ) else: out = None @@ -321,13 +358,20 @@ def time_series_features_extractor(dict_features, signal_windows, fs=None, verbo [ features_final, calc_window_features( - dict_features, feat, fs, features_path=features_path, header_names=names) - ], axis=0) + dict_features, + feat, + fs, + features_path=features_path, + header_names=names, + ), + ], + axis=0, + ) if verbose == 1: display_progress_bar(i, len(signal_windows), out) else: raise SystemExit( - "n_jobs value is not valid. " "Choose an integer value or None for no multiprocessing." + "n_jobs value is not valid. " "Choose an integer value or None for no multiprocessing.", ) # single window else: @@ -342,15 +386,22 @@ def time_series_features_extractor(dict_features, signal_windows, fs=None, verbo ) if verbose == 1: - print("\n"+"*** Feature extraction finished ***") + print("\n" + "*** Feature extraction finished ***") # Assuring the same feature extraction order features_final = features_final.reindex(sorted(features_final.columns), axis=1) return features_final.reset_index(drop=True) -def calc_window_features(dict_features, signal_window, fs, verbose=1, single_window=False, **kwargs): - """This function computes features matrix for one window. +def calc_window_features( + dict_features, + signal_window, + fs, + verbose=1, + single_window=False, + **kwargs, +): + r"""This function computes features matrix for one window. Parameters ---------- @@ -377,11 +428,10 @@ def calc_window_features(dict_features, signal_window, fs, verbose=1, single_win pandas DataFrame (columns) names of the features (data) values of each features for signal - """ - features_path = kwargs.get('features_path', None) - header_names = kwargs.get('header_names', None) + features_path = kwargs.get("features_path", None) + header_names = kwargs.get("header_names", None) # To handle object type signals signal_window = np.array(signal_window).astype(float) @@ -391,8 +441,9 @@ def calc_window_features(dict_features, signal_window, fs, verbose=1, single_win if header_names is None: header_names = np.array([0]) if single_axis else np.arange(signal_window.shape[-1]) else: - if (len(header_names) != signal_window.shape[-1] and not single_axis) or \ - (len(header_names) != 1 and single_axis): + if (len(header_names) != signal_window.shape[-1] and not single_axis) or ( + len(header_names) != 1 and single_axis + ): raise Exception("header_names dimension does not match input columns.") # Execute imports @@ -400,10 +451,10 @@ def calc_window_features(dict_features, signal_window, fs, verbose=1, single_win domain = dict_features.keys() if features_path: - sys.path.append(features_path[:-len(features_path.split(os.sep)[-1])-1]) - exec("import "+features_path.split(os.sep)[-1][:-3]) + sys.path.append(features_path[: -len(features_path.split(os.sep)[-1]) - 1]) + exec("import " + features_path.split(os.sep)[-1][:-3]) importlib.reload(sys.modules[features_path.split(os.sep)[-1][:-3]]) - exec("from " + features_path.split(os.sep)[-1][:-3]+" import *") + exec("from " + features_path.split(os.sep)[-1][:-3] + " import *") # Create global arrays feature_results = [] @@ -415,8 +466,7 @@ def calc_window_features(dict_features, signal_window, fs, verbose=1, single_win feat_nb = np.hstack([list(dict_features[_type].keys()) for _type in domain]) - if (get_ipython().__class__.__name__ == 'ZMQInteractiveShell') or ( - get_ipython().__class__.__name__ == 'Shell'): + if (get_ipython().__class__.__name__ == "ZMQInteractiveShell") or (get_ipython().__class__.__name__ == "Shell"): out = display(progress_bar_notebook(0, len(feat_nb)), display_id=True) else: out = None @@ -460,25 +510,31 @@ def calc_window_features(dict_features, signal_window, fs, verbose=1, single_win # Eval feature results if single_axis: - eval_result = locals()[func_total](signal_window, **parameters_total) + eval_result = locals()[func_total]( + signal_window, + **parameters_total, + ) eval_result = np.array([eval_result]) for ax in range(len(header_names)): sig_ax = signal_window if single_axis else signal_window[:, ax] eval_result_ax = locals()[func_total](sig_ax, **parameters_total) # Function returns more than one element - if type(eval_result_ax) == tuple: + if isinstance(eval_result_ax, tuple): if np.isnan(eval_result_ax[0]): eval_result_ax = np.zeros(len(eval_result_ax)) for rr in range(len(eval_result_ax)): feature_results += [eval_result_ax[rr]] - feature_names += [str(header_names[ax]) + "_" + feat + "_" + str(rr)] + feature_names += [ + str(header_names[ax]) + "_" + feat + "_" + str(rr), + ] else: feature_results += [eval_result_ax] feature_names += [str(header_names[ax]) + "_" + feat] features = pd.DataFrame( - data=np.array(feature_results).reshape(1, len(feature_results)), columns=np.array(feature_names) + data=np.array(feature_results).reshape(1, len(feature_results)), + columns=np.array(feature_names), ) return features diff --git a/tsfel/feature_extraction/features.json b/tsfel/feature_extraction/features.json index 7f6e010..d29853b 100644 --- a/tsfel/feature_extraction/features.json +++ b/tsfel/feature_extraction/features.json @@ -484,8 +484,8 @@ "use": "yes" }, "Autocorrelation": { - "complexity": "constant", - "description": "Computes autocorrelation of the signal.", + "complexity": "linear", + "description": "Calculates the first lag after the (1/e) crossing of the autocorrelation function (ACF).", "function": "tsfel.autocorr", "parameters": "", "n_features": 1, @@ -502,6 +502,16 @@ "n_features": 1, "use": "yes" }, + "Lempel-Ziv complexity": { + "complexity": "linear", + "description": "Computes the Lempel-Ziv's (LZ) complexity index, normalized by the signal's length.", + "function": "tsfel.lempel_ziv", + "parameters": { + "threshold": null + }, + "n_features": 1, + "use": "no" + }, "Mean absolute diff": { "complexity": "constant", "description": "Computes mean absolute differences of the signal.", @@ -598,5 +608,59 @@ "emg" ] } + }, + "fractal": { + "Detrended fluctuation analysis": { + "complexity": "nlog", + "description": "Computes the Detrended Fluctuation Analysis (DFA) of the signal.", + "function": "tsfel.dfa", + "parameters": "", + "n_features": 1, + "use": "no" + }, + "Higuchi fractal dimension": { + "complexity": "squared", + "description": "Computes the fractal dimension of a signal using Higuchi's method (HFD).", + "function": "tsfel.higuchi_fractal_dimension", + "parameters": "", + "n_features": 1, + "use": "no" + }, + "Hurst exponent": { + "complexity": "squared", + "description": "Computes the Hurst exponent of the signal through the Rescaled range (R/S) analysis.", + "function": "tsfel.hurst_exponent", + "parameters": "", + "n_features": 1, + "use": "no" + }, + "Maximum fractal length": { + "complexity": "squared", + "description": "Computes the Maximum Fractal Length (MFL) of the signal, which is the average length at the smallest scale, measured from the logarithmic plot determining FD. The Higuchi's method is used.", + "function": "tsfel.maximum_fractal_length", + "parameters": "", + "n_features": 1, + "use": "no" + }, + "Petrosian fractal dimension": { + "complexity": "log", + "description": "Computes the Petrosian Fractal Dimension of a signal.", + "function": "tsfel.petrosian_fractal_dimension", + "parameters": "", + "n_features": 1, + "use": "no" + }, + "Multiscale entropy": { + "complexity": "linear", + "description": "Computes the Multiscale entropy (MSE) of the signal, that performs the entropy analysis over multiple time scales.", + "function": "tsfel.mse", + "parameters": { + "m": 3, + "maxscale": null, + "tolerance": null + }, + "n_features": 1, + "use": "no" + } } -} \ No newline at end of file +} diff --git a/tsfel/feature_extraction/features.py b/tsfel/feature_extraction/features.py index fd5a48a..bd77a17 100644 --- a/tsfel/feature_extraction/features.py +++ b/tsfel/feature_extraction/features.py @@ -1,16 +1,29 @@ +import warnings + import scipy.signal -from tsfel.feature_extraction.features_utils import * +from statsmodels.tsa.stattools import acf +from tsfel.constants import FEATURES_MIN_SIZE +from tsfel.feature_extraction.features_utils import * +warning_flag = False +warning_msg = ( + "The fractal features will not be calculated and will be replaced with 'nan' because the length of the input signal is smaller than the required minimum of " + + str(FEATURES_MIN_SIZE) + + " data points." +) # ############################################# TEMPORAL DOMAIN ##################################################### # @set_domain("domain", "temporal") -@set_domain("tag", "inertial") def autocorr(signal): - """Computes autocorrelation of the signal. + """Calculates the first 1/e crossing of the autocorrelation function (ACF). + The adjusted ACF is calculated using the `statsmodels.tsa.stattools.acf`. + Following the recommendations for long time series (size > 450), we use the + FFT convolution. This feature measures the first time lag at which the + autocorrelation function drops below 1/e (= 0.3679). - Feature computational cost: 1 + Feature computational cost: 2 Parameters ---------- @@ -19,12 +32,22 @@ def autocorr(signal): Returns ------- - float - Cross correlation of 1-dimensional sequence - + int + The first time lag at which the ACF drops below 1/e (= 0.3679). """ - signal = np.array(signal) - return float(np.correlate(signal, signal)) + n = len(signal) + threshold = 0.36787944117144233 # 1 / np.exp(1) + + # For constant input signals, the ACF remains constant, and the expected values for all lags other than + # lag 0 will be zero. We standardize that (1/e) occurs at lag 1. + if np.all(signal == signal[0]): + return 1 + + a = acf(signal, adjusted=True, fft=n > 450, nlags=(int(n / 3)))[1:] + indices = np.where(a < threshold)[0] + first1e_acf = indices[0] + 1 if indices.size > 0 else None + + return first1e_acf @set_domain("domain", "temporal") @@ -44,7 +67,6 @@ def calc_centroid(signal, fs): ------- float Temporal centroid - """ time = compute_time(signal, fs) @@ -77,7 +99,6 @@ def negative_turning(signal): ------- float Number of negative turning points - """ diff_sig = np.diff(signal) array_signal = np.arange(len(diff_sig[:-1])) @@ -102,7 +123,6 @@ def positive_turning(signal): ------- float Number of positive turning points - """ diff_sig = np.diff(signal) @@ -117,19 +137,18 @@ def positive_turning(signal): def mean_abs_diff(signal): """Computes mean absolute differences of the signal. - Feature computational cost: 1 - - Parameters - ---------- - signal : nd-array - Input from which mean absolute deviation is computed + Feature computational cost: 1 - Returns - ------- - float - Mean absolute difference result + Parameters + ---------- + signal : nd-array + Input from which mean absolute deviation is computed - """ + Returns + ------- + float + Mean absolute difference result + """ return np.mean(np.abs(np.diff(signal))) @@ -137,19 +156,18 @@ def mean_abs_diff(signal): def mean_diff(signal): """Computes mean of differences of the signal. - Feature computational cost: 1 - - Parameters - ---------- - signal : nd-array - Input from which mean of differences is computed + Feature computational cost: 1 - Returns - ------- - float - Mean difference result + Parameters + ---------- + signal : nd-array + Input from which mean of differences is computed - """ + Returns + ------- + float + Mean difference result + """ return np.mean(np.diff(signal)) @@ -157,19 +175,18 @@ def mean_diff(signal): def median_abs_diff(signal): """Computes median absolute differences of the signal. - Feature computational cost: 1 - - Parameters - ---------- - signal : nd-array - Input from which median absolute difference is computed + Feature computational cost: 1 - Returns - ------- - float - Median absolute difference result + Parameters + ---------- + signal : nd-array + Input from which median absolute difference is computed - """ + Returns + ------- + float + Median absolute difference result + """ return np.median(np.abs(np.diff(signal))) @@ -177,19 +194,18 @@ def median_abs_diff(signal): def median_diff(signal): """Computes median of differences of the signal. - Feature computational cost: 1 - - Parameters - ---------- - signal : nd-array - Input from which median of differences is computed + Feature computational cost: 1 - Returns - ------- - float - Median difference result + Parameters + ---------- + signal : nd-array + Input from which median of differences is computed - """ + Returns + ------- + float + Median difference result + """ return np.median(np.diff(signal)) @@ -211,29 +227,27 @@ def distance(signal): ------- float Signal distance - """ diff_sig = np.diff(signal).astype(float) - return np.sum([np.sqrt(1 + diff_sig ** 2)]) + return np.sum([np.sqrt(1 + diff_sig**2)]) @set_domain("domain", "temporal") def sum_abs_diff(signal): """Computes sum of absolute differences of the signal. - Feature computational cost: 1 - - Parameters - ---------- - signal : nd-array - Input from which sum absolute difference is computed + Feature computational cost: 1 - Returns - ------- - float - Sum absolute difference result + Parameters + ---------- + signal : nd-array + Input from which sum absolute difference is computed - """ + Returns + ------- + float + Sum absolute difference result + """ return np.sum(np.abs(np.diff(signal))) @@ -256,7 +270,6 @@ def zero_cross(signal): ------- int Number of times that signal value cross the zero axis - """ return len(np.where(np.diff(np.sign(signal)))[0]) @@ -278,7 +291,6 @@ def slope(signal): ------- float Slope - """ t = np.linspace(0, len(signal) - 1, len(signal)) @@ -287,7 +299,8 @@ def slope(signal): @set_domain("domain", "temporal") def auc(signal, fs): - """Computes the area under the curve of the signal computed with trapezoid rule. + """Computes the area under the curve of the signal computed with trapezoid + rule. Feature computational cost: 1 @@ -301,7 +314,6 @@ def auc(signal, fs): ------- float The area under the curve value - """ t = compute_time(signal, fs) @@ -330,13 +342,40 @@ def neighbourhood_peaks(signal, n=10): signal = np.array(signal) subsequence = signal[n:-n] # initial iteration - peaks = ((subsequence > np.roll(signal, 1)[n:-n]) & (subsequence > np.roll(signal, -1)[n:-n])) - for i in range(2, n + 1): - peaks &= (subsequence > np.roll(signal, i)[n:-n]) - peaks &= (subsequence > np.roll(signal, -i)[n:-n]) + peaks = (subsequence > np.roll(signal, 1)[n:-n]) & (subsequence > np.roll(signal, -1)[n:-n]) + for i in np.arange(2, n + 1): + peaks &= subsequence > np.roll(signal, i)[n:-n] + peaks &= subsequence > np.roll(signal, -i)[n:-n] return np.sum(peaks) +@set_domain("domain", "temporal") +def lempel_ziv(signal, threshold=None): + """Computes the Lempel-Ziv's (LZ) complexity index, normalized by the + signal's length. + + Parameters + ---------- + signal : np.ndarray + Input signal. + amp_thres : float, optional + Amplitude Threshold for the binarisation. If None, the mean of the signal is used. + + Returns + ------- + lz_index : float + Lempel-Ziv complexity index + """ + if threshold is None: + threshold = np.mean(signal) + + binary_signal = (signal > threshold).astype(int) + string_binary_signal = "".join(map(str, binary_signal)) + lz_index = calc_lempel_ziv_complexity(string_binary_signal) + + return lz_index + + # ############################################ STATISTICAL DOMAIN #################################################### # @set_domain("domain", "statistical") @set_domain("tag", "audio") @@ -354,7 +393,6 @@ def abs_energy(signal): ------- float Absolute energy - """ return np.sum(np.abs(signal) ** 2) @@ -377,7 +415,6 @@ def average_power(signal, fs): ------- float Average power - """ time = compute_time(signal, fs) @@ -406,7 +443,6 @@ def entropy(signal, prob="standard"): ------- float The normalized entropy value - """ if prob == "standard": @@ -451,7 +487,6 @@ def hist(signal, nbins=10, r=1): ------- nd-array The values of the histogram - """ histsig, bin_edges = np.histogram(signal, bins=nbins, range=[-r, r]) # TODO:subsampling parameter @@ -473,7 +508,6 @@ def interq_range(signal): ------- float Interquartile range result - """ return np.percentile(signal, 75) - np.percentile(signal, 25) @@ -493,7 +527,6 @@ def kurtosis(signal): ------- float Kurtosis result - """ return scipy.stats.kurtosis(signal) @@ -513,7 +546,6 @@ def skewness(signal): ------- int Skewness result - """ return scipy.stats.skew(signal) @@ -533,7 +565,6 @@ def calc_max(signal): ------- float Maximum result - """ return np.max(signal) @@ -553,7 +584,6 @@ def calc_min(signal): ------- float Minimum result - """ return np.min(signal) @@ -574,7 +604,6 @@ def calc_mean(signal): ------- float Mean result - """ return np.mean(signal) @@ -594,7 +623,6 @@ def calc_median(signal): ------- float Median result - """ return np.median(signal) @@ -614,7 +642,6 @@ def mean_abs_deviation(signal): ------- float Mean absolute deviation result - """ return np.mean(np.abs(signal - np.mean(signal, axis=0)), axis=0) @@ -634,7 +661,6 @@ def median_abs_deviation(signal): ------- float Mean absolute deviation result - """ return scipy.stats.median_abs_deviation(signal, scale=1) @@ -657,7 +683,6 @@ def rms(signal): ------- float Root mean square - """ return np.sqrt(np.sum(np.array(signal) ** 2) / len(signal)) @@ -677,7 +702,6 @@ def calc_std(signal): ------- float Standard deviation result - """ return np.std(signal) @@ -697,7 +721,6 @@ def calc_var(signal): ------- float Variance result - """ return np.var(signal) @@ -717,14 +740,14 @@ def pk_pk_distance(signal): ------- float peak to peak distance - """ return np.abs(np.max(signal) - np.min(signal)) @set_domain("domain", "statistical") def ecdf(signal, d=10): - """Computes the values of ECDF (empirical cumulative distribution function) along the time axis. + """Computes the values of ECDF (empirical cumulative distribution function) + along the time axis. Feature computational cost: 1 @@ -749,8 +772,8 @@ def ecdf(signal, d=10): @set_domain("domain", "statistical") def ecdf_slope(signal, p_init=0.5, p_end=0.75): - """Computes the slope of the ECDF between two percentiles. - Possibility to return infinity values. + """Computes the slope of the ECDF between two percentiles. Possibility to + return infinity values. Feature computational cost: 1 @@ -778,7 +801,7 @@ def ecdf_slope(signal, p_init=0.5, p_end=0.75): @set_domain("domain", "statistical") -def ecdf_percentile(signal, percentile=[0.2, 0.8]): +def ecdf_percentile(signal, percentile=None): """Computes the percentile value of the ECDF. Feature computational cost: 1 @@ -795,6 +818,8 @@ def ecdf_percentile(signal, percentile=[0.2, 0.8]): float The input value(s) of the ECDF """ + if percentile is None: + percentile = [0.2, 0.8] signal = np.array(signal) if isinstance(percentile, str): percentile = eval(percentile) @@ -819,8 +844,9 @@ def ecdf_percentile(signal, percentile=[0.2, 0.8]): @set_domain("domain", "statistical") -def ecdf_percentile_count(signal, percentile=[0.2, 0.8]): - """Computes the cumulative sum of samples that are less than the percentile. +def ecdf_percentile_count(signal, percentile=None): + """Computes the cumulative sum of samples that are less than the + percentile. Feature computational cost: 1 @@ -836,6 +862,9 @@ def ecdf_percentile_count(signal, percentile=[0.2, 0.8]): float The cumulative sum of samples """ + if percentile is None: + percentile = [0.2, 0.8] + signal = np.array(signal) if isinstance(percentile, str): percentile = eval(percentile) @@ -861,6 +890,7 @@ def ecdf_percentile_count(signal, percentile=[0.2, 0.8]): # ############################################## SPECTRAL DOMAIN ##################################################### # + @set_domain("domain", "spectral") def spectral_distance(signal, fs): """Computes the signal spectral distance. @@ -881,7 +911,6 @@ def spectral_distance(signal, fs): ------- float spectral distance - """ f, fmag = calc_fft(signal, fs) @@ -913,7 +942,6 @@ def fundamental_frequency(signal, fs): ------- f0: float Predominant frequency of the signal - """ signal = signal - np.mean(signal) f, fmag = calc_fft(signal, fs) @@ -950,7 +978,6 @@ def max_power_spectrum(signal, fs): ------- nd-array Max value of the power spectrum density - """ if np.std(signal) == 0: return float(max(scipy.signal.welch(signal, fs, nperseg=len(signal))[1])) @@ -1038,7 +1065,6 @@ def spectral_centroid(signal, fs): ------- float Centroid - """ f, fmag = calc_fft(signal, fs) if not np.sum(fmag): @@ -1068,7 +1094,6 @@ def spectral_decrease(signal, fs): ------- float Spectral decrease - """ f, fmag = calc_fft(signal, fs) @@ -1109,7 +1134,6 @@ def spectral_kurtosis(signal, fs): ------- float Spectral Kurtosis - """ f, fmag = calc_fft(signal, fs) if not spectral_spread(signal, fs): @@ -1140,7 +1164,6 @@ def spectral_skewness(signal, fs): ------- float Spectral Skewness - """ f, fmag = calc_fft(signal, fs) spect_centr = spectral_centroid(signal, fs) @@ -1173,7 +1196,6 @@ def spectral_spread(signal, fs): ------- float Spectral Spread - """ f, fmag = calc_fft(signal, fs) spect_centroid = spectral_centroid(signal, fs) @@ -1208,7 +1230,6 @@ def spectral_slope(signal, fs): ------- float Spectral Slope - """ f, fmag = calc_fft(signal, fs) sum_fmag = fmag.sum() @@ -1219,11 +1240,11 @@ def spectral_slope(signal, fs): if not ([f]) or (sum_fmag == 0): return 0 else: - if not (len_f * dot_ff - sum_f ** 2): + if not (len_f * dot_ff - sum_f**2): return 0 else: num_ = (1 / sum_fmag) * (len_f * np.sum(f * fmag) - sum_f * sum_fmag) - denom_ = (len_f * dot_ff - sum_f ** 2) + denom_ = len_f * dot_ff - sum_f**2 return num_ / denom_ @@ -1250,7 +1271,6 @@ def spectral_variation(signal, fs): ------- float Spectral Variation - """ f, fmag = calc_fft(signal, fs) @@ -1261,7 +1281,7 @@ def spectral_variation(signal, fs): if not sum2 or not sum3: variation = 1 else: - variation = 1 - (sum1 / ((sum2 ** 0.5) * (sum3 ** 0.5))) + variation = 1 - (sum1 / ((sum2**0.5) * (sum3**0.5))) return variation @@ -1283,7 +1303,6 @@ def spectral_positive_turning(signal, fs): ------- float Number of positive turning points - """ f, fmag = calc_fft(signal, fs) diff_sig = np.diff(fmag) @@ -1316,7 +1335,6 @@ def spectral_roll_off(signal, fs): ------- float Spectral roll-off - """ f, fmag = calc_fft(signal, fs) cum_ff = np.cumsum(fmag) @@ -1345,7 +1363,6 @@ def spectral_roll_on(signal, fs): ------- float Spectral roll-on - """ f, fmag = calc_fft(signal, fs) cum_ff = np.cumsum(fmag) @@ -1375,17 +1392,16 @@ def human_range_energy(signal, fs): ------- float Human range energy ratio - """ f, fmag = calc_fft(signal, fs) - allenergy = np.sum(fmag ** 2) + allenergy = np.sum(fmag**2) if allenergy == 0: # For handling the occurrence of Nan values return 0.0 - hr_energy = np.sum(fmag[np.argmin(np.abs(0.6 - f)):np.argmin(np.abs(2.5 - f))] ** 2) + hr_energy = np.sum(fmag[np.argmin(np.abs(0.6 - f)) : np.argmin(np.abs(2.5 - f))] ** 2) ratio = hr_energy / allenergy @@ -1426,7 +1442,6 @@ def mfcc(signal, fs, pre_emphasis=0.97, nfft=512, nfilt=40, num_ceps=12, cep_lif ------- nd-array MEL cepstral coefficients - """ filter_banks = filterbank(signal, fs, pre_emphasis, nfft, nfilt) @@ -1466,7 +1481,6 @@ def power_bandwidth(signal, fs): ------- float Occupied power in bandwidth - """ # Computing the power spectrum density if np.std(signal) == 0: @@ -1511,7 +1525,6 @@ def fft_mean_coeff(signal, fs, nfreq=256): ------- nd-array The mean value of each spectrogram frequency - """ if nfreq > len(signal) // 2 + 1: nfreq = len(signal) // 2 + 1 @@ -1542,7 +1555,6 @@ def lpcc(signal, n_coeff=12): ------- nd-array Linear prediction cepstral coefficients - """ # 12-20 cepstral coefficients are sufficient for speech recognition lpc_coeffs = lpc(signal, n_coeff) @@ -1575,14 +1587,13 @@ def spectral_entropy(signal, fs): ------- float The normalized spectral entropy value - """ # Removing DC component sig = signal - np.mean(signal) f, fmag = calc_fft(sig, fs) - power = fmag ** 2 + power = fmag**2 if power.sum() == 0: return 0.0 @@ -1623,7 +1634,6 @@ def wavelet_entropy(signal, function=scipy.signal.ricker, widths=np.arange(1, 10 ------- float wavelet entropy - """ if np.sum(signal) == 0: return 0.0 @@ -1658,7 +1668,6 @@ def wavelet_abs_mean(signal, function=scipy.signal.ricker, widths=np.arange(1, 1 ------- tuple CWT absolute mean value - """ return tuple(np.abs(np.mean(wavelet(signal, function, widths), axis=1))) @@ -1684,9 +1693,8 @@ def wavelet_std(signal, function=scipy.signal.ricker, widths=np.arange(1, 10)): ------- tuple CWT std - """ - return tuple((np.std(wavelet(signal, function, widths), axis=1))) + return tuple(np.std(wavelet(signal, function, widths), axis=1)) @set_domain("domain", "spectral") @@ -1710,9 +1718,8 @@ def wavelet_var(signal, function=scipy.signal.ricker, widths=np.arange(1, 10)): ------- tuple CWT variance - """ - return tuple((np.var(wavelet(signal, function, widths), axis=1))) + return tuple(np.var(wavelet(signal, function, widths), axis=1)) @set_domain("domain", "spectral") @@ -1739,9 +1746,230 @@ def wavelet_energy(signal, function=scipy.signal.ricker, widths=np.arange(1, 10) ------- tuple CWT energy - """ cwt = wavelet(signal, function, widths) - energy = np.sqrt(np.sum(cwt ** 2, axis=1) / np.shape(cwt)[1]) + energy = np.sqrt(np.sum(cwt**2, axis=1) / np.shape(cwt)[1]) return tuple(energy) + + +# ############################################## FRACTAL DOMAIN ##################################################### # +@set_domain("domain", "fractal") +def dfa(signal): + """Computes the Detrended Fluctuation Analysis (DFA) of the signal. + + Parameters + ---------- + signal : np.ndarray + Input signal. + + Returns + ------- + alpha_dfa : float + Scaling exponent in DFA. + """ + global warning_flag + + if np.var(signal) == 0 and np.all(signal == signal[0]): + return np.nan + + n = len(signal) + + if n < FEATURES_MIN_SIZE: + if not warning_flag: + warnings.warn(warning_msg, UserWarning) + warning_flag = True + return np.nan + + accumulated_signal = np.cumsum(signal - np.mean(signal)) + windows = set(np.linspace(4, n // 10, n // 2, dtype=int)) + fluct = np.zeros(len(windows)) + + for idx, window in enumerate(windows): + fluct[idx] = np.sqrt(np.mean(calc_rms(accumulated_signal, window) ** 2)) + + i_plateau = find_plateau(np.log(fluct)) + fluct = fluct[0:i_plateau] + windows = list(windows)[0:i_plateau] + + coeffs = np.polyfit(np.log(windows), np.log(fluct), 1) + alpha_dfa = coeffs[0] + + return alpha_dfa + + +@set_domain("domain", "fractal") +def hurst_exponent(signal): + """Computes the Hurst exponent of the signal through the Rescaled range + (R/S) analysis. + + Parameters + ---------- + signal : np.ndarray + Input signal. + + Returns + ------- + h_exp : float + Hurst exponent. + """ + global warning_flag + + if np.var(signal) == 0 and np.all(signal == signal[0]): + return np.nan + + n = len(signal) + + if n < FEATURES_MIN_SIZE: + if not warning_flag: + warnings.warn(warning_msg, UserWarning) + warning_flag = True + return np.nan + + lags = set(np.linspace(4, n // 10, n // 2, dtype=int)) + rs = [compute_rs(signal, lag) for lag in lags] + + n_values = np.array(list(lags))[np.isfinite(rs)] + rs = np.array(rs)[np.isfinite(rs)] + + coeffs = np.polyfit(np.log10(n_values), np.log10(rs), 1) + h_exp = coeffs[0] + + return h_exp + + +@set_domain("domain", "fractal") +def higuchi_fractal_dimension(signal): + """Computes the fractal dimension of a signal using Higuchi's method (HFD). + + Parameters + ---------- + signal : np.ndarray + Input signal. + + Returns + ------- + hfd : float + Fractal dimension. + """ + global warning_flag + + n = len(signal) + + if n < FEATURES_MIN_SIZE: + if not warning_flag: + warnings.warn(warning_msg, UserWarning) + warning_flag = True + return np.nan + + k_values, lk = calc_lengths_higuchi(signal) + + coeffs = np.polyfit(np.log(1 / k_values), np.log(lk), 1) + hfd = coeffs[0] + + return hfd + + +@set_domain("domain", "fractal") +def maximum_fractal_length(signal): + """Computes the Maximum Fractal Length (MFL) of the signal, which is the + average length at the smallest scale, measured from the logarithmic plot + determining FD. The Higuchi's method is used. + + Parameters + ---------- + signal : np.ndarray + Input signal. + + Returns + ------- + mfl : float + Maximum Fractal Length. + """ + global warning_flag + + n = len(signal) + + if n < FEATURES_MIN_SIZE: + if not warning_flag: + warnings.warn(warning_msg, UserWarning) + warning_flag = True + return np.nan + + k_values, lk = calc_lengths_higuchi(signal) + + coeffs = np.polyfit(np.log10(1 / k_values), np.log10(lk), 1) + trendpoly = np.poly1d(coeffs) + mfl_value = trendpoly(0) + + return mfl_value + + +@set_domain("domain", "fractal") +def petrosian_fractal_dimension(signal): + """Computes the Petrosian Fractal Dimension of a signal. + + Parameters + ---------- + signal : np.ndarray + Input signal. + + Returns + ------- + pfd : float + Petrosian Fractal Dimension. + """ + n = len(signal) + diff_signal = np.diff(np.sign(np.diff(signal))) + num_sign_changes = np.sum(diff_signal != 0) + + pfd = np.log10(n) / (np.log10(n) + np.log10(n / (n + 0.4 * num_sign_changes))) + + return pfd + + +@set_domain("domain", "fractal") +def mse(signal, m=3, maxscale=None, tolerance=None): + """Computes the Multiscale entropy (MSE) of the signal, that performs the + entropy analysis over multiple time scales. + + Parameters + ---------- + signal : np.ndarray + Input signal. + m : int + Embedding dimension for the sample entropy, defaults to 3. + maxscale : int + Maximum scale factor, defaults to 1/13 of the length of the input signal. + tolerance : float + Tolerance value, defaults to 0.2 times the standard deviation of the input signal. + + Returns + ------- + mse_area : np.ndarray + Normalized area under the MSE curve. + """ + global warning_flag + + if np.var(signal) == 0 and np.all(signal == signal[0]): + return np.nan + + n = len(signal) + + if n < FEATURES_MIN_SIZE: + if not warning_flag: + warnings.warn(warning_msg, UserWarning) + warning_flag = True + return np.nan + + if tolerance is None: + tolerance = 0.2 * np.std(signal) + + if maxscale is None: + maxscale = n // (10 + 3) + + mse_values = np.array([sample_entropy(coarse_graining(signal, i + 1), m, tolerance) for i in np.arange(maxscale)]) + mse_values_finite = mse_values[np.isfinite(mse_values)] + mse_area = np.trapz(mse_values_finite) / len(mse_values_finite) + + return mse_area diff --git a/tsfel/feature_extraction/features_settings.py b/tsfel/feature_extraction/features_settings.py index 7ba8f83..46336fb 100644 --- a/tsfel/feature_extraction/features_settings.py +++ b/tsfel/feature_extraction/features_settings.py @@ -1,7 +1,9 @@ import json -import tsfel + import numpy as np +import tsfel + def load_json(json_path): """Loads the json file given by filename. @@ -15,7 +17,6 @@ def load_json(json_path): ------- Dict Dictionary - """ return json.load(open(json_path)) @@ -27,7 +28,7 @@ def get_features_by_domain(domain=None, json_path=None): Parameters ---------- domain : string - Available domains: "statistical"; "spectral"; "temporal" + Available domains: "statistical"; "spectral"; "temporal"; "fractal" If domain equals None, then the features settings from all domains are returned. json_path : string Directory of json file. Default: package features.json directory @@ -36,20 +37,23 @@ def get_features_by_domain(domain=None, json_path=None): ------- Dict Dictionary with the features settings - """ if json_path is None: json_path = tsfel.__path__[0] + "/feature_extraction/features.json" - if domain not in ['statistical', 'temporal', 'spectral', None]: + if domain not in ["statistical", "temporal", "spectral", "fractal", None]: raise SystemExit( - 'No valid domain. Choose: statistical, temporal, spectral or None (for all feature settings).') + "No valid domain. Choose: statistical, temporal, spectral, fractal or None (for all feature settings).", + ) dict_features = load_json(json_path) if domain is None: return dict_features else: + if domain == "fractal": + for k in dict_features[domain]: + dict_features[domain][k]["use"] = "yes" return {domain: dict_features[domain]} @@ -68,14 +72,14 @@ def get_features_by_tag(tag=None, json_path=None): ------- Dict Dictionary with the features settings - """ if json_path is None: json_path = tsfel.__path__[0] + "/feature_extraction/features.json" if tag not in ["audio", "inertial", "ecg", "eeg", "emg", None]: raise SystemExit( - "No valid tag. Choose: audio, inertial, ecg, eeg, emg or None.") + "No valid tag. Choose: audio, inertial, ecg, eeg, emg or None.", + ) features_tag = {} dict_features = load_json(json_path) if tag is None: @@ -90,18 +94,21 @@ def get_features_by_tag(tag=None, json_path=None): try: js_tag = dict_features[domain][feat]["tag"] if isinstance(js_tag, list): - if any([tag in js_t for js_t in js_tag]): - features_tag[domain].update({feat: dict_features[domain][feat]}) + if any(tag in js_t for js_t in js_tag): + features_tag[domain].update( + {feat: dict_features[domain][feat]}, + ) elif js_tag == tag: features_tag[domain].update({feat: dict_features[domain][feat]}) except KeyError: continue # To remove empty dicts - return dict([[d, features_tag[d]] for d in list(features_tag.keys()) if bool(features_tag[d])]) + return {d: features_tag[d] for d in list(features_tag.keys()) if bool(features_tag[d])} def get_number_features(dict_features): - """Count the total number of features based on input parameters of each feature + """Count the total number of features based on input parameters of each + feature. Parameters ---------- diff --git a/tsfel/feature_extraction/features_utils.py b/tsfel/feature_extraction/features_utils.py index fcafdf7..cfb21a2 100644 --- a/tsfel/feature_extraction/features_utils.py +++ b/tsfel/feature_extraction/features_utils.py @@ -1,5 +1,6 @@ -import scipy import numpy as np +import scipy +from sklearn.neighbors import KDTree def set_domain(key, value): @@ -24,14 +25,13 @@ def compute_time(signal, fs): ------- time : float list Signal time - """ - return np.arange(0, len(signal))/fs + return np.arange(0, len(signal)) / fs def calc_fft(signal, fs): - """ This functions computes the fft of a signal. + """This functions computes the fft of a signal. Parameters ---------- @@ -46,11 +46,10 @@ def calc_fft(signal, fs): Frequency values (xx axis) fmag: nd-array Amplitude of the frequency values (yy axis) - """ fmag = np.abs(np.fft.rfft(signal)) - f = np.fft.rfftfreq(len(signal), d=1/fs) + f = np.fft.rfftfreq(len(signal), d=1 / fs) return f.copy(), fmag.copy() @@ -81,7 +80,6 @@ def filterbank(signal, fs, pre_emphasis=0.97, nfft=512, nfilt=40): ------- nd-array MEL-spaced filterbank - """ # Signal is already a window from the original signal, so no frame is needed. @@ -91,38 +89,51 @@ def filterbank(signal, fs, pre_emphasis=0.97, nfft=512, nfilt=40): # pre-emphasis filter to amplify the high frequencies - emphasized_signal = np.append(np.array(signal)[0], np.array(signal[1:]) - pre_emphasis * np.array(signal[:-1])) + emphasized_signal = np.append( + np.array(signal)[0], + np.array(signal[1:]) - pre_emphasis * np.array(signal[:-1]), + ) # Fourier transform and Power spectrum - mag_frames = np.absolute(np.fft.rfft(emphasized_signal, nfft)) # Magnitude of the FFT + mag_frames = np.absolute( + np.fft.rfft(emphasized_signal, nfft), + ) # Magnitude of the FFT - pow_frames = ((1.0 / nfft) * (mag_frames ** 2)) # Power Spectrum + pow_frames = (1.0 / nfft) * (mag_frames**2) # Power Spectrum low_freq_mel = 0 - high_freq_mel = (2595 * np.log10(1 + (fs / 2) / 700)) # Convert Hz to Mel - mel_points = np.linspace(low_freq_mel, high_freq_mel, nfilt + 2) # Equally spaced in Mel scale - hz_points = (700 * (10 ** (mel_points / 2595) - 1)) # Convert Mel to Hz + high_freq_mel = 2595 * np.log10(1 + (fs / 2) / 700) # Convert Hz to Mel + mel_points = np.linspace( + low_freq_mel, + high_freq_mel, + nfilt + 2, + ) # Equally spaced in Mel scale + hz_points = 700 * (10 ** (mel_points / 2595) - 1) # Convert Mel to Hz filter_bin = np.floor((nfft + 1) * hz_points / fs) fbank = np.zeros((nfilt, int(np.floor(nfft / 2 + 1)))) - for m in range(1, nfilt + 1): + for m in np.arange(1, nfilt + 1): f_m_minus = int(filter_bin[m - 1]) # left f_m = int(filter_bin[m]) # center f_m_plus = int(filter_bin[m + 1]) # right - for k in range(f_m_minus, f_m): + for k in np.arange(f_m_minus, f_m): fbank[m - 1, k] = (k - filter_bin[m - 1]) / (filter_bin[m] - filter_bin[m - 1]) - for k in range(f_m, f_m_plus): + for k in np.arange(f_m, f_m_plus): fbank[m - 1, k] = (filter_bin[m + 1] - k) / (filter_bin[m + 1] - filter_bin[m]) # Area Normalization # If we don't normalize the noise will increase with frequency because of the filter width. - enorm = 2.0 / (hz_points[2:nfilt + 2] - hz_points[:nfilt]) + enorm = 2.0 / (hz_points[2 : nfilt + 2] - hz_points[:nfilt]) fbank *= enorm[:, np.newaxis] filter_banks = np.dot(pow_frames, fbank.T) - filter_banks = np.where(filter_banks == 0, np.finfo(float).eps, filter_banks) # Numerical Stability + filter_banks = np.where( + filter_banks == 0, + np.finfo(float).eps, + filter_banks, + ) # Numerical Stability filter_banks = 20 * np.log10(filter_banks) # dB return filter_banks @@ -143,12 +154,11 @@ def autocorr_norm(signal): ------- nd-array Autocorrelation result - """ variance = np.var(signal) signal = np.copy(signal - signal.mean()) - r = scipy.signal.correlate(signal, signal)[-len(signal):] + r = scipy.signal.correlate(signal, signal)[-len(signal) :] if (signal == 0).all(): return np.zeros(len(signal)) @@ -175,7 +185,6 @@ def create_symmetric_matrix(acf, order=11): ------- nd-array Symmetric Matrix - """ smatrix = np.empty((order, order)) @@ -204,7 +213,6 @@ def lpc(signal, n_coeff=12): ------- nd-array Linear prediction coefficients - """ if signal.ndim > 1: @@ -216,25 +224,26 @@ def lpc(signal, n_coeff=12): order = n_coeff - 1 # Calculate LPC with Yule-Walker - acf = np.correlate(signal, signal, 'full') + acf = np.correlate(signal, signal, "full") - r = np.zeros(order+1, 'float32') + r = np.zeros(order + 1, "float32") # Assuring that works for all type of input lengths - nx = np.min([order+1, len(signal)]) - r[:nx] = acf[len(signal)-1:len(signal)+order] + nx = np.min([order + 1, len(signal)]) + r[:nx] = acf[len(signal) - 1 : len(signal) + order] smatrix = create_symmetric_matrix(r[:-1], order) if np.sum(smatrix) == 0: - return tuple(np.zeros(order+1)) + return tuple(np.zeros(order + 1)) lpc_coeffs = np.dot(np.linalg.inv(smatrix), -r[1:]) - return tuple(np.concatenate(([1.], lpc_coeffs))) + return tuple(np.concatenate(([1.0], lpc_coeffs))) def create_xx(features): - """Computes the range of features amplitude for the probability density function calculus. + """Computes the range of features amplitude for the probability density + function calculus. Parameters ---------- @@ -245,13 +254,12 @@ def create_xx(features): ------- nd-array range of features amplitude - """ features_ = np.copy(features) if max(features_) < 0: - max_f = - max(features_) + max_f = -max(features_) min_f = min(features_) else: min_f = min(features_) @@ -266,7 +274,8 @@ def create_xx(features): def kde(features): - """Computes the probability density function of the input signal using a Gaussian KDE (Kernel Density Estimate) + """Computes the probability density function of the input signal using a + Gaussian KDE (Kernel Density Estimate) Parameters ---------- @@ -277,7 +286,6 @@ def kde(features): ------- nd-array probability density values - """ features_ = np.copy(features) xx = create_xx(features_) @@ -286,13 +294,14 @@ def kde(features): noise = np.random.randn(len(features_)) * 0.0001 features_ = np.copy(features_ + noise) - kernel = scipy.stats.gaussian_kde(features_, bw_method='silverman') + kernel = scipy.stats.gaussian_kde(features_, bw_method="silverman") return np.array(kernel(xx) / np.sum(kernel(xx))) def gaussian(features): - """Computes the probability density function of the input signal using a Gaussian function + """Computes the probability density function of the input signal using a + Gaussian function. Parameters ---------- @@ -302,7 +311,6 @@ def gaussian(features): ------- nd-array probability density values - """ features_ = np.copy(features) @@ -336,7 +344,6 @@ def wavelet(signal, function=scipy.signal.ricker, widths=np.arange(1, 10)): nd-array The result of the CWT along the time axis matrix with size (len(widths),len(signal)) - """ if isinstance(function, str): @@ -353,15 +360,257 @@ def wavelet(signal, function=scipy.signal.ricker, widths=np.arange(1, 10)): def calc_ecdf(signal): """Computes the ECDF of the signal. - Parameters - ---------- - signal : nd-array - Input from which ECDF is computed - Returns - ------- - nd-array - Sorted signal and computed ECDF. + Parameters + ---------- + signal : nd-array + Input from which ECDF is computed + Returns + ------- + nd-array + Sorted signal and computed ECDF. + """ + return np.sort(signal), np.arange(1, len(signal) + 1) / len(signal) + + +def coarse_graining(signal, scale): + """Applies a coarse-graining process to a time series: for a given scale factor, it splits + the signal into non-overlapping windows and averages the data points. + + Parameters + ---------- + signal : np.ndarray + Input signal. + scale : int + Scale factor, determines the length of the non-overlapping windows. + + Returns + ------- + coarsegrained_signal : np.ndarray + Coarse-grained signal. + """ + + n = len(signal) + windows = n // scale + usable_n = windows * scale + + windowed_signal = np.reshape(signal[0:usable_n], (windows, scale)) + coarsegrained_signal = np.nanmean(windowed_signal, axis=1) + + return coarsegrained_signal + + +def get_templates(signal, m=3): + """Helper function for the sample entropy calculation. Divides a signal + into templates vectors of length m. + + Parameters + ---------- + signal : np.ndarray + Input signal. + m : int + Embedding dimension that defines the length of the template vectors, defaults to 3. + + Returns + ------- + np.ndarray + Array of template vectors. + """ + return np.array([signal[i : i + m] for i in np.arange(len(signal) - m + 1)]) + + +def sample_entropy(signal, m, tolerance): + """Computes the sample entropy of a signal. + + Parameters + ---------- + signal : np.ndarray + Input signal. + m : int + Embedding dimension that defines the length of the template vectors, defaults to 3. + tolerance : float + Tolerance value, defaults to 0.2 times the standard deviation of the input signal. + + Returns + ------- + float + Sample Entropy of a signal. + """ + templates_B = get_templates(signal, m) + templates_B = templates_B[:-1] + kdtree_B = KDTree(templates_B, metric="chebyshev") + count_B = kdtree_B.query_radius(templates_B, tolerance, count_only=True).astype( + np.float64, + ) + proportion_B = np.mean((count_B - 1) / (templates_B.shape[0] - 1)) + + templates_A = get_templates(signal, m + 1) + kdtree_A = KDTree(templates_A, metric="chebyshev") + count_A = kdtree_A.query_radius(templates_A, tolerance, count_only=True).astype( + np.float64, + ) + proportion_A = np.mean((count_A - 1) / (templates_A.shape[0] - 1)) + + if proportion_B > 0 and proportion_A > 0: + return -np.log(proportion_A / proportion_B) + return np.nan + + +def calc_rms(signal, window): + """Windowed Root Mean Square (RMS) with linear detrending. + + Parameters + ---------- + signal: nd-array + Signal + window: int + Length of the window in which RMS will be calculated + + Returns + ------- + rms : nd-array + RMS data in each window with length len(signal)//window + """ + num_windows = len(signal) // window + rms = np.zeros(num_windows) + + for idx in np.arange(num_windows): + start_idx = idx * window + end_idx = start_idx + window + windowed_signal = signal[start_idx:end_idx] + + coeff = np.polyfit(np.arange(window), windowed_signal, 1) + detrended_window = windowed_signal - np.polyval(coeff, np.arange(window)) + rms[idx] = np.sqrt(np.mean(detrended_window**2)) + + return rms + + +def compute_rs(signal, lag): + """Computes the average rescaled range for a window of length lag. + + Parameters + ---------- + signal : np.ndarray + Input signal. + lag : int + Window length. + + Returns + ------- + float + Average R/S. + """ + n = len(signal) + + windowed_signal = np.reshape(signal[: n - n % lag], (-1, lag)) + mean_windows = np.mean(windowed_signal, axis=1) + accumulated_windowed_signal = np.cumsum( + windowed_signal - np.reshape(mean_windows, (-1, 1)), + axis=1, + ) + + r = np.max(accumulated_windowed_signal, axis=1) - np.min( + accumulated_windowed_signal, + axis=1, + ) + s = np.std(windowed_signal, axis=1) + + rs = np.divide(r, s) + + return np.mean(rs) + + +def calc_lengths_higuchi(signal): + """Computes the lengths for different subdivisions, using the Higuchi's + method. + + Parameters + ---------- + signal : np.ndarray + Input signal. + + Returns + ------- + lk : nd-array + Length of curve for different subdivisions + """ + n = len(signal) + k_values = np.arange(1, n // 10) + lk = [] + + for k in k_values: + lmk = 0 + for m in np.arange(1, k + 1): + sum_length = 0 + for i in np.arange(1, (n - m) // k + 1): + sum_length += abs(signal[m + i * k - 1] - signal[m + (i - 1) * k - 1]) + lmk += (sum_length * (n - 1)) / (((n - m) // k) * k**2) + lk.append(lmk / k) + + return k_values, lk + + +def calc_lempel_ziv_complexity(sequence): + """Manual implementation of the Lempel-Ziv complexity. + + It is defined as the number of different substrings encountered as + the stream is viewed from begining to the end. + + Reference: + https://github.com/Naereen/Lempel-Ziv_Complexity/blob/master/src/lempel_ziv_complexity.py + + Parameters + ---------- + sequence : string + Binarised signal, as a string of characters + + Returns + ------- + LZ index + """ + + sub_strings = set() + + ind = 0 + inc = 1 + while True: + if ind + inc > len(sequence): + break + sub_str = sequence[ind : ind + inc] + if sub_str in sub_strings: + inc += 1 + else: + sub_strings.add(sub_str) + ind += inc + inc = 1 + + return len(sub_strings) / len(sequence) + + +def find_plateau(y, threshold=0.1, consecutive_points=5): + """Finds a plateau (if it exists). + + Parameters + ---------- + y : np.ndarray + Array of y-axis values. + threshold : float + Slope threshold to consider as a plateau (default is 0.1). + consecutive_points: int + Number of consecutive points with a small derivative to consider as a plateau (default is 5). + + Returns + ------- + Index of the beggining of the plateau if it is found, length of y otherwise. + """ + + dy = np.diff(y) - """ - return np.sort(signal), np.arange(1, len(signal)+1)/len(signal) + for i in np.arange(len(dy) - consecutive_points + 1): + if np.all(np.abs(dy[i : i + consecutive_points]) < threshold): + plateau_value = np.mean(y[i : i + consecutive_points]) + if plateau_value > np.mean(y): + return i + # No plateau found + return len(y) diff --git a/tsfel/utils/__init__.py b/tsfel/utils/__init__.py index afd3ece..5346aa8 100644 --- a/tsfel/utils/__init__.py +++ b/tsfel/utils/__init__.py @@ -1,4 +1,5 @@ -from tsfel.utils.calculate_complexity import * -from tsfel.utils.signal_processing import * +from tsfel.constants import * from tsfel.utils.add_personal_features import * +from tsfel.utils.calculate_complexity import * from tsfel.utils.progress_bar import * +from tsfel.utils.signal_processing import * diff --git a/tsfel/utils/add_personal_features.py b/tsfel/utils/add_personal_features.py index c2a5a2c..74183b1 100644 --- a/tsfel/utils/add_personal_features.py +++ b/tsfel/utils/add_personal_features.py @@ -21,10 +21,9 @@ def add_feature_json(features_path, json_path): json_path: string Personal .json file directory containing existing features from TSFEL. New customised features will be added to file in this directory. - """ - sys.path.append(features_path[:-len(features_path.split(os.sep)[-1]) - 1]) + sys.path.append(features_path[: -len(features_path.split(os.sep)[-1]) - 1]) exec("import " + features_path.split(os.sep)[-1][:-3]) # Reload module containing the new features @@ -32,13 +31,13 @@ def add_feature_json(features_path, json_path): exec("import " + features_path.split(os.sep)[-1][:-3] + " as pymodule") # Functions from module containing the new features - functions_list = [o for o in getmembers(locals()['pymodule']) if isfunction(o[1])] + functions_list = [o for o in getmembers(locals()["pymodule"]) if isfunction(o[1])] function_names = [fname[0] for fname in functions_list] # Check if @set_domain was declared on features module vset_domain = False - for fname, f in list(locals()['pymodule'].__dict__.items()): + for fname, f in list(locals()["pymodule"].__dict__.items()): if getattr(f, "domain", None) is not None: @@ -71,7 +70,7 @@ def add_feature_json(features_path, json_path): for p in args_name[1:]: if p not in list(defaults.keys()): - if p == 'fs': + if p == "fs": # Assigning a default value for fs if not given defaults[p] = 100 else: @@ -82,11 +81,12 @@ def add_feature_json(features_path, json_path): defaults = "" # Settings of new feature - new_feature = {"description": descrip, - "parameters": defaults, - "function": fname, - "use": use - } + new_feature = { + "description": descrip, + "parameters": defaults, + "function": fname, + "use": use, + } # Check if domain exists try: @@ -96,7 +96,7 @@ def add_feature_json(features_path, json_path): # Insert tag if it is declared if tag is not None: - feat_json[domain][fname]['tag'] = tag + feat_json[domain][fname]["tag"] = tag # Write new feature on json file with open(json_path, "w") as fout: @@ -104,9 +104,10 @@ def add_feature_json(features_path, json_path): # Calculate feature complexity compute_complexity(fname, domain, json_path, features_path=features_path) - print('Feature '+str(fname)+' was added.') + print("Feature " + str(fname) + " was added.") if vset_domain is False: - warnings.warn('No features were added. Please declare @set_domain.', stacklevel=2) - - + warnings.warn( + "No features were added. Please declare @set_domain.", + stacklevel=2, + ) diff --git a/tsfel/utils/calculate_complexity.py b/tsfel/utils/calculate_complexity.py index b06c77a..ce3b7c3 100644 --- a/tsfel/utils/calculate_complexity.py +++ b/tsfel/utils/calculate_complexity.py @@ -1,34 +1,36 @@ -import time import json +import time + import numpy as np from scipy.optimize import curve_fit -from tsfel.feature_extraction.features_settings import load_json + from tsfel.feature_extraction.calc_features import calc_window_features +from tsfel.feature_extraction.features_settings import load_json # curves def n_squared(x, no): - """The model function""" - return no * x ** 2 + """The model function.""" + return no * x**2 def n_nlog(x, no): - """The model function""" + """The model function.""" return no * x * np.log(x) def n_linear(x, no): - """The model function""" + """The model function.""" return no * x def n_log(x, no): - """The model function""" + """The model function.""" return no * np.log(x) def n_constant(x, no): - """The model function""" + """The model function.""" return np.zeros(len(x)) + no @@ -46,7 +48,6 @@ def find_best_curve(t, signal): ------- str Best fit curve name - """ all_chisq = [] @@ -59,7 +60,14 @@ def find_best_curve(t, signal): # Fit the curve for curve in list_curves: start = 1 - popt, pcov = curve_fit(curve, t, signal, sigma=sig, p0=start, absolute_sigma=True) + popt, pcov = curve_fit( + curve, + t, + signal, + sigma=sig, + p0=start, + absolute_sigma=True, + ) # Compute chi square nexp = curve(t, *popt) @@ -73,13 +81,13 @@ def find_best_curve(t, signal): curve_name = str(list_curves[idx_best]) idx1 = curve_name.find("n_") idx2 = curve_name.find("at") - curve_name = curve_name[idx1 + 2:idx2 - 1] + curve_name = curve_name[idx1 + 2 : idx2 - 1] return curve_name def compute_complexity(feature, domain, json_path, **kwargs): - """Computes the feature complexity. + r"""Computes the feature complexity. Parameters ---------- @@ -100,12 +108,11 @@ def compute_complexity(feature, domain, json_path, **kwargs): Feature complexity Writes complexity in json file - """ dictionary = load_json(json_path) - features_path = kwargs.get('features_path', None) + features_path = kwargs.get("features_path", None) # The inputs from this function should be replaced by a dictionary one_feat_dict = {domain: {feature: dictionary[domain][feature]}} @@ -121,7 +128,12 @@ def compute_complexity(feature, domain, json_path, **kwargs): for _ in range(20): start = time.time() - calc_window_features(one_feat_dict, wave[:int(ti)], fs, features_path=features_path) + calc_window_features( + one_feat_dict, + wave[: int(ti)], + fs, + features_path=features_path, + ) end = time.time() s += [end - start] @@ -129,16 +141,16 @@ def compute_complexity(feature, domain, json_path, **kwargs): signal += [np.mean(s)] curve_name = find_best_curve(t, signal) - dictionary[domain][feature]['complexity'] = curve_name + dictionary[domain][feature]["complexity"] = curve_name with open(json_path, "w") as write_file: json.dump(dictionary, write_file, indent=4, sort_keys=True) - if curve_name == 'constant' or curve_name == 'log': + if curve_name == "constant" or curve_name == "log": return 1 - elif curve_name == 'linear': + elif curve_name == "linear": return 2 - elif curve_name == 'nlog' or curve_name == 'squared': + elif curve_name == "nlog" or curve_name == "squared": return 3 else: return 0 diff --git a/tsfel/utils/progress_bar.py b/tsfel/utils/progress_bar.py index cea58de..6632e70 100644 --- a/tsfel/utils/progress_bar.py +++ b/tsfel/utils/progress_bar.py @@ -1,8 +1,17 @@ -from IPython.display import HTML from IPython import get_ipython +from IPython.display import HTML -def progress_bar_terminal(iteration, total, prefix="", suffix="", decimals=0, length=100, fill="█", printend="\r"): +def progress_bar_terminal( + iteration, + total, + prefix="", + suffix="", + decimals=0, + length=100, + fill="█", + printend="\r", +): """Call in a loop to create terminal progress bar. Parameters @@ -28,7 +37,7 @@ def progress_bar_terminal(iteration, total, prefix="", suffix="", decimals=0, le percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) filledlength = int(length * iteration // total) bar = fill * filledlength + "-" * (length - filledlength) - print("\r%s |%s| %s%% %s" % (prefix, bar, percent, suffix), end=printend) + print(f"\r{prefix} |{bar}| {percent}% {suffix}", end=printend) # Print New Line on Complete if iteration == total: print() @@ -47,7 +56,6 @@ def progress_bar_notebook(iteration, total=100): Returns ------- Progress bar for notebooks - """ result = int((iteration / total) * 100) return HTML( @@ -64,8 +72,10 @@ def progress_bar_notebook(iteration, total=100): """.format( - value=iteration, max_value=total, result=result - ) + value=iteration, + max_value=total, + result=result, + ), ) @@ -79,7 +89,6 @@ def display_progress_bar(iteration, total, out): total: int total iterations out: progress bar notebook output - """ if ( @@ -89,5 +98,11 @@ def display_progress_bar(iteration, total, out): ): out.update(progress_bar_notebook(iteration + 1, total)) else: - progress_bar_terminal(iteration + 1, total, prefix="Progress:", suffix="Complete", length=50) - return \ No newline at end of file + progress_bar_terminal( + iteration + 1, + total, + prefix="Progress:", + suffix="Complete", + length=50, + ) + return diff --git a/tsfel/utils/signal_processing.py b/tsfel/utils/signal_processing.py index 1f267c4..ede575b 100644 --- a/tsfel/utils/signal_processing.py +++ b/tsfel/utils/signal_processing.py @@ -4,7 +4,8 @@ def signal_window_splitter(signal, window_size, overlap=0): - """Splits the signal into windows + """Splits the signal into windows. + Parameters ---------- signal : nd-array or pandas DataFrame @@ -20,19 +21,20 @@ def signal_window_splitter(signal, window_size, overlap=0): list of signal windows """ if not isinstance(window_size, int): - raise SystemExit('window_size must be an integer.') + raise SystemExit("window_size must be an integer.") step = int(round(window_size)) if overlap == 0 else int(round(window_size * (1 - overlap))) if step == 0: - raise SystemExit('Invalid overlap. ' - 'Choose a lower overlap value.') + raise SystemExit( + "Invalid overlap. " "Choose a lower overlap value.", + ) if len(signal) % window_size == 0 and overlap == 0: - return [signal[i:i + window_size] for i in range(0, len(signal), step)] + return [signal[i : i + window_size] for i in range(0, len(signal), step)] else: - return [signal[i:i + window_size] for i in range(0, len(signal) - window_size + 1, step)] + return [signal[i : i + window_size] for i in range(0, len(signal) - window_size + 1, step)] def merge_time_series(data, fs_resample, time_unit): - """Time series data interpolation + """Time series data interpolation. Parameters ---------- @@ -47,7 +49,6 @@ def merge_time_series(data, fs_resample, time_unit): ------- DataFrame Interpolated data - """ # time interval for interpolation @@ -58,16 +59,23 @@ def merge_time_series(data, fs_resample, time_unit): # interpolation data_new = np.copy(x_new.reshape(len(x_new), 1)) - header_values = ['time'] + header_values = ["time"] for k, dn in data.items(): header_values += [k + str(i) for i in range(1, np.shape(dn)[1])] - data_new = np.hstack((data_new, np.array([interp1d(dn.iloc[:, 0], dn.iloc[:, ax])(x_new) for ax in range(1, np.shape(dn)[1])]).T)) + data_new = np.hstack( + ( + data_new, + np.array( + [interp1d(dn.iloc[:, 0], dn.iloc[:, ax])(x_new) for ax in range(1, np.shape(dn)[1])], + ).T, + ), + ) return pd.DataFrame(data=data_new[:, 1:], columns=header_values[1:]) def correlated_features(features, threshold=0.95): - """Compute pairwise correlation of features using pearson method + """Compute pairwise correlation of features using pearson method. Parameters ---------- @@ -79,7 +87,6 @@ def correlated_features(features, threshold=0.95): ------- DataFrame correlated features names - """ corr_matrix = features.corr().abs() # Select upper triangle of correlation matrix