A stepped-wedge trial assigns clusters to sequences. Every sequence starts under control; sequences then cross over to the intervention at different periods, and once a sequence has crossed it stays crossed. By the end of the trial every cluster has received the intervention.
The package encodes exactly that and nothing more. A cluster can be a hospital ward, a clinic, a school, a general practice, a village, or an individual clinician; a sequence can be a region, a wave, a specialty, or an arbitrary randomisation group. The software does not care.
Design and assumptions are kept apart, because they answer different questions:
sw_design()] – who crosses over, and
when.sw_assumptions()] – what the outcome
is expected to do.The most common design gives one baseline period and then crosses one sequence per period.
design <- sw_design(
clusters_per_sequence = c(8, 8, 8, 8),
crossover_period = c(2, 3, 4, 5),
n_periods = 5
)
design
#> <sw_design>
#> 4 sequences, 5 periods, 32 clusters total
#> clusters per sequence: 8, 8, 8, 8
#> treatment schedule (rows = sequences, cols = periods):
#> Period 1 Period 2 Period 3 Period 4 Period 5
#> Sequence 1 0 1 1 1 1
#> Sequence 2 0 0 1 1 1
#> Sequence 3 0 0 0 1 1
#> Sequence 4 0 0 0 0 1Designs are not restricted to that shape. Sequences may hold unequal numbers of clusters, several sequences may cross at once, and there may be more than one baseline period. Anything expressible as a 0/1 schedule is expressible here:
schedule <- matrix(
c(0, 0, 1, 1, 1,
0, 0, 0, 1, 1,
0, 0, 0, 0, 1),
nrow = 3, byrow = TRUE
)
custom <- sw_design(
clusters_per_sequence = c(8, 10, 12), # unequal
treatment = schedule # two baseline periods
)
custom
#> <sw_design>
#> 3 sequences, 5 periods, 30 clusters total
#> clusters per sequence: 8, 10, 12
#> treatment schedule (rows = sequences, cols = periods):
#> Period 1 Period 2 Period 3 Period 4 Period 5
#> Sequence 1 0 0 1 1 1
#> Sequence 2 0 0 0 1 1
#> Sequence 3 0 0 0 0 1assumptions <- sw_assumptions(
baseline_prob = 0.05, # control-arm risk
treatment_or = 1.6, # target effect
icc = 0.05, # cluster correlation
period_effects = c(0, 0.05, 0.10, 0.15, 0.20), # secular trend, log-odds
n_per_cluster_period = 20
)
assumptions
#> <sw_assumptions>
#> outcome: binary
#> treatment: OR = 1.600 (log-odds 0.470)
#> cluster_sd = 0.416 (icc = 0.050)
#> baseline (prob scale): 0.050
#> period effects (log-odds): 0.00, 0.05, 0.10, 0.15, 0.20Three points worth drawing out.
Specify the ICC, not the random-effect SD. Most
people can defend an ICC and almost nobody can defend a random-intercept
standard deviation. Supply icc and the conversion is
handled; supply cluster_sd if you prefer. Supplying both is
an error unless they agree.
Model the secular trend explicitly. Outcomes often
drift over time for reasons unrelated to the intervention. In a
stepped-wedge design time and treatment are partially confounded by
construction, so a trend you do not simulate is a trend you have not
accounted for. period_effects puts it in the generating
model, where the analysis has to contend with it.
Cluster sizes need not be equal. Supply a single number, a sequence-by-period matrix, or a function:
trial <- simulate_swcrt(design, assumptions, seed = 123)
head(trial)
#> cluster_id sequence sequence_idx period intervention n events true_prob
#> 1 1 Sequence 1 1 1 0 20 2 0.04001509
#> 2 1 Sequence 1 1 2 1 20 1 0.06551863
#> 3 1 Sequence 1 1 3 1 20 2 0.06864724
#> 4 1 Sequence 1 1 4 1 20 2 0.07191375
#> 5 1 Sequence 1 1 5 1 20 2 0.07532312
#> 6 2 Sequence 1 1 1 0 20 1 0.04564163The columns are deliberately neutral: cluster_id,
sequence, period, intervention,
n, events. Any aggregated cluster-period
dataset in this shape can be fed to the analysis function, whether it
came from simulate_swcrt() or from a real trial.
The default analysis is the standard Hussey-Hughes model: treatment, a categorical period effect, and a cluster random intercept. You can rename any column, or hand over a formula outright:
power <- power_swcrt(design, assumptions, nsim = 1000, seed = 1, n_cores = 4)
summary(power)
plot(power)The returned object reports more than a single number, and the extras are not decoration. The Monte Carlo interval tells you how much of the estimate is simulation noise. The failure and singular-fit rates tell you whether the analysis model is actually estimable at this design size – a “power” computed from replicates that mostly failed to converge is not a power. Bias and coverage tell you whether the estimator is behaving.
The interval for power is exact (Clopper-Pearson) rather than Wald. A Wald interval collapses to zero width when the estimate hits 0 or 1, which is exactly the high-power regime a power analysis is trying to land in.
Power depends on assumptions you cannot verify in advance, so vary them.
grid <- power_grid(
design, assumptions,
vary = list(
treatment_or = c(1.3, 1.5, 1.75, 2.0),
icc = c(0.02, 0.05, 0.10),
clusters_per_sequence = list(c(6, 6, 6, 6), c(8, 8, 8, 8), c(10, 10, 10, 10))
),
nsim = 500, n_cores = 4, seed = 1
)
summary(grid)
plot(grid)Simulations are reproducible under a given seed
regardless of n_cores: each replicate draws from its own
random-number stream, so adding cores makes the run faster without
moving the answer.
validate_engine() re-runs the power calculation with an
independently written reference implementation that shares no code with
the main engine, and reports whether the two agree within Monte Carlo
error.
compare_with_swdpwr() will also line the simulation up
against a closed-form calculation, if you have that package installed.
Treat the result as information rather than as a target: closed-form
stepped-wedge power is asymptotic in the number of clusters, and with
the small cluster counts these trials usually run it can report power
well above what a simulated trial achieves. Where the two disagree, the
simulation is the one that reflects the model you will actually fit.