Epilepsy: Repeated Measures on Poisson Counts

Breslow and Clayton (1993) analysed data originally reported by Thall and Vail (1990) on seizure counts from a randomised trial of anti-convulsant therapy in epilepsy. Fifty-nine patients were followed over four successive clinic visits, and the number of seizures in the interval before each visit was recorded, along with each patient's treatment assignment, baseline seizure count, and age. The question is whether the treatment reduces the seizure rate after adjusting for these covariates.

The model is a Poisson generalised linear mixed model (model III of Breslow and Clayton). The log seizure rate is a linear function of centred covariates — log baseline count, treatment, a treatment-by-baseline interaction, log age, and an indicator for the fourth visit — plus a subject-level random effect and a subject-by-visit random effect that allows for extra-Poisson variation. This example is from Volume 1 of the classic BUGS examples; see also the OpenBUGS documentation page.

Model

For patient $j = 1, \dots, 59$ and visit $k = 1, \dots, 4$,

\[\begin{aligned} y_{jk} &\sim \text{Poisson}(\mu_{jk}) \\ \log \mu_{jk} &= a_0 + \alpha_{\text{Base}} \left( \log(\text{Base}_j / 4) - \overline{\log(\text{Base}/4)} \right) + \alpha_{\text{Trt}} \left( \text{Trt}_j - \overline{\text{Trt}} \right) + \alpha_{\text{BT}} \left( \text{BT}_j - \overline{\text{BT}} \right) \\ &\quad + \alpha_{\text{Age}} \left( \log(\text{Age}_j) - \overline{\log(\text{Age})} \right) + \alpha_{\text{V4}} \left( \text{V4}_k - \overline{\text{V4}} \right) + b1_j + b_{jk} \\ b1_j &\sim \text{Normal}(0, \sigma_{b1}^2), \qquad b_{jk} \sim \text{Normal}(0, \sigma_b^2) \end{aligned}\]

with vague normal priors on the coefficients and vague gamma priors on the random-effect precisions. The original BUGS program uses R-style dotted names such as alpha.Base and tau.b; in Julia these are written with the var"..." syntax, which lets a variable name contain a dot.

using JuliaBUGS

epil = @bugs begin
    for j in 1:N
        for k in 1:T
            mu[j, k] = exp(
                a0 + var"alpha.Base" * (var"log.Base4"[j] - var"log.Base4.bar") +
                var"alpha.Trt" * (Trt[j] - var"Trt.bar") +
                var"alpha.BT" * (BT[j] - var"BT.bar") +
                var"alpha.Age" * (var"log.Age"[j] - var"log.Age.bar") +
                var"alpha.V4" * (V4[k] - var"V4.bar") + b1[j] + b[j, k]
            )
            y[j, k] ~ dpois(mu[j, k])
            b[j, k] ~ dnorm(0.0, var"tau.b")       # subject*visit random effects
        end
        b1[j] ~ dnorm(0.0, var"tau.b1")        # subject random effects
        BT[j] = Trt[j] * var"log.Base4"[j]    # interaction
        var"log.Base4"[j] = log(Base[j] / 4)
        var"log.Age"[j] = log(Age[j])
    end

    # covariate means:
    var"log.Age.bar" = mean(var"log.Age"[:])
    var"Trt.bar" = mean(Trt[:])
    var"BT.bar" = mean(BT[:])
    var"log.Base4.bar" = mean(var"log.Base4"[:])
    var"V4.bar" = mean(V4[:])

    # priors:
    a0 ~ dnorm(0.0, 1.0E-4)
    var"alpha.Base" ~ dnorm(0.0, 1.0E-4)
    var"alpha.Trt" ~ dnorm(0.0, 1.0E-4)
    var"alpha.BT" ~ dnorm(0.0, 1.0E-4)
    var"alpha.Age" ~ dnorm(0.0, 1.0E-4)
    var"alpha.V4" ~ dnorm(0.0, 1.0E-4)
    var"tau.b1" ~ dgamma(1.0E-3, 1.0E-3)
    var"sigma.b1" = 1.0 / sqrt(var"tau.b1")
    var"tau.b" ~ dgamma(1.0E-3, 1.0E-3)
    var"sigma.b" = 1.0 / sqrt(var"tau.b")

    # re-calculate intercept on original scale:
    alpha0 = a0 - var"alpha.Base" * var"log.Base4.bar" - var"alpha.Trt" * var"Trt.bar" -
             var"alpha.BT" * var"BT.bar" - var"alpha.Age" * var"log.Age.bar" -
             var"alpha.V4" * var"V4.bar"
