bs-python-utils 0.5.3__tar.gz → 0.8.0__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.
Files changed (54) hide show
  1. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/PKG-INFO +65 -30
  2. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/README.md +45 -8
  3. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/__init__.py +1 -1
  4. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/bivariate_quantiles.py +73 -57
  5. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/bs_altair.py +248 -92
  6. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/bs_logging.py +3 -2
  7. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/bs_opt.py +124 -78
  8. bs_python_utils-0.8.0/bs_python_utils/bs_plots.py +62 -0
  9. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/bs_seaborn.py +61 -29
  10. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/bs_sparse_gaussian.py +3 -2
  11. bs_python_utils-0.8.0/bs_python_utils/bsmplutils.py +94 -0
  12. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/bsnputils.py +175 -86
  13. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/bssputils.py +15 -15
  14. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/bsstats.py +47 -30
  15. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/bsutils.py +49 -29
  16. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/chebyshev.py +66 -53
  17. bs_python_utils-0.8.0/bs_python_utils/examples/examples_altair.py +225 -0
  18. {bs_python_utils-0.5.3/bs_python_utils → bs_python_utils-0.8.0/bs_python_utils/examples}/examples_mem.py +2 -1
  19. bs_python_utils-0.8.0/bs_python_utils/examples/examples_mpl.py +13 -0
  20. bs_python_utils-0.5.3/bs_python_utils/example_opt.py → bs_python_utils-0.8.0/bs_python_utils/examples/examples_opt.py +3 -2
  21. {bs_python_utils-0.5.3/bs_python_utils → bs_python_utils-0.8.0/bs_python_utils/examples}/examples_seaborn.py +14 -10
  22. {bs_python_utils-0.5.3/bs_python_utils → bs_python_utils-0.8.0/bs_python_utils/examples}/examples_sklearn.py +2 -1
  23. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/pandas_utils.py +4 -4
  24. bs_python_utils-0.8.0/bs_python_utils/sklearn_utils.py +123 -0
  25. bs_python_utils-0.8.0/bs_python_utils/streamlit_utils.py +100 -0
  26. bs_python_utils-0.8.0/bs_python_utils.egg-info/PKG-INFO +118 -0
  27. bs_python_utils-0.8.0/bs_python_utils.egg-info/SOURCES.txt +46 -0
  28. bs_python_utils-0.8.0/bs_python_utils.egg-info/dependency_links.txt +1 -0
  29. bs_python_utils-0.8.0/bs_python_utils.egg-info/requires.txt +17 -0
  30. bs_python_utils-0.8.0/bs_python_utils.egg-info/top_level.txt +1 -0
  31. bs_python_utils-0.8.0/pyproject.toml +31 -0
  32. bs_python_utils-0.8.0/setup.cfg +4 -0
  33. bs_python_utils-0.8.0/tests/test_bivariate_quantiles.py +80 -0
  34. bs_python_utils-0.8.0/tests/test_bs_opt.py +81 -0
  35. bs_python_utils-0.8.0/tests/test_bs_sparse_gaussian.py +17 -0
  36. bs_python_utils-0.8.0/tests/test_bsnputils.py +338 -0
  37. bs_python_utils-0.8.0/tests/test_bssputils.py +29 -0
  38. bs_python_utils-0.8.0/tests/test_bsstats.py +56 -0
  39. bs_python_utils-0.8.0/tests/test_bsutils.py +169 -0
  40. bs_python_utils-0.8.0/tests/test_chebyshev.py +256 -0
  41. bs_python_utils-0.8.0/tests/test_nptri.py +51 -0
  42. bs_python_utils-0.8.0/tests/test_random_draws.py +32 -0
  43. bs_python_utils-0.8.0/tests/test_sklearn_utils.py +52 -0
  44. bs_python_utils-0.5.3/LICENSE +0 -21
  45. bs_python_utils-0.5.3/bs_python_utils/bs_plots.py +0 -3
  46. bs_python_utils-0.5.3/bs_python_utils/bsmplutils.py +0 -36
  47. bs_python_utils-0.5.3/bs_python_utils/examples_altair.py +0 -182
  48. bs_python_utils-0.5.3/bs_python_utils/sklearn_utils.py +0 -76
  49. bs_python_utils-0.5.3/pyproject.toml +0 -103
  50. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/Timer.py +0 -0
  51. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/bs_mathstr.py +0 -0
  52. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/bs_mem.py +0 -0
  53. {bs_python_utils-0.5.3 → bs_python_utils-0.8.0}/bs_python_utils/distance_covariances.py +0 -0
  54. {bs_python_utils-0.5.3/bs_python_utils → bs_python_utils-0.8.0/bs_python_utils/examples}/examples_distance_covariances.py +0 -0
