Bayesian_Zahra_v1.1.py 6.2 KB

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