end
BUGSModelDef:
begin
    for j = 1:N
        for k = 1:T
            mu[j, k] = exp(a0 + var"alpha.Base" * (var"log.Base4"[j] - var"log.Base4.bar") + var"alpha.Trt" * (Trt[j] - var"Trt.bar") + var"alpha.BT" * (BT[j] - var"BT.bar") + var"alpha.Age" * (var"log.Age"[j] - var"log.Age.bar") + var"alpha.V4" * (V4[k] - var"V4.bar") + b1[j] + b[j, k])
            y[j, k] ~ dpois(mu[j, k])
            b[j, k] ~ dnorm(0.0, var"tau.b")
        end
        b1[j] ~ dnorm(0.0, var"tau.b1")
        BT[j] = Trt[j] * var"log.Base4"[j]
        var"log.Base4"[j] = log(Base[j] / 4)
        var"log.Age"[j] = log(Age[j])
    end
    var"log.Age.bar" = mean(var"log.Age"[:])
    var"Trt.bar" = mean(Trt[:])
    var"BT.bar" = mean(BT[:])
    var"log.Base4.bar" = mean(var"log.Base4"[:])
    var"V4.bar" = mean(V4[:])
    a0 ~ dnorm(0.0, 0.0001)
    var"alpha.Base" ~ dnorm(0.0, 0.0001)
    var"alpha.Trt" ~ dnorm(0.0, 0.0001)
    var"alpha.BT" ~ dnorm(0.0, 0.0001)
    var"alpha.Age" ~ dnorm(0.0, 0.0001)
    var"alpha.V4" ~ dnorm(0.0, 0.0001)
    var"tau.b1" ~ dgamma(0.001, 0.001)
    var"sigma.b1" = 1.0 / sqrt(var"tau.b1")
    var"tau.b" ~ dgamma(0.001, 0.001)
    var"sigma.b" = 1.0 / sqrt(var"tau.b")
    alpha0 = ((((a0 - var"alpha.Base" * var"log.Base4.bar") - var"alpha.Trt" * var"Trt.bar") - var"alpha.BT" * var"BT.bar") - var"alpha.Age" * var"log.Age.bar") - var"alpha.V4" * var"V4.bar"
end

Data

The data set is too large to display comfortably here, so we load it from the copy that ships with JuliaBUGS. It contains N = 59 patients and T = 4 visits, the 59 × 4 matrix y of seizure counts, the treatment indicator Trt (0 = placebo, 1 = active treatment), the baseline seizure count Base, each patient's Age in years, and V4, an indicator that equals 1 only at the fourth visit.

