multivariate-probit 0.3.2__tar.gz → 0.3.3__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 (64) hide show
  1. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/CITATION.cff +2 -2
  2. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/PKG-INFO +1 -1
  3. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/api.md +3 -2
  4. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/pyproject.toml +1 -1
  5. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/src/multivariate_probit/__init__.py +1 -1
  6. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/src/multivariate_probit/_mvn.py +64 -10
  7. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/src/multivariate_probit/ifm.py +14 -4
  8. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/src/multivariate_probit/model.py +10 -2
  9. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/src/multivariate_probit/results.py +10 -2
  10. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/.github/ISSUE_TEMPLATE/use-case.md +0 -0
  11. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/.gitignore +0 -0
  12. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/LICENSE +0 -0
  13. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/README.md +0 -0
  14. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/benchmarks/bench.py +0 -0
  15. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/benchmarks/benchmarks.ipynb +0 -0
  16. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/benchmarks/ref_d12_n3000_100_rho0.4_s0.npz +0 -0
  17. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/benchmarks/ref_d16_n3000_100_rho0.4_s0.npz +0 -0
  18. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/benchmarks/ref_d3_n3000_100_rho0.4_s0.npz +0 -0
  19. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/benchmarks/ref_d5_n3000_100_rho0.4_s0.npz +0 -0
  20. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/benchmarks/ref_d8_n3000_100_rho0.4_s0.npz +0 -0
  21. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/benchmarks/ref_pairwise_d12_n3000_100_s0.npz +0 -0
  22. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/benchmarks/ref_pairwise_d16_n3000_100_s0.npz +0 -0
  23. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/benchmarks/ref_pairwise_d3_n3000_100_s0.npz +0 -0
  24. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/benchmarks/ref_pairwise_d5_n3000_100_s0.npz +0 -0
  25. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/benchmarks/ref_pairwise_d8_n3000_100_s0.npz +0 -0
  26. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/benchmarks/results.csv +0 -0
  27. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/benchmarks.md +0 -0
  28. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/fiml.md +0 -0
  29. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/ifm.md +0 -0
  30. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/implementation.md +0 -0
  31. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/limitations.md +0 -0
  32. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/studies/README.md +0 -0
  33. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/studies/comparators.md +0 -0
  34. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/studies/ghk.md +0 -0
  35. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/studies/index-correlation-shortcut.md +0 -0
  36. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/studies/log-score-evaluator.md +0 -0
  37. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/studies/margin-gaps.md +0 -0
  38. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/studies/mulan-multilabel.md +0 -0
  39. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/studies/multilabel-benchmarks.md +0 -0
  40. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/studies/sparse-label-transfer.md +0 -0
  41. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/studies/uci-credit.md +0 -0
  42. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/use-cases/README.md +0 -0
  43. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/use-cases/claims-fast-track.md +0 -0
  44. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/use-cases/content-moderation.md +0 -0
  45. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/use-cases/cyber.md +0 -0
  46. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/use-cases/fraud-vendor-fusion.md +0 -0
  47. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/use-cases/hcc-codes.md +0 -0
  48. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/docs/use-cases/property-imagery.md +0 -0
  49. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/hatch_build.py +0 -0
  50. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/src/multivariate_probit/_corr.py +0 -0
  51. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/src/multivariate_probit/_remote.py +0 -0
  52. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/src/multivariate_probit/inner.py +0 -0
  53. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/src/multivariate_probit/linear.py +0 -0
  54. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/src/multivariate_probit/orthant/__init__.py +0 -0
  55. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/tests/conftest.py +0 -0
  56. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/tests/test_calibration.py +0 -0
  57. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/tests/test_conditional.py +0 -0
  58. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/tests/test_contract.py +0 -0
  59. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/tests/test_correlation_projection.py +0 -0
  60. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/tests/test_evaluators.py +0 -0
  61. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/tests/test_linear.py +0 -0
  62. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/tests/test_modal_fit.py +0 -0
  63. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/tests/test_rf.py +0 -0
  64. {multivariate_probit-0.3.2 → multivariate_probit-0.3.3}/tests/test_xgboost.py +0 -0
@@ -6,8 +6,8 @@ authors:
6
6
  - family-names: Shipman
