Bayesian_Zahra_v1.1.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. # Constrained Bayesian fit (Gamma for NC, Beta-Prime for AE) — no regularization, no r-prior
  2. import numpy as np
  3. import matplotlib.pyplot as plt
  4. from scipy import optimize
  5. from scipy.special import betaln, gammaln
  6. import scipy.io as io
  7. import os
  8. # determine git root
  9. root = os.popen("git rev-parse --show-toplevel").read().strip()
  10. suv = io.loadmat(os.path.join(root, "data", "suv_percentilesSLOthenUWM.mat"))['lung_SUVperc_COMBINED'][0:58, :, :]
  11. flags = io.loadmat(os.path.join(root, "data", "flags_combined.mat"))['flags'][0:58, 3] # 0=NC, 1=AE
  12. # Feature X = max SUV_94 per subject; label y = flags
  13. X = np.nanmax(suv[:, :, 94], axis=1).astype(float).ravel()
  14. y = np.asarray(flags, int).ravel()
  15. # Guard for logs
  16. X = np.clip(X, 1e-12, None)
  17. p_emp = float(y.mean())
  18. # Small helpers
  19. def logistic(z):
  20. z = np.clip(z, -60, 60)
  21. return 1.0 / (1.0 + np.exp(-z))
  22. def sigmoid(t):
  23. return 1.0 / (1.0 + np.exp(-t))
  24. def softplus(t):
  25. t = np.asarray(t, float)
  26. return np.log1p(np.exp(-np.abs(t))) + np.maximum(t, 0.0)
  27. # dE(x) pieces for log-odds
  28. def dE_ess(x, a, b, s, k, th):
  29. x = np.asarray(x, float)
  30. return (a - k) * np.log(x) - (a + b) * np.log1p(x / s) + x / th
  31. def dE_const(a, b, s, k, th):
  32. return -(a * np.log(s)) - betaln(a, b) + k * np.log(th) + gammaln(k)
  33. def dE_full(x, a, b, s, k, th):
  34. return dE_ess(x, a, b, s, k, th) + dE_const(a, b, s, k, th)
  35. # Monotonicity cap for theta
  36. def theta_max(a, b, k, s, eps=1e-12):
  37. A = a - k
  38. if A <= 0:
  39. return np.inf
  40. r = np.sqrt(a + b) - np.sqrt(max(A, eps))
  41. if r <= 1e-12:
  42. return np.inf
  43. return s / (r * r)
  44. # φ = [p_raw, b_raw, s_raw, k_raw, d_raw, u_raw] (unconstrained)
  45. def unpack_phi_mono(phi):
  46. p_raw, b_raw, s_raw, k_raw, d_raw, u_raw = phi
  47. p = sigmoid(p_raw) # (0,1)
  48. b = softplus(b_raw) + 1e-6 # >0
  49. s = softplus(s_raw) + 1e-6 # >0
  50. k = softplus(k_raw) + 1e-6 # >0
  51. delta = softplus(d_raw) + 1e-6 # >0
  52. a = k + delta # enforce a > k
  53. th_cap = theta_max(a, b, k, s) # theta cap from monotonicity
  54. th = th_cap * sigmoid(u_raw) # 0 < theta <= th_cap
  55. return p, a, b, s, k, th
  56. # Prior on p
  57. TAU = 25.0 # shrink toward empirical AE rate
  58. alpha = max(TAU * p_emp, 1e-6)
  59. beta = max(TAU * (1.0 - p_emp), 1e-6)
  60. prior_r = None
  61. #prior_r = (1.01, 1.01)
  62. #prior_r = (3, 3)
  63. # Objective: negative log-posterior (likelihood + Beta prior on p)
  64. def neg_post_phi_mono(phi, X, y):
  65. p, a, b, s, k, th = unpack_phi_mono(phi)
  66. eps = 1e-12
  67. L = (np.log(p) - np.log(1 - p)) + dE_full(X, a, b, s, k, th)
  68. px = logistic(L)
  69. nll = -np.sum(y * np.log(px + eps) + (1 - y) * np.log(1 - px + eps))
  70. # Beta(alpha, beta) prior on p → negative log-prior
  71. npr_p = -((alpha - 1) * np.log(p + eps) + (beta - 1) * np.log(1 - p + eps))
  72. if prior_r is None: return nll + npr_p
  73. # Prior on r = theta / theta_max (softly avoid boundaries)
  74. thcap = theta_max(a, b, k, s)
  75. if np.isfinite(thcap) and thcap > 0:
  76. r = np.clip(th / thcap, 1e-9, 1 - 1e-9)
  77. # Negative log Beta prior: -[(α-1)log r + (β-1)log(1-r)] (const dropped)
  78. npr_r = -((prior_r[0] -1) * np.log(r) + (prior_r[1]-1)* np.log(1.0 - r))
  79. else:
  80. npr_r = 0.0
  81. return nll + npr_p + npr_r
  82. # Initialization (stable, simple)
  83. def init_phi(X, y):
  84. # Gamma(k, theta) MoM for NC group (only need k0 as a safe size proxy)
  85. X0 = X[y == 0]
  86. m0 = X0.mean() if X0.size else X.mean()
  87. v0 = X0.var() if X0.size else X.var()
  88. k0 = 2.0 if v0 <= 0 else max((m0**2)/(v0 + 1e-9), 1.5)
  89. # AE median to seed s0
  90. X1 = X[y == 1]
  91. m1 = np.median(X1) if X1.size else np.median(X)
  92. p0 = np.clip(float(y.mean()), 1e-3, 1 - 1e-3)
  93. b0, s0 = 1.5, max(m1, 0.5)
  94. return np.array([
  95. np.log(p0 / (1 - p0)), # p_raw
  96. np.log(np.expm1(b0) + 1e-9), # b_raw
  97. np.log(np.expm1(s0) + 1e-9), # s_raw
  98. np.log(np.expm1(k0) + 1e-9), # k_raw
  99. np.log(np.expm1(1.0) + 1e-9), # d_raw (delta)
  100. -0.2 # u_raw (keeps theta a bit below cap initially)
  101. ], float)
  102. # Fit wrapper (one retry)
  103. def fit_bayes_mono(X, y, phi_start=None, rng=None):
  104. if rng is None:
  105. rng = np.random.default_rng(0)
  106. if phi_start is None:
  107. phi_start = init_phi(X, y)
  108. obj = lambda phi: neg_post_phi_mono(phi, X, y)
  109. res = optimize.minimize(
  110. obj, phi_start, method="L-BFGS-B",
  111. options={"maxiter": 6000, "ftol": 1e-9}
  112. )
  113. if not (res.success and np.isfinite(res.fun)):
  114. phi_try = phi_start + rng.normal(0, 0.2, size=phi_start.shape)
  115. res = optimize.minimize(
  116. obj, phi_try, method="L-BFGS-B",
  117. options={"maxiter": 6000, "ftol": 1e-9}
  118. )
  119. return unpack_phi_mono(res.x), res
  120. # prediction
  121. def P_with(theta, x):
  122. p, a, b, s, k, th = theta
  123. L = (np.log(p) - np.log(1 - p)) + dE_full(x, a, b, s, k, th)
  124. return logistic(L)
  125. # Run fit + plot
  126. theta_hat, res = fit_bayes_mono(X, y)
  127. print("Optimization success:", res.success, " fval:", float(res.fun))
  128. (p,a,b,s,k,th) = theta_hat
  129. thcap = theta_max(a, b, k, s)
  130. print("theta (p,a,b,s,k,theta):", tuple(float(t) for t in theta_hat), "ratio(th):", th/thcap)
  131. # x-range (cap right end at 10 for readability)
  132. x_lo = max(1e-6, float(X.min()) * 0.8)
  133. x_hi = min(10.0, float(X.max()) * 1.2)
  134. xg = np.linspace(x_lo, x_hi, 600)
  135. p_curve = P_with(theta_hat, xg)
  136. fig, ax = plt.subplots(figsize=(7.0, 4.6), dpi=140)
  137. ax.plot(xg, p_curve, color="#000000", lw=2.2, label="P(AE|x) (MAP)")
  138. # overlay data with tiny vertical jitter so points don't overlap
  139. rng_plot = np.random.default_rng(999)
  140. jit = (rng_plot.random(len(y)) - 0.5) * 0.06
  141. ax.scatter(X[y==0], (y + jit)[y==0], s=22, alpha=0.55, color="#2ca02c", edgecolors='none', label='NC')
  142. ax.scatter(X[y==1], (y + jit)[y==1], s=26, alpha=0.75, color="#ff7f0e", edgecolors='none', label='AE')
  143. ax.set_ylim(-0.05, 1.05)
  144. ax.set_xlabel('x')
  145. ax.set_ylabel('P(AE | x)')
  146. if prior_r is None:
  147. ax.set_title('Constrained Bayesian fit (no regularization, prior on p)')
  148. else:
  149. ax.set_title(f'Constrained Bayesian fit (no regularization, prior on p and prior r{prior_r})')
  150. ax.grid(alpha=0.3)
  151. ax.legend(loc='lower right', frameon=False)
  152. plt.tight_layout()
  153. plt.show()