knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )
if (knitr::is_html_output()) { knitr::asis_output( "<style>table > caption { caption-side: top; caption-location: top; }</style>" ) }
library(gsDesign)
This vignette emphasizes numerical reproduction of SAS PROC SEQDESIGN
survival sample size outputs in gsDesign when design assumptions are matched.
It is primarily about translating a SAS design specification to gsSurv(),
not about post-design power sensitivity calculations. For an introductory
survival sample size workflow that is not tied to SAS output, see
vignette("gsSurvBasicExamples"). For the reverse question, where enrollment,
dropout, analysis timing, and hazard ratio assumptions are fixed and the goal
is to compute achieved power, see vignette("gsSurvPower").
We first reproduce the SAS fractional-time table to printed precision. We then show options that may lead to confusion when translating SAS PROC SEQDESIGN calls to gsDesign, including the event formula, alpha convention, and accrual and follow-up assumptions. The examples below touch three common survival design questions:
Consider the example described in the SAS Documentation:
Computing Sample Size for Survival Data with Uniform Accrual.
The first PROC SEQDESIGN call in that example does not specify ACCTIME; SAS
therefore derives a range of possible accrual times. The comparison below uses
the subsequent SAS call with ACCTIME=18, which fixes the maximum sample size
at 15 * 18 = 270 subjects and asks SAS to solve the follow-up time needed
for the target power. The SAS survival sample size model uses the Schoenfeld
formula for the targeted event counts. The computed sample size and event
counts at each analysis are continuous values, not rounded to integers.
proc seqdesign; /* Group sequential design procedure */ ErrorSpend: design /* Label for this design block */ nstages=4 /* Total analyses including final */ method=errfuncobf /* Lan-DeMets O'Brien-Fleming spending */ alt=twosided /* Two-sided alternative hypothesis */ stop=reject /* Early stop only for efficacy */ alpha=0.05 /* Total two-sided Type I error */ beta=0.10 /* Type II error (90% power) */ ; samplesize model= /* Survival model */ twosamplesurvival( /* Two-sample survival endpoint */ nullhazard = 0.03466 /* Control hazard under H0 */ hazard = 0.01733 /* Experimental hazard under H1 */ accrual = uniform /* Uniform enrollment */ accrate = 15 /* Subjects enrolled per time unit */ acctime = 18 /* Accrual duration in time units */ ); run;
SAS assumptions summary:
SAS does produce a sample size in this example. In the "Sample Size Summary"
table for the ACCTIME=18 case, it reports Max Sample Size = 270,
Follow-up Time = 7.133226, and Total Time = 25.13323. The maximum sample
size is not calculated from the event formula; it is implied directly by the
fixed accrual rate and accrual duration:
15 subjects/time unit * 18 time units = 270 subjects.
The fractional-time design reports analyses at information fractions 0.25, 0.50, 0.75, and 1.00. We focus on that design, where the analysis times are not rounded.
sas_fractional <- data.frame( Analysis = 1:4, Events = c(22.26962, 44.53924, 66.80886, 89.07847), Calendar_time = c(11.2631, 16.2875, 20.4926, 25.13323), N = c(168.95, 244.31, 270.00, 270.00), Upper_Z = c(4.33263, 2.96333, 2.35902, 2.01409) )
The rest of this vignette identifies the key assumptions in each system and
explains why alternative parameter translations can produce different results.
For additional background on the time-to-event methods in gsDesign, including
the default Lachin-Foulkes calculations, see vignette("SurvivalOverview").
There are three practical translation points that affect the output from the example above.
method = "Schoenfeld" to
match the SAS event formula. The default Lachin-Foulkes method is slightly
more conservative for this example, so matching the event formula matters for
numerical reproduction.gsDesign() and gsSurv()gsDesign() stores and spends the one-sided Type I error. Thus, for a
symmetric two-sided design (test.type = 2), alpha = 0.025 means 0.025 in
each tail, or 0.05 total two-sided Type I error.
gsSurv() follows the same convention internally. If a user prefers to enter
the total two-sided alpha, alpha = 0.05, sided = 2 is equivalent to
alpha = 0.025, sided = 1 for this example because gsSurv() passes
alpha / sided to gsDesign().
Here we use test.type = 2 and alpha = 0.025 to make the symmetric
two-sided gsDesign() object match the SAS two-sided total alpha of 0.05.
With ACCTIME=18, SAS fixes the accrual duration and total maximum sample size
and solves for the additional follow-up time. To match this in gsSurv(), set
both T = NULL and minfup = NULL. This tells gsSurv() to keep the input
accrual rate and accrual duration fixed, then solve the follow-up duration
needed for the final group sequential event requirement.
This is a common source of apparent disagreement. For a fixed-duration
survival design, T is the total study duration and minfup is the minimum
follow-up after enrollment closes. Specifying both can therefore change which
quantity gsSurv() solves for. The SAS comparison fixes accrual at 18 time
units and lets the total time be derived.
The translation used below is summarized as follows:
knitr::kable( data.frame( Quantity = c( "Two-sided Type I error", "Symmetric two-sided design", "Analysis timing input", "Event formula", "Accrual duration", "Total study duration", "Follow-up after accrual" ), SAS = c( "alpha = 0.05 total", "Early stop to reject either side", "Information fractions 0.25, 0.50, 0.75, 1.00", "Schoenfeld log-rank information", "ACCTIME = 18", "Total Time = 25.13323", "Derived as 7.133226" ), gsDesign = c( "alpha = 0.025 per tail", "test.type = 2", "gsSurv(timing = c(.25, .50, .75, 1))", "method = \"Schoenfeld\"", "R = 18", "T = NULL", "minfup = NULL" ), Reason = c( "gsDesign stores and spends one-sided alpha", "Mirrors the upper and lower efficacy boundaries", "Uses the SAS fractional information schedule", "Avoids Lachin-Foulkes default event calculation", "Keeps the same fixed accrual duration", "Lets gsSurv() solve total time from fixed accrual", "Lets gsSurv() solve the follow-up duration" ), check.names = FALSE ), caption = "Translation from the SAS PROC SEQDESIGN example to gsDesign inputs." )
gsSurv()gsSurv() with aligned parametersLet's start our work with gsDesign by defining parameters to match the SAS fractional-time design:
k <- 4 alpha_sas <- 0.05 # Two-sided total alpha (SAS convention) alpha_gsdesign <- alpha_sas / 2 # gsDesign uses one-sided alpha beta <- 0.10 # 1 - power = 0.10 -> 90% power lambdaC <- 0.03466 # Control hazard rate lambdaE <- 0.01733 # Experimental hazard rate HR <- lambdaE / lambdaC # = 0.5 timing <- c(0.25, 0.50, 0.75, 1.00) # Equally spaced information fractions accrate <- 15 # Uniform accrual rate (subjects per time unit) accrual_duration <- 18 # Accrual duration (time units) sas_total_time <- 25.13323 sas_followup_time <- sas_total_time - accrual_duration N <- accrate * accrual_duration
Instead of starting with a call to gsDesign::gsDesign(), we begin with
gsDesign::gsSurv(). The gsSurv() function combines nSurv() with
gsDesign() (group sequential boundaries) in one call.
It is the standard gsDesign function for designing time-to-event trials.
The call below uses the same two-sided symmetric structure as SAS:
test.type = 2 for symmetric two-sided boundaries;alpha = 0.025, the one-sided alpha corresponding to SAS's total
two-sided alpha of 0.05;method = "Schoenfeld" for the SAS event formula;T = NULL and minfup = NULL so the accrual rate and accrual duration
remain fixed while gsSurv() solves the follow-up duration.des_2 <- gsSurv( k = 4, test.type = 2, # Symmetric two-sided design alpha = alpha_gsdesign, # One-sided alpha; SAS total alpha is 2 * this beta = 0.10, sfu = sfLDOF, timing = c(.25, .50, .75, 1), lambdaC = 0.03466, hr = 0.5, eta = 0, # Assume no dropout gamma = accrate, R = accrual_duration, T = NULL, minfup = NULL, ratio = 1, method = "Schoenfeld" ) des_2
Observations:
Using gsSurv() with parameters that align with SAS settings, the event
counts, sample size, and analysis times reproduce the SAS fractional-time
output to the printed precision available. The final follow-up duration is
r round(des_2$minfup, 5), giving total study duration
r round(des_2$T[des_2$k], 5). Small fifth-decimal differences in Z-boundary
comparisons may reflect limited SAS printed precision unless the SAS output is
rerun with more digits.
gs_fractional <- data.frame( Analysis = 1:k, Events_SAS = sas_fractional$Events, Events_gsDesign = des_2$n.I, Time_SAS = sas_fractional$Calendar_time, Time_gsDesign = des_2$T, N_SAS = sas_fractional$N, N_gsDesign = rowSums(des_2$eNC) + rowSums(des_2$eNE), Z_SAS = sas_fractional$Upper_Z, Z_gsDesign = des_2$upper$bound ) knitr::kable( round(gs_fractional, 5), caption = "Fractional-time SAS output compared with gsSurv()." )
The final event count is 89.07847 in both systems. The print() and
gsBoundSummary() methods display rounded integer sample sizes and events,
but the object retains the fractional values shown above.
If the follow-up time is fixed instead, gsSurv() can answer a different
sample size question: keeping enrollment rates and minimum follow-up fixed,
how long must enrollment continue to power the trial? The translation is
T = NULL with minfup set to the fixed follow-up time. In this SAS example,
the solution returns the same accrual duration because the fixed follow-up time
is the one SAS solved from the fixed-accrual design:
des_fixed_followup <- gsSurv( k = 4, test.type = 2, alpha = alpha_gsdesign, beta = 0.10, sfu = sfLDOF, timing = c(.25, .50, .75, 1), lambdaC = 0.03466, hr = 0.5, eta = 0, gamma = accrate, R = accrual_duration, T = NULL, minfup = sas_followup_time, ratio = 1, method = "Schoenfeld" ) fixed_followup_check <- data.frame( Quantity = c( "Final study duration", "Accrual duration", "Final events", "Final N" ), Solved_followup = c( des_2$T[k], sum(des_2$R), des_2$n.I[k], sum(des_2$eNC[k, ] + des_2$eNE[k, ]) ), Specified_followup = c( des_fixed_followup$T[k], sum(des_fixed_followup$R), des_fixed_followup$n.I[k], sum(des_fixed_followup$eNC[k, ] + des_fixed_followup$eNE[k, ]) ) ) fixed_followup_check[-1] <- lapply(fixed_followup_check[-1], round, digits = 5) knitr::kable( fixed_followup_check, caption = "Both fixed-accrual translations produce the same final design." )
A third use case is to keep both the enrollment rates and enrollment duration
fixed, then vary minimum follow-up and evaluate power. That is useful for
sensitivity analysis, but it is not always a stable sample size workflow: for
some assumptions the trial remains overpowered or underpowered for all
plausible follow-up durations. Use gsSurvPower() when the design quantities
are fixed and the question is achieved power.
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.