1616# ' @param alpha A numeric scalar of the total significance level to be spent.
1717# ' Must be between 0 and 1.
1818# ' @param info_frac A numeric scalar or vector of information fractions. Values
19- # ' must be in \[0, 1\] . When `info_frac = 0`, the spending is 0. When
20- # ' `info_frac = 1`, the spending equals `alpha`.
19+ # ' must be non-negative . When `info_frac = 0`, the spending is 0. When
20+ # ' `info_frac > = 1`, the spending is capped at `alpha`.
2121# ' @param gamma A numeric scalar for the gamma parameter of the
2222# ' Hwang-Shih-DeCani spending function. Common choices are `gamma = -4`
2323# ' (approximates O'Brien-Fleming), `gamma = 1` (approximates Pocock), and
8989# ' spending_linear(0.025, c(1/3, 2/3, 1))
9090spending_of <- function (alpha , info_frac ) {
9191 stopifnot(
92- " info_frac must be in [0, 1]" = all(info_frac > = 0 & info_frac < = 1 )
92+ " info_frac must be non-negative" = all(info_frac > = 0 ),
93+ " At most one info_frac value can be >= 1" = sum(info_frac > = 1 ) < = 1
9394 )
9495 result <- 2 * (1 - stats :: pnorm(stats :: qnorm(1 - alpha / 2 ) / sqrt(info_frac )))
9596 result [info_frac == 0 ] <- 0
97+ result <- pmin(result , alpha )
9698 result
9799}
98100
@@ -103,10 +105,12 @@ spending_of <- function(alpha, info_frac) {
103105# ' spending_pocock(0.025, 0.5)
104106spending_pocock <- function (alpha , info_frac ) {
105107 stopifnot(
106- " info_frac must be in [0, 1]" = all(info_frac > = 0 & info_frac < = 1 )
108+ " info_frac must be non-negative" = all(info_frac > = 0 ),
109+ " At most one info_frac value can be >= 1" = sum(info_frac > = 1 ) < = 1
107110 )
108111 result <- alpha * log(1 + (exp(1 ) - 1 ) * info_frac )
109112 result [info_frac == 0 ] <- 0
113+ result <- pmin(result , alpha )
110114 result
111115}
112116
@@ -119,14 +123,16 @@ spending_pocock <- function(alpha, info_frac) {
119123# ' spending_hsd(0.025, 0.5, gamma = 0)
120124spending_hsd <- function (alpha , info_frac , gamma = - 4 ) {
121125 stopifnot(
122- " info_frac must be in [0, 1]" = all(info_frac > = 0 & info_frac < = 1 )
126+ " info_frac must be non-negative" = all(info_frac > = 0 ),
127+ " At most one info_frac value can be >= 1" = sum(info_frac > = 1 ) < = 1
123128 )
124129 if (gamma == 0 ) {
125130 result <- alpha * info_frac
126131 } else {
127132 result <- alpha * (1 - exp(- gamma * info_frac )) / (1 - exp(- gamma ))
128133 }
129134 result [info_frac == 0 ] <- 0
135+ result <- pmin(result , alpha )
130136 result
131137}
132138
@@ -137,9 +143,10 @@ spending_hsd <- function(alpha, info_frac, gamma = -4) {
137143# ' spending_linear(0.025, 0.5)
138144spending_linear <- function (alpha , info_frac ) {
139145 stopifnot(
140- " info_frac must be in [0, 1]" = all(info_frac > = 0 & info_frac < = 1 )
146+ " info_frac must be non-negative" = all(info_frac > = 0 ),
147+ " At most one info_frac value can be >= 1" = sum(info_frac > = 1 ) < = 1
141148 )
142- alpha * info_frac
149+ pmin( alpha * info_frac , alpha )
143150}
144151
145152
@@ -207,12 +214,163 @@ spending_with_time <- function(spending_fn, spending_time) {
207214 stopifnot(
208215 " spending_fn must be a function" = is.function(spending_fn ),
209216 " spending_time must be a numeric vector" = is.numeric(spending_time ),
210- " spending_time must be in [0, 1]" =
211- all(spending_time > = 0 & spending_time < = 1 )
217+ " spending_time must be non-negative" =
218+ all(spending_time > = 0 ),
219+ " At most one spending_time value can be >= 1" =
220+ sum(spending_time > = 1 ) < = 1
212221 )
213222
214223 function (alpha , info_frac ) {
215224 st <- spending_time [seq_along(info_frac )]
216225 spending_fn(alpha , st )
217226 }
218227}
228+
229+
230+ # ' Wang-Tsiatis spending function
231+ # '
232+ # ' @description
233+ # ' Computes the implied cumulative alpha spending from the Wang-Tsiatis family
234+ # ' of group sequential boundaries. The Wang-Tsiatis boundaries at analysis
235+ # ' \eqn{k} with information fraction \eqn{t_k} are defined as:
236+ # ' \deqn{c_k = C \cdot t_k^{\Delta - 0.5},}
237+ # ' where \eqn{\Delta} is the shape parameter and \eqn{C} is a constant
238+ # ' calibrated so that the overall Type I error equals \eqn{\alpha}.
239+ # '
240+ # ' Special cases:
241+ # ' * \eqn{\Delta = 0.5}: Pocock boundaries (equal Z-scale boundaries across
242+ # ' analyses).
243+ # ' * \eqn{\Delta = 0}: O'Brien-Fleming boundaries (very conservative at
244+ # ' early analyses).
245+ # ' * \eqn{0 < \Delta < 0.5}: intermediate between O'Brien-Fleming and Pocock.
246+ # '
247+ # ' Unlike the Lan-DeMets approximations ([spending_of()], [spending_pocock()]),
248+ # ' this function computes the **exact** boundaries from the Wang-Tsiatis
249+ # ' family and derives the implied spending. It is computationally more
250+ # ' expensive because it requires root-finding and multivariate normal
251+ # ' integration at each call.
252+ # '
253+ # ' @param alpha A numeric scalar of the total significance level.
254+ # ' @param info_frac A numeric vector of information fractions at each analysis.
255+ # ' Must be non-negative, with at most one value \eqn{\geq 1}.
256+ # ' @param delta A numeric scalar for the shape parameter \eqn{\Delta}.
257+ # ' The default is `0.5` (Pocock). Use `0` for O'Brien-Fleming.
258+ # ' @param maxpts An integer scalar for the maximum number of function values
259+ # ' for [mvtnorm::GenzBretz()]. The default is 25000.
260+ # ' @param abseps A numeric scalar for the absolute error tolerance for
261+ # ' [mvtnorm::GenzBretz()]. The default is 1e-6.
262+ # '
263+ # ' @return A numeric vector the same length as `info_frac` of cumulative alpha
264+ # ' spent at each information fraction.
265+ # '
266+ # ' @seealso [spending_of()] and [spending_pocock()] for the Lan-DeMets
267+ # ' approximations, [gs_boundaries()] for computing boundaries from spending
268+ # ' functions, [graph_test_shortcut_gsd()] for the graphical procedure.
269+ # '
270+ # ' @references
271+ # ' Wang, S. K., and Tsiatis, A. A. (1987). Approximately optimal one-parameter
272+ # ' boundaries for group sequential trials. \emph{Biometrics}, 43(1), 193-199.
273+ # '
274+ # ' @export
275+ # '
276+ # ' @examples
277+ # ' # Exact O'Brien-Fleming (delta = 0)
278+ # ' spending_wt(0.025, c(0.5, 1), delta = 0)
279+ # '
280+ # ' # Exact Pocock (delta = 0.5)
281+ # ' spending_wt(0.025, c(0.5, 1), delta = 0.5)
282+ # '
283+ # ' # Intermediate (delta = 0.25)
284+ # ' spending_wt(0.025, c(1/3, 2/3, 1), delta = 0.25)
285+ # '
286+ # ' # Compare with Lan-DeMets approximations
287+ # ' spending_of(0.025, c(1/3, 2/3, 1)) # Lan-DeMets OBF approximation
288+ # ' spending_wt(0.025, c(1/3, 2/3, 1), 0) # Exact OBF
289+ # '
290+ # ' # Use in graph_test_shortcut_gsd (wrap to fix delta)
291+ # ' \donttest{
292+ # ' g <- graph_create(c(0.5, 0.5), rbind(c(0, 1), c(1, 0)))
293+ # ' p <- rbind(H1 = c(0.024, 0.01), H2 = c(0.015, 0.005))
294+ # ' graph_test_shortcut_gsd(
295+ # ' graph = g, p = p, alpha = 0.025,
296+ # ' info_frac = c(0.5, 1),
297+ # ' spending_fn = function(a, t) spending_wt(a, t, delta = 0.25)
298+ # ' )
299+ # ' }
300+ spending_wt <- function (alpha , info_frac , delta = 0.5 ,
301+ maxpts = 25000 , abseps = 1e-6 ) {
302+ stopifnot(
303+ " info_frac must be non-negative" = all(info_frac > = 0 ),
304+ " At most one info_frac value can be >= 1" = sum(info_frac > = 1 ) < = 1 ,
305+ " delta must be a numeric scalar" = is.numeric(delta ) && length(delta ) == 1
306+ )
307+
308+ K <- length(info_frac )
309+
310+ # Handle edge cases
311+ if (alpha < = 0 ) return (rep(0 , K ))
312+ if (K == 1 ) return (pmin(alpha , alpha ))
313+
314+ # Correlation matrix
315+ corr <- gs_corr(info_frac )
316+
317+ # Wang-Tsiatis boundary shape: c_k = C * t_k^(delta - 0.5)
318+ # For info_frac = 0, the shape is Inf (or 0 depending on delta),
319+ # handle by setting those boundaries to Inf (never cross)
320+ shape <- ifelse(info_frac == 0 , 0 , info_frac ^ (delta - 0.5 ))
321+
322+ algo <- mvtnorm :: GenzBretz(maxpts = maxpts , abseps = abseps )
323+
324+ # Find C such that P(cross at some k | H0) = alpha
325+ # P(cross) = 1 - P(Z_1 < c_1, ..., Z_K < c_K)
326+ find_C <- function (C_val ) {
327+ bounds_z <- C_val * shape
328+ # Replace any Inf or very large bounds with 20 for numerical stability
329+ bounds_z <- pmin(bounds_z , 20 )
330+
331+ prob_no_cross <- mvtnorm :: pmvnorm(
332+ upper = bounds_z ,
333+ corr = corr ,
334+ algorithm = algo
335+ )[[1 ]]
336+
337+ (1 - prob_no_cross ) - alpha
338+ }
339+
340+ # Search for C. Boundaries are on the Z-scale, so C is typically 1-5
341+ C_root <- tryCatch(
342+ stats :: uniroot(find_C , interval = c(0.1 , 20 ), tol = abseps ),
343+ error = function (e ) {
344+ # Widen search if needed
345+ stats :: uniroot(find_C , interval = c(0.01 , 50 ), tol = abseps )
346+ }
347+ )
348+ C_val <- C_root $ root
349+ bounds_z <- C_val * shape
350+ bounds_z <- pmin(bounds_z , 20 )
351+
352+ # Compute implied cumulative spending at each analysis k:
353+ # alpha_k = P(Z_1 >= c_1 or ... or Z_k >= c_k)
354+ # = 1 - P(Z_1 < c_1, ..., Z_k < c_k)
355+ cum_spending <- numeric (K )
356+ for (k in seq_len(K )) {
357+ if (info_frac [k ] == 0 ) {
358+ cum_spending [k ] <- 0
359+ next
360+ }
361+ if (k == 1 ) {
362+ # Univariate case: P(Z >= c_1) = 1 - Phi(c_1)
363+ cum_spending [k ] <- stats :: pnorm(bounds_z [1 ], lower.tail = FALSE )
364+ } else {
365+ cum_spending [k ] <- 1 - mvtnorm :: pmvnorm(
366+ upper = bounds_z [seq_len(k )],
367+ corr = corr [seq_len(k ), seq_len(k )],
368+ algorithm = algo
369+ )[[1 ]]
370+ }
371+ }
372+
373+ # Cap at alpha for numerical stability
374+ cum_spending <- pmin(cum_spending , alpha )
375+ cum_spending
376+ }
0 commit comments