nsevt 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.
- nsevt-0.1.0/LICENSE +21 -0
- nsevt-0.1.0/PKG-INFO +139 -0
- nsevt-0.1.0/README.md +100 -0
- nsevt-0.1.0/pyproject.toml +45 -0
- nsevt-0.1.0/setup.cfg +4 -0
- nsevt-0.1.0/src/nsevt/__init__.py +38 -0
- nsevt-0.1.0/src/nsevt/conformal.py +138 -0
- nsevt-0.1.0/src/nsevt/gpd.py +195 -0
- nsevt-0.1.0/src/nsevt/transportability.py +132 -0
- nsevt-0.1.0/src/nsevt/trend.py +194 -0
- nsevt-0.1.0/src/nsevt/twoscale.py +131 -0
- nsevt-0.1.0/src/nsevt.egg-info/PKG-INFO +139 -0
- nsevt-0.1.0/src/nsevt.egg-info/SOURCES.txt +17 -0
- nsevt-0.1.0/src/nsevt.egg-info/dependency_links.txt +1 -0
- nsevt-0.1.0/src/nsevt.egg-info/requires.txt +15 -0
- nsevt-0.1.0/src/nsevt.egg-info/top_level.txt +1 -0
- nsevt-0.1.0/tests/test_conformal_twoscale_arena.py +71 -0
- nsevt-0.1.0/tests/test_gpd.py +49 -0
- nsevt-0.1.0/tests/test_trend.py +45 -0
nsevt-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Elí Gaiska Salomón Guzmán
|
|
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.
|
nsevt-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nsevt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Non-stationary extreme-value tail risk with honest uncertainty: permutation-calibrated trend tests, power/MDE, multi-source transportability, and block-conformal prediction under temporal dependence.
|
|
5
|
+
Author-email: Elí Gaiska Salomón Guzmán <salomon.eli@colpos.mx>
|
|
6
|
+
Maintainer-email: Elí Gaiska Salomón Guzmán <salomon.eli@colpos.mx>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/GaiskaSalomon/nsevt
|
|
9
|
+
Project-URL: Repository, https://github.com/GaiskaSalomon/nsevt
|
|
10
|
+
Project-URL: Issues, https://github.com/GaiskaSalomon/nsevt/issues
|
|
11
|
+
Keywords: extreme value theory,generalized Pareto,non-stationarity,permutation test,conformal prediction,Wasserstein,climate extremes
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: numpy>=1.21
|
|
27
|
+
Requires-Dist: scipy>=1.7
|
|
28
|
+
Provides-Extra: demo
|
|
29
|
+
Requires-Dist: streamlit>=1.30; extra == "demo"
|
|
30
|
+
Requires-Dist: pandas>=1.3; extra == "demo"
|
|
31
|
+
Requires-Dist: matplotlib>=3.5; extra == "demo"
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
36
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# nsevt — non-stationary extreme-value tail risk with honest uncertainty
|
|
41
|
+
|
|
42
|
+
[](https://github.com/GaiskaSalomon/nsevt/actions/workflows/ci.yml)
|
|
43
|
+
[](https://pypi.org/project/nsevt/)
|
|
44
|
+
[](https://pypi.org/project/nsevt/)
|
|
45
|
+
[](LICENSE)
|
|
46
|
+
|
|
47
|
+
`nsevt` is a small, dependency-light Python package (NumPy + SciPy) for the
|
|
48
|
+
**honest** analysis of trends in environmental extremes. It packages a workflow
|
|
49
|
+
that existing EVT tools (`extRemes`, `ismev`, `POT`, `pyextremes`, `texmex`) do
|
|
50
|
+
not offer as a single, tested pipeline:
|
|
51
|
+
|
|
52
|
+
1. **Bounded-tail detection** — a peaks-over-threshold GPD fit with a
|
|
53
|
+
profile-likelihood interval for the shape and a bootstrap of the finite upper
|
|
54
|
+
endpoint (`ξ<0` ⇒ a finite physical ceiling).
|
|
55
|
+
2. **A permutation-calibrated trend test** on the tail scale — exact in finite
|
|
56
|
+
samples, avoiding the unreliable asymptotic χ² for a boundary-adjacent
|
|
57
|
+
parameter on a few hundred exceedances.
|
|
58
|
+
3. **Power / minimum-detectable-effect** — turns any non-rejection into a
|
|
59
|
+
quantitative statement of what the record can resolve.
|
|
60
|
+
4. **Multi-source transportability** ("evidence arena") — does an apparent trend
|
|
61
|
+
*survive* changing the data source, or is it an instrumental artifact?
|
|
62
|
+
5. **Block-conformal prediction bands** — distribution-free coverage for extreme
|
|
63
|
+
quantiles **under temporal dependence**.
|
|
64
|
+
6. **A two-scale Wasserstein trend test** for a series of distributions each
|
|
65
|
+
estimated from a small per-period sample, with an exact
|
|
66
|
+
location/scale/shape energy decomposition.
|
|
67
|
+
|
|
68
|
+
The numerics of the GPD and permutation machinery are ported verbatim from the
|
|
69
|
+
frozen, unit-tested research code, so results are reproducible and identical.
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install -e . # from this directory
|
|
75
|
+
pip install -e ".[demo]" # also install the Streamlit demo dependencies
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Quick start
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import numpy as np, nsevt
|
|
82
|
+
|
|
83
|
+
rng = np.random.default_rng(0)
|
|
84
|
+
# 500 observations; tail excesses above u=40 are bounded (xi<0)
|
|
85
|
+
x = 40 + rng.gamma(2.0, 8.0, size=500)
|
|
86
|
+
|
|
87
|
+
fit = nsevt.gpd_pot(x, threshold=40)
|
|
88
|
+
print(fit.summary())
|
|
89
|
+
print("1-in-100 return level:", fit.return_level(100, rate=(x > 40).mean()))
|
|
90
|
+
|
|
91
|
+
# is there a trend in the tail scale? (block = year label per observation)
|
|
92
|
+
year = rng.integers(1980, 2024, size=500)
|
|
93
|
+
tr = nsevt.trend_permutation(x[x > 40] - 40, year[x > 40])
|
|
94
|
+
print("trend/decade:", round(tr["trend_per_decade"], 3),
|
|
95
|
+
"p_perm:", tr["p_permutation"])
|
|
96
|
+
|
|
97
|
+
# what can the record resolve?
|
|
98
|
+
mde = nsevt.min_detectable_effect(x[x > 40] - 40, year[x > 40])
|
|
99
|
+
print("MDE per decade:", mde["mde_per_decade"])
|
|
100
|
+
|
|
101
|
+
# distribution-free 90% prediction band under dependence
|
|
102
|
+
band = nsevt.block_conformal(x, threshold=40, alpha=0.10)
|
|
103
|
+
print("upper bound at sigma:", band.predict_upper(fit.sigma))
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## The multi-source arena
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
arena = nsevt.transportability(
|
|
110
|
+
[("operational", x_op, year_op),
|
|
111
|
+
("independent", x_ind, year_ind),
|
|
112
|
+
("homogenized", x_homog, year_homog)],
|
|
113
|
+
threshold=40)
|
|
114
|
+
print(arena.table())
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The verdict distinguishes a **robust bounded tail** (shape reproduces in every
|
|
118
|
+
source) from an **apparent trend that does not survive** the change of source —
|
|
119
|
+
the core methodological contribution of the accompanying research.
|
|
120
|
+
|
|
121
|
+
## Modules
|
|
122
|
+
|
|
123
|
+
| module | purpose |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `nsevt.gpd` | POT-GPD fit, profile CI for ξ, endpoint bootstrap, return levels |
|
|
126
|
+
| `nsevt.trend` | permutation trend test, power/MDE, block-bootstrap trend CI |
|
|
127
|
+
| `nsevt.transportability` | multi-source evidence arena |
|
|
128
|
+
| `nsevt.conformal` | block / split conformal prediction bands for tails |
|
|
129
|
+
| `nsevt.twoscale` | two-scale Wasserstein distributional trend test + energy split |
|
|
130
|
+
|
|
131
|
+
## Tests
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pip install ".[test]" && pytest
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT. See `LICENSE`.
|
nsevt-0.1.0/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# nsevt — non-stationary extreme-value tail risk with honest uncertainty
|
|
2
|
+
|
|
3
|
+
[](https://github.com/GaiskaSalomon/nsevt/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/nsevt/)
|
|
5
|
+
[](https://pypi.org/project/nsevt/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
`nsevt` is a small, dependency-light Python package (NumPy + SciPy) for the
|
|
9
|
+
**honest** analysis of trends in environmental extremes. It packages a workflow
|
|
10
|
+
that existing EVT tools (`extRemes`, `ismev`, `POT`, `pyextremes`, `texmex`) do
|
|
11
|
+
not offer as a single, tested pipeline:
|
|
12
|
+
|
|
13
|
+
1. **Bounded-tail detection** — a peaks-over-threshold GPD fit with a
|
|
14
|
+
profile-likelihood interval for the shape and a bootstrap of the finite upper
|
|
15
|
+
endpoint (`ξ<0` ⇒ a finite physical ceiling).
|
|
16
|
+
2. **A permutation-calibrated trend test** on the tail scale — exact in finite
|
|
17
|
+
samples, avoiding the unreliable asymptotic χ² for a boundary-adjacent
|
|
18
|
+
parameter on a few hundred exceedances.
|
|
19
|
+
3. **Power / minimum-detectable-effect** — turns any non-rejection into a
|
|
20
|
+
quantitative statement of what the record can resolve.
|
|
21
|
+
4. **Multi-source transportability** ("evidence arena") — does an apparent trend
|
|
22
|
+
*survive* changing the data source, or is it an instrumental artifact?
|
|
23
|
+
5. **Block-conformal prediction bands** — distribution-free coverage for extreme
|
|
24
|
+
quantiles **under temporal dependence**.
|
|
25
|
+
6. **A two-scale Wasserstein trend test** for a series of distributions each
|
|
26
|
+
estimated from a small per-period sample, with an exact
|
|
27
|
+
location/scale/shape energy decomposition.
|
|
28
|
+
|
|
29
|
+
The numerics of the GPD and permutation machinery are ported verbatim from the
|
|
30
|
+
frozen, unit-tested research code, so results are reproducible and identical.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e . # from this directory
|
|
36
|
+
pip install -e ".[demo]" # also install the Streamlit demo dependencies
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import numpy as np, nsevt
|
|
43
|
+
|
|
44
|
+
rng = np.random.default_rng(0)
|
|
45
|
+
# 500 observations; tail excesses above u=40 are bounded (xi<0)
|
|
46
|
+
x = 40 + rng.gamma(2.0, 8.0, size=500)
|
|
47
|
+
|
|
48
|
+
fit = nsevt.gpd_pot(x, threshold=40)
|
|
49
|
+
print(fit.summary())
|
|
50
|
+
print("1-in-100 return level:", fit.return_level(100, rate=(x > 40).mean()))
|
|
51
|
+
|
|
52
|
+
# is there a trend in the tail scale? (block = year label per observation)
|
|
53
|
+
year = rng.integers(1980, 2024, size=500)
|
|
54
|
+
tr = nsevt.trend_permutation(x[x > 40] - 40, year[x > 40])
|
|
55
|
+
print("trend/decade:", round(tr["trend_per_decade"], 3),
|
|
56
|
+
"p_perm:", tr["p_permutation"])
|
|
57
|
+
|
|
58
|
+
# what can the record resolve?
|
|
59
|
+
mde = nsevt.min_detectable_effect(x[x > 40] - 40, year[x > 40])
|
|
60
|
+
print("MDE per decade:", mde["mde_per_decade"])
|
|
61
|
+
|
|
62
|
+
# distribution-free 90% prediction band under dependence
|
|
63
|
+
band = nsevt.block_conformal(x, threshold=40, alpha=0.10)
|
|
64
|
+
print("upper bound at sigma:", band.predict_upper(fit.sigma))
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## The multi-source arena
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
arena = nsevt.transportability(
|
|
71
|
+
[("operational", x_op, year_op),
|
|
72
|
+
("independent", x_ind, year_ind),
|
|
73
|
+
("homogenized", x_homog, year_homog)],
|
|
74
|
+
threshold=40)
|
|
75
|
+
print(arena.table())
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The verdict distinguishes a **robust bounded tail** (shape reproduces in every
|
|
79
|
+
source) from an **apparent trend that does not survive** the change of source —
|
|
80
|
+
the core methodological contribution of the accompanying research.
|
|
81
|
+
|
|
82
|
+
## Modules
|
|
83
|
+
|
|
84
|
+
| module | purpose |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `nsevt.gpd` | POT-GPD fit, profile CI for ξ, endpoint bootstrap, return levels |
|
|
87
|
+
| `nsevt.trend` | permutation trend test, power/MDE, block-bootstrap trend CI |
|
|
88
|
+
| `nsevt.transportability` | multi-source evidence arena |
|
|
89
|
+
| `nsevt.conformal` | block / split conformal prediction bands for tails |
|
|
90
|
+
| `nsevt.twoscale` | two-scale Wasserstein distributional trend test + energy split |
|
|
91
|
+
|
|
92
|
+
## Tests
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install ".[test]" && pytest
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nsevt"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Non-stationary extreme-value tail risk with honest uncertainty: permutation-calibrated trend tests, power/MDE, multi-source transportability, and block-conformal prediction under temporal dependence."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Elí Gaiska Salomón Guzmán", email = "salomon.eli@colpos.mx" }]
|
|
13
|
+
maintainers = [{ name = "Elí Gaiska Salomón Guzmán", email = "salomon.eli@colpos.mx" }]
|
|
14
|
+
keywords = ["extreme value theory", "generalized Pareto", "non-stationarity",
|
|
15
|
+
"permutation test", "conformal prediction", "Wasserstein", "climate extremes"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Atmospheric Science",
|
|
28
|
+
]
|
|
29
|
+
dependencies = ["numpy>=1.21", "scipy>=1.7"]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
demo = ["streamlit>=1.30", "pandas>=1.3", "matplotlib>=3.5"]
|
|
33
|
+
test = ["pytest>=7.0"]
|
|
34
|
+
dev = ["build>=1.0", "twine>=5.0", "pytest>=7.0"]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/GaiskaSalomon/nsevt"
|
|
38
|
+
Repository = "https://github.com/GaiskaSalomon/nsevt"
|
|
39
|
+
Issues = "https://github.com/GaiskaSalomon/nsevt/issues"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
nsevt-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""nsevt: non-stationary extreme-value tail risk with honest uncertainty.
|
|
2
|
+
|
|
3
|
+
A small, dependency-light toolkit for the *honest* analysis of trends in
|
|
4
|
+
environmental extremes:
|
|
5
|
+
|
|
6
|
+
* :func:`gpd_pot` -- peaks-over-threshold GPD fit with a profile-likelihood
|
|
7
|
+
interval for the shape and a bootstrap of the finite upper endpoint;
|
|
8
|
+
* :func:`trend_permutation` / :func:`min_detectable_effect` -- a
|
|
9
|
+
permutation-calibrated test of a tail-scale trend and a Monte-Carlo
|
|
10
|
+
power/MDE analysis (what can this record actually resolve?);
|
|
11
|
+
* :func:`transportability` -- the multi-source "evidence arena": does a trend
|
|
12
|
+
survive changing the data source, or is it an artifact?
|
|
13
|
+
* :func:`block_conformal` -- distribution-free prediction bands for extreme
|
|
14
|
+
quantiles with coverage guarantees under temporal dependence;
|
|
15
|
+
* :func:`twoscale_trend` -- a Wasserstein/Frechet-mean trend test for a series
|
|
16
|
+
of distributions estimated from small per-period samples.
|
|
17
|
+
|
|
18
|
+
The numerics of the GPD and permutation machinery are ported verbatim from the
|
|
19
|
+
frozen, unit-tested code of the accompanying research, so results are identical.
|
|
20
|
+
"""
|
|
21
|
+
from .gpd import fit_gpd, profile_ci_xi, upper_endpoint, gpd_pot, GPDFit
|
|
22
|
+
from .trend import (trend_permutation, trend_power, min_detectable_effect,
|
|
23
|
+
block_bootstrap_trend_ci)
|
|
24
|
+
from .transportability import transportability, ArenaResult, SourceResult
|
|
25
|
+
from .conformal import block_conformal, split_conformal, ConformalBand
|
|
26
|
+
from .twoscale import twoscale_trend, wasserstein_decomposition, TwoScaleResult
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"fit_gpd", "profile_ci_xi", "upper_endpoint", "gpd_pot", "GPDFit",
|
|
32
|
+
"trend_permutation", "trend_power", "min_detectable_effect",
|
|
33
|
+
"block_bootstrap_trend_ci",
|
|
34
|
+
"transportability", "ArenaResult", "SourceResult",
|
|
35
|
+
"block_conformal", "split_conformal", "ConformalBand",
|
|
36
|
+
"twoscale_trend", "wasserstein_decomposition", "TwoScaleResult",
|
|
37
|
+
"__version__",
|
|
38
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Block-conformal prediction bands for non-stationary extreme quantiles.
|
|
2
|
+
|
|
3
|
+
Classical split-conformal prediction gives finite-sample coverage under
|
|
4
|
+
*exchangeability*, which temporally dependent extremes violate. Block conformal
|
|
5
|
+
calibrates over contiguous temporal **blocks** treated as the approximately
|
|
6
|
+
exchangeable units, yielding
|
|
7
|
+
|
|
8
|
+
P(X in C_alpha) >= 1 - alpha - o(1)
|
|
9
|
+
|
|
10
|
+
under beta-mixing, with the o(1) term controlled by the block length. The
|
|
11
|
+
non-conformity score is the **standardized excess** ``s = (x - u)/sigma``: by the
|
|
12
|
+
pivotal property of the GPD, dividing by the (possibly covariate-dependent)
|
|
13
|
+
scale removes the covariate/non-stationarity, leaving a common score law across
|
|
14
|
+
covariate values.
|
|
15
|
+
|
|
16
|
+
Implements the method of the block-conformal-EVT manuscript.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Optional, Sequence
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
from .gpd import fit_gpd
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ConformalBand:
|
|
30
|
+
"""A one-sided upper conformal prediction band for a tail."""
|
|
31
|
+
threshold: float
|
|
32
|
+
alpha: float
|
|
33
|
+
q_standardized: float
|
|
34
|
+
method: str
|
|
35
|
+
n_blocks: int
|
|
36
|
+
block_length: int
|
|
37
|
+
|
|
38
|
+
def predict_upper(self, scale: float) -> float:
|
|
39
|
+
"""Upper prediction bound ``u + q * sigma`` at scale ``sigma``."""
|
|
40
|
+
return float(self.threshold + self.q_standardized * scale)
|
|
41
|
+
|
|
42
|
+
def coverage(self, x_new: Sequence[float], scale) -> float:
|
|
43
|
+
"""Empirical coverage of the band on new exceedances ``x_new``.
|
|
44
|
+
|
|
45
|
+
``scale`` is a scalar or per-observation array of ``sigma`` values used
|
|
46
|
+
to build the band for each new point.
|
|
47
|
+
"""
|
|
48
|
+
x_new = np.asarray(x_new, float)
|
|
49
|
+
scale = np.broadcast_to(np.asarray(scale, float), x_new.shape)
|
|
50
|
+
upper = self.threshold + self.q_standardized * scale
|
|
51
|
+
return float(np.mean(x_new <= upper))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _blocks(n: int, block_length: Optional[int], n_blocks: Optional[int]):
|
|
55
|
+
if block_length is None and n_blocks is None:
|
|
56
|
+
block_length = max(2, int(round(n ** 0.5))) # ell ~ sqrt(T)
|
|
57
|
+
if block_length is None:
|
|
58
|
+
block_length = max(2, n // int(n_blocks))
|
|
59
|
+
edges = list(range(0, n, block_length))
|
|
60
|
+
return [(edges[i], min(edges[i] + block_length, n)) for i in range(len(edges))]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def block_conformal(x: Sequence[float], threshold: float, alpha: float = 0.1,
|
|
64
|
+
scale=None, order: Optional[Sequence] = None,
|
|
65
|
+
block_length: Optional[int] = None,
|
|
66
|
+
n_blocks: Optional[int] = None) -> ConformalBand:
|
|
67
|
+
"""One-sided block-conformal upper band for peaks over ``threshold``.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
x : array
|
|
72
|
+
Raw observations; only ``x > threshold`` are used as calibration
|
|
73
|
+
exceedances.
|
|
74
|
+
threshold : float
|
|
75
|
+
Tail threshold ``u``.
|
|
76
|
+
alpha : float
|
|
77
|
+
Miscoverage level; the band targets coverage ``>= 1 - alpha``.
|
|
78
|
+
scale : float or array, optional
|
|
79
|
+
Per-observation GPD scale ``sigma`` used to standardize excesses. If
|
|
80
|
+
``None``, a constant scale is estimated by GPD MLE on the exceedances.
|
|
81
|
+
For non-stationary/covariate-dependent tails, pass ``sigma(z_i)``.
|
|
82
|
+
order : array, optional
|
|
83
|
+
Temporal ordering key for the exceedances (e.g. time); blocks are formed
|
|
84
|
+
on the sorted order. If ``None``, input order is assumed temporal.
|
|
85
|
+
block_length, n_blocks : int, optional
|
|
86
|
+
Block size / count. Default block length ``~ sqrt(#exceedances)``.
|
|
87
|
+
"""
|
|
88
|
+
x = np.asarray(x, float)
|
|
89
|
+
mask = x > threshold
|
|
90
|
+
z = x[mask] - threshold
|
|
91
|
+
if z.size < 4:
|
|
92
|
+
raise ValueError(f"only {z.size} exceedances; need >= 4 for calibration")
|
|
93
|
+
if scale is None:
|
|
94
|
+
sigma = fit_gpd(z)["sigma"]
|
|
95
|
+
s = z / sigma
|
|
96
|
+
else:
|
|
97
|
+
scale = np.asarray(scale, float)
|
|
98
|
+
sc = scale[mask] if scale.shape == x.shape else scale
|
|
99
|
+
s = z / sc
|
|
100
|
+
if order is not None:
|
|
101
|
+
order = np.asarray(order)[mask] if np.asarray(order).shape == x.shape else np.asarray(order)
|
|
102
|
+
s = s[np.argsort(order, kind="stable")]
|
|
103
|
+
|
|
104
|
+
blocks = _blocks(len(s), block_length, n_blocks)
|
|
105
|
+
# per-block aggregate score: within-block (1-alpha) quantile
|
|
106
|
+
agg = np.array([np.quantile(s[a:b], 1 - alpha) for a, b in blocks if b > a])
|
|
107
|
+
K = len(agg)
|
|
108
|
+
if K < 2:
|
|
109
|
+
raise ValueError("need >= 2 calibration blocks; reduce block_length")
|
|
110
|
+
# finite-sample conformal quantile of the block aggregates
|
|
111
|
+
lvl = min(1.0, np.ceil((1 - alpha) * (K + 1)) / K)
|
|
112
|
+
q = float(np.quantile(agg, lvl))
|
|
113
|
+
bl = blocks[0][1] - blocks[0][0]
|
|
114
|
+
return ConformalBand(threshold=float(threshold), alpha=float(alpha),
|
|
115
|
+
q_standardized=q, method="block-conformal",
|
|
116
|
+
n_blocks=K, block_length=int(bl))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def split_conformal(x: Sequence[float], threshold: float, alpha: float = 0.1,
|
|
120
|
+
scale=None) -> ConformalBand:
|
|
121
|
+
"""Marginal split-conformal upper band (ignores temporal dependence).
|
|
122
|
+
|
|
123
|
+
Provided as the baseline whose coverage the block version corrects under
|
|
124
|
+
dependence; use :func:`block_conformal` for dependent extremes.
|
|
125
|
+
"""
|
|
126
|
+
x = np.asarray(x, float)
|
|
127
|
+
mask = x > threshold
|
|
128
|
+
z = x[mask] - threshold
|
|
129
|
+
if z.size < 4:
|
|
130
|
+
raise ValueError(f"only {z.size} exceedances; need >= 4")
|
|
131
|
+
sigma = fit_gpd(z)["sigma"] if scale is None else None
|
|
132
|
+
s = z / (sigma if scale is None else np.asarray(scale, float)[mask])
|
|
133
|
+
n = len(s)
|
|
134
|
+
lvl = min(1.0, np.ceil((1 - alpha) * (n + 1)) / n)
|
|
135
|
+
q = float(np.quantile(s, lvl))
|
|
136
|
+
return ConformalBand(threshold=float(threshold), alpha=float(alpha),
|
|
137
|
+
q_standardized=q, method="split-conformal",
|
|
138
|
+
n_blocks=1, block_length=n)
|