Skip to content

Commit 0d3beed

Browse files
committed
updated spending functions to include WT
1 parent 50c866e commit 0d3beed

13 files changed

Lines changed: 696 additions & 44 deletions

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,6 @@ export(spending_linear)
5656
export(spending_of)
5757
export(spending_pocock)
5858
export(spending_with_time)
59+
export(spending_wt)
5960
export(three_doses_two_primary_two_secondary)
6061
export(two_doses_two_primary_two_secondary)

R/graph_test_shortcut_gsd.R

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@
4444
#' `NA` padding, `info_frac` must be a matrix with `NA` in the same
4545
#' positions as `p`.
4646
#'
47-
#' Non-`NA` values must be in (0, 1] and monotonically non-decreasing per
48-
#' hypothesis. The last non-`NA` value does not need to be 1, allowing
47+
#' Non-`NA` values must be positive and monotonically non-decreasing per
48+
#' hypothesis. Values greater than 1 are allowed (e.g., when more
49+
#' information is collected than planned). The spending functions cap
50+
#' the cumulative spending at `alpha` for information fractions at or
51+
#' above 1. The last non-`NA` value does not need to be 1, allowing
4952
#' the procedure to be applied up to an interim analysis.
5053
#' @param spending_fn Spending function(s) for computing group sequential
5154
#' boundaries. Can be:
@@ -562,7 +565,7 @@ gsd_test <- function(graph, p, alpha, info_frac, spending_fn, look_back,
562565
if (test_values) {
563566
tv_details[[k]] <- gsd_test_values_details(
564567
step_graph, p, k, alpha, info_frac, spending_fn,
565-
newly_in_order, hyp_names, rejected, has_data_k
568+
newly_in_order, hyp_names, rejected, active_at_k
566569
)
567570

568571
# Add Look_back column (FALSE for all standard rows)
@@ -587,9 +590,14 @@ gsd_test <- function(graph, p, alpha, info_frac, spending_fn, look_back,
587590
tv_details[[k]]$Analysis == k)
588591

589592
if (length(hyp_row) > 0) {
590-
# Has data at analysis k: set Reject to FALSE and insert
591-
# look_back rows after it
592-
tv_details[[k]]$Reject[hyp_row] <- FALSE
593+
# Has data at analysis k: check if the nominal p-value at
594+
# analysis k also crosses the boundary. If not, set Reject
595+
# to FALSE (the rejection is only via look_back).
596+
p_at_k <- tv_details[[k]]$p[hyp_row]
597+
b_at_k <- tv_details[[k]]$Boundary[hyp_row]
598+
if (is.na(p_at_k) || p_at_k > b_at_k) {
599+
tv_details[[k]]$Reject[hyp_row] <- FALSE
600+
}
593601
before <- tv_details[[k]][seq_len(hyp_row), , drop = FALSE]
594602
after <- if (hyp_row < nrow(tv_details[[k]])) {
595603
tv_details[[k]][(hyp_row + 1):nrow(tv_details[[k]]), ,
@@ -822,8 +830,8 @@ gsd_input_val <- function(graph, p, alpha, info_frac, spending_fn, look_back,
822830
"Information fractions must have the same number of columns as p" =
823831
ncol(info_frac) == num_analyses,
824832
"Information fractions must be numeric" = is.numeric(info_frac),
825-
"Non-NA information fractions must be in (0, 1]" =
826-
length(if_non_na) == 0 || all(if_non_na > 0 & if_non_na <= 1),
833+
"Non-NA information fractions must be positive" =
834+
length(if_non_na) == 0 || all(if_non_na > 0),
827835
"Spending functions must be a list of functions" =
828836
is.list(spending_fn) &&
829837
all(vapply(spending_fn, is.function, logical(1))),

R/print.gsd_graph_report.R

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,15 @@ print.gsd_graph_report <- function(x, ..., precision = 6, indent = 2) {
145145
check.names = FALSE
146146
)
147147
names(df_summary)[[1]] <- formatC("Hypothesis", width = hyp_width)
148-
names(df_summary)[[2]] <- "Adj.P-value"
148+
names(df_summary)[[2]] <- "Adj.P-value*"
149149

150150
print(df_summary, row.names = FALSE)
151151

152+
cat(pad, "(*) Adjusted p-values account for both the group sequential",
153+
" design and the\n", pad, " graphical multiple comparison procedure.",
154+
" Based on repeated p-values when\n", pad, " look_back = FALSE,",
155+
" and sequential p-values when look_back = TRUE.\n", sep = "")
156+
152157
# Rejection sequence
153158
rej_seq <- x$outputs$rejection_sequence
154159
if (length(rej_seq) > 0) {
@@ -201,9 +206,10 @@ print.gsd_graph_report <- function(x, ..., precision = 6, indent = 2) {
201206

202207
# Print footnote for look_back hypotheses
203208
if (has_look_back) {
204-
cat(pad, "(*) Rejected via look_back: nominal p-value did not cross",
205-
" the boundary at the\n", pad, " current analysis, but",
206-
" crossed the boundary at an earlier analysis.\n", sep = "")
209+
cat(pad, "(*) Rejected via look_back: the nominal p-value crossed",
210+
" the boundary at an\n", pad, " earlier analysis with the",
211+
" hypothesis weight updated via graph propagation.\n",
212+
sep = "")
207213
}
208214
cat("\n")
209215
}

R/spending_functions.R

Lines changed: 167 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
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
@@ -89,10 +89,12 @@
8989
#' spending_linear(0.025, c(1/3, 2/3, 1))
9090
spending_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)
104106
spending_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)
120124
spending_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)
138144
spending_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+
}

man/graph_test_shortcut_gsd.Rd

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/gsd_input_val.Rd

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/spending_functions.Rd

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)