pyriodicity 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyriodicity-0.2.0/LICENSE +21 -0
- pyriodicity-0.2.0/PKG-INFO +106 -0
- pyriodicity-0.2.0/README.md +86 -0
- pyriodicity-0.2.0/pyproject.toml +25 -0
- pyriodicity-0.2.0/pyriodicity/__init__.py +7 -0
- pyriodicity-0.2.0/pyriodicity/detectors/__init__.py +9 -0
- pyriodicity-0.2.0/pyriodicity/detectors/acf.py +117 -0
- pyriodicity-0.2.0/pyriodicity/detectors/autoperiod.py +242 -0
- pyriodicity-0.2.0/pyriodicity/detectors/fft.py +110 -0
- pyriodicity-0.2.0/pyriodicity/tools/__init__.py +17 -0
- pyriodicity-0.2.0/pyriodicity/tools/_tools.py +57 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Iskander Gaba
|
|
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.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pyriodicity
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Pyriodicity provides intuitive and easy-to-use Python implementation for periodicity (seasonality) detection in univariate time series.
|
|
5
|
+
Home-page: https://github.com/iskandergaba/pyriodicity
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: signal,time,series,forecast,analysis,seasonality,trend,decomposition
|
|
8
|
+
Author: Iskander Gaba
|
|
9
|
+
Author-email: iskander@hey.com
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Dist: scipy (>=1.14.0,<2.0.0)
|
|
17
|
+
Project-URL: Repository, https://github.com/iskandergaba/pyriodicity
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
<div align="center">
|
|
21
|
+
<h1>Pyriodicity</h1>
|
|
22
|
+
|
|
23
|
+
[](https://pypi.org/project/pyriodicity/)
|
|
24
|
+

|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## About Pyriodicity
|
|
31
|
+
Pyriodicity provides intuitive and easy-to-use Python implementation for periodicity (seasonality) detection in univariate time series. Pyriodicity supports the following detection methods:
|
|
32
|
+
- [Autocorrelation Function (ACF)](https://otexts.com/fpp3/acf.html)
|
|
33
|
+
- [Autoperiod]( https://doi.org/10.1137/1.9781611972757.40)
|
|
34
|
+
- [Fast Fourier Transform (FFT)](https://otexts.com/fpp3/useful-predictors.html#fourier-series)
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
To install the latest version of `pyriodicity`, simply run:
|
|
38
|
+
|
|
39
|
+
```shell
|
|
40
|
+
pip install pyriodicity
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Example
|
|
44
|
+
Start by loading a the `co2` timeseries emissions sample data from [`statsmodels`](https://www.statsmodels.org)
|
|
45
|
+
```python
|
|
46
|
+
from statsmodels.datasets import co2
|
|
47
|
+
data = co2.load().data
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
You can then resample the data to whatever frequency you want. In this example, we downsample the data to a monthly frequency
|
|
51
|
+
```python
|
|
52
|
+
data = data.resample("ME").mean().ffill()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use `Autoperiod` to find the list of periods based in this data (if any).
|
|
56
|
+
```python
|
|
57
|
+
from pyriodicity import Autoperiod
|
|
58
|
+
autoperiod = Autoperiod(data)
|
|
59
|
+
periods = autoperiod.fit()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
There are multiple parameters you can play with should you wish to. For example, you can specify a lower percentile value for a more lenient detection
|
|
63
|
+
```python
|
|
64
|
+
autoperiod.fit(percentile=90)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or increase the number of random data permutations for a better power threshold estimation
|
|
68
|
+
```python
|
|
69
|
+
autoperiod.fit(k=300)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Alternatively, you can use other periodicity detection methods such as `ACFPeriodicityDetector` and `FFTPeriodicityDetector` and compare results and performances.
|
|
73
|
+
|
|
74
|
+
## Development Environment Setup
|
|
75
|
+
This project is built and published using [Poetry](https://python-poetry.org). To setup a development environment for this project you can follow these steps:
|
|
76
|
+
|
|
77
|
+
1. Install one of the compatible [Python](https://www.python.org) versions indicated above.
|
|
78
|
+
2. Install [Poetry](https://python-poetry.org/docs/#installing-with-pipx).
|
|
79
|
+
3. Navigate to the root folder and install dependencies in a virtual environment:
|
|
80
|
+
```shell
|
|
81
|
+
poetry install
|
|
82
|
+
```
|
|
83
|
+
4. If everything worked properly, you should have an environment under the name `pyriodicity-py3.*` activated. You can verify this by running:
|
|
84
|
+
```shell
|
|
85
|
+
poetry env list
|
|
86
|
+
```
|
|
87
|
+
5. You can run tests using the command:
|
|
88
|
+
```shell
|
|
89
|
+
poetry run pytest
|
|
90
|
+
```
|
|
91
|
+
6. To export the detailed dependency list, consider running the following:
|
|
92
|
+
```shell
|
|
93
|
+
# Add poetry-plugin-export plugin to poetry
|
|
94
|
+
poetry self add poetry-plugin-export
|
|
95
|
+
|
|
96
|
+
# Export the package dependencies to requirements.txt
|
|
97
|
+
poetry export --output requirements.txt
|
|
98
|
+
|
|
99
|
+
# If you wish to export all the dependencies, including those needed for testing, run the following command
|
|
100
|
+
poetry export --with test --output requirements-dev.txt
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## References
|
|
104
|
+
- [1] Hyndman, R.J., & Athanasopoulos, G. (2021) Forecasting: principles and practice, 3rd edition, OTexts: Melbourne, Australia. [OTexts.com/fpp3](https://otexts.com/fpp3). Accessed on 09-15-2024.
|
|
105
|
+
- [2] Vlachos, M., Yu, P., & Castelli, V. (2005). On periodicity detection and Structural Periodic similarity. Proceedings of the 2005 SIAM International Conference on Data Mining. [doi.org/10.1137/1.9781611972757.40](https://doi.org/10.1137/1.9781611972757.40).
|
|
106
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>Pyriodicity</h1>
|
|
3
|
+
|
|
4
|
+
[](https://pypi.org/project/pyriodicity/)
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## About Pyriodicity
|
|
12
|
+
Pyriodicity provides intuitive and easy-to-use Python implementation for periodicity (seasonality) detection in univariate time series. Pyriodicity supports the following detection methods:
|
|
13
|
+
- [Autocorrelation Function (ACF)](https://otexts.com/fpp3/acf.html)
|
|
14
|
+
- [Autoperiod]( https://doi.org/10.1137/1.9781611972757.40)
|
|
15
|
+
- [Fast Fourier Transform (FFT)](https://otexts.com/fpp3/useful-predictors.html#fourier-series)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
To install the latest version of `pyriodicity`, simply run:
|
|
19
|
+
|
|
20
|
+
```shell
|
|
21
|
+
pip install pyriodicity
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Example
|
|
25
|
+
Start by loading a the `co2` timeseries emissions sample data from [`statsmodels`](https://www.statsmodels.org)
|
|
26
|
+
```python
|
|
27
|
+
from statsmodels.datasets import co2
|
|
28
|
+
data = co2.load().data
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You can then resample the data to whatever frequency you want. In this example, we downsample the data to a monthly frequency
|
|
32
|
+
```python
|
|
33
|
+
data = data.resample("ME").mean().ffill()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Use `Autoperiod` to find the list of periods based in this data (if any).
|
|
37
|
+
```python
|
|
38
|
+
from pyriodicity import Autoperiod
|
|
39
|
+
autoperiod = Autoperiod(data)
|
|
40
|
+
periods = autoperiod.fit()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
There are multiple parameters you can play with should you wish to. For example, you can specify a lower percentile value for a more lenient detection
|
|
44
|
+
```python
|
|
45
|
+
autoperiod.fit(percentile=90)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or increase the number of random data permutations for a better power threshold estimation
|
|
49
|
+
```python
|
|
50
|
+
autoperiod.fit(k=300)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Alternatively, you can use other periodicity detection methods such as `ACFPeriodicityDetector` and `FFTPeriodicityDetector` and compare results and performances.
|
|
54
|
+
|
|
55
|
+
## Development Environment Setup
|
|
56
|
+
This project is built and published using [Poetry](https://python-poetry.org). To setup a development environment for this project you can follow these steps:
|
|
57
|
+
|
|
58
|
+
1. Install one of the compatible [Python](https://www.python.org) versions indicated above.
|
|
59
|
+
2. Install [Poetry](https://python-poetry.org/docs/#installing-with-pipx).
|
|
60
|
+
3. Navigate to the root folder and install dependencies in a virtual environment:
|
|
61
|
+
```shell
|
|
62
|
+
poetry install
|
|
63
|
+
```
|
|
64
|
+
4. If everything worked properly, you should have an environment under the name `pyriodicity-py3.*` activated. You can verify this by running:
|
|
65
|
+
```shell
|
|
66
|
+
poetry env list
|
|
67
|
+
```
|
|
68
|
+
5. You can run tests using the command:
|
|
69
|
+
```shell
|
|
70
|
+
poetry run pytest
|
|
71
|
+
```
|
|
72
|
+
6. To export the detailed dependency list, consider running the following:
|
|
73
|
+
```shell
|
|
74
|
+
# Add poetry-plugin-export plugin to poetry
|
|
75
|
+
poetry self add poetry-plugin-export
|
|
76
|
+
|
|
77
|
+
# Export the package dependencies to requirements.txt
|
|
78
|
+
poetry export --output requirements.txt
|
|
79
|
+
|
|
80
|
+
# If you wish to export all the dependencies, including those needed for testing, run the following command
|
|
81
|
+
poetry export --with test --output requirements-dev.txt
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## References
|
|
85
|
+
- [1] Hyndman, R.J., & Athanasopoulos, G. (2021) Forecasting: principles and practice, 3rd edition, OTexts: Melbourne, Australia. [OTexts.com/fpp3](https://otexts.com/fpp3). Accessed on 09-15-2024.
|
|
86
|
+
- [2] Vlachos, M., Yu, P., & Castelli, V. (2005). On periodicity detection and Structural Periodic similarity. Proceedings of the 2005 SIAM International Conference on Data Mining. [doi.org/10.1137/1.9781611972757.40](https://doi.org/10.1137/1.9781611972757.40).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "pyriodicity"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Pyriodicity provides intuitive and easy-to-use Python implementation for periodicity (seasonality) detection in univariate time series."
|
|
5
|
+
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = ["README.md"]
|
|
8
|
+
repository = "https://github.com/iskandergaba/pyriodicity"
|
|
9
|
+
|
|
10
|
+
authors = ["Iskander Gaba <iskander@hey.com>"]
|
|
11
|
+
keywords = ["signal", "time", "series", "forecast", "analysis", "seasonality", "trend", "decomposition"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
[tool.poetry.dependencies]
|
|
15
|
+
python = "^3.10"
|
|
16
|
+
scipy = "^1.14.0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
[tool.poetry.group.test.dependencies]
|
|
20
|
+
pytest = "^7.4.3"
|
|
21
|
+
statsmodels = "^0.14.2"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["poetry-core"]
|
|
25
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from typing import Callable, Optional, Union
|
|
2
|
+
|
|
3
|
+
from numpy.typing import ArrayLike, NDArray
|
|
4
|
+
from scipy.signal import argrelmax
|
|
5
|
+
|
|
6
|
+
from pyriodicity.tools import acf, apply_window, detrend, to_1d_array
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ACFPeriodicityDetector:
|
|
10
|
+
"""
|
|
11
|
+
Autocorrelation function (ACF) based periodicity detector.
|
|
12
|
+
|
|
13
|
+
Find the periods in a given signal or series using its ACF. A lag value
|
|
14
|
+
is considered a period if it is a local maximum of the ACF.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
endog : array_like
|
|
19
|
+
Data to be investigated. Must be squeezable to 1-d.
|
|
20
|
+
|
|
21
|
+
References
|
|
22
|
+
----------
|
|
23
|
+
.. [1] Hyndman, R.J., & Athanasopoulos, G. (2021)
|
|
24
|
+
Forecasting: principles and practice, 3rd edition, OTexts: Melbourne, Australia.
|
|
25
|
+
OTexts.com/fpp3/acf.html. Accessed on 09-15-2024.
|
|
26
|
+
|
|
27
|
+
Examples
|
|
28
|
+
--------
|
|
29
|
+
Start by loading a timeseries datasets.
|
|
30
|
+
|
|
31
|
+
>>> from statsmodels.datasets import co2
|
|
32
|
+
>>> data = co2.load().data
|
|
33
|
+
|
|
34
|
+
You can resample the data to whatever frequency you want.
|
|
35
|
+
|
|
36
|
+
>>> data = data.resample("ME").mean().ffill()
|
|
37
|
+
|
|
38
|
+
Use ACFPeriodicityDetector to find the list of seasonality periods using the ACF.
|
|
39
|
+
|
|
40
|
+
>>> acf_detector = ACFPeriodicityDetector(data)
|
|
41
|
+
>>> periods = acf_detector.fit()
|
|
42
|
+
|
|
43
|
+
You can get the most prominent period by setting max_period_count to 1
|
|
44
|
+
|
|
45
|
+
>>> acf_detector.fit(max_period_count=1)
|
|
46
|
+
|
|
47
|
+
You can also use a different correlation function like Spearman
|
|
48
|
+
|
|
49
|
+
>>> acf_detector.fit(correlation_func="spearman")
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, endog: ArrayLike):
|
|
53
|
+
self.y = to_1d_array(endog)
|
|
54
|
+
|
|
55
|
+
def fit(
|
|
56
|
+
self,
|
|
57
|
+
max_period_count: Optional[int] = None,
|
|
58
|
+
detrend_func: Optional[Union[str, Callable[[ArrayLike], NDArray]]] = "linear",
|
|
59
|
+
window_func: Optional[Union[str, float, tuple]] = None,
|
|
60
|
+
correlation_func: Optional[str] = "pearson",
|
|
61
|
+
) -> NDArray:
|
|
62
|
+
"""
|
|
63
|
+
Find periods in the given series.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
max_period_count : int, optional, default = None
|
|
68
|
+
Maximum number of periods to look for.
|
|
69
|
+
detrend_func : str, callable, default = None
|
|
70
|
+
The kind of detrending to be applied on the series. It can either be
|
|
71
|
+
'linear' or 'constant' if it the parameter is of 'str' type, or a
|
|
72
|
+
custom function that returns a detrended series.
|
|
73
|
+
window_func : float, str, tuple optional, default = None
|
|
74
|
+
Window function to be applied to the time series. Check
|
|
75
|
+
'window' parameter documentation for scipy.signal.get_window
|
|
76
|
+
function for more information on the accepted formats of this
|
|
77
|
+
parameter.
|
|
78
|
+
correlation_func : str, default = 'pearson'
|
|
79
|
+
The correlation function to be used to calculate the ACF of the time
|
|
80
|
+
series. Possible values are ['pearson', 'spearman', 'kendall'].
|
|
81
|
+
|
|
82
|
+
See Also
|
|
83
|
+
--------
|
|
84
|
+
scipy.signal.detrend
|
|
85
|
+
Remove linear trend along axis from data.
|
|
86
|
+
scipy.signal.get_window
|
|
87
|
+
Return a window of a given length and type.
|
|
88
|
+
scipy.stats.kendalltau
|
|
89
|
+
Calculate Kendall's tau, a correlation measure for ordinal data.
|
|
90
|
+
scipy.stats.pearsonr
|
|
91
|
+
Pearson correlation coefficient and p-value for testing non-correlation.
|
|
92
|
+
scipy.stats.spearmanr
|
|
93
|
+
Calculate a Spearman correlation coefficient with associated p-value.
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
NDArray
|
|
99
|
+
List of detected periods.
|
|
100
|
+
"""
|
|
101
|
+
# Detrend data
|
|
102
|
+
self.y = self.y if detrend_func is None else detrend(self.y, detrend_func)
|
|
103
|
+
|
|
104
|
+
# Apply window on data
|
|
105
|
+
self.y = self.y if window_func is None else apply_window(self.y, window_func)
|
|
106
|
+
|
|
107
|
+
# Compute the ACF
|
|
108
|
+
acf_arr = acf(self.y, len(self.y) // 2, correlation_func)
|
|
109
|
+
|
|
110
|
+
# Find the local argmax of the first half of the ACF array
|
|
111
|
+
local_argmax = argrelmax(acf_arr)[0]
|
|
112
|
+
|
|
113
|
+
# Argsort the local maxima in the ACF array in a descending order
|
|
114
|
+
periods = local_argmax[acf_arr[local_argmax].argsort()][::-1]
|
|
115
|
+
|
|
116
|
+
# Return the requested maximum count of detected periods
|
|
117
|
+
return periods[:max_period_count]
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from typing import Callable, Optional, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import ArrayLike, NDArray
|
|
5
|
+
from scipy.signal import argrelmax, periodogram
|
|
6
|
+
from scipy.stats import linregress
|
|
7
|
+
|
|
8
|
+
from pyriodicity.tools import acf, apply_window, detrend, to_1d_array
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Autoperiod:
|
|
12
|
+
"""
|
|
13
|
+
Autoperiod periodicity detector.
|
|
14
|
+
|
|
15
|
+
Find the periods in a given signal or series using Autoperiod.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
endog : array_like
|
|
20
|
+
Data to be investigated. Must be squeezable to 1-d.
|
|
21
|
+
|
|
22
|
+
References
|
|
23
|
+
----------
|
|
24
|
+
.. [1] Vlachos, M., Yu, P., & Castelli, V. (2005).
|
|
25
|
+
On periodicity detection and Structural Periodic similarity.
|
|
26
|
+
Proceedings of the 2005 SIAM International Conference on Data Mining.
|
|
27
|
+
https://doi.org/10.1137/1.9781611972757.40
|
|
28
|
+
|
|
29
|
+
Examples
|
|
30
|
+
--------
|
|
31
|
+
Start by loading a timeseries dataset.
|
|
32
|
+
|
|
33
|
+
>>> from statsmodels.datasets import co2
|
|
34
|
+
>>> data = co2.load().data
|
|
35
|
+
|
|
36
|
+
You can resample the data to whatever frequency you want.
|
|
37
|
+
|
|
38
|
+
>>> data = data.resample("ME").mean().ffill()
|
|
39
|
+
|
|
40
|
+
Use Autoperiod to find the list of periods in the data.
|
|
41
|
+
|
|
42
|
+
>>> autoperiod = Autoperiod(data)
|
|
43
|
+
>>> periods = autoperiod.fit()
|
|
44
|
+
|
|
45
|
+
You can specify a lower percentile value for a more lenient detection
|
|
46
|
+
|
|
47
|
+
>>> autoperiod.fit(percentile=90)
|
|
48
|
+
|
|
49
|
+
Or increase the number of random data permutations for a better power threshold estimation
|
|
50
|
+
|
|
51
|
+
>>> autoperiod.fit(k=300)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, endog: ArrayLike):
|
|
55
|
+
self.y = to_1d_array(endog)
|
|
56
|
+
|
|
57
|
+
def fit(
|
|
58
|
+
self,
|
|
59
|
+
k: int = 100,
|
|
60
|
+
percentile: int = 95,
|
|
61
|
+
detrend_func: Optional[Union[str, Callable[[ArrayLike], NDArray]]] = "linear",
|
|
62
|
+
window_func: Optional[Union[str, float, tuple]] = None,
|
|
63
|
+
correlation_func: Optional[str] = "pearson",
|
|
64
|
+
) -> NDArray:
|
|
65
|
+
"""
|
|
66
|
+
Find periods in the given series.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
k : int, optional, default = 100
|
|
71
|
+
The number of times the data is randomly permuted while estimating the
|
|
72
|
+
power threshold.
|
|
73
|
+
percentile : int, optional, default = 95
|
|
74
|
+
Percentage for the percentile parameter used in computing the power
|
|
75
|
+
threshold. Value must be between 0 and 100 inclusive.
|
|
76
|
+
detrend_func : str, callable, default = None
|
|
77
|
+
The kind of detrending to be applied on the series. It can either be
|
|
78
|
+
'linear' or 'constant' if it the parameter is of 'str' type, or a
|
|
79
|
+
custom function that returns a detrended series.
|
|
80
|
+
window_func : float, str, tuple optional, default = None
|
|
81
|
+
Window function to be applied to the time series. Check
|
|
82
|
+
'window' parameter documentation for scipy.signal.get_window
|
|
83
|
+
function for more information on the accepted formats of this
|
|
84
|
+
parameter.
|
|
85
|
+
correlation_func : str, default = 'pearson'
|
|
86
|
+
The correlation function to be used to calculate the ACF of the time
|
|
87
|
+
series. Possible values are ['pearson', 'spearman', 'kendall'].
|
|
88
|
+
|
|
89
|
+
See Also
|
|
90
|
+
--------
|
|
91
|
+
scipy.signal.detrend
|
|
92
|
+
Remove linear trend along axis from data.
|
|
93
|
+
scipy.signal.get_window
|
|
94
|
+
Return a window of a given length and type.
|
|
95
|
+
scipy.stats.kendalltau
|
|
96
|
+
Calculate Kendall's tau, a correlation measure for ordinal data.
|
|
97
|
+
scipy.stats.pearsonr
|
|
98
|
+
Pearson correlation coefficient and p-value for testing non-correlation.
|
|
99
|
+
scipy.stats.spearmanr
|
|
100
|
+
Calculate a Spearman correlation coefficient with associated p-value.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
NDArray
|
|
105
|
+
List of detected periods.
|
|
106
|
+
"""
|
|
107
|
+
# Detrend data
|
|
108
|
+
self.y = self.y if detrend_func is None else detrend(self.y, detrend_func)
|
|
109
|
+
# Apply window on data
|
|
110
|
+
self.y = self.y if window_func is None else apply_window(self.y, window_func)
|
|
111
|
+
|
|
112
|
+
# Compute the power threshold
|
|
113
|
+
p_threshold = self._power_threshold(self.y, k, percentile)
|
|
114
|
+
|
|
115
|
+
# Find period hints
|
|
116
|
+
freq, power = periodogram(self.y, window=None, detrend=None)
|
|
117
|
+
period_hints = np.array(
|
|
118
|
+
[
|
|
119
|
+
1 / f
|
|
120
|
+
for f, p in zip(freq, power)
|
|
121
|
+
if f >= 1 / len(freq) and p >= p_threshold
|
|
122
|
+
]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Compute the ACF
|
|
126
|
+
length = len(self.y)
|
|
127
|
+
acf_arr = acf(self.y, nlags=length, correlation_func=correlation_func)
|
|
128
|
+
|
|
129
|
+
# Validate period hints
|
|
130
|
+
period_hints_valid = []
|
|
131
|
+
for p in period_hints:
|
|
132
|
+
q = length / p
|
|
133
|
+
start = np.floor((p + length / (q + 1)) / 2 - 1).astype(int)
|
|
134
|
+
end = np.ceil((p + length / (q - 1)) / 2 + 1).astype(int)
|
|
135
|
+
|
|
136
|
+
splits = [
|
|
137
|
+
self._split(np.arange(len(acf_arr)), acf_arr, start, end, i)
|
|
138
|
+
for i in range(start + 2, end)
|
|
139
|
+
]
|
|
140
|
+
line1, line2, _ = splits[
|
|
141
|
+
np.array([error for _, _, error in splits]).argmin()
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
if line1.slope > 0 > line2.slope:
|
|
145
|
+
period_hints_valid.append(p)
|
|
146
|
+
|
|
147
|
+
period_hints_valid = np.array(period_hints_valid)
|
|
148
|
+
|
|
149
|
+
# Return the closest ACF peak for each valid period hint
|
|
150
|
+
local_argmax = argrelmax(acf_arr)[0]
|
|
151
|
+
return np.array(
|
|
152
|
+
list(
|
|
153
|
+
{
|
|
154
|
+
min(local_argmax, key=lambda x: abs(x - p))
|
|
155
|
+
for p in period_hints_valid
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _power_threshold(y: ArrayLike, k: int, p: int) -> float:
|
|
162
|
+
"""
|
|
163
|
+
Compute the power threshold as the p-th percentile of the maximum
|
|
164
|
+
power values of the periodogram of k permutations of the data.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
y : array_like
|
|
169
|
+
Data to be investigated. Must be squeezable to 1-d.
|
|
170
|
+
k : int
|
|
171
|
+
The number of times the data is randomly permuted to compute
|
|
172
|
+
the maximum power values.
|
|
173
|
+
p : int
|
|
174
|
+
The percentile value used to compute the power threshold.
|
|
175
|
+
It determines the cutoff point in the sorted list of the maximum
|
|
176
|
+
power values from the periodograms of the permuted data.
|
|
177
|
+
Value must be between 0 and 100 inclusive.
|
|
178
|
+
|
|
179
|
+
See Also
|
|
180
|
+
--------
|
|
181
|
+
scipy.signal.periodogram
|
|
182
|
+
Estimate power spectral density using a periodogram.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
float
|
|
187
|
+
Power threshold of the target data.
|
|
188
|
+
"""
|
|
189
|
+
max_powers = []
|
|
190
|
+
while len(max_powers) < k:
|
|
191
|
+
_, power_p = periodogram(
|
|
192
|
+
np.random.permutation(y), window=None, detrend=None
|
|
193
|
+
)
|
|
194
|
+
max_powers.append(power_p.max())
|
|
195
|
+
max_powers.sort()
|
|
196
|
+
return np.percentile(max_powers, p)
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _split(x: ArrayLike, y: ArrayLike, start: int, end: int, split: int) -> tuple:
|
|
200
|
+
"""
|
|
201
|
+
Approximate a function at [start, end] with two line segments at
|
|
202
|
+
[start, split - 1] and [split, end].
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
x : array_like
|
|
207
|
+
The x-coordinates of the data points.
|
|
208
|
+
y : array_like
|
|
209
|
+
The y-coordinates of the data points.
|
|
210
|
+
start : int
|
|
211
|
+
The start index of the data points to be approximated.
|
|
212
|
+
end : int
|
|
213
|
+
The end index of the data points to be approximated.
|
|
214
|
+
split : int
|
|
215
|
+
The split index of the data points to be approximated.
|
|
216
|
+
|
|
217
|
+
See Also
|
|
218
|
+
--------
|
|
219
|
+
scipy.stats.linregress
|
|
220
|
+
Calculate a linear least-squares regression for two sets of measurements.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
linregress
|
|
225
|
+
The first line segment.
|
|
226
|
+
linregress
|
|
227
|
+
The second line segment.
|
|
228
|
+
float
|
|
229
|
+
The error of the approximation.
|
|
230
|
+
"""
|
|
231
|
+
x1, y1, x2, y2 = (
|
|
232
|
+
x[start:split],
|
|
233
|
+
y[start:split],
|
|
234
|
+
x[split : end + 1],
|
|
235
|
+
y[split : end + 1],
|
|
236
|
+
)
|
|
237
|
+
line1 = linregress(x1, y1)
|
|
238
|
+
line2 = linregress(x2, y2)
|
|
239
|
+
error = np.sum(np.abs(y1 - (line1.intercept + line1.slope * x1))) + np.sum(
|
|
240
|
+
np.abs(y2 - (line2.intercept + line2.slope * x2))
|
|
241
|
+
)
|
|
242
|
+
return line1, line2, error
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from typing import Callable, Optional, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import ArrayLike, NDArray
|
|
5
|
+
|
|
6
|
+
from pyriodicity.tools import apply_window, detrend, to_1d_array
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FFTPeriodicityDetector:
|
|
10
|
+
"""
|
|
11
|
+
Fast Fourier Transform (FFT) based periodicity detector.
|
|
12
|
+
|
|
13
|
+
Find the periods in a given signal or series using FFT.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
endog : array_like
|
|
18
|
+
Data to be investigated. Must be squeezable to 1-d.
|
|
19
|
+
|
|
20
|
+
References
|
|
21
|
+
----------
|
|
22
|
+
.. [1] Hyndman, R.J., & Athanasopoulos, G. (2021)
|
|
23
|
+
Forecasting: principles and practice, 3rd edition, OTexts: Melbourne, Australia.
|
|
24
|
+
OTexts.com/fpp3/useful-predictors.html#fourier-series. Accessed on 09-15-2024.
|
|
25
|
+
|
|
26
|
+
Examples
|
|
27
|
+
--------
|
|
28
|
+
Start by loading a timeseries dataset.
|
|
29
|
+
|
|
30
|
+
>>> from statsmodels.datasets import co2
|
|
31
|
+
>>> data = co2.load().data
|
|
32
|
+
|
|
33
|
+
You can resample the data to whatever frequency you want.
|
|
34
|
+
|
|
35
|
+
>>> data = data.resample("ME").mean().ffill()
|
|
36
|
+
|
|
37
|
+
Use FFTPeriodicityDetector to find the list of periods using FFT, ordered
|
|
38
|
+
by corresponding frequency amplitudes in a descending order.
|
|
39
|
+
|
|
40
|
+
>>> fft_detector = FFTPeriodicityDetector(data)
|
|
41
|
+
>>> periods = fft_detector.fit()
|
|
42
|
+
|
|
43
|
+
You can optionally specify a window function for pre-processing.
|
|
44
|
+
|
|
45
|
+
>>> periods = fft_detector.fit(window_func="blackman")
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, endog: ArrayLike):
|
|
49
|
+
self.y = to_1d_array(endog)
|
|
50
|
+
|
|
51
|
+
def fit(
|
|
52
|
+
self,
|
|
53
|
+
max_period_count: Optional[int] = None,
|
|
54
|
+
detrend_func: Optional[Union[str, Callable[[ArrayLike], NDArray]]] = "linear",
|
|
55
|
+
window_func: Optional[Union[float, str, tuple]] = None,
|
|
56
|
+
) -> NDArray:
|
|
57
|
+
"""
|
|
58
|
+
Find periods in the given series.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
max_period_count : int, optional, default = None
|
|
63
|
+
Maximum number of periods to look for.
|
|
64
|
+
detrend_func : str, callable, default = None
|
|
65
|
+
The kind of detrending to be applied on the series. It can either be
|
|
66
|
+
'linear' or 'constant' if it the parameter is of 'str' type, or a
|
|
67
|
+
custom function that returns a detrended series.
|
|
68
|
+
window_func : float, str, tuple optional, default = None
|
|
69
|
+
Window function to be applied to the time series. Check
|
|
70
|
+
'window' parameter documentation for scipy.signal.get_window
|
|
71
|
+
function for more information on the accepted formats of this
|
|
72
|
+
parameter.
|
|
73
|
+
|
|
74
|
+
See Also
|
|
75
|
+
--------
|
|
76
|
+
numpy.fft
|
|
77
|
+
Discrete Fourier Transform.
|
|
78
|
+
scipy.signal.detrend
|
|
79
|
+
Remove linear trend along axis from data.
|
|
80
|
+
scipy.signal.get_window
|
|
81
|
+
Return a window of a given length and type.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
NDArray
|
|
86
|
+
List of detected periods.
|
|
87
|
+
"""
|
|
88
|
+
# Detrend data
|
|
89
|
+
self.y = self.y if detrend_func is None else detrend(self.y, detrend_func)
|
|
90
|
+
|
|
91
|
+
# Apply the window function on the data
|
|
92
|
+
self.y = (
|
|
93
|
+
self.y
|
|
94
|
+
if window_func is None
|
|
95
|
+
else apply_window(self.y, window_func=window_func)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Compute DFT and ignore the zero frequency
|
|
99
|
+
freqs = np.fft.rfftfreq(len(self.y), d=1)[1:]
|
|
100
|
+
ft = np.fft.rfft(self.y)[1:]
|
|
101
|
+
|
|
102
|
+
# Compute periods and their respective amplitudes
|
|
103
|
+
periods = np.round(1 / freqs)
|
|
104
|
+
amps = abs(ft)
|
|
105
|
+
|
|
106
|
+
# A period cannot be greater than half the length of the series
|
|
107
|
+
filter = periods < len(self.y) // 2
|
|
108
|
+
|
|
109
|
+
# Return periods in descending order of their corresponding amplitudes
|
|
110
|
+
return periods[filter][np.argsort(-amps[filter])][:max_period_count]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from ._tools import (
|
|
2
|
+
acf,
|
|
3
|
+
apply_window,
|
|
4
|
+
detrend,
|
|
5
|
+
remove_overloaded_kwargs,
|
|
6
|
+
seasonality_strength,
|
|
7
|
+
to_1d_array,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"acf",
|
|
12
|
+
"apply_window",
|
|
13
|
+
"detrend",
|
|
14
|
+
"remove_overloaded_kwargs",
|
|
15
|
+
"seasonality_strength",
|
|
16
|
+
"to_1d_array",
|
|
17
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import Callable, Dict, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import ArrayLike, NDArray
|
|
5
|
+
from scipy.signal import detrend as _detrend
|
|
6
|
+
from scipy.signal import get_window
|
|
7
|
+
from scipy.stats import kendalltau, pearsonr, spearmanr
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def to_1d_array(x: ArrayLike) -> NDArray:
|
|
12
|
+
y = np.ascontiguousarray(np.squeeze(np.asarray(x)), dtype=np.double)
|
|
13
|
+
if y.ndim != 1:
|
|
14
|
+
raise ValueError("y must be a 1d array")
|
|
15
|
+
return y
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def remove_overloaded_kwargs(kwargs: Dict, args: List) -> Dict:
|
|
20
|
+
for arg in args:
|
|
21
|
+
kwargs.pop(arg, None)
|
|
22
|
+
return kwargs
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def seasonality_strength(seasonal: ArrayLike, resid: ArrayLike) -> float:
|
|
27
|
+
return max(0, 1 - np.var(resid) / np.var(seasonal + resid))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def apply_window(x: ArrayLike, window_func: Union[str, float, tuple]) -> NDArray:
|
|
32
|
+
return x * get_window(window=window_func, Nx=len(x))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def detrend(
|
|
37
|
+
x: ArrayLike,
|
|
38
|
+
method: Union[str, Callable[[ArrayLike], NDArray]],
|
|
39
|
+
) -> NDArray:
|
|
40
|
+
if isinstance(method, str):
|
|
41
|
+
return _detrend(x, type=method)
|
|
42
|
+
return method(x)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def acf(
|
|
47
|
+
x: ArrayLike,
|
|
48
|
+
nlags: int,
|
|
49
|
+
correlation_func: Optional[str] = "pearson",
|
|
50
|
+
) -> NDArray:
|
|
51
|
+
if not 0 < nlags <= len(x):
|
|
52
|
+
raise ValueError("nlags must be a postive integer less than the data length")
|
|
53
|
+
if correlation_func == "spearman":
|
|
54
|
+
return np.array([spearmanr(x, np.roll(x, l)).statistic for l in range(nlags)])
|
|
55
|
+
elif correlation_func == "kendall":
|
|
56
|
+
return np.array([kendalltau(x, np.roll(x, l)).statistic for l in range(nlags)])
|
|
57
|
+
return np.array([pearsonr(x, np.roll(x, l)).statistic for l in range(nlags)])
|