16 Validating gsSurv results with rpact
The rpact package (Wassmer and Pahlke 2025) provides an independent implementation of group sequential design methods for clinical trials. Cross-validating survival design results between gsDesign and rpact builds confidence that designs are correctly specified.
The Lachin and Foulkes method used by default in gsDesign is unique to the gsDesign package. However, the commonly used Schoenfeld method and the sometimes used Freedman method are available in both packages, enabling direct comparison. In this chapter we compare fixed sample size and event count calculations, then proceed to group sequential design boundaries, sample sizes, and event counts.
16.1 Fixed design comparisons
16.1.1 Schoenfeld method
We consider a two-arm superiority trial with:
- Median control survival: 12 months (\(\lambda_C = \log(2)/12\)).
- Hazard ratio: HR = 0.7.
- One-sided \(\alpha\): 0.025, Power: 90%.
- Enrollment: Uniform over 18 months.
- Minimum follow-up: 12 months (total study duration 30 months).
- Dropout rate: 5% per year in each arm.
We compute the required number of events using the Schoenfeld method in both packages. In rpact, typeOfComputation = "Schoenfeld" is the default for getSampleSizeSurvival().
# --- gsDesign: Schoenfeld method ---
gs_fixed <- nSurv(
lambdaC = log(2) / 12,
hr = 0.7,
hr0 = 1,
eta = -log(0.95) / 12,
alpha = 0.025,
beta = 0.1,
T = 30,
minfup = 12,
method = "Schoenfeld"
)
# --- rpact: Schoenfeld method (default) ---
rp_fixed <- getSampleSizeSurvival(
hazardRatio = 0.7,
lambda2 = log(2) / 12,
alpha = 0.025,
beta = 0.1,
sided = 1,
accrualTime = c(0, 18),
accrualIntensity = 0.1,
followUpTime = 12,
dropoutRate1 = 0.05,
dropoutRate2 = 0.05,
dropoutTime = 12,
typeOfComputation = "Schoenfeld"
)
cat(
"Schoenfeld method event comparison:",
"\n gsDesign events: ", ceiling(gs_fixed$d),
"\n rpact events: ", ceiling(rp_fixed$maxNumberOfEvents),
"\n"
)
#> Schoenfeld method event comparison:
#> gsDesign events: 331
#> rpact events: 331The Schoenfeld approximation yields the number of events directly from the formula
\[ d = \frac{(Z_{1-\alpha} + Z_{1-\beta})^2}{\xi(1-\xi)(\log \text{HR})^2} \]
where \(\xi\) is the proportion randomized to the experimental group. This formula depends only on the hazard ratio, Type I error, power, and allocation ratio — not on enrollment or follow-up patterns. Both packages should produce matching event counts.
The sample size (number of subjects to enroll) and study duration differ because they depend on how enrollment and follow-up are modeled. The Lachin and Foulkes method in gsDesign jointly solves for both events and sample size, while the Schoenfeld method in rpact and gsDesign first determines events, then calculates the sample size needed to produce those events within the specified enrollment and follow-up period.
16.1.2 Freedman method
The Freedman method provides a slightly different approximation. We compare:
# --- gsDesign: Freedman method ---
gs_freedman <- nSurv(
lambdaC = log(2) / 12,
hr = 0.7,
hr0 = 1,
eta = -log(0.95) / 12,
alpha = 0.025,
beta = 0.1,
T = 30,
minfup = 12,
method = "Freedman"
)
# --- rpact: Freedman method ---
rp_freedman <- getSampleSizeSurvival(
hazardRatio = 0.7,
lambda2 = log(2) / 12,
alpha = 0.025,
beta = 0.1,
sided = 1,
accrualTime = c(0, 18),
accrualIntensity = 0.1,
followUpTime = 12,
dropoutRate1 = 0.05,
dropoutRate2 = 0.05,
dropoutTime = 12,
typeOfComputation = "Freedman"
)
cat(
"Freedman method event comparison:",
"\n gsDesign events: ", ceiling(gs_freedman$d),
"\n rpact events: ", ceiling(rp_freedman$maxNumberOfEvents),
"\n"
)
#> Freedman method event comparison:
#> gsDesign events: 338
#> rpact events: 33816.1.3 Lachin-Foulkes comparison
For completeness, we note that the Lachin and Foulkes method is available only in gsDesign. However, we can compare the gsDesign Lachin-Foulkes result with the rpact Schoenfeld result to understand the difference:
gs_lf <- nSurv(
lambdaC = log(2) / 12,
hr = 0.7,
hr0 = 1,
eta = -log(0.95) / 12,
alpha = 0.025,
beta = 0.1,
T = 30,
minfup = 12
)
comparison <- data.frame(
Method = c("Lachin-Foulkes (gsDesign)", "Schoenfeld (gsDesign)",
"Freedman (gsDesign)", "Schoenfeld (rpact)", "Freedman (rpact)"),
Events = c(
ceiling(gs_lf$d), ceiling(gs_fixed$d), ceiling(gs_freedman$d),
ceiling(rp_fixed$maxNumberOfEvents), ceiling(rp_freedman$maxNumberOfEvents)
),
`Sample size` = c(
ceiling(gs_lf$n), ceiling(gs_fixed$n), ceiling(gs_freedman$n),
ceiling(rp_fixed$maxNumberOfSubjects), ceiling(rp_freedman$maxNumberOfSubjects)
),
check.names = FALSE
)
knitr::kable(comparison,
caption = "Fixed design comparison across methods and packages"
)| Method | Events | Sample size |
|---|---|---|
| Lachin-Foulkes (gsDesign) | 330 | 548 |
| Schoenfeld (gsDesign) | 331 | 548 |
| Freedman (gsDesign) | 338 | 560 |
| Schoenfeld (rpact) | 331 | 548 |
| Freedman (rpact) | 338 | 560 |
The Lachin and Foulkes method differs from Schoenfeld and Freedman in that it accounts for the variance of the log hazard ratio under both the null and alternative hypotheses separately, producing a slightly different event count. The Schoenfeld and Freedman approximations use simpler variance formulas that yield event counts independent of enrollment patterns.
16.2 Group sequential design comparisons
For group sequential designs, we compare boundaries, event counts, and sample sizes. The mapping of spending functions between packages is:
| gsDesign | rpact | Description |
|---|---|---|
sfu = sfLDOF |
typeOfDesign = "asOF" |
Lan-DeMets O’Brien-Fleming \(\alpha\)-spending |
sfu = sfLDPocock |
typeOfDesign = "asP" |
Lan-DeMets Pocock \(\alpha\)-spending |
sfu = sfHSD, sfupar = γ |
typeOfDesign = "asHSD", gammaA = γ |
Hwang-Shih-DeCani \(\alpha\)-spending |
sfu = sfPower, sfupar = ρ |
typeOfDesign = "asKD", gammaA = ρ |
Kim-DeMets (power) \(\alpha\)-spending |
sfu = "OF" |
typeOfDesign = "OF" |
O’Brien-Fleming bounds |
sfu = "Pocock" |
typeOfDesign = "P" |
Pocock bounds |
sfl = sfHSD, sflpar = γ |
typeBetaSpending = "bsHSD", gammaB = γ |
HSD \(\beta\)-spending |
sfl = sfLDOF |
typeBetaSpending = "bsOF" |
Lan-DeMets OF \(\beta\)-spending |
16.2.1 One-sided efficacy design
We start with the simplest case: a one-sided design with only an efficacy bound using Lan-DeMets O’Brien-Fleming spending.
# --- gsDesign: one-sided, 3-analysis ---
gs1 <- gsDesign(
k = 3,
test.type = 1,
alpha = 0.025,
beta = 0.1,
sfu = sfLDOF
)
# --- rpact: equivalent one-sided design ---
rp1 <- getDesignGroupSequential(
kMax = 3,
alpha = 0.025,
beta = 0.1,
sided = 1,
typeOfDesign = "asOF"
)
cat("Efficacy bounds comparison (Z-scale):\n")
#> Efficacy bounds comparison (Z-scale):
bounds_compare <- data.frame(
Analysis = 1:3,
gsDesign = round(gs1$upper$bound, 4),
rpact = round(rp1$criticalValues, 4),
Difference = round(gs1$upper$bound - rp1$criticalValues, 6)
)
knitr::kable(bounds_compare, caption = "One-sided efficacy bounds")| Analysis | gsDesign | rpact | Difference |
|---|---|---|---|
| 1 | 3.7103 | 3.7103 | 0 |
| 2 | 2.5114 | 2.5114 | 0 |
| 3 | 1.9930 | 1.9930 | 0 |
16.2.2 Design with non-binding futility
Next we compare a design with non-binding futility bounds using Hwang-Shih-DeCani spending for both bounds.
# --- gsDesign: non-binding futility (test.type = 4) ---
gs4 <- gsDesign(
k = 3,
test.type = 4,
alpha = 0.025,
beta = 0.1,
sfu = sfHSD, sfupar = -4,
sfl = sfHSD, sflpar = -2
)
# --- rpact: non-binding futility ---
rp4 <- getDesignGroupSequential(
kMax = 3,
alpha = 0.025,
beta = 0.1,
sided = 1,
typeOfDesign = "asHSD", gammaA = -4,
typeBetaSpending = "bsHSD", gammaB = -2,
bindingFutility = FALSE
)
cat("Efficacy bounds:\n")
#> Efficacy bounds:
eff_compare <- data.frame(
Analysis = 1:3,
gsDesign = round(gs4$upper$bound, 4),
rpact = round(rp4$criticalValues, 4),
Difference = round(gs4$upper$bound - rp4$criticalValues, 6)
)
knitr::kable(eff_compare, caption = "Non-binding design: efficacy bounds")| Analysis | gsDesign | rpact | Difference |
|---|---|---|---|
| 1 | 3.0107 | 3.0107 | 0 |
| 2 | 2.5465 | 2.5465 | 0 |
| 3 | 1.9992 | 1.9992 | 0 |
cat("\nFutility bounds (interim analyses only):\n")
#>
#> Futility bounds (interim analyses only):
# rpact futilityBounds has k-1 elements (interims only)
fut_compare <- data.frame(
Analysis = 1:2,
gsDesign = round(gs4$lower$bound[1:2], 4),
rpact = round(rp4$futilityBounds, 4),
Difference = round(gs4$lower$bound[1:2] - rp4$futilityBounds, 6)
)
knitr::kable(fut_compare, caption = "Non-binding design: futility bounds")| Analysis | gsDesign | rpact | Difference |
|---|---|---|---|
| 1 | -0.2387 | -0.2387 | 0 |
| 2 | 0.9411 | 0.9411 | 0 |
16.2.3 Survival group sequential design
Now we combine the group sequential boundaries with survival sample size calculations. We use the Schoenfeld method in both packages for comparability.
# --- gsDesign: Survival GSD with Schoenfeld method ---
gs_surv <- gsSurv(
k = 3,
test.type = 4,
alpha = 0.025,
beta = 0.1,
sfu = sfHSD, sfupar = -4,
sfl = sfHSD, sflpar = -2,
lambdaC = log(2) / 12,
hr = 0.7,
eta = -log(0.95) / 12,
gamma = 1,
R = 18,
T = 30,
minfup = 12,
method = "Schoenfeld"
)
# --- rpact: Survival GSD ---
rp_design <- getDesignGroupSequential(
kMax = 3,
alpha = 0.025,
beta = 0.1,
sided = 1,
typeOfDesign = "asHSD", gammaA = -4,
typeBetaSpending = "bsHSD", gammaB = -2,
bindingFutility = FALSE
)
rp_surv <- getSampleSizeSurvival(
design = rp_design,
hazardRatio = 0.7,
lambda2 = log(2) / 12,
accrualTime = c(0, 18),
accrualIntensity = 0.1,
followUpTime = 12,
dropoutRate1 = 0.05,
dropoutRate2 = 0.05,
dropoutTime = 12,
typeOfComputation = "Schoenfeld"
)
cat("Efficacy bounds (Z-scale):\n")
#> Efficacy bounds (Z-scale):
cat(" gsDesign:", round(gs_surv$upper$bound, 4), "\n")
#> gsDesign: 3.0107 2.5465 1.9992
cat(" rpact: ", round(rp_design$criticalValues, 4), "\n")
#> rpact: 3.0107 2.5465 1.9992
cat("\nFutility bounds (Z-scale, interims only):\n")
#>
#> Futility bounds (Z-scale, interims only):
cat(" gsDesign:", round(gs_surv$lower$bound[1:2], 4), "\n")
#> gsDesign: -0.2388 0.941
cat(" rpact: ", round(rp_design$futilityBounds, 4), "\n")
#> rpact: -0.2387 0.9411
cat("\nCumulative events at each analysis:\n")
#>
#> Cumulative events at each analysis:
cat(" gsDesign:", ceiling(gs_surv$n.I), "\n")
#> gsDesign: 118 236 354
cat(" rpact: ", ceiling(rp_surv$cumulativeEventsPerStage), "\n")
#> rpact: 118 236 354The boundaries should match closely since both packages implement the same spending function algorithms. Differences in event counts and sample sizes may arise from:
- Different methods for computing expected events given enrollment and follow-up assumptions.
- Different handling of dropout rates (annual vs. monthly parameterization).
- Different numerical algorithms for solving the sample size equation.
16.2.4 O’Brien-Fleming and Pocock bounds
For Wang-Tsiatis boundary families (O’Brien-Fleming and Pocock), both packages implement the exact boundaries rather than spending function approximations.
# O'Brien-Fleming bounds
gs_of <- gsDesign(k = 4, test.type = 1, alpha = 0.025, sfu = "OF")
rp_of <- getDesignGroupSequential(
kMax = 4, alpha = 0.025, sided = 1, typeOfDesign = "OF"
)
# Pocock bounds
gs_p <- gsDesign(k = 4, test.type = 1, alpha = 0.025, sfu = "Pocock")
rp_p <- getDesignGroupSequential(
kMax = 4, alpha = 0.025, sided = 1, typeOfDesign = "P"
)
cat("O'Brien-Fleming bounds:\n")
#> O'Brien-Fleming bounds:
cat(" gsDesign:", round(gs_of$upper$bound, 4), "\n")
#> gsDesign: 4.0486 2.8628 2.3375 2.0243
cat(" rpact: ", round(rp_of$criticalValues, 4), "\n")
#> rpact: 4.0486 2.8628 2.3375 2.0243
cat("\nPocock bounds:\n")
#>
#> Pocock bounds:
cat(" gsDesign:", round(gs_p$upper$bound, 4), "\n")
#> gsDesign: 2.3613 2.3613 2.3613 2.3613
cat(" rpact: ", round(rp_p$criticalValues, 4), "\n")
#> rpact: 2.3613 2.3613 2.3613 2.361316.2.5 Sample size inflation factor
Both packages report how much the maximum sample size (or events) increases relative to a fixed design. We compare the inflation factor:
16.3 Notes on parameter mapping
When comparing results between gsDesign and rpact, the following parameter mappings should be kept in mind:
16.3.1 Dropout rates
gsDesign uses an instantaneous hazard rate for dropout (\(\eta\)), while rpact uses a proportion of dropouts over a specified time period (dropoutRate, dropoutTime). The conversion is:
\[ \text{dropoutRate (rpact)} = 1 - e^{-\eta \times \text{dropoutTime}} \]
For a 5% annual dropout rate with dropoutTime = 12 months:
16.3.2 Enrollment specification
gsDesign uses gamma (enrollment rates per time unit) and R (enrollment duration vector), while rpact uses accrualTime (time interval endpoints starting from 0) and accrualIntensity (rate per time unit or relative). For uniform enrollment at rate \(\gamma\) over \(R\) months:
-
gsDesign:
gamma = γ, R = R -
rpact:
accrualTime = c(0, R), accrualIntensity = γ(absolute) oraccrualIntensity = 0.1(relative, rpact scales to match)
16.3.3 Spending function parameters
Hwang-Shih-DeCani parameters have the same sign convention in both packages: negative \(\gamma\) produces conservative (O’Brien-Fleming-like) spending, positive \(\gamma\) produces aggressive (Pocock-like) spending.
16.3.4 Test sidedness
gsDesign uses test.type to control sidedness and bound structure, while rpact uses sided (1 or 2) combined with bindingFutility and typeBetaSpending. A gsDesign test.type = 4 (non-binding futility) corresponds to rpact sided = 1, bindingFutility = FALSE with a beta spending function specified.