--- title: "Simulating SMART designs with sim_treatment" subtitle: "Building two-stage SMARTs step by step" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Simulating SMART designs with sim_treatment} %\VignetteEngine{knitr::rmarkdown} --- ```{r setup, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4 ) ``` ## Introduction The `sim_treatment()` function provides a flexible way to simulate treatment assignments one stage at a time. It can be used to simulate various common SMART designs, including those with response indicators or feasible sets. An additional function `block_rand()` can implement permuted block randomization to ensure greater balance than binomial randomization in later stages. This vignette walks through three two-stage designs: 1. **No response status** — all participants are re-randomized at stage 2. 2. **Response, non-responders only** — a response indicator is observed between stages and only non-responders are re-randomized. 3. **Response, everyone re-randomized** — a response indicator is observed but both responders and non-responders receive a new randomization at stage 2. The first stage helps to build our intuition in the simplest case. The second and third showcase additional features of the function. Each section includes a diagram of the design, sample simulation code, and the resulting data. ```{r load} library(rsmart) ``` ## Design 1: No response status In the simplest two-stage SMART, every participant is randomized at stage 1 and again at stage 2 regardless of any intermediate outcome. This design has four embedded treatment regimes. ```{r design1-diagram, echo=FALSE, fig.cap="Design 1: Two-stage SMART without a response. All participants are re-randomized at stage 2.", fig.height=4, fig.width=8} oldpar <- par(no.readonly = TRUE) par(mar = c(1.5, 0.2, 0.5, 0.2), family = "sans") plot.new() plot.window(xlim = c(0, 11), ylim = c(-0.8, 6.5)) # Colors (matching reference image) col_rand <- "#5B7FBA" # blue circles for randomization col_trt <- "#1A8A7A" # teal boxes for treatments col_line <- "grey30" # connecting lines col_phase <- "#6EC6B8" # light teal for phase boxes # Helper: draw a filled rounded-rect label (simulated with rect + text) draw_box <- function(x, y, label, w = 0.9, h = 0.4, bg = col_trt, tcol = "white", cex = 0.72) { rect(x - w, y - h, x + w, y + h, col = bg, border = NA) text(x, y, label, cex = cex, col = tcol, font = 2) } # --- Stage 1 randomization node (circle) --- symbols(1, 3, circles = 0.35, inches = FALSE, add = TRUE, bg = col_rand, fg = col_rand) text(1, 3, "R", cex = 0.95, font = 2, col = "white") # --- Stage 1 branches --- segments(1.35, 3, 2.8, 5, lwd = 2, col = col_line) segments(1.35, 3, 2.8, 1, lwd = 2, col = col_line) # --- Stage 1 treatment boxes --- draw_box(3.7, 5, "Treatment A") draw_box(3.7, 1, "Treatment B") segments(2.8, 5, 2.8, 5, lwd = 2, col = col_line) # connector segments(2.8, 1, 2.8, 1, lwd = 2, col = col_line) # Arrows from branch end to box arrows(2.8, 5, 2.8, 5, length = 0, lwd = 2, col = col_line) segments(2.8, 5, 3.7 - 0.9, 5, lwd = 2, col = col_line) segments(2.8, 1, 3.7 - 0.9, 1, lwd = 2, col = col_line) # --- Connectors from treatment boxes to stage 2 R nodes --- segments(3.7 + 0.9, 5, 5.8 - 0.35, 5, lwd = 2, col = col_line) segments(3.7 + 0.9, 1, 5.8 - 0.35, 1, lwd = 2, col = col_line) # --- Stage 2 randomization nodes (circles) --- symbols(5.8, 5, circles = 0.35, inches = FALSE, add = TRUE, bg = col_rand, fg = col_rand) text(5.8, 5, "R", cex = 0.85, font = 2, col = "white") symbols(5.8, 1, circles = 0.35, inches = FALSE, add = TRUE, bg = col_rand, fg = col_rand) text(5.8, 1, "R", cex = 0.85, font = 2, col = "white") # --- Stage 2 branches (upper: a1=0) --- segments(6.15, 5, 7.5, 5.9, lwd = 1.8, col = col_line) segments(6.15, 5, 7.5, 4.1, lwd = 1.8, col = col_line) # --- Stage 2 branches (lower: a1=1) --- segments(6.15, 1, 7.5, 1.9, lwd = 1.8, col = col_line) segments(6.15, 1, 7.5, 0.1, lwd = 1.8, col = col_line) # Connectors to outcome boxes segments(7.5, 5.9, 9.5 - 0.9, 5.9, lwd = 1.8, col = col_line) segments(7.5, 4.1, 9.5 - 0.9, 4.1, lwd = 1.8, col = col_line) segments(7.5, 1.9, 9.5 - 0.9, 1.9, lwd = 1.8, col = col_line) segments(7.5, 0.1, 9.5 - 0.9, 0.1, lwd = 1.8, col = col_line) # --- Outcome treatment boxes --- draw_box(9.5, 5.9, "Treatment C", w = 0.95) draw_box(9.5, 4.1, "Treatment D", w = 0.95) draw_box(9.5, 1.9, "Treatment E", w = 0.95) draw_box(9.5, 0.1, "Treatment F", w = 0.95) # --- Stage legend at the bottom --- draw_box(x = 1, y = -0.6, label = "Phase:", w = 0.5, h = 0.18, bg = col_phase, tcol = "white", cex = 0.7) draw_box(x = 3.7, y = -0.6, label = "Stage 1", h = 0.18, bg = col_phase, tcol = "white", cex = 0.7) draw_box(x = 9.5, y = -0.6, label = "Stage 2", h = 0.18, bg = col_phase, tcol = "white", cex = 0.7) par(oldpar) ``` This design has four embedded regimes: (A, C), (A, D), (B, E), and (B, F), which will be encoded as `(a1=0, a2=0)`, `(a1=0, a2=1)`, `(a1=1, a2=0)`, `(a1=1, a2=1)`, respectively, using `sim_treatment()`. ### Simulation Consider a possible trial with `500` participants. The age of participants is recorded at entry and their compliance with treatment at stage 2. We assign treatments with equal randomization using a permuted block design. We provide `sim_treatment()` with: - `n_treatments`: the number of treatments available at the stage for randomized participants - `dat`: a dataset with the initial covariates and IDs at the first stage - `stage`: the stage of the trial that treatments should be assigned for - `rand_prob_fn`: the `block_rand` function to generate permuted block randomization, with each treatment repeated twice in a block The `block_rand` function here will use a block size of 4 so a sample block could be `2 1 1 2`. This will ensure that treatment imbalance between groups is minimized. Because there is no response status, the input `randomize_response` is implicitly left at its default (`NULL`). The function will return the data frame with the treatment assignment for each participant. ```{r design1-sim} set.seed(42) n <- 500 # Start with baseline data dat1 <- data.frame( id = 1:n, x1 = round(runif(n, 25, 75)), # age x2 = rbinom(n, 1, 0.9) # treatment compliance ) # Stage 1: block randomization dat1 <- sim_treatment(n_treatments = 2, dat = dat1, stage = 1, rand_prob_fn = block_rand(block_rep = 2)) # Stage 2: everyone is re-randomized within a1 groups (no response) dat1 <- sim_treatment(n_treatments = 2, dat = dat1, stage = 2, rand_prob_fn = block_rand(block_rep = 2)) table(dat1$a1, dat1$a2) head(dat1) ``` We see that the resulting data set has the columns `r colnames(dat1)`. The treatments are appended as `a`. ## Design 2: Only non-responders are re-randomized Commonly, tailoring variables are used in SMARTs to determine sequential treatment options. A response indicator is observed between stages and then governs available treatements. Often, it may be unethical to re-randomized away from a treatment that is working well and responders are given a second stage treatment deterministically. Alternatively, non-responders may be discontinued or given a single salvage therapy. Here we encode this status using `r2` and will assign responders `r2 = 1` a single treatment (coded `a2 = 0`). Non-responders will be re-randomized. In the case where responders are randomized and non-responders are not, then the encoding of `r2 = 1` represents non-responders and the function `sim_treatment` will assign responders, now encoded as `r2 = 0`, treatments. ```{r design2-diagram, echo=FALSE, fig.cap="Design 2: Two-stage SMART where only non-responders are re-randomized at stage 2.", fig.height=5.5, fig.width=9} oldpar <- par(no.readonly = TRUE) par(mar = c(1.5, 0.2, 0.5, 0.2), family = "sans") plot.new() plot.window(xlim = c(0, 13), ylim = c(-1.5, 9)) # Colors col_rand <- "#5B7FBA" col_trt <- "#1A8A7A" col_line <- "grey30" col_resp <- "grey50" col_phase <- "#6EC6B8" draw_box <- function(x, y, label, w = 1.0, h = 0.4, bg = col_trt, tcol = "white", cex = 0.7) { rect(x - w, y - h, x + w, y + h, col = bg, border = NA) text(x, y, label, cex = cex, col = tcol, font = 2) } # --- Stage 1 randomization node --- symbols(1, 4, circles = 0.38, inches = FALSE, add = TRUE, bg = col_rand, fg = col_rand) text(1, 4, "R", cex = 0.95, font = 2, col = "white") # --- Stage 1 branches --- segments(1.38, 4, 2.5, 7, lwd = 2, col = col_line) segments(1.38, 4, 2.5, 1, lwd = 2, col = col_line) # --- Stage 1 treatment boxes --- draw_box(3.5, 7, "Treatment A") draw_box(3.5, 1, "Treatment B") segments(2.5, 7, 3.5 - 1.0, 7, lwd = 2, col = col_line) segments(2.5, 1, 3.5 - 1.0, 1, lwd = 2, col = col_line) # --- Response decision boxes --- draw_box(5.7, 7, "Response?", w = 0.9, bg = col_phase) draw_box(5.7, 1, "Response?", w = 0.9, bg = col_phase) segments(3.5 + 1.0, 7, 5.7 - 0.9, 7, lwd = 2, col = col_line) segments(3.5 + 1.0, 1, 5.7 - 0.9, 1, lwd = 2, col = col_line) # --- Upper arm (a1 = 0) response branches --- # Yes (responder) - dashed segments(6.6, 7, 7.5, 8.2, lwd = 1.5, col = col_resp, lty = 2) text(7.2, 8.5, "Yes", cex = 0.7, col = col_resp, font = 3) # No (non-responder) - solid segments(6.6, 7, 7.5, 5.8, lwd = 1.8, col = col_line) text(7.2, 5.5, "No", cex = 0.7, col = col_line, font = 3) # Responder deterministic treatment segments(7.5, 8.2, 10.8 - 1.0, 8.2, lwd = 1.5, col = col_resp, lty = 2) draw_box(10.8, 8.2, "Treatment C", w = 1.0) # Non-responder randomization symbols(8.2, 5.8, circles = 0.32, inches = FALSE, add = TRUE, bg = col_rand, fg = col_rand) text(8.2, 5.8, "R", cex = 0.75, font = 2, col = "white") segments(7.5, 5.8, 8.2 - 0.32, 5.8, lwd = 1.8, col = col_line) # Non-responder branches (upper) segments(8.52, 5.8, 9.3, 6.6, lwd = 1.5, col = col_line) segments(8.52, 5.8, 9.3, 5.0, lwd = 1.5, col = col_line) segments(9.3, 6.6, 10.8 - 1.0, 6.6, lwd = 1.5, col = col_line) segments(9.3, 5.0, 10.8 - 1.0, 5.0, lwd = 1.5, col = col_line) draw_box(10.8, 6.6, "Treatment D", w = 1.0) draw_box(10.8, 5.0, "Treatment E", w = 1.0) # --- Lower arm (a1 = 1) response branches --- # Yes (responder) - dashed segments(6.6, 1, 7.5, 2.2, lwd = 1.5, col = col_resp, lty = 2) text(7.2, 2.5, "Yes", cex = 0.7, col = col_resp, font = 3) # No (non-responder) - solid segments(6.6, 1, 7.5, -0.2, lwd = 1.8, col = col_line) text(7.2, -0.5, "No", cex = 0.7, col = col_line, font = 3) # Responder deterministic treatment segments(7.5, 2.2, 10.8 - 1.0, 2.2, lwd = 1.5, col = col_resp, lty = 2) draw_box(10.8, 2.2, "Treatment F", w = 1.0) # Non-responder randomization symbols(8.2, -0.2, circles = 0.32, inches = FALSE, add = TRUE, bg = col_rand, fg = col_rand) text(8.2, -0.2, "R", cex = 0.75, font = 2, col = "white") segments(7.5, -0.2, 8.2 - 0.32, -0.2, lwd = 1.8, col = col_line) # Non-responder branches (lower) segments(8.52, -0.2, 9.3, 0.6, lwd = 1.5, col = col_line) segments(8.52, -0.2, 9.3, -1.0, lwd = 1.5, col = col_line) segments(9.3, 0.6, 10.8 - 1.0, 0.6, lwd = 1.5, col = col_line) segments(9.3, -1.0, 10.8 - 1.0, -1.0, lwd = 1.5, col = col_line) draw_box(10.8, 0.6, "Treatment G", w = 1.0) draw_box(10.8, -1.0, "Treatment H", w = 1.0) # --- Phase legend --- draw_box(x = 1, y = -1.7, label = "Phase:", w = 0.5, h = 0.18, bg = col_phase, tcol = "white", cex = 0.65) draw_box(x = 3.5, y = -1.7, label = "Stage 1", w = 0.9, h = 0.18, bg = col_phase, tcol = "white", cex = 0.65) draw_box(x = 5.7, y = -1.7, label = "Response", w = 0.9, h = 0.18, bg = col_phase, tcol = "white", cex = 0.65) draw_box(x = 10.8, y = -1.7, label = "Stage 2", w = 0.9, h = 0.18, bg = col_phase, tcol = "white", cex = 0.65) par(oldpar) ``` Responders follow a deterministic path (dashed lines). There are still four embedded regimes, but now follow rules "give treamtent 1 at stage 1, if response give treatment 2 at stage 2, otherwise give treatment 3 at stage 2. (1, 2, 3)" The embedded regimes are now encoded as: `(a1=0, a2=0, a2=0)`, `(a1=0, a2=0, a2=1)`, `(a1=1, a2=0, a2=0)`, `(a1=1, a2=0, a2=1)`. ### Simulation We use similar inputs as before, but with the additional input `randomize_response = "N"` so that we assign `a2 = 0` to all responders and randomize only the non-responders within each `a1` group. ```{r design2-sim} set.seed(42) n <- 500 dat2 <- data.frame( id = 1:n, t1 = sort(round(runif(n, 0, 365 * 2))), # enrollment day x1 = round(runif(n, 25, 75)) # age ) # Stage 1: block randomization (data sorted by t1) dat2 <- sim_treatment(n_treatments = 2, dat = dat2, stage = 1, rand_prob_fn = block_rand(block_rep = 2)) # Generate response and stage 2 timing dat2$r2 <- rbinom(n, 1, 0.5) dat2$t2 <- dat2$t1 + 100 + round(runif(n, -10, 10)) # Sort by t2 before stage 2 randomization (see note below) dat2 <- dat2[order(dat2$t2), ] # Stage 2: only non-responders re-randomized dat2 <- sim_treatment(n_treatments = 2, dat = dat2, stage = 2, rand_prob_fn = block_rand(block_rep = 2), randomize_response = "N") table(dat2$a1, dat2$a2, dat2$r2, dnn = c("a1", "a2", "r2")) ``` Because of the use of the permuted block randomization, we see that among responders and conditioned on the first treatment, there is relative balance between treatment assignments. All responders `r2 = 1` received `a2 = 0`. ## Design 3: Both responders and non-responders re-randomized In some SMARTs, all participants are re-randomized at stage 2, with treatments allowed to vary by response status. This means responders may or may not receive different second-stage treatments than non-responders, and the randomization probabilities or allocation schedules can differ between the two groups. ```{r design3-diagram, echo=FALSE, fig.cap="Design 3: Two-stage SMART where both responders and non-responders are re-randomized at stage 2.", fig.height=6.5, fig.width=9} oldpar <- par(no.readonly = TRUE) par(mar = c(1.5, 0.2, 0.5, 0.2), family = "sans") plot.new() plot.window(xlim = c(0, 13), ylim = c(-2.5, 10.5)) # Colors col_rand <- "#5B7FBA" col_trt <- "#1A8A7A" col_line <- "grey30" col_resp <- "grey50" col_phase <- "#6EC6B8" draw_box <- function(x, y, label, w = 1.0, h = 0.4, bg = col_trt, tcol = "white", cex = 0.7) { rect(x - w, y - h, x + w, y + h, col = bg, border = NA) text(x, y, label, cex = cex, col = tcol, font = 2) } # --- Stage 1 randomization node --- symbols(1, 4, circles = 0.38, inches = FALSE, add = TRUE, bg = col_rand, fg = col_rand) text(1, 4, "R", cex = 0.95, font = 2, col = "white") # --- Stage 1 branches --- segments(1.38, 4, 2.5, 7.5, lwd = 2, col = col_line) segments(1.38, 4, 2.5, 0.5, lwd = 2, col = col_line) # --- Stage 1 treatment boxes --- draw_box(3.5, 7.5, "Treatment A") draw_box(3.5, 0.5, "Treatment B") segments(2.5, 7.5, 3.5 - 1.0, 7.5, lwd = 2, col = col_line) segments(2.5, 0.5, 3.5 - 1.0, 0.5, lwd = 2, col = col_line) # --- Response decision boxes --- draw_box(5.7, 7.5, "Response?", w = 0.9, bg = col_phase) draw_box(5.7, 0.5, "Response?", w = 0.9, bg = col_phase) segments(3.5 + 1.0, 7.5, 5.7 - 0.9, 7.5, lwd = 2, col = col_line) segments(3.5 + 1.0, 0.5, 5.7 - 0.9, 0.5, lwd = 2, col = col_line) # ===== Upper arm (a1 = 0) ===== # Yes (responder) segments(6.6, 7.5, 7.3, 9.2, lwd = 1.8, col = col_line) text(7.0, 9.5, "Yes", cex = 0.7, col = col_resp, font = 3) # No (non-responder) segments(6.6, 7.5, 7.3, 5.8, lwd = 1.8, col = col_line) text(7.0, 5.5, "No", cex = 0.7, col = col_line, font = 3) # Responder randomization (a1=0, resp) symbols(7.9, 9.2, circles = 0.30, inches = FALSE, add = TRUE, bg = col_rand, fg = col_rand) text(7.9, 9.2, "R", cex = 0.65, font = 2, col = "white") segments(7.3, 9.2, 7.9 - 0.30, 9.2, lwd = 1.5, col = col_line) segments(8.2, 9.2, 9.0, 9.9, lwd = 1.3, col = col_line) segments(8.2, 9.2, 9.0, 8.5, lwd = 1.3, col = col_line) segments(9.0, 9.9, 10.8 - 1.0, 9.9, lwd = 1.3, col = col_line) segments(9.0, 8.5, 10.8 - 1.0, 8.5, lwd = 1.3, col = col_line) draw_box(10.8, 9.9, "Treatment C", w = 1.0) draw_box(10.8, 8.5, "Treatment D", w = 1.0) # Non-responder randomization (a1=0, non-resp) symbols(7.9, 5.8, circles = 0.30, inches = FALSE, add = TRUE, bg = col_rand, fg = col_rand) text(7.9, 5.8, "R", cex = 0.65, font = 2, col = "white") segments(7.3, 5.8, 7.9 - 0.30, 5.8, lwd = 1.5, col = col_line) segments(8.2, 5.8, 9.0, 6.5, lwd = 1.3, col = col_line) segments(8.2, 5.8, 9.0, 5.1, lwd = 1.3, col = col_line) segments(9.0, 6.5, 10.8 - 1.0, 6.5, lwd = 1.3, col = col_line) segments(9.0, 5.1, 10.8 - 1.0, 5.1, lwd = 1.3, col = col_line) draw_box(10.8, 6.5, "Treatment E", w = 1.0) draw_box(10.8, 5.1, "Treatment F", w = 1.0) # ===== Lower arm (a1 = 1) ===== # Yes (responder) segments(6.6, 0.5, 7.3, 2.2, lwd = 1.8, col = col_line) text(7.0, 2.5, "Yes", cex = 0.7, col = col_resp, font = 3) # No (non-responder) segments(6.6, 0.5, 7.3, -1.2, lwd = 1.8, col = col_line) text(7.0, -1.5, "No", cex = 0.7, col = col_line, font = 3) # Responder randomization (a1=1, resp) symbols(7.9, 2.2, circles = 0.30, inches = FALSE, add = TRUE, bg = col_rand, fg = col_rand) text(7.9, 2.2, "R", cex = 0.65, font = 2, col = "white") segments(7.3, 2.2, 7.9 - 0.30, 2.2, lwd = 1.5, col = col_line) segments(8.2, 2.2, 9.0, 2.9, lwd = 1.3, col = col_line) segments(8.2, 2.2, 9.0, 1.5, lwd = 1.3, col = col_line) segments(9.0, 2.9, 10.8 - 1.0, 2.9, lwd = 1.3, col = col_line) segments(9.0, 1.5, 10.8 - 1.0, 1.5, lwd = 1.3, col = col_line) draw_box(10.8, 2.9, "Treatment G", w = 1.0) draw_box(10.8, 1.5, "Treatment H", w = 1.0) # Non-responder randomization (a1=1, non-resp) symbols(7.9, -1.2, circles = 0.30, inches = FALSE, add = TRUE, bg = col_rand, fg = col_rand) text(7.9, -1.2, "R", cex = 0.65, font = 2, col = "white") segments(7.3, -1.2, 7.9 - 0.30, -1.2, lwd = 1.5, col = col_line) segments(8.2, -1.2, 9.0, -0.5, lwd = 1.3, col = col_line) segments(8.2, -1.2, 9.0, -1.9, lwd = 1.3, col = col_line) segments(9.0, -0.5, 10.8 - 1.0, -0.5, lwd = 1.3, col = col_line) segments(9.0, -1.9, 10.8 - 1.0, -1.9, lwd = 1.3, col = col_line) draw_box(10.8, -0.5, "Treatment I", w = 1.0) draw_box(10.8, -1.9, "Treatment J", w = 1.0) # --- Phase legend --- draw_box(x = 1, y = -2.6, label = "Phase:", w = 0.5, h = 0.18, bg = col_phase, tcol = "white", cex = 0.65) draw_box(x = 3.5, y = -2.6, label = "Stage 1", w = 0.9, h = 0.18, bg = col_phase, tcol = "white", cex = 0.65) draw_box(x = 5.7, y = -2.6, label = "Response", w = 0.9, h = 0.18, bg = col_phase, tcol = "white", cex = 0.65) draw_box(x = 10.8, y = -2.6, label = "Stage 2", w = 0.9, h = 0.18, bg = col_phase, tcol = "white", cex = 0.65) par(oldpar) ``` Because both responders and non-responders are randomized at stage 2, the number of embedded regimes grows. With the canonical encoding, each regime specifies a stage-1 treatment, a responder stage-2 treatment, and a non-responder stage-2 treatment, yielding $2 \times 2 \times 2 = 8$ embedded regimes. For completeness, we can enumerate these regimes as follows: 1. (0, 0, 0) 2. (0, 0, 1) 3. (0, 1, 0) 4. (0, 1, 1) 5. (1, 0, 0) 6. (1, 0, 1) 7. (1, 1, 0) 8. (1, 1, 1) Again, using the same format where these are the treatments given at stage 1, at stage 2 for responders, and at stage 2 for non-responders. ### Simulation Setting `randomize_response = "Y"` includes the response column `r2` in the grouping variables so that randomization is performed independently within each combination of `a1` and `r2`. ```{r design3-sim} set.seed(42) n <- 500 dat3 <- data.frame( id = 1:n, t1 = sort(round(runif(n, 0, 365 * 2))), x1 = round(runif(n, 25, 75)) ) # Stage 1 dat3 <- sim_treatment(n_treatments = 2, dat = dat3, stage = 1, rand_prob_fn = block_rand(block_rep = 2)) # Generate response and timing dat3$r2 <- rbinom(n, 1, 0.5) dat3$t2 <- dat3$t1 + 100 + round(runif(n, -10, 10)) # Sort by t2 before stage 2 dat3 <- dat3[order(dat3$t2), ] # Stage 2: responders AND non-responders randomized (stratified by response) dat3 <- sim_treatment(n_treatments = 2, dat = dat3, stage = 2, rand_prob_fn = block_rand(block_rep = 2), randomize_response = "Y") table(dat3$a1, dat3$a2, dat3$r2, dnn = c("a1", "a2", "r2")) ``` Unlike Design 2, responders now receive both `a2 = 0` and `a2 = 1`. We again see that conditioned on response status and first treatment, the a2 assignments are relatively well-balanced. ## Note on ordering and block randomization When using `block_rand()` (or any allocation-schedule–based randomization), the order in which participants appear in the data frame determines their position in the allocation schedule. If enrollment timing matters — for example, in a group sequential design — the data should be **sorted by the relevant time variable before calling `sim_treatment()`** at each stage. For stage 1, sort by the enrollment time (e.g., `t1`): ```r dat <- dat[order(dat$t1), ] dat <- sim_treatment(n_treatments = 2, dat = dat, stage = 1, rand_prob_fn = block_rand(block_rep = 2)) ``` For stage 2 and beyond, sort by the time of re-randomization (e.g., `t2`): ```r dat <- dat[order(dat$t2), ] dat <- sim_treatment(n_treatments = 2, dat = dat, stage = 2, rand_prob_fn = block_rand(block_rep = 2), randomize_response = "N") ``` This ensures that the permuted block allocation schedule aligns with the temporal order of participant arrivals at each decision point. If simple randomization (the default `rand_prob_fn`) is used instead of block randomization, the row order does not affect the allocation and sorting is not required.