@@ -1,28 +1,26 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: bs-python-utils
3
- Version: 0.5.3
4
- Summary: my Python utilities
5
- Home-page: https://github.com/bsalanie/bs-python-utils
6
- Author: Bernard Salanie
7
- Author-email: bsalanie@columbia.edu
8
- Requires-Python: >=3.8,<4.0
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.8
11
- Classifier: Programming Language :: Python :: 3.9
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
- Requires-Dist: altair (>=4.2.2,<5.0.0)
15
- Requires-Dist: altair-saver (>=0.5.0,<0.6.0)
16
- Requires-Dist: emcee (>=3.1.4,<4.0.0)
17
- Requires-Dist: griffe (>=0.32.3,<0.33.0)
18
- Requires-Dist: matplotlib (>=3.7.1,<4.0.0)
19
- Requires-Dist: scikit-learn (>=1.2.2,<2.0.0)
20
- Requires-Dist: seaborn (>=0.12.2,<0.13.0)
21
- Requires-Dist: statsmodels (>=0.13.5,<0.14.0)
22
- Requires-Dist: vega-datasets (>=0.9.0,<0.10.0)
23
- Project-URL: Documentation, https://bsalanie.github.io/bs-python-utils/
24
- Project-URL: Repository, https://github.com/bsalanie/bs-python-utils
3
+ Version: 0.8.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.12
25
6
  Description-Content-Type: text/markdown
7
+ Requires-Dist: altair>=5.5.0
8
+ Requires-Dist: altair-saver>=0.5.0
9
+ Requires-Dist: emcee>=3.1.6
10
+ Requires-Dist: matplotlib>=3.10.6
11
+ Requires-Dist: mkdocs>=1.6.1
12
+ Requires-Dist: mkdocs-material>=9.6.20
13
+ Requires-Dist: mkdocstrings-python>=1.18.2
14
+ Requires-Dist: numpy>=2.3.3
15
+ Requires-Dist: pandas>=2.3.2
16
+ Requires-Dist: pre-commit>=4.3.0
17
+ Requires-Dist: pytest>=8.4.2
18
+ Requires-Dist: scikit-learn>=1.7.2
19
+ Requires-Dist: scipy>=1.16.2
20
+ Requires-Dist: seaborn>=0.13.2
21
+ Requires-Dist: statsmodels>=0.14.5
22
+ Requires-Dist: streamlit>=1.49.1
23
+ Requires-Dist: vega-datasets>=0.9.0
26
24
 
27
25
  # bs-python-utils
28
26
 
@@ -32,35 +30,72 @@ Description-Content-Type: text/markdown
32
30
 
