Leuk: Cox Regression
This example is the classic leukemia survival dataset, in which the remission times of 42 patients are compared between two treatment groups. In the data each patient carries a treatment covariate Z coded as $\pm 0.5$ (21 patients in each group), an observed time obs.t, and a failure indicator fail that records whether the observed time is a true failure or a right-censored follow-up time. The goal is to estimate the effect of treatment on the hazard of relapse and to recover the survival curve in each group.
The model is a Cox proportional-hazards regression in which the integrated baseline hazard is estimated non-parametrically, following the counting-process formulation of Clayton (1994) using the notation of Andersen and Gill (1982). Rather than fitting a censored survival likelihood directly, the increments of each patient's counting process are treated as independent Poisson variables whose means depend on a risk-set indicator, the treatment effect $\beta$, and the jump $d\Lambda_0$ in the baseline hazard over each time interval; the hazard increments are given a conjugate gamma-process prior. This reformulation looks somewhat indirect, but it lays the groundwork for extensions to frailty models, time-dependent covariates, and smoothed hazards. It is one of the examples in Volume 1 of the classic BUGS examples; see also the OpenBUGS version of this example.
Model
\[\begin{aligned} dN_i(t_j) &\sim \text{Poisson}\left(I_i(t_j)\right) \\ I_i(t_j) &= Y_i(t_j)\, \exp(\beta z_i)\, d\Lambda_0(t_j) \\ d\Lambda_0(t_j) &\sim \text{Gamma}\left(c\, d\Lambda_0^*(t_j),\ c\right) \\ \beta &\sim \text{Normal}(0,\ 10^{-6}) \end{aligned}\]
Here $Y_i(t_j)$ is the risk-set indicator (1 if patient $i$ is still under observation at time $t_j$), $dN_i(t_j)$ counts a failure of patient $i$ in the interval starting at $t_j$, and $d\Lambda_0^*$ is a prior guess at the hazard with $c$ controlling the strength of that guess.
using JuliaBUGS
leuk = @bugs begin
# Set up data
for i in 1:N
for j in 1:T
# risk set = 1 if obs.t >= t
Y[i, j] = step(var"obs.t"[i] - t[j] + eps)
# counting process jump = 1 if obs.t in [ t[j], t[j+1] )
# i.e. if t[j] <= obs.t < t[j+1]
dN[i, j] = Y[i, j] * step(t[j + 1] - var"obs.t"[i] - eps) * fail[i]
end
end
# Model
for j in 1:T
for i in 1:N
dN[i, j] ~ dpois(Idt[i, j]) # Likelihood
Idt[i, j] = Y[i, j] * exp(beta * Z[i]) * dL0[j] # Intensity
end
dL0[j] ~ dgamma(mu[j], c)
mu[j] = var"dL0.star"[j] * c # prior mean hazard
# Survivor function = exp(-Integral{l0(u)du})^exp(beta*z)
var"S.treat"[j] = pow(exp(-sum(dL0[1:j])), exp(beta * -0.5))
var"S.placebo"[j] = pow(exp(-sum(dL0[1:j])), exp(beta * 0.5))
end
c = 0.001
r = 0.1
for j in 1:T
var"dL0.star"[j] = r * (t[j + 1] - t[j])
end
beta ~ dnorm(0.0, 0.000001)
endBUGSModelDef:
begin
for i = 1:N
for j = 1:T
Y[i, j] = _step((var"obs.t"[i] - t[j]) + eps)
dN[i, j] = Y[i, j] * _step((t[j + 1] - var"obs.t"[i]) - eps) * fail[i]
end
end
for j = 1:T
for i = 1:N
dN[i, j] ~ dpois(Idt[i, j])
Idt[i, j] = Y[i, j] * exp(beta * Z[i]) * dL0[j]
end
dL0[j] ~ dgamma(mu[j], c)
mu[j] = var"dL0.star"[j] * c
var"S.treat"[j] = pow(exp(-(sum(dL0[1:j]))), exp(beta * -0.5))
var"S.placebo"[j] = pow(exp(-(sum(dL0[1:j]))), exp(beta * 0.5))
end
c = 0.001
r = 0.1
for j = 1:T
var"dL0.star"[j] = r * (t[j + 1] - t[j])
end
beta ~ dnorm(0.0, 1.0e-6)
endNames such as var"obs.t", var"dL0.star", var"S.treat", and var"S.placebo" are the R-style dotted variable names from the original BUGS program, written with Julia's var"..." syntax so they can be kept exactly as they appear in the classic example.
Data
The data are supplied as a NamedTuple: the number of patients N, the number of distinct failure times T, a small constant eps used in the risk-set comparisons, the observed times obs.t (in weeks), the failure indicator fail, the treatment covariate Z (coded $\pm 0.5$), and the grid of interval boundaries t.
data = (
N = 42,
T = 17,
eps = 1.0E-10,
var"obs.t" = [
1, 1, 2, 2, 3, 4, 4, 5, 5, 8, 8, 8, 8, 11, 11, 12, 12, 15, 17, 22, 23, 6,
6, 6, 6, 7, 9, 10, 10, 11, 13, 16, 17, 19, 20, 22, 23, 25, 32, 32, 34, 35
],
fail = [
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0
],
Z = [
0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
-0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5,
-0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5
],
t = [1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 15, 16, 17, 22, 23, 35]
)
model = leuk(data, (; beta=0.0))BUGSModel (parameters are in transformed (unconstrained) space, with dimension 18):
Model parameters:
beta
dL0[17], dL0[16], dL0[15], dL0[14], dL0[13], dL0[12], dL0[11], dL0[10], dL0[9], dL0[8], dL0[7], dL0[6], dL0[5], dL0[4], dL0[3], dL0[2], dL0[1]
Variable sizes and types:
dL0.star: size = (17,), type = Vector{Float64}
c: type = Float64
eps: type = Float64
dN: size = (42, 17), type = Matrix{Float64}
obs.t: size = (42,), type = Vector{Int64}
S.treat: size = (17,), type = Vector{Float64}
S.placebo: size = (17,), type = Vector{Float64}
dL0: size = (17,), type = Vector{Float64}
Idt: size = (42, 17), type = Matrix{Float64}
Z: size = (42,), type = Vector{Float64}
N: type = Int64
fail: size = (42,), type = Vector{Int64}
mu: size = (17,), type = Vector{Float64}
Y: size = (42, 17), type = Matrix{Float64}
r: type = Float64
T: type = Int64
beta: type = Float64
t: size = (18,), type = Vector{Int64}
The initial value for beta matters here: its prior dnorm(0.0, 0.000001) has standard deviation 1000, and a random draw far in the tails overflows exp(beta * Z[i]) during model construction. Passing a starting value avoids that.
All of the classic examples ship with the package in JuliaBUGS.BUGSExamples, so the model definition, data, and initial values above are also available directly as JuliaBUGS.BUGSExamples.VOLUME_1.leuk.
Sampling
We draw posterior samples with the NUTS sampler from AdvancedHMC, rebuilding the model with gradient support first.
using AbstractMCMC, AdvancedHMC, ADTypes, Mooncake, FlexiChains
using LogDensityProblems
model = leuk(data, (; beta=0.0); adtype=AutoMooncake(; config=nothing))
n_samples, n_adapts = 2000, 1000
D = LogDensityProblems.dimension(model)
chain = AbstractMCMC.sample(
model, NUTS(0.8), n_samples;
chain_type=VNChain, n_adapts=n_adapts,
init_params=rand(D), discard_initial=n_adapts,
)
summarystats(chain)BUGS-style initial values for this example are available as JuliaBUGS.BUGSExamples.VOLUME_1.leuk.inits and can be applied with initialize!(model, inits).
This is a survival model with right-censored data: the fail indicator marks which observed times are true failures rather than censored follow-up times. Instead of a censored or truncated likelihood, the Cox model is recast in counting-process form, so censoring enters only through the risk-set indicators Y and the failure counts dN, and the likelihood reduces to an ordinary Poisson model over continuous parameters. No special censoring machinery or discrete latent variables are involved.
Results
No reference posterior summaries ship with this example in JuliaBUGS.BUGSExamples. Published results for the quantities of interest, including the treatment effect beta and the survivor functions S.treat and S.placebo, are available on the OpenBUGS page for this example, and a correctly converged chain's summarystats should reproduce them up to Monte Carlo error.
See also: the example gallery overview and the getting-started tutorial.