funcsim 0.2.2__tar.gz → 0.2.3__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.
- {funcsim-0.2.2/funcsim.egg-info → funcsim-0.2.3}/PKG-INFO +1 -1
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/__init__.py +1 -1
- funcsim-0.2.3/funcsim/distfit.py +722 -0
- {funcsim-0.2.2 → funcsim-0.2.3/funcsim.egg-info}/PKG-INFO +1 -1
- {funcsim-0.2.2 → funcsim-0.2.3}/setup.py +1 -1
- funcsim-0.2.2/funcsim/distfit.py +0 -307
- {funcsim-0.2.2 → funcsim-0.2.3}/LICENSE +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/MANIFEST.in +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/README.md +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/addist.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/conversions.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/copfit.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/core.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/cpt.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/cvmdist.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/dependence.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/ecdfgof.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/edf.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/eut.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/imanconover.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/kde.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/ksdist.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/multicore.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/nearby.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/plotting.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/rdarrays.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/screen.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/shapiro.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/shrinkage.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/testsim.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/utests.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/vect.py +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim.egg-info/SOURCES.txt +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim.egg-info/dependency_links.txt +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim.egg-info/requires.txt +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/funcsim.egg-info/top_level.txt +0 -0
- {funcsim-0.2.2 → funcsim-0.2.3}/setup.cfg +0 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
|
|
2
|
+
import numbers
|
|
3
|
+
from collections import namedtuple
|
|
4
|
+
import scipy.stats as stats
|
|
5
|
+
from scipy import optimize
|
|
6
|
+
from scipy.stats._distn_infrastructure import rv_continuous
|
|
7
|
+
import numpy as np
|
|
8
|
+
import warnings
|
|
9
|
+
from .ecdfgof import adtest, cvmtest
|
|
10
|
+
from . import conversions
|
|
11
|
+
from typing import Optional, Tuple
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# "name", "scipy_name", "lower_bound", "upper_bound"
|
|
15
|
+
#
|
|
16
|
+
# The two flags describe the support of the *fitted* distribution. A few
|
|
17
|
+
# families realize different kinds of support in different regions of their
|
|
18
|
+
# shape-parameter space (e.g., the generalized extreme value distribution is
|
|
19
|
+
# bounded below for negative shape and bounded above for positive shape).
|
|
20
|
+
# Such families are listed once per kind of support they can realize on a
|
|
21
|
+
# region of positive measure; `compare` checks the support of every fitted
|
|
22
|
+
# distribution and drops any whose support does not match its group.
|
|
23
|
+
candidates = [
|
|
24
|
+
("alpha", stats.alpha, True, False),
|
|
25
|
+
("anglit", stats.anglit, True, True),
|
|
26
|
+
("arcsine", stats.arcsine, True, True),
|
|
27
|
+
("argus", stats.argus, True, True),
|
|
28
|
+
("Beta", stats.beta, True, True),
|
|
29
|
+
("Beta Prime", stats.betaprime, True, False),
|
|
30
|
+
("Bradford", stats.bradford, True, True),
|
|
31
|
+
("Burr", stats.burr, True, False),
|
|
32
|
+
("Burr Type XII", stats.burr12, True, False),
|
|
33
|
+
("Cauchy", stats.cauchy, False, False),
|
|
34
|
+
("Chi", stats.chi, True, False),
|
|
35
|
+
("Chi^2", stats.chi2, True, False),
|
|
36
|
+
("Cosine", stats.cosine, True, True),
|
|
37
|
+
("Crystal Ball", stats.crystalball, False, False),
|
|
38
|
+
("Double Gamma", stats.dgamma, False, False),
|
|
39
|
+
# ("Double Pareto Lognormal",stats.dpareto_lognorm,True,False),
|
|
40
|
+
("Double Weibull", stats.dweibull, False, False),
|
|
41
|
+
("Erlang", stats.erlang, True, False),
|
|
42
|
+
("Exponential", stats.expon, True, False),
|
|
43
|
+
("Expone. Modified Normal", stats.exponnorm, False, False),
|
|
44
|
+
("Exponentiated Weibull", stats.exponweib, True, False),
|
|
45
|
+
("Exponential Power", stats.exponpow, True, False),
|
|
46
|
+
("F", stats.f, True, False),
|
|
47
|
+
("Fatigue Life", stats.fatiguelife, True, False),
|
|
48
|
+
("Fisk", stats.fisk, True, False),
|
|
49
|
+
("Folded Cauchy", stats.foldcauchy, True, False),
|
|
50
|
+
("Folded Normal", stats.foldnorm, True, False),
|
|
51
|
+
("Generalized Logistic", stats.genlogistic, False, False),
|
|
52
|
+
("Generalized Normal", stats.gennorm, False, False),
|
|
53
|
+
# support [0, inf) for shape c >= 0, [0, -1/c] for c < 0
|
|
54
|
+
("Generalized Pareto", stats.genpareto, True, False),
|
|
55
|
+
("Generalized Pareto", stats.genpareto, True, True),
|
|
56
|
+
("Generalized Exponential", stats.genexpon, True, False),
|
|
57
|
+
# support [1/c, inf) for shape c < 0, (-inf, 1/c] for c > 0; never both
|
|
58
|
+
("Generalized Extreme Value", stats.genextreme, True, False),
|
|
59
|
+
("Generalized Extreme Value", stats.genextreme, False, True),
|
|
60
|
+
("Gauss Hypergeometric", stats.gausshyper, True, True),
|
|
61
|
+
("Gamma", stats.gamma, True, False),
|
|
62
|
+
("Generalized Gamma", stats.gengamma, True, False),
|
|
63
|
+
("Generalized Half-Logistic", stats.genhalflogistic, True, True),
|
|
64
|
+
("Generalized Hyperbolic", stats.genhyperbolic, False, False),
|
|
65
|
+
("Generalized Inverse Gaussian", stats.geninvgauss, True, False),
|
|
66
|
+
("Gibrat", stats.gibrat, True, False),
|
|
67
|
+
("Gompertz", stats.gompertz, True, False),
|
|
68
|
+
("Gumbel Right-skewed", stats.gumbel_r, False, False),
|
|
69
|
+
("Gumbel Left-skewed", stats.gumbel_l, False, False),
|
|
70
|
+
("Half-Cauchy", stats.halfcauchy, True, False),
|
|
71
|
+
("Half-Logistic", stats.halflogistic, True, False),
|
|
72
|
+
("Half-Normal", stats.halfnorm, True, False),
|
|
73
|
+
("Half Generalized Normal", stats.halfgennorm, True, False),
|
|
74
|
+
("Hyperbolic Secant", stats.hypsecant, False, False),
|
|
75
|
+
("Inverse Gamma", stats.invgamma, True, False),
|
|
76
|
+
("Inverse Gaussian", stats.invgauss, True, False),
|
|
77
|
+
("Inverse Weibull", stats.invweibull, True, False),
|
|
78
|
+
# ("Irwin-Hall",stats.irwinhall,True,True),
|
|
79
|
+
("Jones and Faddy Skew-T", stats.jf_skew_t, False, False),
|
|
80
|
+
("Johnson's S_B", stats.johnsonsb, True, True),
|
|
81
|
+
("Johnson's S_U", stats.johnsonsu, False, False),
|
|
82
|
+
# support depends on the signs of both shapes h and k: bounded on both
|
|
83
|
+
# sides (h > 0, k > 0), below only (k <= 0 with h > 0, or k < 0), or
|
|
84
|
+
# above only (h <= 0, k > 0); unbounded only on the line h <= 0, k = 0
|
|
85
|
+
("Four-parameter Kappa", stats.kappa4, True, True),
|
|
86
|
+
("Four-parameter Kappa", stats.kappa4, True, False),
|
|
87
|
+
("Four-parameter Kappa", stats.kappa4, False, True),
|
|
88
|
+
("3-Param Kappa Distribution", stats.kappa3, True, False),
|
|
89
|
+
# ("Landau",stats.landau,False,False),
|
|
90
|
+
("Laplace", stats.laplace, False, False),
|
|
91
|
+
("Asymmetric Laplace", stats.laplace_asymmetric, False, False),
|
|
92
|
+
("Lévy", stats.levy, True, False),
|
|
93
|
+
("Left-skewed Lévy", stats.levy_l, False, True),
|
|
94
|
+
("Lévy stable", stats.levy_stable, False, False),
|
|
95
|
+
("Logistic", stats.logistic, False, False),
|
|
96
|
+
("Log-gamma", stats.loggamma, False, False),
|
|
97
|
+
("Log-Laplace", stats.loglaplace, True, False),
|
|
98
|
+
("Log-normal", stats.lognorm, True, False),
|
|
99
|
+
("Log-uniform", stats.loguniform, True, True),
|
|
100
|
+
("Lomax", stats.lomax, True, False),
|
|
101
|
+
("Maxwell", stats.maxwell, True, False),
|
|
102
|
+
("Mielke's Beta-Kappa", stats.mielke, True, False),
|
|
103
|
+
("Moyal", stats.moyal, False, False),
|
|
104
|
+
("Nakagami", stats.nakagami, True, False),
|
|
105
|
+
("Noncentral chi^2", stats.ncx2, True, False),
|
|
106
|
+
("Noncentral F", stats.ncf, True, False),
|
|
107
|
+
("Noncentral Student’s t", stats.nct, False, False),
|
|
108
|
+
("Normal", stats.norm, False, False),
|
|
109
|
+
("Normal inverse Gaussian", stats.norminvgauss, False, False),
|
|
110
|
+
("Pareto", stats.pareto, True, False),
|
|
111
|
+
("Pearson Type III", stats.pearson3, False, False),
|
|
112
|
+
("Power-function", stats.powerlaw, True, True),
|
|
113
|
+
("Power log-normal", stats.powerlognorm, True, False),
|
|
114
|
+
("Power normal", stats.powernorm, False, False),
|
|
115
|
+
("symmetric beta", stats.rdist, True, True),
|
|
116
|
+
("Rayleigh", stats.rayleigh, True, False),
|
|
117
|
+
("Relativistic Breit–Wigner", stats.rel_breitwigner, True, False),
|
|
118
|
+
("Rice", stats.rice, True, False),
|
|
119
|
+
("Reciprocal inverse Gaussian", stats.recipinvgauss, True, False),
|
|
120
|
+
("Semicircular", stats.semicircular, True, True),
|
|
121
|
+
("Skewed Cauchy", stats.skewcauchy, False, False),
|
|
122
|
+
("Skew-normal", stats.skewnorm, False, False),
|
|
123
|
+
("Studentized range", stats.studentized_range, True, False),
|
|
124
|
+
("Student's t", stats.t, False, False),
|
|
125
|
+
("Trapezoidal", stats.trapezoid, True, True),
|
|
126
|
+
("Triangular", stats.triang, True, True),
|
|
127
|
+
("Truncated exponential", stats.truncexpon, True, True),
|
|
128
|
+
("Truncated normal", stats.truncnorm, True, True),
|
|
129
|
+
("Truncated Pareto", stats.truncpareto, True, True),
|
|
130
|
+
("Truncated Weibull minimum", stats.truncweibull_min, True, True),
|
|
131
|
+
# support [-1/lam, 1/lam] for shape lam > 0, the whole line otherwise
|
|
132
|
+
("Tukey lambda", stats.tukeylambda, True, True),
|
|
133
|
+
("Tukey lambda", stats.tukeylambda, False, False),
|
|
134
|
+
("Uniform", stats.uniform, True, True),
|
|
135
|
+
# scipy's `vonmises` is defined on the whole real line (it is periodic);
|
|
136
|
+
# `vonmises_line` is the same density restricted to [-pi, pi]
|
|
137
|
+
("Von Mises", stats.vonmises_line, True, True),
|
|
138
|
+
("Wald", stats.wald, True, False),
|
|
139
|
+
("Weibull Min Extreme Value", stats.weibull_min, True, False),
|
|
140
|
+
("Weibull Max Extreme Value", stats.weibull_max, False, True),
|
|
141
|
+
("Wrapped Cauchy", stats.wrapcauchy, True, True),
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# container for "fit" results
|
|
146
|
+
FitResult = namedtuple("FitResult",
|
|
147
|
+
"bic aic ad_pval cvm_pval dist distName warnings")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# per-observation penalty for data outside the support, matching scipy
|
|
151
|
+
_PENALTY = 100.0 * np.log(np.finfo(float).max)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# argument validation
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _validate_bound(value, name: str) -> Optional[float]:
|
|
159
|
+
# None (bound not fixed) or a finite real number; bools are rejected
|
|
160
|
+
# because they are the legacy "which group" flags of `compare`
|
|
161
|
+
if value is None:
|
|
162
|
+
return None
|
|
163
|
+
if isinstance(value, (bool, np.bool_)):
|
|
164
|
+
raise TypeError(f"{name} must be a float or None, not a bool")
|
|
165
|
+
if not isinstance(value, numbers.Real):
|
|
166
|
+
raise TypeError(f"{name} must be a float or None, "
|
|
167
|
+
f"not {type(value).__name__}")
|
|
168
|
+
bound = float(value)
|
|
169
|
+
if not np.isfinite(bound):
|
|
170
|
+
raise ValueError(f"{name} must be finite, not {value}")
|
|
171
|
+
return bound
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _check_bounds_against_data(dataA: np.ndarray,
|
|
175
|
+
lower: Optional[float],
|
|
176
|
+
upper: Optional[float]) -> None:
|
|
177
|
+
if lower is not None and upper is not None and not lower < upper:
|
|
178
|
+
raise ValueError(f"lowerBound ({lower}) must be strictly less than "
|
|
179
|
+
f"upperBound ({upper})")
|
|
180
|
+
if lower is not None and dataA.min() < lower:
|
|
181
|
+
raise ValueError(f"lowerBound ({lower}) exceeds the smallest "
|
|
182
|
+
f"observation ({dataA.min()})")
|
|
183
|
+
if upper is not None and dataA.max() > upper:
|
|
184
|
+
raise ValueError(f"upperBound ({upper}) is below the largest "
|
|
185
|
+
f"observation ({dataA.max()})")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# support metadata for scipy distributions
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _shape_dependent_support(scipydist) -> bool:
|
|
193
|
+
# scipy signals a support that depends on the shape parameters by
|
|
194
|
+
# overriding `_get_support`. This is private but has been stable since
|
|
195
|
+
# scipy 1.2; if it ever disappears, every distribution is treated as
|
|
196
|
+
# shape-dependent, which is slower but still correct.
|
|
197
|
+
base = getattr(rv_continuous, "_get_support", None)
|
|
198
|
+
own = getattr(type(scipydist), "_get_support", None)
|
|
199
|
+
if base is None or own is None:
|
|
200
|
+
return True
|
|
201
|
+
return own is not base
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _natural_bounds(scipydist) -> Tuple[bool, bool]:
|
|
205
|
+
# (has lower bound, has upper bound) for the standard form of the
|
|
206
|
+
# distribution. For a shape-dependent support the answer is not known
|
|
207
|
+
# until the distribution is fitted, so both are reported as possible.
|
|
208
|
+
if _shape_dependent_support(scipydist):
|
|
209
|
+
return True, True
|
|
210
|
+
return bool(np.isfinite(scipydist.a)), bool(np.isfinite(scipydist.b))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _check_natural_bounds(scipydist, distName: str,
|
|
214
|
+
lower: Optional[float],
|
|
215
|
+
upper: Optional[float]) -> None:
|
|
216
|
+
has_lower, has_upper = _natural_bounds(scipydist)
|
|
217
|
+
if lower is not None and not has_lower:
|
|
218
|
+
raise ValueError(f"the {distName} distribution has no natural lower "
|
|
219
|
+
f"bound, so lowerBound cannot be fixed")
|
|
220
|
+
if upper is not None and not has_upper:
|
|
221
|
+
raise ValueError(f"the {distName} distribution has no natural upper "
|
|
222
|
+
f"bound, so upperBound cannot be fixed")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _fitted_support_flags(dist) -> Optional[Tuple[bool, bool]]:
|
|
226
|
+
# (lower bound is finite, upper bound is finite) for a frozen
|
|
227
|
+
# distribution, or None if the object cannot report its support
|
|
228
|
+
support = getattr(dist, "support", None)
|
|
229
|
+
if support is None:
|
|
230
|
+
return None
|
|
231
|
+
try:
|
|
232
|
+
lo, hi = support()
|
|
233
|
+
except Exception:
|
|
234
|
+
return None
|
|
235
|
+
return bool(np.isfinite(lo)), bool(np.isfinite(hi))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
# maximum likelihood with one or both bounds of the support fixed
|
|
240
|
+
#
|
|
241
|
+
# A scipy distribution with standard support [a, b] has support
|
|
242
|
+
# [loc + a*scale, loc + b*scale]. Fixing the lower bound at L therefore
|
|
243
|
+
# imposes loc + a*scale = L, and fixing the upper bound at U imposes
|
|
244
|
+
# loc + b*scale = U. When a constraint pins a single scipy parameter
|
|
245
|
+
# (a == 0 or b == 0, or both bounds fixed with constant a and b), scipy's
|
|
246
|
+
# own `fit` is used with `floc`/`fscale`. Otherwise the constraint couples
|
|
247
|
+
# loc and scale (or depends on the shape parameters through a and b), and the
|
|
248
|
+
# penalized negative log-likelihood is minimized directly over the remaining
|
|
249
|
+
# free parameters, mirroring scipy's optimizer and penalty.
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _start_values(scipydist, dataA: np.ndarray) -> Tuple[float, ...]:
|
|
253
|
+
# (shapes..., loc, scale) starting values; `_fitstart` is what scipy's
|
|
254
|
+
# own `fit` uses and is the only source of its per-distribution
|
|
255
|
+
# heuristics, so it is used here with a plain moment-based fallback
|
|
256
|
+
fitstart = getattr(scipydist, "_fitstart", None)
|
|
257
|
+
if fitstart is not None:
|
|
258
|
+
try:
|
|
259
|
+
start = tuple(float(v) for v in fitstart(dataA))
|
|
260
|
+
if len(start) == scipydist.numargs + 2:
|
|
261
|
+
return start
|
|
262
|
+
except Exception:
|
|
263
|
+
pass
|
|
264
|
+
shapes = (1.0,) * scipydist.numargs
|
|
265
|
+
return shapes + (float(np.mean(dataA)), float(np.std(dataA)) or 1.0)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _verified(scipydist, params: Tuple[float, ...],
|
|
269
|
+
lower: Optional[float], upper: Optional[float]
|
|
270
|
+
) -> Tuple[float, ...]:
|
|
271
|
+
# confirm that the fitted distribution really has the requested bounds
|
|
272
|
+
with np.errstate(all="ignore"):
|
|
273
|
+
lo, hi = scipydist.support(*params)
|
|
274
|
+
scale = params[-1]
|
|
275
|
+
if not (np.isfinite(scale) and scale > 0.0):
|
|
276
|
+
raise stats.FitError("Optimization converged to parameters that "
|
|
277
|
+
"are outside the range allowed by the "
|
|
278
|
+
"distribution.")
|
|
279
|
+
tol = 1e-7 * (1.0 + abs(scale))
|
|
280
|
+
if lower is not None and not np.isclose(lo, lower, rtol=1e-7, atol=tol):
|
|
281
|
+
raise stats.FitError(f"fitted lower bound {lo} does not match the "
|
|
282
|
+
f"requested lowerBound {lower}")
|
|
283
|
+
if upper is not None and not np.isclose(hi, upper, rtol=1e-7, atol=tol):
|
|
284
|
+
raise stats.FitError(f"fitted upper bound {hi} does not match the "
|
|
285
|
+
f"requested upperBound {upper}")
|
|
286
|
+
return tuple(params)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _fit_coupled(scipydist, dataA: np.ndarray,
|
|
290
|
+
lower: Optional[float], upper: Optional[float]
|
|
291
|
+
) -> Tuple[float, ...]:
|
|
292
|
+
# free parameters `theta` are the shapes, followed by the scale when only
|
|
293
|
+
# one bound is fixed; loc (and scale, when both bounds are fixed) follow
|
|
294
|
+
# from the constraint(s) and the standard support for those shapes
|
|
295
|
+
numargs = scipydist.numargs
|
|
296
|
+
both = lower is not None and upper is not None
|
|
297
|
+
|
|
298
|
+
def expand(theta) -> Tuple[float, ...]:
|
|
299
|
+
shapes = tuple(float(t) for t in theta[:numargs])
|
|
300
|
+
with np.errstate(all="ignore"):
|
|
301
|
+
a, b = scipydist.support(*shapes)
|
|
302
|
+
a, b = np.float64(a), np.float64(b)
|
|
303
|
+
if both:
|
|
304
|
+
scale = np.float64(upper - lower) / (b - a)
|
|
305
|
+
loc = lower - a * scale
|
|
306
|
+
elif lower is not None:
|
|
307
|
+
scale = np.float64(theta[numargs])
|
|
308
|
+
loc = lower - a * scale
|
|
309
|
+
else:
|
|
310
|
+
scale = np.float64(theta[numargs])
|
|
311
|
+
loc = upper - b * scale
|
|
312
|
+
return shapes + (float(loc), float(scale))
|
|
313
|
+
|
|
314
|
+
def nnlf(theta) -> float:
|
|
315
|
+
params = expand(theta)
|
|
316
|
+
loc, scale = params[-2:]
|
|
317
|
+
if not (np.isfinite(loc) and np.isfinite(scale)) or scale <= 0.0:
|
|
318
|
+
return np.inf
|
|
319
|
+
with np.errstate(all="ignore"):
|
|
320
|
+
logpdf = scipydist.logpdf(dataA, *params)
|
|
321
|
+
finite = np.isfinite(logpdf)
|
|
322
|
+
nbad = int(np.count_nonzero(~finite))
|
|
323
|
+
return -float(logpdf[finite].sum()) + nbad * _PENALTY
|
|
324
|
+
|
|
325
|
+
def initial_theta(shapes, scale0: float):
|
|
326
|
+
if both:
|
|
327
|
+
return list(shapes)
|
|
328
|
+
if not (np.isfinite(scale0) and scale0 > 0.0):
|
|
329
|
+
scale0 = float(np.std(dataA)) or 1.0
|
|
330
|
+
# widen the starting scale, if needed, so that the constrained
|
|
331
|
+
# support covers the data and the optimizer starts unpenalized
|
|
332
|
+
with np.errstate(all="ignore"):
|
|
333
|
+
a, b = scipydist.support(*shapes)
|
|
334
|
+
if np.isfinite(a) and np.isfinite(b) and b > a:
|
|
335
|
+
if lower is not None:
|
|
336
|
+
needed = (dataA.max() - lower) / (b - a)
|
|
337
|
+
else:
|
|
338
|
+
needed = (upper - dataA.min()) / (b - a)
|
|
339
|
+
if np.isfinite(needed) and needed > 0.0:
|
|
340
|
+
scale0 = max(scale0, 1.1 * needed)
|
|
341
|
+
return list(shapes) + [scale0]
|
|
342
|
+
|
|
343
|
+
start = _start_values(scipydist, dataA)
|
|
344
|
+
shapes0, scale0 = start[:numargs], start[-1]
|
|
345
|
+
# a sign flip of the shapes rescues families whose support is bounded on
|
|
346
|
+
# one side or the other depending on the sign of a shape parameter
|
|
347
|
+
trials = [shapes0]
|
|
348
|
+
if any(s != 0.0 for s in shapes0):
|
|
349
|
+
trials.append(tuple(-s for s in shapes0))
|
|
350
|
+
for shapes in trials:
|
|
351
|
+
theta0 = initial_theta(shapes, scale0)
|
|
352
|
+
if np.isfinite(nnlf(theta0)):
|
|
353
|
+
break
|
|
354
|
+
else:
|
|
355
|
+
raise stats.FitError("could not find starting values consistent "
|
|
356
|
+
"with the requested bounds")
|
|
357
|
+
|
|
358
|
+
if len(theta0) == 0:
|
|
359
|
+
# nothing free to optimize: both bounds fixed, no shape parameters
|
|
360
|
+
theta = theta0
|
|
361
|
+
else:
|
|
362
|
+
theta = optimize.fmin(nnlf, theta0, disp=0)
|
|
363
|
+
if not np.isfinite(nnlf(theta)):
|
|
364
|
+
raise stats.FitError("Optimization converged to parameters that "
|
|
365
|
+
"are outside the range allowed by the "
|
|
366
|
+
"distribution.")
|
|
367
|
+
return _verified(scipydist, expand(theta), lower, upper)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _fit_with_bounds(scipydist, dataA: np.ndarray,
|
|
371
|
+
lower: Optional[float], upper: Optional[float]
|
|
372
|
+
) -> Tuple[float, ...]:
|
|
373
|
+
if not _shape_dependent_support(scipydist):
|
|
374
|
+
a, b = float(scipydist.a), float(scipydist.b)
|
|
375
|
+
if lower is not None and upper is not None:
|
|
376
|
+
scale = (upper - lower) / (b - a)
|
|
377
|
+
loc = lower - a * scale
|
|
378
|
+
if scipydist.numargs == 0:
|
|
379
|
+
params = (loc, scale)
|
|
380
|
+
else:
|
|
381
|
+
params = scipydist.fit(dataA, floc=loc, fscale=scale)
|
|
382
|
+
return _verified(scipydist, params, lower, upper)
|
|
383
|
+
if lower is not None and a == 0.0:
|
|
384
|
+
params = scipydist.fit(dataA, floc=lower)
|
|
385
|
+
return _verified(scipydist, params, lower, upper)
|
|
386
|
+
if upper is not None and b == 0.0:
|
|
387
|
+
params = scipydist.fit(dataA, floc=upper)
|
|
388
|
+
return _verified(scipydist, params, lower, upper)
|
|
389
|
+
return _fit_coupled(scipydist, dataA, lower, upper)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
# public API
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def fit(data: conversions.VectorLike,
|
|
397
|
+
scipydist: rv_continuous,
|
|
398
|
+
distName: Optional[str] = None,
|
|
399
|
+
lowerBound: Optional[float] = None,
|
|
400
|
+
upperBound: Optional[float] = None
|
|
401
|
+
) -> FitResult:
|
|
402
|
+
"""
|
|
403
|
+
Fit a univariate distribution for a continuous random variable.
|
|
404
|
+
|
|
405
|
+
This function fits a given scipy.stats univariate distribution to the
|
|
406
|
+
provided data using maximum likelihood estimation. It returns a named
|
|
407
|
+
tuple containing information criteria, goodness-of-fit p-values, the
|
|
408
|
+
frozen distribution, and the distribution name.
|
|
409
|
+
|
|
410
|
+
Parameters
|
|
411
|
+
----------
|
|
412
|
+
data : VectorLike
|
|
413
|
+
The data to fit, as a one-dimensional array or similar.
|
|
414
|
+
scipydist : scipy.stats.rv_continuous
|
|
415
|
+
The scipy.stats distribution object to fit (e.g., stats.norm).
|
|
416
|
+
distName : str, optional
|
|
417
|
+
Name of the distribution. If None, the distribution's name is used.
|
|
418
|
+
lowerBound : float, optional
|
|
419
|
+
If given, the lower bound of the support of the fitted distribution
|
|
420
|
+
is fixed at this value rather than estimated. The distribution must
|
|
421
|
+
have a natural lower bound (e.g., ``stats.expon`` or ``stats.beta``,
|
|
422
|
+
but not ``stats.norm``). Must be finite and no greater than the
|
|
423
|
+
smallest observation. If omitted, all parameters are estimated.
|
|
424
|
+
upperBound : float, optional
|
|
425
|
+
If given, the upper bound of the support of the fitted distribution
|
|
426
|
+
is fixed at this value rather than estimated. The distribution must
|
|
427
|
+
have a natural upper bound (e.g., ``stats.beta`` or
|
|
428
|
+
``stats.weibull_max``). Must be finite, no less than the largest
|
|
429
|
+
observation, and greater than `lowerBound` if both are given. If
|
|
430
|
+
omitted, all parameters are estimated.
|
|
431
|
+
|
|
432
|
+
Returns
|
|
433
|
+
-------
|
|
434
|
+
FitResult
|
|
435
|
+
A Named tuple with fields:
|
|
436
|
+
|
|
437
|
+
bic : float
|
|
438
|
+
Bayesian Information Criterion for the fit.
|
|
439
|
+
aic : float
|
|
440
|
+
Akaike Information Criterion for the fit.
|
|
441
|
+
ad_pval : float
|
|
442
|
+
Anderson-Darling heuristic goodness-of-fit score (see Notes).
|
|
443
|
+
cvm_pval : float
|
|
444
|
+
Cramer-von Mises heuristic goodness-of-fit score (see Notes).
|
|
445
|
+
dist : scipy.stats.rv_continuous
|
|
446
|
+
The frozen fitted distribution object.
|
|
447
|
+
distName : str
|
|
448
|
+
Name of the fitted distribution.
|
|
449
|
+
|
|
450
|
+
Raises
|
|
451
|
+
------
|
|
452
|
+
TypeError
|
|
453
|
+
If `lowerBound` or `upperBound` is not a real number or None
|
|
454
|
+
(bools are rejected).
|
|
455
|
+
ValueError
|
|
456
|
+
If a bound is not finite, if the data fall outside a fixed bound,
|
|
457
|
+
if ``lowerBound >= upperBound``, or if a bound is requested for a
|
|
458
|
+
distribution without the corresponding natural bound.
|
|
459
|
+
|
|
460
|
+
Notes
|
|
461
|
+
-----
|
|
462
|
+
The function uses maximum likelihood estimation for parameter fitting.
|
|
463
|
+
Goodness of fit is assessed using the Anderson-Darling and
|
|
464
|
+
Cramer-von Mises statistics.
|
|
465
|
+
|
|
466
|
+
Fixing a bound is a constraint on the distribution's location and scale
|
|
467
|
+
parameters (a scipy distribution with standard support ``[a, b]`` has
|
|
468
|
+
support ``[loc + a*scale, loc + b*scale]``); it is not truncation. Each
|
|
469
|
+
fixed bound removes one free parameter, and the parameter counts used
|
|
470
|
+
for AIC and BIC are reduced accordingly.
|
|
471
|
+
|
|
472
|
+
The `ad_pval` and `cvm_pval` values are computed from tests that assume
|
|
473
|
+
a fully specified (a priori) null distribution, but the parameters here
|
|
474
|
+
are estimated from the same sample being tested. Consequently these
|
|
475
|
+
values systematically overstate goodness of fit (the Lilliefors
|
|
476
|
+
problem) and are *not* valid p-values. Treat them only as heuristic
|
|
477
|
+
scores for comparing candidate distributions with equal numbers of
|
|
478
|
+
parameters; do not use them for formal hypothesis tests.
|
|
479
|
+
"""
|
|
480
|
+
|
|
481
|
+
dataA = conversions.vlToArray(data)
|
|
482
|
+
|
|
483
|
+
lower = _validate_bound(lowerBound, "lowerBound")
|
|
484
|
+
upper = _validate_bound(upperBound, "upperBound")
|
|
485
|
+
nfixed = int(lower is not None) + int(upper is not None)
|
|
486
|
+
if nfixed > 0:
|
|
487
|
+
_check_bounds_against_data(dataA, lower, upper)
|
|
488
|
+
name = distName if distName is not None else \
|
|
489
|
+
getattr(scipydist, "name", type(scipydist).__name__)
|
|
490
|
+
_check_natural_bounds(scipydist, name, lower, upper)
|
|
491
|
+
|
|
492
|
+
with warnings.catch_warnings(record=True) as w:
|
|
493
|
+
warnings.simplefilter("always")
|
|
494
|
+
# deprecation chatter from third-party libraries is not evidence
|
|
495
|
+
# of a bad fit; do not let it disqualify a candidate
|
|
496
|
+
warnings.simplefilter("ignore", DeprecationWarning)
|
|
497
|
+
warnings.simplefilter("ignore", PendingDeprecationWarning)
|
|
498
|
+
warnings.simplefilter("ignore", FutureWarning)
|
|
499
|
+
|
|
500
|
+
# fit distribution using maximum likelihood
|
|
501
|
+
if nfixed == 0:
|
|
502
|
+
params = scipydist.fit(data)
|
|
503
|
+
else:
|
|
504
|
+
params = _fit_with_bounds(scipydist, dataA, lower, upper)
|
|
505
|
+
|
|
506
|
+
# create a "frozen" distribution object
|
|
507
|
+
dist = scipydist(*params)
|
|
508
|
+
|
|
509
|
+
# calculate log likelihood function and info criteria; each fixed
|
|
510
|
+
# bound removes one free parameter
|
|
511
|
+
loglike = dist.logpdf(dataA).sum()
|
|
512
|
+
k = len(params) - nfixed
|
|
513
|
+
bic = np.log(len(dataA)) * k - 2.0 * loglike # Schwarz
|
|
514
|
+
aic = 2.0 * k - 2.0 * loglike # Akaike
|
|
515
|
+
|
|
516
|
+
# p-values for GOF tests
|
|
517
|
+
ad_pval = adtest(dataA, dist)[1] # Anderson-Darling
|
|
518
|
+
cvm_pval = cvmtest(dataA, dist)[1] # Cramer-von Mises
|
|
519
|
+
|
|
520
|
+
return FitResult(bic=bic, aic=aic, ad_pval=ad_pval, cvm_pval=cvm_pval,
|
|
521
|
+
dist=dist, distName=distName, warnings=w)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _fit_all(data, dist_list, lower=None, upper=None):
|
|
525
|
+
# fit every candidate, isolating failures so that one distribution
|
|
526
|
+
# raising (e.g., scipy.stats.FitError) cannot abort the comparison
|
|
527
|
+
results = []
|
|
528
|
+
failures = []
|
|
529
|
+
for name, dist, *_ in dist_list:
|
|
530
|
+
try:
|
|
531
|
+
results.append(fit(data, dist, name,
|
|
532
|
+
lowerBound=lower, upperBound=upper))
|
|
533
|
+
except Exception as e:
|
|
534
|
+
failures.append((name, e))
|
|
535
|
+
results = sorted(results, key=lambda r: r.bic) # lowest BIC to highest
|
|
536
|
+
return results, failures
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _fstr(value, nchars=8):
|
|
540
|
+
return ("%.3f" % value).rjust(nchars)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _result_line(r, header=False):
|
|
544
|
+
if header is True:
|
|
545
|
+
return (" distribution,"
|
|
546
|
+
" BIC, AIC, AD_score, CvM_score\n")
|
|
547
|
+
else:
|
|
548
|
+
return ("%s, %s, %s, %s, %s\n" %
|
|
549
|
+
(r.distName.rjust(30), _fstr(r.bic), _fstr(r.aic),
|
|
550
|
+
_fstr(r.ad_pval, 6), _fstr(r.cvm_pval, 7)))
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _resolve_side(limit_name: str, limit, bound_name: str, bound
|
|
554
|
+
) -> Tuple[bool, Optional[float]]:
|
|
555
|
+
# reconcile the legacy bool flag and the new float bound for one side of
|
|
556
|
+
# the support -> (distributions must be bounded on this side, value the
|
|
557
|
+
# bound is fixed at or None)
|
|
558
|
+
if limit is not None and not isinstance(limit, (bool, np.bool_)):
|
|
559
|
+
raise TypeError(f"{limit_name} must be a bool or None, not "
|
|
560
|
+
f"{type(limit).__name__}; to fix the bound at a "
|
|
561
|
+
f"value, use {bound_name}")
|
|
562
|
+
fixed = _validate_bound(bound, bound_name)
|
|
563
|
+
if limit is not None and fixed is not None:
|
|
564
|
+
raise ValueError(f"{limit_name} and {bound_name} cannot both be "
|
|
565
|
+
f"given")
|
|
566
|
+
if fixed is not None:
|
|
567
|
+
return True, fixed
|
|
568
|
+
return (bool(limit) if limit is not None else False), None
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def compare(data: conversions.VectorLike,
|
|
572
|
+
lowerLimit: Optional[bool] = None,
|
|
573
|
+
upperLimit: Optional[bool] = None,
|
|
574
|
+
*,
|
|
575
|
+
lowerBound: Optional[float] = None,
|
|
576
|
+
upperBound: Optional[float] = None
|
|
577
|
+
) -> str:
|
|
578
|
+
"""
|
|
579
|
+
Compare fits of univariate distributions for a continuous random variable.
|
|
580
|
+
|
|
581
|
+
This function fits all candidate distributions with the specified support
|
|
582
|
+
(bounded below and/or above, or not) to the provided data. It returns a
|
|
583
|
+
formatted string summarizing the Bayesian Information Criterion (BIC),
|
|
584
|
+
Akaike Information Criterion (AIC), and goodness-of-fit p-values for
|
|
585
|
+
each distribution.
|
|
586
|
+
|
|
587
|
+
Parameters
|
|
588
|
+
----------
|
|
589
|
+
data : array-like
|
|
590
|
+
The data to fit, as a one-dimensional array or sequence.
|
|
591
|
+
lowerLimit : bool, optional
|
|
592
|
+
Deprecated; use `lowerBound` (see Notes). If True, only
|
|
593
|
+
distributions with a lower bound are considered, and the bound is
|
|
594
|
+
estimated along with the other parameters. If False, only
|
|
595
|
+
distributions without a lower bound are considered. Passing this
|
|
596
|
+
argument emits a ``FutureWarning``.
|
|
597
|
+
upperLimit : bool, optional
|
|
598
|
+
Deprecated; use `upperBound` (see Notes). If True, only
|
|
599
|
+
distributions with an upper bound are considered, and the bound is
|
|
600
|
+
estimated along with the other parameters. If False, only
|
|
601
|
+
distributions without an upper bound are considered. Passing this
|
|
602
|
+
argument emits a ``FutureWarning``.
|
|
603
|
+
lowerBound : float, optional
|
|
604
|
+
Keyword only. If given, only distributions with a lower bound are
|
|
605
|
+
considered, and each is fitted with its lower bound fixed at this
|
|
606
|
+
value (which must be finite and no greater than the smallest
|
|
607
|
+
observation). If omitted, and `lowerLimit` is not given, only
|
|
608
|
+
distributions without a lower bound are considered.
|
|
609
|
+
upperBound : float, optional
|
|
610
|
+
Keyword only. If given, only distributions with an upper bound are
|
|
611
|
+
considered, and each is fitted with its upper bound fixed at this
|
|
612
|
+
value (which must be finite, no less than the largest observation,
|
|
613
|
+
and greater than `lowerBound` if that is also given). If omitted,
|
|
614
|
+
and `upperLimit` is not given, only distributions without an upper
|
|
615
|
+
bound are considered.
|
|
616
|
+
|
|
617
|
+
Returns
|
|
618
|
+
-------
|
|
619
|
+
str
|
|
620
|
+
A formatted string summarizing the fit statistics for each candidate
|
|
621
|
+
distribution, sorted by BIC (best to worst).
|
|
622
|
+
|
|
623
|
+
Raises
|
|
624
|
+
------
|
|
625
|
+
TypeError
|
|
626
|
+
If `lowerLimit` or `upperLimit` is not a bool, or if `lowerBound` or
|
|
627
|
+
`upperBound` is not a real number.
|
|
628
|
+
ValueError
|
|
629
|
+
If a bound is not finite, if the data fall outside a fixed bound, if
|
|
630
|
+
``lowerBound >= upperBound``, or if the legacy flag and the bound
|
|
631
|
+
are both given for the same side of the support.
|
|
632
|
+
|
|
633
|
+
Notes
|
|
634
|
+
-----
|
|
635
|
+
For reliable results, at least 50 observations are recommended. The summary
|
|
636
|
+
includes BIC, AIC, and Anderson-Darling and Cramer-von Mises heuristic
|
|
637
|
+
scores for each distribution.
|
|
638
|
+
|
|
639
|
+
Fixing a bound is a constraint on a distribution's location and scale
|
|
640
|
+
parameters, not truncation. Each fixed bound removes one free parameter
|
|
641
|
+
from the count used for AIC and BIC.
|
|
642
|
+
|
|
643
|
+
A few candidate families (the generalized extreme value, generalized
|
|
644
|
+
Pareto, four-parameter kappa, and Tukey lambda distributions) are
|
|
645
|
+
bounded on different sides in different regions of their shape-parameter
|
|
646
|
+
space. These are considered in every group they can belong to, and a
|
|
647
|
+
fitted distribution whose support does not match the requested group is
|
|
648
|
+
dropped from the results with a warning.
|
|
649
|
+
|
|
650
|
+
`lowerLimit` and `upperLimit` form the legacy interface, in which the
|
|
651
|
+
bounds are estimated rather than fixed. They still work but emit a
|
|
652
|
+
``FutureWarning`` and will be removed in a future version; use
|
|
653
|
+
`lowerBound` and `upperBound` instead. The legacy flag for one side of
|
|
654
|
+
the support may be combined with the bound for the other side (e.g.,
|
|
655
|
+
``compare(data, upperLimit=True, lowerBound=0.0)``), but not with the
|
|
656
|
+
bound for the same side.
|
|
657
|
+
|
|
658
|
+
The AD_score and CvM_score columns are nominal p-values from tests that
|
|
659
|
+
assume a fully specified (a priori) null distribution, but the
|
|
660
|
+
parameters are estimated from the same sample being tested. These
|
|
661
|
+
values therefore systematically overstate goodness of fit (the
|
|
662
|
+
Lilliefors problem) and are *not* valid p-values. Use them only as
|
|
663
|
+
heuristic scores for ranking candidate distributions; rankings by BIC
|
|
664
|
+
or AIC additionally account for differing numbers of parameters.
|
|
665
|
+
"""
|
|
666
|
+
has_lower, lower = _resolve_side("lowerLimit", lowerLimit,
|
|
667
|
+
"lowerBound", lowerBound)
|
|
668
|
+
has_upper, upper = _resolve_side("upperLimit", upperLimit,
|
|
669
|
+
"upperBound", upperBound)
|
|
670
|
+
legacy = [name for name, value in (("lowerLimit", lowerLimit),
|
|
671
|
+
("upperLimit", upperLimit))
|
|
672
|
+
if value is not None]
|
|
673
|
+
if legacy:
|
|
674
|
+
msg = (f"the {' and '.join(legacy)} argument"
|
|
675
|
+
f"{'s' if len(legacy) > 1 else ''} of compare "
|
|
676
|
+
f"{'are' if len(legacy) > 1 else 'is'} deprecated and will "
|
|
677
|
+
f"be removed in a future version of funcsim; use lowerBound "
|
|
678
|
+
f"and upperBound instead: pass a float to restrict the "
|
|
679
|
+
f"comparison to distributions with that bound, fixed at the "
|
|
680
|
+
f"given value, or omit the argument for distributions "
|
|
681
|
+
f"without that bound")
|
|
682
|
+
warnings.warn(msg, FutureWarning, stacklevel=2)
|
|
683
|
+
|
|
684
|
+
dataA = conversions.vlToArray(data)
|
|
685
|
+
_check_bounds_against_data(dataA, lower, upper)
|
|
686
|
+
|
|
687
|
+
if len(dataA) < 50:
|
|
688
|
+
msg = (f"using 'compare' with only {len(dataA)} observations "
|
|
689
|
+
f"can produce unreliable results. Interpret "
|
|
690
|
+
f"with caution.")
|
|
691
|
+
warnings.warn(msg, UserWarning)
|
|
692
|
+
dist_list = [d for d in candidates if d[2] == has_lower and
|
|
693
|
+
d[3] == has_upper]
|
|
694
|
+
results, failures = _fit_all(dataA, dist_list, lower, upper)
|
|
695
|
+
results_edit = []
|
|
696
|
+
mismatched = []
|
|
697
|
+
for r in results:
|
|
698
|
+
if len(r.warnings) > 0:
|
|
699
|
+
msg = (f"encountered a problem "
|
|
700
|
+
f"while fitting the {r.distName} distribution. "
|
|
701
|
+
f"It will not be included in the results.")
|
|
702
|
+
warnings.warn(msg, RuntimeWarning)
|
|
703
|
+
continue
|
|
704
|
+
flags = _fitted_support_flags(r.dist)
|
|
705
|
+
if flags is not None and flags != (has_lower, has_upper):
|
|
706
|
+
mismatched.append(r.distName)
|
|
707
|
+
continue
|
|
708
|
+
results_edit.append(r)
|
|
709
|
+
if mismatched:
|
|
710
|
+
msg = (f"the fitted {', '.join(mismatched)} distribution(s) do not "
|
|
711
|
+
f"have the requested support (lower bound: {has_lower}, "
|
|
712
|
+
f"upper bound: {has_upper}) and will not be included in the "
|
|
713
|
+
f"results.")
|
|
714
|
+
warnings.warn(msg, RuntimeWarning)
|
|
715
|
+
for name, exc in failures:
|
|
716
|
+
msg = (f"fitting the {name} distribution raised "
|
|
717
|
+
f"{type(exc).__name__}: {exc}. It will not be included "
|
|
718
|
+
f"in the results.")
|
|
719
|
+
warnings.warn(msg, RuntimeWarning)
|
|
720
|
+
lines = [_result_line(None, header=True)] + \
|
|
721
|
+
list(map(_result_line, results_edit))
|
|
722
|
+
return "".join(lines)
|
funcsim-0.2.2/funcsim/distfit.py
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import sys
|
|
3
|
-
from collections import namedtuple
|
|
4
|
-
import scipy.stats as stats
|
|
5
|
-
from scipy.stats._distn_infrastructure import rv_continuous, rv_discrete
|
|
6
|
-
import numpy as np
|
|
7
|
-
import warnings
|
|
8
|
-
from .ecdfgof import adtest, cvmtest
|
|
9
|
-
from . import conversions
|
|
10
|
-
from typing import Optional
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# "name","scipy_name","lower_limit","upper_limit
|
|
14
|
-
candidates = [
|
|
15
|
-
("alpha",stats.alpha,True,False),
|
|
16
|
-
("anglit",stats.anglit,True,True),
|
|
17
|
-
("arcsine",stats.arcsine,True,True),
|
|
18
|
-
("argus",stats.argus,True,True),
|
|
19
|
-
("Beta",stats.beta,True,True),
|
|
20
|
-
("Beta Prime",stats.betaprime,True,False),
|
|
21
|
-
("Bradford",stats.bradford,True,True),
|
|
22
|
-
("Burr",stats.burr,True,False),
|
|
23
|
-
("Burr Type XII",stats.burr12,True,False),
|
|
24
|
-
("Cauchy",stats.cauchy,False,False),
|
|
25
|
-
("Chi",stats.chi,True,False),
|
|
26
|
-
("Chi^2",stats.chi2,True,False),
|
|
27
|
-
("Cosine",stats.cosine,True,True),
|
|
28
|
-
("Crystal Ball",stats.crystalball,False,False),
|
|
29
|
-
("Double Gamma",stats.dgamma,False,False),
|
|
30
|
-
# ("Double Pareto Lognormal",stats.dpareto_lognorm,True,False),
|
|
31
|
-
("Double Weibull",stats.dweibull,False,False),
|
|
32
|
-
("Erlang",stats.erlang,True,False),
|
|
33
|
-
("Exponential",stats.expon,True,False),
|
|
34
|
-
("Expone. Modified Normal",stats.exponnorm,False,False),
|
|
35
|
-
("Exponentiated Weibull",stats.exponweib,True,False),
|
|
36
|
-
("Exponential Power",stats.exponpow,True,False),
|
|
37
|
-
("F",stats.f,True,False),
|
|
38
|
-
("Fatigue Life",stats.fatiguelife,True,False),
|
|
39
|
-
("Fisk",stats.fisk,True,False),
|
|
40
|
-
("Folded Cauchy",stats.foldcauchy,True,False),
|
|
41
|
-
("Folded Normal",stats.foldnorm,True,False),
|
|
42
|
-
("Generalized Logistic",stats.genlogistic,False,False),
|
|
43
|
-
("Generalized Normal",stats.gennorm,False,False),
|
|
44
|
-
("Generalized Pareto",stats.genpareto,True,False),
|
|
45
|
-
("Generalized Exponential",stats.genexpon,True,False),
|
|
46
|
-
("Generalized Extreme Value",stats.genextreme,True,True),
|
|
47
|
-
("Gauss Hypergeometric",stats.gausshyper,True,True),
|
|
48
|
-
("Gamma",stats.gamma,True,False),
|
|
49
|
-
("Generalized Gamma",stats.gengamma,True,False),
|
|
50
|
-
("Generalized Half-Logistic",stats.genhalflogistic,True,True),
|
|
51
|
-
("Generalized Hyperbolic",stats.genhyperbolic,False,False),
|
|
52
|
-
("Generalized Inverse Gaussian",stats.geninvgauss,True,False),
|
|
53
|
-
("Gibrat",stats.gibrat,True,False),
|
|
54
|
-
("Gompertz",stats.gompertz,True,False),
|
|
55
|
-
("Gumbel Right-skewed",stats.gumbel_r,False,False),
|
|
56
|
-
("Gumbel Left-skewed",stats.gumbel_l,False,False),
|
|
57
|
-
("Half-Cauchy",stats.halfcauchy,True,False),
|
|
58
|
-
("Half-Logistic",stats.halflogistic,True,False),
|
|
59
|
-
("Half-Normal",stats.halfnorm,True,False),
|
|
60
|
-
("Half Generalized Normal",stats.halfgennorm,True,False),
|
|
61
|
-
("Hyperbolic Secant",stats.hypsecant,False,False),
|
|
62
|
-
("Inverse Gamma",stats.invgamma,True,False),
|
|
63
|
-
("Inverse Gaussian",stats.invgauss,True,False),
|
|
64
|
-
("Inverse Weibull",stats.invweibull,True,False),
|
|
65
|
-
# ("Irwin-Hall",stats.irwinhall,True,True),
|
|
66
|
-
("Jones and Faddy Skew-T",stats.jf_skew_t,False,False),
|
|
67
|
-
("Johnson's S_B",stats.johnsonsb,True,True),
|
|
68
|
-
("Johnson's S_U",stats.johnsonsu,False,False),
|
|
69
|
-
("Four-parameter Kappa",stats.kappa4,False,False),
|
|
70
|
-
("3-Param Kappa Distribution",stats.kappa3,True,False),
|
|
71
|
-
# ("Landau",stats.landau,False,False),
|
|
72
|
-
("Laplace",stats.laplace,False,False),
|
|
73
|
-
("Asymmetric Laplace",stats.laplace_asymmetric,False,False),
|
|
74
|
-
("Lévy",stats.levy,True,False),
|
|
75
|
-
("Left-skewed Lévy",stats.levy_l,False,True),
|
|
76
|
-
("Lévy stable",stats.levy_stable,False,False),
|
|
77
|
-
("Logistic",stats.logistic,False,False),
|
|
78
|
-
("Log-gamma",stats.loggamma,False,False),
|
|
79
|
-
("Log-Laplace",stats.loglaplace,True,False),
|
|
80
|
-
("Log-normal",stats.lognorm,True,False),
|
|
81
|
-
("Log-uniform",stats.loguniform,True,True),
|
|
82
|
-
("Lomax",stats.lomax,True,False),
|
|
83
|
-
("Maxwell",stats.maxwell,True,False),
|
|
84
|
-
("Mielke's Beta-Kappa",stats.mielke,True,False),
|
|
85
|
-
("Moyal",stats.moyal,False,False),
|
|
86
|
-
("Nakagami",stats.nakagami,True,False),
|
|
87
|
-
("Noncentral chi^2",stats.ncx2,True,False),
|
|
88
|
-
("Noncentral F",stats.ncf,True,False),
|
|
89
|
-
("Noncentral Student’s t",stats.nct,False,False),
|
|
90
|
-
("Normal",stats.norm,False,False),
|
|
91
|
-
("Normal inverse Gaussian",stats.norminvgauss,False,False),
|
|
92
|
-
("Pareto",stats.pareto,True,False),
|
|
93
|
-
("Pearson Type III",stats.pearson3,False,False),
|
|
94
|
-
("Power-function",stats.powerlaw,True,True),
|
|
95
|
-
("Power log-normal",stats.powerlognorm,True,False),
|
|
96
|
-
("Power normal",stats.powernorm,False,False),
|
|
97
|
-
("symmetric beta",stats.rdist,True,True),
|
|
98
|
-
("Rayleigh",stats.rayleigh,True,False),
|
|
99
|
-
("Relativistic Breit–Wigner",stats.rel_breitwigner,True,False),
|
|
100
|
-
("Rice",stats.rice,True,False),
|
|
101
|
-
("Reciprocal inverse Gaussian",stats.recipinvgauss,True,False),
|
|
102
|
-
("Semicircular",stats.semicircular,True,True),
|
|
103
|
-
("Skewed Cauchy",stats.skewcauchy,False,False),
|
|
104
|
-
("Skew-normal",stats.skewnorm,False,False),
|
|
105
|
-
("Studentized range",stats.studentized_range,True,False),
|
|
106
|
-
("Student's t",stats.t,False,False),
|
|
107
|
-
("Trapezoidal",stats.trapezoid,True,True),
|
|
108
|
-
("Triangular",stats.triang,True,True),
|
|
109
|
-
("Truncated exponential",stats.truncexpon,True,True),
|
|
110
|
-
("Truncated normal",stats.truncnorm,True,True),
|
|
111
|
-
("Truncated Pareto",stats.truncpareto,True,True),
|
|
112
|
-
("Truncated Weibull minimum",stats.truncweibull_min,True,True),
|
|
113
|
-
("Tukey lambda",stats.tukeylambda,True,True),
|
|
114
|
-
("Uniform",stats.uniform,True,True),
|
|
115
|
-
("Von Mises",stats.vonmises,True,True),
|
|
116
|
-
("Wald",stats.wald,True,False),
|
|
117
|
-
("Weibull Min Extreme Value",stats.weibull_min,True,False),
|
|
118
|
-
("Weibull Max Extreme Value",stats.weibull_max,False,True),
|
|
119
|
-
("Wrapped Cauchy",stats.wrapcauchy,True,True),
|
|
120
|
-
]
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
# container for "fit" results
|
|
124
|
-
FitResult = namedtuple("FitResult",
|
|
125
|
-
"bic aic ad_pval cvm_pval dist distName warnings")
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def fit(data: conversions.VectorLike,
|
|
129
|
-
scipydist: rv_continuous,
|
|
130
|
-
distName: Optional[str] = None
|
|
131
|
-
) -> FitResult:
|
|
132
|
-
"""
|
|
133
|
-
Fit a univariate distribution for a continuous random variable.
|
|
134
|
-
|
|
135
|
-
This function fits a given scipy.stats univariate distribution to the
|
|
136
|
-
provided data using maximum likelihood estimation. It returns a named
|
|
137
|
-
tuple containing information criteria, goodness-of-fit p-values, the
|
|
138
|
-
frozen distribution, and the distribution name.
|
|
139
|
-
|
|
140
|
-
Parameters
|
|
141
|
-
----------
|
|
142
|
-
data : VectorLike
|
|
143
|
-
The data to fit, as a one-dimensional array or similar.
|
|
144
|
-
scipydist : scipy.stats.rv_continuous
|
|
145
|
-
The scipy.stats distribution object to fit (e.g., stats.norm).
|
|
146
|
-
distName : str, optional
|
|
147
|
-
Name of the distribution. If None, the distribution's name is used.
|
|
148
|
-
|
|
149
|
-
Returns
|
|
150
|
-
-------
|
|
151
|
-
FitResult
|
|
152
|
-
A Named tuple with fields:
|
|
153
|
-
|
|
154
|
-
bic : float
|
|
155
|
-
Bayesian Information Criterion for the fit.
|
|
156
|
-
aic : float
|
|
157
|
-
Akaike Information Criterion for the fit.
|
|
158
|
-
ad_pval : float
|
|
159
|
-
Anderson-Darling heuristic goodness-of-fit score (see Notes).
|
|
160
|
-
cvm_pval : float
|
|
161
|
-
Cramer-von Mises heuristic goodness-of-fit score (see Notes).
|
|
162
|
-
dist : scipy.stats.rv_continuous
|
|
163
|
-
The frozen fitted distribution object.
|
|
164
|
-
distName : str
|
|
165
|
-
Name of the fitted distribution.
|
|
166
|
-
|
|
167
|
-
Notes
|
|
168
|
-
-----
|
|
169
|
-
The function uses maximum likelihood estimation for parameter fitting.
|
|
170
|
-
Goodness of fit is assessed using the Anderson-Darling and
|
|
171
|
-
Cramer-von Mises statistics.
|
|
172
|
-
|
|
173
|
-
The `ad_pval` and `cvm_pval` values are computed from tests that assume
|
|
174
|
-
a fully specified (a priori) null distribution, but the parameters here
|
|
175
|
-
are estimated from the same sample being tested. Consequently these
|
|
176
|
-
values systematically overstate goodness of fit (the Lilliefors
|
|
177
|
-
problem) and are *not* valid p-values. Treat them only as heuristic
|
|
178
|
-
scores for comparing candidate distributions with equal numbers of
|
|
179
|
-
parameters; do not use them for formal hypothesis tests.
|
|
180
|
-
"""
|
|
181
|
-
|
|
182
|
-
dataA = conversions.vlToArray(data)
|
|
183
|
-
|
|
184
|
-
with warnings.catch_warnings(record=True) as w:
|
|
185
|
-
warnings.simplefilter("always")
|
|
186
|
-
# deprecation chatter from third-party libraries is not evidence
|
|
187
|
-
# of a bad fit; do not let it disqualify a candidate
|
|
188
|
-
warnings.simplefilter("ignore", DeprecationWarning)
|
|
189
|
-
warnings.simplefilter("ignore", PendingDeprecationWarning)
|
|
190
|
-
warnings.simplefilter("ignore", FutureWarning)
|
|
191
|
-
|
|
192
|
-
# fit distribution using maximum likelihood
|
|
193
|
-
params = scipydist.fit(data)
|
|
194
|
-
|
|
195
|
-
# create a "frozen" distribution object
|
|
196
|
-
dist = scipydist(*params)
|
|
197
|
-
|
|
198
|
-
# calculate log likelihood function and info criteria
|
|
199
|
-
loglike = dist.logpdf(dataA).sum()
|
|
200
|
-
bic = np.log(len(dataA)) * len(params) - 2.0 * loglike # Schwarz
|
|
201
|
-
aic = 2.0 * len(params) - 2.0 * loglike # Akaike
|
|
202
|
-
|
|
203
|
-
# p-values for GOF tests
|
|
204
|
-
ad_pval = adtest(dataA, dist)[1] # Anderson-Darling
|
|
205
|
-
cvm_pval = cvmtest(dataA, dist)[1] # Cramer-von Mises
|
|
206
|
-
|
|
207
|
-
return FitResult(bic=bic, aic=aic, ad_pval=ad_pval, cvm_pval=cvm_pval,
|
|
208
|
-
dist=dist, distName=distName, warnings=w)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def _fit_all(data, dist_list):
|
|
212
|
-
# fit every candidate, isolating failures so that one distribution
|
|
213
|
-
# raising (e.g., scipy.stats.FitError) cannot abort the comparison
|
|
214
|
-
results = []
|
|
215
|
-
failures = []
|
|
216
|
-
for name, dist, *_ in dist_list:
|
|
217
|
-
try:
|
|
218
|
-
results.append(fit(data, dist, name))
|
|
219
|
-
except Exception as e:
|
|
220
|
-
failures.append((name, e))
|
|
221
|
-
results = sorted(results, key=lambda r: r.bic) # lowest BIC to highest
|
|
222
|
-
return results, failures
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def _fstr(value, nchars=8):
|
|
226
|
-
return ("%.3f" % value).rjust(nchars)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def _result_line(r, header=False):
|
|
230
|
-
if header is True:
|
|
231
|
-
return (" distribution,"
|
|
232
|
-
" BIC, AIC, AD_score, CvM_score\n")
|
|
233
|
-
else:
|
|
234
|
-
return ("%s, %s, %s, %s, %s\n" %
|
|
235
|
-
(r.distName.rjust(30), _fstr(r.bic), _fstr(r.aic),
|
|
236
|
-
_fstr(r.ad_pval, 6), _fstr(r.cvm_pval, 7)))
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def compare(data: conversions.VectorLike,
|
|
240
|
-
lowerLimit: bool,
|
|
241
|
-
upperLimit: bool
|
|
242
|
-
) -> str:
|
|
243
|
-
"""
|
|
244
|
-
Compare fits of univariate distributions for a continuous random variable.
|
|
245
|
-
|
|
246
|
-
This function fits all candidate distributions with the specified support
|
|
247
|
-
(lower and/or upper limit) to the provided data. It returns a formatted
|
|
248
|
-
string summarizing the Bayesian Information Criterion (BIC), Akaike
|
|
249
|
-
Information Criterion (AIC), and goodness-of-fit p-values for each
|
|
250
|
-
distribution.
|
|
251
|
-
|
|
252
|
-
Parameters
|
|
253
|
-
----------
|
|
254
|
-
data : array-like
|
|
255
|
-
The data to fit, as a one-dimensional array or sequence.
|
|
256
|
-
lowerLimit : bool
|
|
257
|
-
If True, only distributions with a lower bound are considered. If
|
|
258
|
-
False, only distributiuons without a lower bound are considered.
|
|
259
|
-
upperLimit : bool
|
|
260
|
-
If True, only distributions with an upper bound are considered. If
|
|
261
|
-
False, only distributions without an upper bound are considered.
|
|
262
|
-
|
|
263
|
-
Returns
|
|
264
|
-
-------
|
|
265
|
-
str
|
|
266
|
-
A formatted string summarizing the fit statistics for each candidate
|
|
267
|
-
distribution, sorted by BIC (best to worst).
|
|
268
|
-
|
|
269
|
-
Notes
|
|
270
|
-
-----
|
|
271
|
-
For reliable results, at least 50 observations are recommended. The summary
|
|
272
|
-
includes BIC, AIC, and Anderson-Darling and Cramer-von Mises heuristic
|
|
273
|
-
scores for each distribution.
|
|
274
|
-
|
|
275
|
-
The AD_score and CvM_score columns are nominal p-values from tests that
|
|
276
|
-
assume a fully specified (a priori) null distribution, but the
|
|
277
|
-
parameters are estimated from the same sample being tested. These
|
|
278
|
-
values therefore systematically overstate goodness of fit (the
|
|
279
|
-
Lilliefors problem) and are *not* valid p-values. Use them only as
|
|
280
|
-
heuristic scores for ranking candidate distributions; rankings by BIC
|
|
281
|
-
or AIC additionally account for differing numbers of parameters.
|
|
282
|
-
"""
|
|
283
|
-
dataA = conversions.vlToArray(data)
|
|
284
|
-
|
|
285
|
-
if len(dataA) < 50:
|
|
286
|
-
msg = (f"using 'compare' with only {len(dataA)} observations "
|
|
287
|
-
f"can produce unreliable results. Interpret "
|
|
288
|
-
f"with caution.")
|
|
289
|
-
warnings.warn(msg, UserWarning)
|
|
290
|
-
dist_list = [d for d in candidates if d[2] == lowerLimit and
|
|
291
|
-
d[3] == upperLimit]
|
|
292
|
-
results, failures = _fit_all(dataA, dist_list)
|
|
293
|
-
results_edit = [r for r in results if len(r.warnings) == 0]
|
|
294
|
-
for r in results:
|
|
295
|
-
if len(r.warnings) > 0:
|
|
296
|
-
msg = (f"encountered a problem "
|
|
297
|
-
f"while fitting the {r.distName} distribution. "
|
|
298
|
-
f"It will not be included in the results.")
|
|
299
|
-
warnings.warn(msg, RuntimeWarning)
|
|
300
|
-
for name, exc in failures:
|
|
301
|
-
msg = (f"fitting the {name} distribution raised "
|
|
302
|
-
f"{type(exc).__name__}: {exc}. It will not be included "
|
|
303
|
-
f"in the results.")
|
|
304
|
-
warnings.warn(msg, RuntimeWarning)
|
|
305
|
-
lines = [_result_line(None, header=True)] + \
|
|
306
|
-
list(map(_result_line, results_edit))
|
|
307
|
-
return "".join(lines)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|