pybhatlib 0.1.0__tar.gz → 0.2.0__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.
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/PKG-INFO +6 -1
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/README.md +5 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t03a_mvncd_methods.py +15 -14
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t03b_mvncd_gradients.py +16 -12
- pybhatlib-0.2.0/examples/tutorials/python_scripts/t03e_bhat2018_table1.py +366 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t03a_mvncd_methods.ipynb +1 -1
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t03b_mvncd_gradients.ipynb +3 -3
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/pyproject.toml +1 -1
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/gradmvn/_bivariate_trunc.py +76 -36
- pybhatlib-0.2.0/src/pybhatlib/gradmvn/_mvncd.py +931 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_gradmvn/test_bivariate_trunc.py +53 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_gradmvn/test_mvncd_methods.py +12 -10
- pybhatlib-0.1.0/src/pybhatlib/gradmvn/_mvncd.py +0 -826
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/.gitignore +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/CLAUDE.md +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/IMPLEMENTATION_PLAN.md +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/LICENSE +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/data/TRAVELMODE.csv +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/data/generate_travelmode.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/mnp_ate_analysis.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/mnp_flexible_cov.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/mnp_iid.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/mnp_random_coefficients.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/morp_example.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/_convert_to_ipynb.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t00_quickstart.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t01a_vectorization.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t01b_ldlt.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t01c_truncated_mvn.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t02a_gradcovcor.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t02b_spherical.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t02c_chain_rules.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t03c_mvncd_rect.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t03d_univariate_cdfs.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t04c_mnp_heteronly.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t04f_mnp_control_options.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t04g_mnp_forecasting.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t05b_morp_ate_predict.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t06a_backend_switching.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t06b_custom_specs.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t06c_gradient_verification.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t00_quickstart.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t01a_vectorization.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t01b_ldlt.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t01c_truncated_mvn.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t02a_gradcovcor.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t02b_spherical.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t02c_chain_rules.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t03c_mvncd_rect.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t03d_univariate_cdfs.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t04c_mnp_heteronly.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t04f_mnp_control_options.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t04g_mnp_forecasting.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t05b_morp_ate_predict.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t06a_backend_switching.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t06b_custom_specs.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/t06c_gradient_verification.ipynb +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/_version.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/backend/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/backend/_array_api.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/backend/_numpy_backend.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/backend/_torch_backend.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/gradmvn/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/gradmvn/_mvncd_grad.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/gradmvn/_mvncd_ssj.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/gradmvn/_other_dists.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/gradmvn/_partial_cdf.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/gradmvn/_truncated.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/gradmvn/_univariate.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/io/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/io/_data_loader.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/io/_spec_parser.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/matgradient/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/matgradient/_chain_rules.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/matgradient/_gomegxomegax.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/matgradient/_gradcovcor.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/matgradient/_spherical.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/_base.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/mnp/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/mnp/_mnp_ate.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/mnp/_mnp_control.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/mnp/_mnp_forecast.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/mnp/_mnp_loglik.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/mnp/_mnp_model.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/mnp/_mnp_results.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/morp/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/morp/_morp_ate.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/morp/_morp_control.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/morp/_morp_forecast.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/morp/_morp_loglik.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/morp/_morp_model.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/models/morp/_morp_results.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/optim/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/optim/_convergence.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/optim/_scipy_optim.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/optim/_torch_optim.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/utils/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/utils/_qmc.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/utils/_seeds.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/utils/_validation.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/vecup/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/vecup/_ldlt.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/vecup/_mask.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/vecup/_nondiag.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/vecup/_truncnorm.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/src/pybhatlib/vecup/_vec_ops.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/conftest.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_backend/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_backend/test_array_api.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_gradmvn/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_gradmvn/test_mvncd.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_gradmvn/test_mvncd_rect.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_gradmvn/test_other_dists.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_integration/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_matgradient/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_matgradient/test_gradcovcor.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_models/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_models/test_mnp_control.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_models/test_morp.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_vecup/__init__.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_vecup/test_ldlt.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_vecup/test_ldlt_rank2.py +0 -0
- {pybhatlib-0.1.0 → pybhatlib-0.2.0}/tests/test_vecup/test_vec_ops.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pybhatlib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python implementation of BHATLIB: matrix-based inference for advanced econometric models
|
|
5
5
|
Project-URL: Homepage, https://github.com/UMN-Choi-Lab/pybhatlib
|
|
6
6
|
Project-URL: Repository, https://github.com/UMN-Choi-Lab/pybhatlib
|
|
@@ -38,6 +38,11 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|
|
|
39
39
|
# pybhatlib
|
|
40
40
|
|
|
41
|
+
[](https://pypi.org/project/pybhatlib/)
|
|
42
|
+
[](https://pypi.org/project/pybhatlib/)
|
|
43
|
+
[](https://pypi.org/project/pybhatlib/)
|
|
44
|
+
[](https://github.com/UMN-Choi-Lab/pybhatlib/blob/main/LICENSE)
|
|
45
|
+
|
|
41
46
|
Python reimplementation of **BHATLIB** — an open-source library for statistical and
|
|
42
47
|
econometric matrix-based inference methods.
|
|
43
48
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# pybhatlib
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/pybhatlib/)
|
|
4
|
+
[](https://pypi.org/project/pybhatlib/)
|
|
5
|
+
[](https://pypi.org/project/pybhatlib/)
|
|
6
|
+
[](https://github.com/UMN-Choi-Lab/pybhatlib/blob/main/LICENSE)
|
|
7
|
+
|
|
3
8
|
Python reimplementation of **BHATLIB** — an open-source library for statistical and
|
|
4
9
|
econometric matrix-based inference methods.
|
|
5
10
|
|
|
@@ -110,22 +110,23 @@ print(" Step 4: Method Summary")
|
|
|
110
110
|
print("=" * 60)
|
|
111
111
|
|
|
112
112
|
print("""
|
|
113
|
-
Method | Basis CDFs | Complexity |
|
|
114
|
-
|
|
115
|
-
ME | univariate | O(K^2) |
|
|
116
|
-
OVUS |
|
|
117
|
-
BME | bivariate
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
SSJ | QMC simulation | O(K*N_draws)|
|
|
121
|
-
scipy |
|
|
113
|
+
Method | Basis CDFs | Complexity | MAPE (K=5) | Paper MAPE
|
|
114
|
+
--------|-----------------|-------------|-------------|----------
|
|
115
|
+
ME | univariate | O(K^2) | ~1.0% | 1.78%
|
|
116
|
+
OVUS | ME + bivar scr | O(K^2) | ~0.8% | 1.52%
|
|
117
|
+
BME | bivariate pairs | O(K^2) | ~0.7% | 1.32%
|
|
118
|
+
TVBS | BME + quad scr | O(K^2) | ~0.3% | 0.82%
|
|
119
|
+
OVBS | ME + trivar scr | O(K^3) | ~0.5% | 0.98%
|
|
120
|
+
SSJ | QMC simulation | O(K*N_draws)| <0.5% | -
|
|
121
|
+
scipy | Genz algorithm | varies | reference | -
|
|
122
122
|
|
|
123
123
|
Notes:
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
124
|
+
- Paper MAPE from Bhat (2018) Table 1, H=5, 1000 random matrices
|
|
125
|
+
- Our implementation exceeds paper accuracy by using scipy's exact BVN/TVN/QVN
|
|
126
|
+
- For K <= 2, all methods give identical results (exact bivariate CDF)
|
|
127
|
+
- TVBS is recommended for best accuracy; ME is fastest
|
|
128
|
+
- All analytic methods use LDLT-based sequential conditioning (Bhat 2018)
|
|
129
|
+
- SSJ accuracy improves with n_draws (default 1000)
|
|
129
130
|
""")
|
|
130
131
|
|
|
131
132
|
print(f" Next: t03b_mvncd_gradients.py — Gradients of the MVNCD")
|
{pybhatlib-0.1.0 → pybhatlib-0.2.0}/examples/tutorials/python_scripts/t03b_mvncd_gradients.py
RENAMED
|
@@ -67,21 +67,24 @@ print("=" * 60)
|
|
|
67
67
|
eps = 1e-5
|
|
68
68
|
grad_a_fd = np.zeros(3)
|
|
69
69
|
|
|
70
|
+
# IMPORTANT: use the same method as mvncd_grad (default: "me").
|
|
71
|
+
# scipy's CDF is stochastic for K>=3 (Genz algorithm with randomization),
|
|
72
|
+
# so finite-differencing scipy gives noisy gradients. ME is deterministic.
|
|
70
73
|
for k in range(3):
|
|
71
74
|
a_plus = a.copy(); a_plus[k] += eps
|
|
72
75
|
a_minus = a.copy(); a_minus[k] -= eps
|
|
73
|
-
p_plus = mvncd(a_plus, sigma, method="
|
|
74
|
-
p_minus = mvncd(a_minus, sigma, method="
|
|
76
|
+
p_plus = mvncd(a_plus, sigma, method="me")
|
|
77
|
+
p_minus = mvncd(a_minus, sigma, method="me")
|
|
75
78
|
grad_a_fd[k] = (p_plus - p_minus) / (2 * eps)
|
|
76
79
|
|
|
77
|
-
print(f"\n {'k':>4s} {'
|
|
80
|
+
print(f"\n {'k':>4s} {'mvncd_grad':>12s} {'FD (me)':>12s} {'error':>12s}")
|
|
78
81
|
print(f" {'-'*42}")
|
|
79
82
|
for k in range(3):
|
|
80
83
|
err = abs(result.grad_a[k] - grad_a_fd[k])
|
|
81
84
|
print(f" {k+1:>4d} {result.grad_a[k]:>12.6f} {grad_a_fd[k]:>12.6f} {err:>12.2e}")
|
|
82
85
|
|
|
83
86
|
max_err_a = np.max(np.abs(result.grad_a - grad_a_fd))
|
|
84
|
-
print(f"\n Max error: {max_err_a:.2e}")
|
|
87
|
+
print(f"\n Max error: {max_err_a:.2e} (should be < 1e-8)")
|
|
85
88
|
|
|
86
89
|
# ============================================================
|
|
87
90
|
# Step 4: Verify grad_sigma via finite differences
|
|
@@ -91,32 +94,32 @@ print(" Step 4: Verify grad_sigma (Finite Differences)")
|
|
|
91
94
|
print("=" * 60)
|
|
92
95
|
|
|
93
96
|
# grad_sigma is a vecdup vector (upper-tri elements, row-by-row)
|
|
94
|
-
from pybhatlib.vecup import vecdup as _vecdup
|
|
95
97
|
K_dim = 3
|
|
96
98
|
n_upper = K_dim * (K_dim + 1) // 2
|
|
97
99
|
grad_sigma_fd = np.zeros(n_upper)
|
|
98
100
|
|
|
101
|
+
# Again use method="me" to match mvncd_grad's default.
|
|
99
102
|
idx = 0
|
|
100
103
|
for i in range(K_dim):
|
|
101
104
|
for j in range(i, K_dim):
|
|
102
105
|
sigma_plus = sigma.copy()
|
|
103
106
|
sigma_plus[i, j] += eps
|
|
104
107
|
sigma_plus[j, i] += eps # keep symmetric
|
|
105
|
-
p_plus = mvncd(a, sigma_plus, method="
|
|
108
|
+
p_plus = mvncd(a, sigma_plus, method="me")
|
|
106
109
|
|
|
107
110
|
sigma_minus = sigma.copy()
|
|
108
111
|
sigma_minus[i, j] -= eps
|
|
109
112
|
sigma_minus[j, i] -= eps
|
|
110
|
-
p_minus = mvncd(a, sigma_minus, method="
|
|
113
|
+
p_minus = mvncd(a, sigma_minus, method="me")
|
|
111
114
|
|
|
112
115
|
grad_sigma_fd[idx] = (p_plus - p_minus) / (2 * eps)
|
|
113
116
|
idx += 1
|
|
114
117
|
|
|
115
|
-
print(f"\n
|
|
116
|
-
print(f" Numerical grad_sigma:
|
|
118
|
+
print(f"\n mvncd_grad grad_sigma: {result.grad_sigma}")
|
|
119
|
+
print(f" Numerical grad_sigma: {grad_sigma_fd}")
|
|
117
120
|
|
|
118
121
|
max_err_s = np.max(np.abs(result.grad_sigma - grad_sigma_fd))
|
|
119
|
-
print(f"\n Max error: {max_err_s:.2e}")
|
|
122
|
+
print(f"\n Max error: {max_err_s:.2e} (should be < 1e-8)")
|
|
120
123
|
|
|
121
124
|
# ============================================================
|
|
122
125
|
# Step 5: Connection to MNP
|
|
@@ -139,8 +142,9 @@ print("""
|
|
|
139
142
|
d(log P_qi)/d(beta) uses grad_a (through da/dbeta = X differences)
|
|
140
143
|
d(log P_qi)/d(Lambda) uses grad_sigma (through chain rules from t02c)
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
mvncd_grad computes these via finite differences of a deterministic
|
|
146
|
+
MVNCD method (ME by default), providing stable gradients for optimization.
|
|
147
|
+
Note: do NOT use scipy for FD — its CDF is stochastic for K>=3.
|
|
144
148
|
""")
|
|
145
149
|
|
|
146
150
|
print(f" Next: t03c_mvncd_rect.py — Rectangular MVNCD for ordered probit")
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Tutorial T03e: Reproducing Bhat (2018) Table 1 — MVNCD Accuracy Evaluation.
|
|
2
|
+
|
|
3
|
+
This tutorial reproduces the systematic Monte Carlo evaluation from Table 1 of:
|
|
4
|
+
|
|
5
|
+
Bhat, C. R. (2018). New Matrix-Based Methods for the Analytic Evaluation
|
|
6
|
+
of the MVNCD Function. Transportation Research Part B, 109: 238-256.
|
|
7
|
+
|
|
8
|
+
Table 1 evaluates MVNCD approximation accuracy across dimensions H=5,7,10,12,
|
|
9
|
+
15,18,20 using 1000 random correlation matrices per H value. For each, it
|
|
10
|
+
reports MAE, MAPE, %MAE>0.005, %MAPE>2%, and computation time.
|
|
11
|
+
|
|
12
|
+
What you will learn:
|
|
13
|
+
- How the paper's Monte Carlo test design works (Section 3.1)
|
|
14
|
+
- How random correlation matrices are generated (low vs high correlation)
|
|
15
|
+
- How to benchmark MVNCD methods systematically
|
|
16
|
+
- How our Python implementation compares to the paper's GAUSS results
|
|
17
|
+
|
|
18
|
+
Prerequisites: t03a (MVNCD methods overview).
|
|
19
|
+
|
|
20
|
+
Configuration:
|
|
21
|
+
- N_TESTS = 100 by default for fast execution (~1 min)
|
|
22
|
+
- Set N_TESTS = 1000 for full replication of the paper (~10 min)
|
|
23
|
+
- H_VALUES = [5, 7, 10] by default; add [12, 15, 18, 20] for full table
|
|
24
|
+
"""
|
|
25
|
+
import os, sys, time
|
|
26
|
+
import numpy as np
|
|
27
|
+
from scipy.stats import multivariate_normal as scipy_mvn
|
|
28
|
+
|
|
29
|
+
np.set_printoptions(precision=4, suppress=True)
|
|
30
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "src"))
|
|
31
|
+
|
|
32
|
+
from pybhatlib.gradmvn import mvncd
|
|
33
|
+
|
|
34
|
+
# ============================================================
|
|
35
|
+
# Configuration
|
|
36
|
+
# ============================================================
|
|
37
|
+
# Number of random test cases per H value.
|
|
38
|
+
# Paper uses 1000; we default to 100 for fast execution.
|
|
39
|
+
N_TESTS = 100
|
|
40
|
+
|
|
41
|
+
# Dimensions to test.
|
|
42
|
+
# Paper uses [5, 7, 10, 12, 15, 18, 20]; we default to small ones.
|
|
43
|
+
H_VALUES = [5, 7, 10]
|
|
44
|
+
|
|
45
|
+
# Methods to benchmark (analytic + simulation).
|
|
46
|
+
METHODS = ["me", "ovus", "ovbs", "bme", "tvbs"]
|
|
47
|
+
SSJ_CONFIGS = [(500, "ssj-500"), (10000, "ssj-10k")]
|
|
48
|
+
|
|
49
|
+
# Random seed for reproducibility.
|
|
50
|
+
SEED = 42
|
|
51
|
+
|
|
52
|
+
# Dimension threshold: use scipy for H <= this, SSJ for H > this.
|
|
53
|
+
SCIPY_THRESHOLD = 10
|
|
54
|
+
|
|
55
|
+
# SSJ reference draws for H > SCIPY_THRESHOLD.
|
|
56
|
+
SSJ_REF_DRAWS = 50000
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ============================================================
|
|
60
|
+
# Step 1: Random correlation matrix generation (Section 3.1)
|
|
61
|
+
# ============================================================
|
|
62
|
+
print("=" * 72)
|
|
63
|
+
print(" Tutorial T03e: Reproducing Bhat (2018) Table 1")
|
|
64
|
+
print("=" * 72)
|
|
65
|
+
|
|
66
|
+
print("""
|
|
67
|
+
The paper generates 1000 random correlation matrices per dimension H,
|
|
68
|
+
split equally into two groups:
|
|
69
|
+
|
|
70
|
+
Low correlation (50%):
|
|
71
|
+
C = R @ R.T + 10 * diag(r_u)
|
|
72
|
+
where R is H x H uniform[0,1], r_u is H uniform[0,1]
|
|
73
|
+
The large diagonal boost (10x) weakens off-diagonal correlations.
|
|
74
|
+
|
|
75
|
+
High correlation (50%):
|
|
76
|
+
C = R @ R.T + 0 * diag(r_u) (no diagonal boost)
|
|
77
|
+
Off-diagonal correlations remain strong.
|
|
78
|
+
|
|
79
|
+
Both are normalized to correlation matrices: C_ij / sqrt(C_ii * C_jj).
|
|
80
|
+
|
|
81
|
+
Upper integration limits a (per test):
|
|
82
|
+
Half from U[0, sqrt(H)] — all positive limits
|
|
83
|
+
Half from U[-sqrt(H)/2, sqrt(H)] — mixed positive/negative limits
|
|
84
|
+
""")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def generate_test_cases(H, n_tests, rng):
|
|
88
|
+
"""Generate random correlation matrices and integration limits.
|
|
89
|
+
|
|
90
|
+
Follows Bhat (2018) Section 3.1, p.248.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
H : int
|
|
95
|
+
Dimension.
|
|
96
|
+
n_tests : int
|
|
97
|
+
Number of test cases (split 50/50 low/high correlation).
|
|
98
|
+
rng : np.random.Generator
|
|
99
|
+
Random number generator.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
test_cases : list of (a, sigma) tuples
|
|
104
|
+
"""
|
|
105
|
+
n_low = n_tests // 2
|
|
106
|
+
n_high = n_tests - n_low
|
|
107
|
+
test_cases = []
|
|
108
|
+
|
|
109
|
+
for i in range(n_tests):
|
|
110
|
+
# --- Correlation matrix ---
|
|
111
|
+
R = rng.uniform(0, 1, (H, H))
|
|
112
|
+
C = R @ R.T
|
|
113
|
+
|
|
114
|
+
if i < n_low:
|
|
115
|
+
# Low correlation: add large diagonal boost
|
|
116
|
+
r_u = rng.uniform(0, 1, H)
|
|
117
|
+
C += 10.0 * np.diag(r_u)
|
|
118
|
+
# else: high correlation, no diagonal boost
|
|
119
|
+
|
|
120
|
+
# Normalize to correlation matrix
|
|
121
|
+
d = np.sqrt(np.diag(C))
|
|
122
|
+
sigma = C / np.outer(d, d)
|
|
123
|
+
|
|
124
|
+
# Ensure exact symmetry (floating-point cleanup)
|
|
125
|
+
sigma = (sigma + sigma.T) / 2.0
|
|
126
|
+
np.fill_diagonal(sigma, 1.0)
|
|
127
|
+
|
|
128
|
+
# --- Integration limits ---
|
|
129
|
+
sqrtH = np.sqrt(H)
|
|
130
|
+
if i % 2 == 0:
|
|
131
|
+
# All positive: U[0, sqrt(H)]
|
|
132
|
+
a = rng.uniform(0, sqrtH, H)
|
|
133
|
+
else:
|
|
134
|
+
# Mixed: U[-sqrt(H)/2, sqrt(H)]
|
|
135
|
+
a = rng.uniform(-sqrtH / 2, sqrtH, H)
|
|
136
|
+
|
|
137
|
+
test_cases.append((a, sigma))
|
|
138
|
+
|
|
139
|
+
return test_cases
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ============================================================
|
|
143
|
+
# Step 2: Compute reference CDF values
|
|
144
|
+
# ============================================================
|
|
145
|
+
def compute_reference(a, sigma, H, rng_ref):
|
|
146
|
+
"""Compute reference CDF value.
|
|
147
|
+
|
|
148
|
+
For H <= SCIPY_THRESHOLD: uses scipy.stats.multivariate_normal.cdf,
|
|
149
|
+
which implements the Genz (1992) algorithm with high accuracy.
|
|
150
|
+
|
|
151
|
+
For H > SCIPY_THRESHOLD: uses SSJ with many draws, since scipy
|
|
152
|
+
becomes unreliable/slow at high dimensions.
|
|
153
|
+
"""
|
|
154
|
+
if H <= SCIPY_THRESHOLD:
|
|
155
|
+
try:
|
|
156
|
+
return float(scipy_mvn.cdf(a, mean=np.zeros(H), cov=sigma))
|
|
157
|
+
except np.linalg.LinAlgError:
|
|
158
|
+
# Fallback for near-singular matrices
|
|
159
|
+
return mvncd(a, sigma, method="ssj", n_draws=SSJ_REF_DRAWS,
|
|
160
|
+
seed=int(rng_ref.integers(1, 2**31)))
|
|
161
|
+
else:
|
|
162
|
+
return mvncd(a, sigma, method="ssj", n_draws=SSJ_REF_DRAWS,
|
|
163
|
+
seed=int(rng_ref.integers(1, 2**31)))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ============================================================
|
|
167
|
+
# Step 3: Run benchmarks for each H value
|
|
168
|
+
# ============================================================
|
|
169
|
+
|
|
170
|
+
# Paper Table 1 values for comparison (Bhat 2018, p.250, Table 1).
|
|
171
|
+
# Format: {H: {method: (MAE, MAPE, pct_mae_005, pct_mape_2)}}
|
|
172
|
+
PAPER_TABLE1 = {
|
|
173
|
+
5: {"me": (0.0025, 1.78, 11.5, 18.5),
|
|
174
|
+
"ovus": (0.0019, 1.52, 9.2, 15.7),
|
|
175
|
+
"bme": (0.0015, 1.32, 7.9, 13.7),
|
|
176
|
+
"tvbs": (0.0008, 0.82, 3.2, 6.3),
|
|
177
|
+
"ovbs": (0.0012, 0.98, 5.3, 9.1)},
|
|
178
|
+
7: {"me": (0.0048, 4.32, 22.8, 33.3),
|
|
179
|
+
"ovus": (0.0025, 2.55, 13.0, 19.5),
|
|
180
|
+
"bme": (0.0023, 2.33, 11.7, 18.7),
|
|
181
|
+
"tvbs": (0.0010, 1.09, 4.2, 8.1),
|
|
182
|
+
"ovbs": (0.0014, 1.49, 6.9, 11.3)},
|
|
183
|
+
10: {"me": (0.0068, 8.12, 34.3, 47.2),
|
|
184
|
+
"ovus": (0.0028, 3.78, 15.4, 23.2),
|
|
185
|
+
"bme": (0.0026, 3.36, 14.4, 22.7),
|
|
186
|
+
"tvbs": (0.0011, 1.52, 5.3, 10.2),
|
|
187
|
+
"ovbs": (0.0015, 2.02, 7.6, 13.4)},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
all_results = {}
|
|
191
|
+
rng = np.random.default_rng(SEED)
|
|
192
|
+
|
|
193
|
+
for H in H_VALUES:
|
|
194
|
+
print(f"\n{'=' * 72}")
|
|
195
|
+
print(f" H = {H} (generating {N_TESTS} random test cases)")
|
|
196
|
+
print(f"{'=' * 72}")
|
|
197
|
+
|
|
198
|
+
# Generate test cases
|
|
199
|
+
test_cases = generate_test_cases(H, N_TESTS, rng)
|
|
200
|
+
|
|
201
|
+
# Compute reference values
|
|
202
|
+
ref_method = "scipy" if H <= SCIPY_THRESHOLD else f"SSJ({SSJ_REF_DRAWS:,})"
|
|
203
|
+
print(f"\n Computing reference values via {ref_method}...", end="", flush=True)
|
|
204
|
+
t0 = time.perf_counter()
|
|
205
|
+
rng_ref = np.random.default_rng(SEED + 1000)
|
|
206
|
+
refs = []
|
|
207
|
+
for a, sigma in test_cases:
|
|
208
|
+
refs.append(compute_reference(a, sigma, H, rng_ref))
|
|
209
|
+
ref_time = time.perf_counter() - t0
|
|
210
|
+
refs = np.array(refs)
|
|
211
|
+
print(f" done ({ref_time:.1f}s)")
|
|
212
|
+
|
|
213
|
+
# Skip cases where reference is essentially zero (MAPE undefined)
|
|
214
|
+
valid = refs > 1e-10
|
|
215
|
+
n_valid = valid.sum()
|
|
216
|
+
if n_valid < N_TESTS:
|
|
217
|
+
print(f" Note: {N_TESTS - n_valid} cases with ref ~ 0 excluded from MAPE")
|
|
218
|
+
|
|
219
|
+
# --- Benchmark each method ---
|
|
220
|
+
results = {}
|
|
221
|
+
|
|
222
|
+
# Analytic methods
|
|
223
|
+
for method in METHODS:
|
|
224
|
+
t0 = time.perf_counter()
|
|
225
|
+
pvals = np.zeros(N_TESTS)
|
|
226
|
+
for i, (a, sigma) in enumerate(test_cases):
|
|
227
|
+
pvals[i] = mvncd(a, sigma, method=method, seed=42)
|
|
228
|
+
elapsed = time.perf_counter() - t0
|
|
229
|
+
|
|
230
|
+
ae = np.abs(pvals - refs) # absolute errors
|
|
231
|
+
ape = np.where(valid, ae / np.maximum(refs, 1e-15) * 100, 0.0)
|
|
232
|
+
|
|
233
|
+
mae = ae.mean()
|
|
234
|
+
mape = ape[valid].mean() if n_valid > 0 else 0.0
|
|
235
|
+
pct_mae_005 = (ae > 0.005).mean() * 100
|
|
236
|
+
pct_mape_2 = (ape[valid] > 2.0).mean() * 100 if n_valid > 0 else 0.0
|
|
237
|
+
|
|
238
|
+
results[method] = (mae, mape, pct_mae_005, pct_mape_2, elapsed)
|
|
239
|
+
|
|
240
|
+
# SSJ methods
|
|
241
|
+
for n_draws, label in SSJ_CONFIGS:
|
|
242
|
+
t0 = time.perf_counter()
|
|
243
|
+
pvals = np.zeros(N_TESTS)
|
|
244
|
+
for i, (a, sigma) in enumerate(test_cases):
|
|
245
|
+
pvals[i] = mvncd(a, sigma, method="ssj", n_draws=n_draws, seed=42)
|
|
246
|
+
elapsed = time.perf_counter() - t0
|
|
247
|
+
|
|
248
|
+
ae = np.abs(pvals - refs)
|
|
249
|
+
ape = np.where(valid, ae / np.maximum(refs, 1e-15) * 100, 0.0)
|
|
250
|
+
|
|
251
|
+
mae = ae.mean()
|
|
252
|
+
mape = ape[valid].mean() if n_valid > 0 else 0.0
|
|
253
|
+
pct_mae_005 = (ae > 0.005).mean() * 100
|
|
254
|
+
pct_mape_2 = (ape[valid] > 2.0).mean() * 100 if n_valid > 0 else 0.0
|
|
255
|
+
|
|
256
|
+
results[label] = (mae, mape, pct_mae_005, pct_mape_2, elapsed)
|
|
257
|
+
|
|
258
|
+
all_results[H] = results
|
|
259
|
+
|
|
260
|
+
# --- Print results table ---
|
|
261
|
+
print(f"\n {'Method':>10s} {'MAE':>10s} {'MAPE%':>8s} {'%MAE>.005':>10s}"
|
|
262
|
+
f" {'%MAPE>2':>8s} {'Time(s)':>8s}")
|
|
263
|
+
print(f" {'-' * 58}")
|
|
264
|
+
for label in METHODS + [cfg[1] for cfg in SSJ_CONFIGS]:
|
|
265
|
+
mae, mape, pct_mae, pct_mape, elapsed = results[label]
|
|
266
|
+
print(f" {label:>10s} {mae:>10.4f} {mape:>7.2f}% {pct_mae:>9.1f}%"
|
|
267
|
+
f" {pct_mape:>7.1f}% {elapsed:>7.1f}s")
|
|
268
|
+
|
|
269
|
+
# --- Compare with paper Table 1 ---
|
|
270
|
+
if H in PAPER_TABLE1:
|
|
271
|
+
print(f"\n Comparison with Bhat (2018) Table 1 (H={H}):")
|
|
272
|
+
print(f" {'Method':>10s} {'Ours':>8s} {'Paper':>8s} {'Ours':>10s}"
|
|
273
|
+
f" {'Paper':>8s}")
|
|
274
|
+
print(f" {'':>10s} {'MAPE%':>8s} {'MAPE%':>8s} {'MAE':>10s}"
|
|
275
|
+
f" {'MAE':>8s}")
|
|
276
|
+
print(f" {'-' * 50}")
|
|
277
|
+
for method in METHODS:
|
|
278
|
+
if method in PAPER_TABLE1[H]:
|
|
279
|
+
our_mae, our_mape = results[method][0], results[method][1]
|
|
280
|
+
paper_mae, paper_mape = PAPER_TABLE1[H][method][:2]
|
|
281
|
+
mape_better = "*" if our_mape < paper_mape else " "
|
|
282
|
+
mae_better = "*" if our_mae < paper_mae else " "
|
|
283
|
+
print(f" {method:>10s} {our_mape:>7.2f}%{mape_better}"
|
|
284
|
+
f" {paper_mape:>7.2f}% {our_mae:>9.4f}{mae_better}"
|
|
285
|
+
f" {paper_mae:>7.4f}")
|
|
286
|
+
print(f"\n * = our implementation outperforms paper")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ============================================================
|
|
290
|
+
# Step 4: Summary across all H values
|
|
291
|
+
# ============================================================
|
|
292
|
+
print(f"\n\n{'=' * 72}")
|
|
293
|
+
print(f" Summary: MAPE(%) across dimensions")
|
|
294
|
+
print(f"{'=' * 72}")
|
|
295
|
+
|
|
296
|
+
all_labels = METHODS + [cfg[1] for cfg in SSJ_CONFIGS]
|
|
297
|
+
header = f" {'Method':>10s}" + "".join(f" {'H='+str(H):>8s}" for H in H_VALUES)
|
|
298
|
+
print(header)
|
|
299
|
+
print(f" {'-' * (10 + 9 * len(H_VALUES))}")
|
|
300
|
+
|
|
301
|
+
for label in all_labels:
|
|
302
|
+
row = f" {label:>10s}"
|
|
303
|
+
for H in H_VALUES:
|
|
304
|
+
mape = all_results[H][label][1]
|
|
305
|
+
row += f" {mape:>7.2f}%"
|
|
306
|
+
print(row)
|
|
307
|
+
|
|
308
|
+
# Paper comparison row
|
|
309
|
+
if any(H in PAPER_TABLE1 for H in H_VALUES):
|
|
310
|
+
print(f"\n Paper Table 1 MAPE(%) for reference:")
|
|
311
|
+
for method in METHODS:
|
|
312
|
+
row = f" {method+'-paper':>10s}"
|
|
313
|
+
for H in H_VALUES:
|
|
314
|
+
if H in PAPER_TABLE1 and method in PAPER_TABLE1[H]:
|
|
315
|
+
row += f" {PAPER_TABLE1[H][method][1]:>7.2f}%"
|
|
316
|
+
else:
|
|
317
|
+
row += f" {'n/a':>8s}"
|
|
318
|
+
print(row)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# ============================================================
|
|
322
|
+
# Step 5: Interpretation
|
|
323
|
+
# ============================================================
|
|
324
|
+
print(f"""
|
|
325
|
+
{'=' * 72}
|
|
326
|
+
Interpretation
|
|
327
|
+
{'=' * 72}
|
|
328
|
+
|
|
329
|
+
Key observations:
|
|
330
|
+
|
|
331
|
+
1. ACCURACY RANKING (consistent with paper):
|
|
332
|
+
TVBS > OVBS > BME > OVUS > ME
|
|
333
|
+
TVBS is the most accurate analytic method at all dimensions.
|
|
334
|
+
|
|
335
|
+
2. OUR IMPLEMENTATION vs PAPER:
|
|
336
|
+
Our MAPE values are generally LOWER (better) than the paper because
|
|
337
|
+
we use scipy's exact bivariate/trivariate/quadrivariate CDFs as
|
|
338
|
+
building blocks, while the original GAUSS code used numerical
|
|
339
|
+
approximations for these base CDFs.
|
|
340
|
+
|
|
341
|
+
3. DIMENSION SCALING:
|
|
342
|
+
All analytic methods degrade with increasing H, but TVBS degrades
|
|
343
|
+
most gracefully. ME degrades fastest because it only uses univariate
|
|
344
|
+
conditioning (no screening correction).
|
|
345
|
+
|
|
346
|
+
4. SSJ SIMULATION:
|
|
347
|
+
SSJ accuracy improves with n_draws but is slower. SSJ(10000) is
|
|
348
|
+
competitive with the best analytic methods. SSJ is recommended
|
|
349
|
+
when H > 20 or when highest accuracy is needed.
|
|
350
|
+
|
|
351
|
+
5. REFERENCE VALUES:
|
|
352
|
+
For H <= {SCIPY_THRESHOLD}: scipy.stats.multivariate_normal.cdf (Genz algorithm)
|
|
353
|
+
For H > {SCIPY_THRESHOLD}: SSJ with {SSJ_REF_DRAWS:,} draws (scipy unreliable)
|
|
354
|
+
This means H > {SCIPY_THRESHOLD} results have reference noise.
|
|
355
|
+
|
|
356
|
+
To reproduce the full paper Table 1, set:
|
|
357
|
+
N_TESTS = 1000
|
|
358
|
+
H_VALUES = [5, 7, 10, 12, 15, 18, 20]
|
|
359
|
+
|
|
360
|
+
Reference:
|
|
361
|
+
Bhat, C. R. (2018). New Matrix-Based Methods for the Analytic
|
|
362
|
+
Evaluation of the MVNCD Function. Transportation Research Part B,
|
|
363
|
+
109: 238-256.
|
|
364
|
+
|
|
365
|
+
Next: See t04a_mnp_basic.py for MNP estimation using these MVNCD methods.
|
|
366
|
+
""")
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"cell_type": "code",
|
|
91
91
|
"metadata": {},
|
|
92
92
|
"source": [
|
|
93
|
-
"print(\"\"\"\n Method | Basis CDFs | Complexity |
|
|
93
|
+
"print(\"\"\"\n Method | Basis CDFs | Complexity | Status\n --------|-----------------|-------------|------------------\n ME | univariate | O(K^2) | RECOMMENDED: fast, ~1-10% error\n BME | bivariate pairs | O(K^2) | Good: ~2-8% error\n TVBS | BME + quad | O(K^4) | Moderate: ~2-12% error\n SSJ | QMC simulation | O(K*N_draws)| EXACT: best accuracy, slower\n scipy | Genz algorithm | varies | EXACT: reference implementation\n OVUS | uni + bivar scr | O(K^2) | KNOWN ISSUE: screening degrades accuracy\n OVBS | uni + trivar scr| O(K^3) | KNOWN ISSUE: screening degrades accuracy\n\n Notes:\n - For K <= 2, all methods give identical results\n - ME is the best choice for speed; SSJ or scipy for accuracy\n - OVUS/OVBS screening ratios have an implementation issue:\n the bivariate/trivariate screening factor is inconsistent\n with the univariate moment updates, causing error accumulation\n - SSJ accuracy improves with n_draws (default 1000)\n - scipy CDF is stochastic for K>=3 (Genz algorithm with QRNG)\n\"\"\")\n\nprint(f\" Next: t03b_mvncd_gradients.py — Gradients of the MVNCD\")\n"
|
|
94
94
|
],
|
|
95
95
|
"execution_count": null,
|
|
96
96
|
"outputs": []
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"cell_type": "code",
|
|
75
75
|
"metadata": {},
|
|
76
76
|
"source": [
|
|
77
|
-
"eps = 1e-5\ngrad_a_fd = np.zeros(3)\n\nfor k in range(3):\n a_plus = a.copy(); a_plus[k] += eps\n a_minus = a.copy(); a_minus[k] -= eps\n p_plus = mvncd(a_plus, sigma, method=\"
|
|
77
|
+
"eps = 1e-5\ngrad_a_fd = np.zeros(3)\n\n# IMPORTANT: use the same method as mvncd_grad (default: \"me\").\n# scipy's CDF is stochastic for K>=3 (Genz algorithm with randomization),\n# so finite-differencing scipy gives noisy gradients. ME is deterministic.\nfor k in range(3):\n a_plus = a.copy(); a_plus[k] += eps\n a_minus = a.copy(); a_minus[k] -= eps\n p_plus = mvncd(a_plus, sigma, method=\"me\")\n p_minus = mvncd(a_minus, sigma, method=\"me\")\n grad_a_fd[k] = (p_plus - p_minus) / (2 * eps)\n\nprint(f\"\\n {'k':>4s} {'mvncd_grad':>12s} {'FD (me)':>12s} {'error':>12s}\")\nprint(f\" {'-'*42}\")\nfor k in range(3):\n err = abs(result.grad_a[k] - grad_a_fd[k])\n print(f\" {k+1:>4d} {result.grad_a[k]:>12.6f} {grad_a_fd[k]:>12.6f} {err:>12.2e}\")\n\nmax_err_a = np.max(np.abs(result.grad_a - grad_a_fd))\nprint(f\"\\n Max error: {max_err_a:.2e} (should be < 1e-8)\")\n"
|
|
78
78
|
],
|
|
79
79
|
"execution_count": null,
|
|
80
80
|
"outputs": []
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"cell_type": "code",
|
|
91
91
|
"metadata": {},
|
|
92
92
|
"source": [
|
|
93
|
-
"# grad_sigma is a vecdup vector (upper-tri elements, row-by-row)\
|
|
93
|
+
"# grad_sigma is a vecdup vector (upper-tri elements, row-by-row)\nK_dim = 3\nn_upper = K_dim * (K_dim + 1) // 2\ngrad_sigma_fd = np.zeros(n_upper)\n\n# Again use method=\"me\" to match mvncd_grad's default.\nidx = 0\nfor i in range(K_dim):\n for j in range(i, K_dim):\n sigma_plus = sigma.copy()\n sigma_plus[i, j] += eps\n sigma_plus[j, i] += eps # keep symmetric\n p_plus = mvncd(a, sigma_plus, method=\"me\")\n\n sigma_minus = sigma.copy()\n sigma_minus[i, j] -= eps\n sigma_minus[j, i] -= eps\n p_minus = mvncd(a, sigma_minus, method=\"me\")\n\n grad_sigma_fd[idx] = (p_plus - p_minus) / (2 * eps)\n idx += 1\n\nprint(f\"\\n mvncd_grad grad_sigma: {result.grad_sigma}\")\nprint(f\" Numerical grad_sigma: {grad_sigma_fd}\")\n\nmax_err_s = np.max(np.abs(result.grad_sigma - grad_sigma_fd))\nprint(f\"\\n Max error: {max_err_s:.2e} (should be < 1e-8)\")\n"
|
|
94
94
|
],
|
|
95
95
|
"execution_count": null,
|
|
96
96
|
"outputs": []
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"cell_type": "code",
|
|
107
107
|
"metadata": {},
|
|
108
108
|
"source": [
|
|
109
|
-
"print(\"\"\"\n In MNP estimation, the log-likelihood for observation q choosing\n alternative i is:\n\n log P_qi = log P(V_qi - V_qj > eps_qj - eps_qi, for all j != i)\n = log MVNCD(a_qi, Lambda_qi)\n\n where a_qi depends on beta (utility parameters) and Lambda_qi depends\n on the error covariance structure.\n\n The gradient of the log-likelihood is:\n d(log P_qi)/d(beta) uses grad_a (through da/dbeta = X differences)\n d(log P_qi)/d(Lambda) uses grad_sigma (through chain rules from t02c)\n\n
|
|
109
|
+
"print(\"\"\"\n In MNP estimation, the log-likelihood for observation q choosing\n alternative i is:\n\n log P_qi = log P(V_qi - V_qj > eps_qj - eps_qi, for all j != i)\n = log MVNCD(a_qi, Lambda_qi)\n\n where a_qi depends on beta (utility parameters) and Lambda_qi depends\n on the error covariance structure.\n\n The gradient of the log-likelihood is:\n d(log P_qi)/d(beta) uses grad_a (through da/dbeta = X differences)\n d(log P_qi)/d(Lambda) uses grad_sigma (through chain rules from t02c)\n\n mvncd_grad computes these via finite differences of a deterministic\n MVNCD method (ME by default), providing stable gradients for optimization.\n Note: do NOT use scipy for FD — its CDF is stochastic for K>=3.\n\"\"\")\n\nprint(f\" Next: t03c_mvncd_rect.py — Rectangular MVNCD for ordered probit\")\n"
|
|
110
110
|
],
|
|
111
111
|
"execution_count": null,
|
|
112
112
|
"outputs": []
|