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.
Files changed (60) hide show
  1. bs_python_utils/Timer.py +1 -81
  2. bs_python_utils/bivariate_quantiles.py +1 -238
  3. bs_python_utils/bs_altair.py +1 -1111
  4. bs_python_utils/bs_logging.py +1 -168
  5. bs_python_utils/bs_mathstr.py +1 -121
  6. bs_python_utils/bs_mem.py +1 -157
  7. bs_python_utils/bs_opt.py +1 -576
  8. bs_python_utils/bs_plots.py +1 -62
  9. bs_python_utils/bs_seaborn.py +1 -210
  10. bs_python_utils/bs_sparse_gaussian.py +1 -74
  11. bs_python_utils/bsmplutils.py +1 -95
  12. bs_python_utils/bsnputils.py +1 -1221
  13. bs_python_utils/bssputils.py +1 -82
  14. bs_python_utils/bsstats.py +1 -498
  15. bs_python_utils/bsutils.py +1 -410
  16. bs_python_utils/chebyshev.py +1 -550
  17. bs_python_utils/core/Timer.py +81 -0
  18. bs_python_utils/core/__init__.py +1 -0
  19. bs_python_utils/core/bs_logging.py +168 -0
  20. bs_python_utils/core/bs_mathstr.py +121 -0
  21. bs_python_utils/core/bs_mem.py +157 -0
  22. bs_python_utils/core/bsutils.py +410 -0
  23. bs_python_utils/data_anal/__init__.py +1 -0
  24. bs_python_utils/data_anal/pandas_utils.py +245 -0
  25. bs_python_utils/data_anal/sklearn_utils.py +123 -0
  26. bs_python_utils/distance_covariances.py +1 -263
  27. bs_python_utils/examples/examples_altair.py +1 -1
  28. bs_python_utils/examples/examples_distance_covariances.py +5 -1
  29. bs_python_utils/examples/examples_mem.py +1 -1
  30. bs_python_utils/examples/examples_mpl.py +1 -1
  31. bs_python_utils/examples/examples_opt.py +3 -3
  32. bs_python_utils/examples/examples_seaborn.py +5 -1
  33. bs_python_utils/examples/examples_sklearn.py +1 -1
  34. bs_python_utils/examples/examples_sparse_gaussian.py +2 -1
  35. bs_python_utils/numerical/__init__.py +1 -0
  36. bs_python_utils/numerical/bs_sparse_gaussian.py +74 -0
  37. bs_python_utils/numerical/bsnputils.py +1221 -0
  38. bs_python_utils/numerical/bssputils.py +82 -0
  39. bs_python_utils/numerical/chebyshev.py +550 -0
  40. bs_python_utils/opt/__init__.py +1 -0
  41. bs_python_utils/opt/bs_opt.py +576 -0
  42. bs_python_utils/pandas_utils.py +1 -245
  43. bs_python_utils/sklearn_utils.py +1 -123
  44. bs_python_utils/stats/__init__.py +1 -0
  45. bs_python_utils/stats/bivariate_quantiles.py +276 -0
  46. bs_python_utils/stats/bsstats.py +502 -0
  47. bs_python_utils/stats/distance_covariances.py +263 -0
  48. bs_python_utils/streamlit_utils.py +1 -100
  49. bs_python_utils/viz/__init__.py +1 -0
  50. bs_python_utils/viz/bs_altair.py +1111 -0
  51. bs_python_utils/viz/bs_plots.py +62 -0
  52. bs_python_utils/viz/bs_seaborn.py +210 -0
  53. bs_python_utils/viz/bsmplutils.py +95 -0
  54. bs_python_utils/viz/streamlit_utils.py +100 -0
  55. {bs_python_utils-0.9.2.dist-info → bs_python_utils-1.0.dist-info}/METADATA +12 -7
  56. bs_python_utils-1.0.dist-info/RECORD +75 -0
  57. bs_python_utils-0.9.2.dist-info/RECORD +0 -49
  58. {bs_python_utils-0.9.2.dist-info → bs_python_utils-1.0.dist-info}/WHEEL +0 -0
  59. {bs_python_utils-0.9.2.dist-info → bs_python_utils-1.0.dist-info}/licenses/LICENSE +0 -0
  60. {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
- """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
+ from bs_python_utils.stats.bivariate_quantiles import * # noqa: F401, F403