Skip to content

Commit f2e87a9

Browse files
authored
Merge pull request #563 from Merck/562-gs_design_wlr-does-not-approximate-gs_design_ahr-when-it-is-logrank-test
Fix the issue where WLR design is different from AHR design even under logrank tests
2 parents 8a76f97 + de73e8e commit f2e87a9

12 files changed

+192
-4
lines changed

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: gsDesign2
22
Title: Group Sequential Design with Non-Constant Effect
3-
Version: 1.1.5
3+
Version: 1.1.5.1
44
Authors@R: c(
55
person("Keaven", "Anderson", email = "[email protected]", role = c("aut")),
66
person("Yujie", "Zhao", email = "[email protected]", role = c("aut", "cre")),

R/gs_power_wlr.R

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ gs_power_wlr <- function(enroll_rate = define_enroll_rate(duration = c(2, 2, 10)
169169
event = c(30, 40, 50),
170170
analysis_time = NULL,
171171
binding = FALSE,
172+
h1_spending = TRUE,
172173
upper = gs_spending_bound,
173174
lower = gs_spending_bound,
174175
upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025),
@@ -208,12 +209,23 @@ gs_power_wlr <- function(enroll_rate = define_enroll_rate(duration = c(2, 2, 10)
208209
interval = interval
209210
)
210211

212+
# set up H1 spending
213+
if (h1_spending) {
214+
theta1 <- x$theta
215+
info1 <- x$info
216+
} else {
217+
theta1 <- 0
218+
info1 <- x$info0
219+
}
220+
211221
# Given the above statistical information calculate the power ----
212222
y_h1 <- gs_power_npe(
213223
theta = x$theta,
224+
theta0 = 0,
225+
theta1 = theta1,
214226
info = x$info,
215227
info0 = x$info0,
216-
info1 = x$info,
228+
info1 = info1,
217229
info_scale = info_scale,
218230
binding = binding,
219231
upper = upper,
@@ -229,10 +241,10 @@ gs_power_wlr <- function(enroll_rate = define_enroll_rate(duration = c(2, 2, 10)
229241
y_h0 <- gs_power_npe(
230242
theta = 0,
231243
theta0 = 0,
232-
theta1 = x$theta,
244+
theta1 = theta1,
233245
info = x$info0,
234246
info0 = x$info0,
235-
info1 = x$info,
247+
info1 = info1,
236248
info_scale = info_scale,
237249
binding = binding,
238250
upper = upper,

R/utility_wlr.R

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,20 @@ gs_sigma2_wlr <- function(arm0,
299299
p1 <- arm1$size / (arm0$size + arm1$size)
300300
p0 <- 1 - p1
301301

302+
# ----------------------- #
303+
# get enroll duration and relative rate
304+
# ----------------------- #
305+
enroll_duration <- arm0$accr_interval |> diff()
306+
enroll_total_duration <- arm0$accr_interval |> max()
307+
n_enroll_piece <- length(enroll_duration)
308+
enroll_relative_rate <- rep(-10, n_enroll_piece)
309+
for (s in 1:n_enroll_piece) {
310+
enroll_relative_rate[s] <- arm0$accr_param[s] / arm0$accr_param[n_enroll_piece] * enroll_duration[n_enroll_piece] / enroll_duration[s]
311+
}
312+
313+
# ----------------------- #
314+
# get the weights
315+
# ----------------------- #
302316
if (identical(weight, "logrank")) {
303317
weight_fun <- wlr_weight_1
304318
} else {
@@ -311,10 +325,34 @@ gs_sigma2_wlr <- function(arm0,
311325
)
312326
}
313327

328+
# ----------------------- #
329+
# derive sigma2
330+
# ----------------------- #
331+
# from Section 2.3.3 of Yung and Liu 2020, the WLR test statistics is U/sqrt(V)
332+
# for the denominator V, by low of large numbers, we have V/n -> sigma2 in probability, where n is the number of subjects
314333
if (approx == "event_driven") {
334+
# “event_driven" is only for logrank test of PH
315335
nu <- p0 * prob_event(arm0, tmax = tmax) + p1 * prob_event(arm1, tmax = tmax)
316336
sigma2 <- p0 * p1 * nu
317337
} else if (approx %in% c("asymptotic", "generalized_schoenfeld")) {
338+
# if IA time is early than enroll finish, we adjust the accrual to
339+
# ensure the info0 is approximately proportional to events when it is logrnak test
340+
if (tmax < enroll_total_duration) {
341+
arm0 <- arm0
342+
arm1 <- arm1
343+
344+
# adjust the accrual time to the time of analysis
345+
arm0$accr_time <- tmax
346+
arm1$accr_time <- tmax
347+
# truncate the accrual interval to be stop at the time of analysis
348+
arm0$accr_interval <- c(tmax, arm0$accr_interval)[which(c(tmax, arm0$accr_interval) <= tmax)] |> sort()
349+
arm1$accr_interval <- arm0$accr_interval
350+
truncated_enroll_duration <- diff(arm0$accr_interval)
351+
# adjust the accrual probability per interval by the truncated interval
352+
arm0$accr_param <- truncated_enroll_duration * enroll_relative_rate[1:length(truncated_enroll_duration)] / sum(truncated_enroll_duration * enroll_relative_rate[1:length(truncated_enroll_duration)])
353+
arm1$accr_param <- arm0$accr_param
354+
}
355+
318356
sigma2 <- stats::integrate(
319357
function(x) {
320358
weight_fun(x, arm0, arm1)^2 *

man/gs_power_wlr.Rd

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

tests/testthat/test-developer-gs_design_wlr.R

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,93 @@ test_that("Validate if the output info-frac match the planned info-frac, when th
217217
expect_equal(x2$analysis$info_frac0[1], 0.75, tolerance = 5e-6)
218218
expect_equal(x3$analysis$info_frac[1], 0.75, tolerance = 5e-6)
219219
})
220+
221+
test_that("Validate if WLR design under logrank test generates similar design as in AHR", {
222+
x1 <- gs_design_ahr(
223+
enroll_rate = enroll_rate,
224+
fail_rate = fail_rate,
225+
alpha = 0.025,
226+
beta = 0.1,
227+
analysis_time = c(9, 27, 36),
228+
info_frac = NULL,
229+
ratio = ratio,
230+
binding = FALSE,
231+
upper = "gs_spending_bound",
232+
upar = list(sf = "sfLDOF", total_spend = 0.025, param = NULL),
233+
lower = "gs_spending_bound",
234+
lpar = list(sf = "sfLDOF", total_spend = 0.1, param = NULL),
235+
h1_spending = TRUE,
236+
info_scale = "h0_h1_info")
237+
238+
x2 <- gs_design_wlr(
239+
enroll_rate = enroll_rate,
240+
fail_rate = fail_rate,
241+
alpha = 0.025,
242+
beta = 0.1,
243+
analysis_time = c(9, 27, 36),
244+
info_frac = NULL,
245+
ratio = ratio,
246+
binding = FALSE,
247+
weight = "logrank",
248+
upper = "gs_spending_bound",
249+
upar = list(sf = "sfLDOF", total_spend = 0.025, param = NULL),
250+
lower = "gs_spending_bound",
251+
lpar = list(sf = "sfLDOF", total_spend = 0.1, param = NULL),
252+
h1_spending = TRUE,
253+
info_scale = "h0_h1_info")
254+
255+
# sample size is approximately close
256+
expect_equal(x1$analysis$n, x2$analysis$n, tolerance = 1e-2)
257+
258+
# events is approximately close
259+
expect_equal(x1$analysis$event, x2$analysis$event, tolerance = 1e-2)
260+
261+
# ahr is approximately close
262+
expect_equal(x1$analysis$ahr, x2$analysis$ahr, tolerance = 1e-2)
263+
264+
# theta is approximately close
265+
expect_equal(x1$analysis$theta, x2$analysis$theta, tolerance = 5e-2)
266+
267+
# info is approximately close
268+
expect_equal(x1$analysis$info, x2$analysis$info, tolerance = 1e-2)
269+
270+
# info0 is approximately close
271+
expect_equal(x1$analysis$info0, x2$analysis$info0, tolerance = 1e-2)
272+
273+
# info_frac is approximately close
274+
expect_equal(x1$analysis$info_frac, x2$analysis$info_frac, tolerance = 1e-2)
275+
276+
# info_frac0 is approximately close
277+
expect_equal(x1$analysis$info_frac0, x2$analysis$info_frac0, tolerance = 1e-2)
278+
279+
# upper bound is approximately close
280+
expect_equal(x1$bound$z[x1$bound$bound == "upper"], x2$bound$z[x2$bound$bound == "upper"], tolerance = 1e-2)
281+
282+
# lower bound is approximately close
283+
expect_equal(x1$bound$z[x1$bound$bound == "lower"], x2$bound$z[x2$bound$bound == "lower"], tolerance = 5e-2)
284+
285+
# probability of crossing upper bound under H0 is approximately close
286+
expect_equal(x1$bound$probability0[x1$bound$bound == "upper"], x2$bound$probability0[x2$bound$bound == "upper"], tolerance = 1e-2)
287+
288+
# probability of crossing lower bound under H0 is approximately close
289+
expect_equal(x1$bound$probability0[x1$bound$bound == "lower"], x2$bound$probability0[x2$bound$bound == "lower"], tolerance = 1e-2)
290+
291+
# probability of crossing upper bound under H1 is approximately close
292+
expect_equal(x1$bound$probability[x1$bound$bound == "upper"], x2$bound$probability[x2$bound$bound == "upper"], tolerance = 1e-2)
293+
294+
# probability of crossing lower bound under H1 is approximately close
295+
expect_equal(x1$bound$probability[x1$bound$bound == "lower"], x2$bound$probability[x2$bound$bound == "lower"], tolerance = 1e-2)
296+
297+
# hr at upper bound is approximately close
298+
expect_equal(x1$bound$`~hr at bound`[x1$bound$bound == "upper"], x2$bound$`~hr at bound`[x2$bound$bound == "upper"], tolerance = 1e-2)
299+
300+
# hr at lower bound is approximately close
301+
expect_equal(x1$bound$`~hr at bound`[x1$bound$bound == "lower"], x2$bound$`~hr at bound`[x2$bound$bound == "lower"], tolerance = 5e-2)
302+
303+
# nominal p at upper bound is approximately close
304+
expect_equal(x1$bound$`nominal p`[x1$bound$bound == "upper"], x2$bound$`nominal p`[x2$bound$bound == "upper"], tolerance = 1e-2)
305+
306+
# nominal p at lower bound is approximately close
307+
expect_equal(x1$bound$`nominal p`[x1$bound$bound == "upper"], x2$bound$`nominal p`[x2$bound$bound == "upper"], tolerance = 1e-2)
308+
309+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
test_that("statistcial information of WLR under logrank test is approximately same as that from AHR", {
2+
enroll_rate <- define_enroll_rate(duration = c(2, 2, 2, 18), rate = c(1, 2, 3, 4), stratum = "All")
3+
4+
duration <- diff(c(0, 4, 6, 44))
5+
control_rate <- log(2)/c(6, 6, 6)
6+
fail_rate <- define_fail_rate(duration, fail_rate = control_rate,
7+
dropout_rate = c(0.001, 0.001, 0.001),
8+
hr = c(1, 0.8, 0.6))
9+
10+
x1 <- gs_info_ahr(enroll_rate = enroll_rate,
11+
fail_rate = fail_rate,
12+
ratio = 1,
13+
event = NULL,
14+
analysis_time = c(9, 27, 36))
15+
16+
x2 <- gs_info_wlr(enroll_rate = enroll_rate,
17+
fail_rate = fail_rate,
18+
ratio = 1,
19+
event = NULL,
20+
analysis_time = c(9, 27, 36),
21+
weight = "logrank")
22+
23+
# info0 of AHR and WLR is approximately close
24+
expect_equal(x1$info0, x2$info0, tolerance = 1e-2)
25+
# info0 of WLR is approximately proportional to events
26+
expect_equal(x2$event / x2$info0, c(4, 4, 4), tolerance = 1e-2)
27+
})
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
test_that("Validate 2-sided symetric design", {
2+
x <- gs_power_wlr(enroll_rate = define_enroll_rate(duration = 12, rate = 50),
3+
analysis_time = NULL, event = c(100, 200, 300),
4+
upper = gs_spending_bound,
5+
upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025),
6+
lower = gs_spending_bound,
7+
lpar = list(sf = gsDesign::sfLDOF, total_spend = 0.025),
8+
binding = TRUE, h1_spending = FALSE)
9+
10+
expect_equal(x$bound$z[x$bound$bound == "upper"],
11+
-x$bound$z[x$bound$bound == "lower"])
12+
13+
})

0 commit comments

Comments
 (0)