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 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
+ [![CI](https://github.com/GaiskaSalomon/nsevt/actions/workflows/ci.yml/badge.svg)](https://github.com/GaiskaSalomon/nsevt/actions/workflows/ci.yml)
43
+ [![PyPI](https://img.shields.io/pypi/v/nsevt.svg)](https://pypi.org/project/nsevt/)
44
+ [![Python versions](https://img.shields.io/pypi/pyversions/nsevt.svg)](https://pypi.org/project/nsevt/)
45
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![CI](https://github.com/GaiskaSalomon/nsevt/actions/workflows/ci.yml/badge.svg)](https://github.com/GaiskaSalomon/nsevt/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/nsevt.svg)](https://pypi.org/project/nsevt/)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/nsevt.svg)](https://pypi.org/project/nsevt/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)