multivariate-probit 0.3.0__tar.gz → 0.3.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.
Files changed (66) hide show
  1. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/CITATION.cff +1 -1
  2. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/PKG-INFO +5 -3
  3. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/README.md +4 -2
  4. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/benchmarks/bench.py +2 -2
  5. multivariate_probit-0.3.2/benchmarks/results.csv +65 -0
  6. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/api.md +3 -3
  7. multivariate_probit-0.3.2/docs/benchmarks.md +95 -0
  8. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/fiml.md +21 -0
  9. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/studies/ghk.md +5 -0
  10. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/pyproject.toml +1 -1
  11. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/src/multivariate_probit/__init__.py +1 -1
  12. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/src/multivariate_probit/model.py +11 -2
  13. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/src/multivariate_probit/orthant/__init__.py +76 -18
  14. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/tests/test_evaluators.py +53 -0
  15. multivariate_probit-0.3.0/benchmarks/results.csv +0 -55
  16. multivariate_probit-0.3.0/docs/benchmarks.md +0 -90
  17. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/.github/ISSUE_TEMPLATE/use-case.md +0 -0
  18. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/.gitignore +0 -0
  19. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/LICENSE +0 -0
  20. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/benchmarks/benchmarks.ipynb +0 -0
  21. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/benchmarks/ref_d12_n3000_100_rho0.4_s0.npz +0 -0
  22. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/benchmarks/ref_d16_n3000_100_rho0.4_s0.npz +0 -0
  23. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/benchmarks/ref_d3_n3000_100_rho0.4_s0.npz +0 -0
  24. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/benchmarks/ref_d5_n3000_100_rho0.4_s0.npz +0 -0
  25. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/benchmarks/ref_d8_n3000_100_rho0.4_s0.npz +0 -0
  26. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/benchmarks/ref_pairwise_d12_n3000_100_s0.npz +0 -0
  27. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/benchmarks/ref_pairwise_d16_n3000_100_s0.npz +0 -0
  28. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/benchmarks/ref_pairwise_d3_n3000_100_s0.npz +0 -0
  29. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/benchmarks/ref_pairwise_d5_n3000_100_s0.npz +0 -0
  30. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/benchmarks/ref_pairwise_d8_n3000_100_s0.npz +0 -0
  31. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/ifm.md +0 -0
  32. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/implementation.md +0 -0
  33. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/limitations.md +0 -0
  34. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/studies/README.md +0 -0
  35. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/studies/comparators.md +0 -0
  36. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/studies/index-correlation-shortcut.md +0 -0
  37. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/studies/log-score-evaluator.md +0 -0
  38. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/studies/margin-gaps.md +0 -0
  39. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/studies/mulan-multilabel.md +0 -0
  40. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/studies/multilabel-benchmarks.md +0 -0
  41. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/studies/sparse-label-transfer.md +0 -0
  42. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/studies/uci-credit.md +0 -0
  43. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/use-cases/README.md +0 -0
  44. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/use-cases/claims-fast-track.md +0 -0
  45. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/use-cases/content-moderation.md +0 -0
  46. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/use-cases/cyber.md +0 -0
  47. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/use-cases/fraud-vendor-fusion.md +0 -0
  48. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/use-cases/hcc-codes.md +0 -0
  49. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/docs/use-cases/property-imagery.md +0 -0
  50. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/hatch_build.py +0 -0
  51. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/src/multivariate_probit/_corr.py +0 -0
  52. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/src/multivariate_probit/_mvn.py +0 -0
  53. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/src/multivariate_probit/_remote.py +0 -0
  54. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/src/multivariate_probit/ifm.py +0 -0
  55. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/src/multivariate_probit/inner.py +0 -0
  56. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/src/multivariate_probit/linear.py +0 -0
  57. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/src/multivariate_probit/results.py +0 -0
  58. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/tests/conftest.py +0 -0
  59. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/tests/test_calibration.py +0 -0
  60. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/tests/test_conditional.py +0 -0
  61. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/tests/test_contract.py +0 -0
  62. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/tests/test_correlation_projection.py +0 -0
  63. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/tests/test_linear.py +0 -0
  64. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/tests/test_modal_fit.py +0 -0
  65. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/tests/test_rf.py +0 -0
  66. {multivariate_probit-0.3.0 → multivariate_probit-0.3.2}/tests/test_xgboost.py +0 -0
@@ -7,7 +7,7 @@ authors:
7
7
  given-names: Mark
8
8
  orcid: "https://orcid.org/0009-0008-7193-1263"
9
9
  version: 0.3.0
10
- date-released: 2026-10-04
10
+ date-released: 2026-10-06
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.0
3
+ Version: 0.3.2
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
@@ -213,8 +213,10 @@ The backend is chosen with `evaluator=`:
213
213
  of magnitude faster than SciPy, at roughly the accuracy of 100-draw GHK
214
214
  simulation ([studies/ghk.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/studies/ghk.md)). It ships for CPython 3.11
215
215
  and 3.12 on x86-64 Linux (`pip install multivariate-probit[orthant]`); without