data  = JuliaBUGS.BUGSExamples.VOLUME_1.epil.data
inits = JuliaBUGS.BUGSExamples.VOLUME_1.epil.inits
model = epil(data, inits)
BUGSModel (parameters are in transformed (unconstrained) space, with dimension 303):

  Model parameters:
    tau.b
    alpha.Base
    b[59, 4], b[59, 3], b[59, 2], b[59, 1], b[58, 4], b[58, 3], b[58, 2], b[58, 1], b[57, 4], b[57, 3], b[57, 2], b[57, 1], b[56, 4], b[56, 3], b[56, 2], b[56, 1], b[55, 4], b[55, 3], b[55, 2], b[55, 1], b[54, 4], b[54, 3], b[54, 2], b[54, 1], b[53, 4], b[53, 3], b[53, 2], b[53, 1], b[52, 4], b[52, 3], b[52, 2], b[52, 1], b[51, 4], b[51, 3], b[51, 2], b[51, 1], b[50, 4], b[50, 3], b[50, 2], b[50, 1], b[49, 4], b[49, 3], b[49, 2], b[49, 1], b[48, 4], b[48, 3], b[48, 2], b[48, 1], b[47, 4], b[47, 3], b[47, 2], b[47, 1], b[46, 4], b[46, 3], b[46, 2], b[46, 1], b[45, 4], b[45, 3], b[45, 2], b[45, 1], b[44, 4], b[44, 3], b[44, 2], b[44, 1], b[43, 4], b[43, 3], b[43, 2], b[43, 1], b[42, 4], b[42, 3], b[42, 2], b[42, 1], b[41, 4], b[41, 3], b[41, 2], b[41, 1], b[40, 4], b[40, 3], b[40, 2], b[40, 1], b[39, 4], b[39, 3], b[39, 2], b[39, 1], b[38, 4], b[38, 3], b[38, 2], b[38, 1], b[37, 4], b[37, 3], b[37, 2], b[37, 1], b[36, 4], b[36, 3], b[36, 2], b[36, 1], b[35, 4], b[35, 3], b[35, 2], b[35, 1], b[34, 4], b[34, 3], b[34, 2], b[34, 1], b[33, 4], b[33, 3], b[33, 2], b[33, 1], b[32, 4], b[32, 3], b[32, 2], b[32, 1], b[31, 4], b[31, 3], b[31, 2], b[31, 1], b[30, 4], b[30, 3], b[30, 2], b[30, 1], b[29, 4], b[29, 3], b[29, 2], b[29, 1], b[28, 4], b[28, 3], b[28, 2], b[28, 1], b[27, 4], b[27, 3], b[27, 2], b[27, 1], b[26, 4], b[26, 3], b[26, 2], b[26, 1], b[25, 4], b[25, 3], b[25, 2], b[25, 1], b[24, 4], b[24, 3], b[24, 2], b[24, 1], b[23, 4], b[23, 3], b[23, 2], b[23, 1], b[22, 4], b[22, 3], b[22, 2], b[22, 1], b[21, 4], b[21, 3], b[21, 2], b[21, 1], b[20, 4], b[20, 3], b[20, 2], b[20, 1], b[19, 4], b[19, 3], b[19, 2], b[19, 1], b[18, 4], b[18, 3], b[18, 2], b[18, 1], b[17, 4], b[17, 3], b[17, 2], b[17, 1], b[16, 4], b[16, 3], b[16, 2], b[16, 1], b[15, 4], b[15, 3], b[15, 2], b[15, 1], b[14, 4], b[14, 3], b[14, 2], b[14, 1], b[13, 4], b[13, 3], b[13, 2], b[13, 1], b[12, 4], b[12, 3], b[12, 2], b[12, 1], b[11, 4], b[11, 3], b[11, 2], b[11, 1], b[10, 4], b[10, 3], b[10, 2], b[10, 1], b[9, 4], b[9, 3], b[9, 2], b[9, 1], b[8, 4], b[8, 3], b[8, 2], b[8, 1], b[7, 4], b[7, 3], b[7, 2], b[7, 1], b[6, 4], b[6, 3], b[6, 2], b[6, 1], b[5, 4], b[5, 3], b[5, 2], b[5, 1], b[4, 4], b[4, 3], b[4, 2], b[4, 1], b[3, 4], b[3, 3], b[3, 2], b[3, 1], b[2, 4], b[2, 3], b[2, 2], b[2, 1], b[1, 4], b[1, 3], b[1, 2], b[1, 1]
    alpha.BT
    alpha.V4
    alpha.Age
    a0
    b1[59], b1[58], b1[57], b1[56], b1[55], b1[54], b1[53], b1[52], b1[51], b1[50], b1[49], b1[48], b1[47], b1[46], b1[45], b1[44], b1[43], b1[42], b1[41], b1[40], b1[39], b1[38], b1[37], b1[36], b1[35], b1[34], b1[33], b1[32], b1[31], b1[30], b1[29], b1[28], b1[27], b1[26], b1[25], b1[24], b1[23], b1[22], b1[21], b1[20], b1[19], b1[18], b1[17], b1[16], b1[15], b1[14], b1[13], b1[12], b1[11], b1[10], b1[9], b1[8], b1[7], b1[6], b1[5], b1[4], b1[3], b1[2], b1[1]
    tau.b1
    alpha.Trt

  Variable sizes and types:
    alpha.Base: type = Int64
    b: size = (59, 4), type = Matrix{Float64}
    alpha.BT: type = Int64
    Trt.bar: type = Float64
    V4: size = (4,), type = Vector{Int64}
    sigma.b1: type = Float64
    log.Base4: size = (59,), type = Vector{Float64}
    alpha.Trt: type = Int64
    tau.b: type = Int64
    V4.bar: type = Float64
    Age: size = (59,), type = Vector{Int64}
    alpha.V4: type = Int64
    log.Base4.bar: type = Float64
    Base: size = (59,), type = Vector{Int64}
    BT: size = (59,), type = Vector{Float64}
    b1: size = (59,), type = Vector{Float64}
    alpha0: type = Float64
    Trt: size = (59,), type = Vector{Int64}
    N: type = Int64
    BT.bar: type = Float64
    sigma.b: type = Float64
    mu: size = (59, 4), type = Matrix{Float64}
    T: type = Int64
    y: size = (59, 4), type = Matrix{Int64}
    log.Age.bar: type = Float64
    log.Age: size = (59,), type = Vector{Float64}
    alpha.Age: type = Int64
    a0: type = Int64
    tau.b1: type = Int64

We pass the example's published initial values as the second argument to epil rather than calling epil(data) alone. This is a log-linear Poisson model with vague priors, so values drawn at random from the priors can produce an invalid (non-positive or overflowing) rate; starting from sensible values keeps construction and sampling stable.

All of the classic Volume 1 examples ship with the package in JuliaBUGS.BUGSExamples, each providing the model definition, data, initial values, and reference results.

Sampling

To draw posterior samples, we build the model with gradient support and run the NUTS sampler from AdvancedHMC:

using AbstractMCMC, AdvancedHMC, ADTypes, Mooncake, FlexiChains
using LogDensityProblems

model = epil(data, inits; 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.epil.inits and can be applied with initialize!(model, inits).

Results

JuliaBUGS does not ship reference results for this example. For published estimates, see the OpenBUGS documentation page, which reports results alongside the approximate-likelihood fit of Breslow and Clayton (1993); a correctly converged chain's summarystats should agree with those values up to Monte Carlo error.

See also: the example gallery overview and the getting started tutorial.