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.
Files changed (37) hide show
  1. {funcsim-0.2.2/funcsim.egg-info → funcsim-0.2.3}/PKG-INFO +1 -1
  2. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/__init__.py +1 -1
  3. funcsim-0.2.3/funcsim/distfit.py +722 -0
  4. {funcsim-0.2.2 → funcsim-0.2.3/funcsim.egg-info}/PKG-INFO +1 -1
  5. {funcsim-0.2.2 → funcsim-0.2.3}/setup.py +1 -1
  6. funcsim-0.2.2/funcsim/distfit.py +0 -307
  7. {funcsim-0.2.2 → funcsim-0.2.3}/LICENSE +0 -0
  8. {funcsim-0.2.2 → funcsim-0.2.3}/MANIFEST.in +0 -0
  9. {funcsim-0.2.2 → funcsim-0.2.3}/README.md +0 -0
  10. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/addist.py +0 -0
  11. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/conversions.py +0 -0
  12. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/copfit.py +0 -0
  13. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/core.py +0 -0
  14. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/cpt.py +0 -0
  15. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/cvmdist.py +0 -0
  16. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/dependence.py +0 -0
  17. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/ecdfgof.py +0 -0
  18. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/edf.py +0 -0
  19. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/eut.py +0 -0
  20. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/imanconover.py +0 -0
  21. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/kde.py +0 -0
  22. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/ksdist.py +0 -0
  23. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/multicore.py +0 -0
  24. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/nearby.py +0 -0
  25. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/plotting.py +0 -0
  26. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/rdarrays.py +0 -0
  27. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/screen.py +0 -0
  28. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/shapiro.py +0 -0
  29. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/shrinkage.py +0 -0
  30. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/testsim.py +0 -0
  31. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/utests.py +0 -0
  32. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim/vect.py +0 -0
  33. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim.egg-info/SOURCES.txt +0 -0
  34. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim.egg-info/dependency_links.txt +0 -0
  35. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim.egg-info/requires.txt +0 -0
  36. {funcsim-0.2.2 → funcsim-0.2.3}/funcsim.egg-info/top_level.txt +0 -0
  37. {funcsim-0.2.2 → funcsim-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: funcsim
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: functional, simple stochastic simulation
5
5
  Home-page: https://github.com/h-bryant/funcsim
6
6
  Author: Henry Bryant
@@ -48,4 +48,4 @@ def version():
48
48
  str
49
49
  The version string of the package (e.g., '0.1.5').
50
50
  """
51
- return "0.2.2"
51
+ return "0.2.3"
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: funcsim
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: functional, simple stochastic simulation
5
5
  Home-page: https://github.com/h-bryant/funcsim
6
6
  Author: Henry Bryant
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="funcsim",
8
- version="0.2.2",
8
+ version="0.2.3",
9
9
  license="bsd-3-clause",
10
10
  author="Henry Bryant",
11
11
  author_email="henry@tamu.edu",
@@ -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