Bayesian_Zahra.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. #BetaPrime vs Gamma, hard-mono fit WITH constant + weak regularization
  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. # === Load data ===
  8. data_path = "../data/"
  9. suv = io.loadmat(data_path + "suv_percentilesSLOthenUWM.mat")['lung_SUVperc_COMBINED'][0:58, :, :]
  10. flags = io.loadmat(data_path + "flags_combined.mat")['flags'][0:58, 3]
  11. X = np.nanmax(suv[:, :, 94], axis=1).reshape(-1)
  12. X_NC = X[flags == 0]
  13. X_AE = X[flags == 1]
  14. y = np.array([1]*len(X_AE) + [0]*len(X_NC), int) # 1=AE, 0=NC
  15. X = np.concatenate([X_AE, X_NC], axis=0)
  16. # sanity checks now that X,y actually exist
  17. n = len(y); n1 = int(y.sum()); p_emp = n1 / n
  18. rng = np.random.default_rng(12345)
  19. # Helpers
  20. def logistic(z):
  21. z = np.clip(z, -60, 60)
  22. return 1.0/(1.0+np.exp(-z))
  23. def sigmoid(t):
  24. return 1.0/(1.0+np.exp(-t))
  25. def softplus(t):
  26. t = np.asarray(t, float)
  27. return np.log1p(np.exp(-np.abs(t))) + np.maximum(t, 0.0)
  28. # Eq: logit P(AE|x) = log(p/(1-p)) + dE(x)
  29. # where dE(x) = dE_ess(x) + C(params)
  30. def dE_ess(x, a, b, s, k, th):
  31. # Essential (x-dependent) terms:
  32. # (a - k) * log(x) - (a + b) * log(1 + x/s) + x/th
  33. x = np.asarray(x, float)
  34. return (a - k) * np.log(x) - (a + b) * np.log1p(x/s) + x/th
  35. def dE_const(a, b, s, k, th):
  36. # Constant (parameter-only) terms:
  37. # C = -a*log(s) - log B(a,b) + k*log(th) + log Γ(k)
  38. return -(a*np.log(s)) - betaln(a, b) + k*np.log(th) + gammaln(k)
  39. def dE_full(x, a, b, s, k, th):
  40. # Total evidence term: dE(x) = dE_ess(x) + C
  41. return dE_ess(x, a, b, s, k, th) + dE_const(a, b, s, k, th)
  42. # Global monotonicity cap for theta
  43. def theta_max(a, b, k, s, eps=1e-12):
  44. A = a - k
  45. if A <= 0:
  46. return np.inf
  47. r = np.sqrt(a + b) - np.sqrt(max(A, eps))
  48. if r <= 1e-12:
  49. return np.inf
  50. return s/(r*r)
  51. # phi = [p_raw, b_raw, s_raw, k_raw, d_raw, u_raw]
  52. def unpack_phi_mono(phi):
  53. p_raw, b_raw, s_raw, k_raw, d_raw, u_raw = phi
  54. # Map raw parameters into valid constrained space:
  55. # p = sigmoid(p_raw) ∈ (0,1) → AE prior (class prior)
  56. p = sigmoid(p_raw)
  57. # b = softplus(b_raw) > 0 → Beta–Prime shape parameter
  58. b = softplus(b_raw) + 1e-6
  59. # s = softplus(s_raw) > 0 → Beta–Prime scale parameter
  60. s = softplus(s_raw) + 1e-6
  61. # k = softplus(k_raw) > 0 → Gamma shape parameter
  62. k = softplus(k_raw) + 1e-6
  63. # delta = softplus(d_raw) > 0; a = k + delta > k
  64. delta = softplus(d_raw) + 1e-6
  65. a = k + delta
  66. # θ (theta) is constrained: 0 < θ ≤ θ_max(a,b,k,s)
  67. th_cap = theta_max(a, b, k, s)
  68. th = th_cap * sigmoid(u_raw) # map u_raw ∈ R into (0, th_cap]
  69. return p, a, b, s, k, th
  70. # Priors
  71. # Beta prior on p centered at empirical rate
  72. TAU = 25.0 # reduce to ~5 if you want it weaker
  73. alpha = max(TAU * float(p_emp), 1e-6)
  74. beta = max(TAU * (1.0 - float(p_emp)), 1e-6)
  75. # Weak log-normal shrinkage on positive parameters
  76. def nlog_lognormal(x, mu, sigma, eps=1e-12):
  77. # -log LogNormal(x | mu, sigma) up to additive const
  78. x = np.maximum(x, eps)
  79. lx = np.log(x)
  80. return 0.5 * ((lx - mu)/sigma)**2 + lx
  81. # Objective
  82. def neg_post_phi_mono_WITH_CONST_REG(phi, X, y):
  83. p, a, b, s, k, th = unpack_phi_mono(phi)
  84. eps = 1e-12
  85. # Likelihood with constant included
  86. z = (np.log(p) - np.log(1-p)) + dE_full(X, a, b, s, k, th)
  87. px = logistic(z)
  88. nll = -np.sum(y*np.log(px + eps) + (1-y)*np.log(1 - px + eps))
  89. # Prior on p ~ Beta(alpha, beta)
  90. npr_p = -((alpha-1)*np.log(p + eps) + (beta-1)*np.log(1 - p + eps))
  91. # Regularization (weak priors)
  92. # AE median m1: use AE median if present; otherwise overall median.
  93. if (y == 1).any():
  94. m1 = np.median(X[y == 1])
  95. else:
  96. m1 = np.median(X)
  97. reg = 0.0 # total penalty starts at zero
  98. # 1) Gamma shape k (>0): very weak prior centered at 2 (σ=1.2).
  99. reg += nlog_lognormal(k, mu=np.log(2.0), sigma=1.2)
  100. # 2) Beta-Prime shape b (>0): same weak prior.
  101. reg += nlog_lognormal(b, mu=np.log(2.0), sigma=1.2)
  102. # 3) Beta-Prime scale s (>0): center near AE median (tighter σ=0.5).
  103. reg += nlog_lognormal(s, mu=np.log(max(m1, 1e-6)), sigma=0.5)
  104. # 4) Left-tail gap delta = a - k (>0): center around ~1.5 (σ=0.5)
  105. delta = a - k
  106. reg += nlog_lognormal(delta, mu=np.log(1.5), sigma=0.5)
  107. # Keep theta away from the boundary: Beta(3,3) on r = th/th_cap
  108. thcap = theta_max(a, b, k, s)
  109. if np.isfinite(thcap) and thcap > 0:
  110. r = np.clip(th/thcap, 1e-9, 1-1e-9)
  111. npr_r = -((3-1)*np.log(r) + (3-1)*np.log(1 - r))
  112. else:
  113. npr_r = 0.0
  114. return nll + npr_p + reg + npr_r
  115. # Initialization
  116. def init_phi(X, y):
  117. # Method-of-moments init for Gamma(k, theta) using NC data (y==0)
  118. # ref: https://en.wikipedia.org/wiki/Gamma_distribution#Estimation_of_parameters
  119. X0 = X[y==0]
  120. m0 = X0.mean() if X0.size else X.mean() # sample mean
  121. v0 = X0.var() if X0.size else X.var() # sample variance
  122. if v0 <= 0:
  123. # If variance is degenerate, pick a safe, sane starting point
  124. k0, th0 = 2.0, max(m0/2, 0.1)
  125. else:
  126. # MoM: k = m^2 / v, theta = v / m, with small eps and lower bounds
  127. k0 = max((m0**2)/(v0 + 1e-9), 1.5)
  128. th0 = max(v0/(m0 + 1e-9), 0.3)
  129. X1 = X[y==1]
  130. m1 = np.median(X1) if X1.size else np.median(X)
  131. # Empirical AE rate as a starting prior for p. Clip away from 0/1 so logit is finite.
  132. # p0 = n_AE / n, but truncated to [1e-3, 1-1e-3] to avoid infinities in log(p/(1-p)).
  133. p0 = np.clip(float(y.mean()), 1e-3, 1 - 1e-3)
  134. # Simple, stable seeds for AE Beta–Prime:
  135. # b0 = 1.5 → mild shape; not too spiky, not too flat.
  136. # s0 = max(m1, 0.5) → anchor scale near the AE median, but don’t go tiny.
  137. b0, s0 = 1.5, max(m1, 0.5)
  138. # Pack raw parameters φ for the optimizer.
  139. # We optimize in an unconstrained space and map with:
  140. # p = sigmoid(p_raw)
  141. # b,s,k = softplus(raw) + 1e-6
  142. # a = k + softplus(delta_raw) + 1e-6
  143. # theta = theta_max * sigmoid(u_raw)
  144. # To “invert” softplus for the initial guess we use log(expm1(v)) which is the exact inverse
  145. # of softplus when you define softplus(t) = log(1 + exp(t)). The +1e-9 is just numerical padding.
  146. raw = np.array([
  147. np.log(p0/(1-p0)), # p_raw
  148. np.log(np.expm1(b0) + 1e-9), # b_raw
  149. np.log(np.expm1(s0) + 1e-9), # s_raw
  150. np.log(np.expm1(k0) + 1e-9), # k_raw
  151. np.log(np.expm1(1.0) + 1e-9), # delta_raw
  152. -0.2 # u_raw (keeps theta a bit below cap initially)
  153. ], float)
  154. return raw
  155. # Fitting
  156. def fit_hard_mono_WITH_CONST_REG(X, y, phi_start=None, maxtries=6, jitter=0.3, rng=None):
  157. if rng is None:
  158. rng = np.random.default_rng(12345)
  159. if phi_start is None:
  160. phi_start = init_phi(X, y)
  161. phi = phi_start.copy()
  162. last_err = None
  163. for _ in range(maxtries):
  164. res = optimize.minimize(
  165. neg_post_phi_mono_WITH_CONST_REG, phi, args=(X, y),
  166. method="L-BFGS-B",
  167. options=dict(maxiter=12000, ftol=1e-10)
  168. )
  169. if res.success and np.isfinite(res.fun):
  170. return unpack_phi_mono(res.x), res
  171. last_err = res
  172. phi = phi + rng.normal(0, jitter, size=phi.shape)
  173. raise RuntimeError(f"Fit failed. Last status: {getattr(last_err, 'message', 'n/a')}")
  174. # Convenience
  175. def P_with(theta, x):
  176. p, a, b, s, k, th = theta
  177. L = (np.log(p) - np.log(1-p)) + dE_full(x, a, b, s, k, th)
  178. return logistic(L)
  179. def diag_report(theta, X):
  180. p, a, b, s, k, th = theta
  181. thcap = theta_max(a, b, k, s)
  182. C = dE_const(a, b, s, k, th)
  183. logit_p = np.log(p) - np.log(1 - p)
  184. A = a - k
  185. den = np.sqrt(a + b) - np.sqrt(max(A, 1e-12))
  186. xs = np.inf if den <= 1e-12 else s*np.sqrt(max(A,1e-12))/den
  187. print({
  188. "p": p, "a": a, "b": b, "s": s, "k": k, "theta": th,
  189. "theta_max": thcap, "theta/theta_max": (th/thcap if np.isfinite(thcap) else np.nan),
  190. "logit(p)": logit_p, "C": C, "x* (bottleneck)": xs
  191. })
  192. def plot_s_shape(theta, X, y, rng=None, ax=None, label='P(AE | x)'):
  193. if rng is None:
  194. rng = np.random.default_rng(0)
  195. if ax is None:
  196. fig, ax = plt.subplots(figsize=(7, 4.5))
  197. x_lo = max(1e-6, float(X.min())*0.8)
  198. x_hi = float(X.max())*1.2
  199. xg = np.linspace(x_lo, x_hi, 600)
  200. pg = P_with(theta, xg)
  201. ax.plot(xg, pg, lw=2, label=label)
  202. jit = (rng.random(len(X)) - 0.5) * 0.06
  203. y_jit = y + jit
  204. ax.scatter(X[y==0], y_jit[y==0], s=22, alpha=0.35, label='NC (y=0)', edgecolors='none')
  205. ax.scatter(X[y==1], y_jit[y==1], s=28, alpha=0.60, label='AE (y=1)', edgecolors='none')
  206. ax.set_ylim(-0.05, 1.05)
  207. ax.set_xlim(x_lo, x_hi)
  208. ax.set_xlabel('x')
  209. ax.set_ylabel('P(AE | x)')
  210. ax.set_title('S-shaped P(AE | x) with hard-mono fit (constant included, regularized)')
  211. ax.grid(True, alpha=0.3)
  212. ax.legend(loc='lower right', frameon=False)
  213. return ax
  214. # Run fit
  215. theta_hat, res = fit_hard_mono_WITH_CONST_REG(X, y, rng=rng)
  216. print("Optimization success:", res.success, "fval:", res.fun)
  217. diag_report(theta_hat, X)
  218. ax = plot_s_shape(theta_hat, X, y, rng=rng)
  219. plt.show()
  220. #CI Estimation
  221. # 1- Delta-method
  222. # ===== Delta band + compact summaries (minimal) =====
  223. import numpy as np
  224. import matplotlib.pyplot as plt
  225. import numdifftools as nd
  226. from scipy.stats import norm
  227. # 1) Covariance in raw-phi space at MAP
  228. phi_hat = res.x.copy()
  229. f_obj = lambda phi: neg_post_phi_mono_WITH_CONST_REG(np.asarray(phi, float), X, y)
  230. H = nd.Hessian(f_obj, method='central')(phi_hat)
  231. Sigma_phi = np.linalg.pinv(0.5*(H + H.T)) # robust inverse
  232. # 2) Delta band on P(AE|x) via numdifftools.Gradient
  233. x_lo = max(1e-6, float(X.min())*0.8)
  234. x_hi = min(10.0, float(X.max())*1.2)
  235. xg = np.linspace(x_lo, x_hi, 500)
  236. p_hat = np.empty_like(xg)
  237. p_lo = np.empty_like(xg)
  238. p_hi = np.empty_like(xg)
  239. for i, x in enumerate(xg):
  240. gx = g_px(x)
  241. ph = gx(phi_hat)
  242. grad = nd.Gradient(gx, method='central')(phi_hat)
  243. var = float(grad @ Sigma_phi @ grad)
  244. se = np.sqrt(max(var, 0.0))
  245. p_hat[i] = ph
  246. p_lo[i] = np.clip(ph - z*se, 0.0, 1.0)
  247. p_hi[i] = np.clip(ph + z*se, 0.0, 1.0)
  248. # 3) Plot
  249. fig, ax = plt.subplots(figsize=(7.2, 4.4), dpi=140)
  250. ax.plot(xg, p_hat, lw=2.0, label='P(AE|x) @ MAP')
  251. ax.fill_between(xg, p_lo, p_hi, alpha=0.20, label='95% Delta band')
  252. rngp = np.random.default_rng(999); jit = (rngp.random(len(X)) - 0.5) * 0.06
  253. ax.scatter(X[y==0], (y+jit)[y==0], s=22, alpha=0.55, edgecolors='none', label='NC')
  254. ax.scatter(X[y==1], (y+jit)[y==1], s=26, alpha=0.75, edgecolors='none', label='AE')
  255. ax.set_ylim(-0.05, 1.05); ax.set_xlabel('x'); ax.set_ylabel('P(AE | x)')
  256. ax.grid(alpha=0.3); ax.legend(loc='lower right')
  257. plt.tight_layout(); plt.show()
  258. w = p_hi - p_lo
  259. mask = (xg >= float(X.min())) & (xg <= float(X.max()))
  260. print("\nBand width (95% pointwise): "
  261. f"overall mean {w.mean():.3f}, max {w.max():.3f}; "
  262. f"in-range mean {w[mask].mean():.3f}, max {w[mask].max():.3f}")
  263. try:
  264. G = lambda phi: np.array(unpack_phi_mono(np.asarray(phi, float)), float) # -> [p,a,b,s,k,theta]
  265. J = nd.Jacobian(G)(phi_hat)
  266. Sigma_theta = J @ Sigma_phi @ J.T
  267. se = np.sqrt(np.maximum(np.diag(Sigma_theta), 0.0))
  268. theta_hat_vec = G(phi_hat); names = ["p","a","b","s","k","theta"]
  269. print("\nParameter 95% CIs (Delta/Wald):")
  270. for nm, v, svi in zip(names, theta_hat_vec, se):
  271. print(f" {nm:>6s} : {v:.6g} [ {v - z*svi:.6g}, {v + z*svi:.6g} ]")
  272. except Exception as e:
  273. print("(Parameter CI step skipped:", e, ")")