multivariate-probit 0.2.1__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. multivariate_probit-0.2.3/.github/ISSUE_TEMPLATE/use-case.md +23 -0
  2. multivariate_probit-0.2.3/CITATION.cff +20 -0
  3. multivariate_probit-0.2.3/PKG-INFO +274 -0
  4. multivariate_probit-0.2.3/README.md +210 -0
  5. multivariate_probit-0.2.3/benchmarks/bench.py +137 -0
  6. multivariate_probit-0.2.3/benchmarks/benchmarks.ipynb +413 -0
  7. multivariate_probit-0.2.3/benchmarks/ref_d12_n3000_100_rho0.4_s0.npz +0 -0
  8. multivariate_probit-0.2.3/benchmarks/ref_d16_n3000_100_rho0.4_s0.npz +0 -0
  9. multivariate_probit-0.2.3/benchmarks/ref_d3_n3000_100_rho0.4_s0.npz +0 -0
  10. multivariate_probit-0.2.3/benchmarks/ref_d5_n3000_100_rho0.4_s0.npz +0 -0
  11. multivariate_probit-0.2.3/benchmarks/ref_d8_n3000_100_rho0.4_s0.npz +0 -0
  12. multivariate_probit-0.2.3/benchmarks/ref_pairwise_d12_n3000_100_s0.npz +0 -0
  13. multivariate_probit-0.2.3/benchmarks/ref_pairwise_d16_n3000_100_s0.npz +0 -0
  14. multivariate_probit-0.2.3/benchmarks/ref_pairwise_d3_n3000_100_s0.npz +0 -0
  15. multivariate_probit-0.2.3/benchmarks/ref_pairwise_d5_n3000_100_s0.npz +0 -0
  16. multivariate_probit-0.2.3/benchmarks/ref_pairwise_d8_n3000_100_s0.npz +0 -0
  17. multivariate_probit-0.2.3/benchmarks/results.csv +55 -0
  18. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/api.md +18 -1
  19. multivariate_probit-0.2.3/docs/benchmarks.md +90 -0
  20. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/implementation.md +3 -1
  21. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/limitations.md +2 -9
  22. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/README.md +1 -0
  23. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/comparators.md +3 -2
  24. multivariate_probit-0.2.3/docs/studies/ghk.md +108 -0
  25. multivariate_probit-0.2.3/docs/use-cases/README.md +17 -0
  26. multivariate_probit-0.2.3/docs/use-cases/claims-fast-track.md +32 -0
  27. multivariate_probit-0.2.3/docs/use-cases/content-moderation.md +32 -0
  28. multivariate_probit-0.2.3/docs/use-cases/cyber.md +28 -0
  29. multivariate_probit-0.2.3/docs/use-cases/fraud-vendor-fusion.md +38 -0
  30. multivariate_probit-0.2.3/docs/use-cases/hcc-codes.md +36 -0
  31. multivariate_probit-0.2.3/docs/use-cases/property-imagery.md +30 -0
  32. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/pyproject.toml +28 -6
  33. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/__init__.py +1 -1
  34. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/results.py +58 -0
  35. multivariate_probit-0.2.3/tests/test_conditional.py +72 -0
  36. multivariate_probit-0.2.3/tests/test_rf.py +66 -0
  37. multivariate_probit-0.2.1/PKG-INFO +0 -183
  38. multivariate_probit-0.2.1/README.md +0 -126
  39. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/.gitignore +0 -0
  40. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/LICENSE +0 -0
  41. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/ifm.md +0 -0
  42. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/index-correlation-shortcut.md +0 -0
  43. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/log-score-evaluator.md +0 -0
  44. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/margin-gaps.md +0 -0
  45. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/mulan-multilabel.md +0 -0
  46. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/multilabel-benchmarks.md +0 -0
  47. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/sparse-label-transfer.md +0 -0
  48. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/uci-credit.md +0 -0
  49. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/hatch_build.py +0 -0
  50. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/_corr.py +0 -0
  51. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/_mvn.py +0 -0
  52. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/ifm.py +0 -0
  53. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/inner.py +0 -0
  54. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/linear.py +0 -0
  55. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/model.py +0 -0
  56. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/orthant/__init__.py +0 -0
  57. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/tests/conftest.py +0 -0
  58. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/tests/test_calibration.py +0 -0
  59. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/tests/test_contract.py +0 -0
  60. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/tests/test_correlation_projection.py +0 -0
  61. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/tests/test_evaluators.py +0 -0
  62. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/tests/test_linear.py +0 -0
  63. {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/tests/test_xgboost.py +0 -0
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: Use case
3
+ about: Tell us where you are using (or want to use) multivariate-probit
4
+ title: "Use case: "
5
+ labels: use-case
6
+ ---
7
+
8
+ **Domain.** What field or problem? (e.g. fraud, claims, moderation)
9
+
10
+ **Outcomes.** How many 0/1 outcomes (d), and what are they, roughly?
11
+
12
+ **Joint query.** Which question do you ask of the model? (`any`, `all`, `none`,
13
+ `joint(pattern)`, `conditional`, or something the API does not have yet)
14
+
15
+ **Size.** Roughly how many rows do you fit on, and how many do you score?
16
+
17
+ **Inner model.** `linear`, `xgboost`, `rf`, or your own?
18
+
19
+ **Did the joint result change a decision?** For example a threshold, a review
20
+ set, or an action. What changed, compared with treating the outcomes as
21
+ independent?
22
+
23
+ **Anything that got in the way?**
@@ -0,0 +1,20 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ title: "multivariate-probit: joint probabilities for correlated binary outcomes"
4
+ type: software
5
+ authors:
6
+ - family-names: Shipman
7
+ given-names: Mark
8
+ orcid: "https://orcid.org/0009-0008-7193-1263"
9
+ version: 0.2.3
10
+ # TODO(Mark): add date-released (YYYY-MM-DD) when 0.2.3 is published.
11
+ license: MIT
12
+ repository-code: "https://github.com/sign-of-fourier/multivariate-probit"
13
+ url: "https://quantecarlo.com/multivariate-probit"
14
+ keywords:
15
+ - multivariate probit
16
+ - correlated binary outcomes
17
+ - multilabel classification
18
+ - joint probability
19
+ - Gaussian copula
20
+ - inference functions for margins
@@ -0,0 +1,274 @@
1
+ Metadata-Version: 2.5
2
+ Name: multivariate-probit
3
+ Version: 0.2.3
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
+ Project-URL: Homepage, https://quantecarlo.com/multivariate-probit
6
+ Project-URL: Source, https://github.com/sign-of-fourier/multivariate-probit
7
+ Project-URL: Documentation, https://github.com/sign-of-fourier/multivariate-probit/tree/main/docs
8
+ Project-URL: Changelog, https://github.com/sign-of-fourier/multivariate-probit/commits/main
9
+ Project-URL: Issues, https://github.com/sign-of-fourier/multivariate-probit/issues
10
+ Author: Mark Shipman
11
+ License: MIT License
12
+
13
+ Copyright (c) 2026 Mark Shipman
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+ License-File: LICENSE
33
+ Keywords: co-occurrence,correlated binary outcomes,gaussian copula,inference functions for margins,joint probability,label dependence,multi-output,multilabel,multivariate normal cdf,multivariate probit,random forest,scikit-learn,tetrachoric correlation,xgboost
34
+ Classifier: Development Status :: 4 - Beta
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: Intended Audience :: Financial and Insurance Industry
37
+ Classifier: Intended Audience :: Healthcare Industry
38
+ Classifier: Intended Audience :: Science/Research
39
+ Classifier: License :: OSI Approved :: MIT License
40
+ Classifier: Programming Language :: Python :: 3
41
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
42
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
43
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
44
+ Requires-Python: >=3.9
45
+ Requires-Dist: numpy>=1.22
46
+ Requires-Dist: scipy>=1.8
47
+ Provides-Extra: all
48
+ Requires-Dist: cryptography>=42; extra == 'all'
49
+ Requires-Dist: scikit-learn>=1.1; extra == 'all'
50
+ Requires-Dist: xgboost>=1.7; extra == 'all'
51
+ Provides-Extra: dev
52
+ Requires-Dist: cryptography>=42; extra == 'dev'
53
+ Requires-Dist: pytest>=7; extra == 'dev'
54
+ Requires-Dist: scikit-learn>=1.1; extra == 'dev'
55
+ Requires-Dist: xgboost>=1.7; extra == 'dev'
56
+ Provides-Extra: orthant
57
+ Requires-Dist: cryptography>=42; extra == 'orthant'
58
+ Provides-Extra: sklearn
59
+ Requires-Dist: scikit-learn>=1.1; extra == 'sklearn'
60
+ Provides-Extra: xgboost
61
+ Requires-Dist: scikit-learn>=1.1; extra == 'xgboost'
62
+ Requires-Dist: xgboost>=1.7; extra == 'xgboost'
63
+ Description-Content-Type: text/markdown
64
+
65
+ # multivariate-probit
66
+
67
+ **Tie out correlated binary outcomes coherently, without assuming independence.**
68
+
69
+ Fit one model per yes/no outcome, any classifier you like, and this package
70
+ estimates how the outcomes move together. From one fitted model, P(any),
71
+ P(all), every pattern and every conditional agree with each other: the 2^d
72
+ pattern probabilities sum to exactly 1.
73
+
74
+ ```bash
75
+ pip install multivariate-probit # numpy + scipy only
76
+ ```
77
+
78
+ ```python
79
+ from multivariate_probit import MultivariateProbit
80
+
81
+ model = MultivariateProbit(inner="linear", dependence="pairwise").fit(X, Y) # Y: (n, d) of 0/1
82
+
83
+ proba = model.predict_proba(X_new)
84
+ proba.marginal # P(Y_j = 1 | x), shape (n, d)
85
+ proba.any() # P(at least one outcome)
86
+ proba.all([0, 2]) # P(outcomes 0 and 2 both)
87
+ proba.joint([1, 0, 1]) # P(exactly this pattern)
88
+ proba.conditional(2, given={0: 1}) # P(Y_2 = 1 | Y_0 = 1, x)
89
+ model.correlation_ # the fitted correlation matrix Σ, (d, d)
90
+ ```
91
+
92
+ Background and a worked example:
93
+ [quantecarlo.com/multivariate-probit](https://quantecarlo.com/multivariate-probit).
94
+ Using this on a real problem?
95
+ [Tell us in a use-case issue](https://github.com/sign-of-fourier/multivariate-probit/issues/new?template=use-case.md).
96
+
97
+ ## You might be looking for this if…
98
+
99
+ - You need **P(at least one)** across several correlated binary labels, and
100
+ multiplying per-label probabilities gives the wrong answer.
101
+ - Your **multilabel** or **multi-output** model treats labels as independent,
102
+ and you need the **joint probability** of a label combination.
103
+ - You've used R `mvProbit` or Stata `mvprobit` and want **multivariate probit in
104
+ Python** for prediction at scale (no standard errors yet; see below).
105
+ - You want to **combine several fraud or risk model scores** into one
106
+ probability that "any of these happened."
107
+ - You need **co-occurrence** or **label dependence** expressed as one
108
+ correlation matrix, a **tetrachoric correlation** that conditions on features.
109
+ - You want a **Gaussian copula for binary outcomes** on top of XGBoost or
110
+ scikit-learn classifiers.
111
+ - You need a **multi-outcome model that a validator can read**: per-outcome
112
+ coefficients plus an explicit correlation matrix (see
113
+ [Explainability](#explainability-scoped)).
114
+ - You need **P(outcome A | outcome B already happened)** from the same model
115
+ that gives the marginals.
116
+
117
+ ## The model
118
+
119
+ A **multivariate probit**: each outcome is a threshold on a latent Gaussian
120
+ score, and the scores are correlated.
121
+
122
+ ```
123
+ Y_j = 1[ η_j(x) + e_j > 0 ], e ~ N(0, Σ), j = 1 … d
124
+ ```
125
+
126
+ `η_j` comes from any per-outcome model. Σ is a correlation matrix; with
127
+ intercept-only margins, ρ_jk is the classical tetrachoric correlation, and with
128
+ features it is the tetrachoric correlation conditional on x. Fitting is
129
+ two-stage **Inference Functions for Margins** (IFM): each margin is fitted
130
+ alone and cross-fitted, then Σ is estimated by maximum likelihood with the
131
+ margins held fixed ([docs/ifm.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/ifm.md)).
132
+
133
+ ## Why not write it yourself
134
+
135
+ It is a short model to write down. These are the places a first version goes
136
+ wrong; each is handled here and documented.
137
+
138
+ - **In-sample margins drive every fitted correlation toward +1.** An overfit
139
+ margin is most confident on the rows where the labels co-occur, and stage two
140
+ reads that as dependence. Margins are cross-fitted by default (`cv=5`);
141
+ `test_cross_fitting_removes_upward_bias_in_the_correlation` pins it.
142
+ [Mechanism](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/ifm.md#why-cross-fitting-is-required).
143
+ - **Full-likelihood Σ does not scale with d.** Each likelihood call costs
144
+ `n_quad ** (d - 2)` bivariate evaluations per row; at d = 4 and 250 rows a
145
+ joint fit took 2-4 minutes. The pairwise estimator uses only closed-form
146
+ bivariate integrals and took about 0.07 s on the same data, with the same
147
+ accuracy ([studies/comparators.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/studies/comparators.md)).
148
+ - **SciPy's multivariate normal CDF is one call per row.** Scoring a test set
149
+ is a Python loop, measured at 3e-3 to 2 s per row, slowest on the
150
+ near-singular Σ that pairwise fits produce
151
+ ([studies/ghk.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/studies/ghk.md)).
152
+ - **Near-duplicate labels push ρ to ±1, where Σ is singular.** Each ρ is
153
+ bounded to abs(ρ) ≤ 0.999, and a pairwise Σ is projected onto the nearest
154
+ valid correlation matrix before any joint query
155
+ ([docs/ifm.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/ifm.md#positive-definiteness)).
156
+ - **Pairwise estimates need not form a valid matrix.** Three correlations
157
+ estimated separately can be jointly impossible; the projection repairs that
158
+ (`test_nearest_correlation_repairs_an_incoherent_triple`).
159
+
160
+ ## Explainability, scoped
161
+
162
+ - **Linear margins** (`inner="linear"`, the default): per-outcome probit
163
+ coefficients (`model.inner_models_[j].coef_`, `.intercept_`) plus an explicit
164
+ correlation matrix. That is an interpretable joint model end to end.
165
+ - **XGBoost, random forest or other flexible margins**: the dependence is still
166
+ explicit and readable in `correlation_`, but each margin is as much a black
167
+ box as the classifier inside it. SHAP is the tool for explaining a single
168
+ black-box model; this package does not make one explainable.
169
+ - **No standard errors yet.** Coefficients and Σ are point estimates
170
+ ([limitations](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/limitations.md)).
171
+
172
+ ## Bring your own model, or use ours
173
+
174
+ Stage two consumes only each margin's predicted probability, mapped to the
175
+ latent scale, so the per-outcome model is a black box: anything with
176
+ `predict_proba`, or anything with `latent(X)`.
177
+
178
+ | Preset | Estimator | Install |
179
+ | --- | --- | --- |
180
+ | `"linear"` (default), `"probit"` | native probit via IRLS | — |
181
+ | `"xgboost"`, `"xgb"` | `XGBClassifier`, tuned for calibration | `[xgboost]` |
182
+ | `"rf"`, `"random_forest"` | `RandomForestClassifier` | `[sklearn]` |
183
+
184
+ ```python
185
+ MultivariateProbit(inner=XGBClassifier(max_depth=4)) # any sklearn-style classifier
186
+ MultivariateProbit(inner=["linear", "xgboost", "rf"]) # one per outcome
187
+ register_inner("mine", make_my_model) # your own preset
188
+ ```
189
+
190
+ The API is scikit-learn style (`fit`, `predict`, `predict_proba`, `score`,
191
+ `get_params`, `set_params`) without requiring scikit-learn, and works with
192
+ `clone`, `Pipeline` and `cross_val_score`. Contract and
193
+ calibration check: [docs/api.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/api.md#inner-models).
194
+
195
+ ## Scale
196
+
197
+ Scoring a fitted model, `P(Y = y | x)`, is a d-dimensional integral per row.
198
+ The backend is chosen with `evaluator=`:
199
+
200
+ | `evaluator` | Speed | Accuracy | Runs |
201
+ | --- | --- | --- | --- |
202
+ | `"quadrature"` (default) | 8e-5 s per row at d = 3, 0.12 s at d = 6, impractical past 7 | about 1e-5, deterministic | locally |
203
+ | `"scipy"` | 3e-3 to 2 s per row, any d | reference grade, randomised | locally |
204
+ | `"orthant"` | 2e-5 to 9e-5 s per row, d = 4 to 20 | median error ≤ 0.007, deterministic | locally, compiled, keyed above d = 3 |
205
+
206
+ **Speed, with a small accuracy trade-off:** `"orthant"` is two to five orders
207
+ of magnitude faster than SciPy, at roughly the accuracy of 100-draw GHK
208
+ 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
+ and 3.12 on x86-64 Linux (`pip install multivariate-probit[orthant]`); without
210
+ a key it runs d ≤ 3 at `resolution="low"`. Key:
211
+ [quantecarlo.com/orthant_key](https://quantecarlo.com/orthant_key).
212
+
213
+ Benchmarks against other tools: [docs/benchmarks.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/benchmarks.md).
214
+
215
+ ## Where your data goes
216
+
217
+ Nowhere. Fitting and every `evaluator` run on your machine; the package makes
218
+ no network calls, and the `"orthant"` key is checked offline.
219
+
220
+ The one hosted path is a separate package you call yourself:
221
+ [`quantecarlo.orthant_cdf`](https://github.com/sign-of-fourier/quantecarlo#orthant-probabilities-orthant_cdf),
222
+ a GPU service for test sets too large for one machine. It sends the `(N, d)`
223
+ array of latent scores (`model.transform(X)`), the correlation matrix and, for
224
+ a pattern query, that pattern as a sign vector. It never receives X. To keep
225
+ everything local, don't install or call `quantecarlo`.
226
+
227
+ ## When to use what
228
+
229
+ | Tool | The right choice when |
230
+ | --- | --- |
231
+ | scikit-learn `MultiOutputClassifier` | Only per-label probabilities matter, never combinations. |
232
+ | scikit-learn `ClassifierChain` (or an ensemble of chains) | The goal is predicting the exact label set; ensembles of chains had the best subset accuracy at d = 6 in [this benchmark](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/studies/multilabel-benchmarks.md). |
233
+ | Label powerset (one class per label combination) | Few labels, and every combination you care about is common in the training data. |
234
+ | One stacked model on a combined target | Only one fixed combination matters and it has enough positives to train on. |
235
+ | `statsmodels` `Probit`, one per outcome | You need classical inference on each outcome's coefficients and only marginal probabilities. |
236
+ | [pybhatlib](https://github.com/UMN-Choi-Lab/pybhatlib) | Discrete-choice econometrics (multinomial probit, multivariate ordered response probit) by maximum likelihood with Bhat's analytic MVNCD approximation and analytic gradients. |
237
+ | R `mvProbit`, Stata `mvprobit` | A classical full-likelihood multivariate probit with linear indices and small d, inside R or Stata. |
238
+ | **multivariate-probit** | Coherent joint queries (any, all, pattern, conditional) on top of flexible per-outcome models, with scoring that scales to large N. |
239
+
240
+ ## Use cases
241
+
242
+ Problems that fit this shape, each with the joint query that answers it and a
243
+ runnable example on synthetic data:
244
+
245
+ - [Fusing fraud and identity vendor scores](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/use-cases/fraud-vendor-fusion.md): `.any()`, posterior over fraud types
246
+ - [Claims fast-track eligibility](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/use-cases/claims-fast-track.md): P(no SIU ∧ no attorney ∧ no total loss)
247
+ - [HCC codes with hierarchies and interactions](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/use-cases/hcc-codes.md): `.joint()` over code patterns
248
+ - [Content moderation and ad integrity](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/use-cases/content-moderation.md): P(any policy violation)
249
+ - [Cyber: phishing and exploited vulnerabilities](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/use-cases/cyber.md): `.all()`, `.any()`
250
+ - [Property imagery attributes](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/use-cases/property-imagery.md): roof, pool, solar
251
+
252
+ ## Documentation
253
+
254
+ - [docs/ifm.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/ifm.md): the estimation algorithm, and why IFM
255
+ - [docs/api.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/api.md): parameters, evaluators, the inner-model contract
256
+ - [docs/implementation.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/implementation.md): what is hand-rolled, numerical accuracy
257
+ - [docs/limitations.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/limitations.md): known gaps
258
+ - [docs/benchmarks.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/benchmarks.md): comparison against other tools
259
+ - [docs/studies/](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/studies/): every measurement behind the claims above
260
+
261
+ ## Citing
262
+
263
+ See [CITATION.cff](https://github.com/sign-of-fourier/multivariate-probit/blob/main/CITATION.cff).
264
+
265
+ ## Development
266
+
267
+ ```bash
268
+ pip install -e ".[dev]"
269
+ pytest
270
+ ```
271
+
272
+ ## License
273
+
274
+ MIT. See [LICENSE](https://github.com/sign-of-fourier/multivariate-probit/blob/main/LICENSE).
@@ -0,0 +1,210 @@
1
+ # multivariate-probit
2
+
3
+ **Tie out correlated binary outcomes coherently, without assuming independence.**
4
+
5
+ Fit one model per yes/no outcome, any classifier you like, and this package
6
+ estimates how the outcomes move together. From one fitted model, P(any),
7
+ P(all), every pattern and every conditional agree with each other: the 2^d
8
+ pattern probabilities sum to exactly 1.
9
+
10
+ ```bash
11
+ pip install multivariate-probit # numpy + scipy only
12
+ ```
13
+
14
+ ```python
15
+ from multivariate_probit import MultivariateProbit
16
+
17
+ model = MultivariateProbit(inner="linear", dependence="pairwise").fit(X, Y) # Y: (n, d) of 0/1
18
+
19
+ proba = model.predict_proba(X_new)
20
+ proba.marginal # P(Y_j = 1 | x), shape (n, d)
21
+ proba.any() # P(at least one outcome)
22
+ proba.all([0, 2]) # P(outcomes 0 and 2 both)
23
+ proba.joint([1, 0, 1]) # P(exactly this pattern)
24
+ proba.conditional(2, given={0: 1}) # P(Y_2 = 1 | Y_0 = 1, x)
25
+ model.correlation_ # the fitted correlation matrix Σ, (d, d)
26
+ ```
27
+
28
+ Background and a worked example:
29
+ [quantecarlo.com/multivariate-probit](https://quantecarlo.com/multivariate-probit).
30
+ Using this on a real problem?
31
+ [Tell us in a use-case issue](https://github.com/sign-of-fourier/multivariate-probit/issues/new?template=use-case.md).
32
+
33
+ ## You might be looking for this if…
34
+
35
+ - You need **P(at least one)** across several correlated binary labels, and
36
+ multiplying per-label probabilities gives the wrong answer.
37
+ - Your **multilabel** or **multi-output** model treats labels as independent,
38
+ and you need the **joint probability** of a label combination.
39
+ - You've used R `mvProbit` or Stata `mvprobit` and want **multivariate probit in
40
+ Python** for prediction at scale (no standard errors yet; see below).
41
+ - You want to **combine several fraud or risk model scores** into one
42
+ probability that "any of these happened."
43
+ - You need **co-occurrence** or **label dependence** expressed as one
44
+ correlation matrix, a **tetrachoric correlation** that conditions on features.
45
+ - You want a **Gaussian copula for binary outcomes** on top of XGBoost or
46
+ scikit-learn classifiers.
47
+ - You need a **multi-outcome model that a validator can read**: per-outcome
48
+ coefficients plus an explicit correlation matrix (see
49
+ [Explainability](#explainability-scoped)).
50
+ - You need **P(outcome A | outcome B already happened)** from the same model
51
+ that gives the marginals.
52
+
53
+ ## The model
54
+
55
+ A **multivariate probit**: each outcome is a threshold on a latent Gaussian
56
+ score, and the scores are correlated.
57
+
58
+ ```
59
+ Y_j = 1[ η_j(x) + e_j > 0 ], e ~ N(0, Σ), j = 1 … d
60
+ ```
61
+
62
+ `η_j` comes from any per-outcome model. Σ is a correlation matrix; with
63
+ intercept-only margins, ρ_jk is the classical tetrachoric correlation, and with
64
+ features it is the tetrachoric correlation conditional on x. Fitting is
65
+ two-stage **Inference Functions for Margins** (IFM): each margin is fitted
66
+ alone and cross-fitted, then Σ is estimated by maximum likelihood with the
67
+ margins held fixed ([docs/ifm.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/ifm.md)).
68
+
69
+ ## Why not write it yourself
70
+
71
+ It is a short model to write down. These are the places a first version goes
72
+ wrong; each is handled here and documented.
73
+
74
+ - **In-sample margins drive every fitted correlation toward +1.** An overfit
75
+ margin is most confident on the rows where the labels co-occur, and stage two
76
+ reads that as dependence. Margins are cross-fitted by default (`cv=5`);
77
+ `test_cross_fitting_removes_upward_bias_in_the_correlation` pins it.
78
+ [Mechanism](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/ifm.md#why-cross-fitting-is-required).
79
+ - **Full-likelihood Σ does not scale with d.** Each likelihood call costs
80
+ `n_quad ** (d - 2)` bivariate evaluations per row; at d = 4 and 250 rows a
81
+ joint fit took 2-4 minutes. The pairwise estimator uses only closed-form
82
+ bivariate integrals and took about 0.07 s on the same data, with the same
83
+ accuracy ([studies/comparators.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/studies/comparators.md)).
84
+ - **SciPy's multivariate normal CDF is one call per row.** Scoring a test set
85
+ is a Python loop, measured at 3e-3 to 2 s per row, slowest on the
86
+ near-singular Σ that pairwise fits produce
87
+ ([studies/ghk.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/studies/ghk.md)).
88
+ - **Near-duplicate labels push ρ to ±1, where Σ is singular.** Each ρ is
89
+ bounded to abs(ρ) ≤ 0.999, and a pairwise Σ is projected onto the nearest
90
+ valid correlation matrix before any joint query
91
+ ([docs/ifm.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/ifm.md#positive-definiteness)).
92
+ - **Pairwise estimates need not form a valid matrix.** Three correlations
93
+ estimated separately can be jointly impossible; the projection repairs that
94
+ (`test_nearest_correlation_repairs_an_incoherent_triple`).
95
+
96
+ ## Explainability, scoped
97
+
98
+ - **Linear margins** (`inner="linear"`, the default): per-outcome probit
99
+ coefficients (`model.inner_models_[j].coef_`, `.intercept_`) plus an explicit
100
+ correlation matrix. That is an interpretable joint model end to end.
101
+ - **XGBoost, random forest or other flexible margins**: the dependence is still
102
+ explicit and readable in `correlation_`, but each margin is as much a black
103
+ box as the classifier inside it. SHAP is the tool for explaining a single
104
+ black-box model; this package does not make one explainable.
105
+ - **No standard errors yet.** Coefficients and Σ are point estimates
106
+ ([limitations](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/limitations.md)).
107
+
108
+ ## Bring your own model, or use ours
109
+
110
+ Stage two consumes only each margin's predicted probability, mapped to the
111
+ latent scale, so the per-outcome model is a black box: anything with
112
+ `predict_proba`, or anything with `latent(X)`.
113
+
114
+ | Preset | Estimator | Install |
115
+ | --- | --- | --- |
116
+ | `"linear"` (default), `"probit"` | native probit via IRLS | — |
117
+ | `"xgboost"`, `"xgb"` | `XGBClassifier`, tuned for calibration | `[xgboost]` |
118
+ | `"rf"`, `"random_forest"` | `RandomForestClassifier` | `[sklearn]` |
119
+
120
+ ```python
121
+ MultivariateProbit(inner=XGBClassifier(max_depth=4)) # any sklearn-style classifier
122
+ MultivariateProbit(inner=["linear", "xgboost", "rf"]) # one per outcome
123
+ register_inner("mine", make_my_model) # your own preset
124
+ ```
125
+
126
+ The API is scikit-learn style (`fit`, `predict`, `predict_proba`, `score`,
127
+ `get_params`, `set_params`) without requiring scikit-learn, and works with
128
+ `clone`, `Pipeline` and `cross_val_score`. Contract and
129
+ calibration check: [docs/api.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/api.md#inner-models).
130
+
131
+ ## Scale
132
+
133
+ Scoring a fitted model, `P(Y = y | x)`, is a d-dimensional integral per row.
134
+ The backend is chosen with `evaluator=`:
135
+
136
+ | `evaluator` | Speed | Accuracy | Runs |
137
+ | --- | --- | --- | --- |
138
+ | `"quadrature"` (default) | 8e-5 s per row at d = 3, 0.12 s at d = 6, impractical past 7 | about 1e-5, deterministic | locally |
139
+ | `"scipy"` | 3e-3 to 2 s per row, any d | reference grade, randomised | locally |
140
+ | `"orthant"` | 2e-5 to 9e-5 s per row, d = 4 to 20 | median error ≤ 0.007, deterministic | locally, compiled, keyed above d = 3 |
141
+
142
+ **Speed, with a small accuracy trade-off:** `"orthant"` is two to five orders
143
+ of magnitude faster than SciPy, at roughly the accuracy of 100-draw GHK
144
+ 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
+ and 3.12 on x86-64 Linux (`pip install multivariate-probit[orthant]`); without
146
+ a key it runs d ≤ 3 at `resolution="low"`. Key:
147
+ [quantecarlo.com/orthant_key](https://quantecarlo.com/orthant_key).
148
+
149
+ Benchmarks against other tools: [docs/benchmarks.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/benchmarks.md).
150
+
151
+ ## Where your data goes
152
+
153
+ Nowhere. Fitting and every `evaluator` run on your machine; the package makes
154
+ no network calls, and the `"orthant"` key is checked offline.
155
+
156
+ The one hosted path is a separate package you call yourself:
157
+ [`quantecarlo.orthant_cdf`](https://github.com/sign-of-fourier/quantecarlo#orthant-probabilities-orthant_cdf),
158
+ a GPU service for test sets too large for one machine. It sends the `(N, d)`
159
+ array of latent scores (`model.transform(X)`), the correlation matrix and, for
160
+ a pattern query, that pattern as a sign vector. It never receives X. To keep
161
+ everything local, don't install or call `quantecarlo`.
162
+
163
+ ## When to use what
164
+
165
+ | Tool | The right choice when |
166
+ | --- | --- |
167
+ | scikit-learn `MultiOutputClassifier` | Only per-label probabilities matter, never combinations. |
168
+ | scikit-learn `ClassifierChain` (or an ensemble of chains) | The goal is predicting the exact label set; ensembles of chains had the best subset accuracy at d = 6 in [this benchmark](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/studies/multilabel-benchmarks.md). |
169
+ | Label powerset (one class per label combination) | Few labels, and every combination you care about is common in the training data. |
170
+ | One stacked model on a combined target | Only one fixed combination matters and it has enough positives to train on. |
171
+ | `statsmodels` `Probit`, one per outcome | You need classical inference on each outcome's coefficients and only marginal probabilities. |
172
+ | [pybhatlib](https://github.com/UMN-Choi-Lab/pybhatlib) | Discrete-choice econometrics (multinomial probit, multivariate ordered response probit) by maximum likelihood with Bhat's analytic MVNCD approximation and analytic gradients. |
173
+ | R `mvProbit`, Stata `mvprobit` | A classical full-likelihood multivariate probit with linear indices and small d, inside R or Stata. |
174
+ | **multivariate-probit** | Coherent joint queries (any, all, pattern, conditional) on top of flexible per-outcome models, with scoring that scales to large N. |
175
+
176
+ ## Use cases
177
+
178
+ Problems that fit this shape, each with the joint query that answers it and a
179
+ runnable example on synthetic data:
180
+
181
+ - [Fusing fraud and identity vendor scores](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/use-cases/fraud-vendor-fusion.md): `.any()`, posterior over fraud types
182
+ - [Claims fast-track eligibility](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/use-cases/claims-fast-track.md): P(no SIU ∧ no attorney ∧ no total loss)
183
+ - [HCC codes with hierarchies and interactions](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/use-cases/hcc-codes.md): `.joint()` over code patterns
184
+ - [Content moderation and ad integrity](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/use-cases/content-moderation.md): P(any policy violation)
185
+ - [Cyber: phishing and exploited vulnerabilities](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/use-cases/cyber.md): `.all()`, `.any()`
186
+ - [Property imagery attributes](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/use-cases/property-imagery.md): roof, pool, solar
187
+
188
+ ## Documentation
189
+
190
+ - [docs/ifm.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/ifm.md): the estimation algorithm, and why IFM
191
+ - [docs/api.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/api.md): parameters, evaluators, the inner-model contract
192
+ - [docs/implementation.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/implementation.md): what is hand-rolled, numerical accuracy
193
+ - [docs/limitations.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/limitations.md): known gaps
194
+ - [docs/benchmarks.md](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/benchmarks.md): comparison against other tools
195
+ - [docs/studies/](https://github.com/sign-of-fourier/multivariate-probit/blob/main/docs/studies/): every measurement behind the claims above
196
+
197
+ ## Citing
198
+
199
+ See [CITATION.cff](https://github.com/sign-of-fourier/multivariate-probit/blob/main/CITATION.cff).
200
+
201
+ ## Development
202
+
203
+ ```bash
204
+ pip install -e ".[dev]"
205
+ pytest
206
+ ```
207
+
208
+ ## License
209
+
210
+ MIT. See [LICENSE](https://github.com/sign-of-fourier/multivariate-probit/blob/main/LICENSE).