funcsim 0.1.4__py3-none-any.whl
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/__init__.py +54 -0
- funcsim/addist.py +74 -0
- funcsim/conversions.py +203 -0
- funcsim/core.py +279 -0
- funcsim/cpt.py +466 -0
- funcsim/cvmdist.py +79 -0
- funcsim/dependence.py +619 -0
- funcsim/distfit.py +286 -0
- funcsim/ecdfgof.py +193 -0
- funcsim/edf.py +25 -0
- funcsim/eut.py +92 -0
- funcsim/imanconover.py +115 -0
- funcsim/kde.py +168 -0
- funcsim/ksdist.py +206 -0
- funcsim/multicore.py +77 -0
- funcsim/nearby.py +92 -0
- funcsim/plotting.py +569 -0
- funcsim/rdarrays.py +91 -0
- funcsim/screen.py +425 -0
- funcsim/shapiro.py +25 -0
- funcsim/shrinkage.py +288 -0
- funcsim/testsim.py +50 -0
- funcsim/utests.py +52 -0
- funcsim/vect.py +67 -0
- funcsim-0.1.4.dist-info/METADATA +83 -0
- funcsim-0.1.4.dist-info/RECORD +59 -0
- funcsim-0.1.4.dist-info/WHEEL +5 -0
- funcsim-0.1.4.dist-info/licenses/LICENSE +29 -0
- funcsim-0.1.4.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_GBM.py +113 -0
- tests/test_GBM_draw-stdnorm.py +66 -0
- tests/test_GBM_zero-draws.py +62 -0
- tests/test_addist.py +68 -0
- tests/test_conversions.py +153 -0
- tests/test_copula.py +76 -0
- tests/test_covtocorr.py +23 -0
- tests/test_cpt.py +95 -0
- tests/test_cvmdist.py +86 -0
- tests/test_ecdfgof.py +88 -0
- tests/test_edf.py +9 -0
- tests/test_hist_not_data.py +47 -0
- tests/test_imanconover.py +40 -0
- tests/test_indirect_next.py +28 -0
- tests/test_joint_norm.py +67 -0
- tests/test_kde.py +9 -0
- tests/test_ksdist.py +126 -0
- tests/test_nearbypd.py +28 -0
- tests/test_rdarrays.py +45 -0
- tests/test_recdyn_with_no_data0.py +36 -0
- tests/test_shrink.py +64 -0
- tests/test_spearman.py +10 -0
- tests/test_static.py +34 -0
- tests/test_static_zero-draws.py +21 -0
- tests/test_superfluous_data0.py +40 -0
- tests/test_testsim.py +24 -0
- tests/test_utests.py +21 -0
- tests/test_vect.py +50 -0
- tests/test_zzz_compare.py +13 -0
funcsim/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
4
|
+
|
|
5
|
+
from core import simulate
|
|
6
|
+
|
|
7
|
+
from dependence import covtocorr
|
|
8
|
+
from dependence import spearman
|
|
9
|
+
from dependence import MvNorm
|
|
10
|
+
from dependence import MvKde
|
|
11
|
+
from dependence import CopulaGauss
|
|
12
|
+
from dependence import CopulaStudent
|
|
13
|
+
from dependence import CopulaClayton
|
|
14
|
+
from dependence import CopulaGumbel
|
|
15
|
+
|
|
16
|
+
from imanconover import imanconover
|
|
17
|
+
|
|
18
|
+
from shrinkage import shrink
|
|
19
|
+
from nearby import nearestpd
|
|
20
|
+
|
|
21
|
+
from edf import edf
|
|
22
|
+
|
|
23
|
+
from kde import Kde
|
|
24
|
+
|
|
25
|
+
from ecdfgof import kstest
|
|
26
|
+
from ecdfgof import adtest
|
|
27
|
+
from ecdfgof import cvmtest
|
|
28
|
+
|
|
29
|
+
from shapiro import swtest
|
|
30
|
+
|
|
31
|
+
from distfit import fit
|
|
32
|
+
from distfit import compare
|
|
33
|
+
|
|
34
|
+
from utests import utests
|
|
35
|
+
|
|
36
|
+
from screen import screen
|
|
37
|
+
|
|
38
|
+
from cpt import cpt, utilPower, utilNormLog, weightTK, weightPrelec1
|
|
39
|
+
from cpt import weightPrelec2, cptBV
|
|
40
|
+
|
|
41
|
+
from eut import eut, utilIsoelastic
|
|
42
|
+
|
|
43
|
+
from plotting import fan, twofuncs, histpdf, dblscat, qqplot, show
|
|
44
|
+
|
|
45
|
+
def version():
|
|
46
|
+
"""
|
|
47
|
+
Return the current version of the funcsim package.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
str
|
|
52
|
+
The version string of the package (e.g., '0.1.4').
|
|
53
|
+
"""
|
|
54
|
+
return "0.1.4"
|
funcsim/addist.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Distributions of the Anderson-Darling statistic.
|
|
3
|
+
|
|
4
|
+
After doi:18637/jss.v009.i02.
|
|
5
|
+
|
|
6
|
+
Original Work (scikit-gof) Copyright (c) 2015 Wojciech Ruszczewski <scipy@wr.waw.pl>
|
|
7
|
+
|
|
8
|
+
Modified Work Copyright (c) 2020 h-bryant
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import division
|
|
11
|
+
|
|
12
|
+
from numpy import exp, log, sqrt
|
|
13
|
+
from scipy.stats import rv_continuous
|
|
14
|
+
|
|
15
|
+
from vect import vectorize
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ad_unif_gen(rv_continuous):
|
|
19
|
+
"""
|
|
20
|
+
Approximate distribution of the uniform Anderson-Darling statistic
|
|
21
|
+
(with the hypothesized distribution continuous and fully specified).
|
|
22
|
+
"""
|
|
23
|
+
def _argcheck(self, samples):
|
|
24
|
+
return samples > 0
|
|
25
|
+
|
|
26
|
+
@vectorize(otypes=(float,))
|
|
27
|
+
def _cdf(self, statistic, samples):
|
|
28
|
+
if samples == 1:
|
|
29
|
+
# Exact distribution for a single sample (a bit more precise than
|
|
30
|
+
# the approximation). See doi:10.1214/aoms/1177704850 equation 8.
|
|
31
|
+
if statistic <= log(4) - 1:
|
|
32
|
+
return 0.
|
|
33
|
+
else:
|
|
34
|
+
return sqrt(1 - 4 * exp(-1 - statistic))
|
|
35
|
+
pinf = ad_unif_inf(statistic)
|
|
36
|
+
return pinf + ad_unif_fix(samples, pinf)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
ad_unif = ad_unif_gen(a=0, name='ad-unif', shapes='samples')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def ad_unif_inf(statistic):
|
|
43
|
+
"""
|
|
44
|
+
Approximates the limiting distribution to about 5 decimal digits.
|
|
45
|
+
"""
|
|
46
|
+
z = statistic
|
|
47
|
+
if z < 2:
|
|
48
|
+
return (exp(-1.2337141 / z) / sqrt(z) *
|
|
49
|
+
(2.00012 + (.247105 - (.0649821 - (.0347962 -
|
|
50
|
+
(.011672 - .00168691 * z) * z) * z) * z) * z))
|
|
51
|
+
else:
|
|
52
|
+
return exp(-exp(1.0776 - (2.30695 - (.43424 - (.082433 -
|
|
53
|
+
(.008056 - .0003146 * z) * z) * z) * z) * z))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
g1 = lambda x: sqrt(x) * (1 - x) * (49 * x - 102)
|
|
57
|
+
g2 = lambda x: (-.00022633 + (6.54034 - (14.6538 - (14.458 -
|
|
58
|
+
(8.259 - 1.91864 * x) * x) * x) * x) * x)
|
|
59
|
+
g3 = lambda x: (-130.2137 + (745.2337 - (1705.091 - (1950.646 -
|
|
60
|
+
(1116.36 - 255.7844 * x) * x) * x) * x) * x)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def ad_unif_fix(samples, pinf):
|
|
64
|
+
"""
|
|
65
|
+
Corrects the limiting distribution for a finite sample size.
|
|
66
|
+
"""
|
|
67
|
+
n = samples
|
|
68
|
+
c = .01265 + .1757 / n
|
|
69
|
+
if pinf < c:
|
|
70
|
+
return (((.0037 / n + .00078) / n + .00006) / n) * g1(pinf / c)
|
|
71
|
+
elif pinf < .8:
|
|
72
|
+
return ((.01365 / n + .04213) / n) * g2((pinf - c) / (.8 - c))
|
|
73
|
+
else:
|
|
74
|
+
return g3(pinf) / n
|
funcsim/conversions.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from typing import TypeVar, Union, Sequence, Any
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import xarray as xr
|
|
5
|
+
from numpy.typing import NDArray
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
VectorLike = Union[
|
|
10
|
+
Sequence[T], # list[T], tuple[T], etc.
|
|
11
|
+
NDArray[Any], # np.NDArray of any dtype (runtime shape not enforced)
|
|
12
|
+
pd.Series, # 1‑D pandas Series
|
|
13
|
+
pd.DataFrame, # 1‑D pandas DataFrame
|
|
14
|
+
xr.DataArray # 1‑D xarray DataArray
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
ArrayLike = Union[
|
|
18
|
+
NDArray[Any], # np.NDArray of any dtype (runtime shape not enforced)
|
|
19
|
+
pd.DataFrame, # 2‑D pandas Series
|
|
20
|
+
xr.DataArray # 2‑D xarray DataArray
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def vlValidate(vl: VectorLike) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Validate if the input is a vector-like object.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
vl : VectorLike
|
|
31
|
+
The object to validate.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
bool
|
|
36
|
+
True if the input is a vector-like object, False otherwise.
|
|
37
|
+
"""
|
|
38
|
+
if isinstance(vl, (list, tuple, pd.Series)):
|
|
39
|
+
return True
|
|
40
|
+
elif isinstance(vl, np.ndarray):
|
|
41
|
+
a = np.asarray(vl)
|
|
42
|
+
if a.ndim > 2:
|
|
43
|
+
return False
|
|
44
|
+
if a.ndim == 2:
|
|
45
|
+
if not (a.shape[0] == 1 or a.shape[1] == 1):
|
|
46
|
+
return False
|
|
47
|
+
return True
|
|
48
|
+
elif isinstance(vl, xr.DataArray):
|
|
49
|
+
a = np.asarray(vl)
|
|
50
|
+
if a.ndim > 2:
|
|
51
|
+
return False
|
|
52
|
+
if a.ndim == 2:
|
|
53
|
+
if not (a.shape[0] == 1 or a.shape[1] == 1):
|
|
54
|
+
return False
|
|
55
|
+
return True
|
|
56
|
+
elif isinstance(vl, pd.DataFrame):
|
|
57
|
+
a = np.asarray(vl)
|
|
58
|
+
if a.ndim > 2:
|
|
59
|
+
return False
|
|
60
|
+
if a.ndim == 2:
|
|
61
|
+
if not (a.shape[0] == 1 or a.shape[1] == 1):
|
|
62
|
+
return False
|
|
63
|
+
return True
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def vlToArray(vl: VectorLike) -> NDArray:
|
|
68
|
+
"""
|
|
69
|
+
Convert a vector-like object to a 1-D NumPy array.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
vl : VectorLike
|
|
74
|
+
The vector-like object to convert.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
NDArray
|
|
79
|
+
The converted 1-D NumPy array.
|
|
80
|
+
"""
|
|
81
|
+
if not vlValidate(vl):
|
|
82
|
+
raise ValueError("argument passed is not a vector-like object")
|
|
83
|
+
if isinstance(vl, (list, tuple)):
|
|
84
|
+
a = np.array(vl)
|
|
85
|
+
elif isinstance(vl, pd.Series):
|
|
86
|
+
a = vl.to_numpy()
|
|
87
|
+
elif isinstance(vl, xr.DataArray):
|
|
88
|
+
a = vl.values
|
|
89
|
+
else:
|
|
90
|
+
a = np.asarray(vl)
|
|
91
|
+
return a.flatten() # Ensure the array is 1-D
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def alValidate(al: ArrayLike) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Validate if the input is a 2-D array-like object.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
al : ArrayLike
|
|
101
|
+
The object to validate.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
bool
|
|
106
|
+
True if the input is a array-like object, False otherwise.
|
|
107
|
+
"""
|
|
108
|
+
if isinstance(al, np.ndarray):
|
|
109
|
+
a = np.asarray(al)
|
|
110
|
+
elif isinstance(al, xr.DataArray):
|
|
111
|
+
a = np.asarray(al)
|
|
112
|
+
elif isinstance(al, pd.DataFrame):
|
|
113
|
+
a = np.asarray(al)
|
|
114
|
+
else:
|
|
115
|
+
return False
|
|
116
|
+
if a.ndim != 2:
|
|
117
|
+
return False
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def alToArray(al: ArrayLike) -> NDArray:
|
|
122
|
+
"""
|
|
123
|
+
Convert an array-like object to a 2-D NumPy array.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
al : ArrayLike
|
|
128
|
+
The array-like object to convert.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
NDArray
|
|
133
|
+
The converted 2-D NumPy array.
|
|
134
|
+
"""
|
|
135
|
+
if not alValidate(al):
|
|
136
|
+
raise ValueError("argument passed is not an array-like object")
|
|
137
|
+
if isinstance(al, pd.DataFrame):
|
|
138
|
+
a = al.to_numpy()
|
|
139
|
+
elif isinstance(al, xr.DataArray):
|
|
140
|
+
a = al.values
|
|
141
|
+
else: # Assume it's a NumPy array
|
|
142
|
+
a = np.asarray(al)
|
|
143
|
+
return a
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def vlCoords(vl: VectorLike) -> pd.Index:
|
|
147
|
+
"""
|
|
148
|
+
Get or create coordinates for a vector-like object.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
vl : VectorLike
|
|
153
|
+
The vector-like object to convert.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
pandas.Index
|
|
158
|
+
A pandas index with coordinates corresponding to the vector-like object.
|
|
159
|
+
"""
|
|
160
|
+
if not vlValidate(vl):
|
|
161
|
+
raise ValueError("argument passed is not a vector-like object")
|
|
162
|
+
if isinstance(vl, (list, tuple)):
|
|
163
|
+
return pd.Index(list(range(max(shape(vl)))))
|
|
164
|
+
if isinstance(vl, (np.ndarray)):
|
|
165
|
+
if vl.ndim == 1:
|
|
166
|
+
return pd.Index(list(range(len(vl))))
|
|
167
|
+
if vl.ndim == 2:
|
|
168
|
+
if vl.shape[0] == 1:
|
|
169
|
+
return pd.Index(list(range(vl.shape[1])))
|
|
170
|
+
if vl.shape[1] == 1:
|
|
171
|
+
return pd.Index(list(range(vl.shape[0])))
|
|
172
|
+
if isinstance(vl, pd.Series):
|
|
173
|
+
return vl.index
|
|
174
|
+
if isinstance(vl, xr.DataArray):
|
|
175
|
+
dim_size_to_name = dict(zip(vl.shape, vl.dims))
|
|
176
|
+
longest_dim = max(dim_size_to_name.keys())
|
|
177
|
+
ret = vl.coords[dim_size_to_name[longest_dim]]
|
|
178
|
+
return pd.Index(ret)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def alColNames(al: ArrayLike) -> pd.Index:
|
|
182
|
+
"""
|
|
183
|
+
Get or create column names from an array-like object.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
al : ArrayLike
|
|
188
|
+
The array-like object to process.
|
|
189
|
+
|
|
190
|
+
Returns
|
|
191
|
+
-------
|
|
192
|
+
pandas.Index
|
|
193
|
+
Column names of the array-like object.
|
|
194
|
+
"""
|
|
195
|
+
if not alValidate(al):
|
|
196
|
+
raise ValueError("argument passed is not an array-like object")
|
|
197
|
+
if isinstance(al, pd.DataFrame):
|
|
198
|
+
return al.columns
|
|
199
|
+
if isinstance(al, xr.DataArray):
|
|
200
|
+
return al.coords[al.dims[-1]]
|
|
201
|
+
if isinstance(al, np.ndarray):
|
|
202
|
+
return pd.Index([f"v{i}" for i in range(al.shape[1])])
|
|
203
|
+
raise ValueError("Unsupported array-like type")
|
funcsim/core.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from copy import deepcopy as copy
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import xarray as xr
|
|
6
|
+
import multicore
|
|
7
|
+
import rdarrays
|
|
8
|
+
from scipy import stats
|
|
9
|
+
from collections.abc import Callable, Generator
|
|
10
|
+
from typing import Optional
|
|
11
|
+
import inspect
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_arg_count(func):
|
|
15
|
+
sig = inspect.signature(func)
|
|
16
|
+
params = sig.parameters.values()
|
|
17
|
+
|
|
18
|
+
# Count only parameters that are positional or keyword
|
|
19
|
+
# (excluding *args and **kwargs)
|
|
20
|
+
return sum(
|
|
21
|
+
1 for p in params
|
|
22
|
+
if p.kind in (inspect.Parameter.POSITIONAL_ONLY,
|
|
23
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
24
|
+
inspect.Parameter.KEYWORD_ONLY)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _checkhist0(hist0):
|
|
29
|
+
# check that user's hist0 seems sane. Return list of variable names
|
|
30
|
+
# in hist0 order.
|
|
31
|
+
|
|
32
|
+
if isinstance(hist0, xr.DataArray) is False:
|
|
33
|
+
raise ValueError('"hist0" must be an xarray.DataArray')
|
|
34
|
+
|
|
35
|
+
# check that hist0 has the right dimension names
|
|
36
|
+
hist0Coords = hist0.coords
|
|
37
|
+
if not len(hist0Coords) == 2:
|
|
38
|
+
raise ValueError('"hist0" must have exactly two dimensions')
|
|
39
|
+
if not ("variables" in hist0Coords.keys() and
|
|
40
|
+
"steps" in hist0Coords.keys()):
|
|
41
|
+
raise ValueError('"hist0" must have dimensions "variables" and "steps"')
|
|
42
|
+
|
|
43
|
+
# Check for an appropriate index for the 'variables' dimension
|
|
44
|
+
stepsCoords = hist0Coords["steps"]
|
|
45
|
+
if not (np.issubdtype(stepsCoords.dtype, np.integer) or
|
|
46
|
+
isinstance(stepsCoords.to_index(), pd.PeriodIndex)):
|
|
47
|
+
raise ValueError('"hist0" must have either an integer index or a '
|
|
48
|
+
'pandas PeriodIndex for the "steps" dimension')
|
|
49
|
+
|
|
50
|
+
return list(hist0Coords["variables"])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _checkf(f, data0):
|
|
54
|
+
# Count number of times 'f', and any code 'f' invokes, calls 'next(draw)'
|
|
55
|
+
# If 'data0' is None, infer that 'f' is the 'trial' func for a cross-sec sim
|
|
56
|
+
# If 'data0' is a xr.DataArray, infer that 'f' is 'step' for rec. dyn. sim
|
|
57
|
+
# Also, check that f returns something that makes sense.
|
|
58
|
+
fakeugen = _countgen()
|
|
59
|
+
|
|
60
|
+
# check that 'f' returns a dict
|
|
61
|
+
out = f(fakeugen, data0)
|
|
62
|
+
if type(out) != dict:
|
|
63
|
+
raise ValueError('"f" function must return a dict')
|
|
64
|
+
|
|
65
|
+
# check that the dict returned by 'f' has variable names as keys
|
|
66
|
+
varnames = list(out.keys())
|
|
67
|
+
if sum(1 if type(k) is str else 0 for k in varnames) < len(varnames):
|
|
68
|
+
raise ValueError('The keys of the dictionary returned by "f" must be '
|
|
69
|
+
'variable names as strings')
|
|
70
|
+
|
|
71
|
+
calls = int(round((next(fakeugen) - 0.5) * 10**4))
|
|
72
|
+
return calls, varnames
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _countgen():
|
|
76
|
+
# dummy generator for counting calls but always returning approximately 0.5.
|
|
77
|
+
i = 0
|
|
78
|
+
while i < int(10000):
|
|
79
|
+
yield 0.5 + float(i) * 10**-4
|
|
80
|
+
i += 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _makewgen(w, r):
|
|
84
|
+
# given an array 'w' of indep. draws, where rows reflect variables
|
|
85
|
+
# and columns reflect trials, make a generator for trial 'r' tha emits
|
|
86
|
+
# a number of draws equal to the number of RVs
|
|
87
|
+
i = 0
|
|
88
|
+
while i < w.shape[0]:
|
|
89
|
+
yield w[i, r]
|
|
90
|
+
i += 1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _strat(R):
|
|
94
|
+
# stratified sampling for a single uniformly distributed random variable.
|
|
95
|
+
# 'R' (an int) is the number of draws to perform
|
|
96
|
+
# returns a numpy array of floats, each in the interval [0, 1).
|
|
97
|
+
draws = (np.arange(0, R) + np.random.uniform(0.0, 1.0, R)) / float(R)
|
|
98
|
+
np.random.shuffle(draws) # warning: mutating 'draws'
|
|
99
|
+
return draws
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _mcs(K, R):
|
|
103
|
+
# Monte Carlo sampling. For each of K independent uniform (over the
|
|
104
|
+
# unit interval) random variables, create a sample of length R.
|
|
105
|
+
# 'K' (an int) is the number of variables
|
|
106
|
+
# 'R' (an int) is the number of trials
|
|
107
|
+
# returns a KxR numpy array containing draws
|
|
108
|
+
return np.concatenate([[np.random.uniform(0.0, 1.0, R)]
|
|
109
|
+
for i in range(K)], axis=0)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _lhs(K, R):
|
|
113
|
+
# Latin hypercube sampling. For each of K independent uniform (over the
|
|
114
|
+
# unit interval) random variables, create a stratified sample of length R.
|
|
115
|
+
# 'K' (an int) is the number of variables
|
|
116
|
+
# 'R' (an int) is the number of trials
|
|
117
|
+
# returns a KxR numpy array containing draws
|
|
118
|
+
return np.concatenate([[_strat(R)] for i in range(K)], axis=0)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _extendIndex(idx, nNewSteps):
|
|
122
|
+
# extend a 'steps' index; should work for ints or pd.Period
|
|
123
|
+
if len(idx) == 0: # no previous index; just use integers for the new index
|
|
124
|
+
return list(range(nNewSteps))
|
|
125
|
+
newIdx = list(idx)
|
|
126
|
+
[newIdx.append(newIdx[-1] + 1) for i in range(nNewSteps)]
|
|
127
|
+
return newIdx
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def simulate(f: Callable[[Generator[int, float, None],
|
|
131
|
+
Optional[rdarrays.RDdata]],
|
|
132
|
+
dict[str, float]],
|
|
133
|
+
ntrials: Optional[int] = 500,
|
|
134
|
+
nsteps: Optional[int] = 1,
|
|
135
|
+
hist0: Optional[xr.DataArray] = None,
|
|
136
|
+
multi: Optional[bool] = False,
|
|
137
|
+
seed: Optional[int] = 6,
|
|
138
|
+
stdnorm: Optional[bool] = False,
|
|
139
|
+
sampling: Optional[str] = 'lh'
|
|
140
|
+
) -> xr.DataArray:
|
|
141
|
+
"""
|
|
142
|
+
Stochastic simulation.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
f : function
|
|
147
|
+
Function that performs a single trial in a static simulation or a s
|
|
148
|
+
single step through time in a recursive dynamic simulation. Should take
|
|
149
|
+
'ugen' as a first argument in either case, where 'ugen' will be a
|
|
150
|
+
generator that emits standard uniform draws (or standard normal draws,
|
|
151
|
+
if `stdnorm` is True) that will be passed to f by ``simulate``.
|
|
152
|
+
In the case of a recursive dynamic simulation that employs past values,
|
|
153
|
+
f should take 'data' as a second argument, where this will be a type of
|
|
154
|
+
array that is also provided by ``simulate.`` This function should return
|
|
155
|
+
a dict with variable names (as strings) as keys and values for those
|
|
156
|
+
variables (as floats) as values.
|
|
157
|
+
ntrials : int, optional
|
|
158
|
+
The number of trials to perform. Default is 500.
|
|
159
|
+
nsteps : int, optional
|
|
160
|
+
The number of steps to perform in each trial. Default is 1.
|
|
161
|
+
hist0 : xarray.DataArray, optional
|
|
162
|
+
Initial and/or historical data relevant to the simulation.
|
|
163
|
+
Should have dimensions 'variables' and 'steps'. Any lagged values
|
|
164
|
+
needed (recalled) by `f` must be in `hist0`. The 'steps' dimension
|
|
165
|
+
should have either an integer index or a pandas PeriodIndex.
|
|
166
|
+
multi : bool, optional
|
|
167
|
+
Use multiple processes/cores for the simulation. Default is False.
|
|
168
|
+
seed : int, optional
|
|
169
|
+
Seed for pseudo-random number generation. Default is 6.
|
|
170
|
+
stdnorm : book, optional
|
|
171
|
+
If False, ``next(draw)`` within `trialf` will return standard uniform
|
|
172
|
+
random draws. If True, ``next(draw)`` will return standard normal draws.
|
|
173
|
+
Default is False.
|
|
174
|
+
sampling : {'lh', 'mc'}, optional
|
|
175
|
+
If 'lh', Latin Hypercube sampling is employed. If 'mc', simple
|
|
176
|
+
Monte Carlo sampling is employed. Default is 'lh'.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
xarray.DataArray
|
|
181
|
+
3-D xarray.DataArray with dimensions 'trials', 'variables', and 'steps'.
|
|
182
|
+
"""
|
|
183
|
+
if hist0 is not None:
|
|
184
|
+
_checkhist0(hist0)
|
|
185
|
+
|
|
186
|
+
# check that we know how to cope with the types for the 'steps' index
|
|
187
|
+
sidx = hist0.indexes['steps']
|
|
188
|
+
if len(sidx) > 0:
|
|
189
|
+
if not type(sidx[0]) in [pd.Period, np.int64]:
|
|
190
|
+
raise ValueError("'hist0' should have either an integer index"
|
|
191
|
+
" or a pandas.PeriodIndex for the 'steps' "
|
|
192
|
+
"dimension.")
|
|
193
|
+
else:
|
|
194
|
+
sidx = []
|
|
195
|
+
# create an empty hist0.
|
|
196
|
+
variables = np.array([], dtype=str)
|
|
197
|
+
steps = np.array([], dtype=int)
|
|
198
|
+
hist0 = xr.DataArray(
|
|
199
|
+
data=np.empty((0, 0)),
|
|
200
|
+
dims=("steps", "variables"),
|
|
201
|
+
coords={"steps": steps, "variables": variables}
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# check for 'f'
|
|
205
|
+
if not isinstance(f, Callable):
|
|
206
|
+
raise ValueError('"f" must be a callable function')
|
|
207
|
+
|
|
208
|
+
# infer number of arguments in 'f'. If it takes only a single arg, wrap it
|
|
209
|
+
# in an outer func that takes "hist" as a second arg
|
|
210
|
+
numb_f_args = _get_arg_count(f)
|
|
211
|
+
if numb_f_args == 1:
|
|
212
|
+
stepf = lambda draw, hist: f(draw)
|
|
213
|
+
elif numb_f_args == 2:
|
|
214
|
+
stepf = f
|
|
215
|
+
else:
|
|
216
|
+
raise ValueError('"f" should take two arguments at most')
|
|
217
|
+
|
|
218
|
+
# indexes for the final output xr.DataArray
|
|
219
|
+
varNames = hist0.indexes['variables']
|
|
220
|
+
namePositionsPrelim = {nm: i for i, nm in enumerate(varNames)}
|
|
221
|
+
stepLabels = _extendIndex(sidx, nsteps)
|
|
222
|
+
|
|
223
|
+
# create example data object in which data for one trail can accumulate
|
|
224
|
+
dataPrelim = rdarrays.RDdata(hist0.to_masked_array(),
|
|
225
|
+
nsteps, namePositionsPrelim)
|
|
226
|
+
|
|
227
|
+
# infer number of random vars reflected in 'step' fucntion
|
|
228
|
+
# and the variable names being returned by 'step' and their order
|
|
229
|
+
rvs, stepfNames = _checkf(stepf, copy(dataPrelim))
|
|
230
|
+
|
|
231
|
+
# specify that the data objects that accumulate data for each trial
|
|
232
|
+
# will reflect the union of the variables in 'hist0' and the variables
|
|
233
|
+
# returned by 'step'
|
|
234
|
+
# breakpoint()
|
|
235
|
+
varNamesList = list(varNames)
|
|
236
|
+
if stepfNames == varNamesList:
|
|
237
|
+
finalNames = varNamesList
|
|
238
|
+
finalHist0 = hist0
|
|
239
|
+
else:
|
|
240
|
+
finalNames = list(set(varNamesList).union(set(stepfNames)))
|
|
241
|
+
finalHist0 = hist0.copy()
|
|
242
|
+
finalHist0 = finalHist0.reindex(variables=finalNames, fill_value=np.nan)
|
|
243
|
+
|
|
244
|
+
# create a DataArray to hold the data for one trial
|
|
245
|
+
# create example data object in which data for one trail can accumulate
|
|
246
|
+
namePositions = {nm: i for i, nm in enumerate(finalNames)}
|
|
247
|
+
data = rdarrays.RDdata(finalHist0.to_masked_array(), nsteps, namePositions)
|
|
248
|
+
|
|
249
|
+
# draws for all RVs in all time steps, w/ sampling stratified across trials
|
|
250
|
+
if rvs > 0:
|
|
251
|
+
np.random.seed(seed)
|
|
252
|
+
if sampling == 'lh':
|
|
253
|
+
u = _lhs(rvs * nsteps, ntrials) # np.array: (rvs*steps) x trials
|
|
254
|
+
elif sampling == 'mc':
|
|
255
|
+
u = _mcs(rvs * nsteps, ntrials) # monte carlo
|
|
256
|
+
else:
|
|
257
|
+
raise ValueError('sampling must be "lh" or "mc"')
|
|
258
|
+
w = stats.norm.ppf(u) if stdnorm is True else u
|
|
259
|
+
|
|
260
|
+
def trial(r):
|
|
261
|
+
wgen = _makewgen(w, r) if rvs > 0 else None # 'w' gener. for trial 'r'
|
|
262
|
+
# perform all time steps for one trial
|
|
263
|
+
# return _recurse(f=lambda x: step(x, wgen), x0=copy(data), S=steps)
|
|
264
|
+
dataWorking = copy(data)
|
|
265
|
+
for s in range(nsteps):
|
|
266
|
+
# step 's' of trial 'r'
|
|
267
|
+
dataWorking.append(stepf(wgen, dataWorking))
|
|
268
|
+
return dataWorking
|
|
269
|
+
|
|
270
|
+
# create and return 3-D output DataArray, with new dimension 'trials'
|
|
271
|
+
if multi is True:
|
|
272
|
+
out = multicore.parmap(lambda r: trial(r)._a, range(ntrials))
|
|
273
|
+
else:
|
|
274
|
+
out = [trial(r)._a for r in range(ntrials)]
|
|
275
|
+
|
|
276
|
+
prelim = xr.DataArray(out, coords=[('trials', list(range(ntrials))),
|
|
277
|
+
('variables', finalNames),
|
|
278
|
+
('steps', stepLabels)])
|
|
279
|
+
return prelim.transpose('trials', 'variables', 'steps')
|