
Calibrate Futility Spending to Conditional Power Targets
Source:R/gsCPFutilitySpending.R
gsCPFutilitySpending.RdgsCPFutilitySpending() selects parameters for a beta-spending futility
boundary so that conditional power at one or more interim futility bounds
matches specified targets. Each candidate design is reconstructed with
gsDesign(), which recalculates the statistical information required to
retain the reference design's unconditional power.
Arguments
- x
A
gsDesign,gsSurv,gsSurvCalendar, orgsSurvPowerdesign withtest.type3, 4, 7, or 8. The analysis information fractions inx$timingare held fixed. These fractions are distinct from the times supplied to the spending functions; see Information fractions and spending times below.- target_cp
Numeric vector of conditional power targets strictly between zero and one, one per interim index in
i. Each target is the probability of crossing any future efficacy bound, from analysisi + 1through the final analysisx$k, conditional on the statistic at analysisiequaling its candidate futility bound. Withtheta = NULL, the future effect is the observed effect implied by that bound,lower$bound[i] / sqrt(n.I[i]); otherwisethetaspecifies the future effect. Future efficacy and futility boundaries remain in force. With multiple targets, each is a total future conditional power at its corresponding interim, not a next-analysis conditional power.- i
Interim analysis indices corresponding to
target_cp. Values must identify active futility bounds, be unique, and be in1:(x$k - 1). Results are ordered by analysis.- sfl
Futility spending function, supplied as a supported function or its character name. By default, use
x$lower$sf, the reference design's futility spending function. Supplysflto override it.- theta
Optional future effect for conditional power. A scalar is recycled; otherwise its length must equal
target_cp. WhenNULL, the observed effect at each candidate lower bound is used.- control
Optional named list of numerical solver settings. Unspecified components retain their defaults; ordinary use does not require this argument. These settings affect the search, not the effect assumption or CP target. Unknown names and invalid values produce input errors.
startInitial spending parameters (default
NULL). When omitted, use the reference design's parameters if its spending function and parameter count match; otherwise use the family defaults below. Custom spending functions require an explicit starting vector.lower,upperSearch limits for the spending parameters, not Z-boundaries (both default
NULL, selecting the family defaults below). Supplied start and limit vectors must be finite numeric vectors matching the number of free parameters. Limits must satisfylower < upperand contain the starting values.cp_tolMaximum absolute difference between achieved and targeted CP at every selected analysis (default
1e-4). Must be a finite scalar strictly between 0 and 0.1.maxitPositive integer iteration limit for the joint optimizer (default 500), not a global limit on design evaluations or the one-parameter root search.
reltolPositive finite scalar controlling internal numerical convergence (default
1e-10). This does not replace the finalcp_tolacceptance check.backwardAllow a latest-to-earliest coordinate fallback if the initial joint fit misses the targets (default
TRUE).traceDisplay joint-optimizer progress (default
FALSE). Bothbackwardandtracemust be nonmissing scalar logical values.
Value
A calibrated design inheriting from gsCPFutilitySpending and
gsDesign, retaining gsSurv and gsSurvPower when applicable.
The cpFutilitySpending component contains targets, achieved conditional
powers, effects, fitted spending metadata, information, reference efficacy
and harm specifications, and solver diagnostics.
Invalid inputs raise a gsCPFutilitySpending_input_error. A well-formed
target that cannot be attained raises a
gsCPFutilitySpending_infeasible_error; optimizer failure raises a
gsCPFutilitySpending_convergence_error.
Details
Conditional power is the probability of crossing any efficacy bound after
the selected interim, through the final analysis, conditional on the interim
statistic equaling the candidate lower bound. When theta
is NULL, the future effect is the observed effect implied by that bound,
lower$bound[i] / sqrt(n.I[i]), following the default convention in
gsCP(). The calculation conditions on the interim statistic even though it
is at a stopping boundary; future futility bounds remain part of the
conditional power calculation.
For a three-analysis design, target_cp = .3, i = 1 targets probability
0.3 of efficacy at analysis 2 or 3, conditional on \(Z_1 = a_1\), where
\(a_1\) is the fitted futility bound. With a two-parameter spending family,
target_cp = c(.3, .5), i = c(1, 2) additionally targets probability
0.5 of efficacy at analysis 3 conditional on \(Z_2 = a_2\). Each target
uses its own interim conditioning value and, when theta = NULL, its
own bound-implied future effect. These are total future conditional powers,
not probabilities of efficacy only at the next analysis or probabilities
that are added together across targeted interims.
One-target calibration supports the one-parameter families sfHSD,
sfPower, sfExponential, and sfLDOF. Two-target calibration supports
sfLogistic, sfBetaDist, sfCauchy, sfNormal, sfExtremeValue, and
sfExtremeValue2. sfLinear may be used with any number of targets and is
the intended choice for more than two. Its knot times are fixed at the lower
spending times for the targeted analyses; the fitted cumulative spending
proportions are constrained to be strictly increasing and between zero and
one.
With multiple targets, joint constrained optimization first uses the
reference or supplied starting parameters. If needed, a latest-to-earliest
coordinate solve supplies alternative starting values for another joint fit.
A result is returned only when every conditional power residual is within
control$cp_tol. Searches may stop early once all residuals are at most
min(control$cp_tol / 10, 1e-7); computations retain double precision.
The fitted lower spending parameters depend on the complete design,
including efficacy spending. For test.type 7 and 8 they may also depend on
harm spending. Changing any of those specifications requires recalibration.
toInteger() carries the fitted spending function and parameters forward,
but rounding information can change the achieved conditional power and does
not trigger recalibration.
Information fractions and spending times
All six spending calibrators keep the reference analysis information
fractions x$timing fixed. These are the cumulative information
fractions x$n.I / x$n.I[x$k], ending at 1. For example,
x$timing = c(.5, .75, 1) keeps the analyses at 50%, 75%, and 100%
of the final information. Power-preserving calibration may change the
maximum information and thus the absolute information n.I at every
analysis while retaining these fractions. Fixed-information calibration
(gsCAFutilitySpending() and
gsCPOSFutilitySpending(mode = "fixed_information")) also holds
n.I and the efficacy boundaries fixed.
Spending times are the inputs to the spending functions, stored in
x$upper$sTime and x$lower$sTime. These are also retained during
calibration but may differ from the information fractions; for example,
calendar-based spending uses fractions of calendar time. Thus fixed
information fractions do not mean that spending must use information time,
or that absolute calendar analysis dates must be fixed. See
Survival designs for how survival calendar times are handled.
Spending-parameter search defaults
When not supplied in control, parameter limits and fallback starting
values are selected by family:
| Family | Start | Lower | Upper |
sfHSD | -2 | -40 | 40 |
sfPower | 1 | 1e-4 | 50 |
sfExponential | 0.5 | 1e-4 | 1.5 |
sfLDOF | 1 | 0.005 | 20 |
sfBetaDist | c(1, 1) | c(1e-3, 1e-3) | c(50, 50) |
| Other supported two-parameter families | c(0, 1) | c(-20, 1e-3) | c(20, 50) |
| Custom function | Required | -20 per parameter | 20 per parameter |
For sfLinear, start instead contains one cumulative spending
proportion per target, strictly increasing and in (0, 1). When omitted,
starting proportions are derived from the reference design's cumulative
lower spending divided by beta and adjusted to satisfy these constraints.
User-supplied lower and upper are not supported for its
constrained parameterization.
Survival designs
Survival inputs retain their survival classes and endpoint assumptions.
Candidate probabilities are evaluated on the statistical event-count scale.
Power-preserving probability calibration of fixed-duration, rate-scaled
designs rebuilds the survival plan only for the selected fit and checks all
targets again on the returned object. If the deferred search or that check
fails, calibration retries once with full survival reconstruction, retaining
the best available internal parameters as starting values. Effect calibration
and accrual- or follow-up-duration solves reconstruct the plan for every
candidate.
For gsSurv() and gsSurvCalendar() inputs, information fractions,
spending times, and the enrollment/follow-up constraint are retained;
enrollment rates or durations are recalculated as required. Calendar designs
with fixed enrollment and follow-up retain their calendar schedule up to
numerical tolerance. Stored calls are not evaluated.
For gsSurvPower() inputs, power-preserving calibration fixes the
realized calendar times and enrollment periods and rescales enrollment rates
to attain the fitted event counts. The evaluated alternative x$hr
and its achieved power are used, even if the original design alternative
x$hr1 differed. Original event-trigger and calendar-cap rules are not
re-applied: the realized schedule becomes the new plan. Fixed-information
conditional-POS/CA calibration instead retains the survival plan, event
counts and efficacy bounds while updating futility and achieved power.
Priors and explicit theta remain standardized drifts per square root
event, not hazard ratios. Rounding with toInteger() after calibration
can change the target; calibration of an already rounded reference may
return noninteger event counts. The final analysis is not a valid target
index for interim calibration: i identifies the interim bound or
continuation event at which the target is evaluated.
Examples
# \donttest{
# Lan-DeMets O'Brien-Fleming efficacy spending with futility only at IA 1.
x <- gsDesign(
k = 3, test.type = 4, timing = c(.5, .75),
sfu = sfLDOF,
sfl = sfHSD, sflpar = 1,
testLower = c(TRUE, FALSE, FALSE)
)
target_cp <- .3
# With theta = NULL (the default), CP uses the observed effect implied
# by the interim futility bound.
fit <- gsCPFutilitySpending(x, target_cp = target_cp, i = 1)
fit$cpFutilitySpending[c("target_cp", "achieved_cp", "sflpar")]
#> $target_cp
#> [1] 0.3
#>
#> $achieved_cp
#> [1] 0.3
#>
#> $sflpar
#> [1] 2.430891
#>
# Use the fitted spending parameter in the final gsDesign.
final_design <- gsDesign(
k = 3, test.type = 4, timing = c(.5, .75),
sfu = sfLDOF,
sfl = sfHSD, sflpar = fit$cpFutilitySpending$sflpar,
testLower = c(TRUE, FALSE, FALSE)
)
# The final design has CP 0.3000 at the first interim futility bound.
gsBoundSummary(
final_design,
exclude = "B-value"
)
#> Analysis Value Efficacy Futility
#> IA 1: 50% Z 2.9626 1.1538
#> N/Fixed design N: 0.63 p (1-sided) 0.0015 0.1243
#> ~delta at bound 1.1490 0.4475
#> Spending 0.0015 0.0771
#> CP 0.9994 0.3000
#> CP H1 0.9979 0.8145
#> PP 0.9900 0.3552
#> P(Cross) if delta=0 0.0015 0.8757
#> P(Cross) if delta=1 0.3504 0.0771
#> IA 2: 75% Z 2.3590 NA
#> N/Fixed design N: 0.95 p (1-sided) 0.0092 NA
#> ~delta at bound 0.7470 NA
#> Spending 0.0081 NA
#> CP 0.9222 NA
#> CP H1 0.9700 NA
#> PP 0.8901 NA
#> P(Cross) if delta=0 0.0092 NA
#> P(Cross) if delta=1 0.7801 NA
#> Final Z 2.0141 NA
#> N/Fixed design N: 1.27 p (1-sided) 0.0220 NA
#> ~delta at bound 0.5523 NA
#> Spending 0.0154 NA
#> P(Cross) if delta=0 0.0196 NA
#> P(Cross) if delta=1 0.9000 NA
# Use the same test type, timing, and spending in a survival design.
# Other survival inputs use gsSurv() defaults. The IA 1 futility CP row
# again shows 0.3000, agreeing with target_cp up to numerical tolerance.
surv_design <- gsSurv(
k = 3, test.type = 4, timing = c(.5, .75),
sfu = sfLDOF,
sfl = sfHSD, sflpar = fit$cpFutilitySpending$sflpar,
testLower = c(TRUE, FALSE, FALSE)
)
gsBoundSummary(surv_design, exclude = "B-value")
#> Analysis Value Efficacy Futility
#> IA 1: 50% Z 2.9626 1.1538
#> N: 284 p (1-sided) 0.0015 0.1243
#> Events: 102 ~HR at bound 0.5554 0.7953
#> Month: 11 Spending 0.0015 0.0771
#> CP 0.9994 0.3000
#> CP H1 0.9979 0.8145
#> PP 0.9900 0.3552
#> P(Cross) if HR=1 0.0015 0.8757
#> P(Cross) if HR=0.6 0.3504 0.0771
#> IA 2: 75% Z 2.3590 NA
#> N: 318 p (1-sided) 0.0092 NA
#> Events: 153 ~HR at bound 0.6823 NA
#> Month: 14 Spending 0.0081 NA
#> CP 0.9222 NA
#> CP H1 0.9700 NA
#> PP 0.8901 NA
#> P(Cross) if HR=1 0.0092 NA
#> P(Cross) if HR=0.6 0.7801 NA
#> Final Z 2.0141 NA
#> N: 318 p (1-sided) 0.0220 NA
#> Events: 204 ~HR at bound 0.7538 NA
#> Month: 18 Spending 0.0154 NA
#> P(Cross) if HR=1 0.0196 NA
#> P(Cross) if HR=0.6 0.9000 NA
# Survival designs can also be calibrated directly.
surv_fit <- gsCPFutilitySpending(surv_design, target_cp = .7, i = 1)
gsBoundSummary(surv_fit, exclude = "B-value")
#> Analysis Value Efficacy Futility
#> IA 1: 50% Z 2.9626 1.6677
#> N: 374 p (1-sided) 0.0015 0.0477
#> Events: 135 ~HR at bound 0.5999 0.7500
#> Month: 11 Spending 0.0015 0.0969
#> CP 0.9994 0.7000
#> CP H1 0.9994 0.9651
#> PP 0.9901 0.6457
#> P(Cross) if HR=1 0.0015 0.9523
#> P(Cross) if HR=0.6 0.5018 0.0969
#> IA 2: 75% Z 2.3590 NA
#> N: 420 p (1-sided) 0.0092 NA
#> Events: 202 ~HR at bound 0.7173 NA
#> Month: 14 Spending 0.0081 NA
#> CP 0.9222 NA
#> CP H1 0.9845 NA
#> PP 0.8904 NA
#> P(Cross) if HR=1 0.0078 NA
#> P(Cross) if HR=0.6 0.8606 NA
#> Final Z 2.0141 NA
#> N: 420 p (1-sided) 0.0220 NA
#> Events: 269 ~HR at bound 0.7822 NA
#> Month: 18 Spending 0.0154 NA
#> P(Cross) if HR=1 0.0135 NA
#> P(Cross) if HR=0.6 0.9000 NA
# Optionally require an absolute CP residual no larger than 0.000001.
fit_tight <- gsCPFutilitySpending(
x, target_cp = target_cp, i = 1,
control = list(cp_tol = 1e-6)
)
abs(fit_tight$cpFutilitySpending$achieved_cp - target_cp) <= 1e-6
#> [1] TRUE
# Two targets use a two-parameter family and active futility at both interims.
x_two <- gsDesign(
k = 3, test.type = 4, timing = c(.5, .75),
sfl = sfLogistic, sflpar = c(0, 1)
)
fit_two <- gsCPFutilitySpending(
x_two, target_cp = c(.3, .5), i = c(1, 2), theta = NULL
)
# IA 1: efficacy at IA 2 or FA; IA 2: efficacy at FA.
# Independently recompute total future CP using each fitted bound's effect.
achieved <- vapply(1:2, function(j) {
cp <- gsCP(fit_two, i = j, zi = fit_two$lower$bound[j],
theta = fit_two$lower$bound[j] / sqrt(fit_two$n.I[j]))
sum(cp$upper$prob[, 1])
}, numeric(1))
stopifnot(max(abs(achieved - c(.3, .5))) <= 1e-4,
isTRUE(all.equal(fit_two$timing, x_two$timing)))
# }