12.4 Design and the case study in software
Software 12.4.1 (Events and patients). Schoenfeld’s and Freedman’s formulas for a hazard ratio of \(0.7\), the events needed for four hazard ratios, and the conversion to patients with Simpson’s rule:
> library(survival); options(show.signif.stars = FALSE) > events <- function(hr, alpha = 0.05, power = 0.8, p = 0.5) + (qnorm(1 - alpha / 2) + qnorm(power))^2 / (p * (1 - p) * log(hr)^2) > freedman <- (qnorm(0.975) + qnorm(0.8))^2 * (1.7 / 0.3)^2 > round(c(schoenfeld = events(0.7), freedman = freedman)) schoenfeld freedman 247 252 > round(sapply(c(0.5, 0.6, 0.7, 0.8), events)) # events needed, 80% power [1] 65 120 247 631 > # probability of an event: accrual a = 24, further follow-up f = 12 months > S <- function(t, hr = 1) exp(-hr * log(2) / 12 * t) # control median 12 > pev <- function(hr, a = 24, f = 12) + 1 - (S(f, hr) + 4 * S(f + a / 2, hr) + S(f + a, hr)) / 6 # Simpson > pbar <- mean(c(pev(1), pev(0.7))) > round(c(p_control = pev(1), p_treated = pev(0.7), p_mean = pbar), 4) p_control p_treated p_mean 0.7292 0.6059 0.6675 > ceiling(events(0.7) / pbar) # patients needed [1] 370
Reading the output. \(247\) events (Freedman \(252\)) for \(\Delta = 0.7\); the number rises steeply as \(\Delta \) approaches \(1\). With the given accrual and follow-up, two-thirds of patients will be seen to die, so \(370\) are needed.
Python: lifelines.statistics.sample_size_necessary_under_cph.
Software 12.4.2 (Power by simulation). \(2000\) trials of the design of Example 12.1.2, with exponential survival, uniform accrual over \(24\) months and \(12\) months of further follow-up, each analysed by the log-rank test:
> set.seed(12)
> sim <- function(n, hr = 0.7, a = 24, f = 12) {
+ g <- rep(0:1, length.out = n)
+ t <- rexp(n, log(2) / 12 * hr^g) # survival times
+ cens <- f + runif(n, 0, a) # time from entry to end
+ y <- pmin(t, cens); d <- as.numeric(t <= cens)
+ c(p = 1 - pchisq(survdiff(Surv(y, d) ~ g)$chisq, 1), events = sum(d))
+ }
> r <- replicate(2000, sim(370))
> round(c(power = mean(r["p", ] < 0.05), events = mean(r["events", ])), 3)
power events
0.789 247.018
Reading the output. The simulated power is \(78.9\%\) (with simulation error about \(0.9\%\)), close to the \(80\%\) intended, and the trials see \(247\) deaths on average, the number the formula asked for. Changing the simulated survival times gives power for any pattern of hazards.
Python: the same loop with numpy and lifelines.statistics.logrank_test.
Software 12.4.3 (The PBC trial). The \(312\) randomized patients, deaths by arm, the log-rank test and Kaplan–Meier estimates at \(5\) and \(10\) years (trt is \(1\) for D-penicillamine and \(2\) for placebo):
> pb <- pbc[!is.na(pbc$trt), ] # the 312 trial patients
> pb$death <- as.numeric(pb$status == 2) # transplant = censored
> table(trt = pb$trt, death = pb$death)
death
trt 0 1
1 93 65
2 94 60
> survdiff(Surv(time / 365.25, death) ~ trt, data = pb)
Call:
survdiff(formula = Surv(time/365.25, death) ~ trt, data = pb)
N Observed Expected (O-E)^2/E (O-E)^2/V
trt=1 158 65 63.2 0.0502 0.102
trt=2 154 60 61.8 0.0513 0.102
Chisq= 0.1 on 1 degrees of freedom, p= 0.7
> km <- survfit(Surv(time / 365.25, death) ~ trt, data = pb)
> matrix(round(summary(km, times = c(5, 10))$surv, 3), 2,
+ dimnames = list(years = c(5, 10), trt = c("D-pen", "placebo")))
trt
years D-pen placebo
5 0.708 0.715
10 0.425 0.457
Reading the output. Observed and expected deaths are almost equal in both arms (\(65\) against \(63.2\), \(60\) against \(61.8\)), \(p = 0.75\); survival is similar at \(5\) and \(10\) years.
Python: KaplanMeierFitter and logrank_test.
Software 12.4.4 (The Mayo model). The Cox model with age, oedema and three log-transformed laboratory values, its hazard ratios per year and per doubling, the tests of proportional hazards, the concordance index, and the treatment effect adjusted for the model:
> mayo <- coxph(Surv(time, death) ~ age + edema + log(bili) + log(albumin) + + log(protime), data = pb) > unit <- c(1, 1, log(2), log(2), log(2)) # per year; per doubling > round(cbind(HR = exp(coef(mayo) * unit), p = summary(mayo)$coef[, 5]), 4) HR p age 1.0338 0.0001 edema 2.1943 0.0086 log(bili) 1.8404 0.0000 log(albumin) 0.1199 0.0000 log(protime) 8.0781 0.0032 > round(cox.zph(mayo)$table, 3) chisq df p age 0.571 1 0.450 edema 4.295 1 0.038 log(bili) 1.512 1 0.219 log(albumin) 0.153 1 0.696 log(protime) 5.352 1 0.021 GLOBAL 10.551 5 0.061 > round(concordance(mayo)$concordance, 3) [1] 0.844 > m2 <- update(mayo, . ~ . + trt) > round(summary(m2)$coef["trt", c(2, 5)], 3) # treatment, adjusted exp(coef) Pr(>|z|) 1.145 0.465
Reading the output. All five predictors are strong (the \(p\)-values shown as \(0\) are below \(0.00005\)). The proportional hazards tests are acceptable overall; \(C = 0.84\); adjusted for prognosis, the drug’s hazard ratio is \(1.15\), \(p = 0.47\).
Python: CoxPHFitter with check_assumptions and concordance_index_.
Questions on this section
Stuck on something here? Ask below and it stays attached to this topic.