A multi-arm trial randomizes subjects to a shared control and two or more experimental arms, and each experimental arm is compared against the same control. This is the standard screening design for several candidate doses or regimens, where the goal is to identify which experimental arms beat control while controlling the family-wise error rate across the comparisons.
simdata_fast generates such a trial directly. When
n is a vector of length greater than two together with a
per-arm survival list, each arm is generated with the validated
single-group kernel over a common accrual window, and the arms are
stacked into one data frame with a group column labeled 1
to length(n). The analysis path keeps its two-group
contract, so a multi-arm design is analyzed as a set of pairwise
contrasts. Each experimental arm is compared against the control by
subsetting the generated data to those two arms and calling
analysis_fast once per contrast. Because every contrast
reuses the same control arm, the pairwise statistics are positively
correlated, and that correlation is what a Dunnett-type adjustment
exploits to be less conservative than a Bonferroni correction.
This vignette generates a three-arm trial (one control and two experimental arms), runs the two pairwise log-rank tests, and reports the operating characteristics of the screening design under a Bonferroni correction, including the disjunctive power of finding at least one winning arm and the probability of selecting the truly better experimental arm. It then confirms empirically that the family-wise error rate is controlled under the global null.
nsim <- 2000
seed <- 20260711
# Three arms: group 1 = control, groups 2 and 3 = experimental.
# Per-arm sample size (balanced 1:1:1 randomization).
n_arm <- c(150, 150, 150)
# Median survival by arm, in months. The two experimental arms have an
# increasing benefit over control.
mst <- c(control = 12, dose_low = 16, dose_high = 20)
# Piecewise-uniform accrual: constant rate over 12 months enrolling all 450.
a_time <- c(0, 12)
a_rate <- sum(n_arm) / 12
# Single calendar analysis time, in months.
analysis_month <- 30
# One-sided family-wise significance level and the number of experimental arms.
alpha_total <- 0.025
n_contrast <- length(n_arm) - 1L
# Implied exponential hazard ratios of each experimental arm versus control.
data.frame(
arm = names(mst)[-1],
median = mst[-1],
HR_vs_control = round(mst["control"] / mst[-1], 3),
row.names = NULL
)
#> arm median HR_vs_control
#> 1 dose_low 16 0.75
#> 2 dose_high 20 0.60The experimental arm with the longer median has the smaller hazard ratio and is the stronger candidate.
A single simdata_fast call generates all three arms. The
per-arm medians are supplied as an e.median list with one
element per arm, and the arms share the accrual window and the master
seed.
pairwise_fast() convenience wrapperThe loop above is packaged by pairwise_fast, which runs
the control-versus-arm analysis for every experimental arm and stacks
the results into one long data frame with an arm column.
With time.looks all contrasts share the fixed calendar
cutoff, and adjust = "bonferroni" appends a
p.adj column that multiplies each p-value by the number of
contrasts.
pw <- pairwise_fast(
df,
control = control_arm,
time.looks = analysis_month,
stat = "logrank",
side = 1,
adjust = "bonferroni"
)
head(pw[, c("arm", "sim", "cutoff", "logrank.z", "logrank.p", "p.adj")])
#> arm sim cutoff logrank.z logrank.p p.adj
#> 1 2 1 30 -2.949901 1.589381e-03 3.178762e-03
#> 2 2 2 30 -2.369475 8.906683e-03 1.781337e-02
#> 3 2 3 30 -2.286564 1.111065e-02 2.222130e-02
#> 4 2 4 30 -3.101323 9.632898e-04 1.926580e-03
#> 5 2 5 30 -4.009756 3.039077e-05 6.078154e-05
#> 6 2 6 30 -1.214419 1.122938e-01 2.245877e-01The marginal per-contrast rejection rates from the wrapper reproduce the manual loop above.
An event-driven screening design instead lets a designated primary contrast fix the data cutoff. Here arm 3 is the primary comparison, its target of 200 control-plus-arm-3 events defines the per-simulation calendar cutoff, and both contrasts are analyzed at that same cutoff.
pw_ev <- pairwise_fast(
df,
control = control_arm,
event.looks = 200,
primary = 3,
stat = "logrank",
side = 1,
adjust = "bonferroni"
)
head(pw_ev[, c("arm", "sim", "cutoff", "reached", "logrank.z", "p.adj")])
#> arm sim cutoff reached logrank.z p.adj
#> 1 2 1 30.41747 TRUE -2.948070 0.0031976438
#> 2 2 2 28.86407 TRUE -2.304046 0.0212200415
#> 3 2 3 30.85221 TRUE -2.390590 0.0168213177
#> 4 2 4 30.28744 TRUE -3.230967 0.0012337209
#> 5 2 5 30.39027 TRUE -3.860549 0.0001131327
#> 6 2 6 28.89168 TRUE -1.090690 0.2754092176The two contrasts at a given simulation carry the same
cutoff, which is the calendar time at which the primary
contrast reached its event target.
The screening design rejects the control-versus-arm null for an
experimental arm when its one-sided p-value falls below the
multiplicity-adjusted threshold. A Bonferroni correction spends the
family-wise level equally across the two contrasts, giving a
per-contrast threshold of alpha_total / n_contrast. The
marginal (unadjusted) power of each contrast is shown alongside the
adjusted power for reference.
thr_marginal <- alpha_total
thr_bonferroni <- alpha_total / n_contrast
reject_marginal <- P <= thr_marginal
reject_bonferroni <- P <= thr_bonferroni
power_table <- data.frame(
contrast = colnames(P),
power_marginal = round(colMeans(reject_marginal), 3),
power_bonferroni = round(colMeans(reject_bonferroni), 3),
row.names = NULL
)
power_table
#> contrast power_marginal power_bonferroni
#> 1 arm2_vs_ctrl 0.539 0.431
#> 2 arm3_vs_ctrl 0.941 0.899The disjunctive power is the probability of declaring at least one experimental arm superior to control under the Bonferroni rule. The probability of correctly selecting the stronger experimental arm is the probability that the arm with the larger true effect is among the rejected arms.
any_reject <- rowSums(reject_bonferroni) > 0
# The stronger experimental arm is the last column (largest median).
stronger_col <- n_contrast
select_stronger <- reject_bonferroni[, stronger_col]
data.frame(
quantity = c("Disjunctive power (any arm beats control)",
"Power to declare the stronger arm",
"Expected number of arms declared"),
value = c(round(mean(any_reject), 3),
round(mean(select_stronger), 3),
round(mean(rowSums(reject_bonferroni)), 3)),
row.names = NULL
)
#> quantity value
#> 1 Disjunctive power (any arm beats control) 0.911
#> 2 Power to declare the stronger arm 0.899
#> 3 Expected number of arms declared 1.330The two pairwise log-rank statistics share the control arm, so they are positively correlated. This correlation is the reason a Dunnett adjustment is less conservative than Bonferroni, since Bonferroni ignores it and treats the comparisons as if they were independent.
round(stats::cor(Z, use = "complete.obs"), 3)
#> arm2_vs_ctrl arm3_vs_ctrl
#> arm2_vs_ctrl 1.000 0.508
#> arm3_vs_ctrl 0.508 1.000For balanced allocation with a shared control the theoretical correlation of the two standardized statistics is close to one half, and the empirical value here reproduces that magnitude to simulation error.
Regenerating the trial with all three arms sharing the control median gives the global null, under which no experimental arm truly beats control. The Bonferroni rule should reject at least one arm no more than the nominal family-wise level.
df0 <- simdata_fast(
nsim = nsim,
n = n_arm,
a.time = a_time,
a.rate = a_rate,
e.median = list(mst["control"], mst["control"], mst["control"]),
seed = seed + 1
)
P0 <- matrix(NA_real_, nrow = nsim, ncol = n_contrast)
for (k in seq_along(exp_arms)) {
sub0 <- df0[df0$group %in% c(control_arm, exp_arms[k]), ]
res0 <- analysis_fast(sub0, control = control_arm,
time.looks = analysis_month, stat = "logrank", side = 1)
P0[, k] <- res0$logrank.p
}
data.frame(
quantity = c("Per-contrast type I error (nominal)",
"Family-wise error, Bonferroni"),
value = c(round(mean(P0 <= alpha_total), 3),
round(mean(rowSums(P0 <= thr_bonferroni) > 0), 3)),
row.names = NULL
)
#> quantity value
#> 1 Per-contrast type I error (nominal) 0.024
#> 2 Family-wise error, Bonferroni 0.022The per-contrast error is near the nominal one-sided level, and the Bonferroni family-wise error is at or below the target, confirming that the screening rule controls the error rate. A Dunnett procedure would recover some of the conservatism by using the shared-control correlation, at the cost of the closed-form simplicity of the Bonferroni threshold.
The multi-arm generation reuses the validated single-group kernel
once per arm, so every survival, dropout, and accrual draw comes from
the same primitives that back the two-group path, and the whole study
runs in a few seconds. The analysis keeps its two-group contract, so
multiplicity and arm selection are handled as a thin post-processing
layer over the pairwise statistics rather than inside the analysis
kernel. This keeps the design flexible. Any of the statistics offered by
analysis_fast, not only the log-rank test, can drive the
pairwise comparisons, and any multiple-comparison procedure can be
applied to the resulting p-values.
Multi-arm mode shares the master seed across arms and generates each arm over a common accrual window, which is adequate for operating-characteristic screening. It does not reproduce a single randomization list with a common accrual stream, and it does not combine with subgroups or the illness-death model, both of which remain two-group.
Dunnett, C. W. (1955). A multiple comparison procedure for comparing several treatments with a control. Journal of the American Statistical Association, 50(272), 1096-1121.