bs-python-utils 0.8.5__tar.gz → 0.9.2__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.
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/PKG-INFO +5 -1
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/README.md +3 -0
- bs_python_utils-0.9.2/bs_python_utils/bivariate_quantiles.py +238 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils.egg-info/PKG-INFO +5 -1
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils.egg-info/requires.txt +1 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/pyproject.toml +2 -1
- bs_python_utils-0.9.2/tests/test_bivariate_quantiles.py +40 -0
- bs_python_utils-0.8.5/bs_python_utils/bivariate_quantiles.py +0 -306
- bs_python_utils-0.8.5/tests/test_bivariate_quantiles.py +0 -80
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/LICENSE +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid1prec13.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid1prec17.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid1prec9.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid2prec13.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid2prec17.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid2prec9.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid3prec13.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid3prec17.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid3prec9.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid4prec13.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid4prec17.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid4prec9.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid5prec13.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid5prec17.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid5prec9.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/Timer.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/__init__.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/bs_altair.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/bs_logging.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/bs_mathstr.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/bs_mem.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/bs_opt.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/bs_plots.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/bs_seaborn.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/bs_sparse_gaussian.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/bsmplutils.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/bsnputils.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/bssputils.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/bsstats.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/bsutils.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/chebyshev.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/distance_covariances.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/examples/examples_altair.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/examples/examples_distance_covariances.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/examples/examples_mem.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/examples/examples_mpl.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/examples/examples_opt.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/examples/examples_seaborn.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/examples/examples_sklearn.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/examples/examples_sparse_gaussian.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/pandas_utils.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/sklearn_utils.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/streamlit_utils.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils.egg-info/SOURCES.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils.egg-info/dependency_links.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils.egg-info/top_level.txt +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/setup.cfg +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/tests/test_bs_opt.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/tests/test_bs_sparse_gaussian.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/tests/test_bsnputils.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/tests/test_bssputils.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/tests/test_bsstats.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/tests/test_bsutils.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/tests/test_chebyshev.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/tests/test_nptri.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/tests/test_random_draws.py +0 -0
- {bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/tests/test_sklearn_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bs-python-utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Utilities programs for my Python code
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -25,6 +25,7 @@ Requires-Dist: statsmodels>=0.14.5
|
|
|
25
25
|
Requires-Dist: streamlit>=1.49.1
|
|
26
26
|
Requires-Dist: vega-datasets>=0.9.0
|
|
27
27
|
Requires-Dist: colorama>=0.4.6
|
|
28
|
+
Requires-Dist: pyarrow>=21.0.0
|
|
28
29
|
Dynamic: license-file
|
|
29
30
|
|
|
30
31
|
## bs-python-utils
|
|
@@ -42,6 +43,9 @@ Dynamic: license-file
|
|
|
42
43
|
|
|
43
44
|
### Release notes
|
|
44
45
|
|
|
46
|
+
#### 0.9 (March 18, 2026)
|
|
47
|
+
Improved bivariate quantiles.
|
|
48
|
+
|
|
45
49
|
#### 0.8.5 (January 25, 2026)
|
|
46
50
|
Added colored logger.
|
|
47
51
|
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""This takes in observations of a bivariate random variable `y`
|
|
2
|
+
and computes vector quantiles and vector ranks à la
|
|
3
|
+
[Chernozhukov-Galichon-Hallin-Henry (*Ann. Stats.* 2017)](
|
|
4
|
+
https://projecteuclid.org/journals/annals-of-statistics/volume-45/
|
|
5
|
+
issue-1/MongeKantorovich-depth-quantiles-ranks-and-signs/10.1214/16-AOS1450.full).
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Note:
|
|
9
|
+
if the math looks strange in the documentation, just reload the page.
|
|
10
|
+
|
|
11
|
+
The sequence of steps is as follows:
|
|
12
|
+
|
|
13
|
+
1. optimize the weights: `v = solve_for_v(y, n_nodes)` given `n_nodes` Chebyshev nodes for numerical integration
|
|
14
|
+
2. to obtain the $(u_1,u_2)$ quantiles for $(u_1, u_2)\\in [0,1]$, run
|
|
15
|
+
`qtiles_y = bivariate_quantiles_v(y, v, u1, u2)`
|
|
16
|
+
3. to compute the vector ranks for all points in the sample (the barycenters
|
|
17
|
+
of the cells in the power diagram):
|
|
18
|
+
`ranks_y = bivariate_ranks_v(y, v, n_nodes)`
|
|
19
|
+
|
|
20
|
+
Steps 1 and 2 can be combined: `qtiles_y = bivariate_quantiles(y, v, u1, u2, n_nodes)`
|
|
21
|
+
|
|
22
|
+
Steps 1 and 3 can be combined: `ranks_y = bivariate_ranks(y, n_nodes)`
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from typing import cast
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
|
|
29
|
+
from bs_python_utils.bs_opt import minimize_free, print_optimization_results
|
|
30
|
+
from bs_python_utils.bsnputils import TwoArrays, npmaxabs
|
|
31
|
+
from bs_python_utils.bsutils import bs_error_abort
|
|
32
|
+
from bs_python_utils.chebyshev import Interval, cheb_get_nodes_1d
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _compute_ad(y: np.ndarray) -> TwoArrays:
|
|
36
|
+
"""Build the `A` and `dy2` matrices used in the dual optimisation."""
|
|
37
|
+
y1 = y[:, 0]
|
|
38
|
+
dy1 = np.subtract.outer(y1, y1)
|
|
39
|
+
y2 = y[:, 1]
|
|
40
|
+
dy2 = np.subtract.outer(y2, y2)
|
|
41
|
+
np.fill_diagonal(dy2, 1.0)
|
|
42
|
+
dy2 = dy2.T
|
|
43
|
+
a_mat = np.divide(dy1, dy2)
|
|
44
|
+
return a_mat, dy2
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _compute_m_M(
|
|
48
|
+
v: np.ndarray, a_mat: np.ndarray, dy2: np.ndarray, tau1_nodes: np.ndarray
|
|
49
|
+
) -> TwoArrays:
|
|
50
|
+
"""Build the `m` and `M` matrices used in the dual optimisation."""
|
|
51
|
+
dv = np.subtract.outer(v, v)
|
|
52
|
+
b_mat = dv / dy2
|
|
53
|
+
np.fill_diagonal(dy2, 0.0)
|
|
54
|
+
EPS = 1e-12
|
|
55
|
+
maskp = dy2 < EPS
|
|
56
|
+
maskm = dy2 > -EPS
|
|
57
|
+
n, n_nodes = v.size, tau1_nodes.size
|
|
58
|
+
m_low = np.empty((n, n_nodes))
|
|
59
|
+
m_high = np.empty((n, n_nodes))
|
|
60
|
+
for i, tau1 in enumerate(tau1_nodes):
|
|
61
|
+
f_mat = tau1 * a_mat - b_mat
|
|
62
|
+
f_matp = f_mat.copy()
|
|
63
|
+
f_matm = f_mat.copy()
|
|
64
|
+
f_matp[maskp] = 1
|
|
65
|
+
f_matm[maskm] = 0
|
|
66
|
+
m_low[:, i] = np.max(f_matm, axis=1)
|
|
67
|
+
m_high[:, i] = np.min(f_matp, axis=1)
|
|
68
|
+
return np.clip(m_low, 0.0, 1.0), np.clip(m_high, 0.0, 1.0)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def bivariate_quantiles_v(y: np.ndarray, tau: np.ndarray, v: np.ndarray) -> np.ndarray:
|
|
72
|
+
"""Evaluate vector quantiles for a given set of dual weights.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
y: Observations with shape ``(n, 2)``.
|
|
76
|
+
tau: Evaluation points in ``[0, 1]^2`` (shape ``(m, 2)``).
|
|
77
|
+
v: Dual weights solving the optimal transport problem (length ``n``).
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Array of quantile locations with shape ``(m, 2)``.
|
|
81
|
+
"""
|
|
82
|
+
if tau.shape[1] != 2:
|
|
83
|
+
bs_error_abort("tau must have two columns")
|
|
84
|
+
q = y[np.argmax(tau @ y.T - v, axis=1), :]
|
|
85
|
+
return cast(np.ndarray, q)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _objgrad(
|
|
89
|
+
v: np.ndarray, args: list, gr: bool = False
|
|
90
|
+
) -> float | tuple[float, np.ndarray, np.ndarray]:
|
|
91
|
+
"""computes the expectation of $\\psi(U, v)$ and perhaps its gradient wrt `v` or the bivariate ranks
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
v: an `(n-1)`-vector
|
|
95
|
+
args: a list of other arguments `[y, a_mat, dy2, tau1_nodes, tau1_weights, verbose]`
|
|
96
|
+
gr: if `False`, we only return the value of the objective function
|
|
97
|
+
if `True`, we also return the gradient and the bivariate ranks
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
the value of the expectation and perhaps its gradient and the bivariate ranks
|
|
101
|
+
"""
|
|
102
|
+
y = args[0]
|
|
103
|
+
y1 = y[:, 0]
|
|
104
|
+
y2 = y[:, 1]
|
|
105
|
+
n = y.shape[0]
|
|
106
|
+
a_mat, dy2 = args[1], args[2]
|
|
107
|
+
tau1_nodes = args[3]
|
|
108
|
+
tau1_weights = args[4]
|
|
109
|
+
vs1 = np.append(v, -np.sum(v))
|
|
110
|
+
m, M = _compute_m_M(vs1, a_mat, dy2, tau1_nodes)
|
|
111
|
+
# print(f"m is {m}")
|
|
112
|
+
# print(f"M is {M}")
|
|
113
|
+
# import sys
|
|
114
|
+
|
|
115
|
+
# sys.exit(1)
|
|
116
|
+
|
|
117
|
+
EPS = 1e-12
|
|
118
|
+
obj_val = 0.0
|
|
119
|
+
probs = np.zeros(n)
|
|
120
|
+
bivrank = np.zeros((n, 2))
|
|
121
|
+
for k in range(n):
|
|
122
|
+
Mk = M[k, :]
|
|
123
|
+
mk = m[k, :]
|
|
124
|
+
pos_diffs = np.maximum(Mk - mk, 0.0)
|
|
125
|
+
# print(f"pos_diffs for k={k} are {pos_diffs}")
|
|
126
|
+
pos_diffs_sq = np.maximum(Mk * Mk - mk * mk, 0.0)
|
|
127
|
+
probs[k] = pos_diffs @ tau1_weights
|
|
128
|
+
# print(f"probs[{k}] = {probs[k]}")
|
|
129
|
+
factor1 = (tau1_nodes * pos_diffs) @ tau1_weights
|
|
130
|
+
factor2 = (pos_diffs_sq @ tau1_weights) / 2.0
|
|
131
|
+
obj_val += y1[k] * factor1 + y2[k] * factor2 - vs1[k] * probs[k]
|
|
132
|
+
if probs[k] > EPS:
|
|
133
|
+
bivrank[k, 0] = factor1 / probs[k]
|
|
134
|
+
bivrank[k, 1] = factor2 / probs[k]
|
|
135
|
+
|
|
136
|
+
# print(f"{np.min(probs)=}")
|
|
137
|
+
|
|
138
|
+
if gr:
|
|
139
|
+
grad_val = probs[-1] - probs[:-1]
|
|
140
|
+
return obj_val, grad_val, bivrank
|
|
141
|
+
else:
|
|
142
|
+
return obj_val
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _obj(v: np.ndarray, args: list):
|
|
146
|
+
return _objgrad(v, args)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _grad(v: np.ndarray, args: list):
|
|
150
|
+
res_objg = cast(tuple[float, np.ndarray], _objgrad(v, args, gr=True))
|
|
151
|
+
grad_val = res_objg[1]
|
|
152
|
+
verbose = args[-1]
|
|
153
|
+
if verbose:
|
|
154
|
+
print(f"The error on the gradient is {npmaxabs(grad_val)}")
|
|
155
|
+
return grad_val
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _solve_for_v(y: np.ndarray, n_nodes: int = 32, verbose: bool = False) -> TwoArrays:
|
|
159
|
+
"""Solve the dual optimisation to obtain the optimal weights ``v`` and the bivariate ranks
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
y: Observations with shape ``(n, 2)``.
|
|
163
|
+
n_nodes: Number of Chebyshev nodes for the quadrature.
|
|
164
|
+
verbose: Print optimisation diagnostics when ``True``.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Array of length ``n`` containing the optimal weights (including the
|
|
168
|
+
residual term).
|
|
169
|
+
Array of shape ``(n, 2)`` containing the bivariate ranks (the barycenters of the cells in the power diagram).
|
|
170
|
+
"""
|
|
171
|
+
d = y.shape[1]
|
|
172
|
+
|
|
173
|
+
if d != 2:
|
|
174
|
+
bs_error_abort(f"only works for 2-dimensional y, not for {d}")
|
|
175
|
+
|
|
176
|
+
v0 = np.mean(y[:-1, :], 1)
|
|
177
|
+
|
|
178
|
+
interval01 = Interval(0.0, 1.0)
|
|
179
|
+
tau1_nodes, tau1_weights = cheb_get_nodes_1d(interval01, n_nodes)
|
|
180
|
+
|
|
181
|
+
a_mat, dy2 = _compute_ad(y)
|
|
182
|
+
|
|
183
|
+
argsog = [y, a_mat, dy2, tau1_nodes, tau1_weights, verbose]
|
|
184
|
+
|
|
185
|
+
res = minimize_free(_obj, _grad, v0, args=argsog)
|
|
186
|
+
if verbose:
|
|
187
|
+
print_optimization_results(res, "Minimizing over v")
|
|
188
|
+
|
|
189
|
+
if not res.success:
|
|
190
|
+
bs_error_abort("Problem! the optimization failed.")
|
|
191
|
+
vstar = res.x
|
|
192
|
+
if verbose:
|
|
193
|
+
print(f"The final gradient over v is close to 0: error {npmaxabs(res.jac)}")
|
|
194
|
+
_, _, bivranks = cast(tuple, _objgrad(vstar, argsog, gr=True))
|
|
195
|
+
vstar = np.append(vstar, -np.sum(vstar))
|
|
196
|
+
return cast(np.ndarray, vstar), cast(np.ndarray, bivranks)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def bivariate_ranks(
|
|
200
|
+
y: np.ndarray,
|
|
201
|
+
n_nodes: int = 32,
|
|
202
|
+
verbose: bool = False,
|
|
203
|
+
) -> np.ndarray:
|
|
204
|
+
"""Compute the barycentric ranks of each observation.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
y: Observations with shape ``(n, 2)``.
|
|
208
|
+
n_nodes: Number of Chebyshev nodes used in the quadrature.
|
|
209
|
+
verbose: Print diagnostics when ``True``.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Array of average ranks (shape ``(n, 2)``) with ``nan`` for zero-mass cells.
|
|
213
|
+
"""
|
|
214
|
+
d = y.shape[1]
|
|
215
|
+
|
|
216
|
+
if d != 2:
|
|
217
|
+
bs_error_abort(f"only works for 2-dimensional y, not for {d}")
|
|
218
|
+
|
|
219
|
+
_, bivranks = _solve_for_v(y, n_nodes, verbose)
|
|
220
|
+
return cast(np.ndarray, bivranks)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def bivariate_quantiles(
|
|
224
|
+
y: np.ndarray, tau: np.ndarray, n_nodes: int = 32, verbose: bool = False
|
|
225
|
+
) -> np.ndarray:
|
|
226
|
+
"""Solve for the dual weights then evaluate bivariate quantiles.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
y: Observations, shape ``(n, 2)``.
|
|
230
|
+
tau: Query points in ``[0, 1]^2`` (shape ``(m, 2)``).
|
|
231
|
+
n_nodes: Number of Chebyshev nodes for the quadrature.
|
|
232
|
+
verbose: Print optimisation diagnostics when ``True``.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Bivariate quantiles at ``u``.
|
|
236
|
+
"""
|
|
237
|
+
v, _ = _solve_for_v(y, n_nodes, verbose)
|
|
238
|
+
return bivariate_quantiles_v(y, tau, v)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bs-python-utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Utilities programs for my Python code
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -25,6 +25,7 @@ Requires-Dist: statsmodels>=0.14.5
|
|
|
25
25
|
Requires-Dist: streamlit>=1.49.1
|
|
26
26
|
Requires-Dist: vega-datasets>=0.9.0
|
|
27
27
|
Requires-Dist: colorama>=0.4.6
|
|
28
|
+
Requires-Dist: pyarrow>=21.0.0
|
|
28
29
|
Dynamic: license-file
|
|
29
30
|
|
|
30
31
|
## bs-python-utils
|
|
@@ -42,6 +43,9 @@ Dynamic: license-file
|
|
|
42
43
|
|
|
43
44
|
### Release notes
|
|
44
45
|
|
|
46
|
+
#### 0.9 (March 18, 2026)
|
|
47
|
+
Improved bivariate quantiles.
|
|
48
|
+
|
|
45
49
|
#### 0.8.5 (January 25, 2026)
|
|
46
50
|
Added colored logger.
|
|
47
51
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bs-python-utils"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.2"
|
|
4
4
|
description = "Utilities programs for my Python code"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -25,6 +25,7 @@ dependencies = [
|
|
|
25
25
|
"streamlit>=1.49.1",
|
|
26
26
|
"vega-datasets>=0.9.0",
|
|
27
27
|
"colorama>=0.4.6",
|
|
28
|
+
"pyarrow>=21.0.0",
|
|
28
29
|
]
|
|
29
30
|
|
|
30
31
|
[tool.setuptools]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from bs_python_utils.bivariate_quantiles import (
|
|
4
|
+
_compute_ad,
|
|
5
|
+
_solve_for_v,
|
|
6
|
+
)
|
|
7
|
+
from bs_python_utils.bsnputils import npmaxabs
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_bivariate_ranks():
|
|
11
|
+
rng = np.random.default_rng(123)
|
|
12
|
+
n = 500
|
|
13
|
+
y = rng.normal(size=(n, 2))
|
|
14
|
+
nodes = 32
|
|
15
|
+
|
|
16
|
+
_, bivranks = _solve_for_v(y, n_nodes=nodes, verbose=False)
|
|
17
|
+
|
|
18
|
+
bivranks_th = np.empty_like(bivranks)
|
|
19
|
+
for k in range(n):
|
|
20
|
+
bivranks_th[k, 0] = np.mean(y[:, 0] < y[k, 0]) + 0.5 * np.mean(
|
|
21
|
+
y[:, 0] == y[k, 0]
|
|
22
|
+
)
|
|
23
|
+
bivranks_th[k, 1] = np.mean(y[:, 1] < y[k, 1]) + 0.5 * np.mean(
|
|
24
|
+
y[:, 1] == y[k, 1]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
assert npmaxabs(bivranks - bivranks_th) < 0.01
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_compute_ad():
|
|
31
|
+
y = np.array([[0.0, 1.0], [1.0, 1.3], [-1.0, 3.0]])
|
|
32
|
+
a_mat, dy2 = _compute_ad(y)
|
|
33
|
+
|
|
34
|
+
a_mat_th = np.array(
|
|
35
|
+
[[0.0, -1.0 / 0.3, 0.5], [-1.0 / 0.3, 0.0, 2.0 / 1.7], [0.5, 2.0 / 1.7, 0.0]]
|
|
36
|
+
)
|
|
37
|
+
dy2_th = np.array([[1.0, 0.3, 2.0], [-0.3, 1.0, 1.7], [-2.0, -1.7, 1.0]])
|
|
38
|
+
|
|
39
|
+
assert np.allclose(a_mat, a_mat_th)
|
|
40
|
+
assert np.allclose(dy2, dy2_th)
|
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
"""This takes in observations of a bivariate random variable `y`
|
|
2
|
-
and computes vector quantiles and vector ranks à la
|
|
3
|
-
[Chernozhukov-Galichon-Hallin-Henry (*Ann. Stats.* 2017)](
|
|
4
|
-
https://projecteuclid.org/journals/annals-of-statistics/volume-45/
|
|
5
|
-
issue-1/MongeKantorovich-depth-quantiles-ranks-and-signs/10.1214/16-AOS1450.full).
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Note:
|
|
9
|
-
if the math looks strange in the documentation, just reload the page.
|
|
10
|
-
|
|
11
|
-
The sequence of steps is as follows:
|
|
12
|
-
|
|
13
|
-
* choose a number of Chebyshev nodes for numerical integration and optimize
|
|
14
|
-
the weights: `v = solve_for_v(y, n_nodes)`
|
|
15
|
-
* to obtain the $(u_1,u_2)$ quantiles for $(u_1, u_2)\\in [0,1]$, run
|
|
16
|
-
`qtiles_y = bivariate_quantiles_v(y, v, u1, u2)`
|
|
17
|
-
* to compute the vector ranks for all points in the sample (the barycenters
|
|
18
|
-
of the cells in the power diagram):
|
|
19
|
-
`ranks_y = bivariate_ranks_v(y, v, n_nodes)`
|
|
20
|
-
|
|
21
|
-
Steps 1 and 2 can be combined: `qtiles_y = bivariate_quantiles(y, v, u1, u2, n_nodes)`
|
|
22
|
-
|
|
23
|
-
Steps 1 and 3 can be combined: `ranks_y = bivariate_ranks(y, n_nodes)`
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
from typing import cast
|
|
27
|
-
|
|
28
|
-
import numpy as np
|
|
29
|
-
|
|
30
|
-
from bs_python_utils.bs_opt import minimize_free, print_optimization_results
|
|
31
|
-
from bs_python_utils.bsnputils import TwoArrays, npmaxabs
|
|
32
|
-
from bs_python_utils.bsutils import bs_error_abort
|
|
33
|
-
from bs_python_utils.chebyshev import Interval, cheb_get_nodes_1d
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _compute_ab(y_sorted: np.ndarray, v_sorted: np.ndarray) -> TwoArrays:
|
|
37
|
-
"""Build the `A` and `B` matrices used in the dual optimisation."""
|
|
38
|
-
y1 = y_sorted[:, 0]
|
|
39
|
-
dy1 = np.subtract.outer(y1, y1)
|
|
40
|
-
y2 = y_sorted[:, 1]
|
|
41
|
-
dy2 = np.subtract.outer(y2, y2)
|
|
42
|
-
np.fill_diagonal(dy2, 1.0)
|
|
43
|
-
dv = np.subtract.outer(v_sorted, v_sorted)
|
|
44
|
-
with np.errstate(divide="ignore", invalid="ignore"):
|
|
45
|
-
a_mat = np.divide(dy1.T, dy2, where=np.abs(dy2) > 1e-12)
|
|
46
|
-
b_mat = np.divide(dv.T, dy2, where=np.abs(dy2) > 1e-12)
|
|
47
|
-
a_mat = np.nan_to_num(a_mat, nan=0.0, posinf=0.0, neginf=0.0)
|
|
48
|
-
b_mat = np.nan_to_num(b_mat, nan=0.0, posinf=0.0, neginf=0.0)
|
|
49
|
-
return a_mat, b_mat
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _compute_u2_bounds(
|
|
53
|
-
k: int, u1: np.ndarray, a_mat: np.ndarray, b_mat: np.ndarray
|
|
54
|
-
) -> TwoArrays:
|
|
55
|
-
"""Return the admissible interval of ``u2`` that selects index ``k``."""
|
|
56
|
-
n = a_mat.shape[0]
|
|
57
|
-
m = u1.size
|
|
58
|
-
if k == 0:
|
|
59
|
-
left_bound = np.zeros(m)
|
|
60
|
-
a_right = a_mat[0, 1:]
|
|
61
|
-
b_right = b_mat[0, 1:]
|
|
62
|
-
if a_right.size:
|
|
63
|
-
right_bound = np.min(np.outer(u1, a_right) - b_right, 1)
|
|
64
|
-
else:
|
|
65
|
-
right_bound = np.ones(m)
|
|
66
|
-
elif 1 <= k < n - 1:
|
|
67
|
-
a_left = a_mat[k, :k]
|
|
68
|
-
b_left = b_mat[k, :k]
|
|
69
|
-
if a_left.size:
|
|
70
|
-
left_bound = np.max(np.outer(u1, a_left) - b_left, 1)
|
|
71
|
-
else:
|
|
72
|
-
left_bound = np.zeros(m)
|
|
73
|
-
a_right = a_mat[k, (k + 1) :]
|
|
74
|
-
b_right = b_mat[k, (k + 1) :]
|
|
75
|
-
if a_right.size:
|
|
76
|
-
right_bound = np.min(np.outer(u1, a_right) - b_right, 1)
|
|
77
|
-
else:
|
|
78
|
-
right_bound = np.ones(m)
|
|
79
|
-
elif k == n - 1:
|
|
80
|
-
a_left = a_mat[-1, :-1]
|
|
81
|
-
b_left = b_mat[-1, :-1]
|
|
82
|
-
if a_left.size:
|
|
83
|
-
left_bound = np.max(np.outer(u1, a_left) - b_left, 1)
|
|
84
|
-
else:
|
|
85
|
-
left_bound = np.zeros(m)
|
|
86
|
-
right_bound = np.ones(m)
|
|
87
|
-
else:
|
|
88
|
-
bs_error_abort(f"{k=} is not compatible with {n=}")
|
|
89
|
-
left_bound = np.clip(left_bound, 0.0, 1.0)
|
|
90
|
-
right_bound = np.clip(right_bound, 0.0, 1.0)
|
|
91
|
-
|
|
92
|
-
return left_bound, right_bound
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def bivariate_quantiles_v(y: np.ndarray, u: np.ndarray, v: np.ndarray) -> np.ndarray:
|
|
96
|
-
"""Evaluate vector quantiles for a given set of dual weights.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
y: Observations with shape ``(n, 2)``.
|
|
100
|
-
u: Evaluation points in ``[0, 1]^2`` (shape ``(m, 2)``).
|
|
101
|
-
v: Dual weights solving the optimal transport problem (length ``n``).
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
Array of quantile locations with shape ``(m, 2)``.
|
|
105
|
-
"""
|
|
106
|
-
u = np.atleast_2d(u)
|
|
107
|
-
if u.shape[1] != 2:
|
|
108
|
-
bs_error_abort("u must have two columns")
|
|
109
|
-
m = u.shape[0]
|
|
110
|
-
q = np.empty((m, 2))
|
|
111
|
-
block = max(1, min(m, 5_000))
|
|
112
|
-
for start in range(0, m, block):
|
|
113
|
-
stop = min(start + block, m)
|
|
114
|
-
chunk = u[start:stop]
|
|
115
|
-
net_val = chunk @ y.T - v
|
|
116
|
-
k_max = np.argmax(net_val, axis=1)
|
|
117
|
-
q[start:stop] = y[k_max]
|
|
118
|
-
return cast(np.ndarray, q)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def bivariate_ranks_v(
|
|
122
|
-
y: np.ndarray, v: np.ndarray, n_nodes: int = 32, presorted: bool = False
|
|
123
|
-
) -> np.ndarray:
|
|
124
|
-
"""Compute the barycentric ranks of each observation given optimal weights.
|
|
125
|
-
|
|
126
|
-
Args:
|
|
127
|
-
y: Observations with shape ``(n, 2)``.
|
|
128
|
-
v: Dual weights returned by ``solve_for_v_``.
|
|
129
|
-
n_nodes: Number of Chebyshev nodes used in the quadrature.
|
|
130
|
-
presorted: Set to ``True`` when ``y``/``v`` are pre-sorted by the
|
|
131
|
-
second coordinate.
|
|
132
|
-
|
|
133
|
-
Returns:
|
|
134
|
-
Array of average ranks (shape ``(n, 2)``) with ``nan`` for zero-mass cells.
|
|
135
|
-
"""
|
|
136
|
-
n, d = y.shape
|
|
137
|
-
|
|
138
|
-
if d != 2:
|
|
139
|
-
bs_error_abort(f"only works for 2-dimensional y, not for {d}")
|
|
140
|
-
|
|
141
|
-
interval01 = Interval(0.0, 1.0)
|
|
142
|
-
u1_nodes, u1_weights = cheb_get_nodes_1d(interval01, n_nodes)
|
|
143
|
-
|
|
144
|
-
if presorted:
|
|
145
|
-
sort_order = np.arange(n)
|
|
146
|
-
y_sorted = y
|
|
147
|
-
v_sorted = v
|
|
148
|
-
else:
|
|
149
|
-
sort_order = np.argsort(y[:, 1])
|
|
150
|
-
y_sorted = y[sort_order, :]
|
|
151
|
-
v_sorted = v[sort_order]
|
|
152
|
-
|
|
153
|
-
a_mat, b_mat = _compute_ab(y_sorted, v_sorted)
|
|
154
|
-
|
|
155
|
-
average_ranks = np.zeros((n, 2))
|
|
156
|
-
|
|
157
|
-
for k in range(n):
|
|
158
|
-
left_bounds, right_bounds = _compute_u2_bounds(k, u1_nodes, a_mat, b_mat)
|
|
159
|
-
pos_diffs = np.maximum(right_bounds - left_bounds, 0.0)
|
|
160
|
-
pos_diffs_sq = np.maximum(
|
|
161
|
-
right_bounds * right_bounds - left_bounds * left_bounds, 0.0
|
|
162
|
-
)
|
|
163
|
-
prob_k = pos_diffs @ u1_weights
|
|
164
|
-
if prob_k <= 1e-12:
|
|
165
|
-
average_ranks[sort_order[k], :] = np.array([np.nan, np.nan])
|
|
166
|
-
continue
|
|
167
|
-
average_ranks[sort_order[k], 0] = ((u1_nodes * pos_diffs) @ u1_weights) / prob_k
|
|
168
|
-
average_ranks[sort_order[k], 1] = ((pos_diffs_sq @ u1_weights) / 2.0) / prob_k
|
|
169
|
-
|
|
170
|
-
return average_ranks
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def _objgrad(
|
|
174
|
-
v_sorted: np.ndarray, args: list, gr: bool = False
|
|
175
|
-
) -> float | tuple[float, np.ndarray]:
|
|
176
|
-
"""computes the expectation of $\\psi(U, v)$ and perhaps its gradient wrt `v`
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
v_sorted: an `n`-vector of weights, sorted by increasing `y[:, 1]`
|
|
180
|
-
args: a list of other arguments `[y_sorted, u1_nodes, u1_weights, verbose]`
|
|
181
|
-
gr: if `True`, we also evaluate the gradient
|
|
182
|
-
|
|
183
|
-
Returns:
|
|
184
|
-
the value of the expectation and perhaps its gradient
|
|
185
|
-
"""
|
|
186
|
-
y_sorted = args[0]
|
|
187
|
-
n = y_sorted.shape[0]
|
|
188
|
-
u1_nodes = args[1]
|
|
189
|
-
u1_weights = args[2]
|
|
190
|
-
vs1 = np.append(v_sorted, -np.sum(v_sorted))
|
|
191
|
-
a_mat, b_mat = _compute_ab(y_sorted, vs1)
|
|
192
|
-
|
|
193
|
-
obj_val = 0.0
|
|
194
|
-
probs = np.zeros(n)
|
|
195
|
-
for k in range(n):
|
|
196
|
-
left_bounds, right_bounds = _compute_u2_bounds(k, u1_nodes, a_mat, b_mat)
|
|
197
|
-
pos_diffs = np.maximum(right_bounds - left_bounds, 0.0)
|
|
198
|
-
pos_diffs_sq = np.maximum(
|
|
199
|
-
right_bounds * right_bounds - left_bounds * left_bounds, 0.0
|
|
200
|
-
)
|
|
201
|
-
obj_val += (
|
|
202
|
-
y_sorted[k, 0] * ((u1_nodes * pos_diffs) @ u1_weights)
|
|
203
|
-
+ y_sorted[k, 1] * (pos_diffs_sq @ u1_weights) / 2.0
|
|
204
|
-
)
|
|
205
|
-
probs[k] = pos_diffs @ u1_weights
|
|
206
|
-
obj_val -= vs1[k] * probs[k]
|
|
207
|
-
|
|
208
|
-
if gr:
|
|
209
|
-
grad_val = probs[-1] - probs[:-1]
|
|
210
|
-
return obj_val, grad_val
|
|
211
|
-
else:
|
|
212
|
-
return cast(float, obj_val)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def _obj(v_sorted: np.ndarray, args: list):
|
|
216
|
-
return _objgrad(v_sorted, args)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _grad(v_sorted: np.ndarray, args: list):
|
|
220
|
-
res_objg = cast(tuple[float, np.ndarray], _objgrad(v_sorted, args, gr=True))
|
|
221
|
-
grad_val = res_objg[1]
|
|
222
|
-
verbose = args[3]
|
|
223
|
-
if verbose:
|
|
224
|
-
print(f"The error on the gradient is {npmaxabs(grad_val)}")
|
|
225
|
-
return grad_val
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def solve_for_v_(y: np.ndarray, n_nodes: int = 32, verbose: bool = False) -> np.ndarray:
|
|
229
|
-
"""Solve the dual optimisation to obtain the optimal weights ``v``.
|
|
230
|
-
|
|
231
|
-
Args:
|
|
232
|
-
y: Observations with shape ``(n, 2)``.
|
|
233
|
-
n_nodes: Number of Chebyshev nodes for the quadrature.
|
|
234
|
-
verbose: Print optimisation diagnostics when ``True``.
|
|
235
|
-
|
|
236
|
-
Returns:
|
|
237
|
-
Array of length ``n`` containing the optimal weights (including the
|
|
238
|
-
residual term).
|
|
239
|
-
"""
|
|
240
|
-
n, d = y.shape
|
|
241
|
-
|
|
242
|
-
if d != 2:
|
|
243
|
-
bs_error_abort(f"only works for 2-dimensional y, not for {d}")
|
|
244
|
-
|
|
245
|
-
# sort by increasing y[:, 1]
|
|
246
|
-
sort_order = np.argsort(y[:, 1])
|
|
247
|
-
y_sorted = y[sort_order, :]
|
|
248
|
-
|
|
249
|
-
v0 = np.mean(y_sorted[:-1, :], 1)
|
|
250
|
-
|
|
251
|
-
interval01 = Interval(0.0, 1.0)
|
|
252
|
-
u1_nodes, u1_weights = cheb_get_nodes_1d(interval01, n_nodes)
|
|
253
|
-
|
|
254
|
-
argsog = [y_sorted, u1_nodes, u1_weights, verbose]
|
|
255
|
-
|
|
256
|
-
res = minimize_free(_obj, _grad, v0, args=argsog)
|
|
257
|
-
if verbose:
|
|
258
|
-
print_optimization_results(res, "Minimizing over v")
|
|
259
|
-
|
|
260
|
-
if not res.success:
|
|
261
|
-
bs_error_abort("Problem! the optimization failed.")
|
|
262
|
-
vstar = res.x
|
|
263
|
-
if verbose:
|
|
264
|
-
print(f"The final gradient over v is close to 0: error {npmaxabs(res.jac)}")
|
|
265
|
-
vstar1_sorted = np.append(vstar, -np.sum(vstar))
|
|
266
|
-
|
|
267
|
-
# revert to original order
|
|
268
|
-
vstar1 = np.zeros_like(vstar1_sorted)
|
|
269
|
-
vstar1[sort_order] = vstar1_sorted
|
|
270
|
-
|
|
271
|
-
return vstar1
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def bivariate_quantiles(
|
|
275
|
-
y: np.ndarray, u: np.ndarray, n_nodes: int = 32, verbose: bool = False
|
|
276
|
-
) -> np.ndarray:
|
|
277
|
-
"""Solve for the dual weights then evaluate bivariate quantiles.
|
|
278
|
-
|
|
279
|
-
Args:
|
|
280
|
-
y: Observations, shape ``(n, 2)``.
|
|
281
|
-
u: Query points in ``[0, 1]^2`` (shape ``(m, 2)``).
|
|
282
|
-
n_nodes: Number of Chebyshev nodes for the quadrature.
|
|
283
|
-
verbose: Print optimisation diagnostics when ``True``.
|
|
284
|
-
|
|
285
|
-
Returns:
|
|
286
|
-
Bivariate quantiles at ``u``.
|
|
287
|
-
"""
|
|
288
|
-
v = solve_for_v_(y, n_nodes, verbose)
|
|
289
|
-
return bivariate_quantiles_v(y, u, v)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def bivariate_ranks(
|
|
293
|
-
y: np.ndarray, n_nodes: int = 32, verbose: bool = False
|
|
294
|
-
) -> np.ndarray:
|
|
295
|
-
"""Compute ranks by first solving for the optimal weights ``v``.
|
|
296
|
-
|
|
297
|
-
Args:
|
|
298
|
-
y: Observations, shape ``(n, 2)``.
|
|
299
|
-
n_nodes: Number of Chebyshev nodes for the quadrature.
|
|
300
|
-
verbose: Print optimisation diagnostics when ``True``.
|
|
301
|
-
|
|
302
|
-
Returns:
|
|
303
|
-
Average ranks with shape ``(n, 2)``.
|
|
304
|
-
"""
|
|
305
|
-
v = solve_for_v_(y, n_nodes, verbose)
|
|
306
|
-
return bivariate_ranks_v(y, v, n_nodes)
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import pytest
|
|
3
|
-
|
|
4
|
-
from bs_python_utils.bivariate_quantiles import (
|
|
5
|
-
_compute_ab,
|
|
6
|
-
_compute_u2_bounds,
|
|
7
|
-
bivariate_quantiles,
|
|
8
|
-
bivariate_quantiles_v,
|
|
9
|
-
bivariate_ranks,
|
|
10
|
-
bivariate_ranks_v,
|
|
11
|
-
solve_for_v_,
|
|
12
|
-
)
|
|
13
|
-
from bs_python_utils.bsnputils import bsgrid, ecdf, npmaxabs
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def test_bivariate_quantiles_recompute_matches():
|
|
17
|
-
rng = np.random.default_rng(123)
|
|
18
|
-
y = rng.normal(size=(64, 2))
|
|
19
|
-
nodes = 16
|
|
20
|
-
|
|
21
|
-
weights = solve_for_v_(y, n_nodes=nodes, verbose=False)
|
|
22
|
-
|
|
23
|
-
q = np.linspace(0.0, 1.0, 5)
|
|
24
|
-
u_points = bsgrid(q, q)
|
|
25
|
-
|
|
26
|
-
direct = bivariate_quantiles_v(y, u_points, weights)
|
|
27
|
-
via_solver = bivariate_quantiles(y, u_points, n_nodes=nodes, verbose=False)
|
|
28
|
-
|
|
29
|
-
ranks_direct = bivariate_ranks_v(y, weights, n_nodes=nodes)
|
|
30
|
-
ranks_via_solver = bivariate_ranks(y, n_nodes=nodes, verbose=False)
|
|
31
|
-
|
|
32
|
-
assert np.allclose(direct, via_solver)
|
|
33
|
-
assert np.allclose(ranks_direct, ranks_via_solver)
|
|
34
|
-
assert (
|
|
35
|
-
npmaxabs(ranks_direct - np.column_stack((ecdf(y[:, 0]), ecdf(y[:, 1])))) < 0.2
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_chunked_quantiles_matches_numpy_argmax():
|
|
40
|
-
rng = np.random.default_rng(7)
|
|
41
|
-
y = rng.normal(size=(10, 2))
|
|
42
|
-
v = rng.normal(size=10)
|
|
43
|
-
u = rng.uniform(0.0, 1.0, size=(6000, 2))
|
|
44
|
-
|
|
45
|
-
chunked = bivariate_quantiles_v(y, u, v)
|
|
46
|
-
expected = y[np.argmax(u @ y.T - v, axis=1)]
|
|
47
|
-
|
|
48
|
-
assert np.allclose(chunked, expected)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def test_compute_ab_handles_ties():
|
|
52
|
-
y = np.array([[0.0, 1.0], [1.0, 1.0]])
|
|
53
|
-
v = np.array([0.2, -0.2])
|
|
54
|
-
a_mat, b_mat = _compute_ab(y, v)
|
|
55
|
-
|
|
56
|
-
assert np.isfinite(a_mat).all()
|
|
57
|
-
assert np.isfinite(b_mat).all()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@pytest.mark.parametrize("k", [0, 1])
|
|
61
|
-
def test_compute_u2_bounds_degenerate(k):
|
|
62
|
-
y = np.array([[0.0, 0.0], [1.0, 1.0]])
|
|
63
|
-
v = np.array([0.1, -0.1])
|
|
64
|
-
a_mat, b_mat = _compute_ab(y, v)
|
|
65
|
-
nodes = np.linspace(0.0, 1.0, 5)
|
|
66
|
-
|
|
67
|
-
left, right = _compute_u2_bounds(k, nodes, a_mat, b_mat)
|
|
68
|
-
|
|
69
|
-
assert np.isfinite(left).all()
|
|
70
|
-
assert np.isfinite(right).all()
|
|
71
|
-
assert (right >= left).all()
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_bivariate_ranks_zero_mass_returns_nan():
|
|
75
|
-
y = np.array([[0.0, 0.0], [1.0, 1.0]])
|
|
76
|
-
v = np.array([0.1, -0.1])
|
|
77
|
-
ranks = bivariate_ranks_v(y, v, n_nodes=8, presorted=True)
|
|
78
|
-
|
|
79
|
-
assert np.isnan(ranks[0]).all()
|
|
80
|
-
assert np.isfinite(ranks[1]).all()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/examples/examples_seaborn.py
RENAMED
|
File without changes
|
{bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/examples/examples_sklearn.py
RENAMED
|
File without changes
|
{bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils/examples/examples_sparse_gaussian.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bs_python_utils-0.8.5 → bs_python_utils-0.9.2}/bs_python_utils.egg-info/dependency_links.txt
RENAMED
|
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
|