funcsim 0.2.6__tar.gz → 0.2.8__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.6/funcsim.egg-info → funcsim-0.2.8}/PKG-INFO +1 -1
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/__init__.py +2 -2
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/copcompare.py +5 -1
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/copfit.py +126 -8
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/core.py +91 -23
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/dependence.py +528 -43
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/distfit.py +76 -10
- funcsim-0.2.8/funcsim/kde.py +268 -0
- {funcsim-0.2.6 → funcsim-0.2.8/funcsim.egg-info}/PKG-INFO +1 -1
- {funcsim-0.2.6 → funcsim-0.2.8}/setup.py +1 -1
- funcsim-0.2.6/funcsim/kde.py +0 -183
- {funcsim-0.2.6 → funcsim-0.2.8}/LICENSE +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/MANIFEST.in +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/README.md +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/addist.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/conversions.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/cpt.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/cvmdist.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/ecdfgof.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/edf.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/eut.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/imanconover.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/ksdist.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/multicore.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/nearby.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/plotting.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/rdarrays.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/screen.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/shapiro.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/shrinkage.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/testsim.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/utests.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim/vect.py +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim.egg-info/SOURCES.txt +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim.egg-info/dependency_links.txt +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim.egg-info/requires.txt +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/funcsim.egg-info/top_level.txt +0 -0
- {funcsim-0.2.6 → funcsim-0.2.8}/setup.cfg +0 -0
|
@@ -56,7 +56,11 @@ def copcompare(udata: conversions.ArrayLike) -> pd.DataFrame:
|
|
|
56
56
|
and maximum pseudo-likelihood for the Archimedean families. The
|
|
57
57
|
Gaussian and Student's t log-likelihoods are therefore evaluated at
|
|
58
58
|
moment-type estimates rather than at the exact maximum, so their
|
|
59
|
-
criteria are conservative by a small amount.
|
|
59
|
+
criteria are conservative by a small amount. A Student's t row with
|
|
60
|
+
``nu=inf`` means the profile likelihood peaked at the ceiling of the
|
|
61
|
+
degrees-of-freedom search (200), so the fitted copula is the Gaussian
|
|
62
|
+
copula with the Kendall's-tau correlation matrix; it still pays for the
|
|
63
|
+
extra parameter in the criteria. Standard information
|
|
60
64
|
criteria applied to pseudo-likelihoods are approximate (Grønneberg and
|
|
61
65
|
Hjort, 2014); treat differences of a few points as ties. Warnings
|
|
62
66
|
issued while fitting a family (for example, negative dependence in data
|
|
@@ -41,9 +41,13 @@ THETA_MIN_FRANK = 1e-6
|
|
|
41
41
|
# largest admissible theta (tau approx. 0.96-0.99 depending on family)
|
|
42
42
|
_THETA_MAX = 100.0
|
|
43
43
|
|
|
44
|
-
# search bounds for the Student's t degrees of freedom
|
|
44
|
+
# search bounds for the Student's t degrees of freedom. An optimum within
|
|
45
|
+
# _NU_GAUSS_FRAC of the ceiling is reported as nu = inf (the Gaussian
|
|
46
|
+
# copula with the same correlation matrix): the profile likelihood is flat
|
|
47
|
+
# there and the ceiling itself is an artifact of the search, not an estimate
|
|
45
48
|
_NU_MIN = 2.0
|
|
46
49
|
_NU_MAX = 200.0
|
|
50
|
+
_NU_GAUSS_FRAC = 0.99
|
|
47
51
|
|
|
48
52
|
|
|
49
53
|
def _clipu(u: np.ndarray) -> np.ndarray:
|
|
@@ -135,12 +139,45 @@ def _eulerian(n: int) -> list:
|
|
|
135
139
|
return row
|
|
136
140
|
|
|
137
141
|
|
|
142
|
+
def _log_expm1(x: np.ndarray) -> np.ndarray:
|
|
143
|
+
# log(exp(x) - 1) for x > 0, without overflow for large x
|
|
144
|
+
x = np.asarray(x, dtype=float)
|
|
145
|
+
small = np.log(np.expm1(np.minimum(x, 30.0)))
|
|
146
|
+
large = x + np.log1p(-np.exp(-np.maximum(x, 30.0)))
|
|
147
|
+
return np.where(x > 30.0, large, small)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _frank_loglik_neg2(theta: float, u: np.ndarray) -> float:
|
|
151
|
+
# total bivariate Frank log-density for theta < 0. The density is the
|
|
152
|
+
# same expression as for theta > 0 (Nelsen, 2006, table 4.1, family 5);
|
|
153
|
+
# with s = -theta > 0 it reads, every factor positive,
|
|
154
|
+
# c(u, v) = s (e^s - 1) e^{s(u+v)}
|
|
155
|
+
# / [(e^s - 1) + (e^{su} - 1)(e^{sv} - 1)]^2,
|
|
156
|
+
# which is evaluated in log space
|
|
157
|
+
s = -theta
|
|
158
|
+
(M, d) = u.shape
|
|
159
|
+
log_es1 = float(_log_expm1(np.array(s)))
|
|
160
|
+
log_den = np.logaddexp(log_es1,
|
|
161
|
+
_log_expm1(s * u[:, 0]) + _log_expm1(s * u[:, 1]))
|
|
162
|
+
return float(M * (math.log(s) + log_es1) + s * np.sum(u)
|
|
163
|
+
- 2.0 * np.sum(log_den))
|
|
164
|
+
|
|
165
|
+
|
|
138
166
|
def frank_loglik(theta: float, u: np.ndarray) -> float:
|
|
139
|
-
# total Frank copula log-density over the rows of u
|
|
167
|
+
# total Frank copula log-density over the rows of u. theta = 0 is the
|
|
168
|
+
# independence limit (log-density zero); theta < 0 is admissible for two
|
|
169
|
+
# variables only. For theta > 0 and any d:
|
|
140
170
|
# log c = (d-1) log(theta) + log Li_{-(d-1)}(z)
|
|
141
171
|
# + sum_j [-theta u_j - log(1 - exp(-theta u_j))]
|
|
142
172
|
# with log z = sum_j log(1 - exp(-theta u_j)) - (d-1) log(1 - exp(-theta))
|
|
143
173
|
# and Li_{-n}(z) evaluated via Eulerian numbers, in log space
|
|
174
|
+
if theta == 0.0:
|
|
175
|
+
return 0.0
|
|
176
|
+
if theta < 0.0:
|
|
177
|
+
if u.shape[1] != 2:
|
|
178
|
+
raise ValueError("the Frank copula with theta < 0 (negative "
|
|
179
|
+
"dependence) is defined only for two variables")
|
|
180
|
+
return _frank_loglik_neg2(theta, u)
|
|
144
181
|
(M, d) = u.shape
|
|
145
182
|
n = d - 1
|
|
146
183
|
log1me_u = np.log(-np.expm1(-theta * u)) # log(1 - exp(-theta u))
|
|
@@ -160,11 +197,72 @@ def _debye1(x: float) -> float:
|
|
|
160
197
|
# Debye function of order 1: (1/x) * integral_0^x t/(e^t - 1) dt
|
|
161
198
|
if x < 1e-8:
|
|
162
199
|
return 1.0 - x / 4.0
|
|
200
|
+
if x > 200.0:
|
|
201
|
+
# beyond the quadrature's range (never reached by the fitting code):
|
|
202
|
+
# the integrand's mass lies below t ~ 40, so the integral is pi^2/6
|
|
203
|
+
# less the tail integral_x^inf t/(e^t - 1) dt = (x + 1) e^-x (to
|
|
204
|
+
# within e^-2x). This agrees with the quadrature to one ulp at
|
|
205
|
+
# x = 200 and stays finite where expm1 would overflow
|
|
206
|
+
return (math.pi ** 2 / 6.0 - (x + 1.0) * math.exp(-x)) / x
|
|
163
207
|
val = integrate.quad(lambda t: t / math.expm1(t) if t > 0.0 else 1.0,
|
|
164
208
|
0.0, x)[0]
|
|
165
209
|
return val / x
|
|
166
210
|
|
|
167
211
|
|
|
212
|
+
# Bernoulli numbers B_2, B_4, ..., B_14, for the small-theta series of
|
|
213
|
+
# Frank's Kendall's tau
|
|
214
|
+
_BERNOULLI_EVEN = (1.0 / 6.0, -1.0 / 30.0, 1.0 / 42.0, -1.0 / 30.0,
|
|
215
|
+
5.0 / 66.0, -691.0 / 2730.0, 7.0 / 6.0)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def frank_tau(theta: float) -> float:
|
|
219
|
+
# Kendall's tau of the Frank copula, tau = 1 - (4/theta)(1 - D_1(theta)),
|
|
220
|
+
# an odd function of theta with tau(0) = 0 (Nelsen, 2006, eq. 5.1.5).
|
|
221
|
+
# Below |theta| = 0.5 the direct formula loses digits to cancellation
|
|
222
|
+
# (1 - D_1 ~ theta/4), so the power series
|
|
223
|
+
# tau = 4 sum_k B_2k theta^(2k-1) / ((2k+1)(2k)!)
|
|
224
|
+
# = theta/9 - theta^3/900 + theta^5/52920 - ...
|
|
225
|
+
# is used there; its truncation error is below 1e-16 at |theta| = 0.5
|
|
226
|
+
s = abs(float(theta))
|
|
227
|
+
if s == 0.0:
|
|
228
|
+
return 0.0
|
|
229
|
+
if s <= 0.5:
|
|
230
|
+
t = 4.0 * sum(b * s ** (2 * k - 1)
|
|
231
|
+
/ ((2 * k + 1) * math.factorial(2 * k))
|
|
232
|
+
for k, b in enumerate(_BERNOULLI_EVEN, start=1))
|
|
233
|
+
else:
|
|
234
|
+
t = 1.0 - (4.0 / s) * (1.0 - _debye1(s))
|
|
235
|
+
return math.copysign(t, theta)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def frank_theta_from_tau(tau: float) -> float:
|
|
239
|
+
# exact inverse of frank_tau for tau in (-1, 0) or (0, 1): theta takes
|
|
240
|
+
# the sign of tau, and |theta| is found by bracketed root-finding on
|
|
241
|
+
# |tau|. tau(theta) is concave with slope 1/9 at the origin, so
|
|
242
|
+
# theta = 9 |tau| is a lower bracket; the upper bracket is doubled until
|
|
243
|
+
# it overshoots. Unlike frank_theta0 (the fitting start value, which
|
|
244
|
+
# clamps tau at 0.96 and theta at _THETA_MAX) nothing is clamped here
|
|
245
|
+
t = abs(float(tau))
|
|
246
|
+
if not (0.0 < t < 1.0):
|
|
247
|
+
raise ValueError("Kendall's tau for a Frank copula must lie strictly "
|
|
248
|
+
"between -1 and 1 and be nonzero (tau = 0 is "
|
|
249
|
+
"independence, which the Frank copula reaches only "
|
|
250
|
+
f"as theta -> 0); got {tau}")
|
|
251
|
+
|
|
252
|
+
def f(th):
|
|
253
|
+
return frank_tau(th) - t
|
|
254
|
+
|
|
255
|
+
lo = 9.0 * t
|
|
256
|
+
hi = max(2.0 * lo, 1.0)
|
|
257
|
+
while f(hi) < 0.0:
|
|
258
|
+
hi *= 2.0
|
|
259
|
+
if hi > 1e300:
|
|
260
|
+
raise ValueError(f"cannot represent a Frank copula with "
|
|
261
|
+
f"Kendall's tau {tau}")
|
|
262
|
+
theta = optimize.brentq(f, lo, hi, xtol=1e-14, maxiter=500)
|
|
263
|
+
return math.copysign(theta, tau)
|
|
264
|
+
|
|
265
|
+
|
|
168
266
|
def clayton_theta0(tau: float) -> float:
|
|
169
267
|
# Kendall's-tau inversion for Clayton: theta = 2 tau / (1 - tau)
|
|
170
268
|
t = min(tau, 0.97)
|
|
@@ -257,9 +355,19 @@ def fit_gumbel(u: np.ndarray) -> Tuple[float, float, bool]:
|
|
|
257
355
|
|
|
258
356
|
|
|
259
357
|
def fit_frank(u: np.ndarray) -> Tuple[float, float, bool]:
|
|
260
|
-
# fit Frank theta; returns (theta, mean pairwise tau, clamped flag)
|
|
358
|
+
# fit Frank theta; returns (theta, mean pairwise tau, clamped flag).
|
|
359
|
+
# With two variables negative dependence is admissible (theta < 0): the
|
|
360
|
+
# log-likelihood is mirrored so that the positive-side machinery (start
|
|
361
|
+
# value, bracket, floor and ceiling) applies to -theta. With more
|
|
362
|
+
# variables the Frank copula exists only for theta > 0, and non-positive
|
|
363
|
+
# dependence is clamped to the floor
|
|
261
364
|
uc = _clipu(u)
|
|
262
365
|
tb = taubar(uc)
|
|
366
|
+
if tb < 0.0 and uc.shape[1] == 2:
|
|
367
|
+
theta0 = frank_theta0(-tb)
|
|
368
|
+
theta = _fit_theta(lambda th, uu: frank_loglik(-th, uu), uc, theta0,
|
|
369
|
+
THETA_MIN_FRANK, _THETA_MAX)
|
|
370
|
+
return (-theta, tb, False)
|
|
263
371
|
if tb <= 0.0:
|
|
264
372
|
return (THETA_MIN_FRANK, tb, True)
|
|
265
373
|
theta0 = frank_theta0(tb)
|
|
@@ -284,7 +392,10 @@ def gauss_loglik(R: np.ndarray, u: np.ndarray) -> float:
|
|
|
284
392
|
def student_loglik(R: np.ndarray, nu: float, u: np.ndarray) -> float:
|
|
285
393
|
# total Student's t copula log-density over the rows of u for the
|
|
286
394
|
# correlation matrix R and degrees of freedom nu:
|
|
287
|
-
# log c(u) = log t_{nu,R}(x) - sum_k log t_nu(x_k), with x = t_nu^-1(u)
|
|
395
|
+
# log c(u) = log t_{nu,R}(x) - sum_k log t_nu(x_k), with x = t_nu^-1(u).
|
|
396
|
+
# nu = inf is the Gaussian copula with the same R
|
|
397
|
+
if math.isinf(nu):
|
|
398
|
+
return gauss_loglik(R, u)
|
|
288
399
|
uc = _clipu(u)
|
|
289
400
|
x = stats.t.ppf(uc, df=nu)
|
|
290
401
|
d = R.shape[0]
|
|
@@ -292,12 +403,15 @@ def student_loglik(R: np.ndarray, nu: float, u: np.ndarray) -> float:
|
|
|
292
403
|
return float(np.sum(joint) - np.sum(stats.t.logpdf(x, df=nu)))
|
|
293
404
|
|
|
294
405
|
|
|
295
|
-
def fit_student(u: np.ndarray) -> Tuple[np.ndarray, float]:
|
|
296
|
-
# fit a Student's t copula; returns (correlation matrix, deg. of freedom
|
|
406
|
+
def fit_student(u: np.ndarray) -> Tuple[np.ndarray, float, float]:
|
|
407
|
+
# fit a Student's t copula; returns (correlation matrix, deg. of freedom,
|
|
408
|
+
# log-likelihood gain of the fitted nu over the Gaussian limit nu = inf
|
|
409
|
+
# at the same correlation matrix).
|
|
297
410
|
# stage 1: pairwise Kendall's tau -> rho_ij = sin(pi tau_ij / 2),
|
|
298
411
|
# corrected to the nearest positive definite correlation matrix.
|
|
299
412
|
# stage 2: profile maximum pseudo-likelihood for nu, searching on a
|
|
300
|
-
# log scale (the likelihood is flat in nu near the Gaussian limit)
|
|
413
|
+
# log scale (the likelihood is flat in nu near the Gaussian limit). An
|
|
414
|
+
# optimum at the search ceiling is reported as nu = inf, with zero gain
|
|
301
415
|
uc = _clipu(u)
|
|
302
416
|
R = np.sin(0.5 * np.pi * taumatrix(uc))
|
|
303
417
|
np.fill_diagonal(R, 1.0)
|
|
@@ -321,4 +435,8 @@ def fit_student(u: np.ndarray) -> Tuple[np.ndarray, float]:
|
|
|
321
435
|
math.log(_NU_MAX)),
|
|
322
436
|
method="bounded")
|
|
323
437
|
nu = float(math.exp(res.x)) if res.success else _NU_MAX
|
|
324
|
-
|
|
438
|
+
if nu > _NU_GAUSS_FRAC * _NU_MAX:
|
|
439
|
+
return (R, math.inf, 0.0)
|
|
440
|
+
ll_gauss = gauss_loglik(R, uc)
|
|
441
|
+
gain = -negll(math.log(nu)) - ll_gauss
|
|
442
|
+
return (R, nu, float(gain) if np.isfinite(gain) else math.nan)
|
|
@@ -12,18 +12,55 @@ from typing import Optional
|
|
|
12
12
|
import inspect
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
def _stepf_arity(func):
|
|
16
|
+
# how many positional arguments simulate passes to the user's trial or
|
|
17
|
+
# step function: 1 (the draw generator) or 2 (the generator and the
|
|
18
|
+
# per-trial history). The rule, stated in the simulate docstring:
|
|
19
|
+
# * the first parameter receives the generator;
|
|
20
|
+
# * a second parameter receives the history if it has no default or
|
|
21
|
+
# is named 'hist'. A second parameter with a default and another
|
|
22
|
+
# name is a setting (e.g., 'scale=1.0') left at its default, and the
|
|
23
|
+
# function is called with the generator alone;
|
|
24
|
+
# * any further parameters must have defaults; keyword-only
|
|
25
|
+
# parameters (as functools.partial produces) must have defaults;
|
|
26
|
+
# * a parameter named 'hist' anywhere but second is ambiguous.
|
|
27
|
+
# Formerly every parameter was counted, so 'def f(ugen, scale=1.0)'
|
|
28
|
+
# received the history object as 'scale' and failed far from the cause
|
|
29
|
+
P = inspect.Parameter
|
|
30
|
+
try:
|
|
31
|
+
params = list(inspect.signature(func).parameters.values())
|
|
32
|
+
except (TypeError, ValueError):
|
|
33
|
+
raise ValueError('"f" must be a function whose signature can be '
|
|
34
|
+
'inspected') from None
|
|
35
|
+
positional = [p for p in params
|
|
36
|
+
if p.kind in (P.POSITIONAL_ONLY, P.POSITIONAL_OR_KEYWORD)]
|
|
37
|
+
required_kw = [p.name for p in params
|
|
38
|
+
if p.kind is P.KEYWORD_ONLY and p.default is P.empty]
|
|
39
|
+
if required_kw:
|
|
40
|
+
raise ValueError(f'"f" has keyword-only parameter(s) without a '
|
|
41
|
+
f'default ({", ".join(required_kw)}); simulate '
|
|
42
|
+
f'supplies only the draw generator and the history, '
|
|
43
|
+
f'positionally. Bind such parameters with '
|
|
44
|
+
f'functools.partial or give them defaults')
|
|
45
|
+
if not positional:
|
|
46
|
+
raise ValueError('"f" must take the draw generator as its first '
|
|
47
|
+
'parameter')
|
|
48
|
+
required = [p.name for p in positional if p.default is P.empty]
|
|
49
|
+
if len(required) > 2:
|
|
50
|
+
raise ValueError(f'"f" has {len(required)} parameters without '
|
|
51
|
+
f'defaults ({", ".join(required)}); simulate '
|
|
52
|
+
f'supplies at most two: the draw generator and the '
|
|
53
|
+
f'history. Give the others defaults or bind them '
|
|
54
|
+
f'with functools.partial')
|
|
55
|
+
if any(p.name == "hist" and i != 1 for i, p in enumerate(positional)):
|
|
56
|
+
raise ValueError('a parameter of "f" named "hist" receives the '
|
|
57
|
+
'per-trial history and must therefore be the second '
|
|
58
|
+
'parameter')
|
|
59
|
+
if len(required) == 2:
|
|
60
|
+
return 2
|
|
61
|
+
if len(positional) >= 2 and positional[1].name == "hist":
|
|
62
|
+
return 2
|
|
63
|
+
return 1
|
|
27
64
|
|
|
28
65
|
|
|
29
66
|
def _checkhist0(hist0):
|
|
@@ -197,6 +234,17 @@ def simulate(f: Callable[[Generator[int, float, None],
|
|
|
197
234
|
times `f` calls ``next(ugen)`` must be the same in every trial and
|
|
198
235
|
step, because draws are pre-allocated across trials for stratified
|
|
199
236
|
sampling.
|
|
237
|
+
|
|
238
|
+
Which parameters receive what is decided from the signature of `f`:
|
|
239
|
+
the first parameter receives the generator; a second parameter
|
|
240
|
+
receives the history if it has no default or is named ``hist``,
|
|
241
|
+
whereas a second parameter with a default and another name (for
|
|
242
|
+
example ``scale=1.0``) is treated as a setting, left at its default,
|
|
243
|
+
and `f` is called with the generator alone. Any further parameters
|
|
244
|
+
must have defaults, as must keyword-only parameters (which is what
|
|
245
|
+
:func:`functools.partial` produces when binding a scenario
|
|
246
|
+
parameter by name). A parameter named ``hist`` in any position but
|
|
247
|
+
the second is rejected as ambiguous.
|
|
200
248
|
ntrials : int, optional
|
|
201
249
|
The number of trials to perform. Default is 500.
|
|
202
250
|
nsteps : int, optional
|
|
@@ -234,14 +282,37 @@ def simulate(f: Callable[[Generator[int, float, None],
|
|
|
234
282
|
Raises
|
|
235
283
|
------
|
|
236
284
|
ValueError
|
|
237
|
-
If `hist0` is malformed
|
|
238
|
-
|
|
239
|
-
|
|
285
|
+
If `hist0` is malformed; if `f` is not callable, has more than two
|
|
286
|
+
parameters without defaults, has a keyword-only parameter without a
|
|
287
|
+
default, or has a parameter named ``hist`` that is not its second;
|
|
288
|
+
if `f` does not return a dict keyed by strings; or if `sampling` is
|
|
289
|
+
not one of 'lh' or 'mc'.
|
|
240
290
|
RuntimeError
|
|
241
291
|
If `f` consumes a different number of draws in some trial or step
|
|
242
292
|
than it did when first probed.
|
|
243
293
|
funcsim.rdarrays.MissingValue
|
|
244
294
|
If `f` recalls a lagged value that is not available.
|
|
295
|
+
|
|
296
|
+
Notes
|
|
297
|
+
-----
|
|
298
|
+
**Draw allocation and common random numbers.** Before any trial runs,
|
|
299
|
+
the draws are allocated as one column of `ntrials` values per uniform
|
|
300
|
+
that `f` consumes over all steps of a trial, generated in consumption
|
|
301
|
+
order from a single :class:`numpy.random.Generator` seeded with `seed`
|
|
302
|
+
(stratified across trials when `sampling` is ``'lh'``). The j-th
|
|
303
|
+
uniform consumed in trial r therefore depends only on `seed`, `ntrials`,
|
|
304
|
+
`sampling`, j, and r: not on how many uniforms `f` consumes in total,
|
|
305
|
+
on what `f` computes from them, or on `multi`. This is a documented
|
|
306
|
+
guarantee. Its consequence is common random numbers across scenarios:
|
|
307
|
+
two trial functions that consume K and K + 1 uniforms (with `nsteps` of
|
|
308
|
+
1) receive identical first K uniforms in every trial under the same
|
|
309
|
+
`seed` and `ntrials`, so differences in their outputs are due to the
|
|
310
|
+
extra draw and the model alone. With several steps, step s of trial r
|
|
311
|
+
receives uniforms sK + 1 through (s + 1)K of that trial's stream, where
|
|
312
|
+
K is the number consumed per step; two step functions with different
|
|
313
|
+
per-step counts therefore share their draws only in the first step.
|
|
314
|
+
With `stdnorm` True the same statements hold for the standard normal
|
|
315
|
+
draws, which are an elementwise transform of the uniforms.
|
|
245
316
|
"""
|
|
246
317
|
if hist0 is not None:
|
|
247
318
|
_checkhist0(hist0)
|
|
@@ -276,16 +347,13 @@ def simulate(f: Callable[[Generator[int, float, None],
|
|
|
276
347
|
if not isinstance(f, Callable):
|
|
277
348
|
raise ValueError('"f" must be a callable function')
|
|
278
349
|
|
|
279
|
-
#
|
|
280
|
-
# in an outer func that
|
|
281
|
-
|
|
282
|
-
if numb_f_args == 1:
|
|
283
|
-
stepf = functools.partial(_stepf_1arg, f)
|
|
284
|
-
elif numb_f_args == 2:
|
|
350
|
+
# decide from the signature of 'f' whether it takes the history as a
|
|
351
|
+
# second argument; if not, wrap it in an outer func that does
|
|
352
|
+
if _stepf_arity(f) == 2:
|
|
285
353
|
stepf = f
|
|
286
354
|
else:
|
|
287
|
-
|
|
288
|
-
|
|
355
|
+
stepf = functools.partial(_stepf_1arg, f)
|
|
356
|
+
|
|
289
357
|
# indexes for the final output xr.DataArray
|
|
290
358
|
varNames = hist0.indexes['variables']
|
|
291
359
|
namePositionsPrelim = {nm: i for i, nm in enumerate(varNames)}
|