diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..9c6d5d8 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,22 @@ +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + builder: html + configuration: docs/source/conf.py + fail_on_warning: false + +# Optionally build your docs in additional formats such as PDF and ePub +formats: all + +# Optionally set the version of Python and requirements required to build your docs +python: + version: 3.8 + install: + - requirements: docs/requirements.txt + - requirements: requirements.txt \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9c052fe --- /dev/null +++ b/.travis.yml @@ -0,0 +1,60 @@ +language: python +python: +- '3.8' +os: +- linux +branches: + only: + - master +install: +- pip install -r requirements.txt -r requirements-dev.txt +script: pytest -v +jobs: + include: + - stage: test + - stage: release + install: skip + script: skip + python: '3.8' + before_deploy: + - export RELEASE_VERSION=$( cat VERSION ) + - git config --local user.name "jmyrberg" + - git config --local user.email "jesse.myrberg@gmail.com" + - git tag $RELEASE_VERSION + deploy: + provider: releases + name: Version $RELEASE_VERSION + tag_name: "$RELEASE_VERSION" + cleanup: false + prerelease: true + token: + secure: + file: + - dist/*.tar.gz + file_glob: true + on: + branch: master + repo: jmyrberg/mknapsack + after_success: + - git push --tags + - stage: deploy + install: skip + script: skip + python: '3.8' + deploy: + provider: pypi + username: jmyrberg + distributions: sdist + cleanup: false + password: + secure: + on: + branch: master + repo: jmyrberg/aakr +stages: +- name: test + if: type IN (pull_request, cron, api) +- name: release + if: type IN (push) +- name: deploy + if: type IN (push) \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1fe5daf --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Jesse Myrberg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..b1fc69e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include VERSION \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b82fa7 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# aakr + +[![Build Status](https://travis-ci.com/jmyrberg/aakr.svg?branch=master)](https://travis-ci.com/jmyrberg/aakr) + +Python implementation of the Auto-Associative Kernel Regression (AAKR). The algorithm is suitable for signal reconstruction, which further be used for e.g. condition monitoring or anomaly detection. + + +## Installation + +`pip install aakr` + + +## Example usage + +Give examples of normal conditions as pandas DataFrame or numpy array. + +```python +from aakr import AAKR + +aakr = AAKR() +aakr.fit(X_obs_nc) +``` + +Predict normal condition for given observations. + +```python +X_nc = aakr.predict(X_obs) +``` + + +## References + +* [MTM algorithm by Martello and Toth](http://people.sc.fsu.edu/~jburkardt/f77_src/knapsack/knapsack.f) (Fortran) +* [A modified Auto Associative Kernel Regression method for robust signal reconstruction in nuclear power plant components](https://www.researchgate.net/publication/292538769_A_modified_Auto_Associative_Kernel_Regression_method_for_robust_signal_reconstruction_in_nuclear_power_plant_components) + +--- +Jesse Myrberg (jesse.myrberg@gmail.com) \ No newline at end of file diff --git a/Untitled.ipynb b/Untitled.ipynb new file mode 100644 index 0000000..7fec515 --- /dev/null +++ b/Untitled.ipynb @@ -0,0 +1,6 @@ +{ + "cells": [], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..c9ef2eb --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.1dev0 \ No newline at end of file diff --git a/aakr/__init__.py b/aakr/__init__.py new file mode 100644 index 0000000..6836c5a --- /dev/null +++ b/aakr/__init__.py @@ -0,0 +1,4 @@ +from ._aakr import AAKR +from ._version import __version__ + +__all__ = ['AAKR', '__version__'] \ No newline at end of file diff --git a/aakr/_aakr.py b/aakr/_aakr.py new file mode 100644 index 0000000..0e10740 --- /dev/null +++ b/aakr/_aakr.py @@ -0,0 +1,84 @@ +"""Module for models.""" + + +import numpy as np + +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.metrics.pairwise import pairwise_distances, pairwise_kernels +from sklearn.utils.validation import check_array, check_is_fitted + + +class AAKR(TransformerMixin, BaseEstimator): + """A template estimator to be used as a reference implementation. + + For more information regarding how to build your own estimator, read more + in the :ref:`User Guide `. + + Parameters + ---------- + demo_param : str, default='demo_param' + A parameter used for demonstation of how to pass and store paramters. + + Examples + -------- + >>> from aakr import AAKR + >>> import numpy as np + >>> X = np.arange(100).reshape(50, 2) + >>> aakr = AAKR() + >>> aakr.fit(X) + AAKR(metric='euclidean', bw=1, n_jobs=None) + """ + def __init__(self, metric='euclidean', bw=1, n_jobs=None): + self.metric = metric + self.bw = bw + self.n_jobs = n_jobs + + def fit(self, X, y=None): + """A reference implementation of a fitting function for a transformer. + + Parameters + ---------- + X : array-like, shape (n_samples, n_features) + Training examples from normal conditions. + y : None + Not needed, exists for compability purposes. + + Returns + ------- + self : object + Returns self. + """ + X = check_array(X) + self.X_ = X + return self + + def predict(self, X, **kwargs): + """ A reference implementation of a prediction for a classifier. + + Parameters + ---------- + X : array-like, shape (n_samples, n_features) + The input samples. + + Returns + ------- + X_nc : ndarray, shape (n_samples, n_features) + Expected values in normal conditions for each sample and feature. + """ + # Validation + check_is_fitted(self, 'X_') + + X = check_array(X) + + if X.shape[1] != self.X_.shape[1]: + raise ValueError('Shape of input is different from what was seen' + 'in `fit`') + + # Kernel regression + D = pairwise_distances(X=self.X_, Y=X, metric=self.metric, + n_jobs=self.n_jobs, **kwargs) + k = 1 / np.sqrt(2 * np.pi * self.bw ** 2) + w = k * np.exp(-D ** 2 / (2 * self.bw ** 2)) + X_nc = w.T.dot(self.X_) / w.sum(0)[:, None] + + return X_nc diff --git a/aakr/_version.py b/aakr/_version.py new file mode 100644 index 0000000..30d874f --- /dev/null +++ b/aakr/_version.py @@ -0,0 +1,2 @@ +with open('./VERSION', 'r') as f: + __version__ = f.read() \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..4da1d3d --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,123 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + + +# -- Init -------------------------------------------------------------------- +# See https://github.com/miyakogi/m2r/issues/51 +import sphinx + +def monkeypatch(cls): + """ decorator to monkey-patch methods """ + def decorator(f): + method = f.__name__ + old_method = getattr(cls, method) + setattr(cls, method, lambda self, *args, + **kwargs: f(old_method, self, *args, **kwargs)) + return decorator + +# workaround until https://github.com/miyakogi/m2r/pull/55 is merged +@monkeypatch(sphinx.registry.SphinxComponentRegistry) +def add_source_parser(_old_add_source_parser, self, *args, **kwargs): + # signature is (parser: Type[Parser], **kwargs), but m2r expects + # the removed (str, parser: Type[Parser], **kwargs). + if isinstance(args[0], str): + args = args[1:] + return _old_add_source_parser(self, *args, **kwargs) + + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# 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 os +import sys +sys.path.insert(0, os.path.abspath('../../')) + + +# -- Project information ----------------------------------------------------- + +project = 'finscraper' +copyright = '2020, Jesse Myrberg' +author = 'Jesse Myrberg' + +# The full version, including alpha/beta/rc tags +with open('../../VERSION', 'r') as f: + release = f.read().strip() + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'm2r', + 'sphinxcontrib.napoleon', + 'sphinx.ext.autodoc', + 'sphinx_rtd_theme', +] + +# Autodoc settings +autodoc_default_options = { + 'inherited-members': True +} + +# Napoleon settings +napoleon_google_docstring = True +napoleon_numpy_docstring = True +napoleon_include_init_with_doc = True +napoleon_include_private_with_doc = False +napoleon_include_special_with_doc = False +napoleon_use_admonition_for_examples = False +napoleon_use_admonition_for_notes = False +napoleon_use_admonition_for_references = False +napoleon_use_ivar = False +napoleon_use_param = True +napoleon_use_rtype = True +napoleon_use_keyword = True +napoleon_custom_sections = None + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +source_suffix = ['.rst', '.md'] + + +# -- 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_options = { + 'canonical_url': '', + #'analytics_id': 'UA-XXXXXXX-1', # Provided by Google in your dashboard + 'logo_only': False, + 'display_version': True, + 'prev_next_buttons_location': 'bottom', + 'style_external_links': False, + #'vcs_pageview_mode': '', + #'style_nav_header_background': 'white', + # Toc options + 'collapse_navigation': True, + 'sticky_navigation': True, + 'navigation_depth': 4, + 'includehidden': True, + 'titles_only': False +} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..e69de29 diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..a6f5f78 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +pytest>=5.4.2 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1b1ec56 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +numpy>=1.19.4 +pandas>=1.1.5 +scikit-learn>=0.23.2 \ No newline at end of file diff --git a/scripts/build-documentation.sh b/scripts/build-documentation.sh new file mode 100644 index 0000000..12cd70c --- /dev/null +++ b/scripts/build-documentation.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +conda activate finscraper && \ +rm -rf docs/build && \ +sphinx-apidoc -f -o docs/source finscraper && \ +rm -rf docs/source/modules.rst && \ +sphinx-build -b html docs/source docs/build +open -a /Applications/Safari.app file://~/Documents/Personal/finscraper/docs/build/index.html diff --git a/scripts/install-local-conda-env.sh b/scripts/install-local-conda-env.sh new file mode 100644 index 0000000..b20a007 --- /dev/null +++ b/scripts/install-local-conda-env.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +ENV_NAME=aakr +PYTHON_VERSION=3.8 + +conda create -y -n $ENV_NAME python=$PYTHON_VERSION && +conda activate $ENV_NAME && +conda install pip flake8 jupyter -y && +pip install -r requirements.txt -r requirements-dev.txt && +echo "Installation successful" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..224a779 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fd76882 --- /dev/null +++ b/setup.py @@ -0,0 +1,54 @@ +import setuptools + + +with open('README.md', 'r') as f: + long_description = f.read() + +with open('VERSION', 'r') as f: + version = f.read().strip() + +setuptools.setup( + name='aakr', + version=version, + license='MIT', + description='Implementation of Auto Associative Kernel Regression (AAKR)', + long_description=long_description, + long_description_content_type='text/markdown', + author='Jesse Myrberg', + author_email='jesse.myrberg@gmail.com', + url='https://github.com/jmyrberg/aakr', + keywords=['aakr', 'auto', 'associative', 'kernel', 'regression', 'anomaly', 'detection'], + install_requires=[ + 'numpy>=1.19.4', + 'pandas>=1.1.5', + 'scikit-learn>=0.23.2' + ], + packages=setuptools.find_packages(), + include_package_data=True, + classifiers=[ + 'Development Status :: 2 - Pre-Alpha', + 'Programming Language :: Python :: 3', + 'License :: OSI Approved :: MIT License', + 'Intended Audience :: Science/Research', + 'Intended Audience :: Developers', + 'License :: OSI Approved', + 'Topic :: Software Development', + 'Topic :: Scientific/Engineering', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: Unix', + 'Operating System :: MacOS' + ], + extras_require={ + 'tests': [ + 'pytest', + 'pytest-cov'], + 'docs': [ + 'sphinx', + 'sphinx-gallery', + 'sphinx_rtd_theme', + 'numpydoc', + 'matplotlib' + ] + } +) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_aakr.py b/tests/test_aakr.py new file mode 100644 index 0000000..94ebbb8 --- /dev/null +++ b/tests/test_aakr.py @@ -0,0 +1,37 @@ +"""Module for testing AAKR.""" + + +import pytest + +from sklearn.datasets import load_linnerud +from sklearn.utils.testing import assert_allclose + +from aakr import AAKR + + +@pytest.fixture +def data(): + return load_linnerud(return_X_y=True) + + +def test_aakr(data): + X = data[0] + aakr = AAKR() + assert aakr.metric == 'euclidean' + assert aakr.bw == 1 + assert aakr.n_jobs is None + + aakr.fit(X) + assert hasattr(aakr, 'X_') + + X_nc = aakr.predict(X[:3]) + assert_allclose(X_nc, X[:3]) + + +def test_aakr_input_shape_mismatch(data): + X = data[0] + aakr = AAKR().fit(X) + assert aakr.X_.shape[1] == X.shape[1] + + with pytest.raises(ValueError, match='Shape of input is different'): + aakr.predict(X[:3, :-1])