pastas-plugins 0.1.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.
- pastas_plugins-0.1.0/LICENSE +21 -0
- pastas_plugins-0.1.0/PKG-INFO +137 -0
- pastas_plugins-0.1.0/README.md +66 -0
- pastas_plugins-0.1.0/pastas_plugins/__init__.py +35 -0
- pastas_plugins-0.1.0/pastas_plugins/cross_correlation/__init__.py +8 -0
- pastas_plugins-0.1.0/pastas_plugins/cross_correlation/cross_correlation.py +188 -0
- pastas_plugins-0.1.0/pastas_plugins/cross_correlation/plots.py +245 -0
- pastas_plugins-0.1.0/pastas_plugins/cross_correlation/version.py +1 -0
- pastas_plugins-0.1.0/pastas_plugins/modflow/__init__.py +4 -0
- pastas_plugins-0.1.0/pastas_plugins/modflow/modflow.py +177 -0
- pastas_plugins-0.1.0/pastas_plugins/modflow/stressmodels.py +114 -0
- pastas_plugins-0.1.0/pastas_plugins/modflow/version.py +1 -0
- pastas_plugins-0.1.0/pastas_plugins/reservoirs/__init__.py +4 -0
- pastas_plugins-0.1.0/pastas_plugins/reservoirs/reservoir.py +210 -0
- pastas_plugins-0.1.0/pastas_plugins/reservoirs/stressmodels.py +145 -0
- pastas_plugins-0.1.0/pastas_plugins/reservoirs/version.py +1 -0
- pastas_plugins-0.1.0/pastas_plugins/responses/__init__.py +3 -0
- pastas_plugins-0.1.0/pastas_plugins/responses/rfunc.py +125 -0
- pastas_plugins-0.1.0/pastas_plugins/responses/rfunc_utils.py +32 -0
- pastas_plugins-0.1.0/pastas_plugins/responses/version.py +1 -0
- pastas_plugins-0.1.0/pastas_plugins/version.py +1 -0
- pastas_plugins-0.1.0/pastas_plugins.egg-info/PKG-INFO +137 -0
- pastas_plugins-0.1.0/pastas_plugins.egg-info/SOURCES.txt +31 -0
- pastas_plugins-0.1.0/pastas_plugins.egg-info/dependency_links.txt +1 -0
- pastas_plugins-0.1.0/pastas_plugins.egg-info/requires.txt +34 -0
- pastas_plugins-0.1.0/pastas_plugins.egg-info/top_level.txt +1 -0
- pastas_plugins-0.1.0/pyproject.toml +112 -0
- pastas_plugins-0.1.0/setup.cfg +4 -0
- pastas_plugins-0.1.0/tests/test_crosscorrelation.py +111 -0
- pastas_plugins-0.1.0/tests/test_modflow.py +2 -0
- pastas_plugins-0.1.0/tests/test_pastas_plugins_base.py +51 -0
- pastas_plugins-0.1.0/tests/test_reservoirs.py +85 -0
- pastas_plugins-0.1.0/tests/test_responses.py +49 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Pastas
|
|
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,137 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pastas_plugins
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Repository containing plugins to further enhance your pastas experience.
|
|
5
|
+
Author: Pastas Developers Team
|
|
6
|
+
Maintainer-email: "D.A. Brakenhoff" <d.brakenhoff@artesia-water.nl>, "M.A. Vonk" <m.vonk@artesia-water.nl>, "M. Bakker" <markbak@gmail.com>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2024 Pastas
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
|
|
29
|
+
Project-URL: homepage, https://pastas.dev
|
|
30
|
+
Project-URL: repository, https://github.com/pastas/pastas-plugins
|
|
31
|
+
Project-URL: documentation, https://pastas.readthedocs.io
|
|
32
|
+
Keywords: hydrology,groundwater,timeseries,analysis
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Intended Audience :: Science/Research
|
|
35
|
+
Classifier: Intended Audience :: Other Audience
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Topic :: Scientific/Engineering :: Hydrology
|
|
43
|
+
Requires-Python: >=3.9
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
License-File: LICENSE
|
|
46
|
+
Requires-Dist: pastas
|
|
47
|
+
Provides-Extra: all
|
|
48
|
+
Requires-Dist: pastas_plugins[crosscorrelation,modflow]; extra == "all"
|
|
49
|
+
Provides-Extra: crosscorrelation
|
|
50
|
+
Requires-Dist: statsmodels; extra == "crosscorrelation"
|
|
51
|
+
Provides-Extra: modflow
|
|
52
|
+
Requires-Dist: flopy; extra == "modflow"
|
|
53
|
+
Provides-Extra: reservoirs
|
|
54
|
+
Requires-Dist: pastas_plugins; extra == "reservoirs"
|
|
55
|
+
Provides-Extra: responses
|
|
56
|
+
Requires-Dist: pastas_plugins; extra == "responses"
|
|
57
|
+
Provides-Extra: ruffing
|
|
58
|
+
Requires-Dist: ruff; extra == "ruffing"
|
|
59
|
+
Provides-Extra: pytesting
|
|
60
|
+
Requires-Dist: pytest>=7; extra == "pytesting"
|
|
61
|
+
Requires-Dist: pytest-cov; extra == "pytesting"
|
|
62
|
+
Requires-Dist: pytest-sugar; extra == "pytesting"
|
|
63
|
+
Provides-Extra: ci
|
|
64
|
+
Requires-Dist: coverage; extra == "ci"
|
|
65
|
+
Provides-Extra: rtd
|
|
66
|
+
Requires-Dist: pastas_plugins[all]; extra == "rtd"
|
|
67
|
+
Requires-Dist: sphinx_rtd_theme; extra == "rtd"
|
|
68
|
+
Requires-Dist: myst_nb; extra == "rtd"
|
|
69
|
+
Requires-Dist: numpydoc; extra == "rtd"
|
|
70
|
+
Requires-Dist: sphinx_design; extra == "rtd"
|
|
71
|
+
|
|
72
|
+
# pastas-plugins
|
|
73
|
+
|
|
74
|
+
Welcome to the pastas-plugins repository, your one-stop-shop for customized
|
|
75
|
+
cutting-edge additions to Pastas.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
## Current plugins
|
|
79
|
+
|
|
80
|
+
The following plugins are now available:
|
|
81
|
+
|
|
82
|
+
- **`cross_correlation`**: analyze and visualize the cross-correlation between two time series.
|
|
83
|
+
- **`modflow`**: use modflow models as response functions.
|
|
84
|
+
- **`reservoirs`**: use reservoir models to simulate time series.
|
|
85
|
+
- **`responses`**: custom response functions for Pastas.
|
|
86
|
+
|
|
87
|
+
## Installation
|
|
88
|
+
|
|
89
|
+
<!-- TODO: add repo to PYPI so this becomes true: -->
|
|
90
|
+
Install `pastas-plugins` with:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install pastas_plugins
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
If you want to use a specific plugin and want to ensure you install all the requisite
|
|
97
|
+
dependencies, you can use the following command:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install pastas-plugins[<name of plugin>]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
If you want to install them all:
|
|
104
|
+
```bash
|
|
105
|
+
pip install pastas-plugins[all]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Usage
|
|
109
|
+
|
|
110
|
+
Import the pastas-plugins module with:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import pastas_plugins as pp
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
This gives you access to the following functions:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
pp.list_plugins() # list of all plugins
|
|
120
|
+
pp.show_plugin_versions() # show plugin versions
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The function `pp.show_plugin_versions()` will indicate if any dependencies are missing
|
|
124
|
+
for a particular plugin. See the [Installation](#installation) section above for tips
|
|
125
|
+
on how to install dependencies for a particular plugin.
|
|
126
|
+
|
|
127
|
+
To use a particular plugin, you'll have to import it explicitly, e.g.:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from pastas_plugins import responses
|
|
131
|
+
|
|
132
|
+
rfunc = responses.Theis()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Separate plugins are each stored in a separate submodule within the pastas-plugins
|
|
136
|
+
package. You do not need to install the dependencies for each plugin if you're only
|
|
137
|
+
interested in one particular plugin.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# pastas-plugins
|
|
2
|
+
|
|
3
|
+
Welcome to the pastas-plugins repository, your one-stop-shop for customized
|
|
4
|
+
cutting-edge additions to Pastas.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## Current plugins
|
|
8
|
+
|
|
9
|
+
The following plugins are now available:
|
|
10
|
+
|
|
11
|
+
- **`cross_correlation`**: analyze and visualize the cross-correlation between two time series.
|
|
12
|
+
- **`modflow`**: use modflow models as response functions.
|
|
13
|
+
- **`reservoirs`**: use reservoir models to simulate time series.
|
|
14
|
+
- **`responses`**: custom response functions for Pastas.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
<!-- TODO: add repo to PYPI so this becomes true: -->
|
|
19
|
+
Install `pastas-plugins` with:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install pastas_plugins
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If you want to use a specific plugin and want to ensure you install all the requisite
|
|
26
|
+
dependencies, you can use the following command:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install pastas-plugins[<name of plugin>]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If you want to install them all:
|
|
33
|
+
```bash
|
|
34
|
+
pip install pastas-plugins[all]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
Import the pastas-plugins module with:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import pastas_plugins as pp
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This gives you access to the following functions:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
pp.list_plugins() # list of all plugins
|
|
49
|
+
pp.show_plugin_versions() # show plugin versions
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The function `pp.show_plugin_versions()` will indicate if any dependencies are missing
|
|
53
|
+
for a particular plugin. See the [Installation](#installation) section above for tips
|
|
54
|
+
on how to install dependencies for a particular plugin.
|
|
55
|
+
|
|
56
|
+
To use a particular plugin, you'll have to import it explicitly, e.g.:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from pastas_plugins import responses
|
|
60
|
+
|
|
61
|
+
rfunc = responses.Theis()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Separate plugins are each stored in a separate submodule within the pastas-plugins
|
|
65
|
+
package. You do not need to install the dependencies for each plugin if you're only
|
|
66
|
+
interested in one particular plugin.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# ruff : noqa: F401
|
|
2
|
+
import pathlib
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
|
|
5
|
+
# from platform import python_version
|
|
6
|
+
# from packaging.version import parse as parse_version
|
|
7
|
+
from pastas_plugins.version import __version__
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def list_plugins():
|
|
11
|
+
plugins = pathlib.Path(__file__).parent.iterdir()
|
|
12
|
+
plugins = [
|
|
13
|
+
plugin.stem
|
|
14
|
+
for plugin in plugins
|
|
15
|
+
if plugin.is_dir() and not plugin.stem.startswith("_")
|
|
16
|
+
]
|
|
17
|
+
plugins.sort()
|
|
18
|
+
return plugins
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def show_plugin_versions():
|
|
22
|
+
showtip = False
|
|
23
|
+
plugins = list_plugins()
|
|
24
|
+
msg = f"pastas_plugins version : {__version__}\n"
|
|
25
|
+
for plugin in plugins:
|
|
26
|
+
try:
|
|
27
|
+
module = import_module(f"pastas_plugins.{plugin}.version")
|
|
28
|
+
version = module.__version__
|
|
29
|
+
except ModuleNotFoundError:
|
|
30
|
+
showtip = True
|
|
31
|
+
version = "not available (check dependencies)"
|
|
32
|
+
msg += f"- {(plugin + ' version'):25s} : {version}\n"
|
|
33
|
+
if showtip:
|
|
34
|
+
msg += "\nNote: To install missing dependencies use `pip install pastas-plugins[<plugin-name>]`"
|
|
35
|
+
print(msg)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# ruff: noqa: F401
|
|
2
|
+
from pastas_plugins.cross_correlation.cross_correlation import (
|
|
3
|
+
ccf,
|
|
4
|
+
fit_response,
|
|
5
|
+
prewhiten,
|
|
6
|
+
)
|
|
7
|
+
from pastas_plugins.cross_correlation.plots import plot_ccf_overview, plot_corr
|
|
8
|
+
from pastas_plugins.cross_correlation.version import __version__
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from typing import Tuple, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import pastas as ps
|
|
6
|
+
import scipy as sc
|
|
7
|
+
from statsmodels.tsa.ar_model import AutoReg
|
|
8
|
+
from statsmodels.tsa.arima.model import ARIMA
|
|
9
|
+
from statsmodels.tsa.filters.filtertools import convolution_filter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def ccf(
|
|
13
|
+
x: pd.Series,
|
|
14
|
+
y: pd.Series,
|
|
15
|
+
nlags: Union[int, None] = None,
|
|
16
|
+
adjusted: bool = True,
|
|
17
|
+
alpha: Union[float, None] = None,
|
|
18
|
+
) -> Union[pd.Series, pd.DataFrame]:
|
|
19
|
+
"""Cross-correlation of two time series.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
x : pd.Series
|
|
24
|
+
Time series
|
|
25
|
+
y : pd.Series
|
|
26
|
+
Time series, len(y) should be equal to len(x).
|
|
27
|
+
nlags : int or None, optional
|
|
28
|
+
Number of lags to return cross-correlations for, by default None which
|
|
29
|
+
uses nlags equal to len(x).
|
|
30
|
+
adjusted : bool, optional
|
|
31
|
+
If True, denominators for cross-correlation are len(x)-k, otherwise
|
|
32
|
+
len(x), by default True
|
|
33
|
+
alpha : float or None, optional
|
|
34
|
+
If a float between 0 and 1 is given, the confidence intervals for the
|
|
35
|
+
given level are returned in a DataFrame. For instance if alpha=0.05,
|
|
36
|
+
95% confidence intervals are returned where the standard deviation is
|
|
37
|
+
computed according to 1/sqrt(len(x)).
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
pandas Series or DataFrame
|
|
42
|
+
"""
|
|
43
|
+
# check if lengths are equal
|
|
44
|
+
assert len(x) == len(y), "Length of series x and y should be equal"
|
|
45
|
+
# check if series are equidistant
|
|
46
|
+
for series in (x, y):
|
|
47
|
+
if pd.infer_freq(series.index) is None:
|
|
48
|
+
msg = (
|
|
49
|
+
"The frequency of the index of time series %s could not be "
|
|
50
|
+
"inferred. Please provide a time series with a equidistant time step."
|
|
51
|
+
)
|
|
52
|
+
raise ValueError(msg % series.name)
|
|
53
|
+
|
|
54
|
+
n = len(x)
|
|
55
|
+
|
|
56
|
+
xbar = x - x.mean()
|
|
57
|
+
ybar = y - y.mean()
|
|
58
|
+
|
|
59
|
+
d = np.arange(n, 0, -1) if adjusted else n
|
|
60
|
+
cc = sc.signal.correlate(xbar, ybar, mode="full", method="fft")
|
|
61
|
+
cvf = cc[n - 1 :] / (np.std(x) * np.std(y) * d)
|
|
62
|
+
|
|
63
|
+
nlags = n if nlags is None else nlags
|
|
64
|
+
index = pd.Index(np.arange(nlags), name="Lags")
|
|
65
|
+
ret = cvf[:nlags]
|
|
66
|
+
|
|
67
|
+
if alpha is not None:
|
|
68
|
+
interval = sc.stats.norm.ppf(1.0 - alpha / 2.0) / np.sqrt(n)
|
|
69
|
+
crosscorr = pd.DataFrame(
|
|
70
|
+
data=np.vstack([ret, ret - interval, ret + interval]).T,
|
|
71
|
+
index=index,
|
|
72
|
+
columns=["Cross-correlation", f"CI {alpha / 2}", f"CI {1 - alpha / 2}"],
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
crosscorr = pd.Series(
|
|
76
|
+
data=ret,
|
|
77
|
+
index=index,
|
|
78
|
+
name="Cross-correlation",
|
|
79
|
+
)
|
|
80
|
+
return crosscorr
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def prewhiten(
|
|
84
|
+
x: pd.Series, y: Union[pd.Series, None] = None, ar: int = 20, arima: bool = False
|
|
85
|
+
) -> Union[pd.Series, Tuple[pd.Series]]:
|
|
86
|
+
"""Prewhiten time series using AR(ar) model.
|
|
87
|
+
|
|
88
|
+
An AR(ar) model is fitted on time series x. The goal is to obtain residuals that
|
|
89
|
+
adhere to a white noise process. Next, the AR(ar) model is applied to time series Y.
|
|
90
|
+
|
|
91
|
+
Note
|
|
92
|
+
----
|
|
93
|
+
If prewhitened time series for x still shows significant autocorrelation or partial
|
|
94
|
+
autocorrelation, try increasing the number of autoregressive parameters.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
x : pd.Series
|
|
99
|
+
time series on which AR(ar) model will be fitted
|
|
100
|
+
y : pd.Series, optional
|
|
101
|
+
time series that will be filtered using the AR(ar) model fitted on x
|
|
102
|
+
ar : int, optional
|
|
103
|
+
number of autoregressive parameters (sometimes called `p`), by default 20
|
|
104
|
+
arima: bool, optional
|
|
105
|
+
use an ARIMA(ar,0,0) model instead of an AR(ar) model, by default False
|
|
106
|
+
which causes a significant speedup at the cost of a very small accuracy
|
|
107
|
+
penalty
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
pwx : pd.Series
|
|
112
|
+
prewhitened time series for x (should no longer show significant
|
|
113
|
+
autocorrelation or partial autocorrelation)
|
|
114
|
+
pwy : pd.Series, optional
|
|
115
|
+
prewhitened time series for y, if y is provided
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
# fit AR model on x
|
|
119
|
+
if arima:
|
|
120
|
+
ml = ARIMA(x.values, order=(ar, 0, 0), trend="c").fit()
|
|
121
|
+
else:
|
|
122
|
+
ml = AutoReg(x.values, lags=ar, trend="c").fit()
|
|
123
|
+
|
|
124
|
+
# get model filtered model residuals
|
|
125
|
+
residuals = ml.resid[ar:] if arima else ml.resid
|
|
126
|
+
pwx = pd.Series(residuals, index=x.index[ar:])
|
|
127
|
+
|
|
128
|
+
if y is not None:
|
|
129
|
+
# apply same filter on y
|
|
130
|
+
arparams = ml.arparams if arima else ml.params[1:]
|
|
131
|
+
filt = np.append(1.0, -arparams)
|
|
132
|
+
pwy = convolution_filter(y.values, filt=filt, nsides=1)
|
|
133
|
+
pwy = pd.Series(data=pwy[ar:], index=y.index[ar:])
|
|
134
|
+
return pwx, pwy
|
|
135
|
+
else:
|
|
136
|
+
return pwx
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def fit_response(
|
|
140
|
+
ccf: pd.Series,
|
|
141
|
+
rfunc: ps.typing.RFunc,
|
|
142
|
+
scale_factor: float = 1.0,
|
|
143
|
+
dt: float = 1.0,
|
|
144
|
+
) -> np.ndarray[float]:
|
|
145
|
+
"""Fit the response function to the cross-correlation function using least
|
|
146
|
+
squares optimization.
|
|
147
|
+
|
|
148
|
+
Parameters:
|
|
149
|
+
-----------
|
|
150
|
+
ccf : pd.Series
|
|
151
|
+
The cross-correlation function.
|
|
152
|
+
rfunc : ps.typing.RFunc
|
|
153
|
+
The response function to fit on the impulse response.
|
|
154
|
+
scale_factor : float, optional
|
|
155
|
+
Scale factor applied to the cross-correlation function to obtain the
|
|
156
|
+
impulse response, by default 1.0.
|
|
157
|
+
dt : float, optional
|
|
158
|
+
Time step of the response function, by default 1.0.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
--------
|
|
162
|
+
np.ndarray[float]
|
|
163
|
+
The optimized parameters of the response function.
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def obj_func(p):
|
|
168
|
+
"""Objective function for least squares optimization."""
|
|
169
|
+
impulse_response = (ccf * scale_factor).values
|
|
170
|
+
blockr = rfunc.block(p, dt=dt, cutoff=rfunc.cutoff)
|
|
171
|
+
|
|
172
|
+
# make sure length of residuals is constant
|
|
173
|
+
if len(blockr) > len(impulse_response):
|
|
174
|
+
blockr = blockr[: len(impulse_response)]
|
|
175
|
+
elif len(blockr) < len(impulse_response):
|
|
176
|
+
blockr = np.append(blockr, np.zeros(len(impulse_response) - len(blockr)))
|
|
177
|
+
|
|
178
|
+
return impulse_response - blockr
|
|
179
|
+
|
|
180
|
+
params = rfunc.get_init_parameters(rfunc._name)
|
|
181
|
+
pini = params["initial"].values
|
|
182
|
+
bounds = (
|
|
183
|
+
params["pmin"].fillna(-np.inf).values,
|
|
184
|
+
params["pmax"].fillna(np.inf).values,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
res = sc.optimize.least_squares(obj_func, x0=pini, bounds=bounds)
|
|
188
|
+
return res.x
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from statsmodels.graphics.tsaplots import _plot_corr, plot_acf, plot_pacf
|
|
5
|
+
|
|
6
|
+
from pastas_plugins.cross_correlation.cross_correlation import ccf
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def plot_corr(
|
|
10
|
+
corr: pd.Series | pd.DataFrame,
|
|
11
|
+
ax: plt.Axes | None = None,
|
|
12
|
+
vlines_kwargs: dict | None = None,
|
|
13
|
+
**kwargs,
|
|
14
|
+
):
|
|
15
|
+
"""Helper function for the statsmodels _plot_corr function.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
corr : pd.Series or pd.DataFrame
|
|
20
|
+
the correlation result to plot
|
|
21
|
+
ax : plt.Axes, optional
|
|
22
|
+
axes to plot on, by default None
|
|
23
|
+
vlines_kwargs : dict, optional
|
|
24
|
+
keyword arguments for the vlines function, by default None
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
plt.Axes
|
|
29
|
+
axes with the plot
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
if ax is None:
|
|
33
|
+
_, ax = plt.subplots(**kwargs)
|
|
34
|
+
|
|
35
|
+
acf_x = corr.values if isinstance(corr, pd.Series) else corr.iloc[:, 0].values
|
|
36
|
+
confint = corr.iloc[:, 1:3].values if isinstance(corr, pd.DataFrame) else None
|
|
37
|
+
lags = corr.index.values
|
|
38
|
+
vlines_kwargs = {} if vlines_kwargs is None else vlines_kwargs
|
|
39
|
+
_plot_corr(
|
|
40
|
+
ax=ax,
|
|
41
|
+
title="",
|
|
42
|
+
acf_x=acf_x,
|
|
43
|
+
confint=confint,
|
|
44
|
+
lags=lags,
|
|
45
|
+
irregular=False,
|
|
46
|
+
use_vlines=True,
|
|
47
|
+
vlines_kwargs=vlines_kwargs,
|
|
48
|
+
)
|
|
49
|
+
return ax
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def plot_ccf_overview(x, y, nlags=None, tmin=None, tmax=None, axes=None):
|
|
53
|
+
"""Plot an overview of the cross-correlation between two time series.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
x : pd.Series
|
|
58
|
+
Time series 1
|
|
59
|
+
y : pd.Series
|
|
60
|
+
Time series 2
|
|
61
|
+
nlags : int, optional
|
|
62
|
+
number of lags to return cross-correlations for, by default None which
|
|
63
|
+
uses number of lags equal to len(x).
|
|
64
|
+
tmin : str or pd.Timestamp, optional
|
|
65
|
+
tmin for both time series, by default None
|
|
66
|
+
tmax : str or pd.Timestamp, optional
|
|
67
|
+
tmax for both time series, by default None
|
|
68
|
+
axes : Axes mosaic, optional
|
|
69
|
+
if provided, use axes from previous plot
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
axes : Axes mosaic
|
|
74
|
+
return axes of subplots mosaic
|
|
75
|
+
"""
|
|
76
|
+
if tmin is None:
|
|
77
|
+
tmin = np.min([x.index[0], y.index[0]])
|
|
78
|
+
if tmax is None:
|
|
79
|
+
tmax = np.max([x.index[-1], y.index[-1]])
|
|
80
|
+
|
|
81
|
+
x = x.loc[tmin:tmax]
|
|
82
|
+
y = y.loc[tmin:tmax]
|
|
83
|
+
|
|
84
|
+
if axes is None:
|
|
85
|
+
mosaic = [
|
|
86
|
+
["x", "x", "norm", "norm"],
|
|
87
|
+
["y", "y", "norm", "norm"],
|
|
88
|
+
["x-acf", "y-acf", "ccf", "ccf"],
|
|
89
|
+
["x-pacf", "y-pacf", "ccf", "ccf"],
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
fig, axes = plt.subplot_mosaic(mosaic, figsize=(16, 8))
|
|
93
|
+
rescale_axes = False
|
|
94
|
+
newaxes = True
|
|
95
|
+
color1 = "C0"
|
|
96
|
+
color2 = "C1"
|
|
97
|
+
else:
|
|
98
|
+
fig = axes["x"].figure
|
|
99
|
+
rescale_axes = True
|
|
100
|
+
newaxes = False
|
|
101
|
+
color1 = "C2"
|
|
102
|
+
color2 = "C3"
|
|
103
|
+
|
|
104
|
+
# set names if not provided
|
|
105
|
+
if x.name is None:
|
|
106
|
+
x.name = "x"
|
|
107
|
+
if y.name is None:
|
|
108
|
+
y.name = "y"
|
|
109
|
+
|
|
110
|
+
# plot time series
|
|
111
|
+
axes["x"].plot(x.index, x, label=x.name, color=color1)
|
|
112
|
+
axes["x"].legend(loc=(0, 1), frameon=False)
|
|
113
|
+
axes["x"].set_ylabel("x")
|
|
114
|
+
axes["x"].set_xlim(pd.Timestamp(tmin), pd.Timestamp(tmax))
|
|
115
|
+
axes["y"].plot(y.index, y, label=y.name, c=color2)
|
|
116
|
+
axes["y"].legend(loc=(0, 1), frameon=False)
|
|
117
|
+
axes["y"].set_ylabel("y")
|
|
118
|
+
axes["y"].set_xlim(pd.Timestamp(tmin), pd.Timestamp(tmax))
|
|
119
|
+
|
|
120
|
+
# plot normalized series
|
|
121
|
+
xnorm = (x - x.mean()) / x.std()
|
|
122
|
+
ynorm = (y - y.mean()) / y.std()
|
|
123
|
+
axes["norm"].plot(
|
|
124
|
+
xnorm.index, xnorm, label=x.name + " (normalized)", alpha=0.7, color=color1
|
|
125
|
+
)
|
|
126
|
+
axes["norm"].plot(
|
|
127
|
+
ynorm.index, ynorm, label=y.name + " (normalized)", alpha=0.7, color=color2
|
|
128
|
+
)
|
|
129
|
+
axes["norm"].legend(loc=(0, 1), frameon=False, ncol=2)
|
|
130
|
+
axes["norm"].set_ylabel("normalized [-]")
|
|
131
|
+
axes["norm"].set_xlim(pd.Timestamp(tmin), pd.Timestamp(tmax))
|
|
132
|
+
handles, _ = axes["norm"].get_legend_handles_labels()
|
|
133
|
+
|
|
134
|
+
# plot acf, pacf
|
|
135
|
+
plot_acf(
|
|
136
|
+
xnorm,
|
|
137
|
+
ax=axes["x-acf"],
|
|
138
|
+
color=color1,
|
|
139
|
+
alpha=0.05,
|
|
140
|
+
title="",
|
|
141
|
+
zero=False,
|
|
142
|
+
auto_ylims=True,
|
|
143
|
+
vlines_kwargs={"color": "k"},
|
|
144
|
+
)
|
|
145
|
+
plot_acf(
|
|
146
|
+
ynorm,
|
|
147
|
+
ax=axes["y-acf"],
|
|
148
|
+
color=color2,
|
|
149
|
+
alpha=0.05,
|
|
150
|
+
title="",
|
|
151
|
+
zero=False,
|
|
152
|
+
auto_ylims=True,
|
|
153
|
+
vlines_kwargs={"color": "k"},
|
|
154
|
+
)
|
|
155
|
+
axes["x-acf"].set_xlim(left=0.0)
|
|
156
|
+
axes["x-acf"].set_ylabel("ACF [-]")
|
|
157
|
+
(p1,) = axes["x-acf"].plot([], [], marker="o", ls="none", color=color1)
|
|
158
|
+
if not newaxes:
|
|
159
|
+
leg = axes["x-acf"].get_legend()
|
|
160
|
+
handles = leg.legend_handles
|
|
161
|
+
labels = [t.get_text() for t in leg.get_texts()]
|
|
162
|
+
handles += [p1]
|
|
163
|
+
labels += [x.name]
|
|
164
|
+
else:
|
|
165
|
+
handles = [p1]
|
|
166
|
+
labels = [x.name]
|
|
167
|
+
|
|
168
|
+
axes["x-acf"].legend(handles, labels, loc=(0, 1), frameon=False)
|
|
169
|
+
axes["y-acf"].set_xlim(left=0.0)
|
|
170
|
+
axes["y-acf"].get_children()[3].set_facecolor(color2)
|
|
171
|
+
(p2,) = axes["y-acf"].plot([], [], marker="o", ls="none", color=color2)
|
|
172
|
+
if not newaxes:
|
|
173
|
+
leg = axes["y-acf"].get_legend()
|
|
174
|
+
handles = leg.legend_handles
|
|
175
|
+
labels = [t.get_text() for t in leg.get_texts()]
|
|
176
|
+
handles += [p2]
|
|
177
|
+
labels += [y.name]
|
|
178
|
+
else:
|
|
179
|
+
handles = [p2]
|
|
180
|
+
labels = [y.name]
|
|
181
|
+
axes["y-acf"].legend(handles, labels, loc=(0, 1), frameon=False)
|
|
182
|
+
|
|
183
|
+
plot_pacf(
|
|
184
|
+
xnorm,
|
|
185
|
+
method="ywm",
|
|
186
|
+
ax=axes["x-pacf"],
|
|
187
|
+
color=color1,
|
|
188
|
+
alpha=0.05,
|
|
189
|
+
title="",
|
|
190
|
+
zero=False,
|
|
191
|
+
auto_ylims=True,
|
|
192
|
+
vlines_kwargs={"color": "k"},
|
|
193
|
+
)
|
|
194
|
+
plot_pacf(
|
|
195
|
+
ynorm,
|
|
196
|
+
method="ywm",
|
|
197
|
+
ax=axes["y-pacf"],
|
|
198
|
+
color=color2,
|
|
199
|
+
alpha=0.05,
|
|
200
|
+
title="",
|
|
201
|
+
zero=False,
|
|
202
|
+
auto_ylims=True,
|
|
203
|
+
vlines_kwargs={"color": "k"},
|
|
204
|
+
)
|
|
205
|
+
axes["x-pacf"].set_xlim(left=0.0)
|
|
206
|
+
axes["x-pacf"].set_ylabel("PACF [-]")
|
|
207
|
+
axes["x-pacf"].set_xlabel("Lags")
|
|
208
|
+
|
|
209
|
+
axes["y-pacf"].set_xlim(left=0.0)
|
|
210
|
+
axes["y-pacf"].get_children()[3].set_facecolor("C1")
|
|
211
|
+
axes["y-pacf"].set_xlabel("Lags")
|
|
212
|
+
|
|
213
|
+
# ccf
|
|
214
|
+
cc = ccf(x, y, nlags=nlags)
|
|
215
|
+
axes["ccf"].bar(
|
|
216
|
+
cc.index,
|
|
217
|
+
cc,
|
|
218
|
+
width=1.0,
|
|
219
|
+
linewidth=0.5,
|
|
220
|
+
alpha=0.5,
|
|
221
|
+
label=f"CCF ({x.name}|{y.name})",
|
|
222
|
+
)
|
|
223
|
+
axes["ccf"].set_ylabel("CCF [-]")
|
|
224
|
+
axes["ccf"].set_xlabel("Lags")
|
|
225
|
+
axes["ccf"].legend(loc=(0, 1), frameon=False)
|
|
226
|
+
axes["ccf"].set_xlim(left=0.0)
|
|
227
|
+
|
|
228
|
+
share_x = [axes["x"], axes["y"], axes["norm"]]
|
|
229
|
+
for i, iax in enumerate(share_x):
|
|
230
|
+
if i < (len(share_x) - 1):
|
|
231
|
+
iax.sharex(share_x[-1])
|
|
232
|
+
|
|
233
|
+
# share_x = [axes["x-acf"], axes["x-pacf"], axes["y-acf"], axes["y-pacf"]]
|
|
234
|
+
# for i, iax in enumerate(share_x):
|
|
235
|
+
# if i < (len(share_x) - 1):
|
|
236
|
+
# iax.sharex(share_x[-1])
|
|
237
|
+
|
|
238
|
+
fig.tight_layout()
|
|
239
|
+
fig.align_ylabels()
|
|
240
|
+
|
|
241
|
+
if rescale_axes:
|
|
242
|
+
for iax in axes.values():
|
|
243
|
+
iax.autoscale()
|
|
244
|
+
|
|
245
|
+
return axes
|