bs-python-utils 0.0.1__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.
- bs_python_utils/Timer.py +76 -0
- bs_python_utils/__init__.py +0 -0
- bs_python_utils/bs_altair.py +927 -0
- bs_python_utils/bs_logging.py +100 -0
- bs_python_utils/bs_mathstr.py +130 -0
- bs_python_utils/bs_mem.py +148 -0
- bs_python_utils/bs_opt.py +518 -0
- bs_python_utils/bs_plots.py +2 -0
- bs_python_utils/bs_seaborn.py +174 -0
- bs_python_utils/bs_sparse_gaussian.py +46 -0
- bs_python_utils/bsmplutils.py +34 -0
- bs_python_utils/bsnputils.py +957 -0
- bs_python_utils/bssputils.py +79 -0
- bs_python_utils/bsstats.py +463 -0
- bs_python_utils/bsutils.py +363 -0
- bs_python_utils/distance_covariances.py +258 -0
- bs_python_utils/example_opt.py +71 -0
- bs_python_utils/examples_altair.py +195 -0
- bs_python_utils/examples_distance_covariances.py +32 -0
- bs_python_utils/examples_mem.py +25 -0
- bs_python_utils/examples_seaborn.py +37 -0
- bs_python_utils/examples_sklearn.py +33 -0
- bs_python_utils/pandas_utils.py +239 -0
- bs_python_utils/sklearn_utils.py +74 -0
- bs_python_utils-0.0.1.dist-info/LICENSE +21 -0
- bs_python_utils-0.0.1.dist-info/METADATA +71 -0
- bs_python_utils-0.0.1.dist-info/RECORD +28 -0
- bs_python_utils-0.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contains various `scipy` utility programs.
|
|
3
|
+
"""
|
|
4
|
+
from math import sqrt
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import scipy.stats as sts
|
|
9
|
+
from scipy.interpolate import UnivariateSpline
|
|
10
|
+
|
|
11
|
+
from bs_python_utils.bsnputils import check_vector, rice_stderr
|
|
12
|
+
from bs_python_utils.bsutils import bs_error_abort, print_stars
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def describe_array(v: np.ndarray, name: str | None = "v") -> Any:
|
|
16
|
+
"""
|
|
17
|
+
descriptive statistics on an array interpreted as a vector
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
v: the array
|
|
21
|
+
name: its name
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
the `scipy.stats.describe` object
|
|
25
|
+
"""
|
|
26
|
+
print_stars(f"{name} has:")
|
|
27
|
+
d = sts.describe(v, None)
|
|
28
|
+
print(f"Number of elements: {d.nobs}")
|
|
29
|
+
print(f"Minimum: {d.minmax[0]}")
|
|
30
|
+
print(f"Maximum: {d.minmax[1]}")
|
|
31
|
+
print(f"Mean: {d.mean}")
|
|
32
|
+
print(f"Stderr: {sqrt(d.variance)}")
|
|
33
|
+
return d
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def spline_reg(
|
|
37
|
+
y: np.ndarray,
|
|
38
|
+
x: np.ndarray,
|
|
39
|
+
x_new: np.ndarray | None = None,
|
|
40
|
+
is_sorted: bool | None = False,
|
|
41
|
+
smooth: bool | None = True,
|
|
42
|
+
) -> np.ndarray:
|
|
43
|
+
"""
|
|
44
|
+
one-dimensional spline interpolation of vector y on vector x
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
y: vector of y-values
|
|
48
|
+
x: vector of x-values
|
|
49
|
+
x_new: where we evaluate (at the points in `x` by default)
|
|
50
|
+
is_sorted: True if `x` is sorted in increasing order
|
|
51
|
+
smooth: True if we want a smoother; otherwise we go through all points provided
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
values interpolated at `x_new`
|
|
55
|
+
"""
|
|
56
|
+
n = check_vector(x)
|
|
57
|
+
ny = check_vector(y)
|
|
58
|
+
if ny != n:
|
|
59
|
+
bs_error_abort("x and y should have the same size")
|
|
60
|
+
|
|
61
|
+
if not is_sorted:
|
|
62
|
+
# need to sort by increasing value of x
|
|
63
|
+
order_rhs = np.argsort(x)
|
|
64
|
+
rhs = x[order_rhs]
|
|
65
|
+
lhs = y[order_rhs]
|
|
66
|
+
else:
|
|
67
|
+
rhs, lhs = x, y
|
|
68
|
+
|
|
69
|
+
if smooth:
|
|
70
|
+
# we compute a local estimator of the stderr of (y | x) and we use it to enter weights
|
|
71
|
+
sigyx = rice_stderr(lhs, rhs)
|
|
72
|
+
w = 1 / sigyx
|
|
73
|
+
spl = UnivariateSpline(rhs, lhs, w=w)
|
|
74
|
+
else:
|
|
75
|
+
spl = UnivariateSpline(rhs, lhs)
|
|
76
|
+
|
|
77
|
+
xeval = x if x_new is None else x_new
|
|
78
|
+
y_pred = spl(xeval)
|
|
79
|
+
return cast(np.ndarray, y_pred)
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contains some statistical routines.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from itertools import combinations_with_replacement
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import scipy.linalg as spla
|
|
11
|
+
import scipy.stats as sts
|
|
12
|
+
from statsmodels.nonparametric._kernel_base import EstimatorSettings
|
|
13
|
+
from statsmodels.nonparametric.kernel_regression import KernelReg
|
|
14
|
+
|
|
15
|
+
from bs_python_utils.bsnputils import (
|
|
16
|
+
check_matrix,
|
|
17
|
+
check_vector,
|
|
18
|
+
check_vector_or_matrix,
|
|
19
|
+
make_lexico_grid,
|
|
20
|
+
)
|
|
21
|
+
from bs_python_utils.bssputils import spline_reg
|
|
22
|
+
from bs_python_utils.bsutils import bs_error_abort
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class TslsResults:
|
|
27
|
+
"""
|
|
28
|
+
contains full results of a TSLS regression
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
iv_estimates: float | np.ndarray | None
|
|
32
|
+
r2_first_iv: float | np.ndarray | None
|
|
33
|
+
r2_y: float | None
|
|
34
|
+
r2_second: float | np.ndarray | None
|
|
35
|
+
y_proj: float | np.ndarray | None
|
|
36
|
+
y_coeffs: float | np.ndarray | None
|
|
37
|
+
X_IV_proj: float | np.ndarray | None
|
|
38
|
+
b_proj_IV: float | np.ndarray | None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _powers_Z(Z: np.ndarray, degrees: np.ndarray) -> np.ndarray:
|
|
42
|
+
"""
|
|
43
|
+
used internally by `proj_Z`; returns `\\prod_{k=1}^m Z_{\\cdot k}^{l_k}`
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
Z: a matrix `(n, m)`
|
|
47
|
+
list_ints: a list of integers
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
the product of the powers of `Z`
|
|
51
|
+
"""
|
|
52
|
+
if Z.ndim != 2:
|
|
53
|
+
bs_error_abort(f"Z should have dimension 2, not {Z.ndim}")
|
|
54
|
+
m = Z.shape[1]
|
|
55
|
+
mdegs = check_vector(degrees)
|
|
56
|
+
if mdegs != m:
|
|
57
|
+
bs_error_abort("The size of degrees should equal the number of columns of Z")
|
|
58
|
+
res = np.ones(Z.shape[0])
|
|
59
|
+
for i, degi in enumerate(degrees):
|
|
60
|
+
res *= Z[:, i] ** degi
|
|
61
|
+
return res
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _final_proj(Zp: np.ndarray, W: np.ndarray) -> tuple[np.ndarray, np.ndarray, float]:
|
|
65
|
+
MINVAR = 1e-12
|
|
66
|
+
b_proj, _, _, _ = spla.lstsq(Zp, W)
|
|
67
|
+
W_proj = Zp @ b_proj
|
|
68
|
+
if W.ndim == 1:
|
|
69
|
+
var_w = np.var(W)
|
|
70
|
+
r2 = np.var(W_proj) / var_w if var_w > MINVAR else 1.0
|
|
71
|
+
elif W.ndim == 2:
|
|
72
|
+
nw = W.shape[1]
|
|
73
|
+
r2 = np.ones(nw)
|
|
74
|
+
for i in range(nw):
|
|
75
|
+
var_w = np.var(W[:, i])
|
|
76
|
+
if var_w > MINVAR:
|
|
77
|
+
r2[i] = np.var(W_proj[:, i]) / var_w
|
|
78
|
+
else:
|
|
79
|
+
bs_error_abort(f"Wrong number of dimensions {W.ndim} for W")
|
|
80
|
+
return W_proj, b_proj, r2
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _make_Zp(Z: np.ndarray, p: int) -> tuple[np.ndarray, int]:
|
|
84
|
+
nobs, m = Z.shape
|
|
85
|
+
list_vars = list(range(m))
|
|
86
|
+
MAX_NTERMS = round(nobs / 5)
|
|
87
|
+
Zp = np.zeros((nobs, MAX_NTERMS))
|
|
88
|
+
Zp[:, 0] = np.ones(nobs)
|
|
89
|
+
k = 1
|
|
90
|
+
for q in range(1, p + 1):
|
|
91
|
+
listq = list(combinations_with_replacement(list_vars, q))
|
|
92
|
+
lenq = len(listq)
|
|
93
|
+
degrees = np.zeros((m, lenq))
|
|
94
|
+
for i in range(m):
|
|
95
|
+
degrees[i, :] = np.ndarray([x.count(i) for x in listq])
|
|
96
|
+
for j in range(lenq):
|
|
97
|
+
Zp[:, k] = _powers_Z(Z, degrees[:, j])
|
|
98
|
+
k += 1
|
|
99
|
+
if k >= MAX_NTERMS:
|
|
100
|
+
bs_error_abort(f"We don't allow more than {MAX_NTERMS} terms")
|
|
101
|
+
Zp = Zp[:, :k]
|
|
102
|
+
return Zp, k
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def proj_Z(
|
|
106
|
+
W: np.ndarray, Z: np.ndarray, p: int = 1, verbose: bool = False
|
|
107
|
+
) -> tuple[np.ndarray, np.ndarray, float]:
|
|
108
|
+
"""
|
|
109
|
+
project `W` on `Z` up to degree `p` interactions
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
W: variable(s) `(nobs)` or `(nobs, nw)`
|
|
113
|
+
Z: instruments `(nobs) or `(nobs, nz)`;
|
|
114
|
+
they should **not** include a constant term
|
|
115
|
+
p: maximum total degree for interactions of the columns of `Z`
|
|
116
|
+
verbose: prints stuff if True
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
the projections of the columns of `W` on `Z` etc, the coefficients, and the `R^2` of each column
|
|
120
|
+
"""
|
|
121
|
+
nobs = Z.shape[0]
|
|
122
|
+
if W.shape[0] != nobs:
|
|
123
|
+
bs_error_abort("W and Z should have the same number of rows")
|
|
124
|
+
if W.ndim > 2:
|
|
125
|
+
bs_error_abort("W should have 1 or 2 dimensions")
|
|
126
|
+
if Z.ndim > 2:
|
|
127
|
+
bs_error_abort("Z should have 1 or 2 dimensions")
|
|
128
|
+
|
|
129
|
+
if Z.ndim == 1:
|
|
130
|
+
Zp = np.zeros((nobs, 1 + p))
|
|
131
|
+
Zp[:, 0] = np.ones(nobs)
|
|
132
|
+
for q in range(1, p + 1):
|
|
133
|
+
Zp[:, q] = Z**q
|
|
134
|
+
else: # Z is a matrix
|
|
135
|
+
Zp, k = _make_Zp(Z, p)
|
|
136
|
+
if verbose:
|
|
137
|
+
print(f"_proj_Z with degree {p}, using {k} regressors")
|
|
138
|
+
|
|
139
|
+
return _final_proj(Zp, W)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def tsls(y: np.ndarray, X: np.ndarray, Z: np.ndarray) -> TslsResults:
|
|
143
|
+
"""
|
|
144
|
+
TSLS of `y` on `X` with instruments `Z`
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
y: independent variable `(nobs)`
|
|
148
|
+
X: covariates `(nobs, nx)`
|
|
149
|
+
Z: instruments `(nobs, nz)`
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
a `tsls_results` object
|
|
153
|
+
"""
|
|
154
|
+
# first stage
|
|
155
|
+
X_IV_proj, b_proj_IV, r2_first_iv = proj_Z(X, Z)
|
|
156
|
+
# second stage
|
|
157
|
+
y_proj, y_coeffs, r2_y = proj_Z(y, Z)
|
|
158
|
+
_, iv_estimates, r2_second = proj_Z(y_proj, X_IV_proj)
|
|
159
|
+
return TslsResults(
|
|
160
|
+
iv_estimates,
|
|
161
|
+
r2_first_iv,
|
|
162
|
+
r2_y,
|
|
163
|
+
r2_second,
|
|
164
|
+
y_proj,
|
|
165
|
+
y_coeffs,
|
|
166
|
+
X_IV_proj,
|
|
167
|
+
b_proj_IV,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def reg_nonpar(
|
|
172
|
+
y: np.ndarray,
|
|
173
|
+
X: np.ndarray,
|
|
174
|
+
var_types: str | None = None,
|
|
175
|
+
n_sub: int | None = None,
|
|
176
|
+
n_res: int | None = 1,
|
|
177
|
+
) -> tuple[KernelReg, np.ndarray]:
|
|
178
|
+
"""
|
|
179
|
+
nonparametric regression of y on the columns of X;
|
|
180
|
+
bandwidth chosen on a subsample of size nsub if nsub < nobs, and rescaled
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
y: a vector of size nobs
|
|
184
|
+
X: a (nobs) vector or a matrix of shape (nobs, m)
|
|
185
|
+
var_types: specify types of all `X` variables if not all of them are continuous;
|
|
186
|
+
one character per variable
|
|
187
|
+
* 'c' for continuous
|
|
188
|
+
* 'u' discrete unordered
|
|
189
|
+
* 'o' discrete ordered
|
|
190
|
+
n_sub: size of subsample for cross-validation; by default it is `200^{(m+4)/5}`
|
|
191
|
+
n_res: how many subsamples we draw; 1 by default
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
fitted on sample (nobs, with derivatives)
|
|
195
|
+
and bandwidths (m)
|
|
196
|
+
"""
|
|
197
|
+
_ = check_vector_or_matrix(X)
|
|
198
|
+
n_obs = check_vector(y)
|
|
199
|
+
if X.shape[0] != n_obs:
|
|
200
|
+
bs_error_abort("X and y should have the same number of observations")
|
|
201
|
+
m = 1 if X.ndim == 1 else X.shape[1]
|
|
202
|
+
if var_types is None:
|
|
203
|
+
types = "c" * m
|
|
204
|
+
else:
|
|
205
|
+
if len(var_types) != m:
|
|
206
|
+
bs_error_abort("var_types should have one entry for each column of X")
|
|
207
|
+
types = var_types
|
|
208
|
+
|
|
209
|
+
if n_sub is None:
|
|
210
|
+
n_sub = round(200 ** ((m + 4.0) / 5.0))
|
|
211
|
+
|
|
212
|
+
k = KernelReg(
|
|
213
|
+
y,
|
|
214
|
+
X,
|
|
215
|
+
var_type=types,
|
|
216
|
+
defaults=EstimatorSettings(
|
|
217
|
+
efficient=True, n_sub=n_sub, randomize=True, n_res=n_res
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
return k.fit(), k.bw
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def reg_nonpar_fit(
|
|
224
|
+
y: np.ndarray,
|
|
225
|
+
X: np.ndarray,
|
|
226
|
+
var_types: str | None = None,
|
|
227
|
+
n_sub: int | None = None,
|
|
228
|
+
n_res: int = 1,
|
|
229
|
+
verbose: bool = False,
|
|
230
|
+
) -> np.ndarray:
|
|
231
|
+
"""
|
|
232
|
+
nonparametric regression of y on the columns of X; bandwidth chosen on a subsample of size nsub if nsub < nobs, and rescaled
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
y: a vector of size nobs
|
|
236
|
+
X: a (nobs) vector or a matrix of shape (nobs, m)
|
|
237
|
+
var_types: specify types of all `X` variables if not all of them are continuous;
|
|
238
|
+
one character per variable
|
|
239
|
+
* 'c' for continuous
|
|
240
|
+
* 'u' discrete unordered
|
|
241
|
+
* 'o' discrete ordered
|
|
242
|
+
n_sub: size of subsample for cross-validation; by default it is `200^{(m+4)/5}`
|
|
243
|
+
n_res: how many subsamples we draw; 1 by default
|
|
244
|
+
verbose: prints stuff if True
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
fitted values on sample (nobs)
|
|
248
|
+
"""
|
|
249
|
+
kfbw = reg_nonpar(y, X, var_types, n_sub, n_res)
|
|
250
|
+
fitted_vals = cast(np.ndarray, kfbw[0][0])
|
|
251
|
+
return fitted_vals
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def flexible_reg(
|
|
255
|
+
Y: np.ndarray,
|
|
256
|
+
X: np.ndarray,
|
|
257
|
+
mode: str = "NP",
|
|
258
|
+
var_types: str | None = None,
|
|
259
|
+
n_sub: int | None = None,
|
|
260
|
+
n_res: int = 1,
|
|
261
|
+
verbose: bool = False,
|
|
262
|
+
) -> np.ndarray:
|
|
263
|
+
"""
|
|
264
|
+
flexible regression of `Y` on `X`
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
Y: independent variable `(nobs)` or `(nobs, ny)`
|
|
268
|
+
X: covariates `(nobs)` or `(nobs, nx)`; should **not** include a constant term
|
|
269
|
+
mode: what flexible means
|
|
270
|
+
* 'NP': non parametric
|
|
271
|
+
* 'SPL': spline regression, only on one covariate
|
|
272
|
+
* '1': linear
|
|
273
|
+
* '2': quadratic
|
|
274
|
+
var_types: [for 'NP' only] specify types of all `X` variables if not all of them are continuous;
|
|
275
|
+
one character per variable
|
|
276
|
+
* 'c' for continuous
|
|
277
|
+
* 'u' discrete unordered
|
|
278
|
+
* 'o' discrete ordered
|
|
279
|
+
n_sub: [for 'NP' only] size of subsample for cross-validation; \
|
|
280
|
+
by default it is `200^{(m+4)/5}`
|
|
281
|
+
n_res: [for 'NP' only] how many subsamples we draw; 1 if `None`
|
|
282
|
+
verbose: prints stuff if True
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
`E(y|X)` at the sample points
|
|
286
|
+
"""
|
|
287
|
+
if mode == "NP":
|
|
288
|
+
if Y.ndim == 2:
|
|
289
|
+
ny = Y.shape[1]
|
|
290
|
+
Y_fit = np.zeros_like(Y)
|
|
291
|
+
for iy in range(ny):
|
|
292
|
+
Y_fit[:, iy] = reg_nonpar_fit(
|
|
293
|
+
Y[:, iy],
|
|
294
|
+
X,
|
|
295
|
+
var_types=var_types,
|
|
296
|
+
n_sub=n_sub,
|
|
297
|
+
n_res=n_res,
|
|
298
|
+
verbose=verbose,
|
|
299
|
+
)
|
|
300
|
+
return Y_fit
|
|
301
|
+
else:
|
|
302
|
+
return reg_nonpar_fit(
|
|
303
|
+
Y, X, var_types=var_types, n_sub=n_sub, n_res=n_res, verbose=verbose
|
|
304
|
+
)
|
|
305
|
+
elif mode == "SPL":
|
|
306
|
+
if X.ndim > 1:
|
|
307
|
+
bs_error_abort("with a spline, only works in one dimension")
|
|
308
|
+
return spline_reg(Y, X)
|
|
309
|
+
else:
|
|
310
|
+
try:
|
|
311
|
+
imode = int(mode)
|
|
312
|
+
except TypeError:
|
|
313
|
+
bs_error_abort(f"does not accept mode={mode}")
|
|
314
|
+
preg, _, _ = proj_Z(Y, X, p=imode, verbose=verbose)
|
|
315
|
+
return preg
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def bs_multivariate_normal_pdf(
|
|
319
|
+
values_x: np.ndarray, means_x: float | np.ndarray, cov_mat: float | np.ndarray
|
|
320
|
+
) -> np.ndarray:
|
|
321
|
+
"""
|
|
322
|
+
Multivariate (or univariate) normal probability density function at values_x
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
values_x: values at which to evaluate the pdf, an `n`-vector or an `(n, nvars)` matrix
|
|
326
|
+
means_x: means of the multivariate normal, a float or an `(nvars)` vector
|
|
327
|
+
cov_mat: covariance matrix of the multivariate normal, a float or an `(nvars, nvars)` matrix
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
the values of the density at `values_x`
|
|
331
|
+
"""
|
|
332
|
+
ndims_values = check_vector_or_matrix(values_x, "bs_multivariate_normal_pdf")
|
|
333
|
+
if ndims_values == 1: # we are evaluating a univariate normal
|
|
334
|
+
# if not type(means_x) == float:
|
|
335
|
+
# bs_error_abort(f"means_x should be a float as values_x is a vector")
|
|
336
|
+
# if not type(cov_mat) == float:
|
|
337
|
+
# bs_error_abort(f"cov_mat should be a float as values_x is a vector")
|
|
338
|
+
sigma2 = cov_mat
|
|
339
|
+
resid = values_x - means_x
|
|
340
|
+
dval = np.exp(-0.5 * resid * resid / sigma2) / np.sqrt(2 * np.pi * sigma2)
|
|
341
|
+
return cast(np.ndarray, dval)
|
|
342
|
+
else: # we are evaluating a multivariate normal
|
|
343
|
+
n, nvars = values_x.shape
|
|
344
|
+
n_means = check_vector(means_x, "bs_multivariate_normal_pdf")
|
|
345
|
+
if n_means != nvars:
|
|
346
|
+
bs_error_abort(f"means_x should be a vector of size {nvars} not {n_means}")
|
|
347
|
+
nrows, ncols = check_matrix(cov_mat, "bs_multivariate_normal_pdf")
|
|
348
|
+
if nrows != ncols or nrows != nvars:
|
|
349
|
+
bs_error_abort(
|
|
350
|
+
f"cov_mat should be a matrix ({nvars}, {nvars}) not ({nrows}, {ncols})"
|
|
351
|
+
)
|
|
352
|
+
resid = values_x - means_x
|
|
353
|
+
argresid = spla.solve(cov_mat, resid.T)
|
|
354
|
+
argexp = np.zeros(n)
|
|
355
|
+
for i in range(n):
|
|
356
|
+
argexp[i] = np.dot(resid[i, :], argresid[:, i])
|
|
357
|
+
dval = np.exp(-0.5 * argexp) / np.sqrt(
|
|
358
|
+
((2 * np.pi) ** nvars) * spla.det(cov_mat)
|
|
359
|
+
)
|
|
360
|
+
return cast(np.ndarray, dval)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def estimate_pdf(
|
|
364
|
+
x_obs: np.ndarray,
|
|
365
|
+
x_points: np.ndarray,
|
|
366
|
+
MIN_SIZE_NONPAR: int = 200,
|
|
367
|
+
weights: np.ndarray | None = None,
|
|
368
|
+
) -> np.ndarray:
|
|
369
|
+
"""
|
|
370
|
+
return an estimate of the conditional densities of `x` at points `values_x` (Silverman rule)
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
x_obs: an `n`-vector or an `(n, nvars)` matrix of the observed values of `x`
|
|
374
|
+
x_points: an `m`-vector or an `(m, nvars)` matrix of x values
|
|
375
|
+
MIN_SIZE_NONPAR: minimum size above which we use kernel density estimators
|
|
376
|
+
weights: an `n`-vector of weights for the observations, if present
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
the density estimates at `values_x`
|
|
380
|
+
"""
|
|
381
|
+
ndims_x = check_vector_or_matrix(x_obs, "estimate_pdf")
|
|
382
|
+
ndims_valx = check_vector_or_matrix(x_points, "estimate_pdf")
|
|
383
|
+
|
|
384
|
+
if ndims_x == 1:
|
|
385
|
+
n_obs = x_obs.size
|
|
386
|
+
if ndims_valx != 1:
|
|
387
|
+
bs_error_abort(f"x_points should have one dimension, not {ndims_valx}")
|
|
388
|
+
xt_obs = x_obs.reshape((-1, 1))
|
|
389
|
+
nvars = 1
|
|
390
|
+
xt_points = x_points.reshape((-1, 1))
|
|
391
|
+
else: # ndims_x == 2
|
|
392
|
+
n_obs, nvars = x_obs.shape
|
|
393
|
+
if ndims_valx == 1: # only one x point with nv elements
|
|
394
|
+
nv = x_points.size
|
|
395
|
+
else: # several x points with nv elements
|
|
396
|
+
nv = x_points.shape[1]
|
|
397
|
+
if nv != nvars:
|
|
398
|
+
bs_error_abort(f"x_points should have {nvars} variables, not {nv}")
|
|
399
|
+
xt_obs = x_obs
|
|
400
|
+
xt_points = x_points
|
|
401
|
+
|
|
402
|
+
if weights is not None:
|
|
403
|
+
n_w = check_vector(weights, "estimate_pdf")
|
|
404
|
+
if n_w != n_obs:
|
|
405
|
+
bs_error_abort(
|
|
406
|
+
f"if weights is given, it should be a vector of size {n_obs} not {n_w}"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
min_size_np = MIN_SIZE_NONPAR ** ((4.0 + nvars) / 5.0)
|
|
410
|
+
|
|
411
|
+
if n_obs > min_size_np: # cell large enough to use nonparametrics
|
|
412
|
+
# fit joint density of x
|
|
413
|
+
kde = sts.gaussian_kde(xt_obs.T, bw_method="silverman", weights=weights)
|
|
414
|
+
# density of x at values_x
|
|
415
|
+
f_x = kde.evaluate(xt_points.T)
|
|
416
|
+
else:
|
|
417
|
+
# sample too small, we fit a normal
|
|
418
|
+
if ndims_x == 1: # univariate
|
|
419
|
+
mean_x = np.mean(x_obs)
|
|
420
|
+
var_x = np.var(x_obs)
|
|
421
|
+
f_x = bs_multivariate_normal_pdf(x_points, mean_x, var_x)
|
|
422
|
+
else: # multivariate
|
|
423
|
+
means_x = np.mean(x_obs, 0)
|
|
424
|
+
cov_mat = np.cov(x_obs.T)
|
|
425
|
+
f_x = bs_multivariate_normal_pdf(x_points, means_x, cov_mat)
|
|
426
|
+
if weights is not None:
|
|
427
|
+
f_x *= weights / np.mean(weights)
|
|
428
|
+
return cast(np.ndarray, f_x)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def estimate_densities_at_quantiles(
|
|
432
|
+
X: np.ndarray, qtiles: np.ndarray
|
|
433
|
+
) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
434
|
+
"""
|
|
435
|
+
estimate densities of margins at prespecified quantiles (Silverman rule)
|
|
436
|
+
and the joint density at each vector of these quantiles
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
X: `n`-vector or `(n, nx)`-matrix
|
|
440
|
+
qtiles: vector of `nq` numbers between 0 and 1
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
if `X` is a matrix, the `({nq}^{nx}, {nx})` matrices of estimated margin densities \
|
|
444
|
+
and the `{nq}^{nx}` vector of the joint density on the lexicographic grid of quantiles;
|
|
445
|
+
if `X` is a vector, the `nq`-vector of the density at the quantiles, twice
|
|
446
|
+
"""
|
|
447
|
+
ndims_X = check_vector_or_matrix(X, "estimate_densities_")
|
|
448
|
+
if ndims_X == 1:
|
|
449
|
+
f_X = estimate_pdf(X, np.quantile(X, qtiles))
|
|
450
|
+
return f_X, f_X
|
|
451
|
+
else:
|
|
452
|
+
nx = X.shape[1]
|
|
453
|
+
nq = qtiles.size
|
|
454
|
+
f_X_k = np.zeros((nq, nx))
|
|
455
|
+
nodes_mat = np.zeros((nq, nx))
|
|
456
|
+
for i_x in range(nx):
|
|
457
|
+
X_ix = X[:, i_x]
|
|
458
|
+
nodes_mat[:, i_x] = np.quantile(X_ix, qtiles)
|
|
459
|
+
f_X_k[:, i_x] = estimate_pdf(X_ix, nodes_mat[:, i_x])
|
|
460
|
+
f_margins = make_lexico_grid(f_X_k)
|
|
461
|
+
values_X = make_lexico_grid(nodes_mat)
|
|
462
|
+
f_X = estimate_pdf(X, values_X) # joint density
|
|
463
|
+
return f_margins, f_X, values_X
|