| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- """Controlled numerical diagnostic for constrained-Bayesian MCA.
- Data are generated exactly from the Gamma/Betaprime class-conditional model.
- This checks numerical behavior under the model assumptions; it is not a
- clinical validation or a simulation study of statistical performance.
- """
- from __future__ import annotations
- import os
- import sys
- from pathlib import Path
- import matplotlib
- matplotlib.use("Agg")
- import matplotlib.pyplot as plt
- import numpy as np
- import pandas as pd
- from scipy import stats
- EXAMPLE_DIR = Path(__file__).resolve().parent
- PROJECT_ROOT = EXAMPLE_DIR.parents[1]
- if str(PROJECT_ROOT.parent) not in sys.path:
- sys.path.insert(0, str(PROJECT_ROOT.parent))
- from organized_uncertainty_analysis.bayesian.core import (
- P_with,
- estimate_ci_bundle,
- slope_at_x,
- theta_max,
- x_at_p,
- )
- SEED = 20260914
- SAMPLE_SIZES = (1000, 500, 200, 60)
- MCA_DRAWS = int(os.getenv("BAYES_MCA_DIAGNOSTIC_DRAWS", "5000"))
- OUTPUT_DIR = Path(
- os.getenv("BAYES_MCA_DIAGNOSTIC_OUTPUT", str(EXAMPLE_DIR / "mca_diagnostic_outputs"))
- ).resolve()
- OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
- # Feasible generating parameters: omega, a, b, s, k, vartheta.
- OMEGA = 0.30
- A = 8.0
- B = 5.0
- S = 4.0
- K = 5.0
- VARTTHETA = 1.0
- CAP = theta_max(A, B, K, S)
- if not (0.0 < VARTTHETA < CAP):
- raise RuntimeError("The selected generating parameters violate the model constraints.")
- TRUE_THETA = (OMEGA, A, B, S, K, VARTTHETA, CAP)
- def generate_exact_model_data(n: int, seed: int) -> tuple[np.ndarray, np.ndarray]:
- """Draw y from prevalence and x from the matching class distribution."""
- rng = np.random.default_rng(seed)
- y = rng.binomial(1, OMEGA, size=n).astype(int)
- x = np.empty(n, dtype=float)
- nc = y == 0
- ae = ~nc
- x[nc] = rng.gamma(shape=K, scale=VARTTHETA, size=int(nc.sum()))
- x[ae] = stats.betaprime.rvs(A, B, scale=S, size=int(ae.sum()), random_state=rng)
- return np.clip(x, 1e-12, None), y
- def interval(values: np.ndarray) -> tuple[float, float]:
- values = np.asarray(values, float)
- values = values[np.isfinite(values)]
- if not len(values):
- return np.nan, np.nan
- lo, hi = np.quantile(values, [0.025, 0.975])
- return float(lo), float(hi)
- def run_one(n: int, seed: int):
- x, y = generate_exact_model_data(n, seed)
- x_upper = max(float(np.quantile(x, 0.995) * 1.10), 12.0)
- x_grid = np.linspace(max(1e-5, float(x.min()) * 0.75), x_upper, 400)
- result = estimate_ci_bundle(
- x,
- y,
- f"n={n}",
- x_grid,
- B_nonpar=0,
- B_param=0,
- M_mca=MCA_DRAWS,
- mca_seed=seed + 10000,
- seed=seed,
- use_prior_p=True,
- prior_r=(1.05, 1.05),
- tau=25.0,
- n_starts_clean=5,
- n_starts_boot=1,
- progress_every=max(MCA_DRAWS, 1),
- max_hessian_condition=1e6,
- )
- h = result["hessian_diagnostics"]
- m = result["diagnostics_mca"]
- x50_lo, x50_hi = interval(result["x50_mca"])
- s50_lo, s50_hi = interval(result["s50_mca"])
- band_width = np.asarray(result["mca_hi"]) - np.asarray(result["mca_lo"])
- summary = {
- "n": n,
- "events": int(y.sum()),
- "event_fraction": float(y.mean()),
- "objective": float(result["objective"]),
- "x50_hat": float(result["x50"]),
- "x50_mca_lo": x50_lo,
- "x50_mca_hi": x50_hi,
- "s50_hat": float(result["s50"]),
- "s50_mca_lo": s50_lo,
- "s50_mca_hi": s50_hi,
- "hessian_min_eigenvalue": float(h["minimum_eigenvalue"]),
- "hessian_original_condition": float(h["original_condition_number"]),
- "hessian_positive_definite": bool(h["original_positive_definite"]),
- "hessian_stabilized": bool(h["stabilized"]),
- "hessian_reliable": bool(h["reliable"]),
- "mca_attempted": int(m["attempted"]),
- "mca_accepted": int(m["successful_curve"]),
- "mca_acceptance_fraction": float(m["successful_curve"] / max(m["attempted"], 1)),
- "mca_band_mean_width": float(np.mean(band_width)),
- "mca_band_max_width": float(np.max(band_width)),
- }
- curve_table = pd.DataFrame(
- {
- "n": n,
- "x": x_grid,
- "true_probability": P_with(TRUE_THETA, x_grid),
- "fitted_probability": result["pmap"],
- "mca_lower": result["mca_lo"],
- "mca_upper": result["mca_hi"],
- }
- )
- return summary, curve_table
- def main() -> None:
- true_x50 = x_at_p(TRUE_THETA, hi=20.0, hi_max=200.0)
- true_s50 = slope_at_x(TRUE_THETA, true_x50)
- summaries = []
- curves = []
- for index, n in enumerate(SAMPLE_SIZES):
- summary, curve = run_one(n, SEED + index)
- summaries.append(summary)
- curves.append(curve)
- summary_table = pd.DataFrame(summaries)
- summary_table.insert(3, "true_x50", true_x50)
- summary_table.insert(7, "true_s50", true_s50)
- curve_table = pd.concat(curves, ignore_index=True)
- summary_table.to_csv(OUTPUT_DIR / "mca_diagnostic_summary.csv", index=False)
- curve_table.to_csv(OUTPUT_DIR / "mca_diagnostic_curves.csv", index=False)
- fig, axes = plt.subplots(2, 2, figsize=(12, 8), sharey=True)
- for ax, n in zip(axes.flat, SAMPLE_SIZES):
- block = curve_table[curve_table["n"] == n]
- ax.fill_between(block["x"], block["mca_lower"], block["mca_upper"], alpha=0.22)
- ax.plot(block["x"], block["true_probability"], "--", color="tab:orange", label="generating risk")
- ax.plot(block["x"], block["fitted_probability"], color="black", label="MAP fit")
- ax.set_title(f"n={n}")
- ax.set_xlabel("x")
- ax.set_ylim(-0.02, 1.02)
- axes[0, 0].set_ylabel("P(y=1 | x)")
- axes[1, 0].set_ylabel("P(y=1 | x)")
- axes[0, 0].legend(frameon=True)
- fig.tight_layout()
- fig.savefig(OUTPUT_DIR / "mca_diagnostic.png", dpi=300, bbox_inches="tight")
- fig.savefig(OUTPUT_DIR / "mca_diagnostic.pdf", bbox_inches="tight")
- plt.close(fig)
- print(f"True x50={true_x50:.6f}; true s50={true_s50:.6f}")
- print(summary_table.to_string(index=False))
- print(f"\nOutputs: {OUTPUT_DIR}")
- if __name__ == "__main__":
- main()
|