multivariate-probit 0.2.3__tar.gz → 0.3.1__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.
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/CITATION.cff +2 -2
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/PKG-INFO +21 -8
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/README.md +20 -7
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/benchmarks/bench.py +2 -2
- multivariate_probit-0.3.1/benchmarks/results.csv +65 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/api.md +15 -6
- multivariate_probit-0.3.1/docs/benchmarks.md +95 -0
- multivariate_probit-0.3.1/docs/fiml.md +96 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/limitations.md +2 -1
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/studies/ghk.md +5 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/pyproject.toml +1 -1
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/src/multivariate_probit/__init__.py +3 -2
- multivariate_probit-0.3.1/src/multivariate_probit/_remote.py +81 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/src/multivariate_probit/model.py +137 -6
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/src/multivariate_probit/orthant/__init__.py +60 -11
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/tests/test_evaluators.py +42 -0
- multivariate_probit-0.3.1/tests/test_modal_fit.py +192 -0
- multivariate_probit-0.2.3/benchmarks/results.csv +0 -55
- multivariate_probit-0.2.3/docs/benchmarks.md +0 -90
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/.github/ISSUE_TEMPLATE/use-case.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/.gitignore +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/LICENSE +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/benchmarks/benchmarks.ipynb +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/benchmarks/ref_d12_n3000_100_rho0.4_s0.npz +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/benchmarks/ref_d16_n3000_100_rho0.4_s0.npz +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/benchmarks/ref_d3_n3000_100_rho0.4_s0.npz +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/benchmarks/ref_d5_n3000_100_rho0.4_s0.npz +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/benchmarks/ref_d8_n3000_100_rho0.4_s0.npz +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/benchmarks/ref_pairwise_d12_n3000_100_s0.npz +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/benchmarks/ref_pairwise_d16_n3000_100_s0.npz +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/benchmarks/ref_pairwise_d3_n3000_100_s0.npz +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/benchmarks/ref_pairwise_d5_n3000_100_s0.npz +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/benchmarks/ref_pairwise_d8_n3000_100_s0.npz +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/ifm.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/implementation.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/studies/README.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/studies/comparators.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/studies/index-correlation-shortcut.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/studies/log-score-evaluator.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/studies/margin-gaps.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/studies/mulan-multilabel.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/studies/multilabel-benchmarks.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/studies/sparse-label-transfer.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/studies/uci-credit.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/use-cases/README.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/use-cases/claims-fast-track.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/use-cases/content-moderation.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/use-cases/cyber.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/use-cases/fraud-vendor-fusion.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/use-cases/hcc-codes.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/docs/use-cases/property-imagery.md +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/hatch_build.py +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/src/multivariate_probit/_corr.py +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/src/multivariate_probit/_mvn.py +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/src/multivariate_probit/ifm.py +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/src/multivariate_probit/inner.py +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/src/multivariate_probit/linear.py +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/src/multivariate_probit/results.py +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/tests/conftest.py +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/tests/test_calibration.py +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/tests/test_conditional.py +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/tests/test_contract.py +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/tests/test_correlation_projection.py +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/tests/test_linear.py +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/tests/test_rf.py +0 -0
- {multivariate_probit-0.2.3 → multivariate_probit-0.3.1}/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.
|
|
10
|
-
|
|
9
|
+
version: 0.3.0
|
|
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
|
+
Version: 0.3.1
|
|
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
|
|
@@ -101,7 +101,7 @@ Using this on a real problem?
|
|
|
101
101
|
- Your **multilabel** or **multi-output** model treats labels as independent,
|
|
102
102
|
and you need the **joint probability** of a label combination.
|
|
103
103
|
- You've used R `mvProbit` or Stata `mvprobit` and want **multivariate probit in
|
|
104
|
-
Python
|
|
104
|
+
Python**, with standard errors from a full-likelihood fit or prediction at scale.
|
|
105
105
|
- You want to **combine several fraud or risk model scores** into one
|
|
106
106
|
probability that "any of these happened."
|
|
107
107
|
- You need **co-occurrence** or **label dependence** expressed as one
|
|
@@ -129,6 +129,9 @@ features it is the tetrachoric correlation conditional on x. Fitting is
|
|
|
129
129
|
two-stage **Inference Functions for Margins** (IFM): each margin is fitted
|
|
130
130
|
alone and cross-fitted, then Σ is estimated by maximum likelihood with the
|
|
131
131
|
margins held fixed ([docs/ifm.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/ifm.md)).
|
|
132
|
+
For linear margins, `fitter="modal"` is the alternative: full maximum
|
|
133
|
+
likelihood, all margins and Σ at once, on a hosted GPU, with standard errors
|
|
134
|
+
([docs/fiml.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/fiml.md)).
|
|
132
135
|
|
|
133
136
|
## Why not write it yourself
|
|
134
137
|
|
|
@@ -166,8 +169,11 @@ wrong; each is handled here and documented.
|
|
|
166
169
|
explicit and readable in `correlation_`, but each margin is as much a black
|
|
167
170
|
box as the classifier inside it. SHAP is the tool for explaining a single
|
|
168
171
|
black-box model; this package does not make one explainable.
|
|
169
|
-
- **
|
|
170
|
-
|
|
172
|
+
- **Standard errors with `fitter="modal"` only.** The IFM fit gives point
|
|
173
|
+
estimates
|
|
174
|
+
([limitations](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/limitations.md));
|
|
175
|
+
the full-likelihood fit returns `stderr_` and `cov_params_` for the
|
|
176
|
+
coefficients and Σ.
|
|
171
177
|
|
|
172
178
|
## Bring your own model, or use ours
|
|
173
179
|
|
|
@@ -207,15 +213,21 @@ The backend is chosen with `evaluator=`:
|
|
|
207
213
|
of magnitude faster than SciPy, at roughly the accuracy of 100-draw GHK
|
|
208
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
|
|
209
215
|
and 3.12 on x86-64 Linux (`pip install multivariate-probit[orthant]`); without
|
|
210
|
-
a key it runs d ≤ 3 at `resolution="low"`.
|
|
211
|
-
|
|
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).
|
|
212
220
|
|
|
213
221
|
Benchmarks against other tools: [docs/benchmarks.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/benchmarks.md).
|
|
214
222
|
|
|
215
223
|
## Where your data goes
|
|
216
224
|
|
|
217
|
-
Nowhere. Fitting and every `evaluator` run on your machine; the
|
|
218
|
-
no network calls, and the `"orthant"` key is checked offline.
|
|
225
|
+
Nowhere, by default. Fitting and every `evaluator` run on your machine; the
|
|
226
|
+
package makes no network calls, and the `"orthant"` key is checked offline.
|
|
227
|
+
|
|
228
|
+
The exception is opt-in: `fitter="modal"` uploads X, Y and `sample_weight` to
|
|
229
|
+
a GPU service for the full-likelihood fit and gets parameters back. Everything
|
|
230
|
+
after `fit` runs locally.
|
|
219
231
|
|
|
220
232
|
The one hosted path is a separate package you call yourself:
|
|
221
233
|
[`quantecarlo.orthant_cdf`](https://github.com/sign-of-fourier/quantecarlo#orthant-probabilities-orthant_cdf),
|
|
@@ -252,6 +264,7 @@ runnable example on synthetic data:
|
|
|
252
264
|
## Documentation
|
|
253
265
|
|
|
254
266
|
- [docs/ifm.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/ifm.md): the estimation algorithm, and why IFM
|
|
267
|
+
- [docs/fiml.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/fiml.md): the full-likelihood GPU fit (`fitter="modal"`), with standard errors
|
|
255
268
|
- [docs/api.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/api.md): parameters, evaluators, the inner-model contract
|
|
256
269
|
- [docs/implementation.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/implementation.md): what is hand-rolled, numerical accuracy
|
|
257
270
|
- [docs/limitations.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/limitations.md): known gaps
|
|
@@ -37,7 +37,7 @@ Using this on a real problem?
|
|
|
37
37
|
- Your **multilabel** or **multi-output** model treats labels as independent,
|
|
38
38
|
and you need the **joint probability** of a label combination.
|
|
39
39
|
- You've used R `mvProbit` or Stata `mvprobit` and want **multivariate probit in
|
|
40
|
-
Python
|
|
40
|
+
Python**, with standard errors from a full-likelihood fit or prediction at scale.
|
|
41
41
|
- You want to **combine several fraud or risk model scores** into one
|
|
42
42
|
probability that "any of these happened."
|
|
43
43
|
- You need **co-occurrence** or **label dependence** expressed as one
|
|
@@ -65,6 +65,9 @@ features it is the tetrachoric correlation conditional on x. Fitting is
|
|
|
65
65
|
two-stage **Inference Functions for Margins** (IFM): each margin is fitted
|
|
66
66
|
alone and cross-fitted, then Σ is estimated by maximum likelihood with the
|
|
67
67
|
margins held fixed ([docs/ifm.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/ifm.md)).
|
|
68
|
+
For linear margins, `fitter="modal"` is the alternative: full maximum
|
|
69
|
+
likelihood, all margins and Σ at once, on a hosted GPU, with standard errors
|
|
70
|
+
([docs/fiml.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/fiml.md)).
|
|
68
71
|
|
|
69
72
|
## Why not write it yourself
|
|
70
73
|
|
|
@@ -102,8 +105,11 @@ wrong; each is handled here and documented.
|
|
|
102
105
|
explicit and readable in `correlation_`, but each margin is as much a black
|
|
103
106
|
box as the classifier inside it. SHAP is the tool for explaining a single
|
|
104
107
|
black-box model; this package does not make one explainable.
|
|
105
|
-
- **
|
|
106
|
-
|
|
108
|
+
- **Standard errors with `fitter="modal"` only.** The IFM fit gives point
|
|
109
|
+
estimates
|
|
110
|
+
([limitations](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/limitations.md));
|
|
111
|
+
the full-likelihood fit returns `stderr_` and `cov_params_` for the
|
|
112
|
+
coefficients and Σ.
|
|
107
113
|
|
|
108
114
|
## Bring your own model, or use ours
|
|
109
115
|
|
|
@@ -143,15 +149,21 @@ The backend is chosen with `evaluator=`:
|
|
|
143
149
|
of magnitude faster than SciPy, at roughly the accuracy of 100-draw GHK
|
|
144
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
|
|
145
151
|
and 3.12 on x86-64 Linux (`pip install multivariate-probit[orthant]`); without
|
|
146
|
-
a key it runs d ≤ 3 at `resolution="low"`.
|
|
147
|
-
|
|
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).
|
|
148
156
|
|
|
149
157
|
Benchmarks against other tools: [docs/benchmarks.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/benchmarks.md).
|
|
150
158
|
|
|
151
159
|
## Where your data goes
|
|
152
160
|
|
|
153
|
-
Nowhere. Fitting and every `evaluator` run on your machine; the
|
|
154
|
-
no network calls, and the `"orthant"` key is checked offline.
|
|
161
|
+
Nowhere, by default. Fitting and every `evaluator` run on your machine; the
|
|
162
|
+
package makes no network calls, and the `"orthant"` key is checked offline.
|
|
163
|
+
|
|
164
|
+
The exception is opt-in: `fitter="modal"` uploads X, Y and `sample_weight` to
|
|
165
|
+
a GPU service for the full-likelihood fit and gets parameters back. Everything
|
|
166
|
+
after `fit` runs locally.
|
|
155
167
|
|
|
156
168
|
The one hosted path is a separate package you call yourself:
|
|
157
169
|
[`quantecarlo.orthant_cdf`](https://github.com/sign-of-fourier/quantecarlo#orthant-probabilities-orthant_cdf),
|
|
@@ -188,6 +200,7 @@ runnable example on synthetic data:
|
|
|
188
200
|
## Documentation
|
|
189
201
|
|
|
190
202
|
- [docs/ifm.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/ifm.md): the estimation algorithm, and why IFM
|
|
203
|
+
- [docs/fiml.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/fiml.md): the full-likelihood GPU fit (`fitter="modal"`), with standard errors
|
|
191
204
|
- [docs/api.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/api.md): parameters, evaluators, the inner-model contract
|
|
192
205
|
- [docs/implementation.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/implementation.md): what is hand-rolled, numerical accuracy
|
|
193
206
|
- [docs/limitations.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/limitations.md): known gaps
|
|
@@ -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
|
|
108
|
-
|
|
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
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# API reference
|
|
2
2
|
|
|
3
3
|
For the model and a quickstart see [../README.md](../README.md); for the
|
|
4
|
-
estimation algorithm see [ifm.md](ifm.md)
|
|
4
|
+
estimation algorithm see [ifm.md](ifm.md), and for the full-likelihood GPU fit
|
|
5
|
+
see [fiml.md](fiml.md).
|
|
5
6
|
|
|
6
7
|
## MultivariateProbit
|
|
7
8
|
|
|
@@ -17,6 +18,8 @@ MultivariateProbit(
|
|
|
17
18
|
random_state=None,
|
|
18
19
|
evaluator="quadrature",
|
|
19
20
|
resolution="high",
|
|
21
|
+
fitter="ifm",
|
|
22
|
+
n_draws=512,
|
|
20
23
|
)
|
|
21
24
|
```
|
|
22
25
|
|
|
@@ -33,7 +36,9 @@ MultivariateProbit(
|
|
|
33
36
|
| `project_correlation` | `True` | Project a pairwise estimate onto the nearest positive-definite correlation matrix. No effect when `dependence="joint"`. |
|
|
34
37
|
| `random_state` | `None` | Controls the cross-fitting split and `sample`. |
|
|
35
38
|
| `evaluator` | `"quadrature"` | Backend for the orthant probabilities behind `dependence="joint"` and every joint query: `"quadrature"`, `"scipy"` or `"orthant"`. See [Evaluators](#evaluators). |
|
|
36
|
-
| `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
|
+
| `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. |
|
|
37
42
|
|
|
38
43
|
> **Reading `calibration_`.** This is the Cox calibration slope, a deliberately
|
|
39
44
|
> simple scale check: a probit of each outcome on its own fitted index. A slope
|
|
@@ -72,7 +77,7 @@ queries on a pairwise fit still use the chosen backend.
|
|
|
72
77
|
| --- | --- | --- |
|
|
73
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. |
|
|
74
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. |
|
|
75
|
-
| `"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). |
|
|
76
81
|
|
|
77
82
|
For test sets too large for one machine, the same evaluator runs as a hosted
|
|
78
83
|
GPU service, `quantecarlo.orthant_cdf`: about a second per million rows at
|
|
@@ -87,10 +92,14 @@ the
|
|
|
87
92
|
| --- | --- | --- |
|
|
88
93
|
| `inner_models_` | list, length d | The fitted margins, one per outcome. |
|
|
89
94
|
| `correlation_` | (d, d) | The fitted Σ. |
|
|
90
|
-
| `eta_` | (n, d) | The (cross-fitted) latent indices stage two was fitted on. |
|
|
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. |
|
|
91
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. |
|
|
92
|
-
| `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. |
|
|
93
|
-
| `optimize_result_` | OptimizeResult or None | The SciPy result for `dependence="joint"`. |
|
|
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
|
+
| `optimize_result_` | OptimizeResult or None | The SciPy result for `dependence="joint"`. `None` for `fitter="modal"`. |
|
|
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
|
+
| `cov_params_`, `cov_params_robust_` | (k, k) | `fitter="modal"` only. The matching covariance matrices, ordered as `param_names_`. |
|
|
101
|
+
| `param_names_` | list of str | `fitter="modal"` only. `coef[j,k]`, then `intercept[j]`, then `rho[j,k]` for j < k. |
|
|
102
|
+
| `fit_result_` | dict | `fitter="modal"` only. `n_iter`, `converged`, `grad_max`, `n_draws`, `seed`. |
|
|
94
103
|
| `n_outcomes_`, `n_features_in_` | int | |
|
|
95
104
|
|
|
96
105
|
### Methods
|
|
@@ -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).
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Full maximum likelihood on a GPU (`fitter="modal"`)
|
|
2
|
+
|
|
3
|
+
An alternative to [IFM](ifm.md), not a stage of it. Every margin's
|
|
4
|
+
coefficients and the correlation matrix Σ are estimated together, by
|
|
5
|
+
maximising the joint likelihood
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
L(B, Σ) = Σ_i w_i log P(Y = y_i | x_i), η_ij = b_j0 + x_i' b_j
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
on a hosted GPU service. Only linear margins: the joint likelihood has to be
|
|
12
|
+
differentiated through every margin, which rules out boosted trees and forests.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
model = MultivariateProbit(fitter="modal").fit(X, Y)
|
|
16
|
+
model.correlation_ # Σ
|
|
17
|
+
model.stderr_["coef"] # (d, n_features) standard errors
|
|
18
|
+
model.stderr_robust_ # sandwich standard errors
|
|
19
|
+
model.predict_proba(X_new) # runs locally, on any evaluator
|
|
20
|
+
```
|
|
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
|
+
|
|
43
|
+
## When to use it instead of IFM
|
|
44
|
+
|
|
45
|
+
- You need **standard errors** for the coefficients and for Σ. IFM does not
|
|
46
|
+
report them ([limitations.md](limitations.md)); this path does.
|
|
47
|
+
- You want the **efficient estimator**. IFM is consistent but discards the
|
|
48
|
+
information that the other outcomes carry about each margin; full ML does not.
|
|
49
|
+
- The margins are linear anyway. With flexible margins, IFM is the only option.
|
|
50
|
+
|
|
51
|
+
IFM stays the default: it runs locally, takes any inner model, and its cost
|
|
52
|
+
does not grow with the number of margin parameters.
|
|
53
|
+
|
|
54
|
+
## How it works
|
|
55
|
+
|
|
56
|
+
- The probability of each observed pattern is a d-dimensional orthant integral,
|
|
57
|
+
simulated by GHK with randomised quasi-Monte Carlo: one scrambled Sobol set
|
|
58
|
+
of `n_draws` points, shifted per row. The draws are **fixed for the whole
|
|
59
|
+
fit**, so the simulated log-likelihood is a smooth deterministic function of
|
|
60
|
+
the parameters and a quasi-Newton optimiser (L-BFGS) with exact gradients
|
|
61
|
+
applies.
|
|
62
|
+
- Σ is parameterised so that every parameter value is a valid correlation
|
|
63
|
+
matrix: no projection or penalty is ever needed.
|
|
64
|
+
- The fit starts from the margins fitted with Σ = I (exact, no simulation),
|
|
65
|
+
then optimises everything jointly.
|
|
66
|
+
- X is standardised on the service; coefficients come back on the original
|
|
67
|
+
scale. `inner_params={"alpha": ...}` sets the same small ridge on the slopes
|
|
68
|
+
as `ProbitRegressor` (default 1e-6).
|
|
69
|
+
|
|
70
|
+
## Standard errors
|
|
71
|
+
|
|
72
|
+
At the optimum the service computes the exact Hessian of the simulated
|
|
73
|
+
log-likelihood. Two covariance matrices come back, mapped to the reported
|
|
74
|
+
parameters (coefficients on the original X scale, intercepts, and the
|
|
75
|
+
correlations ρ_jk):
|
|
76
|
+
|
|
77
|
+
- `cov_params_` / `stderr_`: inverse Hessian, the classical ML standard errors.
|
|
78
|
+
- `cov_params_robust_` / `stderr_robust_`: the sandwich `H⁻¹ (Σ_i w_i² g_i g_i') H⁻¹`.
|
|
79
|
+
Use it with `sample_weight`, or when the model may be misspecified.
|
|
80
|
+
|
|
81
|
+
`param_names_` gives the order of both matrices. The standard errors treat the
|
|
82
|
+
Sobol draws as fixed: they do not include simulation error, which is small
|
|
83
|
+
next to sampling error at the default `n_draws=512`.
|
|
84
|
+
|
|
85
|
+
## Where the data goes
|
|
86
|
+
|
|
87
|
+
X, Y and `sample_weight` are uploaded to the service. The response holds
|
|
88
|
+
parameters only; the service keeps the result for collection, and nothing
|
|
89
|
+
after `fit` makes a network call — prediction and scoring run locally with the
|
|
90
|
+
chosen `evaluator`.
|
|
91
|
+
|
|
92
|
+
## Cost
|
|
93
|
+
|
|
94
|
+
A fit is submitted, then polled until done. On a T4: about 50 s end to end for
|
|
95
|
+
n = 50,000, 10 features and d = 6, standard errors included; small fits take
|
|
96
|
+
15 to 20 s, plus up to a minute when the service is starting cold.
|
|
@@ -4,7 +4,8 @@ The gaps below are known and stated rather than discovered later.
|
|
|
4
4
|
|
|
5
5
|
## Statistical
|
|
6
6
|
|
|
7
|
-
**No standard errors.** `
|
|
7
|
+
**No standard errors from IFM.** (`fitter="modal"` reports them, for linear
|
|
8
|
+
margins; see [fiml.md](fiml.md).) Under IFM, `correlation_` is a point estimate. Because stage two
|
|
8
9
|
conditions on estimated margins, inverse-Hessian standard errors would be
|
|
9
10
|
wrong; correct inference needs a Godambe (sandwich) information matrix or a
|
|
10
11
|
bootstrap that resamples and refits both stages. Neither is implemented, and
|
|
@@ -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.
|
|
7
|
+
version = "0.3.1"
|
|
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" }
|
|
@@ -4,7 +4,8 @@ A multivariate probit is a Gaussian squashing function wrapped around an
|
|
|
4
4
|
arbitrary inner model. This package fits one by Inference Functions for Margins
|
|
5
5
|
(IFM): margins first, dependence second. The inner model is pluggable -- linear
|
|
6
6
|
by default, gradient boosting or random forests via named presets, anything
|
|
7
|
-
scikit-learn-shaped via the adapter.
|
|
7
|
+
scikit-learn-shaped via the adapter. For linear margins, ``fitter="modal"``
|
|
8
|
+
fits by full maximum likelihood on a hosted GPU instead, with standard errors.
|
|
8
9
|
|
|
9
10
|
>>> from multivariate_probit import MultivariateProbit
|
|
10
11
|
>>> model = MultivariateProbit(inner="linear").fit(X, Y)
|
|
@@ -33,7 +34,7 @@ from .linear import ProbitRegressor
|
|
|
33
34
|
from .model import MultivariateProbit
|
|
34
35
|
from .results import MultivariateProbitProba
|
|
35
36
|
|
|
36
|
-
__version__ = "0.
|
|
37
|
+
__version__ = "0.3.1"
|
|
37
38
|
|
|
38
39
|
__all__ = [
|
|
39
40
|
"MultivariateProbit",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Client for the hosted full-likelihood fit (``fitter="modal"``).
|
|
2
|
+
|
|
3
|
+
The fit runs on a GPU service: the request carries X, Y and the weights; the
|
|
4
|
+
response carries parameters only -- coefficients, intercepts, the correlation
|
|
5
|
+
matrix and their covariance. Everything after that runs locally. The service
|
|
6
|
+
answers a submission with a call id and is then polled, because a fit can
|
|
7
|
+
outlast a single HTTP request.
|
|
8
|
+
|
|
9
|
+
Standard library only (``urllib``), so the package gains no dependency.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import io
|
|
15
|
+
import json
|
|
16
|
+
import time
|
|
17
|
+
import urllib.error
|
|
18
|
+
import urllib.parse
|
|
19
|
+
import urllib.request
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
__all__ = ["fit_remote", "FIT_URL", "RESULT_URL"]
|
|
24
|
+
|
|
25
|
+
FIT_URL = "https://info-29741--bo-gp-service-mvp-fit.modal.run"
|
|
26
|
+
RESULT_URL = "https://info-29741--bo-gp-service-mvp-fit-result.modal.run"
|
|
27
|
+
|
|
28
|
+
_REQUEST_TIMEOUT = 300 # seconds per HTTP request (the upload can be large)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _error_text(exc):
|
|
32
|
+
try:
|
|
33
|
+
body = json.loads(exc.read())
|
|
34
|
+
return body.get("error", str(body))
|
|
35
|
+
except Exception:
|
|
36
|
+
return str(exc)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def fit_remote(X, Y, sample_weight=None, poll_interval=2.0, timeout=3600, **params):
|
|
40
|
+
"""Submit a fit, wait for it, and return the result as a dict of arrays.
|
|
41
|
+
|
|
42
|
+
``params`` are the service's fields: ``n_draws``, ``seed``, ``max_iter``,
|
|
43
|
+
``tol``, ``alpha``, ``se``, ``dtype``. Raises ``ValueError`` if the service
|
|
44
|
+
rejects the input, ``RuntimeError`` if the fit fails or cannot be
|
|
45
|
+
collected, and ``TimeoutError`` after ``timeout`` seconds.
|
|
46
|
+
"""
|
|
47
|
+
arrays = {"X": np.asarray(X, dtype=np.float64), "Y": np.asarray(Y, dtype=np.float64)}
|
|
48
|
+
if sample_weight is not None:
|
|
49
|
+
arrays["sample_weight"] = np.asarray(sample_weight, dtype=np.float64)
|
|
50
|
+
buf = io.BytesIO()
|
|
51
|
+
np.savez(buf, params=np.array(json.dumps(params)), **arrays)
|
|
52
|
+
request = urllib.request.Request(
|
|
53
|
+
FIT_URL,
|
|
54
|
+
data=buf.getvalue(),
|
|
55
|
+
headers={"Content-Type": "application/octet-stream"},
|
|
56
|
+
method="POST",
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
with urllib.request.urlopen(request, timeout=_REQUEST_TIMEOUT) as resp:
|
|
60
|
+
call_id = json.loads(resp.read())["call_id"]
|
|
61
|
+
except urllib.error.HTTPError as exc:
|
|
62
|
+
if exc.code == 400:
|
|
63
|
+
raise ValueError(f"the fitting service rejected the input: {_error_text(exc)}") from None
|
|
64
|
+
raise RuntimeError(f"the fitting service failed ({exc.code}): {_error_text(exc)}") from None
|
|
65
|
+
|
|
66
|
+
url = RESULT_URL + "?" + urllib.parse.urlencode({"call_id": call_id, "format": "npz"})
|
|
67
|
+
deadline = time.monotonic() + timeout
|
|
68
|
+
while True:
|
|
69
|
+
try:
|
|
70
|
+
with urllib.request.urlopen(url, timeout=_REQUEST_TIMEOUT) as resp:
|
|
71
|
+
status, body = resp.status, resp.read()
|
|
72
|
+
except urllib.error.HTTPError as exc:
|
|
73
|
+
raise RuntimeError(
|
|
74
|
+
f"remote fit {call_id} failed ({exc.code}): {_error_text(exc)}"
|
|
75
|
+
) from None
|
|
76
|
+
if status == 200:
|
|
77
|
+
with np.load(io.BytesIO(body), allow_pickle=False) as npz:
|
|
78
|
+
return {key: npz[key] for key in npz.files}
|
|
79
|
+
if time.monotonic() > deadline:
|
|
80
|
+
raise TimeoutError(f"remote fit {call_id} still running after {timeout} s")
|
|
81
|
+
time.sleep(poll_interval)
|