bs-python-utils 0.9.2__py3-none-any.whl → 1.0__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 +1 -81
- bs_python_utils/bivariate_quantiles.py +1 -238
- bs_python_utils/bs_altair.py +1 -1111
- bs_python_utils/bs_logging.py +1 -168
- bs_python_utils/bs_mathstr.py +1 -121
- bs_python_utils/bs_mem.py +1 -157
- bs_python_utils/bs_opt.py +1 -576
- bs_python_utils/bs_plots.py +1 -62
- bs_python_utils/bs_seaborn.py +1 -210
- bs_python_utils/bs_sparse_gaussian.py +1 -74
- bs_python_utils/bsmplutils.py +1 -95
- bs_python_utils/bsnputils.py +1 -1221
- bs_python_utils/bssputils.py +1 -82
- bs_python_utils/bsstats.py +1 -498
- bs_python_utils/bsutils.py +1 -410
- bs_python_utils/chebyshev.py +1 -550
- bs_python_utils/core/Timer.py +81 -0
- bs_python_utils/core/__init__.py +1 -0
- bs_python_utils/core/bs_logging.py +168 -0
- bs_python_utils/core/bs_mathstr.py +121 -0
- bs_python_utils/core/bs_mem.py +157 -0
- bs_python_utils/core/bsutils.py +410 -0
- bs_python_utils/data_anal/__init__.py +1 -0
- bs_python_utils/data_anal/pandas_utils.py +245 -0
- bs_python_utils/data_anal/sklearn_utils.py +123 -0
- bs_python_utils/distance_covariances.py +1 -263
- bs_python_utils/examples/examples_altair.py +1 -1
- bs_python_utils/examples/examples_distance_covariances.py +5 -1
- bs_python_utils/examples/examples_mem.py +1 -1
- bs_python_utils/examples/examples_mpl.py +1 -1
- bs_python_utils/examples/examples_opt.py +3 -3
- bs_python_utils/examples/examples_seaborn.py +5 -1
- bs_python_utils/examples/examples_sklearn.py +1 -1
- bs_python_utils/examples/examples_sparse_gaussian.py +2 -1
- bs_python_utils/numerical/__init__.py +1 -0
- bs_python_utils/numerical/bs_sparse_gaussian.py +74 -0
- bs_python_utils/numerical/bsnputils.py +1221 -0
- bs_python_utils/numerical/bssputils.py +82 -0
- bs_python_utils/numerical/chebyshev.py +550 -0
- bs_python_utils/opt/__init__.py +1 -0
- bs_python_utils/opt/bs_opt.py +576 -0
- bs_python_utils/pandas_utils.py +1 -245
- bs_python_utils/sklearn_utils.py +1 -123
- bs_python_utils/stats/__init__.py +1 -0
- bs_python_utils/stats/bivariate_quantiles.py +276 -0
- bs_python_utils/stats/bsstats.py +502 -0
- bs_python_utils/stats/distance_covariances.py +263 -0
- bs_python_utils/streamlit_utils.py +1 -100
- bs_python_utils/viz/__init__.py +1 -0
- bs_python_utils/viz/bs_altair.py +1111 -0
- bs_python_utils/viz/bs_plots.py +62 -0
- bs_python_utils/viz/bs_seaborn.py +210 -0
- bs_python_utils/viz/bsmplutils.py +95 -0
- bs_python_utils/viz/streamlit_utils.py +100 -0
- {bs_python_utils-0.9.2.dist-info → bs_python_utils-1.0.dist-info}/METADATA +12 -7
- bs_python_utils-1.0.dist-info/RECORD +75 -0
- bs_python_utils-0.9.2.dist-info/RECORD +0 -49
- {bs_python_utils-0.9.2.dist-info → bs_python_utils-1.0.dist-info}/WHEEL +0 -0
- {bs_python_utils-0.9.2.dist-info → bs_python_utils-1.0.dist-info}/licenses/LICENSE +0 -0
- {bs_python_utils-0.9.2.dist-info → bs_python_utils-1.0.dist-info}/top_level.txt +0 -0
bs_python_utils/Timer.py
CHANGED
|
@@ -1,81 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Utilities to time code:
|
|
3
|
-
|
|
4
|
-
* a `Timer` class that can be used as a context manager
|
|
5
|
-
* a `timeit` decorator for functions.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import time
|
|
9
|
-
from functools import wraps
|
|
10
|
-
from typing import Any, Callable, Iterable
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def timeit(func: Callable) -> Callable:
|
|
14
|
-
"""
|
|
15
|
-
Decorator to time a function
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
@wraps(func)
|
|
19
|
-
def wrapper(*args: Iterable, **kwargs: dict) -> Any:
|
|
20
|
-
start = time.perf_counter()
|
|
21
|
-
result = func(*args, **kwargs)
|
|
22
|
-
end = time.perf_counter()
|
|
23
|
-
print(f"{func.__name__} executed in {end - start:.3f} seconds")
|
|
24
|
-
return result
|
|
25
|
-
|
|
26
|
-
return wrapper
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class Timer:
|
|
30
|
-
"""
|
|
31
|
-
A timer that can be started, stopped, and reset as needed by the user.
|
|
32
|
-
It keeps track of the total elapsed time in the `elapsed` attribute::
|
|
33
|
-
|
|
34
|
-
Examples:
|
|
35
|
-
>>> with Timer() as t:
|
|
36
|
-
>>> ....
|
|
37
|
-
>>> print(f"... took {t.elapsed} seconds")
|
|
38
|
-
|
|
39
|
-
use `Timer(time.process_time)` to get only CPU time.
|
|
40
|
-
|
|
41
|
-
can also do:
|
|
42
|
-
|
|
43
|
-
Examples:
|
|
44
|
-
>>> t = Timer()
|
|
45
|
-
>>> t.start()
|
|
46
|
-
>>> t.stop()
|
|
47
|
-
>>> t.start() # will add to the same counter
|
|
48
|
-
>>> t.stop()
|
|
49
|
-
>>> print(f"{t.elapsed} seconds total")
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
def __init__(self, func: Callable = time.perf_counter) -> None:
|
|
53
|
-
self.elapsed = 0.0
|
|
54
|
-
self._func = func
|
|
55
|
-
self._start = None
|
|
56
|
-
|
|
57
|
-
def start(self) -> None:
|
|
58
|
-
if self._start is not None:
|
|
59
|
-
raise RuntimeError("Already started")
|
|
60
|
-
self._start = self._func()
|
|
61
|
-
|
|
62
|
-
def stop(self) -> None:
|
|
63
|
-
if self._start is None:
|
|
64
|
-
raise RuntimeError("Not started")
|
|
65
|
-
end = self._func()
|
|
66
|
-
self.elapsed += end - self._start
|
|
67
|
-
self._start = None
|
|
68
|
-
|
|
69
|
-
def reset(self) -> None:
|
|
70
|
-
self.elapsed = 0.0
|
|
71
|
-
|
|
72
|
-
@property
|
|
73
|
-
def running(self) -> bool:
|
|
74
|
-
return self._start is not None
|
|
75
|
-
|
|
76
|
-
def __enter__(self) -> Any:
|
|
77
|
-
self.start()
|
|
78
|
-
return self
|
|
79
|
-
|
|
80
|
-
def __exit__(self, *args: Iterable) -> None:
|
|
81
|
-
self.stop()
|
|
1
|
+
from bs_python_utils.core.Timer import * # noqa: F401, F403
|
|
@@ -1,238 +1 @@
|
|
|
1
|
-
|
|
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
|
+
from bs_python_utils.stats.bivariate_quantiles import * # noqa: F401, F403
|