gsDesign2
Next-Generation Group Sequential Design
Guide users through next-generation group sequential design using the gsDesign2 R package. Use this skill when the user asks about: gs_design_ahr, gs_power_ahr, gs_update_ahr, sequential_pval, average hazard ratio designs, non-proportional hazards, piecewise enrollment/failure rates, spending time, or information fraction computation.
Group Sequential Design with gsDesign2
Note: This skill targets gsDesign2 >= 1.1.9. The vendored llms.txt may lag behind; local API docs are in llms_local.txt.
API reference
- Vendored function docs:
references/llms.txt - Full function docs (local v1.1.9+):
references/llms_local.txt - Workflow patterns:
references/code_patterns.md
Key functions
Design (solve for sample size)
gs_design_ahr()- AHR-based group sequential design (primary workhorse)gs_design_wlr()- Weighted logrank designgs_design_rd()- Rate difference design (now with minimum risk weighting in 1.1.9)gs_design_combo()- Combination test designgs_design_npe()- Non-proportional effect design (general)fixed_design()- Fixed (non-sequential) designs
Power (given fixed assumptions)
gs_power_ahr()- Power for AHR designsgs_power_wlr()- Power for weighted logrank designsgs_power_rd()- Power for rate difference designsgs_power_combo()- Power for combination testsgs_power_npe()- Power for general NPE designs
Information and bounds
gs_info_ahr()/gs_info_wlr()/gs_info_rd()/gs_info_combo()- Statistical informationgs_spending_bound()- Spending function boundsgs_spending_combo()- Spending bounds for combination testsgs_b()- Fixed boundary valuesgs_bound_summary()- Formatted bound summary (supports multiple alpha levels)
Analysis
gs_update_ahr()- Update bounds with observed data (supports stratified piecewise events)gs_cp_npe()- Conditional power under NPHsequential_pval()- Sequential p-value for AHR designs (new in 1.1.9)
Important: sequential_pval() vs sequentialPValue()
gsDesign2::sequential_pval(): Use with gsDesign2 objects (output ofgs_design_ahr(),gs_power_ahr())gsDesign::sequentialPValue(): Use with gsDesign objects (output ofgsDesign(),gsSurv(),gsSurvCalendar(),gsSurvPower())- Do NOT mix: each function expects the design object from its own package
Enrollment and failure rates
define_enroll_rate()- Piecewise enrollment rates (supports strata)define_fail_rate()- Piecewise failure rates with HR (supports strata)
Utilities
ahr()/ahr_blinded()- Average hazard ratio computationexpected_accrual()/expected_event()/expected_time()- Expected quantitiespw_info()- Piecewise informationto_integer()- Integer sample size roundingppwe()/s2pwe()- Piecewise exponential utilitieswlr_weight()- Weight functions for weighted logrank
Output
as_gt()/as_rtf()- Table output (footnotes can be suppressed)summary()/text_summary()- Text summariesgs_bound_summary()- Bound summary table
Workflow patterns
For detailed code templates, read references/code_patterns.md.
Topics covered: - Enrollment and failure rate setup (piecewise, stratified, scaling to target N) - Fixed designs and group sequential designs with gs_design_ahr() - Power computation with gs_power_ahr() (event = NULL caveat) - Non-proportional hazards scenarios (delayed effect, crossing, diminishing) - Spending functions and bound specification - Spending time (decoupling spending from information fraction) - Rate difference designs for binary endpoints - Weighted logrank and MaxCombo combination tests - Integer rounding with to_integer() - Sequential p-values with sequential_pval() for multiplicity - Updating bounds with gs_update_ahr() (stratified piecewise events) - Conditional power with gs_cp_npe() - Output and reporting (gt, RTF, bound summary) - info_scale options (h0_info, h1_info, h0_h1_info) - Stratified designs (subgroup + complement populations)
Important design considerations
info_frac = NULL: Use withanalysis_timeto let timing drive the design; gsDesign2 derives the information fractionevent = NULLin gs_power_ahr: Always set this when usinganalysis_time; the defaultc(30, 40, 50)causes length mismatchesinfo_scale = "h0_info": Matches gsDesign convention; recommended for multiplicity workflows- Spending time: Use
timinginupar/lparto decouple spending from information fraction (critical for delayed effects and multiple hypotheses) - Stratified designs: Use
stratumcolumn indefine_enroll_rate()anddefine_fail_rate(); scale complement enrollment from subgroup using prevalence sequential_pval()(v1.1.9+): Works directly withgs_design_ahr()output. Use this for gsDesign2-native workflows. For gsDesign objects (fromgsSurv(),gsSurvCalendar(),gsSurvPower()), usegsDesign::sequentialPValue()instead.gs_design_ahr()spending time output (v1.1.8+): Can now output spending time directly for transparencygs_update_ahr()event_tbl: Supports piecewise event tables for delayed-effect designs where events per interval are tracked- Non-binding futility: Use
binding = FALSEso efficacy bounds are computed ignoring the futility bound - Spending functions as strings:
sf = "sfLDOF"works in addition tosf = sfLDOF
Code Patterns
Code Patterns for gsDesign2
Note: These patterns target gsDesign2 >= 1.1.8 (main branch at github.com/Merck/gsDesign2).
Table of Contents
- Enrollment and failure rate setup
- Fixed design
- Group sequential design with gs_design_ahr
- Power computation with gs_power_ahr
- Non-proportional hazards scenarios
- Spending functions and bounds
- Spending time (decoupling spending from information fraction)
- Rate difference designs
- Weighted logrank and combination tests
- Integer sample size rounding
- Sequential p-values
- Updating bounds at analysis (gs_update_ahr)
- Conditional power
- Output and reporting
- info_scale options
- Stratified designs
Enrollment and failure rate setup
gsDesign2 uses define_enroll_rate() and define_fail_rate() tibbles to specify piecewise constant rates.
library(gsDesign2)
library(gsDesign)
# Piecewise enrollment with ramp-up
enroll_rate <- define_enroll_rate(
duration = c(2, 2, 2, 6), # Period durations
rate = c(8, 12, 16, 24) # Patients per month per period
)
# Exponential failure with proportional hazards
fail_rate <- define_fail_rate(
duration = Inf, # Single period (exponential)
fail_rate = log(2) / 12, # Control median = 12 months
hr = 0.7, # Hazard ratio under H1
dropout_rate = 0.001
)
# Delayed treatment effect (non-proportional hazards)
fail_rate_nph <- define_fail_rate(
duration = c(3, Inf), # Changepoint at 3 months
fail_rate = log(2) / c(8, 14), # Piecewise control hazard
hr = c(0.9, 0.6), # HR = 0.9 early, 0.6 after delay
dropout_rate = 0.001
)Scaling enrollment to target N
# Set relative rates, then scale to target sample size
enroll_rate <- define_enroll_rate(
duration = c(2, 2, 2, 18),
rate = 1:4 # Relative rates
)
target_n <- 400
enroll_rate$rate <- enroll_rate$rate * target_n / sum(enroll_rate$duration * enroll_rate$rate)Stratified rates
# Two strata with different enrollment and failure rates
enroll_rate_strat <- define_enroll_rate(
stratum = rep(c("BM+", "BM-"), each = 4),
duration = rep(c(2, 2, 2, 6), 2),
rate = c(3, 5, 7, 10, 2, 3, 4, 6) # Different rates per stratum
)
fail_rate_strat <- define_fail_rate(
stratum = c("BM+", "BM-"),
duration = c(Inf, Inf),
fail_rate = log(2) / 12,
hr = c(0.6, 0.85), # Different HR per stratum
dropout_rate = 0.001
)Fixed design
For single-analysis designs without interim looks.
# AHR-based fixed design
fx <- fixed_design_ahr(
enroll_rate = enroll_rate,
fail_rate = fail_rate,
alpha = 0.025,
power = 0.9,
ratio = 1,
study_duration = 36
)
fx |> summary() |> as_gt()
# Rate difference (binary endpoint)
fx_rd <- fixed_design_rd(
alpha = 0.025,
power = 0.9,
p_c = 0.15,
p_e = 0.30,
rd0 = 0,
n = NULL # Solve for sample size
)Group sequential design with gs_design_ahr
gs_design_ahr() derives sample size targeting a specified power.
Basic design with information fraction
x <- gs_design_ahr(
enroll_rate = enroll_rate,
fail_rate = fail_rate,
alpha = 0.025,
beta = 0.1, # 90% power
ratio = 1,
info_frac = c(0.5, 0.75, 1), # Information fractions
analysis_time = 36, # Final analysis at 36 months
info_scale = "h0_info", # Use null info for spending
upper = gs_spending_bound,
upar = list(sf = sfLDOF, total_spend = 0.025),
lower = gs_spending_bound,
lpar = list(sf = sfHSD, total_spend = 0.1, param = -2),
binding = FALSE # Non-binding futility
)
x |> summary() |> as_gt()Design driven by analysis times (info_frac = NULL)
When info_frac = NULL, the information fraction is derived from enrollment/failure rate assumptions and analysis timing.
x <- gs_design_ahr(
enroll_rate = enroll_rate,
fail_rate = fail_rate_nph,
alpha = 0.025,
beta = 0.1,
info_frac = NULL, # Derived from analysis_time
analysis_time = c(18, 24, 30, 36), # Calendar months
info_scale = "h0_info",
upper = gs_spending_bound,
upar = list(sf = sfLDOF, total_spend = 0.025),
lower = gs_spending_bound,
lpar = list(sf = sfHSD, total_spend = 0.1, param = -2),
binding = FALSE
)One-sided design (no futility)
x <- gs_design_ahr(
enroll_rate = enroll_rate,
fail_rate = fail_rate,
alpha = 0.025,
beta = 0.1,
info_frac = c(0.5, 0.75, 1),
analysis_time = 36,
info_scale = "h0_info",
upper = gs_spending_bound,
upar = list(sf = sfLDOF, total_spend = 0.025),
test_lower = FALSE # No futility bound
)Spending functions as character strings
# Functions can be passed by name (string) instead of function reference
x <- gs_design_ahr(
enroll_rate = enroll_rate,
fail_rate = fail_rate,
alpha = 0.025, beta = 0.1,
info_frac = c(0.5, 0.75, 1),
analysis_time = 36,
upper = "gs_spending_bound",
upar = list(sf = "sfLDOF", total_spend = 0.025),
lower = "gs_spending_bound",
lpar = list(sf = "sfHSD", total_spend = 0.1, param = -2),
binding = FALSE
)Power computation with gs_power_ahr
gs_power_ahr() computes power for given enrollment/failure rates and timing. Does not solve for sample size.
pwr <- gs_power_ahr(
enroll_rate = enroll_rate,
fail_rate = fail_rate,
ratio = 1,
event = NULL, # IMPORTANT: must be NULL to use analysis_time
analysis_time = c(20, 28, 36),
info_scale = "h0_info",
upper = gs_spending_bound,
upar = list(sf = sfLDOF, total_spend = 0.025),
lower = gs_spending_bound,
lpar = list(sf = sfHSD, total_spend = 0.1, param = -2),
test_lower = TRUE,
binding = FALSE
)
pwr |> summary() |> as_gt()Important: event = NULL
gs_power_ahr() has a default event = c(30, 40, 50) which can cause length mismatches. Always set event = NULL when using analysis_time to drive the design.
Non-proportional hazards scenarios
Delayed treatment effect
fail_rate_delayed <- define_fail_rate(
duration = c(4, Inf),
fail_rate = log(2) / 10,
hr = c(1, 0.6), # No effect first 4 months, then HR = 0.6
dropout_rate = 0.001
)Crossing survival curves
fail_rate_crossing <- define_fail_rate(
duration = c(6, Inf),
fail_rate = log(2) / c(9, 12),
hr = c(1.2, 0.5), # Experimental worse early, better late
dropout_rate = 0.001
)Diminishing treatment effect
fail_rate_diminish <- define_fail_rate(
duration = c(6, Inf),
fail_rate = log(2) / 10,
hr = c(0.5, 0.8), # Strong early effect, weaker late
dropout_rate = 0.001
)Spending functions and bounds
Spending bound specification
# Efficacy: Lan-DeMets O'Brien-Fleming
upper = gs_spending_bound
upar = list(sf = sfLDOF, total_spend = 0.025)
# Futility: Hwang-Shih-DeCani with gamma = -2
lower = gs_spending_bound
lpar = list(sf = sfHSD, total_spend = 0.1, param = -2)
# Fixed bounds (not spending-based)
upper = gs_b
upar = c(3, 2.5, 2) # Fixed Z-value bounds
# No lower bound
test_lower = FALSECommon spending function choices
| Spending function | Parameter | Behavior |
|---|---|---|
sfLDOF |
None | Lan-DeMets O’Brien-Fleming (conservative early) |
sfHSD |
param = -4 |
Hwang-Shih-DeCani, O’Brien-Fleming-like |
sfHSD |
param = -2 |
Hwang-Shih-DeCani, moderate |
sfHSD |
param = 1 |
Hwang-Shih-DeCani, Pocock-like |
sfPoints |
param = c(...) |
Custom piecewise linear spending |
Spending time (decoupling spending from information fraction)
By default, alpha spending tracks the information fraction. For delayed treatment effects or multiple hypotheses, it may be desirable to use a different spending schedule. Use spending_time in upar/lpar.
# Spend more conservatively at interims using custom spending time
x <- gs_design_ahr(
enroll_rate = enroll_rate,
fail_rate = fail_rate_nph,
alpha = 0.025,
beta = 0.1,
info_frac = NULL,
analysis_time = c(18, 24, 36),
info_scale = "h0_info",
upper = gs_spending_bound,
upar = list(
sf = sfLDOF,
total_spend = 0.025,
timing = c(0.4, 0.65, 1) # Spend slower than information fraction
),
lower = gs_spending_bound,
lpar = list(
sf = sfHSD,
total_spend = 0.1,
param = -2,
timing = c(0.4, 0.65, 1)
),
binding = FALSE
)Rate difference designs
For binary endpoint designs using risk difference.
# Design for risk difference
x_rd <- gs_design_rd(
p_c = tibble::tibble(stratum = "All", rate = 0.15),
p_e = tibble::tibble(stratum = "All", rate = 0.30),
alpha = 0.025,
beta = 0.1,
ratio = 1,
info_frac = c(0.5, 1),
rd0 = 0,
upper = gs_spending_bound,
upar = list(sf = sfLDOF, total_spend = 0.025),
lower = gs_b,
lpar = rep(-Inf, 2)
)
# Power for rate difference
pwr_rd <- gs_power_rd(
p_c = tibble::tibble(stratum = "All", rate = 0.15),
p_e = tibble::tibble(stratum = "All", rate = 0.30),
n = 200,
ratio = 1,
info_frac = c(0.5, 1),
rd0 = 0,
upper = gs_spending_bound,
upar = list(sf = sfLDOF, total_spend = 0.025),
lower = gs_b,
lpar = rep(-Inf, 2)
)Weighted logrank and combination tests
Fleming-Harrington weighted logrank
x_wlr <- gs_design_wlr(
enroll_rate = enroll_rate,
fail_rate = fail_rate_nph,
alpha = 0.025,
beta = 0.1,
ratio = 1,
weight = function(x, arm0, arm1) {
wlr_weight_fh(x, arm0, arm1, rho = 0, gamma = 0.5)
},
info_frac = c(0.5, 0.75, 1),
analysis_time = 36,
upper = gs_spending_bound,
upar = list(sf = sfLDOF, total_spend = 0.025),
lower = gs_b,
lpar = rep(-Inf, 3)
)MaxCombo (combination of FH tests)
x_combo <- gs_design_combo(
enroll_rate = enroll_rate,
fail_rate = fail_rate_nph,
alpha = 0.025,
beta = 0.1,
ratio = 1,
fh_test = rbind(
data.frame(rho = 0, gamma = 0, tau = -1, test = 1, analysis = 1:3, analysis_time = 36),
data.frame(rho = 0, gamma = 0.5, tau = -1, test = 2, analysis = 1:3, analysis_time = 36)
),
info_frac = c(0.5, 0.75, 1),
upper = gs_spending_combo,
upar = list(sf = sfLDOF, total_spend = 0.025),
lower = gs_b,
lpar = rep(-Inf, 3)
)Integer sample size rounding
# Round to integers (events rounded at IA, rounded up at FA)
x_int <- x |> to_integer()
# With unequal randomization
x_int <- x |> to_integer(ratio = 2)Sequential p-values
sequential_pval() computes the minimum alpha at which an efficacy bound would be rejected. Used with graphical multiplicity methods (Maurer-Bretz).
# Design
x <- gs_design_ahr(
enroll_rate = define_enroll_rate(duration = c(2, 2, 2, 6), rate = c(2.5, 5, 7.5, 10)),
fail_rate = define_fail_rate(duration = Inf, fail_rate = log(2) / 6,
hr = 0.6, dropout_rate = .001),
info_frac = c(.5, .65, .8, 1),
analysis_time = 30,
upper = gs_spending_bound,
upar = list(sf = "sfLDOF", total_spend = 0.025),
lower = gs_spending_bound,
lpar = list(sf = "sfHSD", total_spend = 0.1, param = 2),
binding = FALSE
)
# Compute sequential p-value from observed data
sequential_pval(
gs_design = x,
event = c(100, 160, 190), # Observed events at each analysis
z = c(1.5, 2, 2.5) # Observed Z-statistics (positive = favorable)
)
# At interim (partial data)
sequential_pval(gs_design = x, event = c(100, 160), z = c(1.5, 2))
# With custom spending time
sequential_pval(
gs_design = x,
event = c(100, 160, 190, 230),
z = c(1.5, 2, 2.5, 3),
ustime = c(0.4, 0.6, 0.8, 1)
)Updating bounds at analysis (gs_update_ahr)
When observed events differ from planned, update bounds with gs_update_ahr().
library(gsDesign)
library(gsDesign2)
# Original design
x <- gs_design_ahr(
enroll_rate = define_enroll_rate(duration = c(2, 2, 10), rate = (1:3) / 3),
fail_rate = define_fail_rate(duration = c(3, Inf), fail_rate = log(2) / 9,
hr = c(1, 0.6), dropout_rate = .0001),
alpha = 0.025, beta = 0.1,
info_frac = NULL,
analysis_time = c(20, 36),
info_scale = "h0_info",
upper = gs_spending_bound,
upar = list(sf = sfLDOF, total_spend = 0.025),
test_lower = FALSE
)
# Update with observed events and spending time
x_upd <- gs_update_ahr(
x = x,
alpha = 0.025,
ustime = c(0.8, 1), # Spending time at observed analyses
event_tbl = data.frame(
analysis = c(1, 2),
event = c(180, 250) # Observed events
)
)Stratified update with piecewise events
# Piecewise event table (events per interval per analysis)
event_tbl <- data.frame(
analysis = c(1, 1, 2, 2),
event = c(30, 100, 30, 200) # 30 events in delayed period, rest after
)
x_upd <- gs_update_ahr(
x = x,
alpha = 0.025,
ustime = c(0.55, 1),
event_tbl = event_tbl
)Conditional power
# Conditional power under NPH
cp <- gs_cp_npe(
theta = x$analysis$theta,
info = x$analysis$info,
info0 = x$analysis$info0,
z = 2.0, # Observed Z at interim
i = 1, # Which interim analysis
x = x
)Output and reporting
# Summary table
x |> summary() |> as_gt()
# Bound summary (similar to gsDesign::gsBoundSummary)
x |> gs_bound_summary()
# Text summary
x |> text_summary()
# RTF output
x |> summary() |> as_rtf(file = "design.rtf")
# Suppress footnotes
x |> summary() |> as_gt(footnote = FALSE)
# Multiple alpha levels in bound summary
x |> gs_bound_summary(alpha = c(0.025, 0.0125))info_scale options
Controls how information is computed for bound calculations.
| info_scale | Description | When to use |
|---|---|---|
"h0_h1_info" |
Average of H0 and H1 info (default) | General use |
"h0_info" |
Null hypothesis info only | Matches gsDesign convention; recommended for multiplicity |
"h1_info" |
Alternative hypothesis info only | Rarely used |
# Match gsDesign convention
x <- gs_design_ahr(
...,
info_scale = "h0_info"
)Stratified designs
For overall population designs with subgroups, use stratified enrollment and failure rates.
# Subgroup prevalence
prevalence <- 0.4
# Subgroup enrollment from a driving design
enroll_rate_sub <- x_sub$enroll_rate # From gs_design_ahr output
# Build overall enrollment (BM+ and BM- strata)
enroll_rate_overall <- define_enroll_rate(
stratum = rep(c("BM+", "BM-"), each = nrow(enroll_rate_sub)),
duration = rep(enroll_rate_sub$duration, 2),
rate = c(
enroll_rate_sub$rate,
enroll_rate_sub$rate * (1 - prevalence) / prevalence
)
)
# Stratified failure rates
fail_rate_overall <- define_fail_rate(
stratum = c("BM+", "BM-"),
duration = c(Inf, Inf),
fail_rate = log(2) / 12,
hr = c(0.6, 0.85),
dropout_rate = 0.001
)
# Power for overall population
pwr_overall <- gs_power_ahr(
enroll_rate = enroll_rate_overall,
fail_rate = fail_rate_overall,
ratio = 1,
event = NULL,
analysis_time = c(20, 28, 36),
info_scale = "h0_info",
upper = gs_spending_bound,
upar = list(sf = sfLDOF, total_spend = 0.025),
test_lower = FALSE,
binding = FALSE
)