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:
| Quantity | SAS | gsDesign | Reason |
|---|---|---|---|
| Two-sided Type I error | alpha = 0.05 total | alpha = 0.025 per tail | gsDesign stores and spends one-sided alpha |
| Symmetric two-sided design | Early stop to reject either side | test.type = 2 | Mirrors the upper and lower efficacy boundaries |
| Analysis timing input | Information fractions 0.25, 0.50, 0.75, 1.00 | gsSurv(timing = c(.25, .50, .75, 1)) | Uses the SAS fractional information schedule |
| Event formula | Schoenfeld log-rank information | method = “Schoenfeld” | Avoids Lachin-Foulkes default event calculation |
| Accrual duration | ACCTIME = 18 | R = 18 | Keeps the same fixed accrual duration |
| Total study duration | Total Time = 25.13323 | T = NULL | Lets gsSurv() solve total time from fixed accrual |
| Follow-up after accrual | Derived as 7.133226 | minfup = NULL | Lets gsSurv() solve the follow-up duration |
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_durationInstead 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
#> Group sequential design (method=Schoenfeld; k=4 analyses; Two-sided symmetric)
#> HR=0.500 vs HR0=1.000 | alpha=0.025 (sided=2) | power=90.0%
#> N=270.0 subjects | D=89.1 events | T=25.1 study duration | accrual=18.0 Accrual duration | minfup=7.1 minimum follow-up | ratio=1 randomization ratio (experimental/control)
#>
#> Spending functions:
#> Bounds derived using a Lan-DeMets O'Brien-Fleming approximation spending function (no parameters).
#>
#> Analysis summary:
#> Method: Schoenfeld
#> Analysis Value Efficacy Futility
#> IA 1: 25% Z 4.3326 -4.3326
#> N: 170 p (1-sided) 0.0000 0.0000
#> Events: 23 ~HR at bound 0.1594 6.2728
#> Month: 11 P(Cross) if HR=1 0.0000 0.0000
#> P(Cross) if HR=0.5 0.0035 0.0000
#> IA 2: 50% Z 2.9631 -2.9631
#> N: 246 p (1-sided) 0.0015 0.0015
#> Events: 45 ~HR at bound 0.4115 2.4302
#> Month: 16 P(Cross) if HR=1 0.0015 0.0015
#> P(Cross) if HR=0.5 0.2579 0.0000
#> IA 3: 75% Z 2.3590 -2.3590
#> N: 272 p (1-sided) 0.0092 0.0092
#> Events: 67 ~HR at bound 0.5615 1.7811
#> Month: 20 P(Cross) if HR=1 0.0096 0.0096
#> P(Cross) if HR=0.5 0.6853 0.0000
#> Final Z 2.0141 -2.0141
#> N: 272 p (1-sided) 0.0220 0.0220
#> Events: 90 ~HR at bound 0.6526 1.5323
#> Month: 25 P(Cross) if HR=1 0.0250 0.0250
#> P(Cross) if HR=0.5 0.9000 0.0000
#>
#> Key inputs (names preserved):
#> desc item value input
#> Accrual rate(s) gamma 15 15
#> Accrual rate duration(s) R 18 18
#> Control hazard rate(s) lambdaC 0.035 0.035
#> Control dropout rate(s) eta 0 0
#> Experimental dropout rate(s) etaE 0 etaE
#> Event and dropout rate duration(s) S NULL SObservations:
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 7.13321, giving total study duration
25.13321. 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()."
)| Analysis | Events_SAS | Events_gsDesign | Time_SAS | Time_gsDesign | N_SAS | N_gsDesign | Z_SAS | Z_gsDesign |
|---|---|---|---|---|---|---|---|---|
| 1 | 22.26962 | 22.2696 | 11.26310 | 11.26306 | 168.95 | 168.9459 | 4.33263 | 4.33263 |
| 2 | 44.53924 | 44.5392 | 16.28750 | 16.28746 | 244.31 | 244.3119 | 2.96333 | 2.96313 |
| 3 | 66.80886 | 66.8088 | 20.49260 | 20.49260 | 270.00 | 270.0000 | 2.35902 | 2.35904 |
| 4 | 89.07847 | 89.0784 | 25.13323 | 25.13321 | 270.00 | 270.0000 | 2.01409 | 2.01409 |
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."
)| Quantity | Solved_followup | Specified_followup |
|---|---|---|
| Final study duration | 25.13321 | 25.13322 |
| Accrual duration | 18.00000 | 17.99999 |
| Final events | 89.07840 | 89.07840 |
| Final N | 270.00003 | 269.99988 |
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.