# Simulate a group sequential trial: 4 interim + 1 final look
# O'Brien-Fleming boundaries (conservative early, lenient late)
n_looks <- 5
info_fractions <- seq(0.2, 1.0, by=0.2)
# O'Brien-Fleming critical values (approximate)
obf_alpha <- c(4.56, 3.23, 2.63, 2.28, 2.04) / sqrt(n_looks)
# Simulate trial data with true treatment effect
n_total <- 200
trt <- rep(0:1, each=n_total/2)
y <- 10 - 1.8*trt + rnorm(n_total, 0, 4)
# Compute running z-statistic at each look
look_n <- floor(n_total * info_fractions)
z_stats <- sapply(look_n, function(k) {
idx <- 1:k
m1 <- mean(y[trt==1 & seq_along(trt) %in% idx])
m0 <- mean(y[trt==0 & seq_along(trt) %in% idx])
s <- sd(y[seq_along(y) %in% idx])
(m1 - m0) / (s * sqrt(2/k))
})
tibble(fraction=info_fractions, z=abs(z_stats), boundary=obf_alpha) |>
ggplot(aes(fraction)) +
geom_ribbon(aes(ymin=0, ymax=boundary), fill="#253554", alpha=0.6) +
geom_line(aes(y=boundary), color="#e63946", linewidth=1.2, linetype=2) +
geom_line(aes(y=z), color="#0891b2", linewidth=1.3) +
geom_point(aes(y=z), color="#22d3ee", size=4) +
annotate("text", x=0.85, y=3.0, label="O'Brien-Fleming\nboundary", color="#e63946", size=3.2) +
annotate("text", x=0.55, y=1.2, label="Observed |Z|", color="#0891b2", size=3.2) +
labs(title="Group-sequential trial: |Z| crosses boundary at final look → stop for efficacy",
x="Information fraction (proportion of data observed)", y="|Z statistic|") +
theme_di()