33
31
  [![Build status](https://img.shields.io/github/actions/workflow/status/bsalanie/bs-python-utils/main.yml?branch=main)](https://github.com/bsalanie/bs-python-utils/actions/workflows/main.yml?query=branch%3Amain) <!-- [![codecov](https://codecov.io/gh/bsalanie/bs-python-utils/branch/main/graph/badge.svg)](https://codecov.io/gh/bsalanie/bs-python-utils) --> <!-- [![Commit activity](https://img.shields.io/github/commit-activity/m/bsalanie/bs-python-utils)](https://img.shields.io/github/commit-activity/m/bsalanie/bs-python-utils) --> [![License](https://img.shields.io/github/license/bsalanie/bs-python-utils)](https://img.shields.io/github/license/bsalanie/bs-python-utils)
34
32
 
35
- My Python utilities.
33
+ **My Python utilities**.
36
34
 
37
- - **Github repository**: <https://github.com/bsalanie/bs-python-utils/>
38
- - **Documentation** <https://bsalanie.github.io/bs-python-utils/>
35
+ - **Github repository**: <https://github.com/bsalanie/bs-python-utils/>
36
+ - **Documentation** <https://bsalanie.github.io/bs-python-utils/>
39
37
 
40
38
  ### Release notes
41
- #### 0.5.3 (August 19, 2023)
42
- Improved the documentation.
39
+
40
+ #### 0.8 (October 14, 2025)
41
+ Switched to `uv` for project management. Used Codex to clean up the code and generate more tests. Added the `examples` subdirectory.
42
+
43
+ #### 0.7 (June 18, 2024)
44
+ Added `make_lowertri` in `bsnputils`, and some utility code in `bs_plots`.
45
+
46
+ #### 0.6.4 (November 26, 2023)
47
+ Moved to Altair 5.0; made timing optional in `bs_opt`.
48
+
49
+ #### 0.6.3 (November 26, 2023)
50
+ Updated statsmodels dependency.
51
+
52
+ #### 0.6.1 and 0.6.2 (November 18, 2023)
53
+ Added a Matplotlib plot for discrete choice models, a boxplot for Altair, and a density plot for Seaborn.
54
+
55
+ #### 0.6 (November 9, 2023)
56
+
57
+ Added matrix heatmap plot in Altair and Streamlit utility code.
58
+
59
+ #### 0.5.5 (October 23, 2023)
60
+
61
+ Added `fstring***` in `bsutils`.
62
+
43
63
  #### 0.5.2 (August 16, 2023)
64
+
44
65
  Added two interpolation routines from values at the Chebyshev nodes in `chebyshev` module.
66
+
45
67
  #### 0.5.1 (August 14, 2023)
46
- Added two functions to draw random samples in `bsstats` module.
68
+
69
+ Added two functions to draw random samples in `bsstats` module.
70
+
47
71
  #### 0.5 (August 7, 2023)
72
+
48
73
  Added 1-dimensional root finding in `chebyshev` module.
74
+
49
75
  #### 0.4.2 (August 2, 2023)
76
+
50
77
  Updated the docs.
78
+
51
79
  #### 0.4.1 (July 23, 2023)
80
+
52
81
  Only print if verbose in bivariate quantiles.
82
+
53
83
  #### 0.4 (July 22, 2023)
84
+
54
85
  Added bivariate quantiles and ranks à la optimal transportation.
86
+
55
87
  #### 0.3 (July 21, 2023)
88
+
56
89
  Added in Numpy utils a function to set upper and lower triangle to a scalar;
57
90
  and `minimize_free` in `bs_opt`.
58
91
 
59
92
  #### 0.2 (July 17, 2023)
93
+
60
94
  Fixed printing in accelerated gradient descent.
61
95
 
62
96
  #### 0.1 (June 20, 2023)
63
- Added `grid_function` and the `chebyshev` module.
97
+
98
+ Added `grid_function` and the `chebyshev` module.
64
99
 
65
100
  #### 0.0.6 (May 9, 2023)
66
101
 
@@ -6,35 +6,72 @@
6
6
 
7
7
  [![Build status](https://img.shields.io/github/actions/workflow/status/bsalanie/bs-python-utils/main.yml?branch=main)](https://github.com/bsalanie/bs-python-utils/actions/workflows/main.yml?query=branch%3Amain) <!-- [![codecov](https://codecov.io/gh/bsalanie/bs-python-utils/branch/main/graph/badge.svg)](https://codecov.io/gh/bsalanie/bs-python-utils) --> <!-- [![Commit activity](https://img.shields.io/github/commit-activity/m/bsalanie/bs-python-utils)](https://img.shields.io/github/commit-activity/m/bsalanie/bs-python-utils) --> [![License](https://img.shields.io/github/license/bsalanie/bs-python-utils)](https://img.shields.io/github/license/bsalanie/bs-python-utils)
8
8
 
9
- My Python utilities.
9
+ **My Python utilities**.
10
10
 
11
- - **Github repository**: <https://github.com/bsalanie/bs-python-utils/>
12
- - **Documentation** <https://bsalanie.github.io/bs-python-utils/>
11
+ - **Github repository**: <https://github.com/bsalanie/bs-python-utils/>
12
+ - **Documentation** <https://bsalanie.github.io/bs-python-utils/>
13
13
 
14
14
  ### Release notes
15
- #### 0.5.3 (August 19, 2023)
16
- Improved the documentation.
15
+
16
+ #### 0.8 (October 14, 2025)
17
+ Switched to `uv` for project management. Used Codex to clean up the code and generate more tests. Added the `examples` subdirectory.
18
+
19
+ #### 0.7 (June 18, 2024)
20
+ Added `make_lowertri` in `bsnputils`, and some utility code in `bs_plots`.
21
+
22
+ #### 0.6.4 (November 26, 2023)
23
+ Moved to Altair 5.0; made timing optional in `bs_opt`.
24
+
25
+ #### 0.6.3 (November 26, 2023)
26
+ Updated statsmodels dependency.
27
+
28
+ #### 0.6.1 and 0.6.2 (November 18, 2023)
29
+ Added a Matplotlib plot for discrete choice models, a boxplot for Altair, and a density plot for Seaborn.
30
+
31
+ #### 0.6 (November 9, 2023)
32
+
33
+ Added matrix heatmap plot in Altair and Streamlit utility code.
34
+
35
+ #### 0.5.5 (October 23, 2023)
36
+
37
+ Added `fstring***` in `bsutils`.
38
+
17
39
  #### 0.5.2 (August 16, 2023)
40
+
18
41
  Added two interpolation routines from values at the Chebyshev nodes in `chebyshev` module.
42
+
19
43
  #### 0.5.1 (August 14, 2023)
20
- Added two functions to draw random samples in `bsstats` module.
44
+
45
+ Added two functions to draw random samples in `bsstats` module.
46
+
21
47
  #### 0.5 (August 7, 2023)
48
+
22
49
  Added 1-dimensional root finding in `chebyshev` module.
50
+
23
51
  #### 0.4.2 (August 2, 2023)
52
+
24
53
  Updated the docs.
54
+
25
55
  #### 0.4.1 (July 23, 2023)
56
+
26
57
  Only print if verbose in bivariate quantiles.
58
+
27
59
  #### 0.4 (July 22, 2023)
60
+
28
61
  Added bivariate quantiles and ranks à la optimal transportation.
62
+
29
63
  #### 0.3 (July 21, 2023)
64
+
30
65
  Added in Numpy utils a function to set upper and lower triangle to a scalar;
31
66
  and `minimize_free` in `bs_opt`.
32
67
 
33
68
  #### 0.2 (July 17, 2023)
69
+
34
70
  Fixed printing in accelerated gradient descent.
35
71
 
36
72
  #### 0.1 (June 20, 2023)
37
- Added `grid_function` and the `chebyshev` module.
73
+
74
+ Added `grid_function` and the `chebyshev` module.
38
75
 
39
76
  #### 0.0.6 (May 9, 2023)
40
77
 
@@ -54,4 +91,4 @@ Satisfied mypy.
54
91
 
55
92
  #### 0.0.2 (April 24, 2023)
56
93
 
57
- Fixed main PyPI page.
94
+ Fixed main PyPI page.
@@ -1,4 +1,4 @@
1
- """This package contains a number of functions that I have found useful in my programming.
1
+ """This package contains a number of functions that I have found useful in my programming.
2
2
 
3
3
  * `bsutils` has (inter alia) some I/O functions, error reporting, and $C^2$ extensions of log and exp
4
4
  * `bs_logging` has customized logging
@@ -17,6 +17,7 @@ Steps 1 and 2 can be combined: `qtiles_y = bivariate_quantiles(y, v, u1, u2, n_n
17
17
 
18
18
  Steps 1 and 3 can be combined: `ranks_y = bivariate_ranks(y, n_nodes)`
19
19
  """
20
+
20
21
  from typing import cast
21
22
 
22
23
  import numpy as np
@@ -28,62 +29,55 @@ from bs_python_utils.chebyshev import Interval, cheb_get_nodes_1d
28
29
 
29
30
 
30
31
  def _compute_ab(y_sorted: np.ndarray, v_sorted: np.ndarray) -> TwoArrays:
31
- """evaluates the A and B matrices
32
-
33
- Args:
34
- y_sorted: the observations, an `(n,2)` matrix sorted by increasing `y[:, 1]`
35
- v_sorted: the weights, an `n`-vector in the same sort order
36
-
37
- Returns:
38
- two `(n, n)` matrices
39
-
40
- Warning:
41
- the user must make sure that `y` and `v` have been sorted beforehand
42
- """
43
- n = v_sorted.size
32
+ """Build the `A` and `B` matrices used in the dual optimisation."""
44
33
  y1 = y_sorted[:, 0]
45
34
  dy1 = np.subtract.outer(y1, y1)
46
35
  y2 = y_sorted[:, 1]
47
36
  dy2 = np.subtract.outer(y2, y2)
48
- np.fill_diagonal(dy2, np.ones(n)) # to avoid division by 0
37
+ np.fill_diagonal(dy2, 1.0)
49
38
  dv = np.subtract.outer(v_sorted, v_sorted)
50
- a_mat = dy1.T / dy2
51
- b_mat = dv.T / dy2
39
+ with np.errstate(divide="ignore", invalid="ignore"):
40
+ a_mat = np.divide(dy1.T, dy2, where=np.abs(dy2) > 1e-12)
41
+ b_mat = np.divide(dv.T, dy2, where=np.abs(dy2) > 1e-12)
42
+ a_mat = np.nan_to_num(a_mat, nan=0.0, posinf=0.0, neginf=0.0)
43
+ b_mat = np.nan_to_num(b_mat, nan=0.0, posinf=0.0, neginf=0.0)
52
44
  return a_mat, b_mat
53
45
 
54
46
 
55
47
  def _compute_u2_bounds(
56
48
  k: int, u1: np.ndarray, a_mat: np.ndarray, b_mat: np.ndarray
57
49
  ) -> TwoArrays:
58
- """for given u1, calculates the bounds on u2 that make k the chosen observation
59
-
60
- Args:
61
- k: an integer between 0 and (n-1)
62
- u1: a vector of size `m`
63
- a_mat: the `A` matrix of size `(n,n)`
64
- b_mat: the `B` matrix of size `(n,n)`
65
-
66
- Returns:
67
- two vectors of size `m`, the left and right bounds in [0,1]
68
- """
50
+ """Return the admissible interval of ``u2`` that selects index ``k``."""
69
51
  n = a_mat.shape[0]
70
52
  m = u1.size
71
53
  if k == 0:
72
54
  left_bound = np.zeros(m)
73
55
  a_right = a_mat[0, 1:]
74
56
  b_right = b_mat[0, 1:]
75
- right_bound = np.min(np.outer(u1, a_right) - b_right, 1)
57
+ if a_right.size:
58
+ right_bound = np.min(np.outer(u1, a_right) - b_right, 1)
59
+ else:
60
+ right_bound = np.ones(m)
76
61
  elif 1 <= k < n - 1:
77
62
  a_left = a_mat[k, :k]
78
63
  b_left = b_mat[k, :k]
79
- left_bound = np.max(np.outer(u1, a_left) - b_left, 1)
64
+ if a_left.size:
65
+ left_bound = np.max(np.outer(u1, a_left) - b_left, 1)
66
+ else:
67
+ left_bound = np.zeros(m)
80
68
  a_right = a_mat[k, (k + 1) :]
81
69
  b_right = b_mat[k, (k + 1) :]
82
- right_bound = np.min(np.outer(u1, a_right) - b_right, 1)
70
+ if a_right.size:
71
+ right_bound = np.min(np.outer(u1, a_right) - b_right, 1)
72
+ else:
73
+ right_bound = np.ones(m)
83
74
  elif k == n - 1:
84
75
  a_left = a_mat[-1, :-1]
85
76
  b_left = b_mat[-1, :-1]
86
- left_bound = np.max(np.outer(u1, a_left) - b_left, 1)
77
+ if a_left.size:
78
+ left_bound = np.max(np.outer(u1, a_left) - b_left, 1)
79
+ else:
80
+ left_bound = np.zeros(m)
87
81
  right_bound = np.ones(m)
88
82
  else:
89
83
  bs_error_abort(f"{k=} is not compatible with {n=}")
@@ -94,34 +88,44 @@ def _compute_u2_bounds(
94
88
 
95
89
 
96
90
  def bivariate_quantiles_v(y: np.ndarray, u: np.ndarray, v: np.ndarray) -> np.ndarray:
97
- """computes the vector quantiles of `y` at values `u`, given the converged `v`
91
+ """Evaluate vector quantiles for a given set of dual weights.
98
92
 
99
93
  Args:
100
- y: the observations, an `(n,2)` matrix
101
- u: the values where we want the quantiles, an `(m,2)` matrix in $[0,1]$
102
- v: the converged values of the weights, an `n`-vector
94
+ y: Observations with shape ``(n, 2)``.
95
+ u: Evaluation points in ``[0, 1]^2`` (shape ``(m, 2)``).
96
+ v: Dual weights solving the optimal transport problem (length ``n``).
103
97
 
104
98
  Returns:
105
- an `(m,2)` matrix with the quantiles of `y` at the values `u`
99
+ Array of quantile locations with shape ``(m, 2)``.
106
100
  """
107
- net_val = u @ y.T - v
108
- k_max = np.argmax(net_val, 1)
109
- return cast(np.ndarray, y[k_max])
101
+ u = np.atleast_2d(u)
102
+ if u.shape[1] != 2:
103
+ bs_error_abort("u must have two columns")
104
+ m = u.shape[0]
105
+ q = np.empty((m, 2))
106
+ block = max(1, min(m, 5_000))
107
+ for start in range(0, m, block):
108
+ stop = min(start + block, m)
109
+ chunk = u[start:stop]
110
+ net_val = chunk @ y.T - v
111
+ k_max = np.argmax(net_val, axis=1)
112
+ q[start:stop] = y[k_max]
113
+ return cast(np.ndarray, q)
110
114
 
111
115
 
112
116
  def bivariate_ranks_v(
113
117
  y: np.ndarray, v: np.ndarray, n_nodes: int = 32, presorted: bool = False
114
118
  ) -> np.ndarray:
115
- """computes the vector ranks of `y`, given the converged `v`
119
+ """Compute the barycentric ranks of each observation given optimal weights.
116
120
 
117
121
  Args:
118
- y: the observations, an `(n,2)` matrix
119
- v: the converged values of the weights, an `n`-vector
120
- n_nodes: the number of nodes for Chebyshev integration
121
- presorted: if `True`, then `y` and `v` are sorted by increasing `y[:, 1]`.
122
+ y: Observations with shape ``(n, 2)``.
123
+ v: Dual weights returned by ``solve_for_v_``.
124
+ n_nodes: Number of Chebyshev nodes used in the quadrature.
125
+ presorted: Set to ``True`` when ``y``/``v`` are pre-sorted by the second coordinate.
122
126
 
123
127
  Returns:
124
- an `(n,2)` matrix with the average ranks of `y`
128
+ Array of average ranks (shape ``(n, 2)``) with ``nan`` for zero-mass cells.
125
129
  """
126
130
  n, d = y.shape
127
131
 
@@ -151,6 +155,9 @@ def bivariate_ranks_v(
151
155
  right_bounds * right_bounds - left_bounds * left_bounds, 0.0
152
156
  )
153
157
  prob_k = pos_diffs @ u1_weights
158
+ if prob_k <= 1e-12:
159
+ average_ranks[sort_order[k], :] = np.array([np.nan, np.nan])
160
+ continue
154
161
  average_ranks[sort_order[k], 0] = ((u1_nodes * pos_diffs) @ u1_weights) / prob_k
155
162
  average_ranks[sort_order[k], 1] = ((pos_diffs_sq @ u1_weights) / 2.0) / prob_k
156
163
 
@@ -213,6 +220,16 @@ def _grad(v_sorted: np.ndarray, args: list):
213
220
 
214
221
 
215
222
  def solve_for_v_(y: np.ndarray, n_nodes: int = 32, verbose: bool = False) -> np.ndarray:
223
+ """Solve the dual optimisation to obtain the optimal weights ``v``.
224
+
225
+ Args:
226
+ y: Observations with shape ``(n, 2)``.
227
+ n_nodes: Number of Chebyshev nodes for the quadrature.
228
+ verbose: Print optimisation diagnostics when ``True``.
229
+
230
+ Returns:
231
+ Array of length ``n`` containing the optimal weights (including the residual term).
232
+ """
216
233
  n, d = y.shape
217
234
 
218
235
  if d != 2:
@@ -250,17 +267,16 @@ def solve_for_v_(y: np.ndarray, n_nodes: int = 32, verbose: bool = False) -> np.
250
267
  def bivariate_quantiles(
251
268
  y: np.ndarray, u: np.ndarray, n_nodes: int = 32, verbose: bool = False
252
269
  ) -> np.ndarray:
253
- """computes the bivariate quantiles of `y` at the quantiles `u`
270
+ """Solve for the dual weights then evaluate bivariate quantiles.
254
271
 
255
272
  Args:
256
- y: the observations, an `(n, 2)` matrix
257
- u: the quantiles at which to compute the bivariate quantiles,
258
- an `(m, 2)` matrix
259
- n_nodes: the number of nodes to use for the quadrature
260
- verbose: if `True`, print some information
273
+ y: Observations, shape ``(n, 2)``.
274
+ u: Query points in ``[0, 1]^2`` (shape ``(m, 2)``).
275
+ n_nodes: Number of Chebyshev nodes for the quadrature.
276
+ verbose: Print optimisation diagnostics when ``True``.
261
277
 
262
278
  Returns:
263
- an `(m, 2)` matrix of bivariate quantiles
279
+ Bivariate quantiles at ``u``.
264
280
  """
265
281
  v = solve_for_v_(y, n_nodes, verbose)
266
282
  return bivariate_quantiles_v(y, u, v)
@@ -269,15 +285,15 @@ def bivariate_quantiles(
269
285
  def bivariate_ranks(
270
286
  y: np.ndarray, n_nodes: int = 32, verbose: bool = False
271
287
  ) -> np.ndarray:
272
- """computes the bivariate ranks of `y`
288
+ """Compute ranks by first solving for the optimal weights ``v``.
273
289
 
274
290
  Args:
275
- y: the observations, an `(n, 2)` matrix
276
- n_nodes: the number of nodes to use for the quadrature
277
- verbose: if `True`, print some information
291
+ y: Observations, shape ``(n, 2)``.
292
+ n_nodes: Number of Chebyshev nodes for the quadrature.
293
+ verbose: Print optimisation diagnostics when ``True``.
278
294
 
279
295
  Returns:
280
- the `(n, 2)` matrix of bivariate average ranks
296
+ Average ranks with shape ``(n, 2)``.
281
297
  """
282
298
  v = solve_for_v_(y, n_nodes, verbose)
283
299
  return bivariate_ranks_v(y, v, n_nodes)