Mice — Weibull survival across 4 groups (BUGS "Mice")¶
Reuses survival_mcmc.py; the new angle is a strongly increasing hazard¶
Same parametric machinery as survival_python.ipynb (Weibull proportional hazards with right-censoring) — see that notebook for the full censored-likelihood derivation and the MCMC algorithm. Here the application is the BUGS Vol I "Mice" experiment: 80 mice in 4 groups (20 each), survival in weeks, with administrative censoring at the end of follow-up. Two things differ from Leuk: the covariate is a 4-level group factor, and the hazard turns out to be strongly increasing (Weibull shape $k\gg1$ — ageing / wear-out), where Leuk's was near-flat.
Model¶
$$T_{i}\sim\text{Weibull}:\ \ h(t\mid g)=\lambda_g\,k\,t^{k-1},\quad \lambda_g=\exp(\beta_0+\beta_{g}),\quad S(t)=e^{-\lambda_g t^{k}},$$ with group 1 as baseline ($\beta_1\equiv0$); $e^{\beta_g}$ is the hazard ratio of group $g$ vs group 1, and the median survival of group $g$ is $(\log 2/\lambda_g)^{1/k}$. Censored mice (still alive at the cutoff) contribute only $S(t)$.
import numpy as np, pandas as pd, matplotlib.pyplot as plt
from survival_mcmc import weibull_mle, weibull_rwm, km_estimator, summary
d = pd.read_csv('mice.csv'); G = d['group'].to_numpy()
t = d['time'].to_numpy(float); delta = d['cens'].to_numpy(float)
X = np.column_stack([np.ones(len(d))] + [(G==g).astype(float) for g in (2,3,4)]) # group 1 baseline
for g in (1,2,3,4):
m = G==g; print(' group %d: n=%d events=%d censored=%d median.obs=%.0f' % (g,m.sum(),int(delta[m].sum()),int((1-delta[m]).sum()),np.median(t[m])))
mle, se, _ = weibull_mle(X, t, delta)
o = weibull_rwm(X, t, delta, seed=1); dr = o['draws']; k = np.exp(dr[:,-1])
print('\nWeibull shape k = %.3f 95%% CI [%.2f, %.2f] (k>>1 = strongly increasing hazard / wear-out)' % (k.mean(), np.percentile(k,2.5), np.percentile(k,97.5)))
print('hazard ratios vs group 1:')
for j,g in [(1,2),(2,3),(3,4)]:
hr = np.exp(dr[:,j]); print(' group %d: HR %.2f [%.2f, %.2f] P(HR<1)=%.2f' % (g, hr.mean(), np.percentile(hr,2.5), np.percentile(hr,97.5), (hr<1).mean()))
# posterior median survival per group
beta = dr[:,:4]; meds = {}
for g in (1,2,3,4):
xg = np.array([1, g==2, g==3, g==4], float)
lam = np.exp(beta @ xg); meds[g] = (np.log(2)/lam)**(1/k)
print('median survival (weeks): ' + ' '.join('g%d=%.1f[%.0f,%.0f]'%(g,meds[g].mean(),np.percentile(meds[g],2.5),np.percentile(meds[g],97.5)) for g in (1,2,3,4)))
group 1: n=20 events=19 censored=1 median.obs=24 group 2: n=20 events=13 censored=7 median.obs=32 group 3: n=20 events=16 censored=4 median.obs=24 group 4: n=20 events=17 censored=3 median.obs=21 Weibull shape k = 3.179 95% CI [2.56, 3.82] (k>>1 = strongly increasing hazard / wear-out) hazard ratios vs group 1: group 2: HR 0.34 [0.15, 0.66] P(HR<1)=1.00 group 3: HR 0.74 [0.36, 1.38] P(HR<1)=0.85 group 4: HR 1.57 [0.75, 3.00] P(HR<1)=0.14
median survival (weeks): g1=24.3[21,28] g2=34.9[30,42] g3=27.2[23,32] g4=21.5[18,25]
fig, ax = plt.subplots(1, 2, figsize=(12, 4.6))
cols = ['steelblue','seagreen','goldenrod','firebrick']; kk = np.exp(mle[-1]); grid = np.linspace(0,42,250)
for g in (1,2,3,4):
m = G==g; tk, Sk = km_estimator(t[m], delta[m])
ax[0].step(tk, Sk, where='post', color=cols[g-1], lw=1.7, label=f'group {g}')
xg = np.array([1, g==2, g==3, g==4], float); lam = np.exp(mle[:4] @ xg)
ax[0].plot(grid, np.exp(-lam*grid**kk), '--', color=cols[g-1], lw=1.0)
ax[0].set_xlabel('weeks'); ax[0].set_ylabel('S(t)'); ax[0].set_ylim(0,1.02)
ax[0].set_title('KM (step) vs fitted Weibull (dashed) by group'); ax[0].legend(fontsize=8)
mm = [meds[g] for g in (1,2,3,4)]
ax[1].errorbar(range(1,5), [m.mean() for m in mm],
yerr=[[m.mean()-np.percentile(m,2.5) for m in mm],[np.percentile(m,97.5)-m.mean() for m in mm]],
fmt='o', color='black', capsize=4)
for g in (1,2,3,4): ax[1].scatter(g, mm[g-1].mean(), s=60, color=cols[g-1], zorder=3)
ax[1].set_xticks(range(1,5)); ax[1].set_xlabel('group'); ax[1].set_ylabel('median survival (weeks)')
ax[1].set_title('Posterior median survival by group (95% CrI)')
plt.tight_layout(); plt.savefig('mice_surv.png', dpi=120, bbox_inches='tight'); plt.show()
Results¶
- Shape $k\approx3.2$ (CI ~2.6–3.8), far above 1 — a steeply increasing hazard: mice die faster the longer the experiment runs (ageing/wear-out). This is the qualitative contrast with Leuk ($k\approx1.35$), and it rules out the exponential decisively.
- Group differences: group 2 survives longest (median ≈ 35 wk; HR ≈ 0.34 vs group 1 — a real protective effect, P(HR<1) high), group 4 shortest (median ≈ 21 wk; HR ≈ 1.6). Groups 1 and 3 sit in between.
- The censoring is heaviest in group 2 (7 of 20 still alive at cutoff), which is exactly why its raw median understates the benefit — the censored likelihood credits those survivors, and the fitted curve stays high.
Takeaway¶
Methodologically identical to Leuk (same survival_mcmc.py), this example highlights the Weibull shape as a finding in its own right ($k\gg1$ = wear-out) and shows the model handling a multi-level factor. Cross-checks: PyMC and R (survreg) follow.