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.
@@ -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
+ [![PyPI Version](https://img.shields.io/pypi/v/pyriodicity.svg?label=PyPI)](https://pypi.org/project/pyriodicity/)
24
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyriodicity?label=Python)
25
+ ![GitHub License](https://img.shields.io/github/license/iskandergaba/pyriodicity?label=License)
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
+ [![PyPI Version](https://img.shields.io/pypi/v/pyriodicity.svg?label=PyPI)](https://pypi.org/project/pyriodicity/)
5
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyriodicity?label=Python)
6
+ ![GitHub License](https://img.shields.io/github/license/iskandergaba/pyriodicity?label=License)
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,7 @@
1
+ from .detectors import (
2
+ ACFPeriodicityDetector,
3
+ Autoperiod,
4
+ FFTPeriodicityDetector,
5
+ )
6
+
7
+ __all__ = ["ACFPeriodicityDetector", "Autoperiod", "FFTPeriodicityDetector"]
@@ -0,0 +1,9 @@
1
+ from .acf import ACFPeriodicityDetector
2
+ from .autoperiod import Autoperiod
3
+ from .fft import FFTPeriodicityDetector
4
+
5
+ __all__ = [
6
+ "ACFPeriodicityDetector",
7
+ "Autoperiod",
8
+ "FFTPeriodicityDetector",
9
+ ]
@@ -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)])