├── .gitignore ├── LICENSE ├── README.md ├── imgs ├── randomwalk_smoothing.png ├── sinusoidal_bootstrap.png └── sinusoidal_smoothing.png ├── notebooks ├── Basic Smoothing.ipynb ├── Sinusoidal Smoothing.ipynb ├── Sliding Smoothing.ipynb └── sklearn-wrapper.ipynb ├── requirements.txt ├── setup.py └── tsmoothie ├── __init__.py ├── bootstrap.py ├── regression_basis.py ├── smoother.py ├── utils_class.py └── utils_func.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Created by https://www.gitignore.io/api/python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | # End of https://www.gitignore.io/api/python 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marco Cerliani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tsmoothie 2 | 3 | A python library for time-series smoothing and outlier detection in a vectorized way. 4 | 5 | ## Overview 6 | 7 | tsmoothie computes, in a fast and efficient way, the smoothing of single or multiple time-series. 8 | 9 | The smoothing techniques available are: 10 | 11 | - Exponential Smoothing 12 | - Convolutional Smoothing with various window types (constant, hanning, hamming, bartlett, blackman) 13 | - Spectral Smoothing with Fourier Transform 14 | - Polynomial Smoothing 15 | - Spline Smoothing of various kind (linear, cubic, natural cubic) 16 | - Gaussian Smoothing 17 | - Binner Smoothing 18 | - LOWESS 19 | - Seasonal Decompose Smoothing of various kind (convolution, lowess, natural cubic spline) 20 | - Kalman Smoothing with customizable components (level, trend, seasonality, long seasonality) 21 | 22 | tsmoothie provides the calculation of intervals as result of the smoothing process. This can be useful to identify outliers and anomalies in time-series. 23 | 24 | In relation to the smoothing method used, the interval types available are: 25 | 26 | - sigma intervals 27 | - confidence intervals 28 | - predictions intervals 29 | - kalman intervals 30 | 31 | tsmoothie can carry out a sliding smoothing approach to simulate an online usage. This is possible splitting the time-series into equal sized pieces and smoothing them independently. As always, this functionality is implemented in a vectorized way through the **WindowWrapper** class. 32 | 33 | tsmoothie can operate time-series bootstrap through the **BootstrappingWrapper** class. 34 | 35 | The supported bootstrap algorithms are: 36 | 37 | - none overlapping block bootstrap 38 | - moving block bootstrap 39 | - circular block bootstrap 40 | - stationary bootstrap 41 | 42 | ## Media 43 | 44 | Blog Posts: 45 | 46 | - [Time Series Smoothing for better Clustering](https://towardsdatascience.com/time-series-smoothing-for-better-clustering-121b98f308e8) 47 | - [Time Series Smoothing for better Forecasting](https://towardsdatascience.com/time-series-smoothing-for-better-forecasting-7fbf10428b2) 48 | - [Real-Time Time Series Anomaly Detection](https://towardsdatascience.com/real-time-time-series-anomaly-detection-981cf1e1ca13) 49 | - [Extreme Event Time Series Preprocessing](https://towardsdatascience.com/extreme-event-time-series-preprocessing-90aa59d5630c) 50 | - [Time Series Bootstrap in the age of Deep Learning](https://towardsdatascience.com/time-series-bootstrap-in-the-age-of-deep-learning-b98aa2aa32c4) 51 | 52 | ## Installation 53 | 54 | ```shell 55 | pip install --upgrade tsmoothie 56 | ``` 57 | 58 | The module depends only on NumPy, SciPy and simdkalman. Python 3.6 or above is supported. 59 | 60 | ## Usage: _smoothing_ 61 | 62 | Below a couple of examples of how tsmoothie works. Full examples are available in the [notebooks folder](https://github.com/cerlymarco/tsmoothie/tree/master/notebooks). 63 | 64 | ```python 65 | # import libraries 66 | import numpy as np 67 | import matplotlib.pyplot as plt 68 | from tsmoothie.utils_func import sim_randomwalk 69 | from tsmoothie.smoother import LowessSmoother 70 | 71 | # generate 3 randomwalks of lenght 200 72 | np.random.seed(123) 73 | data = sim_randomwalk(n_series=3, timesteps=200, 74 | process_noise=10, measure_noise=30) 75 | 76 | # operate smoothing 77 | smoother = LowessSmoother(smooth_fraction=0.1, iterations=1) 78 | smoother.smooth(data) 79 | 80 | # generate intervals 81 | low, up = smoother.get_intervals('prediction_interval') 82 | 83 | # plot the smoothed timeseries with intervals 84 | plt.figure(figsize=(18,5)) 85 | 86 | for i in range(3): 87 | 88 | plt.subplot(1,3,i+1) 89 | plt.plot(smoother.smooth_data[i], linewidth=3, color='blue') 90 | plt.plot(smoother.data[i], '.k') 91 | plt.title(f"timeseries {i+1}"); plt.xlabel('time') 92 | 93 | plt.fill_between(range(len(smoother.data[i])), low[i], up[i], alpha=0.3) 94 | ``` 95 | 96 | ![Randomwalk Smoothing](https://raw.githubusercontent.com/cerlymarco/tsmoothie/master/imgs/randomwalk_smoothing.png) 97 | 98 | ```python 99 | # import libraries 100 | import numpy as np 101 | import matplotlib.pyplot as plt 102 | from tsmoothie.utils_func import sim_seasonal_data 103 | from tsmoothie.smoother import DecomposeSmoother 104 | 105 | # generate 3 periodic timeseries of lenght 300 106 | np.random.seed(123) 107 | data = sim_seasonal_data(n_series=3, timesteps=300, 108 | freq=24, measure_noise=30) 109 | 110 | # operate smoothing 111 | smoother = DecomposeSmoother(smooth_type='lowess', periods=24, 112 | smooth_fraction=0.3) 113 | smoother.smooth(data) 114 | 115 | # generate intervals 116 | low, up = smoother.get_intervals('sigma_interval') 117 | 118 | # plot the smoothed timeseries with intervals 119 | plt.figure(figsize=(18,5)) 120 | 121 | for i in range(3): 122 | 123 | plt.subplot(1,3,i+1) 124 | plt.plot(smoother.smooth_data[i], linewidth=3, color='blue') 125 | plt.plot(smoother.data[i], '.k') 126 | plt.title(f"timeseries {i+1}"); plt.xlabel('time') 127 | 128 | plt.fill_between(range(len(smoother.data[i])), low[i], up[i], alpha=0.3) 129 | ``` 130 | 131 | ![Sinusoidal Smoothing](https://raw.githubusercontent.com/cerlymarco/tsmoothie/master/imgs/sinusoidal_smoothing.png) 132 | 133 | **All the available smoothers are fully integrable with sklearn (see [here](https://github.com/cerlymarco/tsmoothie/blob/master/notebooks/sklearn-wrapper.ipynb)).** 134 | 135 | ## Usage: _bootstrap_ 136 | 137 | ```python 138 | # import libraries 139 | import numpy as np 140 | import matplotlib.pyplot as plt 141 | from tsmoothie.utils_func import sim_seasonal_data 142 | from tsmoothie.smoother import ConvolutionSmoother 143 | from tsmoothie.bootstrap import BootstrappingWrapper 144 | 145 | # generate a periodic timeseries of lenght 300 146 | np.random.seed(123) 147 | data = sim_seasonal_data(n_series=1, timesteps=300, 148 | freq=24, measure_noise=15) 149 | 150 | # operate bootstrap 151 | bts = BootstrappingWrapper(ConvolutionSmoother(window_len=8, window_type='ones'), 152 | bootstrap_type='mbb', block_length=24) 153 | bts_samples = bts.sample(data, n_samples=100) 154 | 155 | # plot the bootstrapped timeseries 156 | plt.figure(figsize=(13,5)) 157 | plt.plot(bts_samples.T, alpha=0.3, c='orange') 158 | plt.plot(data[0], c='blue', linewidth=2) 159 | ``` 160 | 161 | ![Sinusoidal Bootstrap](https://raw.githubusercontent.com/cerlymarco/tsmoothie/master/imgs/sinusoidal_bootstrap.png) 162 | 163 | ## References 164 | 165 | - Polynomial, Spline, Gaussian and Binner smoothing are carried out building a regression on custom basis expansions. These implementations are based on the amazing intuitions of Matthew Drury available [here](https://github.com/madrury/basis-expansions/blob/master/examples/comparison-of-smoothing-methods.ipynb) 166 | - Time Series Modelling with Unobserved Components, Matteo M. Pelagatti 167 | - Bootstrap Methods in Time Series Analysis, Fanny Bergström, Stockholms universitet 168 | -------------------------------------------------------------------------------- /imgs/randomwalk_smoothing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cerlymarco/tsmoothie/6eede029acf7e2448e762b8d44e5c8fe2bcda319/imgs/randomwalk_smoothing.png -------------------------------------------------------------------------------- /imgs/sinusoidal_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cerlymarco/tsmoothie/6eede029acf7e2448e762b8d44e5c8fe2bcda319/imgs/sinusoidal_bootstrap.png -------------------------------------------------------------------------------- /imgs/sinusoidal_smoothing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cerlymarco/tsmoothie/6eede029acf7e2448e762b8d44e5c8fe2bcda319/imgs/sinusoidal_smoothing.png -------------------------------------------------------------------------------- /notebooks/sklearn-wrapper.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import matplotlib.pyplot as plt\n", 11 | "from tsmoothie.utils_func import sim_randomwalk\n", 12 | "from tsmoothie.smoother import LowessSmoother\n", 13 | "\n", 14 | "from sklearn.pipeline import make_pipeline\n", 15 | "from sklearn.preprocessing import StandardScaler\n", 16 | "from sklearn.base import TransformerMixin, BaseEstimator" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 2, 22 | "metadata": {}, 23 | "outputs": [ 24 | { 25 | "data": { 26 | "text/plain": [ 27 | "(3, 200)" 28 | ] 29 | }, 30 | "execution_count": 2, 31 | "metadata": {}, 32 | "output_type": "execute_result" 33 | } 34 | ], 35 | "source": [ 36 | "np.random.seed(123)\n", 37 | "data = sim_randomwalk(n_series=3, timesteps=200, \n", 38 | " process_noise=10, measure_noise=30)\n", 39 | "data.shape # (n_series, timesteps)" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 3, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "class LowessSmootherWrap(TransformerMixin, BaseEstimator, LowessSmoother):\n", 49 | "\n", 50 | " def fit(self, X, y=None):\n", 51 | " self._is_fitted = True\n", 52 | " return self\n", 53 | "\n", 54 | " def transform(self, X, y=None):\n", 55 | " self.smooth(X)\n", 56 | " return self.smooth_data\n", 57 | "\n", 58 | " def fit_transform(self, X, y=None):\n", 59 | " return self.fit(X).transform(X)" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 4, 65 | "metadata": {}, 66 | "outputs": [ 67 | { 68 | "data": { 69 | "text/plain": [ 70 | "LowessSmootherWrap(smooth_fraction=0.1)" 71 | ] 72 | }, 73 | "execution_count": 4, 74 | "metadata": {}, 75 | "output_type": "execute_result" 76 | } 77 | ], 78 | "source": [ 79 | "smoother = LowessSmootherWrap(smooth_fraction=0.1, iterations=1)\n", 80 | "smoother.fit(data)" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 5, 86 | "metadata": {}, 87 | "outputs": [ 88 | { 89 | "data": { 90 | "text/plain": [ 91 | "(3, 200)" 92 | ] 93 | }, 94 | "execution_count": 5, 95 | "metadata": {}, 96 | "output_type": "execute_result" 97 | } 98 | ], 99 | "source": [ 100 | "smoothdata = smoother.fit_transform(data)\n", 101 | "smoothdata.shape # (n_series, timesteps)" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 6, 107 | "metadata": {}, 108 | "outputs": [ 109 | { 110 | "data": { 111 | "image/png": "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\n", 112 | "text/plain": [ 113 | "
" 114 | ] 115 | }, 116 | "metadata": { 117 | "needs_background": "light" 118 | }, 119 | "output_type": "display_data" 120 | } 121 | ], 122 | "source": [ 123 | "plt.figure(figsize=(18,5))\n", 124 | "\n", 125 | "for i in range(3):\n", 126 | " \n", 127 | " plt.subplot(1,3,i+1)\n", 128 | " plt.plot(smoothdata[i], linewidth=3, color='blue')\n", 129 | " plt.plot(data[i], '.k')\n", 130 | " plt.title(f\"timeseries {i+1}\"); plt.xlabel('time')" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": 7, 136 | "metadata": {}, 137 | "outputs": [], 138 | "source": [ 139 | "class LowessSmootherWrap(TransformerMixin, BaseEstimator, LowessSmoother):\n", 140 | "\n", 141 | " def fit(self, X, y=None):\n", 142 | " self._is_fitted = True\n", 143 | " return self\n", 144 | "\n", 145 | " def transform(self, X, y=None):\n", 146 | " self.smooth(X.T) # w/ Transpose\n", 147 | " return self.smooth_data.T # w/ Transpose\n", 148 | "\n", 149 | " def fit_transform(self, X, y=None):\n", 150 | " return self.fit(X).transform(X)" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 8, 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "smoother = LowessSmootherWrap(smooth_fraction=0.1, iterations=1)\n", 160 | "pipe = make_pipeline(StandardScaler(), smoother)" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": 9, 166 | "metadata": {}, 167 | "outputs": [ 168 | { 169 | "data": { 170 | "text/plain": [ 171 | "(200, 3)" 172 | ] 173 | }, 174 | "execution_count": 9, 175 | "metadata": {}, 176 | "output_type": "execute_result" 177 | } 178 | ], 179 | "source": [ 180 | "smoothdata = pipe.fit_transform(data.T) # w/ Transpose\n", 181 | "smoothdata.shape # (timesteps, n_series)" 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": 10, 187 | "metadata": {}, 188 | "outputs": [ 189 | { 190 | "data": { 191 | "image/png": "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\n", 192 | "text/plain": [ 193 | "
" 194 | ] 195 | }, 196 | "metadata": { 197 | "needs_background": "light" 198 | }, 199 | "output_type": "display_data" 200 | } 201 | ], 202 | "source": [ 203 | "scaled_data = pipe.named_steps['standardscaler'].transform(data.T)\n", 204 | "\n", 205 | "plt.figure(figsize=(18,5))\n", 206 | "\n", 207 | "for i in range(3):\n", 208 | " \n", 209 | " plt.subplot(1,3,i+1)\n", 210 | " plt.plot(smoothdata[:,i], linewidth=3, color='blue')\n", 211 | " plt.plot(scaled_data[:,i], '.k')\n", 212 | " plt.title(f\"timeseries {i+1}\"); plt.xlabel('time')" 213 | ] 214 | } 215 | ], 216 | "metadata": { 217 | "kernelspec": { 218 | "display_name": "Python 3", 219 | "language": "python", 220 | "name": "python3" 221 | }, 222 | "language_info": { 223 | "codemirror_mode": { 224 | "name": "ipython", 225 | "version": 3 226 | }, 227 | "file_extension": ".py", 228 | "mimetype": "text/x-python", 229 | "name": "python", 230 | "nbconvert_exporter": "python", 231 | "pygments_lexer": "ipython3", 232 | "version": "3.6.6" 233 | } 234 | }, 235 | "nbformat": 4, 236 | "nbformat_minor": 2 237 | } 238 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | simdkalman -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import setup, find_packages 3 | 4 | HERE = pathlib.Path(__file__).parent 5 | 6 | VERSION = '1.0.5' 7 | PACKAGE_NAME = 'tsmoothie' 8 | AUTHOR = 'Marco Cerliani' 9 | AUTHOR_EMAIL = 'cerlymarco@gmail.com' 10 | URL = 'https://github.com/cerlymarco/tsmoothie' 11 | 12 | LICENSE = 'MIT' 13 | DESCRIPTION = 'A python library for timeseries smoothing and outlier detection in a vectorized way.' 14 | LONG_DESCRIPTION = (HERE / "README.md").read_text() 15 | LONG_DESC_TYPE = "text/markdown" 16 | 17 | INSTALL_REQUIRES = [ 18 | 'numpy', 19 | 'scipy', 20 | 'simdkalman' 21 | ] 22 | 23 | setup(name=PACKAGE_NAME, 24 | version=VERSION, 25 | description=DESCRIPTION, 26 | long_description=LONG_DESCRIPTION, 27 | long_description_content_type=LONG_DESC_TYPE, 28 | author=AUTHOR, 29 | license=LICENSE, 30 | author_email=AUTHOR_EMAIL, 31 | url=URL, 32 | install_requires=INSTALL_REQUIRES, 33 | python_requires='>=3', 34 | packages=find_packages() 35 | ) -------------------------------------------------------------------------------- /tsmoothie/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils_class import * 2 | from .utils_func import * 3 | from .regression_basis import * 4 | from .smoother import * 5 | from .bootstrap import * 6 | -------------------------------------------------------------------------------- /tsmoothie/bootstrap.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Define Bootstrapping class. 3 | ''' 4 | 5 | import numpy as np 6 | 7 | from .utils_func import _id_nb_bootstrap, _id_mb_bootstrap, _id_cb_bootstrap, _id_s_bootstrap 8 | 9 | 10 | class BootstrappingWrapper: 11 | """BootstrappingWrapper generates new timeseries samples using 12 | specific algorithms for sequences bootstrapping. 13 | 14 | The BootstrappingWrapper handles single timeseries. Firstly, the 15 | smoothing of the received series is computed. Secondly, the residuals 16 | of the smoothing operation are generated and randomly partitionated 17 | into blocks according to the choosen bootstrapping techniques. Finally, 18 | the residual blocks are sampled in random order, concatenated and then 19 | added to the original smoothing curve in order to obtain a bootstrapped 20 | timeseries. 21 | 22 | The supported bootstrap algorithms are: 23 | - none overlapping block bootstrap ('nbb') 24 | - moving block bootstrap ('mbb') 25 | - circular block bootstrap ('cbb') 26 | - stationary bootstrap ('sb') 27 | 28 | Parameters 29 | ---------- 30 | Smoother : class from tsmoothie.smoother 31 | Every smoother available in tsmoothie.smoother 32 | (except for WindowWrapper). It computes the smoothing on the series 33 | received. 34 | 35 | bootstrap_type : str 36 | The type of algorithm used to compute the bootstrap. 37 | Supported types are: none overlapping block bootstrap ('nbb'), 38 | moving block bootstrap ('mbb'), circular block bootstrap ('cbb'), 39 | stationary bootstrap ('sb'). 40 | 41 | block_length : int 42 | The shape of the blocks used to sample from the residuals of the 43 | smoothing operation and used to bootstrap new samples. 44 | Must be an integer in [3, timesteps). 45 | 46 | Attributes 47 | ---------- 48 | Smoother : class from tsmoothie.smoother 49 | Every smoother available in tsmoothie.smoother 50 | (except for WindowWrapper) that was passed to BootstrappingWrapper. 51 | It as the same properties and attributes of every Smoother. 52 | 53 | Examples 54 | -------- 55 | >>> import numpy as np 56 | >>> from tsmoothie.utils_func import sim_seasonal_data 57 | >>> from tsmoothie.bootstrap import BootstrappingWrapper 58 | >>> from tsmoothie.smoother import * 59 | >>> np.random.seed(33) 60 | >>> data = sim_seasonal_data(n_series=1, timesteps=200, 61 | ... freq=24, measure_noise=10) 62 | >>> bts = BootstrappingWrapper( 63 | ... ConvolutionSmoother(window_len=8, window_type='ones'), 64 | ... bootstrap_type='mbb', block_length=24) 65 | >>> bts_samples = bts.sample(data, n_samples=100) 66 | """ 67 | 68 | def __init__(self, Smoother, bootstrap_type, block_length): 69 | self.Smoother = Smoother 70 | self.bootstrap_type = bootstrap_type 71 | self.block_length = block_length 72 | 73 | def __repr__(self): 74 | return "".format(self.__class__.__name__) 75 | 76 | def __str__(self): 77 | return "".format(self.__class__.__name__) 78 | 79 | def sample(self, data, n_samples=1): 80 | """Bootstrap timeseries. 81 | 82 | Parameters 83 | ---------- 84 | data : array-like of shape (1, timesteps) or also (timesteps,) 85 | Single timeseries to bootstrap. 86 | The data are assumed to be in increasing time order. 87 | 88 | n_samples : int, default=1 89 | How many bootstrapped series to generate. 90 | 91 | Returns 92 | ------- 93 | bootstrap_data : array of shape (n_samples, timesteps) 94 | Bootstrapped samples 95 | """ 96 | 97 | bootstrap_types = ['nbb', 'mbb', 'cbb', 'sb'] 98 | 99 | if self.bootstrap_type not in bootstrap_types: 100 | raise ValueError( 101 | "'{}' is not a supported bootstrap type. " 102 | "Supported types are {}".format( 103 | self.bootstrap_type, bootstrap_types)) 104 | 105 | if not 'tsmoothie.smoother' in str(self.Smoother.__repr__): 106 | raise ValueError("Use a Smoother from tsmoothie.smoother") 107 | 108 | if self.Smoother.__class__.__name__ == 'WindowWrapper': 109 | raise ValueError("WindowWrapper doesn't support bootstrapping") 110 | 111 | if self.block_length < 3: 112 | raise ValueError("block_length must be >= 3") 113 | 114 | if n_samples < 1: 115 | raise ValueError("n_samples must be >= 1") 116 | 117 | data = np.asarray(data) 118 | if np.prod(data.shape) == np.max(data.shape): 119 | data = data.ravel() 120 | nobs = data.shape[0] 121 | 122 | if self.block_length >= nobs: 123 | raise ValueError( 124 | "block_length must be < than the timesteps dimension " 125 | "of the data passed") 126 | 127 | if self.Smoother.__class__.__name__ == 'ExponentialSmoother': 128 | nobs = data.shape[0] - self.Smoother.window_len 129 | if self.block_length >= nobs: 130 | raise ValueError( 131 | "block_length must be < than (timesteps - window_len)") 132 | else: 133 | raise ValueError("The format of data received is not appropriate. " 134 | "BootstrappingWrapper accepts only univariate " 135 | "timeseries") 136 | 137 | self.Smoother.copy = True 138 | self.Smoother.smooth(data) 139 | residuals = self.Smoother.data - self.Smoother.smooth_data 140 | if (np.nan == residuals).any(): 141 | residuals = np.nan_to_num(residuals, nan=0) 142 | 143 | if self.bootstrap_type == 'nbb': 144 | bootstrap_func = _id_nb_bootstrap 145 | elif self.bootstrap_type == 'mbb': 146 | bootstrap_func = _id_mb_bootstrap 147 | elif self.bootstrap_type == 'cbb': 148 | bootstrap_func = _id_cb_bootstrap 149 | else: 150 | bootstrap_func = _id_s_bootstrap 151 | 152 | bootstrap_data = np.empty((n_samples, nobs)) 153 | for i in np.arange(n_samples): 154 | bootstrap_id = bootstrap_func(nobs, self.block_length) 155 | bootstrap_res = residuals[[0], bootstrap_id] 156 | bootstrap_data[i] = self.Smoother.smooth_data + bootstrap_res 157 | 158 | return bootstrap_data -------------------------------------------------------------------------------- /tsmoothie/regression_basis.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Basis functions for regression. 3 | Inspired by: https://github.com/madrury/basis-expansions/blob/master/examples/comparison-of-smoothing-methods.ipynb 4 | ''' 5 | 6 | import numpy as np 7 | 8 | 9 | def polynomial(degree, basis_len): 10 | """Create basis for polynomial regression. 11 | 12 | Returns 13 | ------- 14 | X_base : array 15 | Basis for polynomial regression. 16 | """ 17 | 18 | X = np.arange(basis_len, dtype=np.float64) 19 | X_base = np.repeat([X], degree, axis=0).T 20 | X_base = np.power(X_base, np.arange(1, degree + 1)) 21 | 22 | return X_base 23 | 24 | 25 | def linear_spline(knots, basis_len): 26 | """Create basis for linear spline regression. 27 | 28 | Returns 29 | ------- 30 | X_base : array 31 | Basis for linear spline regression. 32 | """ 33 | 34 | n_knots = len(knots) 35 | X = np.arange(basis_len) 36 | 37 | X_base = np.zeros((basis_len, n_knots + 1)) 38 | X_base[:, 0] = X 39 | 40 | X_base[:, 1:] = X[:, None] - knots[None, :] 41 | X_base[X_base < 0] = 0 42 | 43 | return X_base 44 | 45 | 46 | def cubic_spline(knots, basis_len): 47 | """Create basis for cubic spline regression. 48 | 49 | Returns 50 | ------- 51 | X_base : array 52 | Basis for cubic spline regression. 53 | """ 54 | 55 | n_knots = len(knots) 56 | X = np.arange(basis_len) 57 | 58 | X_base = np.zeros((basis_len, n_knots + 3)) 59 | X_base[:, 0] = X 60 | X_base[:, 1] = X_base[:, 0] * X_base[:, 0] 61 | X_base[:, 2] = X_base[:, 1] * X_base[:, 0] 62 | 63 | X_base[:, 3:] = np.power(X[:, None] - knots[None, :], 3) 64 | X_base[X_base < 0] = 0 65 | 66 | return X_base 67 | 68 | 69 | def natural_cubic_spline(knots, basis_len): 70 | """Create basis for natural cubic spline regression. 71 | 72 | Returns 73 | ------- 74 | X_base : array 75 | Basis for natural cubic spline regression. 76 | """ 77 | 78 | n_knots = len(knots) 79 | X = np.arange(basis_len) 80 | 81 | X_base = np.zeros((basis_len, n_knots - 1)) 82 | X_base[:, 0] = X 83 | 84 | numerator1 = X[:, None] - knots[None, :n_knots - 2] 85 | numerator1[numerator1 < 0] = 0 86 | numerator2 = X[:, None] - knots[None, n_knots - 1] 87 | numerator2[numerator2 < 0] = 0 88 | 89 | numerator = np.power(numerator1, 3) - np.power(numerator2, 3) 90 | denominator = knots[n_knots - 1] - knots[:n_knots - 2] 91 | 92 | numerator1_dd = X[:, None] - knots[None, n_knots - 2] 93 | numerator1_dd[numerator1_dd < 0] = 0 94 | numerator2_dd = X[:, None] - knots[None, n_knots - 1] 95 | numerator2_dd[numerator2_dd < 0] = 0 96 | 97 | numerator_dd = np.power(numerator1_dd, 3) - np.power(numerator2_dd, 3) 98 | denominator_dd = knots[n_knots - 1] - knots[n_knots - 2] 99 | 100 | dd = numerator_dd / denominator_dd 101 | 102 | X_base[:, 1:] = numerator / denominator - dd 103 | 104 | return X_base 105 | 106 | 107 | def gaussian_kernel(knots, sigma, basis_len): 108 | """Create basis for gaussian kernel regression. 109 | 110 | Returns 111 | ------- 112 | X_base : array 113 | Basis for gaussian kernel regression. 114 | """ 115 | 116 | n_knots = len(knots) 117 | X = np.arange(basis_len) / basis_len 118 | 119 | X_base = - np.square(X[:, None] - knots) / (2 * sigma) 120 | X_base = np.exp(X_base) 121 | 122 | return X_base 123 | 124 | 125 | def binner(knots, basis_len): 126 | """Create basis for binner regression. 127 | 128 | Returns 129 | ------- 130 | X_base : array 131 | Basis for binner regression. 132 | """ 133 | 134 | n_knots = len(knots) 135 | X = np.arange(basis_len) 136 | 137 | X_base = np.zeros((basis_len, n_knots + 1)) 138 | X_base[:, 0] = X <= knots[0] 139 | 140 | X_base[:, 1:-1] = np.logical_and( 141 | X[:, None] <= knots[1:][None, :], 142 | X[:, None] > knots[:(n_knots - 1)][None, :]) 143 | 144 | X_base[:, n_knots] = knots[-1] < X 145 | 146 | return X_base 147 | 148 | 149 | def lowess(smooth_fraction, basis_len): 150 | """Create basis for LOWESS. 151 | 152 | Returns 153 | ------- 154 | X_base : array 155 | Basis for LOWESS. 156 | """ 157 | 158 | X = np.arange(basis_len) 159 | 160 | r = int(np.ceil(smooth_fraction * basis_len)) 161 | r = min(r, basis_len - 1) 162 | 163 | X = X[:, None] - X[None, :] 164 | 165 | h = np.sort(np.abs(X), axis=1)[:, r] 166 | 167 | X_base = np.abs(X / h).clip(0.0, 1.0) 168 | X_base = np.power(1 - np.power(X_base, 3), 3) 169 | 170 | return X_base 171 | -------------------------------------------------------------------------------- /tsmoothie/smoother.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Define Smoother classes. 3 | ''' 4 | 5 | import numpy as np 6 | from scipy.signal import fftconvolve 7 | import simdkalman 8 | 9 | from .utils_class import LinearRegression 10 | from .utils_func import (create_windows, sigma_interval, kalman_interval, 11 | confidence_interval, prediction_interval) 12 | from .utils_func import (_check_noise_dict, _check_knots, _check_weights, 13 | _check_data, _check_data_nan, _check_output) 14 | from .regression_basis import (polynomial, linear_spline, cubic_spline, natural_cubic_spline, 15 | gaussian_kernel, binner, lowess) 16 | 17 | 18 | _interval_types = { 19 | 'KalmanSmoother': 20 | ['sigma_interval', 'kalman_interval'], 21 | 'PolynomialSmoother': 22 | ['sigma_interval', 'confidence_interval', 'prediction_interval'], 23 | 'SplineSmoother': 24 | ['sigma_interval', 'confidence_interval', 'prediction_interval'], 25 | 'GaussianSmoother': 26 | ['sigma_interval', 'confidence_interval', 'prediction_interval'], 27 | 'BinnerSmoother': 28 | ['sigma_interval', 'confidence_interval', 'prediction_interval'], 29 | 'LowessSmoother': 30 | ['sigma_interval', 'confidence_interval', 'prediction_interval'], 31 | 'ExponentialSmoother': 32 | ['sigma_interval'], 33 | 'ConvolutionSmoother': 34 | ['sigma_interval'], 35 | 'DecomposeSmoother': 36 | ['sigma_interval'], 37 | 'SpectralSmoother': 38 | ['sigma_interval'] 39 | } 40 | 41 | 42 | class _BaseSmoother: 43 | """Base class to build each Smoother. 44 | 45 | Warning: This class should not be used directly. Use derived classes 46 | instead. 47 | """ 48 | 49 | def __init__(self, copy=True): 50 | self.copy = copy 51 | 52 | def __repr__(self): 53 | return "".format(self.__class__.__name__) 54 | 55 | def __str__(self): 56 | return "".format(self.__class__.__name__) 57 | 58 | def _store_results(self, smooth_data, **objtosave): 59 | """Private method to store results.""" 60 | 61 | self.smooth_data = smooth_data 62 | 63 | if self.copy: 64 | for name, obj in objtosave.items(): 65 | setattr(self, str(name), obj) 66 | 67 | def get_intervals(self, interval_type, confidence=0.05, n_sigma=2): 68 | """Obtain intervals from the smoothed timeseries. 69 | Take care to set copy=True when defining the smoother. 70 | 71 | Supported interval types are: 72 | 1) 'sigma_interval'; 73 | 2) 'confidence_interval'; 74 | 3) 'prediction_interval'; 75 | 4) 'kalman_interval'. 76 | 77 | Each Smooter supports different interval types: 78 | - 'KalmanSmoother' => (1,4); 79 | - 'PolynomialSmoother' => (1,2,3); 80 | - 'SplineSmoother' => (1,2,3); 81 | - 'GaussianSmoother' => (1,2,3); 82 | - 'BinnerSmoother' => (1,2,3); 83 | - 'LowessSmoother' => (1,2,3); 84 | - 'ExponentialSmoother' => (1); 85 | - 'ConvolutionSmoother' => (1); 86 | - 'DecomposeSmoother' => (1); 87 | - 'SpectralSmoother' => (1); 88 | - 'WindowWrapper' => depends on the Smoother received. 89 | 90 | Parameters 91 | ---------- 92 | interval_type : str 93 | Type of interval used to produce the lower and upper bands. 94 | 95 | confidence : float, default=0.05 96 | Effective only for 'confidence_interval', 'prediction_interval' 97 | or 'kalman_interval'. 98 | The significance level for the intervals calculated as 99 | (1-confidence). 100 | 101 | n_sigma : int, default=2 102 | Effective only for 'sigma_interval'. 103 | How many standard deviations, calculated on residuals of the 104 | smoothing operation, are used to obtain the intervals. 105 | 106 | Returns 107 | ------- 108 | low : array of shape (series, timesteps) 109 | Lower bands. 110 | 111 | up : array of shape (series, timesteps) 112 | Upper bands. 113 | """ 114 | 115 | if self.__class__.__name__ == 'WindowWrapper': 116 | 117 | if not hasattr(self.Smoother, 'data'): 118 | raise ValueError( 119 | "Pass some data to the smoother before computing intervals, " 120 | "setting copy=True") 121 | 122 | if interval_type not in _interval_types[self.Smoother.__class__.__name__]: 123 | raise ValueError( 124 | "'{}' is not a supported interval type for this smoother. " 125 | "Supported types are {}".format( 126 | interval_type, _interval_types[self.Smoother.__class__.__name__])) 127 | 128 | if interval_type == 'sigma_interval': 129 | low, up = sigma_interval( 130 | self.Smoother.data, self.Smoother.smooth_data, n_sigma) 131 | 132 | elif interval_type == 'kalman_interval': 133 | low, up = kalman_interval( 134 | self.Smoother.data, self.Smoother.smooth_data, 135 | self.Smoother.cov, confidence) 136 | 137 | elif (interval_type == 'confidence_interval' or 138 | interval_type == 'prediction_interval'): 139 | interval_f = eval(interval_type) 140 | low, up = interval_f( 141 | self.Smoother.data, self.Smoother.smooth_data, 142 | self.Smoother.X, confidence) 143 | 144 | else: 145 | 146 | if not hasattr(self, 'data'): 147 | raise ValueError( 148 | "Pass some data to the smoother before computing intervals, " 149 | "setting copy=True") 150 | 151 | if interval_type not in _interval_types[self.__class__.__name__]: 152 | raise ValueError( 153 | "'{}' is not a supported interval type for this smoother. " 154 | "Supported types are {}".format( 155 | interval_type, _interval_types[self.__class__.__name__])) 156 | 157 | if interval_type == 'sigma_interval': 158 | low, up = sigma_interval(self.data, self.smooth_data, n_sigma) 159 | 160 | elif interval_type == 'kalman_interval': 161 | low, up = kalman_interval( 162 | self.data, self.smooth_data, self.cov, confidence) 163 | 164 | elif (interval_type == 'confidence_interval' or 165 | interval_type == 'prediction_interval'): 166 | interval_f = eval(interval_type) 167 | low, up = interval_f( 168 | self.data, self.smooth_data, self.X, confidence) 169 | 170 | return low, up 171 | 172 | 173 | class ExponentialSmoother(_BaseSmoother): 174 | """ExponentialSmoother operates convolutions of fixed dimensions 175 | on the series using a weighted windows. The weights are the same 176 | for all windows and are computed using an exponential decay. 177 | The most recent observations are most important than the past ones. 178 | This is imposed choosing a parameter (alpha). 179 | No padded is provided in order to not alter the results at the edges. 180 | For this reason, this technique doesn't operate smoothing until 181 | the observations at position window_len. 182 | 183 | The ExponentialSmoother automatically vectorizes, in an efficient way, 184 | the desired smoothing operation on all the series received. 185 | 186 | Parameters 187 | ---------- 188 | window_len : int 189 | Greater than equal to 1. The length of the window used to compute 190 | the exponential smoothing. 191 | 192 | alpha : float 193 | Between 0 and 1. (1-alpha) provides the importance of the past 194 | obsevations when computing the smoothing. 195 | 196 | copy : bool, default=True 197 | If True, the raw data received by the smoother and the smoothed 198 | results can be accessed using 'data' and 'smooth_data' attributes. 199 | This is useful to calculate the intervals. If set to False the 200 | interval calculation is disabled. In order to save memory, set it to 201 | False if you are interested only in the smoothed results. 202 | 203 | Attributes 204 | ---------- 205 | smooth_data : array of shape (series, timesteps-window_len) 206 | Smoothed data derived from the smoothing operation. 207 | It has the same shape of the raw data received without the first 208 | observations until window_len. It is accessible after computhing 209 | smoothing, otherwise None is returned. 210 | 211 | data : array of shape (series, timesteps-window_len) 212 | Raw data received by the smoother. It is accessible with 'copy'=True 213 | and after computhing smoothing, otherwise None is returned. 214 | 215 | Examples 216 | -------- 217 | >>> import numpy as np 218 | >>> from tsmoothie.utils_func import sim_randomwalk 219 | >>> from tsmoothie.smoother import * 220 | >>> np.random.seed(33) 221 | >>> data = sim_randomwalk(n_series=10, timesteps=200, 222 | ... process_noise=10, measure_noise=30) 223 | >>> smoother = ExponentialSmoother(window_len=20, alpha=0.3) 224 | >>> smoother.smooth(data) 225 | >>> low, up = smoother.get_intervals('sigma_interval') 226 | """ 227 | 228 | def __init__(self, window_len, alpha, copy=True): 229 | self.window_len = window_len 230 | self.alpha = alpha 231 | self.copy = copy 232 | 233 | def smooth(self, data): 234 | """Smooth timeseries. 235 | 236 | Parameters 237 | ---------- 238 | data : array-like of shape (series, timesteps) or also (timesteps,) 239 | for single timeseries 240 | Timeseries to smooth. The data are assumed to be in increasing 241 | time order in each timeseries. 242 | 243 | Returns 244 | ------- 245 | self : returns an instance of self 246 | """ 247 | 248 | if self.window_len < 1: 249 | raise ValueError("window_len must be >= 1") 250 | 251 | if self.alpha > 1 or self.alpha < 0: 252 | raise ValueError("alpha must be in the range [0,1]") 253 | 254 | data = _check_data(data) 255 | 256 | if self.window_len >= data.shape[0]: 257 | raise ValueError( 258 | "window_len must be < than timesteps dimension " 259 | "of the data received") 260 | 261 | w = np.power((1 - self.alpha), np.arange(self.window_len)) 262 | 263 | if data.ndim == 2: 264 | w = np.repeat([w / w.sum()], data.shape[1], axis=0).T 265 | else: 266 | w = w / w.sum() 267 | 268 | smooth = fftconvolve(w, data, mode='full', axes=0) 269 | smooth = smooth[self.window_len:data.shape[0]] 270 | data = data[self.window_len:data.shape[0]] 271 | 272 | smooth = _check_output(smooth) 273 | data = _check_output(data) 274 | 275 | self._store_results(smooth_data=smooth, data=data) 276 | 277 | return self 278 | 279 | 280 | class ConvolutionSmoother(_BaseSmoother): 281 | """ConvolutionSmoother operates convolutions of fixed dimensions 282 | on the series using a weighted windows. The weights can assume 283 | different format but they are the same for all the windows and 284 | fixed for the whole procedure. The series are padded, reflecting themself, 285 | with a quantity equal to the window size in both ends to avoid loss of 286 | information. 287 | 288 | The ConvolutionSmoother automatically vectorizes, in an efficient way, 289 | the desired smoothing operation on all the series received. 290 | 291 | Parameters 292 | ---------- 293 | window_len : int 294 | Greater than equal to 1. The length of the window used to compute 295 | the convolutions. 296 | 297 | window_type : str 298 | The type of the window used to compute the convolutions. 299 | Supported types are: 'ones', 'hanning', 'hamming', 'bartlett', 'blackman'. 300 | 301 | copy : bool, default=True 302 | If True, the raw data received by the smoother and the smoothed 303 | results can be accessed using 'data' and 'smooth_data' attributes. 304 | This is useful to calculate the intervals. If set to False the 305 | interval calculation is disabled. In order to save memory, set it to 306 | False if you are interested only in the smoothed results. 307 | 308 | Attributes 309 | ---------- 310 | smooth_data : array of shape (series, timesteps) 311 | Smoothed data derived from the smoothing operation. It is accessible 312 | after computhing smoothing, otherwise None is returned. 313 | 314 | data : array of shape (series, timesteps) 315 | Raw data received by the smoother. It is accessible with 'copy'=True 316 | and after computhing smoothing, otherwise None is returned. 317 | 318 | Examples 319 | -------- 320 | >>> import numpy as np 321 | >>> from tsmoothie.utils_func import sim_randomwalk 322 | >>> from tsmoothie.smoother import * 323 | >>> np.random.seed(33) 324 | >>> data = sim_randomwalk(n_series=10, timesteps=200, 325 | ... process_noise=10, measure_noise=30) 326 | >>> smoother = ConvolutionSmoother(window_len=10, window_type='ones') 327 | >>> smoother.smooth(data) 328 | >>> low, up = smoother.get_intervals('sigma_interval') 329 | """ 330 | 331 | def __init__(self, window_len, window_type, copy=True): 332 | self.window_len = window_len 333 | self.window_type = window_type 334 | self.copy = copy 335 | 336 | def smooth(self, data): 337 | """Smooth timeseries. 338 | 339 | Parameters 340 | ---------- 341 | data : array-like of shape (series, timesteps) or also (timesteps,) 342 | for single timeseries 343 | Timeseries to smooth. The data are assumed to be in increasing 344 | time order in each timeseries. 345 | 346 | Returns 347 | ------- 348 | self : returns an instance of self 349 | """ 350 | 351 | window_types = ['ones', 'hanning', 'hamming', 'bartlett', 'blackman'] 352 | 353 | if self.window_type not in window_types: 354 | raise ValueError( 355 | "'{}' is not a supported window type. " 356 | "Supported types are {}".format(self.window_type, window_types)) 357 | 358 | if self.window_len < 1: 359 | raise ValueError("window_len must be >= 1") 360 | 361 | data = _check_data(data) 362 | 363 | if self.window_len % 2 == 0: 364 | window_len = int(self.window_len + 1) 365 | else: 366 | window_len = self.window_len 367 | 368 | if self.window_type == 'ones': 369 | w = np.ones(window_len) 370 | else: 371 | w = eval('np.' + self.window_type + '(window_len)') 372 | 373 | if data.ndim == 2: 374 | pad_data = np.pad( 375 | data, ((window_len, window_len), (0, 0)), mode='symmetric') 376 | w = np.repeat([w / w.sum()], pad_data.shape[1], axis=0).T 377 | else: 378 | pad_data = np.pad(data, window_len, mode='symmetric') 379 | w = w / w.sum() 380 | 381 | smooth = fftconvolve(w, pad_data, mode='valid', axes=0) 382 | smooth = smooth[(window_len // 2 + 1):-(window_len // 2 + 1)] 383 | 384 | smooth = _check_output(smooth) 385 | data = _check_output(data) 386 | 387 | self._store_results(smooth_data=smooth, data=data) 388 | 389 | return self 390 | 391 | 392 | class SpectralSmoother(_BaseSmoother): 393 | """SpectralSmoother smoothes the timeseries applying a Fourier 394 | Transform. It maintains the most important frequencies, suppressing 395 | the others in the Fourier domain. This results in a smoother curves 396 | when returning to a real domain. 397 | 398 | The SpectralSmoother automatically vectorizes, in an efficient way, 399 | the desired smoothing operation on all the series passed. 400 | 401 | Parameters 402 | ---------- 403 | smooth_fraction : float 404 | Between 0 and 1. The smoothing strength. A lower value of 405 | smooth_fraction will result in a smoother curve. It's the proportion 406 | of frequencies used in the discrete Fourier Transform to smooth 407 | the curve. 408 | 409 | pad_len : int 410 | Greater than equal to 1. The length of the padding used at each 411 | timeseries edge to center the series and obtain better smoothings. 412 | 413 | copy : bool, default=True 414 | If True, the raw data received by the smoother and the smoothed 415 | results can be accessed using 'data' and 'smooth_data' attributes. 416 | This is useful to calculate the intervals. If set to False the 417 | interval calculation is disabled. In order to save memory, set it to 418 | False if you are interested only in the smoothed results. 419 | 420 | Attributes 421 | ---------- 422 | smooth_data : array of shape (series, timesteps) 423 | Smoothed data derived from the smoothing operation. It is accessible 424 | after computhing smoothing, otherwise None is returned. 425 | 426 | data : array of shape (series, timesteps) 427 | Raw data received by the smoother. It is accessible with 'copy'=True 428 | and after computhing smoothing, otherwise None is returned. 429 | 430 | Examples 431 | -------- 432 | >>> import numpy as np 433 | >>> from tsmoothie.utils_func import sim_seasonal_data 434 | >>> from tsmoothie.smoother import * 435 | >>> np.random.seed(33) 436 | >>> data = sim_seasonal_data(n_series=3, timesteps=200, 437 | ... freq=24, measure_noise=15) 438 | >>> smoother = SpectralSmoother(smooth_fraction=0.2, pad_len=20) 439 | >>> smoother.smooth(data) 440 | >>> low, up = smoother.get_intervals('sigma_interval') 441 | """ 442 | 443 | def __init__(self, smooth_fraction, pad_len, copy=True): 444 | self.smooth_fraction = smooth_fraction 445 | self.pad_len = pad_len 446 | self.copy = copy 447 | 448 | def smooth(self, data): 449 | """Smooth timeseries. 450 | 451 | Parameters 452 | ---------- 453 | data : array-like of shape (series, timesteps) or also (timesteps,) 454 | for single timeseries 455 | Timeseries to smooth. The data are assumed to be in increasing 456 | time order in each timeseries. 457 | 458 | Returns 459 | ------- 460 | self : returns an instance of self 461 | """ 462 | 463 | if self.smooth_fraction >= 1 or self.smooth_fraction <= 0: 464 | raise ValueError("smooth_fraction must be in the range (0,1)") 465 | 466 | if self.pad_len < 1: 467 | raise ValueError("pad_len must be >= 1") 468 | 469 | data = _check_data(data) 470 | 471 | if data.ndim == 2: 472 | pad_data = np.pad( 473 | data, ((self.pad_len, self.pad_len), (0, 0)), 474 | mode='symmetric') 475 | else: 476 | pad_data = np.pad(data, self.pad_len, mode='symmetric') 477 | 478 | rfft = np.fft.rfft(pad_data, axis=0) 479 | n_coeff = int(rfft.shape[0] * self.smooth_fraction) 480 | 481 | if rfft.ndim == 2: 482 | rfft[n_coeff:, :] = 0 483 | else: 484 | rfft[n_coeff:] = 0 485 | 486 | if data.shape[0] % 2 > 0: 487 | n = 2 * rfft.shape[0] - 1 488 | else: 489 | n = 2 * (rfft.shape[0] - 1) 490 | 491 | smooth = np.fft.irfft(rfft, n=n, axis=0) 492 | smooth = smooth[self.pad_len:-self.pad_len] 493 | 494 | smooth = _check_output(smooth) 495 | data = _check_output(data) 496 | 497 | self._store_results(smooth_data=smooth, data=data) 498 | 499 | return self 500 | 501 | 502 | class PolynomialSmoother(_BaseSmoother): 503 | """PolynomialSmoother smoothes the timeseries applying a linear 504 | regression on an ad-hoc basis expansion. 505 | The input space, used to build the basis expansion, consists in 506 | a single continuos increasing sequence. 507 | 508 | The PolynomialSmoother automatically vectorizes, in an efficient way, 509 | the desired smoothing operation on all the series received. 510 | 511 | Parameters 512 | ---------- 513 | degree : int 514 | The polynomial order used to build the basis. 515 | 516 | copy : bool, default=True 517 | If True, the raw data received by the smoother and the smoothed 518 | results can be accessed using 'data' and 'smooth_data' attributes. 519 | This is useful to calculate the intervals. If set to False the 520 | interval calculation is disabled. In order to save memory, set it to 521 | False if you are interested only in the smoothed results. 522 | 523 | Attributes 524 | ---------- 525 | smooth_data : array of shape (series, timesteps) 526 | Smoothed data derived from the smoothing operation. It is accessible 527 | after computhing smoothing, otherwise None is returned. 528 | 529 | data : array of shape (series, timesteps) 530 | Raw data received by the smoother. It is accessible with 'copy'=True 531 | and after computhing smoothing, otherwise None is returned. 532 | 533 | Examples 534 | -------- 535 | >>> import numpy as np 536 | >>> from tsmoothie.utils_func import sim_randomwalk 537 | >>> from tsmoothie.smoother import * 538 | >>> np.random.seed(33) 539 | >>> data = sim_randomwalk(n_series=10, timesteps=200, 540 | ... process_noise=10, measure_noise=30) 541 | >>> smoother = PolynomialSmoother(degree=6) 542 | >>> smoother.smooth(data) 543 | >>> low, up = smoother.get_intervals('prediction_interval') 544 | """ 545 | 546 | def __init__(self, degree, copy=True): 547 | self.degree = degree 548 | self.copy = copy 549 | 550 | def smooth(self, data, weights=None): 551 | """Smooth timeseries. 552 | 553 | Parameters 554 | ---------- 555 | data : array-like of shape (series, timesteps) or also (timesteps,) 556 | for single timeseries 557 | Timeseries to smooth. The data are assumed to be in increasing 558 | time order in each timeseries. 559 | 560 | weights : array-like of shape (timesteps,), default=None 561 | Individual weights for each timestep. In case of multidimesional 562 | timeseries, the same weights are used for all the timeseries. 563 | 564 | Returns 565 | ------- 566 | self : returns an instance of self 567 | """ 568 | 569 | if self.degree < 1: 570 | raise ValueError("degree must be > 0") 571 | 572 | data = _check_data(data) 573 | basis_len = data.shape[0] 574 | weights = _check_weights(weights, basis_len) 575 | 576 | X_base = polynomial(self.degree, basis_len) 577 | 578 | lr = LinearRegression(fit_intercept=True) 579 | lr.fit(X_base, data, sample_weight=weights) 580 | 581 | smooth = lr.predict(X_base) 582 | 583 | smooth = _check_output(smooth) 584 | data = _check_output(data) 585 | 586 | self._store_results(smooth_data=smooth, X=X_base, data=data) 587 | 588 | return self 589 | 590 | 591 | class SplineSmoother(_BaseSmoother): 592 | """SplineSmoother smoothes the timeseries applying a linear regression 593 | on an ad-hoc basis expansion. Three types of spline smoothing are 594 | available: 'linear spline', 'cubic spline', 'natural cubic spline'. 595 | In all of the available methods, the input space consists in a single 596 | continuos increasing sequence. 597 | 598 | Two possibilities are available: 599 | - smooth the timeseries in equal intervals, where the number of 600 | intervals is a user defined parameter (n_knots); 601 | - smooth the timeseries in custom length intervals, where the interval 602 | positions are defined by the user as normalize points (knots). 603 | The two methods are exclusive: the usage of n_knots makes not effective 604 | the usage of knots and vice-versa. 605 | 606 | The SplineSmoother automatically vectorizes, in an efficient way, 607 | the desired smoothing operation on all the series received. 608 | 609 | Parameters 610 | ---------- 611 | spline_type : str 612 | Type of spline smoother to operate. Supported types are 'linear_spline', 613 | 'cubic_spline' or 'natural_cubic_spline'. 614 | 615 | n_knots : int 616 | Between 1 and timesteps for 'linear_spline' and 'natural_cubic_spline'. 617 | Between 3 and timesteps for 'natural_cubic_spline'. 618 | Number of equal intervals used to divide the input space and smooth 619 | the timeseries. A lower value of n_knots will result in a smoother curve. 620 | 621 | knots : array-like of shape (n_knots,), default=None 622 | With length of at least 1 for 'linear_spline' and 'natural_cubic_spline'. 623 | With length of at least 3 for 'natural_cubic_spline'. 624 | Normalized points in the range [0,1] that specify in which sections 625 | divide the input space. A lower number of knots will result in a 626 | smoother curve. 627 | 628 | copy : bool, default=True 629 | If True, the raw data received by the smoother and the smoothed 630 | results can be accessed using 'data' and 'smooth_data' attributes. 631 | This is useful to calculate the intervals. If set to False the 632 | interval calculation is disabled. In order to save memory, set it to 633 | False if you are interested only in the smoothed results. 634 | 635 | Attributes 636 | ---------- 637 | smooth_data : array of shape (series, timesteps) 638 | Smoothed data derived from the smoothing operation. It is accessible 639 | after computhing smoothing, otherwise None is returned. 640 | 641 | data : array of shape (series, timesteps) 642 | Raw data received by the smoother. It is accessible with 'copy'=True 643 | and after computhing smoothing, otherwise None is returned. 644 | 645 | Examples 646 | -------- 647 | >>> import numpy as np 648 | >>> from tsmoothie.utils_func import sim_randomwalk 649 | >>> from tsmoothie.smoother import * 650 | >>> np.random.seed(33) 651 | >>> data = sim_randomwalk(n_series=10, timesteps=200, 652 | ... process_noise=10, measure_noise=30) 653 | >>> smoother = SplineSmoother(n_knots=6, spline_type='natural_cubic_spline') 654 | >>> smoother.smooth(data) 655 | >>> low, up = smoother.get_intervals('prediction_interval') 656 | """ 657 | 658 | def __init__(self, spline_type, n_knots, knots=None, copy=True): 659 | self.spline_type = spline_type 660 | self.n_knots = n_knots 661 | self.knots = knots 662 | self.copy = copy 663 | 664 | def smooth(self, data, weights=None): 665 | """Smooth timeseries. 666 | 667 | Parameters 668 | ---------- 669 | data : array-like of shape (series, timesteps) or also (timesteps,) 670 | for single timeseries 671 | Timeseries to smooth. The data are assumed to be in increasing 672 | time order in each timeseries. 673 | 674 | weights : array-like of shape (timesteps,), default=None 675 | Individual weights for each timestep. In case of multidimesional 676 | timeseries, the same weights are used for all the timeseries. 677 | 678 | Returns 679 | ------- 680 | self : returns an instance of self 681 | """ 682 | 683 | spline_types = {'linear_spline': 1, 'cubic_spline': 1, 684 | 'natural_cubic_spline': 3} 685 | 686 | if self.spline_type not in spline_types: 687 | raise ValueError("'{}' is not a supported spline type. " 688 | "Supported types are {}".format( 689 | self.spline_type, list(spline_types.keys()))) 690 | 691 | data = _check_data(data) 692 | basis_len = data.shape[0] 693 | weights = _check_weights(weights, basis_len) 694 | 695 | if self.knots is not None: 696 | knots = _check_knots( 697 | self.knots, spline_types[self.spline_type])[1:-1] * basis_len 698 | 699 | else: 700 | if self.n_knots < spline_types[self.spline_type]: 701 | raise ValueError( 702 | "'{}' requires n_knots >= {}".format( 703 | self.spline_type, spline_types[self.spline_type])) 704 | 705 | if self.n_knots > basis_len: 706 | raise ValueError( 707 | "n_knots must be <= than timesteps dimension " 708 | "of the data received") 709 | 710 | knots = np.linspace(0, basis_len, self.n_knots + 2)[1:-1] 711 | 712 | f = eval(self.spline_type) 713 | X_base = f(knots, basis_len) 714 | 715 | lr = LinearRegression(fit_intercept=True) 716 | lr.fit(X_base, data, sample_weight=weights) 717 | 718 | smooth = lr.predict(X_base) 719 | 720 | smooth = _check_output(smooth) 721 | data = _check_output(data) 722 | 723 | self._store_results(smooth_data=smooth, X=X_base, data=data) 724 | 725 | return self 726 | 727 | 728 | class GaussianSmoother(_BaseSmoother): 729 | """GaussianSmoother smoothes the timeseries applying a linear 730 | regression on an ad-hoc basis expansion. The features created with 731 | this method are obtained applying a gaussian kernel centered to specified 732 | points of the input space. 733 | In timeseries domain, the input space consists in a single continuos 734 | increasing sequence. 735 | 736 | Two possibilities are available: 737 | - smooth the timeseries in equal intervals, where the number of 738 | intervals is a user defined parameter (n_knots); 739 | - smooth the timeseries in custom length intervals, where the interval 740 | positions are defined by the user as normalize points (knots). 741 | The two methods are exclusive: the usage of n_knots makes not effective 742 | the usage of knots and vice-versa. 743 | 744 | The GaussianSmoother automatically vectorizes, in an efficient way, 745 | the desired smoothing operation on all the series received. 746 | 747 | Parameters 748 | ---------- 749 | sigma : float 750 | sigma in the gaussian kernel. 751 | 752 | n_knots : int 753 | Between 1 and timesteps. Number of equal intervals used to divide 754 | the input space and smooth the timeseries. A lower value of n_knots 755 | will result in a smoother curve. 756 | 757 | knots : array-like of shape (n_knots,), default=None 758 | With length of at least 1. Normalized points in the range [0,1] that 759 | specify in which sections divide the input space. A lower number of 760 | knots will result in a smoother curve. 761 | 762 | copy : bool, default=True 763 | If True, the raw data received by the smoother and the smoothed 764 | results can be accessed using 'data' and 'smooth_data' attributes. 765 | This is useful to calculate the intervals. If set to False the 766 | interval calculation is disabled. In order to save memory, set it to 767 | False if you are interested only in the smoothed results. 768 | 769 | Attributes 770 | ---------- 771 | smooth_data : array of shape (series, timesteps) 772 | Smoothed data derived from the smoothing operation. It is accessible 773 | after computhing smoothing, otherwise None is returned. 774 | 775 | data : array of shape (series, timesteps) 776 | Raw data received by the smoother. It is accessible with 'copy'=True 777 | and after computhing smoothing, otherwise None is returned. 778 | 779 | Examples 780 | -------- 781 | >>> import numpy as np 782 | >>> from tsmoothie.utils_func import sim_randomwalk 783 | >>> from tsmoothie.smoother import * 784 | >>> np.random.seed(33) 785 | >>> data = sim_randomwalk(n_series=10, timesteps=200, 786 | ... process_noise=10, measure_noise=30) 787 | >>> smoother = GaussianSmoother(n_knots=6, sigma=0.1) 788 | >>> smoother.smooth(data) 789 | >>> low, up = smoother.get_intervals('prediction_interval') 790 | """ 791 | 792 | def __init__(self, sigma, n_knots, knots=None, copy=True): 793 | self.sigma = sigma 794 | self.n_knots = n_knots 795 | self.knots = knots 796 | self.copy = copy 797 | 798 | def smooth(self, data, weights=None): 799 | """Smooth timeseries. 800 | 801 | Parameters 802 | ---------- 803 | data : array-like of shape (series, timesteps) or also (timesteps,) 804 | for single timeseries 805 | Timeseries to smooth. The data are assumed to be in increasing 806 | time order in each timeseries. 807 | 808 | weights : array-like of shape (timesteps,), default=None 809 | Individual weights for each timestep. In case of multidimesional 810 | timeseries, the same weights are used for all the timeseries. 811 | 812 | Returns 813 | ------- 814 | self : returns an instance of self 815 | """ 816 | 817 | if self.sigma <= 0: 818 | raise ValueError("sigma must be > 0") 819 | 820 | data = _check_data(data) 821 | basis_len = data.shape[0] 822 | weights = _check_weights(weights, basis_len) 823 | 824 | if self.knots is not None: 825 | knots = _check_knots(self.knots, 1)[1:-1] 826 | 827 | else: 828 | if self.n_knots < 1: 829 | raise ValueError("n_knots must be > 0") 830 | 831 | if self.n_knots > basis_len: 832 | raise ValueError( 833 | "n_knots must be <= than timesteps dimension " 834 | "of the data received") 835 | 836 | knots = np.linspace(0, 1, self.n_knots + 2)[1:-1] 837 | 838 | X_base = gaussian_kernel(knots, self.sigma, basis_len) 839 | 840 | lr = LinearRegression(fit_intercept=True) 841 | lr.fit(X_base, data, sample_weight=weights) 842 | 843 | smooth = lr.predict(X_base) 844 | 845 | smooth = _check_output(smooth) 846 | data = _check_output(data) 847 | 848 | self._store_results(smooth_data=smooth, X=X_base, data=data) 849 | 850 | return self 851 | 852 | 853 | class BinnerSmoother(_BaseSmoother): 854 | """BinnerSmoother smoothes the timeseries applying a linear regression 855 | on an ad-hoc basis expansion. The features created with this method 856 | are obtained binning the input space into intervals. 857 | An indicator feature is created for each bin, indicating where 858 | a given observation falls into. 859 | In timeseries domain, the input space consists in a single continuos 860 | increasing sequence. 861 | 862 | Two possibilities are available: 863 | - smooth the timeseries in equal intervals, where the number of 864 | intervals is a user defined parameter (n_knots); 865 | - smooth the timeseries in custom length intervals, where the interval 866 | positions are defined by the user as normalize points (knots). 867 | The two methods are exclusive: the usage of n_knots makes not effective 868 | the usage of knots and vice-versa. 869 | 870 | The BinnerSmoother automatically vectorizes, in an efficient way, 871 | the desired smoothing operation on all the series received. 872 | 873 | Parameters 874 | ---------- 875 | n_knots : int 876 | Between 1 and timesteps. Number of equal intervals used to divide 877 | the input space and smooth the timeseries. A lower value of n_knots 878 | will result in a smoother curve. 879 | 880 | knots : array-like of shape (n_knots,), default=None 881 | With length of at least 1. Normalized points in the range [0,1] that 882 | specify in which sections divide the input space. A lower number of 883 | knots will result in a smoother curve. 884 | 885 | copy : bool, default=True 886 | If True, the raw data received by the smoother and the smoothed 887 | results can be accessed using 'data' and 'smooth_data' attributes. 888 | This is useful to calculate the intervals. If set to False the 889 | interval calculation is disabled. In order to save memory, set it to 890 | False if you are interested only in the smoothed results. 891 | 892 | Attributes 893 | ---------- 894 | smooth_data : array of shape (series, timesteps) 895 | Smoothed data derived from the smoothing operation. It is accessible 896 | after computhing smoothing, otherwise None is returned. 897 | 898 | data : array of shape (series, timesteps) 899 | Raw data received by the smoother. It is accessible with 'copy'=True 900 | and after computhing smoothing, otherwise None is returned. 901 | 902 | Examples 903 | -------- 904 | >>> import numpy as np 905 | >>> from tsmoothie.utils_func import sim_randomwalk 906 | >>> from tsmoothie.smoother import * 907 | >>> np.random.seed(33) 908 | >>> data = sim_randomwalk(n_series=10, timesteps=200, 909 | ... process_noise=10, measure_noise=30) 910 | >>> smoother = BinnerSmoother(n_knots=6) 911 | >>> smoother.smooth(data) 912 | >>> low, up = smoother.get_intervals('prediction_interval') 913 | """ 914 | 915 | def __init__(self, n_knots, knots=None, copy=True): 916 | self.n_knots = n_knots 917 | self.knots = knots 918 | self.copy = copy 919 | 920 | def smooth(self, data, weights=None): 921 | """Smooth timeseries. 922 | 923 | Parameters 924 | ---------- 925 | data : array-like of shape (series, timesteps) or also (timesteps,) 926 | for single timeseries 927 | Timeseries to smooth. The data are assumed to be in increasing 928 | time order in each timeseries. 929 | 930 | weights : array-like of shape (timesteps,), default=None 931 | Individual weights for each timestep. In case of multidimesional 932 | timeseries, the same weights are used for all the timeseries. 933 | 934 | Returns 935 | ------- 936 | self : returns an instance of self 937 | """ 938 | 939 | data = _check_data(data) 940 | basis_len = data.shape[0] 941 | weights = _check_weights(weights, basis_len) 942 | 943 | if self.knots is not None: 944 | knots = _check_knots(self.knots, 1)[1:-1] * basis_len 945 | 946 | else: 947 | if self.n_knots < 1: 948 | raise ValueError("n_knots must be > 0") 949 | 950 | if self.n_knots > basis_len: 951 | raise ValueError( 952 | "n_knots must be <= than timesteps dimension " 953 | "of the data received") 954 | 955 | knots = np.linspace(0, basis_len, self.n_knots + 2)[1:-1] 956 | 957 | X_base = binner(knots, basis_len) 958 | 959 | lr = LinearRegression(fit_intercept=True) 960 | lr.fit(X_base, data, sample_weight=weights) 961 | 962 | smooth = lr.predict(X_base) 963 | 964 | smooth = _check_output(smooth) 965 | data = _check_output(data) 966 | 967 | self._store_results(smooth_data=smooth, X=X_base, data=data) 968 | 969 | return self 970 | 971 | 972 | class LowessSmoother(_BaseSmoother): 973 | """LowessSmoother uses LOWESS (locally-weighted scatterplot smoothing) 974 | to smooth the timeseries. This smoothing technique is a non-parametric 975 | regression method that essentially fit a unique linear regression 976 | for every data point by including nearby data points to estimate 977 | the slope and intercept. The presented method is robust because it 978 | performs residual-based reweightings simply specifing the number of 979 | iterations to operate. 980 | 981 | The LowessSmoother automatically vectorizes, in an efficient way, 982 | the desired smoothing operation on all the series passed. 983 | 984 | Parameters 985 | ---------- 986 | smooth_fraction : float 987 | Between 0 and 1. The smoothing span. A larger value of smooth_fraction 988 | will result in a smoother curve. 989 | 990 | iterations : int 991 | Between 1 and 6. The number of residual-based reweightings to perform. 992 | 993 | batch_size : int, default=None 994 | How many timeseries are smoothed simultaneously. This parameter is 995 | important because LowessSmoother is a memory greedy process. Setting 996 | it low, with big timeseries, helps to avoid MemoryError. By default 997 | None means that all the timeseries are smoothed simultaneously. 998 | 999 | copy : bool, default=True 1000 | If True, the raw data received by the smoother and the smoothed 1001 | results can be accessed using 'data' and 'smooth_data' attributes. 1002 | This is useful to calculate the intervals. If set to False the 1003 | interval calculation is disabled. In order to save memory, set it to 1004 | False if you are interested only in the smoothed results. 1005 | 1006 | Attributes 1007 | ---------- 1008 | smooth_data : array of shape (series, timesteps) 1009 | Smoothed data derived from the smoothing operation. It is accessible 1010 | after computhing smoothing, otherwise None is returned. 1011 | 1012 | data : array of shape (series, timesteps) 1013 | Raw data received by the smoother. It is accessible with 'copy'=True 1014 | and after computhing smoothing, otherwise None is returned. 1015 | 1016 | Examples 1017 | -------- 1018 | >>> import numpy as np 1019 | >>> from tsmoothie.utils_func import sim_randomwalk 1020 | >>> from tsmoothie.smoother import * 1021 | >>> np.random.seed(33) 1022 | >>> data = sim_randomwalk(n_series=10, timesteps=200, 1023 | ... process_noise=10, measure_noise=30) 1024 | >>> smoother = LowessSmoother(smooth_fraction=0.3, iterations=1) 1025 | >>> smoother.smooth(data) 1026 | >>> low, up = smoother.get_intervals('prediction_interval') 1027 | """ 1028 | 1029 | def __init__(self, smooth_fraction, iterations=1, 1030 | batch_size=None, copy=True): 1031 | self.smooth_fraction = smooth_fraction 1032 | self.iterations = iterations 1033 | self.batch_size = batch_size 1034 | self.copy = copy 1035 | 1036 | def smooth(self, data): 1037 | """Smooth timeseries. 1038 | 1039 | Parameters 1040 | ---------- 1041 | data : array-like of shape (series, timesteps) or also (timesteps,) 1042 | for single timeseries 1043 | Timeseries to smooth. The data are assumed to be in increasing 1044 | time order in each timeseries. 1045 | 1046 | Returns 1047 | ------- 1048 | self : returns an instance of self 1049 | """ 1050 | 1051 | if self.smooth_fraction >= 1 or self.smooth_fraction <= 0: 1052 | raise ValueError("smooth_fraction must be in the range (0,1)") 1053 | 1054 | if self.iterations <= 0 or self.iterations > 6: 1055 | raise ValueError("iterations must be in the range (0,6]") 1056 | 1057 | data = _check_data(data) 1058 | if data.ndim == 1: 1059 | data = data[:, None] 1060 | timesteps, n_timeseries = data.shape 1061 | 1062 | if self.batch_size is not None: 1063 | if self.batch_size <= 0 or self.batch_size > n_timeseries: 1064 | raise ValueError("batch_size must be in the range (0,series]") 1065 | 1066 | X = np.arange(timesteps) / (timesteps - 1) 1067 | w_init = lowess(self.smooth_fraction, timesteps) 1068 | 1069 | delta = np.ones_like(data) 1070 | 1071 | if self.batch_size is None: 1072 | batches = [np.arange(0, n_timeseries)] 1073 | else: 1074 | batches = np.split( 1075 | np.arange(0, n_timeseries), 1076 | np.arange(self.batch_size, n_timeseries + self.batch_size - 1, 1077 | self.batch_size)) 1078 | smooth = np.empty_like(data) 1079 | 1080 | for iteration in range(self.iterations): 1081 | 1082 | for B in batches: 1083 | 1084 | try: 1085 | w = delta[:, None, B] * w_init[..., None] 1086 | # (timesteps, timesteps, n_series) 1087 | wy = w * data[:, None, B] 1088 | # (timesteps, timesteps, n_series) 1089 | wyx = wy * X[:, None, None] 1090 | # (timesteps, timesteps, n_series) 1091 | wx = w * X[:, None, None] 1092 | # (timesteps, timesteps, n_series) 1093 | wxx = wx * X[:, None, None] 1094 | # (timesteps, timesteps, n_series) 1095 | 1096 | b = np.array([wy.sum(axis=0), wyx.sum(axis=0)]).T 1097 | # (n_series, timesteps, 2) 1098 | A = np.array([[w.sum(axis=0), wx.sum(axis=0)], 1099 | [wx.sum(axis=0), wxx.sum(axis=0)]]) 1100 | # (2, 2, timesteps, n_series) 1101 | 1102 | XtX = (A.transpose(1, 0, 2, 3)[None, ...] * A[:, None, ...]).sum(2) 1103 | # (2, 2, timesteps, n_series) 1104 | XtX = np.linalg.pinv(XtX.transpose(3, 2, 0, 1)) 1105 | # (n_series, timesteps, 2, 2) 1106 | XtXXt = (XtX[..., None] * A.transpose(3, 2, 1, 0)[..., None, :]).sum(2) 1107 | # (n_series, timesteps, 2, 2) 1108 | betas = np.squeeze(XtXXt @ b[..., None], -1) 1109 | # (n_series, timesteps, 2) 1110 | 1111 | smooth[:, B] = (betas[..., 0] + betas[..., 1] * X).T 1112 | # (timesteps, n_series) 1113 | 1114 | residuals = data[:, B] - smooth[:, B] 1115 | s = np.median(np.abs(residuals), axis=0).clip(1e-5) 1116 | delta[:, B] = (residuals / (6.0 * s)).clip(-1, 1) 1117 | delta[:, B] = np.square(1 - np.square(delta[:, B])) 1118 | 1119 | except MemoryError: 1120 | raise StopIteration( 1121 | "Reduce the batch_size provided in order to not encounter " 1122 | "memory errors. Provided batch_size is {}. By default batch_size " 1123 | "is set to None. This means that all the timeseries " 1124 | "passed are smoothed simultaneously".format(self.batch_size)) 1125 | 1126 | smooth = _check_output(smooth) 1127 | data = _check_output(data) 1128 | 1129 | self._store_results(smooth_data=smooth, X=X * (timesteps - 1), data=data) 1130 | 1131 | return self 1132 | 1133 | 1134 | class DecomposeSmoother(_BaseSmoother): 1135 | """DecomposeSmoother smoothes the timeseries applying a standard 1136 | seasonal decomposition. The seasonal decomposition can be carried out 1137 | using different smoothing techniques available in tsmoothie. 1138 | 1139 | The DecomposeSmoother automatically vectorizes, in an efficient way, 1140 | the desired smoothing operation on all the series received. 1141 | 1142 | Parameters 1143 | ---------- 1144 | smooth_type : str 1145 | The type of smoothing used to compute the seasonal decomposition. 1146 | Supported types are: 'convolution', 'lowess', 'natural_cubic_spline'. 1147 | 1148 | periods : list 1149 | List of seasonal periods of the timeseries. Multiple periods are 1150 | allowed. Each period must be an integer reater than 0. 1151 | 1152 | method : str, default='additive' 1153 | Type of seasonal component. 1154 | Supported types are: 'additive', 'multiplicative'. 1155 | 1156 | **smoothargs : Smoothing arguments 1157 | The same accepted by the smoother referring to smooth_type. 1158 | 1159 | copy : bool, default=True 1160 | If True, the raw data received by the smoother and the smoothed 1161 | results can be accessed using 'data' and 'smooth_data' attributes. 1162 | This is useful to calculate the intervals. If set to False the 1163 | interval calculation is disabled. In order to save memory, set it to 1164 | False if you are interested only in the smoothed results. 1165 | 1166 | Attributes 1167 | ---------- 1168 | smooth_data : array of shape (series, timesteps) 1169 | Smoothed data derived from the smoothing operation. It is accessible 1170 | after computhing smoothing, otherwise None is returned. 1171 | 1172 | data : array of shape (series, timesteps) 1173 | Raw data received by the smoother. It is accessible with 'copy'=True 1174 | and after computhing smoothing, otherwise None is returned. 1175 | 1176 | Examples 1177 | -------- 1178 | >>> import numpy as np 1179 | >>> from tsmoothie.utils_func import sim_seasonal_data 1180 | >>> from tsmoothie.smoother import * 1181 | >>> np.random.seed(33) 1182 | >>> data = sim_seasonal_data(n_series=3, timesteps=300, 1183 | ... freq=24, measure_noise=30) 1184 | >>> smoother = DecomposeSmoother(smooth_type='convolution', periods=24, 1185 | ... window_len=30, window_type='ones') 1186 | >>> smoother.smooth(data) 1187 | >>> low, up = smoother.get_intervals('sigma_interval') 1188 | """ 1189 | 1190 | def __init__(self, smooth_type, periods, method='additive', copy=True, 1191 | **smoothargs): 1192 | self.smooth_type = smooth_type 1193 | self.periods = periods 1194 | self.method = method 1195 | self.copy = copy 1196 | self.smoothargs = smoothargs 1197 | 1198 | def smooth(self, data): 1199 | """Smooth timeseries. 1200 | 1201 | Parameters 1202 | ---------- 1203 | data : array-like of shape (series, timesteps) or also (timesteps,) 1204 | for single timeseries 1205 | Timeseries to smooth. The data are assumed to be in increasing 1206 | time order in each timeseries. 1207 | 1208 | Returns 1209 | ------- 1210 | self : returns an instance of self 1211 | """ 1212 | 1213 | smooth_types = ['convolution', 'lowess', 'natural_cubic_spline'] 1214 | methods = ['additive', 'multiplicative'] 1215 | 1216 | if self.smooth_type not in smooth_types: 1217 | raise ValueError( 1218 | "'{}' is not a supported smooth type. " 1219 | "Supported types are {}".format(self.smooth_type, smooth_types)) 1220 | 1221 | if self.method not in methods: 1222 | raise ValueError("'{}' is not a supported method type. " 1223 | "Supported types are {}".format( 1224 | self.method, methods)) 1225 | 1226 | if not isinstance(self.periods, list): 1227 | periods = [self.periods] 1228 | else: 1229 | periods = self.periods 1230 | 1231 | for p in periods: 1232 | if p <= 0 or not isinstance(p, int): 1233 | raise ValueError("periods must a list containing int > 0") 1234 | 1235 | if self.smooth_type == 'convolution': 1236 | smoother = ConvolutionSmoother(copy=True, **self.smoothargs) 1237 | smoother.smooth(data) 1238 | elif self.smooth_type == 'lowess': 1239 | smoother = LowessSmoother(copy=True, **self.smoothargs) 1240 | smoother.smooth(data) 1241 | elif self.smooth_type == 'natural_cubic_spline': 1242 | smoother = SplineSmoother(copy=True, spline_type=self.smooth_type, 1243 | **self.smoothargs) 1244 | smoother.smooth(data) 1245 | 1246 | if self.method == 'additive': 1247 | detrended = smoother.data - smoother.smooth_data 1248 | else: 1249 | detrended = smoother.data / smoother.smooth_data 1250 | 1251 | period_averages = [] 1252 | for p in periods: 1253 | period_averages.append( 1254 | np.array([np.mean(detrended[:, i::p], axis=1) 1255 | for i in range(p)]).T) 1256 | 1257 | if self.method == 'additive': 1258 | period_averages = [p_a - np.mean(p_a, axis=1, keepdims=True) 1259 | for p_a in period_averages] 1260 | else: 1261 | period_averages = [p_a / np.mean(p_a, axis=1, keepdims=True) 1262 | for p_a in period_averages] 1263 | 1264 | nobs = smoother.data.shape[1] 1265 | seasonal = [np.tile(p_a, (1, nobs // periods[i] + 1))[:, :nobs] 1266 | for i, p_a in enumerate(period_averages)] 1267 | 1268 | data = smoother.data 1269 | smooth = smoother.smooth_data 1270 | for season in seasonal: 1271 | if self.method == 'additive': 1272 | smooth += season 1273 | else: 1274 | smooth *= season 1275 | 1276 | self._store_results(smooth_data=smooth, data=data) 1277 | 1278 | return self 1279 | 1280 | 1281 | class KalmanSmoother(_BaseSmoother): 1282 | """KalmanSmoother smoothes the timeseries using the Kalman smoothing 1283 | technique. The Kalman smoother provided here can be represented 1284 | in the state space form. For this reason, it's necessary to provide 1285 | an adequate matrix representation of all the components. It's possible 1286 | to define a Kalman smoother that takes into account the following 1287 | structure present in our series: 'level', 'trend', 'seasonality' and 1288 | 'long seasonality'. All these features have an addictive behaviour. 1289 | 1290 | The KalmanSmoother automatically vectorizes, in an efficient way, 1291 | the desired smoothing operation on all the series received. 1292 | 1293 | Parameters 1294 | ---------- 1295 | component : str 1296 | Specify the patterns and the dinamycs present in our series. 1297 | The possibilities are: 'level', 'level_trend', 1298 | 'level_season', 'level_trend_season', 'level_longseason', 1299 | 'level_trend_longseason', 'level_season_longseason', 1300 | 'level_trend_season_longseason'. Each single component is 1301 | delimited by the '_' notation. 1302 | 1303 | component_noise : dict 1304 | Specify in a dictionary the noise (in float term) of each single 1305 | component provided in the 'component' argument. If a noise of a 1306 | component, not provided in the 'component' argument, is provided, 1307 | it's automatically ignored. 1308 | 1309 | observation_noise : float, default=1.0 1310 | The noise level generated by the data measurement. 1311 | 1312 | n_seasons : int, default=None 1313 | The period of the seasonal component. If a seasonal component 1314 | is not provided in the 'component' argument, it's automatically 1315 | ignored. 1316 | 1317 | n_longseasons : int, default=None 1318 | The period of the long seasonal component. If a long seasonal 1319 | component is not provided in the 'component' argument, it's 1320 | automatically ignored. 1321 | 1322 | copy : bool, default=True 1323 | If True, the raw data received by the smoother and the smoothed 1324 | results can be accessed using 'data' and 'smooth_data' attributes. 1325 | This is useful to calculate the intervals. If set to False the 1326 | interval calculation is disabled. In order to save memory, set it to 1327 | False if you are interested only in the smoothed results. 1328 | 1329 | Attributes 1330 | ---------- 1331 | smooth_data : array of shape (series, timesteps) 1332 | Smoothed data derived from the smoothing operation. It is accessible 1333 | after computhing smoothing, otherwise None is returned. 1334 | 1335 | data : array of shape (series, timesteps) 1336 | Raw data received by the smoother. It is accessible with 'copy'=True 1337 | and after computhing smoothing, otherwise None is returned. 1338 | 1339 | Examples 1340 | -------- 1341 | >>> import numpy as np 1342 | >>> from tsmoothie.utils_func import sim_randomwalk 1343 | >>> from tsmoothie.smoother import * 1344 | >>> np.random.seed(33) 1345 | >>> data = sim_randomwalk(n_series=10, timesteps=200, 1346 | ... process_noise=10, measure_noise=30) 1347 | >>> smoother = KalmanSmoother(component='level_trend', 1348 | ... component_noise={'level':0.1, 'trend':0.1}) 1349 | >>> smoother.smooth(data) 1350 | >>> low, up = smoother.get_intervals('kalman_interval') 1351 | """ 1352 | 1353 | def __init__(self, component, component_noise, observation_noise=1., 1354 | n_seasons=None, n_longseasons=None, copy=True): 1355 | self.component = component 1356 | self.component_noise = component_noise 1357 | self.observation_noise = observation_noise 1358 | self.n_seasons = n_seasons 1359 | self.n_longseasons = n_longseasons 1360 | self.copy = copy 1361 | 1362 | def smooth(self, data): 1363 | """Smooth timeseries. 1364 | 1365 | Parameters 1366 | ---------- 1367 | data : array-like of shape (series, timesteps) or also (timesteps,) 1368 | for single timeseries 1369 | Timeseries to smooth. The data are assumed to be in increasing 1370 | time order in each timeseries. 1371 | 1372 | Returns 1373 | ------- 1374 | self : returns an instance of self 1375 | """ 1376 | 1377 | components = ['level', 'level_trend', 1378 | 'level_season', 'level_trend_season', 1379 | 'level_longseason', 'level_trend_longseason', 1380 | 'level_season_longseason', 'level_trend_season_longseason'] 1381 | 1382 | if self.component not in components: 1383 | raise ValueError( 1384 | "'{}' is unsupported. Pass one of {}".format( 1385 | self.component, components)) 1386 | 1387 | _noise = _check_noise_dict(self.component_noise, self.component) 1388 | data = _check_data_nan(data) 1389 | 1390 | if self.component == 'level': 1391 | 1392 | A = [[1]] # level 1393 | Q = [[_noise['level']]] 1394 | H = [[1]] 1395 | 1396 | elif self.component == 'level_trend': 1397 | 1398 | A = [[1, 1], # level 1399 | [0, 1]] # trend 1400 | Q = np.diag([_noise['level'], _noise['trend']]) 1401 | H = [[1, 0]] 1402 | 1403 | elif self.component == 'level_season': 1404 | 1405 | if self.n_seasons is None: 1406 | raise ValueError( 1407 | "you should specify n_seasons when using a seasonal component") 1408 | 1409 | A = np.zeros((self.n_seasons, self.n_seasons)) 1410 | A[0, 0] = 1 # level 1411 | A[1, 1:] = [-1.0] * (self.n_seasons - 1) # season 1412 | A[2:, 1:-1] = np.eye(self.n_seasons - 2) # season 1413 | Q = np.diag([_noise['level'], 1414 | _noise['season']] + [0] * (self.n_seasons - 2)) 1415 | H = [[1, 1] + [0] * (self.n_seasons - 2)] 1416 | 1417 | elif self.component == 'level_trend_season': 1418 | 1419 | if self.n_seasons is None: 1420 | raise ValueError( 1421 | "you should specify n_seasons when using a seasonal component") 1422 | 1423 | A = np.zeros((self.n_seasons + 1, self.n_seasons + 1)) 1424 | A[:2, :2] = [[1, 1], # level 1425 | [0, 1]] # trend 1426 | A[2, 2:] = [-1.0] * (self.n_seasons - 1) # season 1427 | A[3:, 2:-1] = np.eye(self.n_seasons - 2) # season 1428 | Q = np.diag([_noise['level'], _noise['trend'], 1429 | _noise['season']] + [0] * (self.n_seasons - 2)) 1430 | H = [[1, 0, 1] + [0] * (self.n_seasons - 2)] 1431 | 1432 | elif self.component == 'level_longseason': 1433 | 1434 | if self.n_longseasons is None: 1435 | raise ValueError( 1436 | "you should specify n_longseasons when using a " 1437 | "long seasonal component") 1438 | 1439 | period_cycle_sin = np.sin(2 * np.pi / self.n_longseasons) 1440 | period_cycle_cos = np.cos(2 * np.pi / self.n_longseasons) 1441 | 1442 | A = [[1, 0, 0], # level 1443 | [0, period_cycle_cos, period_cycle_sin], # long season 1444 | [0, -period_cycle_sin, period_cycle_cos]] # long season 1445 | Q = np.diag([_noise['level'], 1446 | _noise['longseason'], _noise['longseason']]) 1447 | H = [[1, 1, 0]] 1448 | 1449 | elif self.component == 'level_trend_longseason': 1450 | 1451 | if self.n_longseasons is None: 1452 | raise ValueError( 1453 | "you should specify n_longseasons when using a " 1454 | "long seasonal component") 1455 | 1456 | period_cycle_sin = np.sin(2 * np.pi / self.n_longseasons) 1457 | period_cycle_cos = np.cos(2 * np.pi / self.n_longseasons) 1458 | 1459 | A = [[1, 1, 0, 0], # level 1460 | [0, 1, 0, 0], # trend 1461 | [0, 0, period_cycle_cos, period_cycle_sin], # long season 1462 | [0, 0, -period_cycle_sin, period_cycle_cos]] # long season 1463 | Q = np.diag([_noise['level'], _noise['trend'], 1464 | _noise['longseason'], _noise['longseason']]), 1465 | H = [[1, 0, 1, 0]] 1466 | 1467 | elif self.component == 'level_season_longseason': 1468 | 1469 | if self.n_seasons is None: 1470 | raise ValueError( 1471 | "you should specify n_seasons when using a seasonal component") 1472 | 1473 | if self.n_longseasons is None: 1474 | raise ValueError( 1475 | "you should specify n_longseasons when using a " 1476 | "long seasonal component") 1477 | 1478 | period_cycle_sin = np.sin(2 * np.pi / self.n_longseasons) 1479 | period_cycle_cos = np.cos(2 * np.pi / self.n_longseasons) 1480 | 1481 | A = np.zeros((self.n_seasons + 2, self.n_seasons + 2)) 1482 | A[0, 0] = 1 # level 1483 | A[1:3, 1:3] = [[period_cycle_cos, period_cycle_sin], # long season 1484 | [-period_cycle_sin, period_cycle_cos]] # long season 1485 | A[3, 3:] = [-1.0] * (self.n_seasons - 1) # season 1486 | A[4:, 3:-1] = np.eye(self.n_seasons - 2) # season 1487 | Q = np.diag([_noise['level'], 1488 | _noise['longseason'], _noise['longseason'], 1489 | _noise['season']] + [0] * (self.n_seasons - 2)) 1490 | H = [[1, 1, 0, 1] + [0] * (self.n_seasons - 2)] 1491 | 1492 | elif self.component == 'level_trend_season_longseason': 1493 | 1494 | if self.n_seasons is None: 1495 | raise ValueError( 1496 | "you should specify n_seasons when using a seasonal component") 1497 | 1498 | if self.n_longseasons is None: 1499 | raise ValueError( 1500 | "you should specify n_longseasons when using a " 1501 | "long seasonal component") 1502 | 1503 | period_cycle_sin = np.sin(2 * np.pi / self.n_longseasons) 1504 | period_cycle_cos = np.cos(2 * np.pi / self.n_longseasons) 1505 | 1506 | A = np.zeros((self.n_seasons + 2 + 1, self.n_seasons + 2 + 1)) 1507 | A[:2, :2] = [[1, 1], # level 1508 | [0, 1]] # trend 1509 | A[2:4, 2:4] = [[period_cycle_cos, period_cycle_sin], # long season 1510 | [-period_cycle_sin, period_cycle_cos]] # long season 1511 | A[4, 4:] = [-1.0] * (self.n_seasons - 1) # season 1512 | A[5:, 4:-1] = np.eye(self.n_seasons - 2) # season 1513 | Q = np.diag([_noise['level'], _noise['trend'], 1514 | _noise['longseason'], _noise['longseason'], 1515 | _noise['season']] + [0] * (self.n_seasons - 2)) 1516 | H = [[1, 0, 1, 0, 1] + [0] * (self.n_seasons - 2)] 1517 | 1518 | kf = simdkalman.KalmanFilter( 1519 | state_transition=A, 1520 | process_noise=Q, 1521 | observation_model=H, 1522 | observation_noise=self.observation_noise) 1523 | 1524 | smoothed = kf.smooth(data) 1525 | smoothed_obs = smoothed.observations.mean 1526 | cov = np.sqrt(smoothed.observations.cov) 1527 | 1528 | smoothed_obs = _check_output(smoothed_obs, transpose=False) 1529 | cov = _check_output(cov, transpose=False) 1530 | data = _check_output(data, transpose=False) 1531 | 1532 | self._store_results(smooth_data=smoothed_obs, cov=cov, data=data) 1533 | 1534 | return self 1535 | 1536 | 1537 | class WindowWrapper(_BaseSmoother): 1538 | """WindowWrapper smooths timeseries partitioning them into equal 1539 | sliding segments and treating them as new standalone timeseries. 1540 | The WindowWrapper handles single timeseries. After the sliding windows 1541 | are generated, the WindowWrapper smooths them using the smoother it 1542 | receives as input parameter. In this way, the smoothing can be carried 1543 | out like a multiple smoothing task. 1544 | 1545 | The WindowWrapper automatically vectorizes, in an efficient way, 1546 | the sliding window creation and the desired smoothing operation. 1547 | 1548 | Parameters 1549 | ---------- 1550 | Smoother : class from tsmoothie.smoother 1551 | Every smoother available in tsmoothie.smoother. 1552 | It computes the smoothing on the series received. 1553 | 1554 | window_shape : int 1555 | Grather than 1. The shape of the sliding windows used to divide 1556 | the series to smooth. 1557 | 1558 | step : int, default=1 1559 | The step used to generate the sliding windows. 1560 | 1561 | Attributes 1562 | ---------- 1563 | Smoother : class from tsmoothie.smoother 1564 | Every smoother available in tsmoothie.smoother that was passed to 1565 | WindowWrapper. 1566 | It as the same properties and attributes of every Smoother. 1567 | 1568 | Examples 1569 | -------- 1570 | >>> import numpy as np 1571 | >>> from tsmoothie.utils_func import sim_randomwalk 1572 | >>> from tsmoothie.smoother import * 1573 | >>> np.random.seed(33) 1574 | >>> data = sim_randomwalk(n_series=1, timesteps=200, 1575 | ... process_noise=10, measure_noise=30) 1576 | >>> smoother = WindowWrapper( 1577 | ... LowessSmoother(smooth_fraction=0.3, iterations=1), 1578 | ... window_shape=30) 1579 | >>> smoother.smooth(data) 1580 | >>> low, up = smoother.get_intervals('prediction_interval') 1581 | """ 1582 | 1583 | def __init__(self, Smoother, window_shape, step=1): 1584 | self.Smoother = Smoother 1585 | self.window_shape = window_shape 1586 | self.step = step 1587 | 1588 | def smooth(self, data): 1589 | """Smooth timeseries. 1590 | 1591 | Parameters 1592 | ---------- 1593 | data : array-like of shape (1, timesteps) or also (timesteps,) 1594 | Single timeseries to smooth. The data are assumed to be in 1595 | increasing time order. 1596 | 1597 | Returns 1598 | ------- 1599 | self : returns an instance of self 1600 | """ 1601 | 1602 | if not 'tsmoothie.smoother' in str(self.Smoother.__repr__): 1603 | raise ValueError("Use a Smoother from tsmoothie.smoother") 1604 | 1605 | data = np.asarray(data) 1606 | if np.prod(data.shape) == np.max(data.shape): 1607 | data = data.ravel()[:, None] 1608 | else: 1609 | raise ValueError( 1610 | "The format of data received is not appropriate. " 1611 | "WindowWrapper accepts only univariate timeseries") 1612 | 1613 | if data.shape[0] < self.window_shape: 1614 | raise ValueError("window_shape must be <= than timesteps") 1615 | 1616 | data = create_windows(data, window_shape=self.window_shape, step=self.step) 1617 | data = np.squeeze(data, -1) 1618 | 1619 | self.Smoother.smooth(data) 1620 | 1621 | return self -------------------------------------------------------------------------------- /tsmoothie/utils_class.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A collection of utility classes. 3 | ''' 4 | 5 | import numpy as np 6 | from scipy import sparse 7 | 8 | 9 | class LinearRegression(object): 10 | """Ordinary least squares Linear Regression. 11 | 12 | Linear model that estimates coefficients to minimize the residual 13 | sum of squares between the observed targets and the predictions. 14 | It automatically handles single and multiple targets. 15 | 16 | It's a modified version of the Linear Regression implemented 17 | in scikit-learn. 18 | 19 | Parameters 20 | ---------- 21 | fit_intercept : bool, default=True 22 | Whether to calculate the intercept for this model. If set 23 | to False, no intercept will be used in calculations. 24 | 25 | Attributes 26 | ---------- 27 | coef_ : array of shape (n_coef,) for univariate data or 28 | (sample, n_coef) for multivariate data 29 | Array containing the estimated coefficients of the linear model. 30 | Available after fitting. 31 | 32 | residues_ : array of shape (sample,) 33 | Sums of residuals; squared Euclidean 2-norm for each sample. 34 | Available after fitting. 35 | 36 | rank_ : int 37 | Rank of exogenous variable matrix. 38 | Available after fitting. 39 | 40 | singular_ : array of shape (n_coef,) 41 | Singular values of the exogenous variable matrix. 42 | Available after fitting. 43 | 44 | intercept_ : array of shape (sample,) 45 | Array containing the estimated intercepts of the linear model. 46 | If fit_intercept = False it returns 0. 47 | Available after fitting. 48 | """ 49 | 50 | def __init__(self, fit_intercept=True): 51 | self.fit_intercept = fit_intercept 52 | 53 | def __repr__(self): 54 | return "" 55 | 56 | def __str__(self): 57 | return "" 58 | 59 | def _preprocess_data(self, X, y, sample_weight): 60 | """Center and scale data. Centers data to have mean zero along 61 | axis 0. If fit_intercept=False no centering is done. If 62 | sample_weight is not None, then the weighted mean of X and y is 63 | zero, and not the mean itself. This is here because nearly all 64 | linear models will want their data to be centered. This function 65 | also systematically makes y consistent with X.dtype. 66 | 67 | Returns 68 | ------- 69 | X : array 70 | 71 | y : array 72 | 73 | X_offset : array 74 | 75 | y_offset : array 76 | 77 | X_scale : array 78 | """ 79 | 80 | X = X.copy(order='K') 81 | y = y.copy(order='K') 82 | y = np.asarray(y, dtype=X.dtype) 83 | 84 | if self.fit_intercept: 85 | 86 | X_offset = np.average(X, axis=0, weights=sample_weight) 87 | X -= X_offset 88 | X_scale = np.ones(X.shape[1], dtype=X.dtype) 89 | 90 | y_offset = np.average(y, axis=0, weights=sample_weight) 91 | y -= y_offset 92 | 93 | else: 94 | 95 | X_offset = np.zeros(X.shape[1], dtype=X.dtype) 96 | X_scale = np.ones(X.shape[1], dtype=X.dtype) 97 | 98 | if y.ndim == 1: 99 | y_offset = X.dtype.type(0) 100 | else: 101 | y_offset = np.zeros(y.shape[1], dtype=X.dtype) 102 | 103 | return X, y, X_offset, y_offset, X_scale 104 | 105 | def _rescale_data(self, X, y, sample_weight): 106 | """Rescale data sample-wise by square root of sample_weight. 107 | 108 | Returns 109 | ------- 110 | X_rescaled : array 111 | 112 | y_rescaled : array 113 | """ 114 | 115 | n_samples = X.shape[0] 116 | sample_weight = np.sqrt(sample_weight) 117 | sw_matrix = sparse.dia_matrix((sample_weight, 0), 118 | shape=(n_samples, n_samples)) 119 | X = sw_matrix @ X 120 | y = sw_matrix @ y 121 | 122 | return X, y 123 | 124 | def fit(self, X, y, sample_weight): 125 | """Fit linear model. 126 | 127 | Parameters 128 | ---------- 129 | X : array of shape (n_samples, n_features) 130 | Training data. 131 | 132 | y : array of shape (n_samples,) or (n_samples, n_targets) 133 | Target values. 134 | 135 | sample_weight : array of shape (n_samples,), default=None 136 | Individual weights for each sample. 137 | 138 | Returns 139 | ------- 140 | self : returns an instance of self 141 | """ 142 | 143 | X, y, X_offset, y_offset, X_scale = self._preprocess_data( 144 | X, y, sample_weight=sample_weight) 145 | 146 | if np.unique(sample_weight).shape[0] > 1: 147 | X, y = self._rescale_data(X, y, sample_weight) 148 | 149 | self.coef_, self.residues_, self.rank_, self.singular_ = \ 150 | np.linalg.lstsq(X, y, rcond=None) 151 | 152 | self.coef_ = self.coef_.T 153 | 154 | if self.fit_intercept: 155 | self.coef_ = self.coef_ / X_scale 156 | self.intercept_ = y_offset - np.dot(X_offset, self.coef_.T) 157 | else: 158 | self.intercept_ = 0. 159 | 160 | return self 161 | 162 | def predict(self, X): 163 | """Compute the predictions with fitted coefficients. 164 | 165 | Parameters 166 | ---------- 167 | X : array of shape (n_samples, n_features) 168 | Exogenous data. 169 | 170 | Returns 171 | ------- 172 | pred : array 173 | """ 174 | 175 | pred = (X @ self.coef_.T) + self.intercept_ 176 | 177 | return pred -------------------------------------------------------------------------------- /tsmoothie/utils_func.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A collection of utility functions. 3 | ''' 4 | 5 | import numpy as np 6 | import scipy.stats as stats 7 | from typing import Iterable 8 | 9 | 10 | def sim_seasonal_data(n_series, timesteps, measure_noise, 11 | freq=None, level=None, amp=None): 12 | """Generate sinusoidal data with periodic patterns. 13 | 14 | Parameters 15 | ---------- 16 | n_series : int 17 | Number of timeseries to generate. 18 | 19 | timesteps : int 20 | How many timesteps every generated series must have. 21 | 22 | measure_noise : int 23 | The noise present in the signals. 24 | 25 | freq : int or 1D array-like, default=None 26 | The frequencies of the sinusoidal timeseries to generate. 27 | If a single integer is passed, all the series generated have 28 | the same frequencies. If a 1D array-like is passed, the 29 | frequencies of timeseries are random sampled from the iterable 30 | passed. If None, the frequencies are random generated. 31 | 32 | level : int or 1D array-like, default=None 33 | The levels of the sinusoidal timeseries to generate. 34 | If a single integer is passed, all the series generated have 35 | the same levels. If a 1D array-like is passed, the levels 36 | of timeseries are random sampled from the iterable passed. 37 | If None, the levels are random generated. 38 | 39 | amp : int or 1D array-like, default=None 40 | The amplitudes of the sinusoidal timeseries to generate. 41 | If a single integer is passed, all the series generated have 42 | the same amplitudes. If a 1D array-like is passed, the amplitudes 43 | of timeseries are random sampled from the iterable passed. 44 | If None, the amplitudes are random generated. 45 | 46 | Returns 47 | ------- 48 | data : array of shape (series, timesteps) 49 | The generated sinusoidal timeseries. 50 | """ 51 | 52 | if freq is None: 53 | freq = np.random.randint(3, int(np.sqrt(timesteps)), (n_series, 1)) 54 | elif isinstance(freq, Iterable): 55 | freq = np.random.choice(freq, size=n_series)[:, None] 56 | else: 57 | freq = np.asarray([[freq]] * n_series) 58 | 59 | if level is None: 60 | level = np.random.uniform(-100, 100, (n_series, 1)) 61 | elif isinstance(level, Iterable): 62 | level = np.random.choice(level, size=n_series)[:, None] 63 | else: 64 | level = np.asarray([[level]] * n_series) 65 | 66 | if amp is None: 67 | amp = np.random.uniform(3, 100, (n_series, 1)) 68 | elif isinstance(amp, Iterable): 69 | amp = np.random.choice(amp, size=n_series)[:, None] 70 | else: 71 | amp = np.asarray([[amp]] * n_series) 72 | 73 | t = np.repeat([np.arange(timesteps)], n_series, axis=0) 74 | e = np.random.normal(0, measure_noise, (n_series, timesteps)) 75 | data = level + amp * np.sin(t * (2 * np.pi / freq)) + e 76 | 77 | return data 78 | 79 | 80 | def sim_randomwalk(n_series, timesteps, process_noise, measure_noise, 81 | level=None): 82 | """Generate randomwalks. 83 | 84 | Parameters 85 | ---------- 86 | n_series : int 87 | Number of randomwalks to generate. 88 | 89 | timesteps : int 90 | How many timesteps every generated randomwalks must have. 91 | 92 | process_noise : int 93 | The noise present in randomwalks creation. 94 | 95 | measure_noise : int 96 | The noise present in the signals. 97 | 98 | level : int or 1D array-like, default=None 99 | The levels of the randomwalks to generate. 100 | If a single integer is passed, all the randomwalks have 101 | the same levels. If a 1D array-like is passed, the levels 102 | of the randomwalks are random sampled from the iterable 103 | passed. If None, the levels are set to 0 for all the series. 104 | 105 | Returns 106 | ------- 107 | data : array of shape (series, timesteps) 108 | The generated randomwalks. 109 | """ 110 | 111 | if level is None: 112 | level = 0 113 | if isinstance(level, Iterable): 114 | level = np.random.choice(level, size=n_series)[:, None] 115 | else: 116 | level = np.asarray([[level]] * n_series) 117 | 118 | data = np.random.normal(0, process_noise, size=(n_series, timesteps)) 119 | e = np.random.normal(0, measure_noise, size=(n_series, timesteps)) 120 | data = level + np.cumsum(data, axis=1) + e 121 | 122 | return data 123 | 124 | 125 | def create_windows(data, window_shape, step=1, 126 | start_id=None, end_id=None): 127 | """Create sliding windows of the same length from the series 128 | received as input. 129 | 130 | create_windows vectorizes, in an efficient way, the windows creation 131 | on all the series received. 132 | 133 | Parameters 134 | ---------- 135 | data : 2D array of shape (timestemps, series) 136 | Timeseries to slide into equal size windows. 137 | 138 | window_shape : int 139 | Grather than 1. The shape of the sliding windows used to divide 140 | the input series. 141 | 142 | step : int, default=1 143 | The step used to generate the sliding windows. The overlapping 144 | portion of two adjacent windows can be defined as 145 | (window_shape - step). 146 | 147 | start_id : int, default=None 148 | The starting position from where operate slicing. The same for 149 | all the series. If None, the windows are generated from the index 0. 150 | 151 | end_id : int, default=None 152 | The ending position of the slicing operation. The same for all the 153 | series. If None, the windows end on the last position available. 154 | 155 | Returns 156 | ------- 157 | window_data : 3D array of shape (window_slices, window_shape, series) 158 | The input data sliced into windows of the same lengths. 159 | """ 160 | 161 | data = np.asarray(data) 162 | 163 | if data.ndim != 2: 164 | raise ValueError( 165 | "Pass a 2D array-like in the format (timestemps, series)") 166 | 167 | if window_shape < 1: 168 | raise ValueError("window_shape must be >= 1") 169 | 170 | if start_id is None: 171 | start_id = 0 172 | 173 | if end_id is None: 174 | end_id = data.shape[0] 175 | 176 | data = data[int(start_id):int(end_id), :] 177 | 178 | window_shape = (int(window_shape), data.shape[-1]) 179 | step = (int(step),) * data.ndim 180 | 181 | slices = tuple(slice(None, None, st) for st in step) 182 | indexing_strides = data[slices].strides 183 | 184 | win_indices_shape = ((np.array(data.shape) - window_shape) // step) + 1 185 | 186 | new_shape = tuple(list(win_indices_shape) + list(window_shape)) 187 | strides = tuple(list(indexing_strides) + list(data.strides)) 188 | 189 | window_data = np.lib.stride_tricks.as_strided( 190 | data, shape=new_shape, strides=strides) 191 | 192 | return np.squeeze(window_data, 1) 193 | 194 | 195 | def sigma_interval(true, prediction, n_sigma): 196 | """Compute smoothing intervals as n_sigma times the residuals of the 197 | smoothing process. 198 | 199 | Returns 200 | ------- 201 | low : array 202 | Lower bands. 203 | 204 | up : array 205 | Upper bands. 206 | """ 207 | 208 | std = np.nanstd(true - prediction, axis=1, keepdims=True) 209 | 210 | low = prediction - n_sigma * std 211 | up = prediction + n_sigma * std 212 | 213 | return low, up 214 | 215 | 216 | def kalman_interval(true, prediction, cov, confidence=0.05): 217 | """Compute smoothing intervals from a Kalman smoothing process. 218 | 219 | Returns 220 | ------- 221 | low : array 222 | Lower bands. 223 | 224 | up : array 225 | Upper bands. 226 | """ 227 | 228 | g = stats.norm.ppf(1 - confidence / 2) 229 | 230 | resid = true - prediction 231 | std_err = np.sqrt(np.nanmean(np.square(resid), axis=1, keepdims=True)) 232 | 233 | low = prediction - g * (std_err * cov) 234 | up = prediction + g * (std_err * cov) 235 | 236 | return low, up 237 | 238 | 239 | def confidence_interval(true, prediction, exog, confidence, 240 | add_intercept=True): 241 | """Compute confidence intervals for regression tasks. 242 | 243 | Returns 244 | ------- 245 | low : array 246 | Lower bands. 247 | 248 | up : array 249 | Upper bands. 250 | """ 251 | 252 | if exog.ndim == 1: 253 | exog = exog[:, None] 254 | 255 | if add_intercept: 256 | exog = np.concatenate([np.ones((len(exog), 1)), exog], axis=1) 257 | 258 | N = exog.shape[0] 259 | d_free = exog.shape[1] 260 | t = stats.t.ppf(1 - confidence / 2, N - d_free) 261 | 262 | resid = true - prediction 263 | mse = (np.square(resid).sum(axis=1, keepdims=True) / (N - d_free)).T 264 | 265 | hat_matrix_diag = (exog * np.linalg.pinv(exog).T).sum(axis=1, keepdims=True) 266 | predict_mean_se = np.sqrt(hat_matrix_diag * mse).T 267 | 268 | low = prediction - t * predict_mean_se 269 | up = prediction + t * predict_mean_se 270 | 271 | return low, up 272 | 273 | 274 | def prediction_interval(true, prediction, exog, confidence, 275 | add_intercept=True): 276 | """Compute prediction intervals for regression tasks. 277 | 278 | Returns 279 | ------- 280 | low : array 281 | Lower bands. 282 | 283 | up : array 284 | Upper bands. 285 | """ 286 | 287 | if exog.ndim == 1: 288 | exog = exog[:, None] 289 | 290 | if add_intercept: 291 | exog = np.concatenate([np.ones((len(exog), 1)), exog], axis=1) 292 | 293 | N = exog.shape[0] 294 | d_free = exog.shape[1] 295 | t = stats.t.ppf(1 - confidence / 2, N - d_free) 296 | 297 | resid = true - prediction 298 | mse = (np.square(resid).sum(axis=1, keepdims=True) / (N - d_free)).T 299 | 300 | covb = np.linalg.pinv(np.dot(exog.T, exog))[..., None] * mse 301 | predvar = mse + (exog[..., None] * 302 | np.dot(covb.transpose(2, 0, 1), exog.T).T).sum(1) 303 | predstd = np.sqrt(predvar).T 304 | 305 | low = prediction - t * predstd 306 | up = prediction + t * predstd 307 | 308 | return low, up 309 | 310 | 311 | def _check_noise_dict(noise_dict, component): 312 | """Ensure noise compatibility for the noises of the components 313 | provided when building a state space model. 314 | 315 | Returns 316 | ------- 317 | noise_dict : dict 318 | Checked input. 319 | """ 320 | 321 | sub_component = component.split('_') 322 | 323 | if isinstance(noise_dict, dict): 324 | for c in sub_component: 325 | 326 | if c not in noise_dict: 327 | raise ValueError( 328 | "You need to provide noise for '{}' component".format(c)) 329 | 330 | if noise_dict[c] < 0: 331 | raise ValueError( 332 | "noise for '{}' must be >= 0".format(c)) 333 | 334 | return noise_dict 335 | 336 | else: 337 | raise ValueError( 338 | "noise should be a dict. Received {}".format(type(noise_dict))) 339 | 340 | 341 | def _check_knots(knots, min_n_knots): 342 | """Ensure knots compatibility for the knots provided when building 343 | bases for linear regression. 344 | 345 | Returns 346 | ------- 347 | knots : array 348 | Checked input. 349 | """ 350 | 351 | knots = np.asarray(knots, dtype=np.float64) 352 | 353 | if np.prod(knots.shape) == np.max(knots.shape): 354 | knots = knots.ravel() 355 | 356 | if knots.ndim != 1: 357 | raise ValueError("knots must be a list or 1D array") 358 | 359 | knots = np.unique(knots) 360 | min_k, max_k = knots[0], knots[-1] 361 | 362 | if min_k < 0 or max_k > 1: 363 | raise ValueError("Every knot must be in the range [0,1]") 364 | 365 | if min_k > 0: 366 | knots = np.append(0., knots) 367 | 368 | if max_k < 1: 369 | knots = np.append(knots, 1.) 370 | 371 | if knots.shape[0] < min_n_knots + 2: 372 | raise ValueError( 373 | "Provide at least {} knots in the range (0,1)".format(min_n_knots)) 374 | 375 | return knots 376 | 377 | 378 | def _check_weights(weights, basis_len): 379 | """Ensure weights compatibility for the weights provided in 380 | linear regression applications. 381 | 382 | Returns 383 | ------- 384 | weights : array 385 | Checked input. 386 | """ 387 | 388 | if weights is None: 389 | return np.ones(basis_len, dtype=np.float64) 390 | 391 | weights = np.asarray(weights, dtype=np.float64) 392 | 393 | if np.prod(weights.shape) == np.max(weights.shape): 394 | weights = weights.ravel() 395 | 396 | if weights.ndim != 1: 397 | raise ValueError("Sample weights must be a list or 1D array") 398 | 399 | if weights.shape[0] != basis_len: 400 | raise ValueError( 401 | "Sample weights length must be equal to timesteps " 402 | "dimension of the data received") 403 | 404 | if np.any(weights < 0): 405 | raise ValueError("weights must be >= 0") 406 | 407 | if np.logical_or(np.isnan(weights), np.isinf(weights)).any(): 408 | raise ValueError("weights must not contain NaNs or Inf") 409 | 410 | return weights 411 | 412 | 413 | def _check_data(data): 414 | """Ensure data compatibility for the series received by the smoother. 415 | 416 | Returns 417 | ------- 418 | data : array 419 | Checked input. 420 | """ 421 | 422 | data = np.asarray(data) 423 | 424 | if np.prod(data.shape) == np.max(data.shape): 425 | data = data.ravel() 426 | 427 | if data.ndim > 2: 428 | raise ValueError( 429 | "The format of data received is not appropriate. " 430 | "Pass an object with data in this format (series, timesteps)") 431 | 432 | if data.ndim == 0: 433 | raise ValueError( 434 | "Pass an object with data in this format (series, timesteps)") 435 | 436 | if data.dtype not in [np.float16, np.float32, np.float64, 437 | np.int8, np.int16, np.int32, np.int64]: 438 | raise ValueError("data contains not numeric types") 439 | 440 | if np.logical_or(np.isnan(data), np.isinf(data)).any(): 441 | raise ValueError("data must not contain NaNs or Inf") 442 | 443 | return data.T 444 | 445 | 446 | def _check_data_nan(data): 447 | """Ensure data compatibility for the series received by the smoother. 448 | (Without checking for inf and nans). 449 | 450 | Returns 451 | ------- 452 | data : array 453 | Checked input. 454 | """ 455 | 456 | data = np.asarray(data) 457 | 458 | if np.prod(data.shape) == np.max(data.shape): 459 | data = data.ravel() 460 | 461 | if data.ndim > 2: 462 | raise ValueError( 463 | "The format of data received is not appropriate. " 464 | "Pass an objet with data in this format (series, timesteps)") 465 | 466 | if data.ndim == 0: 467 | raise ValueError( 468 | "Pass an object with data in this format (series, timesteps)") 469 | 470 | if data.dtype not in [np.float16, np.float32, np.float64, 471 | np.int8, np.int16, np.int32, np.int64]: 472 | raise ValueError("data contains not numeric types") 473 | 474 | return data 475 | 476 | 477 | def _check_output(output, transpose=True): 478 | """Ensure output compatibility for the series returned by the smoother. 479 | 480 | Returns 481 | ------- 482 | output : array 483 | Checked input. 484 | """ 485 | 486 | if transpose: 487 | output = output.T 488 | 489 | if output.ndim == 1: 490 | output = output[None, :] 491 | 492 | return output 493 | 494 | 495 | def _id_nb_bootstrap(n_obs, block_length): 496 | """Create bootstrapped indexes with the none overlapping block bootstrap 497 | ('nbb') strategy given the number of observations in a timeseries and 498 | the length of the blocks. 499 | 500 | Returns 501 | ------- 502 | _id : array 503 | Bootstrapped indexes. 504 | """ 505 | 506 | n_blocks = int(np.ceil(n_obs / block_length)) 507 | nexts = np.repeat([np.arange(0, block_length)], n_blocks, axis=0) 508 | 509 | blocks = np.random.permutation( 510 | np.arange(0, n_obs, block_length) 511 | ).reshape(-1, 1) 512 | 513 | _id = (blocks + nexts).ravel() 514 | _id = _id[_id < n_obs] 515 | 516 | return _id 517 | 518 | 519 | def _id_mb_bootstrap(n_obs, block_length): 520 | """Create bootstrapped indexes with the moving block bootstrap 521 | ('mbb') strategy given the number of observations in a timeseries 522 | and the length of the blocks. 523 | 524 | Returns 525 | ------- 526 | _id : array 527 | Bootstrapped indexes. 528 | """ 529 | 530 | n_blocks = int(np.ceil(n_obs / block_length)) 531 | nexts = np.repeat([np.arange(0, block_length)], n_blocks, axis=0) 532 | 533 | last_block = n_obs - block_length 534 | blocks = np.random.randint(0, last_block, (n_blocks, 1)) 535 | _id = (blocks + nexts).ravel()[:n_obs] 536 | 537 | return _id 538 | 539 | 540 | def _id_cb_bootstrap(n_obs, block_length): 541 | """Create bootstrapped indexes with the circular block bootstrap 542 | ('cbb') strategy given the number of observations in a timeseries 543 | and the length of the blocks. 544 | 545 | Returns 546 | ------- 547 | _id : array 548 | Bootstrapped indexes. 549 | """ 550 | 551 | n_blocks = int(np.ceil(n_obs / block_length)) 552 | nexts = np.repeat([np.arange(0, block_length)], n_blocks, axis=0) 553 | 554 | last_block = n_obs 555 | blocks = np.random.randint(0, last_block, (n_blocks, 1)) 556 | _id = np.mod((blocks + nexts).ravel(), n_obs)[:n_obs] 557 | 558 | return _id 559 | 560 | 561 | def _id_s_bootstrap(n_obs, block_length): 562 | """Create bootstrapped indexes with the stationary bootstrap 563 | ('sb') strategy given the number of observations in a timeseries 564 | and the length of the blocks. 565 | 566 | Returns 567 | ------- 568 | _id : array 569 | Bootstrapped indexes. 570 | """ 571 | 572 | random_block_length = np.random.poisson(block_length, n_obs) 573 | random_block_length[random_block_length < 3] = 3 574 | random_block_length[random_block_length >= n_obs] = n_obs 575 | random_block_length = random_block_length[random_block_length.cumsum() <= n_obs] 576 | residual_block = n_obs - random_block_length.sum() 577 | if residual_block > 0: 578 | random_block_length = np.append(random_block_length, residual_block) 579 | 580 | n_blocks = random_block_length.shape[0] 581 | nexts = np.zeros((n_blocks, random_block_length.max() + 1)) 582 | nexts[np.arange(n_blocks), random_block_length] = 1 583 | nexts = np.flip(nexts, 1).cumsum(1).cumsum(1).ravel() 584 | nexts = (nexts[nexts > 1] - 2).astype(int) 585 | 586 | last_block = n_obs - random_block_length.max() 587 | blocks = np.zeros(n_obs, dtype=int) 588 | if last_block > 0: 589 | blocks = np.random.randint(0, last_block, n_blocks) 590 | blocks = np.repeat(blocks, random_block_length) 591 | _id = blocks + nexts 592 | 593 | return _id --------------------------------------------------------------------------------