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.
- multivariate_probit-0.2.3/.github/ISSUE_TEMPLATE/use-case.md +23 -0
- multivariate_probit-0.2.3/CITATION.cff +20 -0
- multivariate_probit-0.2.3/PKG-INFO +274 -0
- multivariate_probit-0.2.3/README.md +210 -0
- multivariate_probit-0.2.3/benchmarks/bench.py +137 -0
- multivariate_probit-0.2.3/benchmarks/benchmarks.ipynb +413 -0
- multivariate_probit-0.2.3/benchmarks/ref_d12_n3000_100_rho0.4_s0.npz +0 -0
- multivariate_probit-0.2.3/benchmarks/ref_d16_n3000_100_rho0.4_s0.npz +0 -0
- multivariate_probit-0.2.3/benchmarks/ref_d3_n3000_100_rho0.4_s0.npz +0 -0
- multivariate_probit-0.2.3/benchmarks/ref_d5_n3000_100_rho0.4_s0.npz +0 -0
- multivariate_probit-0.2.3/benchmarks/ref_d8_n3000_100_rho0.4_s0.npz +0 -0
- multivariate_probit-0.2.3/benchmarks/ref_pairwise_d12_n3000_100_s0.npz +0 -0
- multivariate_probit-0.2.3/benchmarks/ref_pairwise_d16_n3000_100_s0.npz +0 -0
- multivariate_probit-0.2.3/benchmarks/ref_pairwise_d3_n3000_100_s0.npz +0 -0
- multivariate_probit-0.2.3/benchmarks/ref_pairwise_d5_n3000_100_s0.npz +0 -0
- multivariate_probit-0.2.3/benchmarks/ref_pairwise_d8_n3000_100_s0.npz +0 -0
- multivariate_probit-0.2.3/benchmarks/results.csv +55 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/api.md +18 -1
- multivariate_probit-0.2.3/docs/benchmarks.md +90 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/implementation.md +3 -1
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/limitations.md +2 -9
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/README.md +1 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/comparators.md +3 -2
- multivariate_probit-0.2.3/docs/studies/ghk.md +108 -0
- multivariate_probit-0.2.3/docs/use-cases/README.md +17 -0
- multivariate_probit-0.2.3/docs/use-cases/claims-fast-track.md +32 -0
- multivariate_probit-0.2.3/docs/use-cases/content-moderation.md +32 -0
- multivariate_probit-0.2.3/docs/use-cases/cyber.md +28 -0
- multivariate_probit-0.2.3/docs/use-cases/fraud-vendor-fusion.md +38 -0
- multivariate_probit-0.2.3/docs/use-cases/hcc-codes.md +36 -0
- multivariate_probit-0.2.3/docs/use-cases/property-imagery.md +30 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/pyproject.toml +28 -6
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/__init__.py +1 -1
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/results.py +58 -0
- multivariate_probit-0.2.3/tests/test_conditional.py +72 -0
- multivariate_probit-0.2.3/tests/test_rf.py +66 -0
- multivariate_probit-0.2.1/PKG-INFO +0 -183
- multivariate_probit-0.2.1/README.md +0 -126
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/.gitignore +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/LICENSE +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/ifm.md +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/index-correlation-shortcut.md +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/log-score-evaluator.md +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/margin-gaps.md +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/mulan-multilabel.md +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/multilabel-benchmarks.md +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/sparse-label-transfer.md +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/docs/studies/uci-credit.md +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/hatch_build.py +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/_corr.py +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/_mvn.py +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/ifm.py +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/inner.py +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/linear.py +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/model.py +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/src/multivariate_probit/orthant/__init__.py +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/tests/conftest.py +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/tests/test_calibration.py +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/tests/test_contract.py +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/tests/test_correlation_projection.py +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/tests/test_evaluators.py +0 -0
- {multivariate_probit-0.2.1 → multivariate_probit-0.2.3}/tests/test_linear.py +0 -0
- {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).
|