Skip to contents

gsCPFutilitySpending() 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.

Usage

gsCPFutilitySpending(
  x,
  target_cp,
  i = seq_along(target_cp),
  sfl = x$lower$sf,
  theta = NULL,
  control = list()
)

Arguments

x

A gsDesign, gsSurv, gsSurvCalendar, or gsSurvPower design with test.type 3, 4, 7, or 8. The analysis information fractions in x$timing are 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 analysis i + 1 through the final analysis x$k, conditional on the statistic at analysis i equaling its candidate futility bound. With theta = NULL, the future effect is the observed effect implied by that bound, lower$bound[i] / sqrt(n.I[i]); otherwise theta specifies 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 in 1:(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. Supply sfl to override it.

theta

Optional future effect for conditional power. A scalar is recycled; otherwise its length must equal target_cp. When NULL, 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.

start

Initial 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, upper

Search 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 satisfy lower < upper and contain the starting values.

cp_tol

Maximum 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.

maxit

Positive integer iteration limit for the joint optimizer (default 500), not a global limit on design evaluations or the one-parameter root search.

reltol

Positive finite scalar controlling internal numerical convergence (default 1e-10). This does not replace the final cp_tol acceptance check.

backward

Allow a latest-to-earliest coordinate fallback if the initial joint fit misses the targets (default TRUE).

trace

Display joint-optimizer progress (default FALSE). Both backward and trace must 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:

FamilyStartLowerUpper
sfHSD-2-4040
sfPower11e-450
sfExponential0.51e-41.5
sfLDOF10.00520
sfBetaDistc(1, 1)c(1e-3, 1e-3)c(50, 50)
Other supported two-parameter familiesc(0, 1)c(-20, 1e-3)c(20, 50)
Custom functionRequired-20 per parameter20 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)))
# }