Skip to content

Commit 5c7b82f

Browse files
Calculate sequential p-values for AHR objects (#612)
* Development: - Add a new function to calculate the sequential p-values for AHR objects - Add developer tests - Update the Rd file of `sequential_pval` * Fix CI/CD checking errors by (1) adding `sequential_pval` to _pkgdown.yml, (2) reducing the long row in the example, (3) correcting the code by removing the call of `x` to `gs_design`, and (4) adding namespace to `gsBound1` * Review, Co-authored-by: John Blischak <jdblischak@gmail.com> - Update R/sequential_pval.R , improve the decision of binding and non-binding - Update R/sequential_pval.R, apply `get_sf` - Update R/sequential_pval.R, remove `@keywords internal` - Update R/sequential_pval.R, apply `get_sf` - remove namespace of `gsDesign2::`
1 parent 3760112 commit 5c7b82f

5 files changed

Lines changed: 296 additions & 0 deletions

File tree

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export(gs_update_ahr)
5252
export(ppwe)
5353
export(pw_info)
5454
export(s2pwe)
55+
export(sequential_pval)
5556
export(text_summary)
5657
export(to_integer)
5758
export(wlr_weight_1)

R/sequential_pval.R

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#' @title Sequential p-value computation for gsDesign2
2+
#' @description \code{sequential_pval} computes a sequential p-value for a group sequential design
3+
#' created with \code{gs_design_ahr()} using a spending function approach.
4+
#' It is the minimum of repeated p-values computed at each analysis (Jennison and Turnbull, 2000).
5+
#' This is particularly useful for multiplicity methods such as the graphical method for group sequential designs
6+
#' where sequential p-values for multiple hypotheses can be used as nominal p-values to plug into a multiplicity graph.
7+
#' A sequential p-value is described as the minimum alpha level at which a one-sided group sequential bound would
8+
#' be rejected given interim and final observed results.
9+
#' Mild restrictions are required on spending functions used, but these are satisfied for commonly used spending functions
10+
#' such as the Lan-DeMets spending function approximating an O'Brien-Fleming bound or a Hwang-Shih-DeCani spending function.
11+
#'
12+
#' @param gs_design Group sequential design generated by \code{gs_design_ahr()}.
13+
#' @param event Event counts; numeric vector with increasing, positive values with at most one
14+
#' value greater than or equal to largest value in \code{gs_design$analysis$event};
15+
#' NOTE: if NULL, planned \code{event} will be used (\code{gs_design$analysis$event})
16+
#' @param z z-value tests corresponding to analyses in \code{info}; positive values indicate a positive finding; must have the same length as \code{info}.
17+
#' @param ustime Spending time for upper bound at specified analyses; specify default: \code{NULL} if this is to be based on information fraction;
18+
#' if not \code{NULL}, must have the same length as \code{info}; increasing positive values with at most 1 greater than or equal to 1.
19+
#' @param interval Interval for search to derive p-value; Default: \code{c(1e-05, 0.9999)}. Lower end of interval must be >0 and upper end must be < 1.
20+
#' The primary reason to not use the defaults would likely be if a test were vs a Type I error <0.0001.
21+
#' @return Sequential p-value (single numeric one-sided p-value between 0 and 1).
22+
#' Note that if the sequential p-value is less than the lower end of the input interval,
23+
#' the lower of interval will be returned.
24+
#' Similarly, if the sequential p-value is greater than the upper end of the input interval,
25+
#' then the upper end of interval is returned.
26+
#' @references
27+
#' Jennison C and Turnbull BW (2000), \emph{Group Sequential
28+
#' Methods with Applications to Clinical Trials}. Boca Raton: Chapman and Hall.
29+
#'
30+
#' Liu, Qing, and Keaven M. Anderson. "On adaptive extensions of group sequential trials for clinical investigations." \emph{Journal of the American Statistical Association} 103.484 (2008): 1621-1630.
31+
#'
32+
#' Maurer, Willi, and Frank Bretz. "Multiple testing in group sequential trials using graphical approaches." \emph{Statistics in Biopharmaceutical Research} 5.4 (2013): 311-320.
33+
#' @details
34+
#' Solution is found with a search using \code{uniroot}.
35+
#' This finds the maximum alpha-level for which an efficacy bound is crossed,
36+
#' completely ignoring any futility bound.
37+
#' @export
38+
#'
39+
#' @examples
40+
#' # Derive Group Sequential Design using gsDesign2
41+
#' x <- gs_design_ahr(
42+
#' enroll_rate = define_enroll_rate(duration = c(2, 2, 2, 6),
43+
#' rate = c(2.5, 5, 7.5, 10)),
44+
#' fail_rate = define_fail_rate(duration = Inf, fail_rate = log(2) / 6,
45+
#' hr = 0.6, dropout_rate = .001),
46+
#' info_frac = c(.5, .65, .8, 1),
47+
#' analysis_time = 30,
48+
#' upper = gs_spending_bound,
49+
#' upar = list(sf = "sfLDOF", total_spend = 0.025),
50+
#' lower = gs_spending_bound,
51+
#' lpar = list(sf = "sfHSD", total_spend = 0.1, param = 2),
52+
#' binding = FALSE
53+
#' )
54+
#'
55+
#' # Analysis at interim analysis 2
56+
#' sequential_pval(gs_design = x, event = c(100, 160), z = c(1.5, 2))
57+
#'
58+
#' # Use planned spending for interim and final analyses
59+
#' sequential_pval(gs_design = x,
60+
#' event = c(100, 160, 190),
61+
#' z = c(1.5, 2, 2.5))
62+
sequential_pval <- function(gs_design,
63+
event = NULL,
64+
z = NULL,
65+
ustime = NULL,
66+
interval = c(.00001, .9999)) {
67+
q_e <- gs_design$input$ratio / (1 + gs_design$input$ratio)
68+
69+
# gs_design should be object returned from gs_design_ahr or gs_power_ahr with >=2 analyses
70+
if (gs_design$design != "ahr") stop("gs_design must be an object created with gs_design_ahr() or gs_power_ahr()")
71+
if (nrow(gs_design$analysis) == 1) stop("gs_design must be an GSD object with more than 1 analysis")
72+
73+
# check if gs_design is 1-sided, or has non-binding futility bounds
74+
one_sided <- all(gs_design$bound$bound == "upper")
75+
nonbinding <- !gs_design$input$binding
76+
if (!(one_sided || nonbinding)) stop("gs_design must be one-sided or have non-binding futility bounds")
77+
78+
# if event is specified, check if it is an increasing numerical vector
79+
if (!is.null(event)) check_increasing(event)
80+
81+
# if ustime is specified, check if it is an increasing numerical vector ranging within (0, 1]
82+
if (!is.null(ustime)) check_info_frac(ustime)
83+
84+
# if event not specified, assume planned values used
85+
if (is.null(event)) event <- gs_design$analysis$event
86+
87+
# if ustime not specified, use information fraction from design
88+
if (is.null(ustime)) ustime <- event/max(gs_design$analysis$event)
89+
90+
# if z not specified, stop
91+
if (is.null(z)) stop("z must be provided")
92+
93+
# check that z is numeric vector
94+
if (!is.vector(z, mode = "numeric")) stop("z must be numeric vector")
95+
96+
# check lengths match
97+
if (length(event) != length(z)) stop("event and z must have the same length")
98+
if (length(event) != length(ustime)) stop("event and ustime must have the same length")
99+
100+
# check that ustime has at most 1 value >= 1
101+
if (sum(ustime >= 1) > 1) stop("No more than one value of ustime >= 1")
102+
103+
# check the interval is 2 values in (0,1), exclusive of endpoints
104+
if (!is.vector(interval, mode = "numeric") || length(interval) != 2 || min(interval) <= 0 || max(interval) >= 1) {
105+
stop("interval must be 2 distinct values strictly between 0 and 1")
106+
}
107+
108+
# Get spending function and parameters from gs_design
109+
# sf is a character string, so we need to get the actual function from gsDesign namespace
110+
sf_upper <- get_sf(gs_design$input$upar$sf)
111+
sf_param <- gs_design$input$upar$param
112+
113+
# check upper end of p-value interval input
114+
probhi <- sf_upper(alpha = max(interval), t = ustime, param = sf_param)$spend
115+
if (length(z) > 1) probhi <- probhi - c(0, probhi[1:(length(z) - 1)])
116+
if (min(gsDesign::gsBound1(I = event * (1 - q_e) * q_e, theta = 0, a = rep(-20, length(z)),
117+
probhi = probhi)$b - z) > 0) return(max(interval))
118+
119+
# check lower end of p-value interval input
120+
probhi <- sf_upper(alpha = min(interval), t = ustime, param = sf_param)$spend
121+
if (length(z) > 1) probhi <- probhi - c(0, probhi[1:(length(z) - 1)])
122+
if (min(gsDesign::gsBound1(I = event * (1 - q_e) * q_e, theta = 0, a = rep(-20, length(z)),
123+
probhi = probhi)$b - z) < 0) return(min(interval))
124+
125+
# if answer is between interval bounds, find it with root-finding
126+
x <- try(uniroot(sequential_zdiff, interval = -qnorm(interval), gs_design = gs_design,
127+
event = event, z = z, ustime = ustime))
128+
129+
if (inherits(x, "try-error")) stop("Failed to find root for sequential p-value")
130+
131+
pnorm(-x$root)
132+
}
133+
134+
#' This function calculates the difference between:
135+
#' Computed efficacy bound at a given alpha level (using the spending function)
136+
#' Observed Z-statistic
137+
#' @noRd
138+
sequential_zdiff <- function(x,
139+
gs_design,
140+
event = (1:3) * 50,
141+
z = 1:3,
142+
ustime = NULL) {
143+
q_e <- gs_design$input$ratio / (1 + gs_design$input$ratio)
144+
alpha <- pnorm(-x)
145+
146+
sf_upper <- get_sf(gs_design$input$upar$sf)
147+
sf_param <- gs_design$input$upar$param
148+
149+
probhi <- sf_upper(alpha = alpha, t = ustime, param = sf_param)$spend
150+
if (length(z) > 1) probhi <- probhi - c(0, probhi[1:(length(z) - 1)])
151+
152+
return(min(gsDesign::gsBound1(I = event * (1 - q_e) * q_e, theta = 0, a = rep(-20, length(z)),
153+
probhi = probhi)$b - z))
154+
}

_pkgdown.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ reference:
4949
- gs_design_ahr
5050
- gs_update_ahr
5151
- ahr_blinded
52+
- sequential_pval
5253
- title: "Weighted logrank"
5354
desc: >
5455
Functions for the weighted logrank test (WLR) method.

man/sequential_pval.Rd

Lines changed: 84 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
test_that("Comparision with gsDesign::sequentialPValue", {
2+
alpha <- 0.025
3+
beta <- 0.1
4+
ratio <- 1
5+
6+
# Enrollment
7+
enroll_rate <- define_enroll_rate(
8+
duration = c(2, 2, 10),
9+
rate = (1:3) / 3)
10+
11+
# Failure and dropout
12+
fail_rate <- define_fail_rate(
13+
duration = Inf, fail_rate = log(2) / 9,
14+
hr = 0.6, dropout_rate = .0001)
15+
16+
# IA and FA analysis time
17+
study_duration <- 36
18+
19+
# Randomization ratio
20+
ratio <- 1
21+
22+
# Spending
23+
upper <- gs_spending_bound
24+
lower <- gs_b
25+
upar <- list(sf = "sfLDOF", total_spend = alpha)
26+
lpar <- rep(-Inf, 3)
27+
28+
# ------------------------------ #
29+
# original design of gsDesign #
30+
# ------------------------------ #
31+
x_gsd <- gsSurv(k = 3, test.type = 1, alpha = alpha, beta = beta,
32+
astar = 0, timing = 1:3/3,
33+
sfu = sfLDOF, sfupar = 0,
34+
sfl = sfLDOF, sflpar = 0,
35+
lambdaC = log(2) / 9, hr = 0.6, hr0 = 1,
36+
eta = fail_rate$dropout_rate |> unique(),
37+
gamma = enroll_rate$rate,
38+
R = enroll_rate$duration,
39+
S = NULL, T = study_duration,
40+
minfup = study_duration - sum(enroll_rate$duration),
41+
ratio = ratio)
42+
43+
# ------------------------------ #
44+
# original design of gsDesign2 #
45+
# ------------------------------ #
46+
x_gsd2 <- gs_design_ahr(enroll_rate = enroll_rate, fail_rate = fail_rate,
47+
alpha = alpha, beta = beta, info_frac = 1:3/3, ratio = ratio,
48+
upper = upper, upar = upar,
49+
lower = lower, lpar = lpar,
50+
analysis_time = study_duration)
51+
52+
seq_pva_gsd <- sequentialPValue(x_gsd, n.I = c(40, 120), Z = c(1.5,2))
53+
seq_pva_gsd2 <- sequential_pval(x_gsd2, event = c(40, 120) * (x_gsd2$analysis$event[3] / x_gsd$n.I[3]), z = c(1.5, 2))
54+
55+
expect_equal(seq_pva_gsd, seq_pva_gsd2)
56+
})

0 commit comments

Comments
 (0)