216
- a key it runs d ≤ 3 at `resolution="low"`. Key:
217
- [quantecarlo.com/orthant_key](https://quantecarlo.com/orthant_key).
216
+ a key it runs d ≤ 3 at `resolution="low"`. The key is read from `$ORTHANT_KEY` or
217
+ `~/.orthant/key` at import, or passed in code with
218
+ `from multivariate_probit import orthant; orthant.activate(key)` (on Spark, call it inside
219
+ the function the workers run). Key: [quantecarlo.com/orthant_key](https://quantecarlo.com/orthant_key).
218
220
 
219
221
  Benchmarks against other tools: [docs/benchmarks.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/benchmarks.md).
220
222
 
@@ -149,8 +149,10 @@ The backend is chosen with `evaluator=`:
149
149
  of magnitude faster than SciPy, at roughly the accuracy of 100-draw GHK
150
150
  simulation ([studies/ghk.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/studies/ghk.md)). It ships for CPython 3.11
151
151
  and 3.12 on x86-64 Linux (`pip install multivariate-probit[orthant]`); without
152
- a key it runs d ≤ 3 at `resolution="low"`. Key:
153
- [quantecarlo.com/orthant_key](https://quantecarlo.com/orthant_key).
152
+ a key it runs d ≤ 3 at `resolution="low"`. The key is read from `$ORTHANT_KEY` or
153
+ `~/.orthant/key` at import, or passed in code with
154
+ `from multivariate_probit import orthant; orthant.activate(key)` (on Spark, call it inside
155
+ the function the workers run). Key: [quantecarlo.com/orthant_key](https://quantecarlo.com/orthant_key).
154
156
 
155
157
  Benchmarks against other tools: [docs/benchmarks.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/benchmarks.md).
156
158
 
@@ -104,8 +104,8 @@ def run_cell(d, n_train=3000, n_test=100, rho=0.4, seed=0, sigma="equi"):
104
104
  if d <= 6:
105
105
  res["mvp quadrature"] = timed(lambda: model.set_params(evaluator="quadrature").joint_proba(Xte, Yte))
106
106
  if orthant is not None and (ORTHANT_KEYED or d <= 3):
107
- r = "high" if ORTHANT_KEYED else "low"
108
- res[f"mvp orthant ({r})"] = timed(lambda: model.set_params(evaluator="orthant", resolution=r).joint_proba(Xte, Yte))
107
+ for r in (("high", "highest") if ORTHANT_KEYED else ("low",)):
108
+ res[f"mvp orthant ({r})"] = timed(lambda: model.set_params(evaluator="orthant", resolution=r).joint_proba(Xte, Yte))
109
109
  res["SciPy default"] = timed(lambda: model.set_params(evaluator="scipy").joint_proba(Xte, Yte))
110
110
  if mvncd_batch is not None:
111
111
  st = np.moveaxis(stack, 2, 0)
@@ -0,0 +1,65 @@
1
+ sigma,d,N,method,median_err,max_err,s_per_row
2
+ equi,3,100,mvp quadrature,1.3373382605444406e-05,5.564767189014663e-05,7.751391967758536e-05
3
+ equi,3,100,mvp orthant (high),5.190348623980261e-05,0.00019774969627306405,1.484813867136836e-05
4
+ equi,3,100,mvp orthant (highest),1.999845194528005e-06,7.989768001565034e-06,4.311794182285666e-05
5
+ equi,3,100,SciPy default,2.039637077871914e-06,1.1415786741186817e-05,0.005793280370999128
6
+ equi,3,100,pybhatlib ovus,0.00011229346488104974,0.0008524556947959311,8.469653083011508e-05
7
+ equi,3,100,statsmodels,0.028443441360488153,0.09919722660040897,6.613903678953648e-07
8
+ equi,3,100,reference,0.0,0.0,0.2536748156798421
9
+ equi,5,100,mvp quadrature,2.0314513939401896e-05,0.00011324180994054522,0.04333823672030121
10
+ equi,5,100,mvp orthant (high),8.080162918654848e-05,0.00030384074452138865,2.3601381108164788e-05
11
+ equi,5,100,mvp orthant (highest),3.4247200694226265e-06,2.3859122737224236e-05,7.270404836162925e-05
12
+ equi,5,100,SciPy default,1.3427915335623153e-06,7.990427117937626e-06,0.010302326420787721
13
+ equi,5,100,pybhatlib ovus,0.00039917378471633624,0.0019016346577496457,0.00011997280875220894
14
+ equi,5,100,statsmodels,0.024622620503874038,0.12870241315304662,5.677388980984688e-07
15
+ equi,5,100,reference,0.0,0.0,0.5339089114402304
16
+ equi,8,100,mvp orthant (high),7.013787736394328e-05,0.00045342350585386715,4.317081067711115e-05
17
+ equi,8,100,mvp orthant (highest),4.605751738051556e-06,2.9230892634346528e-05,9.955347049981356e-05
18
+ equi,8,100,SciPy default,1.0030036643121978e-06,8.134689687355756e-06,0.01642337770899758
19
+ equi,8,100,pybhatlib ovus,0.0003370945286850363,0.0023083975100191556,0.0001771104708313942
20
+ equi,8,100,statsmodels,0.014453125416131472,0.11538392212916126,9.872694499790668e-07
21
+ equi,8,100,reference,0.0,0.0,1.4400106923800196
22
+ equi,12,100,mvp orthant (high),6.245144142705012e-05,0.0007179989696481925,6.305043119937181e-05
23
+ equi,12,100,mvp orthant (highest),5.63448417025339e-06,4.668168500868153e-05,0.00014906150056049228
24
+ equi,12,100,SciPy default,3.440844550360589e-07,7.96331593577726e-06,0.02377823827089742
25
+ equi,12,100,pybhatlib ovus,0.0002047092197251764,0.0035788660943063266,0.0002432926092296839
26
+ equi,12,100,statsmodels,0.006303004648829415,0.07762653255283464,1.5006307512521743e-06
27
+ equi,12,100,reference,0.0,0.0,1.6423227695701643
28
+ equi,16,100,mvp orthant (high),1.630594965073808e-05,0.0006243813016577615,9.883297141641379e-05
29
+ equi,16,100,mvp orthant (highest),1.1074220600440065e-06,2.9310608489277468e-05,0.00022130653960630297
30
+ equi,16,100,SciPy default,6.853848620851782e-08,4.3920697129634645e-06,0.03263484545052051
31
+ equi,16,100,pybhatlib ovus,1.6800734753088424e-05,0.0011253768512156778,0.00031500173965469
32
+ equi,16,100,statsmodels,0.0014514737962008938,0.05355898580397629,1.4994502998888493e-06
33
+ equi,16,100,reference,0.0,0.0,1.1454083026899025
34
+ pairwise,3,100,mvp quadrature,4.975128970532983e-06,8.276622284841828e-05,7.341289892792702e-05
35
+ pairwise,3,100,mvp orthant (high),0.0006363036604397354,0.002893273763736187,2.0136469975113868e-05
36
+ pairwise,3,100,mvp orthant (highest),0.00011323696696274865,0.0007099285566469216,4.4253640808165074e-05
37
+ pairwise,3,100,SciPy default,1.3494787358958593e-06,2.586364471635072e-05,0.0073384556197561325
38
+ pairwise,3,100,pybhatlib ovus,0.00037654161082817383,0.004713394298326956,8.070134790614248e-05
39
+ pairwise,3,100,statsmodels,0.037060543222106224,0.15997644750429899,4.6383123844861987e-07
40
+ pairwise,3,100,reference,0.0,0.0,0.24052267621998907
41
+ pairwise,5,100,mvp quadrature,5.607947987679163e-06,6.266772635254991e-05,0.04593952419003472
42
+ pairwise,5,100,mvp orthant (high),0.0060661393002539865,0.03370856471106998,2.4112039245665073e-05
43
+ pairwise,5,100,mvp orthant (highest),0.003141591472459862,0.0325453719435822,6.538510089740157e-05
44
+ pairwise,5,100,SciPy default,1.2512113251161167e-06,2.1201235567813725e-05,0.14286554857157172
45
+ pairwise,5,100,pybhatlib ovus,0.0014663128072703105,0.02764332982515838,0.00012095558922737837
46
+ pairwise,5,100,statsmodels,0.052292238612919734,0.2549403651447862,5.610100924968719e-07
47
+ pairwise,5,100,reference,0.0,0.0,1.2560263323498657
48
+ pairwise,8,100,mvp orthant (high),0.0047540425543466,0.03786870554320598,3.998839994892478e-05
49
+ pairwise,8,100,mvp orthant (highest),0.0021531855423506108,0.013414597153998009,9.109209990128875e-05
50
+ pairwise,8,100,SciPy default,2.2860621589921443e-06,3.321167501391109e-05,1.132777205838356
51
+ pairwise,8,100,pybhatlib ovus,0.0015355099588129813,0.010313908094296734,0.00017519862158223986
52
+ pairwise,8,100,statsmodels,0.024908203618945243,0.1426044668035415,8.142902515828609e-07
53
+ pairwise,8,100,reference,0.0,0.0,4.434727118879964
54
+ pairwise,12,100,mvp orthant (high),0.003428055959417961,0.04309079684926198,6.395480129867793e-05
55
+ pairwise,12,100,mvp orthant (highest),0.002445889327158528,0.02153141541464762,0.00015708803897723555
56
+ pairwise,12,100,SciPy default,1.9157011284364617e-06,5.195613406111521e-05,1.5738827282516286
57
+ pairwise,12,100,pybhatlib ovus,0.0007431598443556342,0.01388904489935694,0.0002463792893104255
58
+ pairwise,12,100,statsmodels,0.014263734107914516,0.1854686635860365,1.2842798605561255e-06
59
+ pairwise,12,100,reference,0.0,0.0,9.518902401560045
60
+ pairwise,16,100,mvp orthant (high),0.0037733872849075424,0.04512852480876324,8.360587060451507e-05
61
+ pairwise,16,100,mvp orthant (highest),0.00293222915103126,0.045234964595115376,0.00021089559188112618
62
+ pairwise,16,100,SciPy default,1.7024296601283496e-06,1.0376357636343592e-05,1.7738397101382726
63
+ pairwise,16,100,pybhatlib ovus,0.0009786447220386783,0.015062755675490355,0.0003229634207673371
64
+ pairwise,16,100,statsmodels,0.010651175036494289,0.18479811475521019,1.8178089521825314e-06
65
+ pairwise,16,100,reference,0.0,0.0,19.51646036452992
@@ -36,7 +36,7 @@ MultivariateProbit(
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
38
  | `evaluator` | `"quadrature"` | Backend for the orthant probabilities behind `dependence="joint"` and every joint query: `"quadrature"`, `"scipy"` or `"orthant"`. See [Evaluators](#evaluators). |
39
- | `resolution` | `"high"` | Passed to `evaluator="orthant"`; ignored otherwise. |
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
41
  | `n_draws` | `512` | Sobol draws per row for `fitter="modal"`; ignored otherwise. |
42
42
 
@@ -77,7 +77,7 @@ queries on a pairwise fit still use the chosen backend.
77
77
  | --- | --- | --- |
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
- | `"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`; see https://quantecarlo.com/orthant_key. Accuracy and speed against GHK and SciPy: [studies/ghk.md](studies/ghk.md). |
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
81
 
82
82
  For test sets too large for one machine, the same evaluator runs as a hosted
83
83
  GPU service, `quantecarlo.orthant_cdf`: about a second per million rows at
@@ -94,7 +94,7 @@ the
94
94
  | `correlation_` | (d, d) | The fitted Σ. |
95
95
  | `eta_` | (n, d) | The (cross-fitted) latent indices stage two was fitted on; for `fitter="modal"`, the in-sample indices of the fitted model. |
96
96
  | `calibration_` | (d, 2) | Intercept and slope of a probit of each outcome on its own fitted index — the calibration slope. See the note below. |
97
- | `nll_` | float | Negative log-likelihood at the end of the dependence fit. Joint and pairwise fits optimise different objectives, so the values are not comparable across settings. |
97
+ | `nll_` | float | Negative log-likelihood at the end of the dependence fit. Joint and pairwise fits optimise different objectives, so the values are not comparable across settings. For `fitter="modal"`, the simulated joint negative log-likelihood of all margins and Σ. |
98
98
  | `optimize_result_` | OptimizeResult or None | The SciPy result for `dependence="joint"`. `None` for `fitter="modal"`. |
99
99
  | `stderr_`, `stderr_robust_` | dict | `fitter="modal"` only. Standard errors keyed `"coef"` (d, n_features), `"intercept"` (d,), `"correlation"` (d, d); inverse-Hessian and sandwich. |
100
100
  | `cov_params_`, `cov_params_robust_` | (k, k) | `fitter="modal"` only. The matching covariance matrices, ordered as `param_names_`. |
@@ -0,0 +1,95 @@
1
+ # Benchmarks against other tools
2
+
3
+ **Task.** Score `P(Y = y | x)`, the probability of the pattern each held-out
4
+ row actually has, from one fitted model. Synthetic data: 6 features, linear
5
+ margins, 3,000 training rows, 100 scored rows per d. The model is
6
+ `MultivariateProbit(inner="linear", dependence="pairwise", random_state=0)`.
7
+ Every column scores that same fitted model (η̂, Σ̂), except statsmodels.
8
+
9
+ There are two shapes of true Σ:
10
+
11
+ - **Equicorrelated**, ρ = 0.4: the easy case.
12
+ - **Pairwise-shaped**: a rank-2 factor plus noise, projected to the nearest
13
+ correlation matrix (smallest eigenvalue about 1e-8), the same construction
14
+ as [studies/ghk.md](studies/ghk.md). A fitted pairwise Σ̂ lands on that PSD
15
+ boundary, which is hard for every approximation.
16
+
17
+ **Reference.** SciPy `multivariate_normal.cdf` with `abseps=1e-7`,
18
+ `releps=1e-6`, one row at a time. Error is the absolute difference from it in
19
+ probability: median over rows, maximum in parentheses. Time is wall-clock per
20
+ row after one warm-up call, on a two-core x86-64 Linux machine.
21
+
22
+ Reproduce with [benchmarks/benchmarks.ipynb](../benchmarks/benchmarks.ipynb)
23
+ (logic in `benchmarks/bench.py`; raw numbers in `benchmarks/results.csv`).
24
+ The reference values are cached in `benchmarks/ref_*.npz` together with the
25
+ problem they were computed for, so a rerun skips the slow part and a changed
26
+ problem recomputes. The `orthant` column needs a key in `$ORTHANT_KEY` or
27
+ `~/.orthant/key`.
28
+
29
+ ### Equicorrelated Σ (ρ = 0.4)
30
+
31
+ | d | median p | multivariate-probit `orthant` (`resolution="high"`) | multivariate-probit `orthant` (`resolution="highest"`) | multivariate-probit `quadrature` | SciPy, default tolerances | pybhatlib (Bhat OVUS) | statsmodels |
32
+ | --- | --- | --- | --- | --- | --- | --- | --- |
33
+ | 3 | 0.348 | 1.5e-05 s / 0.0001 (0.0002) | 4.3e-05 s / 0.0000 (0.0000) | 7.8e-05 s / 0.0000 (0.0001) | 5.8e-03 s / 0.0000 (0.0000) | 8.5e-05 s / 0.0001 (0.0009) | 6.6e-07 s / 0.0284 (0.0992) |
34
+ | 5 | 0.154 | 2.4e-05 s / 0.0001 (0.0003) | 7.3e-05 s / 0.0000 (0.0000) | 4.3e-02 s / 0.0000 (0.0001) | 1.0e-02 s / 0.0000 (0.0000) | 1.2e-04 s / 0.0004 (0.0019) | 5.7e-07 s / 0.0246 (0.1287) |
35
+ | 8 | 0.101 | 4.3e-05 s / 0.0001 (0.0005) | 1.0e-04 s / 0.0000 (0.0000) | impractical past d = 7 | 1.6e-02 s / 0.0000 (0.0000) | 1.8e-04 s / 0.0003 (0.0023) | 9.9e-07 s / 0.0145 (0.1154) |
36
+ | 12 | 0.022 | 6.3e-05 s / 0.0001 (0.0007) | 1.5e-04 s / 0.0000 (0.0000) | impractical past d = 7 | 2.4e-02 s / 0.0000 (0.0000) | 2.4e-04 s / 0.0002 (0.0036) | 1.5e-06 s / 0.0063 (0.0776) |
37
+ | 16 | 0.0024 | 9.9e-05 s / 0.0000 (0.0006) | 2.2e-04 s / 0.0000 (0.0000) | impractical past d = 7 | 3.3e-02 s / 0.0000 (0.0000) | 3.2e-04 s / 0.0000 (0.0011) | 1.5e-06 s / 0.0015 (0.0536) |
38
+
39
+ ### Pairwise-shaped Σ (near-singular)
40
+
41
+ | d | median p | multivariate-probit `orthant` (`resolution="high"`) | multivariate-probit `orthant` (`resolution="highest"`) | multivariate-probit `quadrature` | SciPy, default tolerances | pybhatlib (Bhat OVUS) | statsmodels |
42
+ | --- | --- | --- | --- | --- | --- | --- | --- |
43
+ | 3 | 0.431 | 2.0e-05 s / 0.0006 (0.0029) | 4.4e-05 s / 0.0001 (0.0007) | 7.3e-05 s / 0.0000 (0.0001) | 7.3e-03 s / 0.0000 (0.0000) | 8.1e-05 s / 0.0004 (0.0047) | 4.6e-07 s / 0.0371 (0.1600) |
44
+ | 5 | 0.171 | 2.4e-05 s / 0.0061 (0.0337) | 6.5e-05 s / 0.0031 (0.0325) | 4.6e-02 s / 0.0000 (0.0001) | 1.4e-01 s / 0.0000 (0.0000) | 1.2e-04 s / 0.0015 (0.0276) | 5.6e-07 s / 0.0523 (0.2549) |
45
+ | 8 | 0.097 | 4.0e-05 s / 0.0048 (0.0379) | 9.1e-05 s / 0.0022 (0.0134) | impractical past d = 7 | 1.1e+00 s / 0.0000 (0.0000) | 1.8e-04 s / 0.0015 (0.0103) | 8.1e-07 s / 0.0249 (0.1426) |
46
+ | 12 | 0.025 | 6.4e-05 s / 0.0034 (0.0431) | 1.6e-04 s / 0.0024 (0.0215) | impractical past d = 7 | 1.6e+00 s / 0.0000 (0.0001) | 2.5e-04 s / 0.0007 (0.0139) | 1.3e-06 s / 0.0143 (0.1855) |
47
+ | 16 | 0.013 | 8.4e-05 s / 0.0038 (0.0451) | 2.1e-04 s / 0.0029 (0.0452) | impractical past d = 7 | 1.8e+00 s / 0.0000 (0.0000) | 3.2e-04 s / 0.0010 (0.0151) | 1.8e-06 s / 0.0107 (0.1848) |
48
+
49
+ R `mvProbit` was not run (see below).
50
+
51
+ The reference itself took 0.24 to 1.6 s per row on the equicorrelated Σ and
52
+ 0.24 to 20 s per row on the pairwise-shaped one. At d = 16 on the
53
+ equicorrelated Σ most pattern probabilities are small (median 0.0024), so the
54
+ maximum error says more than the median there.
55
+
56
+ ## Reading the tables
57
+
58
+ - **statsmodels** fits one `Probit` per outcome and has no correlation matrix,
59
+ so its pattern probability is a product of marginals. Its error is the cost
60
+ of ignoring Σ, not of numerical integration, and it does not shrink with
61
+ more data. Its time covers prediction only.
62
+ - **`orthant`** scores all rows in one compiled call.
63
+ - **`resolution="high"`** was the fastest of the integrating methods at every
64
+ d on both Σ shapes: 3 to 6 times faster than pybhatlib, and 330 to 440 times
65
+ faster than SciPy on the equicorrelated Σ. Median error 0.0001 or less on the
66
+ equicorrelated Σ (maximum up to 0.0007, against pybhatlib's 0.0009 to
67
+ 0.0036). On the pairwise-shaped Σ, median error 0.0006 to 0.0061, maximum up
68
+ to 0.045, the largest of the integrating methods.
69
+ - **`resolution="highest"`** costs 2.2 to 3.1 times `"high"`'s time and is
70
+ still 1.4 to 2 times faster than pybhatlib. On the equicorrelated Σ its
71
+ median and maximum error are below 0.00005 at every d. On the
72
+ pairwise-shaped Σ its median error is 0.0001 to 0.0031 (`"high"`: 0.0006 to
73
+ 0.0061) and its maximum up to 0.045; pybhatlib's median there is 0.0004 to
74
+ 0.0015.
75
+ - **pybhatlib** scores with Bhat's analytic approximation (`mvncd_batch`,
76
+ `method="ovus"`), one row at a time in Python. It is 70 to 105 times faster
77
+ than SciPy on the equicorrelated Σ. On the pairwise-shaped Σ its median error
78
+ is 0.0004 to 0.0015 and its maximum 0.0047 to 0.028. pybhatlib also has a PyTorch backend that runs on
79
+ your own GPU, but in pybhatlib 0.4.0 `mvncd` converts its inputs to NumPy
80
+ and `mvncd_batch` loops over rows in Python, so the GPU does not speed up
81
+ this scoring.
82
+ - **SciPy** stays accurate on the pairwise-shaped Σ but slows sharply, to
83
+ 1.1 to 1.8 s per row from d = 8. That is 54 to 69 times its time on the
84
+ equicorrelated Σ at the same d.
85
+ - **`quadrature`** matched the reference on both shapes up to d = 5.
86
+ - **R `mvProbit`** was not run: R is not installed on the benchmark machine,
87
+ and the notebook has no R column yet.
88
+
89
+ ## What this does not establish
90
+
91
+ - Two Σ shapes, one seed, and 100 rows per d. The pairwise-shaped Σ is one
92
+ construction of a near-singular matrix; see [studies/ghk.md](studies/ghk.md)
93
+ and [studies/comparators.md](studies/comparators.md) for more.
94
+ - Scoring only. Fitting time against full-likelihood estimation is in
95
+ [studies/comparators.md](studies/comparators.md).
@@ -19,6 +19,27 @@ model.stderr_robust_ # sandwich standard errors
19
19
  model.predict_proba(X_new) # runs locally, on any evaluator
20
20
  ```
21
21
 
22
+ ## What changes, and what doesn't
23
+
24
+ Only `fitter="modal"` changes. After `fit`, the model is used exactly as an
25
+ IFM fit is: `transform`, `predict_proba` and its joint queries, `joint_proba`,
26
+ `score`, `sample`, `correlation_`, `inner_models_[j].coef_`, `get_params` /
27
+ `set_params`. Prediction runs locally on the chosen `evaluator`.
28
+
29
+ The differences:
30
+
31
+ - `inner` must be `"linear"`; `inner_params` may set only `alpha`. Anything
32
+ else raises.
33
+ - `dependence`, `cv`, `optimizer` and `project_correlation` do not apply.
34
+ - `fit` uploads X, Y and `sample_weight` and waits for the service (see
35
+ [Cost](#cost)).
36
+ - Extra attributes: `stderr_`, `stderr_robust_`, `cov_params_`,
37
+ `cov_params_robust_`, `param_names_`, `fit_result_`.
38
+ - `eta_` holds in-sample indices (there is no cross-fitting); `nll_` is the
39
+ simulated joint negative log-likelihood; `optimize_result_` is `None`.
40
+ - An int `random_state` seeds the Sobol draws; otherwise the seed is 0, so
41
+ repeated fits agree.
42
+
22
43
  ## When to use it instead of IFM
23
44
 
24
45
  - You need **standard errors** for the coefficients and for Σ. IFM does not
@@ -6,6 +6,11 @@ without measuring it. For *scoring* a fitted model, where that objection does
6
6
  not apply, how does GHK compare with the compiled `orthant` backend in
7
7
  accuracy and speed?
8
8
 
9
+ > **Version note (2026-10).** This study measured the backend shipped in 0.2.1. Later builds integrate
10
+ > `resolution="high"` on a finer rule and add `resolution="highest"`, which is more accurate at a few times the
11
+ > cost. The tables below were not rerun; current numbers for both settings are in
12
+ > [benchmarks.md](../benchmarks.md).
13
+
9
14
  ## Summary
10
15
 
11
16
  | | accuracy | speed (s per row) |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "multivariate-probit"
7
- version = "0.3.0"
7
+ version = "0.3.2"
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.0"
37
+ __version__ = "0.3.2"
38
38
 
39
39
  __all__ = [
40
40
  "MultivariateProbit",
@@ -165,8 +165,17 @@ class MultivariateProbit:
165
165
  n_quad : int, default=24
166
166
  Gauss-Legendre order for ``evaluator="quadrature"``. Lower it if
167
167
  fitting with many outcomes gets slow.
168
- resolution : {"high", "low"}, default="high"
169
- Passed to ``evaluator="orthant"``; ignored otherwise.
168
+ resolution : {"high", "highest", "smooth", "low"}, default="high"
169
+ Passed to ``evaluator="orthant"``; ignored otherwise. ``"highest"``
170
+ is the most accurate setting, at about 2-3 times the time of
171
+ ``"high"``: in docs/benchmarks.md its median error is under 0.00005 on
172
+ an equicorrelated covariance (``"high"``: up to 0.0001) and
173
+ 0.0001-0.0031 on a near-singular one (``"high"``: 0.0006-0.0061).
174
+ ``"smooth"`` costs the same as ``"highest"`` and is continuous in the
175
+ parameters (no threshold where the method changes, near-duplicates
176
+ included), for gradient-based fitting; it is not uniformly more
177
+ accurate than ``"high"``. ``"low"`` is the cheapest. ``"highest"`` and
178
+ ``"smooth"`` are not available in 0.3.0 and earlier.
170
179
  optimizer : str, default="Nelder-Mead"
171
180
  Passed to ``scipy.optimize.minimize`` for ``dependence="joint"``.
172
181
  Derivative-free by design: the orthant likelihood has no convenient
@@ -5,10 +5,17 @@ On import this binds ``orthant.cdf`` to one of two binaries:
5
5
  * a valid key present -> the paid binary is decrypted in memory and bound;
6
6
  full accuracy, unlimited dimension.
7
7
  * no key / bad key -> the plaintext free binary is bound (n <= 3,
8
- resolution="low" only) and a one-time notice is printed to stderr.
8
+ resolution="low" only) and a one-time notice is printed to stderr on the
9
+ first call.
9
10
 
10
- The key is read from ``$ORTHANT_KEY`` or ``~/.orthant/key``. Get one at
11
- https://quantecarlo.com/orthant_key
11
+ The key is read from ``$ORTHANT_KEY`` or ``~/.orthant/key`` at import, or
12
+ passed at any time with ``activate(key)`` (no environment needed, no restart):
13
+
14
+ >>> from multivariate_probit import orthant
15
+ >>> orthant.activate("my-key")
16
+ 'paid'
17
+
18
+ Get a key at https://quantecarlo.com/orthant_key
12
19
  """
13
20
  import glob
14
21
  import importlib.machinery
@@ -18,7 +25,7 @@ import struct
18
25
  import sys
19
26
  import tempfile
20
27
 
21
- __all__ = ["cdf"]
28
+ __all__ = ["cdf", "activate"]
22
29
 
23
30
  _URL = "https://quantecarlo.com/orthant_key"
24
31
  _DIR = os.path.dirname(os.path.abspath(__file__))
@@ -26,6 +33,7 @@ _MAGIC = b"ORT1"
26
33
  _SCRYPT_N, _SCRYPT_R, _SCRYPT_P = 2 ** 14, 8, 1
27
34
 
28
35
  tier = "free" # set to "paid" when the paid binary is bound
36
+ _notice = None # why we are in free mode; printed once, on the first free call
29
37
 
30
38
 
31
39
  def _read_key():
@@ -52,8 +60,12 @@ def _import_extension(data, modname):
52
60
  fd = os.memfd_create(modname)
53
61
  try:
54
62
  os.write(fd, data)
63
+ path = "/proc/self/fd/%d" % fd
64
+ # The path has no .so suffix, so the loader must be given
65
+ # explicitly; inferred from the path, the spec would be None.
55
66
  spec = importlib.util.spec_from_file_location(
56
- qualified, "/proc/self/fd/%d" % fd)
67
+ qualified, path,
68
+ loader=importlib.machinery.ExtensionFileLoader(qualified, path))
57
69
  mod = importlib.util.module_from_spec(spec)
58
70
  spec.loader.exec_module(mod)
59
71
  return mod
@@ -124,18 +136,55 @@ def _warn(msg):
124
136
  pass
125
137
 
126
138
 
139
+ def _free_cdf(free):
140
+ """The free binary's cdf, printing the free-mode notice on its first call."""
141
+ def cdf(*args, **kwargs):
142
+ global _notice
143
+ if _notice:
144
+ _warn(_notice)
145
+ _notice = None
146
+ return free(*args, **kwargs)
147
+ cdf.__doc__ = free.__doc__
148
+ return cdf
149
+
150
+
151
+ def activate(key):
152
+ """Bind the paid binary with ``key``, at any time; return ``"paid"``.
153
+
154
+ Unlike the import-time lookup this never falls back: a wrong key raises
155
+ ``ValueError`` and a binary that cannot load here raises ``ImportError``,
156
+ leaving the current binding unchanged.
157
+ """
158
+ global tier, _notice
159
+ if not isinstance(key, str) or not key.strip():
160
+ raise ValueError("orthant: activate() needs a non-empty key string")
161
+ try:
162
+ data = _decrypt_paid(key.strip(), _tag())
163
+ except ImportError as e:
164
+ if (e.name or "").startswith("cryptography"):
165
+ raise ImportError("orthant: activate() needs the cryptography package: "
166
+ "pip install cryptography (or multivariate-probit[orthant])",
167
+ name=e.name) from None
168
+ raise
169
+ except Exception as e:
170
+ raise ValueError("orthant: key not accepted by this binary (%s). "
171
+ "Renew or get a key: %s" % (type(e).__name__, _URL)) from None
172
+ globals()["cdf"] = _import_extension(data, "_ocore").cdf
173
+ tier, _notice = "paid", None
174
+ return tier
175
+
176
+
127
177
  def _bind():
128
- """Pick a binary, bind cdf, and return the tier."""
178
+ """Pick a binary at import, bind cdf, and return the tier."""
179
+ global _notice
129
180
  tag = _tag()
130
181
  key = _read_key()
131
182
  if key is not None:
132
183
  try:
133
- mod = _import_extension(_decrypt_paid(key, tag), "_ocore")
184
+ data = _decrypt_paid(key, tag)
134
185
  except ImportError as e:
135
- # Not a key problem: a missing dependency or a binary that won't
136
- # load on this platform. Say so rather than blaming the key.
137
186
  notice = ("orthant: key found but the paid binary could not be "
138
- "loaded (%s) -- running in FREE mode (n<=3, "
187
+ "decrypted (%s) -- running in FREE mode (n<=3, "
139
188
  "resolution='low').\n" % e)
140
189
  if (e.name or "").startswith("cryptography"):
141
190
  notice += " pip install cryptography\n"
@@ -144,15 +193,24 @@ def _bind():
144
193
  "mode (n<=3, resolution='low').\n"
145
194
  " Renew or get a key: %s\n" % _URL)
146
195
  else:
147
- globals()["cdf"] = mod.cdf
148
- return "paid"
196
+ try:
197
+ globals()["cdf"] = _import_extension(data, "_ocore").cdf
198
+ return "paid"
199
+ except Exception as e:
200
+ # The key was right; loading the binary failed. Say so
201
+ # rather than blaming the key.
202
+ notice = ("orthant: key accepted but the paid binary could not "
203
+ "be loaded (%s: %s) -- running in FREE mode (n<=3, "
204
+ "resolution='low').\n" % (type(e).__name__, e))
149
205
  else:
150
- notice = ("orthant: FREE mode -- n<=3 and resolution='low' only.\n"
151
- " For full accuracy and higher dimensions, get a "
152
- "key: %s\n" % _URL)
153
-
154
- globals()["cdf"] = _load_free(tag).cdf
155
- _warn(notice)
206
+ notice = ("orthant: FREE mode -- n<=3 and resolution='low' only. No key "
207
+ "in $ORTHANT_KEY or ~/.orthant/key when this module was "
208
+ "imported;\n"
209
+ " pass one with orthant.activate(key). Get a key: "
210
+ "%s\n" % _URL)
211
+
212
+ globals()["cdf"] = _free_cdf(_load_free(tag).cdf)
213
+ _notice = notice
156
214
  return "free"
157
215
 
158
216
 
@@ -156,3 +156,56 @@ def test_orthant_with_key_runs_above_three(tmp_path):
156
156
  tmp_path=tmp_path,
157
157
  )
158
158
  assert out.returncode == 0, out.stderr
159
+
160
+
161
+ @orthant_built
162
+ def test_orthant_activate_rejects_a_wrong_key_and_stays_free(tmp_path):
163
+ out = _run_orthant(
164
+ """
165
+ from multivariate_probit import orthant
166
+ assert orthant.tier == "free"
167
+ try:
168
+ orthant.activate("not-a-key")
169
+ except ValueError as exc:
170
+ print(exc)
171
+ else:
172
+ raise SystemExit("no error")
173
+ assert orthant.tier == "free"
174
+ eta, Y, corr = problem(3)
175
+ pattern_prob(eta, Y, corr, evaluator="orthant", resolution="low")
176
+ """,
177
+ tmp_path=tmp_path,
178
+ )
179
+ assert out.returncode == 0, out.stderr
180
+ assert "key not accepted" in out.stdout
181
+ assert out.stderr.count("FREE mode") == 1 # the notice comes once, at first use
182
+
183
+
184
+ @orthant_built
185
+ @pytest.mark.skipif(not os.environ.get("ORTHANT_KEY"), reason="needs $ORTHANT_KEY")
186
+ def test_orthant_activate_after_import_without_environment(tmp_path):
187
+ out = _run_orthant(
188
+ f"""
189
+ from multivariate_probit import orthant
190
+ assert orthant.tier == "free"
191
+ assert orthant.activate({os.environ["ORTHANT_KEY"]!r}) == "paid"
192
+ eta, Y, corr = problem(5)
193
+ got = pattern_prob(eta, Y, corr, evaluator="orthant", resolution="highest")
194
+ ref = pattern_prob(eta, Y, corr)
195
+ assert np.abs(got - ref).max() < 1e-2, np.abs(got - ref).max()
196
+ """,
197
+ tmp_path=tmp_path,
198
+ )
199
+ assert out.returncode == 0, out.stderr
200
+ assert "FREE mode" not in out.stderr
201
+
202
+
203
+ @orthant_built
204
+ def test_orthant_loads_an_extension_from_memory():
205
+ # The paid binary is loaded from bytes. Where os.memfd_create exists the
206
+ # path has no .so suffix, which once left the spec without a loader.
207
+ from multivariate_probit.orthant import _import_extension, _tag
208
+
209
+ with open(ORTHANT_DIR / f"_ofree.{_tag()}.so", "rb") as f:
210
+ mod = _import_extension(f.read(), "_ofree")
211
+ assert callable(mod.cdf)
@@ -1,55 +0,0 @@
1
- sigma,d,N,method,median_err,max_err,s_per_row
2
- equi,3,100,mvp quadrature,1.3373382605444406e-05,5.564767189014663e-05,6.151222012704238e-05
3
- equi,3,100,mvp orthant (high),5.520783257679379e-05,0.0001977558612211161,9.499299922026694e-06
4
- equi,3,100,SciPy default,1.7085000221517266e-06,2.872515295393452e-05,0.0042396432999521495
5
- equi,3,100,pybhatlib ovus,0.00011229346488104974,0.0008524556947959311,6.468125007813797e-05
6
- equi,3,100,statsmodels,0.028443441360488153,0.09919722660040897,5.445600254461169e-07
7
- equi,3,100,reference,0.0,0.0,0.2536748156798421
8
- equi,5,100,mvp quadrature,2.0314513939401896e-05,0.00011324180994054522,0.03316033348994097
9
- equi,5,100,mvp orthant (high),8.582073115437003e-05,0.0003006023566725635,1.3224330032244325e-05
10
- equi,5,100,SciPy default,1.3144313745008795e-06,1.410367111304156e-05,0.0065609772101743145
11
- equi,5,100,pybhatlib ovus,0.00039917378471633624,0.0019016346577496457,9.364616998936981e-05
12
- equi,5,100,statsmodels,0.024622620503874038,0.12870241315304662,6.427397602237761e-07
13
- equi,5,100,reference,0.0,0.0,0.5339089114402304
14
- equi,8,100,mvp orthant (high),7.170267413460912e-05,0.00047284481673742773,2.604458015412092e-05
15
- equi,8,100,SciPy default,1.0657093905339088e-06,7.46774341275902e-06,0.013163221169961617
16
- equi,8,100,pybhatlib ovus,0.0003370945286850363,0.0023083975100191556,0.00014096851024078205
17
- equi,8,100,statsmodels,0.014453125416131472,0.11538392212916126,1.2416898971423507e-06
18
- equi,8,100,reference,0.0,0.0,1.4400106923800196
19
- equi,12,100,mvp orthant (high),5.782104286452053e-05,0.0007241255415409431,4.110162000870332e-05
20
- equi,12,100,SciPy default,3.4970988831498073e-07,4.815527437126987e-06,0.018762066540075464
21
- equi,12,100,pybhatlib ovus,0.0002047092197251764,0.0035788660943063266,0.00019040222017792984
22
- equi,12,100,statsmodels,0.006303004648829415,0.07762653255283464,1.182040141429752e-06
23
- equi,12,100,reference,0.0,0.0,1.6423227695701643
24
- equi,16,100,mvp orthant (high),1.5439688762976245e-05,0.0006260612582136282,6.588060990907252e-05
25
- equi,16,100,SciPy default,7.660225022907471e-08,7.498227887592357e-06,0.026153904990060255
26
- equi,16,100,pybhatlib ovus,1.6800734753088424e-05,0.0011253768512156778,0.00026122707000467924
27
- equi,16,100,statsmodels,0.0014514737962008938,0.05355898580397629,2.2758700652047993e-06
28
- equi,16,100,reference,0.0,0.0,1.1454083026899025
29
- pairwise,3,100,mvp quadrature,4.975128970532983e-06,8.276622284841828e-05,6.013165984768421e-05
30
- pairwise,3,100,mvp orthant (high),0.0007572622538304392,0.0028934841345368834,9.238499915227294e-06
31
- pairwise,3,100,SciPy default,1.0863552671880328e-06,1.1356497155445844e-05,0.006485114450042601
32
- pairwise,3,100,pybhatlib ovus,0.00037654161082817383,0.004713394298326956,6.38040600460954e-05
33
- pairwise,3,100,statsmodels,0.037060543222106224,0.15997644750429899,4.1702005546540024e-07
34
- pairwise,3,100,reference,0.0,0.0,0.24052267621998907
35
- pairwise,5,100,mvp quadrature,5.607947987679163e-06,6.266772635254991e-05,0.03469973466999363
36
- pairwise,5,100,mvp orthant (high),0.006131866821483319,0.033843011353965835,1.3460909831337631e-05
37
- pairwise,5,100,SciPy default,2.0348250380219923e-06,8.649425807485955e-06,0.10544797601993196
38
- pairwise,5,100,pybhatlib ovus,0.0014663128072703105,0.02764332982515838,9.492035984294489e-05
39
- pairwise,5,100,statsmodels,0.052292238612919734,0.2549403651447862,5.712499842047691e-07
40
- pairwise,5,100,reference,0.0,0.0,1.2560263323498657
41
- pairwise,8,100,mvp orthant (high),0.0047540407934710425,0.037869385076046036,2.4516569974366577e-05
42
- pairwise,8,100,SciPy default,1.8501012514909876e-06,1.4681123618909275e-05,0.8790659057398443
43
- pairwise,8,100,pybhatlib ovus,0.0015355099588129813,0.010313908094296734,0.00013880397018510847
44
- pairwise,8,100,statsmodels,0.024908203618945243,0.1426044668035415,9.262599633075297e-07
45
- pairwise,8,100,reference,0.0,0.0,4.434727118879964
46
- pairwise,12,100,mvp orthant (high),0.003427530742914099,0.04306180419633249,3.916209010640159e-05
47
- pairwise,12,100,SciPy default,1.6616917820439989e-06,2.8483187402228438e-05,1.20114108054986
48
- pairwise,12,100,pybhatlib ovus,0.0007431598443556342,0.01388904489935694,0.00019705019018147142
49
- pairwise,12,100,statsmodels,0.014263734107914516,0.1854686635860365,1.1522200657054783e-06
50
- pairwise,12,100,reference,0.0,0.0,9.518902401560045
51
- pairwise,16,100,mvp orthant (high),0.00374560158932665,0.045287863431270364,5.415496008936316e-05
52
- pairwise,16,100,SciPy default,1.8019881021374973e-06,9.824805690655108e-06,1.8181418986897915
53
- pairwise,16,100,pybhatlib ovus,0.0009786447220386783,0.015062755675490355,0.0002470732299843803
54
- pairwise,16,100,statsmodels,0.010651175036494289,0.18479811475521019,1.4770901179872453e-06
55
- pairwise,16,100,reference,0.0,0.0,19.51646036452992
@@ -1,90 +0,0 @@
1
- # Benchmarks against other tools
2
-
3
- **Task.** Score `P(Y = y | x)`, the probability of the pattern each held-out
4
- row actually has, from one fitted model. Synthetic data: 6 features, linear
5
- margins, 3,000 training rows, 100 scored rows per d. The model is
6
- `MultivariateProbit(inner="linear", dependence="pairwise", random_state=0)`.
7
- Every column scores that same fitted model (η̂, Σ̂), except statsmodels.
8
-
9
- There are two shapes of true Σ:
10
-
11
- - **Equicorrelated**, ρ = 0.4: the easy case.
12
- - **Pairwise-shaped**: a rank-2 factor plus noise, projected to the nearest
13
- correlation matrix (smallest eigenvalue about 1e-8), the same construction
14
- as [studies/ghk.md](studies/ghk.md). A fitted pairwise Σ̂ lands on that PSD
15
- boundary, which is hard for every approximation.
16
-
17
- **Reference.** SciPy `multivariate_normal.cdf` with `abseps=1e-7`,
18
- `releps=1e-6`, one row at a time. Error is the absolute difference from it in
19
- probability: median over rows, maximum in parentheses. Time is wall-clock per
20
- row after one warm-up call, on a two-core x86-64 Linux machine.
21
-
22
- Reproduce with [benchmarks/benchmarks.ipynb](../benchmarks/benchmarks.ipynb)
23
- (logic in `benchmarks/bench.py`; raw numbers in `benchmarks/results.csv`).
24
- The reference values are cached in `benchmarks/ref_*.npz` together with the
25
- problem they were computed for, so a rerun skips the slow part and a changed
26
- problem recomputes. The `orthant` column needs a key in `$ORTHANT_KEY` or
27
- `~/.orthant/key`.
28
-
29
- ### Equicorrelated Σ (ρ = 0.4)
30
-
31
- | d | median p | multivariate-probit `orthant` (`resolution="high"`) | multivariate-probit `quadrature` | SciPy, default tolerances | pybhatlib (Bhat OVUS) | statsmodels |
32
- | --- | --- | --- | --- | --- | --- | --- |
33
- | 3 | 0.348 | 9.5e-6 s / 0.0001 (0.0002) | 6.2e-5 s / 0.0000 (0.0001) | 4.2e-3 s / 0.0000 (0.0000) | 6.5e-5 s / 0.0001 (0.0009) | 5.4e-7 s / 0.0284 (0.0992) |
34
- | 5 | 0.154 | 1.3e-5 s / 0.0001 (0.0003) | 3.3e-2 s / 0.0000 (0.0001) | 6.6e-3 s / 0.0000 (0.0000) | 9.4e-5 s / 0.0004 (0.0019) | 6.4e-7 s / 0.0246 (0.1287) |
35
- | 8 | 0.101 | 2.6e-5 s / 0.0001 (0.0005) | impractical past d = 7 | 1.3e-2 s / 0.0000 (0.0000) | 1.4e-4 s / 0.0003 (0.0023) | 1.2e-6 s / 0.0145 (0.1154) |
36
- | 12 | 0.022 | 4.1e-5 s / 0.0001 (0.0007) | impractical past d = 7 | 1.9e-2 s / 0.0000 (0.0000) | 1.9e-4 s / 0.0002 (0.0036) | 1.2e-6 s / 0.0063 (0.0776) |
37
- | 16 | 0.0024 | 6.6e-5 s / 0.0000 (0.0006) | impractical past d = 7 | 2.6e-2 s / 0.0000 (0.0000) | 2.6e-4 s / 0.0000 (0.0011) | 2.3e-6 s / 0.0015 (0.0536) |
38
-
39
- ### Pairwise-shaped Σ (near-singular)
40
-
41
- | d | median p | multivariate-probit `orthant` (`resolution="high"`) | multivariate-probit `quadrature` | SciPy, default tolerances | pybhatlib (Bhat OVUS) | statsmodels |
42
- | --- | --- | --- | --- | --- | --- | --- |
43
- | 3 | 0.431 | 9.2e-6 s / 0.0008 (0.0029) | 6.0e-5 s / 0.0000 (0.0001) | 6.5e-3 s / 0.0000 (0.0000) | 6.4e-5 s / 0.0004 (0.0047) | 4.2e-7 s / 0.0371 (0.1600) |
44
- | 5 | 0.171 | 1.3e-5 s / 0.0061 (0.0338) | 3.5e-2 s / 0.0000 (0.0001) | 1.1e-1 s / 0.0000 (0.0000) | 9.5e-5 s / 0.0015 (0.0276) | 5.7e-7 s / 0.0523 (0.2549) |
45
- | 8 | 0.097 | 2.5e-5 s / 0.0048 (0.0379) | impractical past d = 7 | 8.8e-1 s / 0.0000 (0.0000) | 1.4e-4 s / 0.0015 (0.0103) | 9.3e-7 s / 0.0249 (0.1426) |
46
- | 12 | 0.025 | 3.9e-5 s / 0.0034 (0.0431) | impractical past d = 7 | 1.2e+0 s / 0.0000 (0.0000) | 2.0e-4 s / 0.0007 (0.0139) | 1.2e-6 s / 0.0143 (0.1855) |
47
- | 16 | 0.013 | 5.4e-5 s / 0.0037 (0.0453) | impractical past d = 7 | 1.8e+0 s / 0.0000 (0.0000) | 2.5e-4 s / 0.0010 (0.0151) | 1.5e-6 s / 0.0107 (0.1848) |
48
-
49
- R `mvProbit` was not run (see below).
50
-
51
- The reference itself took 0.24 to 1.6 s per row on the equicorrelated Σ and
52
- 0.24 to 20 s per row on the pairwise-shaped one. At d = 16 on the
53
- equicorrelated Σ most pattern probabilities are small (median 0.0024), so the
54
- maximum error says more than the median there.
55
-
56
- ## Reading the tables
57
-
58
- - **statsmodels** fits one `Probit` per outcome and has no correlation matrix,
59
- so its pattern probability is a product of marginals. Its error is the cost
60
- of ignoring Σ, not of numerical integration, and it does not shrink with
61
- more data. Its time covers prediction only.
62
- - **`orthant`** scores all rows in one compiled call. It was the fastest of
63
- the integrating methods at every d on both Σ shapes: 4 to 7 times faster
64
- than pybhatlib, and 390 to 510 times faster than SciPy on the
65
- equicorrelated Σ. On the equicorrelated Σ it also had the smallest maximum
66
- error of the approximations. On the pairwise-shaped Σ it was the *least*
67
- accurate integrating method: median error 0.0008 to 0.0061, maximum up to
68
- 0.045.
69
- - **pybhatlib** scores with Bhat's analytic approximation (`mvncd_batch`,
70
- `method="ovus"`), one row at a time in Python. It is 65 to 100 times faster
71
- than SciPy on the equicorrelated Σ. On the pairwise-shaped Σ its median error
72
- (0.0004 to 0.0015) was 2 to 5 times smaller than `orthant`'s, and so was its
73
- maximum from d = 5 up. pybhatlib also has a PyTorch backend that runs on
74
- your own GPU, but in pybhatlib 0.4.0 `mvncd` converts its inputs to NumPy
75
- and `mvncd_batch` loops over rows in Python, so the GPU does not speed up
76
- this scoring.
77
- - **SciPy** stays accurate on the pairwise-shaped Σ but slows sharply, to
78
- 0.9 to 1.8 s per row from d = 8. That is 65 to 70 times its time on the
79
- equicorrelated Σ at the same d.
80
- - **`quadrature`** matched the reference on both shapes up to d = 5.
81
- - **R `mvProbit`** was not run: R is not installed on the benchmark machine,
82
- and the notebook has no R column yet.
83
-
84
- ## What this does not establish
85
-
86
- - Two Σ shapes, one seed, and 100 rows per d. The pairwise-shaped Σ is one
87
- construction of a near-singular matrix; see [studies/ghk.md](studies/ghk.md)
88
- and [studies/comparators.md](studies/comparators.md) for more.
89
- - Scoring only. Fitting time against full-likelihood estimation is in
90
- [studies/comparators.md](studies/comparators.md).