7
7
  given-names: Mark
8
8
  orcid: "https://orcid.org/0009-0008-7193-1263"
9
- version: 0.3.0
10
- date-released: 2026-10-06
9
+ version: 0.3.3
10
+ date-released: 2026-10-29
11
11
  license: MIT
12
12
  repository-code: "https://github.com/sign-of-fourier/multivariate-probit"
13
13
  url: "https://quantecarlo.com/multivariate-probit"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: multivariate-probit
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Joint probabilities for correlated binary outcomes: P(any), P(all), patterns and conditionals that agree. Any classifier per outcome; scikit-learn-style API.
5
5
  Project-URL: Homepage, https://quantecarlo.com/multivariate-probit
6
6
  Project-URL: Source, https://github.com/sign-of-fourier/multivariate-probit
@@ -35,10 +35,10 @@ MultivariateProbit(
35
35
  | `optimizer` | `"Nelder-Mead"` | Passed to `scipy.optimize.minimize` for `dependence="joint"`. Derivative-free by design. |
36
36
  | `project_correlation` | `True` | Project a pairwise estimate onto the nearest positive-definite correlation matrix. No effect when `dependence="joint"`. |
37
37
  | `random_state` | `None` | Controls the cross-fitting split and `sample`. |
38
- | `evaluator` | `"quadrature"` | Backend for the orthant probabilities behind `dependence="joint"` and every joint query: `"quadrature"`, `"scipy"` or `"orthant"`. See [Evaluators](#evaluators). |
38
+ | `evaluator` | `"quadrature"` | Backend for the orthant probabilities behind `dependence="joint"` and every joint query: `"quadrature"`, `"scipy"`, `"orthant"` or `"ghk"`. See [Evaluators](#evaluators). |
39
39
  | `resolution` | `"high"` | Passed to `evaluator="orthant"`; ignored otherwise. `"high"`, `"highest"` (most accurate, about 2–3× the time of `"high"`: median error under 0.00005 on an equicorrelated Σ against up to 0.0001 at `"high"`, and 0.0001–0.0031 on a near-singular Σ against 0.0006–0.0061; see [benchmarks.md](benchmarks.md)), `"smooth"` (the cost of `"highest"`, continuous in the parameters with no threshold where the method changes, for gradient-based fitting; not uniformly more accurate than `"high"`) or `"low"` (cheapest). |
40
40
  | `fitter` | `"ifm"` | `"ifm"`: the local two-stage fit. `"modal"`: full maximum likelihood on a hosted GPU service ([fiml.md](fiml.md)) — `inner="linear"` only, `inner_params` may set only `alpha`; `dependence`, `cv`, `optimizer` and `project_correlation` do not apply. Uploads X, Y and `sample_weight`. An int `random_state` seeds its Sobol draws (default seed 0). |
41
- | `n_draws` | `512` | Sobol draws per row for `fitter="modal"`; ignored otherwise. |
41
+ | `n_draws` | `512` | Sobol draws per row for `fitter="modal"` and `evaluator="ghk"`; ignored otherwise. Powers of 2 keep the Sobol set balanced. |
42
42
 
43
43
  > **Reading `calibration_`.** This is the Cox calibration slope, a deliberately
44
44
  > simple scale check: a probit of each outcome on its own fitted index. A slope
@@ -78,6 +78,7 @@ queries on a pairwise fit still use the chosen backend.
78
78
  | `"quadrature"` | Genz's recursive conditioning with a fixed Gauss-Legendre rule of order `n_quad` (see [implementation.md](implementation.md)). | Deterministic and accurate. Cost grows as `n_quad ** (d - 2)`, so it becomes impractical well before d = 10. |
79
79
  | `"scipy"` | `scipy.stats.multivariate_normal.cdf`, one row at a time. | Reaches any d, but is slow per row and randomised: a `dependence="joint"` fit is not exactly reproducible, and its noisy objective can stall the simplex. |
80
80
  | `"orthant"` | An optional compiled package bundled as `multivariate_probit.orthant`. Install with `pip install multivariate-probit[orthant]`. | Built for CPython 3.11 and 3.12 on x86-64 Linux only; elsewhere it raises `ImportError`. Without a key it accepts d ≤ 3 at `resolution="low"` only, and raises `ValueError` outside that. A key is read from `$ORTHANT_KEY` or `~/.orthant/key` at import, or passed at any time with `orthant.activate(key)` (returns `"paid"`; a wrong key raises `ValueError`). Each process needs its own key: on Spark, call `activate` inside the function the workers run, or set the variable in the cluster environment; see https://quantecarlo.com/orthant_key. Accuracy and speed against GHK and SciPy: [studies/ghk.md](studies/ghk.md). |
81
+ | `"ghk"` | The GHK simulator: per row, variables ordered by ascending limit, sequential truncated-normal conditioning on `n_draws` scrambled Sobol points shared by all rows (fixed seed). Pure NumPy, no key. | Deterministic and smooth in the parameters, reaches any d, vectorised over rows. Error falls with `n_draws` (roughly 1e-3 max, 1e-5 median at 128-512 draws, d 3-5); cost is linear in `n_draws` and about quadratic in d. |
81
82
 
82
83
  For test sets too large for one machine, the same evaluator runs as a hosted
83
84
  GPU service, `quantecarlo.orthant_cdf`: about a second per million rows at
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "multivariate-probit"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "Joint probabilities for correlated binary outcomes: P(any), P(all), patterns and conditionals that agree. Any classifier per outcome; scikit-learn-style API."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -34,7 +34,7 @@ from .linear import ProbitRegressor
34
34
  from .model import MultivariateProbit
35
35
  from .results import MultivariateProbitProba
36
36
 
37
- __version__ = "0.3.2"
37
+ __version__ = "0.3.3"
38
38
 
39
39
  __all__ = [
40
40
  "MultivariateProbit",
@@ -14,6 +14,7 @@ from __future__ import annotations
14
14
  from functools import lru_cache
15
15
 
16
16
  import numpy as np
17
+ from scipy.special import log_ndtr, ndtr, ndtri
17
18
  from scipy.stats import norm
18
19
 
19
20
  __all__ = [
@@ -36,9 +37,11 @@ _EPS = 1e-12
36
37
  # The backends that can evaluate a lower-orthant probability. The choice is a
37
38
  # speed/accuracy trade-off left to the caller: "quadrature" is the
38
39
  # deterministic evaluator below, "scipy" is scipy.stats.multivariate_normal.cdf
39
- # (randomised quasi-Monte Carlo, one row at a time), and "orthant" is the
40
- # optional compiled package bundled under ``multivariate_probit.orthant``.
41
- EVALUATORS = ("quadrature", "scipy", "orthant")
40
+ # (randomised quasi-Monte Carlo, one row at a time), "orthant" is the
41
+ # optional compiled package bundled under ``multivariate_probit.orthant``, and
42
+ # "ghk" is the GHK simulator below with fixed scrambled Sobol draws
43
+ # (deterministic, vectorised, any d).
44
+ EVALUATORS = ("quadrature", "scipy", "orthant", "ghk")
42
45
 
43
46
 
44
47
  @lru_cache(maxsize=8)
@@ -136,6 +139,50 @@ def _scipy_orthant(A, corr_stack):
136
139
  return out
137
140
 
138
141
 
142
+ @lru_cache(maxsize=8)
143
+ def _sobol(dim, n_draws, seed):
144
+ """Scrambled Sobol points in (0, 1)^dim, cached so repeated calls (an
145
+ optimizer's objective) reuse the same draws."""
146
+ from scipy.stats import qmc
147
+
148
+ return qmc.Sobol(dim, scramble=True, seed=seed).random(n_draws)
149
+
150
+
151
+ def _ghk_orthant(A, corr_stack, n_draws=512, seed=0, max_cells=2_000_000):
152
+ """GHK (Geweke-Hajivassiliou-Keane) estimate of the lower orthant, row by row.
153
+
154
+ Per row the variables are reordered by ascending limit, the correlation is
155
+ Cholesky-factored, and each draw samples the variables in sequence from
156
+ their truncated conditionals; the estimate is the mean over draws of the
157
+ product of the conditional probabilities. The draws are one scrambled
158
+ Sobol set shared by all rows (seed fixed), so the result is deterministic
159
+ and smooth in A and the correlations. Error falls with ``n_draws``.
160
+ """
161
+ n, d = A.shape
162
+ if d == 1:
163
+ return norm.cdf(A[:, 0])
164
+ S = np.moveaxis(corr_stack, 2, 0)
165
+ order = np.argsort(A, axis=1)
166
+ A = np.take_along_axis(A, order, axis=1)
167
+ S = S[np.arange(n)[:, None, None], order[:, :, None], order[:, None, :]]
168
+ L = np.linalg.cholesky(S)
169
+ U = _sobol(d - 1, int(n_draws), seed)
170
+ out = np.empty(n)
171
+ step = max(1, max_cells // (n_draws * d))
172
+ for lo in range(0, n, step):
173
+ a, Lc = A[lo:lo + step], L[lo:lo + step]
174
+ logp = np.zeros((len(a), n_draws))
175
+ z = np.zeros((len(a), n_draws, d - 1))
176
+ for j in range(d):
177
+ mu = z[:, :, :j] @ Lc[:, j, :j, None]
178
+ b = (a[:, j, None] - mu[:, :, 0]) / Lc[:, j, j, None]
179
+ logp += log_ndtr(b)
180
+ if j < d - 1:
181
+ z[:, :, j] = ndtri(np.clip(U[:, j] * ndtr(b), 1e-300, 1.0 - 1e-16))
182
+ out[lo:lo + step] = np.exp(logp).mean(axis=1)
183
+ return out
184
+
185
+
139
186
  def _compiled_orthant(A, corr_stack, resolution):
140
187
  try:
141
188
  from . import orthant
@@ -153,11 +200,11 @@ def _compiled_orthant(A, corr_stack, resolution):
153
200
  return np.asarray(prob, dtype=float)
154
201
 
155
202
 
156
- def lower_orthant(A, corr_stack, evaluator="quadrature", n_quad=24, resolution="high"):
203
+ def lower_orthant(A, corr_stack, evaluator="quadrature", n_quad=24, resolution="high", n_draws=512):
157
204
  """:func:`mvn_orthant` with a choice of backend.
158
205
 
159
- ``n_quad`` is used only by ``"quadrature"`` and ``resolution`` only by
160
- ``"orthant"``. A backend that cannot run as asked raises; nothing falls
206
+ ``n_quad`` is used only by ``"quadrature"``, ``resolution`` only by
207
+ ``"orthant"`` and ``n_draws`` only by ``"ghk"``. A backend that cannot run as asked raises; nothing falls
161
208
  back to another backend. Results are clipped to [0, 1].
162
209
  """
163
210
  A = np.atleast_2d(np.asarray(A, dtype=float))
@@ -167,6 +214,8 @@ def lower_orthant(A, corr_stack, evaluator="quadrature", n_quad=24, resolution="
167
214
  prob = _scipy_orthant(A, corr_stack)
168
215
  elif evaluator == "orthant":
169
216
  prob = _compiled_orthant(A, corr_stack, resolution)
217
+ elif evaluator == "ghk":
218
+ prob = _ghk_orthant(A, corr_stack, n_draws=n_draws)
170
219
  else:
171
220
  raise ValueError(f"evaluator must be one of {EVALUATORS}; got {evaluator!r}")
172
221
  return np.clip(prob, 0.0, 1.0)
@@ -192,14 +241,14 @@ def signed_corr_stack(corr, signs):
192
241
  return stack
193
242
 
194
243
 
195
- def orthant_prob(upper, corr, n_quad=24, evaluator="quadrature", resolution="high"):
244
+ def orthant_prob(upper, corr, n_quad=24, evaluator="quadrature", resolution="high", n_draws=512):
196
245
  """P(Z_j <= upper[i, j] for all j) for a single shared correlation matrix."""
197
246
  upper = np.atleast_2d(np.asarray(upper, dtype=float))
198
247
  stack = signed_corr_stack(corr, np.ones_like(upper))
199
- return lower_orthant(upper, stack, evaluator, n_quad=n_quad, resolution=resolution)
248
+ return lower_orthant(upper, stack, evaluator, n_quad=n_quad, resolution=resolution, n_draws=n_draws)
200
249
 
201
250
 
202
- def pattern_prob(eta, Y, corr, n_quad=24, evaluator="quadrature", resolution="high"):
251
+ def pattern_prob(eta, Y, corr, n_quad=24, evaluator="quadrature", resolution="high", n_draws=512):
203
252
  """``P(Y = y | x)`` for the multivariate probit, row by row.
204
253
 
205
254
  With ``s = 2y - 1`` the event ``{Y = y}`` is the orthant
@@ -215,5 +264,10 @@ def pattern_prob(eta, Y, corr, n_quad=24, evaluator="quadrature", resolution="hi
215
264
 
216
265
  signs = 2.0 * Y - 1.0
217
266
  return lower_orthant(
218
- signs * eta, signed_corr_stack(corr, signs), evaluator, n_quad=n_quad, resolution=resolution
267
+ signs * eta,
268
+ signed_corr_stack(corr, signs),
269
+ evaluator,
270
+ n_quad=n_quad,
271
+ resolution=resolution,
272
+ n_draws=n_draws,
219
273
  )
@@ -114,14 +114,16 @@ def free_from_corr(corr):
114
114
 
115
115
 
116
116
  def joint_log_likelihood(
117
- corr, eta, Y, weights=None, n_quad=24, evaluator="quadrature", resolution="high"
117
+ corr, eta, Y, weights=None, n_quad=24, evaluator="quadrature", resolution="high", n_draws=512
118
118
  ):
119
119
  """Full d-variate log-likelihood in ``corr``, with the margins held fixed.
120
120
 
121
- ``evaluator``, ``n_quad`` and ``resolution`` select and tune the orthant
121
+ ``evaluator``, ``n_quad``, ``resolution`` and ``n_draws`` select and tune the orthant
122
122
  backend; see :func:`~multivariate_probit._mvn.lower_orthant`.
123
123
  """
124
- prob = pattern_prob(eta, Y, corr, n_quad=n_quad, evaluator=evaluator, resolution=resolution)
124
+ prob = pattern_prob(
125
+ eta, Y, corr, n_quad=n_quad, evaluator=evaluator, resolution=resolution, n_draws=n_draws
126
+ )
125
127
  prob = np.clip(prob, _LL_EPS, None)
126
128
  ll = np.log(prob)
127
129
  if weights is None:
@@ -140,6 +142,7 @@ def joint_correlation(
140
142
  options=None,
141
143
  evaluator="quadrature",
142
144
  resolution="high",
145
+ n_draws=512,
143
146
  ):
144
147
  """Estimate the latent correlation matrix by full-information ML in ``R``.
145
148
 
@@ -181,7 +184,14 @@ def joint_correlation(
181
184
  if np.linalg.eigvalsh(corr).min() <= min_eigenvalue:
182
185
  return 1e10 # not positive definite -- push the optimizer away
183
186
  return -joint_log_likelihood(
184
- corr, eta, Y, weights=weights, n_quad=n_quad, evaluator=evaluator, resolution=resolution
187
+ corr,
188
+ eta,
189
+ Y,
190
+ weights=weights,
191
+ n_quad=n_quad,
192
+ evaluator=evaluator,
193
+ resolution=resolution,
194
+ n_draws=n_draws,
185
195
  )
186
196
 
187
197
  result = minimize(neg_ll, free_from_corr(start), method=optimizer, options=options)
@@ -144,7 +144,7 @@ class MultivariateProbit:
144
144
  absorbed part of the noise, which attenuates the estimated
145
145
  correlations toward zero. ``None`` skips cross-fitting -- reasonable
146
146
  for the linear default, risky for anything that can overfit.
147
- evaluator : {"quadrature", "scipy", "orthant"}, default="quadrature"
147
+ evaluator : {"quadrature", "scipy", "orthant", "ghk"}, default="quadrature"
148
148
  Backend for the orthant probabilities behind ``dependence="joint"``
149
149
  and every joint query. The speed/accuracy trade-off is the caller's:
150
150
 
@@ -158,6 +158,10 @@ class MultivariateProbit:
158
158
  Linux; ``pip install multivariate-probit[orthant]``). Without a key
159
159
  it accepts ``d <= 3`` and ``resolution="low"`` only; outside that,
160
160
  and on any platform it was not built for, it raises.
161
+ - ``"ghk"``: the GHK simulator with ``n_draws`` fixed scrambled Sobol
162
+ draws per row. Deterministic, vectorised and free (no key), any
163
+ ``d``; error falls with ``n_draws``. Not available in 0.3.2 and
164
+ earlier.
161
165
 
162
166
  Results are clipped to [0, 1]. No backend ever falls back to another.
163
167
  ``dependence="pairwise"`` needs only bivariate probabilities, which
@@ -197,7 +201,8 @@ class MultivariateProbit:
197
201
  ``sample_weight`` are uploaded; only parameters come back, and every
198
202
  prediction afterwards runs locally on ``evaluator``.
199
203
  n_draws : int, default=512
200
- Sobol draws per row for ``fitter="modal"``; ignored otherwise.
204
+ Sobol draws per row for ``fitter="modal"`` and ``evaluator="ghk"``;
205
+ ignored otherwise. Powers of 2 keep the Sobol set balanced.
201
206
 
202
207
  Attributes
203
208
  ----------
@@ -320,6 +325,7 @@ class MultivariateProbit:
320
325
  optimizer=self.optimizer,
321
326
  evaluator=self.evaluator,
322
327
  resolution=self.resolution,
328
+ n_draws=self.n_draws,
323
329
  )
324
330
  self.nll_ = None if self.optimize_result_ is None else float(self.optimize_result_.fun)
325
331
  elif self.dependence == "pairwise":
@@ -525,6 +531,7 @@ class MultivariateProbit:
525
531
  n_quad=self.n_quad,
526
532
  evaluator=self.evaluator,
527
533
  resolution=self.resolution,
534
+ n_draws=self.n_draws,
528
535
  )
529
536
 
530
537
  def predict_marginal_proba(self, X):
@@ -556,6 +563,7 @@ class MultivariateProbit:
556
563
  n_quad=self.n_quad,
557
564
  evaluator=self.evaluator,
558
565
  resolution=self.resolution,
566
+ n_draws=self.n_draws,
559
567
  )
560
568
 
561
569
  def joint_log_proba(self, X, Y):
@@ -21,18 +21,24 @@ class MultivariateProbitProba:
21
21
  correlation matrix was estimated in the first place.
22
22
  """
23
23
 
24
- def __init__(self, eta, corr, n_quad=24, evaluator="quadrature", resolution="high"):
24
+ def __init__(self, eta, corr, n_quad=24, evaluator="quadrature", resolution="high", n_draws=512):
25
25
  self.eta = np.atleast_2d(np.asarray(eta, dtype=float))
26
26
  self.marginal = norm.cdf(self.eta)
27
27
  self.corr = np.asarray(corr, dtype=float)
28
28
  self.n_quad = n_quad
29
29
  self.evaluator = evaluator
30
30
  self.resolution = resolution
31
+ self.n_draws = n_draws
31
32
 
32
33
  def _orthant(self, upper, corr):
33
34
  stack = signed_corr_stack(corr, np.ones_like(upper))
34
35
  return lower_orthant(
35
- upper, stack, self.evaluator, n_quad=self.n_quad, resolution=self.resolution
36
+ upper,
37
+ stack,
38
+ self.evaluator,
39
+ n_quad=self.n_quad,
40
+ resolution=self.resolution,
41
+ n_draws=self.n_draws,
36
42
  )
37
43
 
38
44
  # ------------------------------------------------------ array behaviour
@@ -72,6 +78,7 @@ class MultivariateProbitProba:
72
78
  n_quad=self.n_quad,
73
79
  evaluator=self.evaluator,
74
80
  resolution=self.resolution,
81
+ n_draws=self.n_draws,
75
82
  )
76
83
 
77
84
  def all(self, outcomes=None):
@@ -102,6 +109,7 @@ class MultivariateProbitProba:
102
109
  n_quad=self.n_quad,
103
110
  evaluator=self.evaluator,
104
111
  resolution=self.resolution,
112
+ n_draws=self.n_draws,
105
113
  )
106
114
 
107
115
  def conditional(self, event, given):