ppidest 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.
- ppidest-0.1.0/PKG-INFO +195 -0
- ppidest-0.1.0/README.md +174 -0
- ppidest-0.1.0/pyproject.toml +41 -0
- ppidest-0.1.0/pyproject.toml.orig +34 -0
- ppidest-0.1.0/src/ppidest/__init__.py +199 -0
- ppidest-0.1.0/src/ppidest/distributions.py +240 -0
- ppidest-0.1.0/src/ppidest/divergences.py +185 -0
- ppidest-0.1.0/src/ppidest/estimators.py +395 -0
- ppidest-0.1.0/src/ppidest/plotting.py +106 -0
ppidest-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ppidest
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Plotting Position-Information Divergence framework for parameter estimation of univariate distributions
|
|
5
|
+
Keywords: statistics,parameter-estimation,information-divergence,plotting-positions,extremes
|
|
6
|
+
Author: Takuya Kawanishi
|
|
7
|
+
Author-email: Takuya Kawanishi <takuya@exanalytics.sakura.ne.jp>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
15
|
+
Requires-Dist: numpy>=2.5.3
|
|
16
|
+
Requires-Dist: scipy>=1.18.1
|
|
17
|
+
Requires-Python: >=3.13
|
|
18
|
+
Project-URL: Repository, https://codeberg.org/takuya_kawanishi/ppidest
|
|
19
|
+
Project-URL: Homepage, https://codeberg.org/takuya_kawanishi/ppidest
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# ppidest
|
|
23
|
+
|
|
24
|
+
**Plotting Position — Information Divergence** framework for parameter
|
|
25
|
+
estimation of univariate distributions.
|
|
26
|
+
|
|
27
|
+
The empirical cumulative distribution function of a sample is discretized
|
|
28
|
+
using *plotting positions*, and a parametric distribution is fitted by
|
|
29
|
+
minimizing an *information divergence* between the empirical gaps and the
|
|
30
|
+
gaps implied by the candidate CDF.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Requires Python ≥ 3.13.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uv sync # create the environment and install the package
|
|
38
|
+
uv run python -m unittest discover -s tests # run the test suite
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Dependencies: `numpy`, `scipy`. The package is also installable from a
|
|
42
|
+
source checkout with `pip install .` or `uv pip install .`, and the
|
|
43
|
+
distributions on PyPI ship a `ppidest` console script (see
|
|
44
|
+
[Command line](#command-line)).
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
Fit a GEV model to a small sample using the default (Kullback-Leibler)
|
|
49
|
+
divergence:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import numpy as np
|
|
53
|
+
import ppidest
|
|
54
|
+
|
|
55
|
+
xs = np.array([0.38, 0.51, 1.44, 2.14])
|
|
56
|
+
|
|
57
|
+
# Plotting Position – Information Divergence estimator
|
|
58
|
+
res = ppidest.find_ppid_min(xs, ppidest.gev_cdf, [0.0, 1.0, 0.25])
|
|
59
|
+
print(res.x) # fitted (loc, scale, shape) -> [0.5600, 0.3639, 1.0585]
|
|
60
|
+
print(res.fun) # minimized divergence -> 0.17543
|
|
61
|
+
|
|
62
|
+
# Compute a return level from the fitted model
|
|
63
|
+
ppidest.gev_return_level(100.0, res.x)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Comparison across estimation methods (`scipy.stats` is used only to
|
|
67
|
+
generate the data / provide the density here):
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import scipy.stats
|
|
71
|
+
|
|
72
|
+
xs = np.sort(scipy.stats.norm.rvs(loc=1.0, scale=2.0, size=8))
|
|
73
|
+
|
|
74
|
+
# Density/CDF callables must use the (x, par) convention
|
|
75
|
+
def norm_pdf(x, par):
|
|
76
|
+
return scipy.stats.norm.pdf(x, loc=par[0], scale=par[1])
|
|
77
|
+
|
|
78
|
+
# Maximum likelihood from the PDF
|
|
79
|
+
ml = ppidest.ML(xs, norm_pdf).find_mle([1.0, 2.0])
|
|
80
|
+
|
|
81
|
+
# PPID with a different divergence and plot position
|
|
82
|
+
ppid = ppidest.PPID(xs, ppidest.normal_cdf, plotposition="Weibull")
|
|
83
|
+
res = ppid.find_min_div([1.0, 2.0], divergence="Jensen-Shannon")
|
|
84
|
+
|
|
85
|
+
# Forward extra positional arguments to the CDF callable
|
|
86
|
+
def norm_cdf_given_mu(x, par, mu):
|
|
87
|
+
return ppidest.normal_cdf(x, [mu, par[0]])
|
|
88
|
+
|
|
89
|
+
ppid = ppidest.PPID(xs, norm_cdf_given_mu)
|
|
90
|
+
res = ppid.find_min_div([1.0, 2.0], args=(0.0,)) # scale only, mu fixed
|
|
91
|
+
```
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Background
|
|
95
|
+
|
|
96
|
+
For a sorted sample `x_(1) <= ... <= x_(n)` a plotting position assigns a
|
|
97
|
+
probability to the i-th order statistic:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
p_i = (i - alpha) / (n + 1 - alpha - beta)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`alpha` and `beta` are fixed by the chosen scheme:
|
|
104
|
+
|
|
105
|
+
| Scheme | alpha | beta |
|
|
106
|
+
|--------------|-------|------|
|
|
107
|
+
| Weibull | 0.0 | 0.0 |
|
|
108
|
+
| median | 0.3 | 0.3 |
|
|
109
|
+
| Gringorten | 0.44 | 0.44 |
|
|
110
|
+
| Hazen | 0.5 | 0.5 |
|
|
111
|
+
|
|
112
|
+
Ties are aggregated so each unique value carries the summed probability
|
|
113
|
+
of the order statistics sharing it, and the support is augmented with two
|
|
114
|
+
boundary bins at `0` and `1`. The result is an empirical probability
|
|
115
|
+
vector `d*` (the "empirical gaps"). A candidate distribution with
|
|
116
|
+
parameters `θ` gives model gaps
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
dd_j(θ) = F(x_j; θ) - F(x_{j-1}; θ), with x_0 = -inf, x_{k+1} = +inf
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The estimator minimizes an information divergence `D(d* || dd(θ))` over
|
|
123
|
+
`θ`:
|
|
124
|
+
|
|
125
|
+
- **Kullback-Leibler** —
|
|
126
|
+
`Σ d* log(d*/dd)`
|
|
127
|
+
- **generalized KL** —
|
|
128
|
+
`Σ d* log(d*/dd) - d* + dd`
|
|
129
|
+
- **symmetric KL** —
|
|
130
|
+
`Σ (dd - d*) log(dd/d*)`
|
|
131
|
+
- **Jensen-Shannon** —
|
|
132
|
+
`½ Σ d* log(d*/m) + dd log(dd/m)`, `m = (d* + dd)/2`
|
|
133
|
+
- **beta** (order `β`) —
|
|
134
|
+
`Σ d*(d*^(β-1) - dd^(β-1))/(β-1) - (d*^β - dd^β)/β`
|
|
135
|
+
- **power** (index `λ`) —
|
|
136
|
+
`1/(λ(λ+1)) Σ d*[(d*/dd)^λ - 1]`
|
|
137
|
+
- **Rényi** (order `α`) —
|
|
138
|
+
`1/(α-1) log Σ d*^α dd^(1-α)`
|
|
139
|
+
|
|
140
|
+
## Package layout
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
src/ppidest/
|
|
144
|
+
distributions.py distribution cdf/pdf/quantile functions
|
|
145
|
+
(normal, GEV, three-parameter Weibull)
|
|
146
|
+
plotting.py plotting-position schemes and empirical gaps
|
|
147
|
+
divergences.py information divergences between probability vectors
|
|
148
|
+
estimators.py ML, PPID estimators
|
|
149
|
+
__init__.py public API and legacy calc_* aliases
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Distributions
|
|
153
|
+
|
|
154
|
+
All distribution functions share the signature `f(x, par)` with
|
|
155
|
+
`par = (loc, scale, shape)` (normal and Weibull parameters differ, see
|
|
156
|
+
their docstrings):
|
|
157
|
+
|
|
158
|
+
- `normal_cdf`, `normal_pdf`
|
|
159
|
+
- `gev_cdf`, `gev_pdf`, `gev_quantile`, `gev_return_level`
|
|
160
|
+
(GEV shape `ξ` uses the convention `ξ > 0` → heavy tail)
|
|
161
|
+
- `weibull_cdf`, `weibull_pdf`
|
|
162
|
+
|
|
163
|
+
### Divergences
|
|
164
|
+
|
|
165
|
+
`ppidest.kl_divergence`, `kl_generalized_divergence`,
|
|
166
|
+
`kl_symmetric_divergence`, `jensen_shannon_divergence`,
|
|
167
|
+
`beta_divergence(das, ddas, beta)`, `power_divergence(das, ddas, lmbd)`,
|
|
168
|
+
`renyi_divergence(das, ddas, alpha)` — all take the empirical gaps
|
|
169
|
+
`das` and model gaps `ddas` as 1-D arrays.
|
|
170
|
+
|
|
171
|
+
### Estimators
|
|
172
|
+
|
|
173
|
+
| Estimator | Objective | Fit method |
|
|
174
|
+
|-----------|-----------|------------|
|
|
175
|
+
| `ML(xs, pdf)` | negative log-likelihood | `find_mle` |
|
|
176
|
+
| `PPID(xs, cdf)` | chosen information divergence | `find_min_div` |
|
|
177
|
+
|
|
178
|
+
Every `find_*` method mirrors the keyword arguments of
|
|
179
|
+
`scipy.optimize.minimize` (`method`, `jac`, `hess`, `bounds`, ...).
|
|
180
|
+
`PPID.find_min_div` accepts `divergence` and, for the parametric
|
|
181
|
+
divergences, `pdiv`. The parametric divergence arguments are passed as
|
|
182
|
+
singular floats (the pre-1.0 API required a length-one list).
|
|
183
|
+
|
|
184
|
+
The legacy `calc_gev_cdf`, ... names are exported from the package root as
|
|
185
|
+
aliases for backward compatibility, e.g. `ppidest.calc_gev_cdf` is the
|
|
186
|
+
same object as `ppidest.gev_cdf`.
|
|
187
|
+
|
|
188
|
+
## Notes on optimization
|
|
189
|
+
|
|
190
|
+
Nelder–Mead is the default solver because the objective only needs
|
|
191
|
+
CDF evaluation. During the search the parameters may leave the support
|
|
192
|
+
of the distribution, producing `NaN`; these evaluations are harmless and
|
|
193
|
+
the corresponding warnings are suppressed inside the distribution and
|
|
194
|
+
divergence functions. Careful initial values (e.g. from the method of
|
|
195
|
+
moments) are recommended for the scale and shape parameters.
|
ppidest-0.1.0/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# ppidest
|
|
2
|
+
|
|
3
|
+
**Plotting Position — Information Divergence** framework for parameter
|
|
4
|
+
estimation of univariate distributions.
|
|
5
|
+
|
|
6
|
+
The empirical cumulative distribution function of a sample is discretized
|
|
7
|
+
using *plotting positions*, and a parametric distribution is fitted by
|
|
8
|
+
minimizing an *information divergence* between the empirical gaps and the
|
|
9
|
+
gaps implied by the candidate CDF.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Requires Python ≥ 3.13.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv sync # create the environment and install the package
|
|
17
|
+
uv run python -m unittest discover -s tests # run the test suite
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Dependencies: `numpy`, `scipy`. The package is also installable from a
|
|
21
|
+
source checkout with `pip install .` or `uv pip install .`, and the
|
|
22
|
+
distributions on PyPI ship a `ppidest` console script (see
|
|
23
|
+
[Command line](#command-line)).
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
Fit a GEV model to a small sample using the default (Kullback-Leibler)
|
|
28
|
+
divergence:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import numpy as np
|
|
32
|
+
import ppidest
|
|
33
|
+
|
|
34
|
+
xs = np.array([0.38, 0.51, 1.44, 2.14])
|
|
35
|
+
|
|
36
|
+
# Plotting Position – Information Divergence estimator
|
|
37
|
+
res = ppidest.find_ppid_min(xs, ppidest.gev_cdf, [0.0, 1.0, 0.25])
|
|
38
|
+
print(res.x) # fitted (loc, scale, shape) -> [0.5600, 0.3639, 1.0585]
|
|
39
|
+
print(res.fun) # minimized divergence -> 0.17543
|
|
40
|
+
|
|
41
|
+
# Compute a return level from the fitted model
|
|
42
|
+
ppidest.gev_return_level(100.0, res.x)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Comparison across estimation methods (`scipy.stats` is used only to
|
|
46
|
+
generate the data / provide the density here):
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import scipy.stats
|
|
50
|
+
|
|
51
|
+
xs = np.sort(scipy.stats.norm.rvs(loc=1.0, scale=2.0, size=8))
|
|
52
|
+
|
|
53
|
+
# Density/CDF callables must use the (x, par) convention
|
|
54
|
+
def norm_pdf(x, par):
|
|
55
|
+
return scipy.stats.norm.pdf(x, loc=par[0], scale=par[1])
|
|
56
|
+
|
|
57
|
+
# Maximum likelihood from the PDF
|
|
58
|
+
ml = ppidest.ML(xs, norm_pdf).find_mle([1.0, 2.0])
|
|
59
|
+
|
|
60
|
+
# PPID with a different divergence and plot position
|
|
61
|
+
ppid = ppidest.PPID(xs, ppidest.normal_cdf, plotposition="Weibull")
|
|
62
|
+
res = ppid.find_min_div([1.0, 2.0], divergence="Jensen-Shannon")
|
|
63
|
+
|
|
64
|
+
# Forward extra positional arguments to the CDF callable
|
|
65
|
+
def norm_cdf_given_mu(x, par, mu):
|
|
66
|
+
return ppidest.normal_cdf(x, [mu, par[0]])
|
|
67
|
+
|
|
68
|
+
ppid = ppidest.PPID(xs, norm_cdf_given_mu)
|
|
69
|
+
res = ppid.find_min_div([1.0, 2.0], args=(0.0,)) # scale only, mu fixed
|
|
70
|
+
```
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Background
|
|
74
|
+
|
|
75
|
+
For a sorted sample `x_(1) <= ... <= x_(n)` a plotting position assigns a
|
|
76
|
+
probability to the i-th order statistic:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
p_i = (i - alpha) / (n + 1 - alpha - beta)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`alpha` and `beta` are fixed by the chosen scheme:
|
|
83
|
+
|
|
84
|
+
| Scheme | alpha | beta |
|
|
85
|
+
|--------------|-------|------|
|
|
86
|
+
| Weibull | 0.0 | 0.0 |
|
|
87
|
+
| median | 0.3 | 0.3 |
|
|
88
|
+
| Gringorten | 0.44 | 0.44 |
|
|
89
|
+
| Hazen | 0.5 | 0.5 |
|
|
90
|
+
|
|
91
|
+
Ties are aggregated so each unique value carries the summed probability
|
|
92
|
+
of the order statistics sharing it, and the support is augmented with two
|
|
93
|
+
boundary bins at `0` and `1`. The result is an empirical probability
|
|
94
|
+
vector `d*` (the "empirical gaps"). A candidate distribution with
|
|
95
|
+
parameters `θ` gives model gaps
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
dd_j(θ) = F(x_j; θ) - F(x_{j-1}; θ), with x_0 = -inf, x_{k+1} = +inf
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The estimator minimizes an information divergence `D(d* || dd(θ))` over
|
|
102
|
+
`θ`:
|
|
103
|
+
|
|
104
|
+
- **Kullback-Leibler** —
|
|
105
|
+
`Σ d* log(d*/dd)`
|
|
106
|
+
- **generalized KL** —
|
|
107
|
+
`Σ d* log(d*/dd) - d* + dd`
|
|
108
|
+
- **symmetric KL** —
|
|
109
|
+
`Σ (dd - d*) log(dd/d*)`
|
|
110
|
+
- **Jensen-Shannon** —
|
|
111
|
+
`½ Σ d* log(d*/m) + dd log(dd/m)`, `m = (d* + dd)/2`
|
|
112
|
+
- **beta** (order `β`) —
|
|
113
|
+
`Σ d*(d*^(β-1) - dd^(β-1))/(β-1) - (d*^β - dd^β)/β`
|
|
114
|
+
- **power** (index `λ`) —
|
|
115
|
+
`1/(λ(λ+1)) Σ d*[(d*/dd)^λ - 1]`
|
|
116
|
+
- **Rényi** (order `α`) —
|
|
117
|
+
`1/(α-1) log Σ d*^α dd^(1-α)`
|
|
118
|
+
|
|
119
|
+
## Package layout
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
src/ppidest/
|
|
123
|
+
distributions.py distribution cdf/pdf/quantile functions
|
|
124
|
+
(normal, GEV, three-parameter Weibull)
|
|
125
|
+
plotting.py plotting-position schemes and empirical gaps
|
|
126
|
+
divergences.py information divergences between probability vectors
|
|
127
|
+
estimators.py ML, PPID estimators
|
|
128
|
+
__init__.py public API and legacy calc_* aliases
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Distributions
|
|
132
|
+
|
|
133
|
+
All distribution functions share the signature `f(x, par)` with
|
|
134
|
+
`par = (loc, scale, shape)` (normal and Weibull parameters differ, see
|
|
135
|
+
their docstrings):
|
|
136
|
+
|
|
137
|
+
- `normal_cdf`, `normal_pdf`
|
|
138
|
+
- `gev_cdf`, `gev_pdf`, `gev_quantile`, `gev_return_level`
|
|
139
|
+
(GEV shape `ξ` uses the convention `ξ > 0` → heavy tail)
|
|
140
|
+
- `weibull_cdf`, `weibull_pdf`
|
|
141
|
+
|
|
142
|
+
### Divergences
|
|
143
|
+
|
|
144
|
+
`ppidest.kl_divergence`, `kl_generalized_divergence`,
|
|
145
|
+
`kl_symmetric_divergence`, `jensen_shannon_divergence`,
|
|
146
|
+
`beta_divergence(das, ddas, beta)`, `power_divergence(das, ddas, lmbd)`,
|
|
147
|
+
`renyi_divergence(das, ddas, alpha)` — all take the empirical gaps
|
|
148
|
+
`das` and model gaps `ddas` as 1-D arrays.
|
|
149
|
+
|
|
150
|
+
### Estimators
|
|
151
|
+
|
|
152
|
+
| Estimator | Objective | Fit method |
|
|
153
|
+
|-----------|-----------|------------|
|
|
154
|
+
| `ML(xs, pdf)` | negative log-likelihood | `find_mle` |
|
|
155
|
+
| `PPID(xs, cdf)` | chosen information divergence | `find_min_div` |
|
|
156
|
+
|
|
157
|
+
Every `find_*` method mirrors the keyword arguments of
|
|
158
|
+
`scipy.optimize.minimize` (`method`, `jac`, `hess`, `bounds`, ...).
|
|
159
|
+
`PPID.find_min_div` accepts `divergence` and, for the parametric
|
|
160
|
+
divergences, `pdiv`. The parametric divergence arguments are passed as
|
|
161
|
+
singular floats (the pre-1.0 API required a length-one list).
|
|
162
|
+
|
|
163
|
+
The legacy `calc_gev_cdf`, ... names are exported from the package root as
|
|
164
|
+
aliases for backward compatibility, e.g. `ppidest.calc_gev_cdf` is the
|
|
165
|
+
same object as `ppidest.gev_cdf`.
|
|
166
|
+
|
|
167
|
+
## Notes on optimization
|
|
168
|
+
|
|
169
|
+
Nelder–Mead is the default solver because the objective only needs
|
|
170
|
+
CDF evaluation. During the search the parameters may leave the support
|
|
171
|
+
of the distribution, producing `NaN`; these evaluations are harmless and
|
|
172
|
+
the corresponding warnings are suppressed inside the distribution and
|
|
173
|
+
divergence functions. Careful initial values (e.g. from the method of
|
|
174
|
+
moments) are recommended for the scale and shape parameters.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ppidest"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Plotting Position-Information Divergence framework for parameter estimation of univariate distributions"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
keywords = [
|
|
9
|
+
"statistics",
|
|
10
|
+
"parameter-estimation",
|
|
11
|
+
"information-divergence",
|
|
12
|
+
"plotting-positions",
|
|
13
|
+
"extremes",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"numpy>=2.5.3",
|
|
25
|
+
"scipy>=1.18.1",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[[project.authors]]
|
|
29
|
+
name = "Takuya Kawanishi"
|
|
30
|
+
email = "takuya@exanalytics.sakura.ne.jp"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
ppidest = "ppidest:main"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Repository = "https://codeberg.org/takuya_kawanishi/ppidest"
|
|
37
|
+
Homepage = "https://codeberg.org/takuya_kawanishi/ppidest"
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["uv_build>=0.12.10,<0.13.0"]
|
|
41
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ppidest"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Plotting Position-Information Divergence framework for parameter estimation of univariate distributions"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Takuya Kawanishi", email = "takuya@exanalytics.sakura.ne.jp" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["statistics", "parameter-estimation", "information-divergence", "plotting-positions", "extremes"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Science/Research",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"numpy>=2.5.3",
|
|
22
|
+
"scipy>=1.18.1",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
ppidest = "ppidest:main"
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Repository = "https://codeberg.org/takuya_kawanishi/ppidest"
|
|
30
|
+
Homepage = "https://codeberg.org/takuya_kawanishi/ppidest"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["uv_build>=0.12.10,<0.13.0"]
|
|
34
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Plotting Position-Information Divergence (PPID) framework.
|
|
2
|
+
|
|
3
|
+
Parameter estimation for univariate distributions based on discretizing
|
|
4
|
+
the cumulative distribution function with plotting positions and
|
|
5
|
+
minimizing information divergences against the empirical gaps.
|
|
6
|
+
|
|
7
|
+
Public API
|
|
8
|
+
----------
|
|
9
|
+
- Distributions: :func:`normal_cdf`, :func:`normal_pdf`, :func:`gev_cdf`,
|
|
10
|
+
:func:`gev_pdf`, :func:`gev_quantile`, :func:`gev_return_level`,
|
|
11
|
+
:func:`weibull_cdf`, :func:`weibull_pdf`.
|
|
12
|
+
- Plotting positions: :func:`plotting_positions`.
|
|
13
|
+
- Divergences: :func:`kl_divergence`, :func:`kl_generalized_divergence`,
|
|
14
|
+
:func:`kl_symmetric_divergence`, :func:`jensen_shannon_divergence`,
|
|
15
|
+
:func:`beta_divergence`, :func:`power_divergence`,
|
|
16
|
+
:func:`renyi_divergence`.
|
|
17
|
+
- Estimators: :class:`ML`, :class:`PPID`
|
|
18
|
+
and the convenience wrapper :func:`find_ppid_min`.
|
|
19
|
+
|
|
20
|
+
The ``calc_*`` names from earlier releases are re-exported here as
|
|
21
|
+
backward-compatible aliases (e.g. :data:`calc_gev_cdf`).
|
|
22
|
+
|
|
23
|
+
Command line
|
|
24
|
+
------------
|
|
25
|
+
The ``ppidest`` console script fits a distribution to sample values given
|
|
26
|
+
on the command line::
|
|
27
|
+
|
|
28
|
+
ppidest 0.38 0.51 1.44 2.14
|
|
29
|
+
ppidest 0.38 0.51 1.44 2.14 --distribution normal
|
|
30
|
+
ppidest 0.38 0.51 1.44 2.14 --plotting-position Weibull \\
|
|
31
|
+
--information-divergence "Jensen-Shannon"
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import argparse
|
|
35
|
+
|
|
36
|
+
import numpy as np
|
|
37
|
+
|
|
38
|
+
from .distributions import (
|
|
39
|
+
gev_cdf,
|
|
40
|
+
gev_pdf,
|
|
41
|
+
gev_quantile,
|
|
42
|
+
gev_return_level,
|
|
43
|
+
normal_cdf,
|
|
44
|
+
normal_pdf,
|
|
45
|
+
weibull_cdf,
|
|
46
|
+
weibull_pdf,
|
|
47
|
+
calc_gev_cdf,
|
|
48
|
+
calc_gev_pdf,
|
|
49
|
+
calc_gev_quantile,
|
|
50
|
+
calc_norm_cdf,
|
|
51
|
+
calc_norm_pdf,
|
|
52
|
+
calc_return_level_gev,
|
|
53
|
+
calc_weibull_cdf,
|
|
54
|
+
calc_weibull_pdf,
|
|
55
|
+
)
|
|
56
|
+
from .divergences import (
|
|
57
|
+
beta_divergence,
|
|
58
|
+
jensen_shannon_divergence,
|
|
59
|
+
kl_divergence,
|
|
60
|
+
kl_generalized_divergence,
|
|
61
|
+
kl_symmetric_divergence,
|
|
62
|
+
power_divergence,
|
|
63
|
+
renyi_divergence,
|
|
64
|
+
)
|
|
65
|
+
from .estimators import (
|
|
66
|
+
ML,
|
|
67
|
+
PPID,
|
|
68
|
+
DIVERGENCE_NAMES,
|
|
69
|
+
find_ppid_min,
|
|
70
|
+
)
|
|
71
|
+
from .plotting import (
|
|
72
|
+
PLOTTING_POSITIONS,
|
|
73
|
+
plotting_position_coefficients,
|
|
74
|
+
plotting_positions,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
__version__ = "0.1.0"
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
# distributions
|
|
81
|
+
"normal_cdf", "normal_pdf",
|
|
82
|
+
"gev_cdf", "gev_pdf", "gev_quantile", "gev_return_level",
|
|
83
|
+
"weibull_cdf", "weibull_pdf",
|
|
84
|
+
# plotting positions
|
|
85
|
+
"PLOTTING_POSITIONS", "plotting_position_coefficients",
|
|
86
|
+
"plotting_positions",
|
|
87
|
+
# divergences
|
|
88
|
+
"kl_divergence", "kl_generalized_divergence", "kl_symmetric_divergence",
|
|
89
|
+
"jensen_shannon_divergence", "beta_divergence", "power_divergence",
|
|
90
|
+
"renyi_divergence",
|
|
91
|
+
# estimators
|
|
92
|
+
"ML", "PPID", "find_ppid_min",
|
|
93
|
+
# legacy aliases
|
|
94
|
+
"calc_norm_cdf", "calc_norm_pdf",
|
|
95
|
+
"calc_gev_cdf", "calc_gev_pdf", "calc_gev_quantile",
|
|
96
|
+
"calc_return_level_gev", "calc_weibull_cdf", "calc_weibull_pdf",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
#: Name of the parameter vector entry for each supported distribution.
|
|
101
|
+
_PARAMETER_NAMES = {
|
|
102
|
+
"gev": ("loc", "scale", "shape"),
|
|
103
|
+
"normal": ("loc", "scale"),
|
|
104
|
+
"weibull": ("threshold", "scale", "shape"),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#: CDF callable for each supported distribution.
|
|
108
|
+
_DISTRIBUTIONS = {
|
|
109
|
+
"gev": gev_cdf,
|
|
110
|
+
"normal": normal_cdf,
|
|
111
|
+
"weibull": weibull_cdf,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _initial_parameters(name, xs):
|
|
116
|
+
"""Return a sensible parameter guess derived from the sample."""
|
|
117
|
+
loc = float(np.mean(xs))
|
|
118
|
+
scale = float(np.std(xs))
|
|
119
|
+
if name == "normal":
|
|
120
|
+
return [loc, scale]
|
|
121
|
+
if name == "gev":
|
|
122
|
+
return [loc, scale, 0.0]
|
|
123
|
+
# three-parameter Weibull: threshold below the minimum observation,
|
|
124
|
+
# scale on the order of the observed range.
|
|
125
|
+
mn, mx = float(xs.min()), float(xs.max())
|
|
126
|
+
threshold = mn - 0.1 * (mx - mn) - 1e-9
|
|
127
|
+
return [threshold, (mx - mn) + 1e-9, 2.0]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def main(argv=None):
|
|
131
|
+
"""Console entry point: fit a distribution to command-line sample data.
|
|
132
|
+
|
|
133
|
+
The parameters are fitted with the :class:`PPID` estimator using the
|
|
134
|
+
plotting position and information divergence given by
|
|
135
|
+
``--plotting-position`` (default ``Hazen``) and
|
|
136
|
+
``--information-divergence`` (default ``Kullback-Leibler``).
|
|
137
|
+
"""
|
|
138
|
+
parser = argparse.ArgumentParser(
|
|
139
|
+
prog="ppidest",
|
|
140
|
+
description="Fit a univariate distribution by minimizing an "
|
|
141
|
+
"information divergence between plotting-position "
|
|
142
|
+
"empirical gaps and the candidate CDF gaps.")
|
|
143
|
+
parser.add_argument(
|
|
144
|
+
"xs", type=float, nargs="+", metavar="x",
|
|
145
|
+
help="sample values to fit")
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"-d", "--distribution", choices=sorted(_DISTRIBUTIONS),
|
|
148
|
+
default="gev",
|
|
149
|
+
help="distribution family to fit (default: gev)")
|
|
150
|
+
parser.add_argument(
|
|
151
|
+
"-p", "--plotting-position", choices=sorted(PLOTTING_POSITIONS),
|
|
152
|
+
default="Hazen",
|
|
153
|
+
help="plotting-position scheme (default: Hazen)")
|
|
154
|
+
parser.add_argument(
|
|
155
|
+
"-i", "--information-divergence", choices=sorted(DIVERGENCE_NAMES),
|
|
156
|
+
default="Kullback-Leibler",
|
|
157
|
+
help="information divergence to minimize (default: "
|
|
158
|
+
"Kullback-Leibler)")
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
"--pdiv", type=float, default=0.5, metavar="VALUE",
|
|
161
|
+
help="order/index parameter for the beta, power or Renyi "
|
|
162
|
+
"divergences (default: 0.5)")
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
"--initial", type=float, nargs="+", metavar="PAR",
|
|
165
|
+
help="initial parameters, overriding the data-driven defaults "
|
|
166
|
+
"(e.g. '--initial 0 1 0.25' for GEV)")
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--version", action="version", version=f"%(prog)s {__version__}")
|
|
169
|
+
args = parser.parse_args(argv)
|
|
170
|
+
|
|
171
|
+
xs = np.asarray(args.xs)
|
|
172
|
+
if xs.size < 2:
|
|
173
|
+
parser.error("at least two sample values are required")
|
|
174
|
+
|
|
175
|
+
name = args.distribution
|
|
176
|
+
cdf = _DISTRIBUTIONS[name]
|
|
177
|
+
par_names = _PARAMETER_NAMES[name]
|
|
178
|
+
if args.initial is not None:
|
|
179
|
+
if len(args.initial) != len(par_names):
|
|
180
|
+
parser.error(
|
|
181
|
+
f"--initial must provide {len(par_names)} values for "
|
|
182
|
+
f"{name} ({', '.join(par_names)})")
|
|
183
|
+
par_0 = list(args.initial)
|
|
184
|
+
else:
|
|
185
|
+
par_0 = _initial_parameters(name, xs)
|
|
186
|
+
|
|
187
|
+
res = PPID(xs, cdf, plotposition=args.plotting_position,
|
|
188
|
+
divergence=args.information_divergence).find_min_div(
|
|
189
|
+
par_0, pdiv=args.pdiv)
|
|
190
|
+
|
|
191
|
+
header = ("distribution", "plotting position", "information divergence")
|
|
192
|
+
values = (name, args.plotting_position, args.information_divergence)
|
|
193
|
+
for key, value in zip(header, values):
|
|
194
|
+
print(f"{key:26s}: {value}")
|
|
195
|
+
for pname, pvalue in zip(par_names, res.x):
|
|
196
|
+
print(f"{pname:26s}: {pvalue:.6g}")
|
|
197
|
+
print(f"{'minimum divergence':26s}: {res.fun:.6g}")
|
|
198
|
+
print(f"{'converged':26s}: {res.success}")
|
|
199
|
+
return res
|