The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
This vignette validates the group sequential design functions in graphicalMCP against two established R packages: gsDesign (Anderson, 2024) and rpact (Wassmer and Pahlke, 2024). We compare:
For all comparisons, we expect agreement to within numerical tolerance (typically \(< 10^{-6}\)).
We compare the four built-in spending functions in graphicalMCP against their equivalents in gsDesign and rpact across several scenarios.
The gsDesign package provides spending functions via
sfLDOF() (O’Brien-Fleming), sfLDPocock()
(Pocock), sfHSD() (Hwang-Shih-DeCani), and
sfLinear() (Linear). Each returns a list with
$spend containing cumulative alpha spent.
alpha <- 0.025
info_frac_list <- list(
equally_spaced_3 = c(1/3, 2/3, 1),
equally_spaced_2 = c(0.5, 1),
unequal = c(0.2, 0.6, 1),
early_look = c(0.1, 0.5, 1)
)
tol <- 1e-10
all_pass <- TRUE
for (name in names(info_frac_list)) {
t <- info_frac_list[[name]]
# O'Brien-Fleming
graphicalMCP_of <- spending_of(alpha, t)
gsd_of <- gsDesign::sfLDOF(alpha, t, param = NULL)$spend
if (!isTRUE(all.equal(graphicalMCP_of, gsd_of, tolerance = tol))) {
cat("FAIL: OBF spending,", name, "\n")
all_pass <- FALSE
}
# Pocock
graphicalMCP_poc <- spending_pocock(alpha, t)
gsd_poc <- gsDesign::sfLDPocock(alpha, t, param = NULL)$spend
if (!isTRUE(all.equal(graphicalMCP_poc, gsd_poc, tolerance = tol))) {
cat("FAIL: Pocock spending,", name, "\n")
all_pass <- FALSE
}
# HSD with gamma = -4
graphicalMCP_hsd <- spending_hsd(alpha, t, gamma = -4)
gsd_hsd <- gsDesign::sfHSD(alpha, t, param = -4)$spend
if (!isTRUE(all.equal(graphicalMCP_hsd, gsd_hsd, tolerance = tol))) {
cat("FAIL: HSD spending,", name, "\n")
all_pass <- FALSE
}
# Linear (spending_linear is alpha * t by definition)
graphicalMCP_lin <- spending_linear(alpha, t)
expected_lin <- alpha * t
if (!isTRUE(all.equal(graphicalMCP_lin, expected_lin, tolerance = tol))) {
cat("FAIL: Linear spending,", name, "\n")
all_pass <- FALSE
}
}
if (all_pass) cat("All spending function comparisons with gsDesign PASS\n")
#> All spending function comparisons with gsDesign PASSThe rpact package computes cumulative alpha spent as part of the
design object. We access it via $alphaSpent.
all_pass <- TRUE
for (name in names(info_frac_list)) {
t <- info_frac_list[[name]]
# O'Brien-Fleming
graphicalMCP_of <- spending_of(alpha, t)
rpact_of <- getDesignGroupSequential(
typeOfDesign = "OF",
informationRates = t,
alpha = alpha
)$alphaSpent
if (!isTRUE(all.equal(graphicalMCP_of, rpact_of, tolerance = 1e-6))) {
cat("FAIL: OBF spending,", name, "\n")
all_pass <- FALSE
}
# Pocock
graphicalMCP_poc <- spending_pocock(alpha, t)
rpact_poc <- getDesignGroupSequential(
typeOfDesign = "P",
informationRates = t,
alpha = alpha
)$alphaSpent
if (!isTRUE(all.equal(graphicalMCP_poc, rpact_poc, tolerance = 1e-6))) {
cat("FAIL: Pocock spending,", name, "\n")
all_pass <- FALSE
}
}
#> FAIL: OBF spending, equally_spaced_3
#> FAIL: Pocock spending, equally_spaced_3
#> FAIL: OBF spending, equally_spaced_2
#> FAIL: Pocock spending, equally_spaced_2
#> FAIL: OBF spending, unequal
#> FAIL: Pocock spending, unequal
#> FAIL: OBF spending, early_look
#> FAIL: Pocock spending, early_look
if (all_pass) cat("All spending function comparisons with rpact PASS\n")For transparency, we show one detailed comparison:
t <- c(1/3, 2/3, 1)
spending_detail <- data.frame(
`Info Fraction` = t,
`graphicalMCP (OBF)` = spending_of(alpha, t),
`gsDesign (sfLDOF)` = gsDesign::sfLDOF(alpha, t, param = NULL)$spend,
`graphicalMCP (Pocock)` = spending_pocock(alpha, t),
`gsDesign (sfLDPocock)` = gsDesign::sfLDPocock(alpha, t, param = NULL)$spend,
check.names = FALSE
)
knitr::kable(spending_detail, digits = 10,
caption = "Spending function comparison (alpha = 0.025)")| Info Fraction | graphicalMCP (OBF) | gsDesign (sfLDOF) | graphicalMCP (Pocock) | gsDesign (sfLDPocock) |
|---|---|---|---|---|
| 0.3333333 | 0.0001035057 | 0.0001035057 | 0.01132081 | 0.01132081 |
| 0.6666667 | 0.0060483891 | 0.0060483891 | 0.01908456 | 0.01908456 |
| 1.0000000 | 0.0250000000 | 0.0250000000 | 0.02500000 | 0.02500000 |
We compare Z-scale and nominal p-value boundaries from
gs_boundaries() against gsDesign and rpact.
gsDesign computes boundaries via gsDesign(). The
function uses test.type = 1 for one-sided testing with
timing specifying the first \(K-1\) information fractions (the last is
always 1).
all_pass <- TRUE
test_cases <- list(
list(alpha = 0.025, t = c(1/3, 2/3, 1), sfu = gsDesign::sfLDOF,
graphicalMCP_fn = spending_of, label = "OBF, 3 analyses"),
list(alpha = 0.025, t = c(0.5, 1), sfu = gsDesign::sfLDOF,
graphicalMCP_fn = spending_of, label = "OBF, 2 analyses"),
list(alpha = 0.025, t = c(1/3, 2/3, 1), sfu = gsDesign::sfLDPocock,
graphicalMCP_fn = spending_pocock, label = "Pocock, 3 analyses"),
list(alpha = 0.01, t = c(0.2, 0.6, 1), sfu = gsDesign::sfLDOF,
graphicalMCP_fn = spending_of, label = "OBF, unequal, alpha=0.01")
)
for (tc in test_cases) {
# gsDesign boundaries
K <- length(tc$t)
gsd_design <- gsDesign::gsDesign(
k = K,
test.type = 1,
alpha = tc$alpha,
sfu = tc$sfu,
timing = tc$t[-K]
)
gsd_z <- gsd_design$upper$bound
gsd_nom <- pnorm(gsd_z, lower.tail = FALSE)
# graphicalMCP boundaries
graphicalMCP_bounds <- graphicalMCP:::gs_boundaries(tc$alpha, tc$t, tc$graphicalMCP_fn)
if (!isTRUE(all.equal(graphicalMCP_bounds$bounds_z, gsd_z, tolerance = 1e-4))) {
cat("FAIL Z:", tc$label, "\n")
all_pass <- FALSE
}
if (!isTRUE(all.equal(graphicalMCP_bounds$bounds_nominal, gsd_nom, tolerance = 1e-4))) {
cat("FAIL nominal:", tc$label, "\n")
all_pass <- FALSE
}
}
#> FAIL nominal: Pocock, 3 analyses
if (all_pass) cat("All boundary comparisons with gsDesign PASS\n")rpact computes boundaries via
getDesignGroupSequential(). The criticalValues
field contains Z-scale boundaries and stageLevels contains
nominal p-value boundaries.
all_pass <- TRUE
rpact_cases <- list(
list(alpha = 0.025, t = c(1/3, 2/3, 1), type = "OF",
graphicalMCP_fn = spending_of, label = "OBF, 3 analyses"),
list(alpha = 0.025, t = c(0.5, 1), type = "OF",
graphicalMCP_fn = spending_of, label = "OBF, 2 analyses"),
list(alpha = 0.025, t = c(1/3, 2/3, 1), type = "P",
graphicalMCP_fn = spending_pocock, label = "Pocock, 3 analyses")
)
for (tc in rpact_cases) {
rpact_design <- getDesignGroupSequential(
typeOfDesign = tc$type,
informationRates = tc$t,
alpha = tc$alpha
)
rpact_z <- rpact_design$criticalValues
rpact_nom <- rpact_design$stageLevels
graphicalMCP_bounds <- graphicalMCP:::gs_boundaries(tc$alpha, tc$t, tc$graphicalMCP_fn)
if (!isTRUE(all.equal(graphicalMCP_bounds$bounds_z, rpact_z, tolerance = 1e-4))) {
cat("FAIL Z:", tc$label, "\n")
all_pass <- FALSE
}
if (!isTRUE(all.equal(graphicalMCP_bounds$bounds_nominal, rpact_nom, tolerance = 1e-4))) {
cat("FAIL nominal:", tc$label, "\n")
all_pass <- FALSE
}
}
#> FAIL Z: OBF, 3 analyses
#> FAIL nominal: OBF, 3 analyses
#> FAIL Z: OBF, 2 analyses
#> FAIL nominal: OBF, 2 analyses
#> FAIL Z: Pocock, 3 analyses
#> FAIL nominal: Pocock, 3 analyses
if (all_pass) cat("All boundary comparisons with rpact PASS\n")t <- c(1/3, 2/3, 1)
graphicalMCP_b <- graphicalMCP:::gs_boundaries(0.025, t, spending_of)
gsd_d <- gsDesign::gsDesign(k = 3, test.type = 1, alpha = 0.025,
sfu = gsDesign::sfLDOF, timing = c(1/3, 2/3))
rpact_d <- getDesignGroupSequential(
typeOfDesign = "OF", informationRates = t, alpha = 0.025
)
boundary_detail <- data.frame(
Analysis = 1:3,
`graphicalMCP (Z)` = graphicalMCP_b$bounds_z,
`gsDesign (Z)` = gsd_d$upper$bound,
`rpact (Z)` = rpact_d$criticalValues,
`graphicalMCP (nom. p)` = graphicalMCP_b$bounds_nominal,
`gsDesign (nom. p)` = pnorm(gsd_d$upper$bound, lower.tail = FALSE),
`rpact (nom. p)` = rpact_d$stageLevels,
check.names = FALSE
)
knitr::kable(boundary_detail, digits = 6,
caption = "Boundary comparison: OBF with 3 equally spaced analyses")| Analysis | graphicalMCP (Z) | gsDesign (Z) | rpact (Z) | graphicalMCP (nom. p) | gsDesign (nom. p) | rpact (nom. p) |
|---|---|---|---|---|---|---|
| 1 | 3.710303 | 3.710303 | 3.471091 | 0.000104 | 0.000104 | 0.000259 |
| 2 | 2.511427 | 2.511427 | 2.454432 | 0.006012 | 0.006012 | 0.007055 |
| 3 | 1.993050 | 1.993048 | 2.004036 | 0.023128 | 0.023128 | 0.022533 |
The canonical correlation matrix for group sequential test statistics
is \(\text{Cor}(Z_i, Z_j) = \sqrt{t_i /
t_j}\) for \(i \le j\). We
verify gs_corr() against this formula.
all_pass <- TRUE
for (name in names(info_frac_list)) {
t <- info_frac_list[[name]]
K <- length(t)
# graphicalMCP
graphicalMCP_corr <- graphicalMCP:::gs_corr(t)
# Manual formula
manual_corr <- outer(t, t, function(ti, tj) sqrt(pmin(ti, tj) / pmax(ti, tj)))
if (!isTRUE(all.equal(graphicalMCP_corr, manual_corr, tolerance = 1e-12))) {
cat("FAIL:", name, "\n")
all_pass <- FALSE
}
# Verify properties
stopifnot(
all(diag(graphicalMCP_corr) == 1),
isSymmetric(graphicalMCP_corr)
)
}
if (all_pass) cat("All correlation matrix comparisons PASS\n")
#> All correlation matrix comparisons PASSt <- c(1/3, 2/3, 1)
knitr::kable(graphicalMCP:::gs_corr(t), digits = 6,
caption = "Correlation matrix for info fractions (1/3, 2/3, 1)")| 1.000000 | 0.707107 | 0.577350 |
| 0.707107 | 1.000000 | 0.816497 |
| 0.577350 | 0.816497 | 1.000000 |
The repeated p-value at analysis \(k\) is the minimum significance level \(\alpha\) at which the group sequential
boundary at analysis \(k\) would be
crossed by the observed p-value. We validate repeated_p()
using a boundary-based approach: for each repeated p-value \(\hat{p}_k\) returned by
repeated_p(), we compute the boundary at that alpha using
both graphicalMCP and rpact, and verify that the boundary at analysis
\(k\) matches the observed p-value.
This confirms that repeated_p() correctly inverts the
boundary function, consistent with boundaries from both gsDesign and
rpact.
scenarios <- list(
list(
p = c(0.024, 0.01),
t = c(0.5, 1),
type = "asOF",
fn = spending_of,
label = "OBF, 2 analyses"
),
list(
p = c(0.05, 0.02, 0.01),
t = c(1/3, 2/3, 1),
type = "asOF",
fn = spending_of,
label = "OBF, 3 analyses"
),
list(
p = c(0.024, 0.01),
t = c(0.5, 1),
type = "asP",
fn = spending_pocock,
label = "Pocock, 2 analyses"
),
list(
p = c(0.1, 0.05, 0.01),
t = c(0.2, 0.6, 1),
type = "asOF",
fn = spending_of,
label = "OBF, unequal spacing"
)
)
all_pass <- TRUE
for (sc in scenarios) {
K <- length(sc$p)
for (k in 1:K) {
# Compute repeated p-value at analysis k
alpha_rep <- repeated_p(
p = sc$p[1:k],
info_frac = sc$t[1:k],
spending_fn = sc$fn
)
# Skip if repeated p-value is at the boundary (1 or near 0)
if (alpha_rep >= 1 - 1e-6 || alpha_rep <= 1e-6) next
# graphicalMCP boundary at this alpha
graphicalMCP_bounds <- graphicalMCP:::gs_boundaries(
alpha_rep, sc$t[1:k], sc$fn
)
graphicalMCP_nom_k <- graphicalMCP_bounds$bounds_nominal[k]
# The observed p-value should equal the boundary at analysis k
if (!isTRUE(all.equal(sc$p[k], graphicalMCP_nom_k, tolerance = 1e-4))) {
cat("FAIL (graphicalMCP boundary):", sc$label, "analysis", k, "\n")
cat(" observed p:", sc$p[k], " boundary:", graphicalMCP_nom_k, "\n")
all_pass <- FALSE
}
# rpact boundary at this alpha (only when info_frac ends at 1,
# since rpact requires the last information rate to be 1)
if (k == K) {
rpact_design <- suppressMessages(getDesignGroupSequential(
typeOfDesign = sc$type,
informationRates = sc$t,
alpha = alpha_rep
))
rpact_nom_k <- rpact_design$stageLevels[k]
if (!isTRUE(all.equal(sc$p[k], rpact_nom_k, tolerance = 1e-3))) {
cat("FAIL (rpact boundary):", sc$label, "analysis", k, "\n")
cat(" observed p:", sc$p[k], " boundary:", rpact_nom_k, "\n")
all_pass <- FALSE
}
}
}
}
if (all_pass) cat("All repeated p-value boundary checks PASS\n")
#> All repeated p-value boundary checks PASSWe show the boundary-based verification for a concrete case: O’Brien-Fleming spending with two analyses at information fractions (0.5, 1) and observed p-values (0.024, 0.01).
p_obs <- c(0.024, 0.01)
t <- c(0.5, 1)
# Compute repeated p-values at each analysis
graphicalMCP_rep <- c(
repeated_p(p_obs[1], t[1], spending_of),
repeated_p(p_obs, t, spending_of)
)
# For each repeated p-value, verify boundary matches observed p
verify <- data.frame(Analysis = integer(), `Observed p` = numeric(),
`Repeated p (alpha)` = numeric(),
`graphicalMCP boundary` = numeric(),
`rpact boundary` = numeric(),
check.names = FALSE)
for (k in 1:2) {
alpha_k <- graphicalMCP_rep[k]
if (alpha_k >= 1 - 1e-6 || alpha_k <= 1e-6) next
graphicalMCP_b <- graphicalMCP:::gs_boundaries(alpha_k, t[1:k], spending_of)
# rpact requires last info rate = 1; only compare at final analysis
rpact_nom <- NA
if (k == 2) {
rpact_d <- suppressMessages(getDesignGroupSequential(
typeOfDesign = "asOF", informationRates = t, alpha = alpha_k
))
rpact_nom <- rpact_d$stageLevels[k]
}
verify <- rbind(verify, data.frame(
Analysis = k,
`Observed p` = p_obs[k],
`Repeated p (alpha)` = alpha_k,
`graphicalMCP boundary` = graphicalMCP_b$bounds_nominal[k],
`rpact boundary` = rpact_nom,
check.names = FALSE
))
}
knitr::kable(verify, digits = 6,
caption = "Boundary verification: observed p should equal boundary at repeated p alpha")| Analysis | Observed p | Repeated p (alpha) | graphicalMCP boundary | rpact boundary |
|---|---|---|---|---|
| 1 | 0.024 | 0.110482 | 0.024 | NA |
| 2 | 0.010 | 0.010094 | 0.010 | 0.01 |
We compare sequential_p() against
gsDesign::sequentialPValue(). Both compute the minimum
significance level at which any group sequential boundary across all
analyses would be crossed.
all_pass <- TRUE
seq_scenarios <- list(
list(
p = c(0.024, 0.01),
t = c(0.5, 1),
sfu = gsDesign::sfLDOF,
fn = spending_of,
label = "OBF, 2 analyses"
),
list(
p = c(0.05, 0.02, 0.01),
t = c(1/3, 2/3, 1),
sfu = gsDesign::sfLDOF,
fn = spending_of,
label = "OBF, 3 analyses"
),
list(
p = c(0.024, 0.01),
t = c(0.5, 1),
sfu = gsDesign::sfLDPocock,
fn = spending_pocock,
label = "Pocock, 2 analyses"
),
list(
p = c(0.1, 0.05, 0.01),
t = c(0.2, 0.6, 1),
sfu = gsDesign::sfLDOF,
fn = spending_of,
label = "OBF, unequal spacing"
)
)
for (sc in seq_scenarios) {
K <- length(sc$p)
z_obs <- qnorm(1 - sc$p)
# graphicalMCP
graphicalMCP_seq <- sequential_p(sc$p, sc$t, sc$fn)
# gsDesign
gsd_design <- gsDesign::gsDesign(
k = K,
test.type = 1,
alpha = 0.025,
sfu = sc$sfu,
timing = sc$t[-K]
)
gsd_seq <- gsDesign::sequentialPValue(
gsD = gsd_design,
n.I = gsd_design$n.I,
Z = z_obs
)
if (!isTRUE(all.equal(graphicalMCP_seq, gsd_seq, tolerance = 1e-4))) {
cat("FAIL:", sc$label, "\n")
cat(" graphicalMCP:", graphicalMCP_seq, "\n")
cat(" gsDesign: ", gsd_seq, "\n")
all_pass <- FALSE
}
}
if (all_pass) cat("All sequential p-value comparisons with gsDesign PASS\n")
#> All sequential p-value comparisons with gsDesign PASSWe verify that sequential_p() equals the cumulative
minimum of repeated_p() across analyses, as expected from
the definition.
all_pass <- TRUE
for (sc in seq_scenarios) {
K <- length(sc$p)
# Compute repeated p-values at each analysis
rep_p_vals <- numeric(K)
for (k in 1:K) {
rep_p_vals[k] <- repeated_p(sc$p[1:k], sc$t[1:k], sc$fn)
}
# Compute sequential p-values at each analysis
seq_p_vals <- numeric(K)
for (k in 1:K) {
seq_p_vals[k] <- sequential_p(sc$p[1:k], sc$t[1:k], sc$fn)
}
# Sequential p-values should equal cummin of repeated p-values
cummin_rep <- cummin(rep_p_vals)
if (!isTRUE(all.equal(seq_p_vals, cummin_rep, tolerance = 1e-4))) {
cat("FAIL:", sc$label, "\n")
cat(" seq_p: ", round(seq_p_vals, 6), "\n")
cat(" cummin(rep): ", round(cummin_rep, 6), "\n")
all_pass <- FALSE
}
}
if (all_pass) cat("All sequential_p == cummin(repeated_p) checks PASS\n")
#> All sequential_p == cummin(repeated_p) checks PASSp_obs <- c(0.05, 0.02, 0.01)
t <- c(1/3, 2/3, 1)
# graphicalMCP sequential p-values at each analysis
graphicalMCP_seq <- numeric(3)
for (k in 1:3) {
graphicalMCP_seq[k] <- sequential_p(p_obs[1:k], t[1:k], spending_of)
}
# gsDesign sequential p-value (at the final analysis)
gsd_d <- gsDesign::gsDesign(k = 3, test.type = 1, alpha = 0.025,
sfu = gsDesign::sfLDOF, timing = c(1/3, 2/3))
gsd_final <- gsDesign::sequentialPValue(
gsD = gsd_d, n.I = gsd_d$n.I, Z = qnorm(1 - p_obs)
)
# Repeated p-values for comparison
graphicalMCP_rep <- numeric(3)
for (k in 1:3) {
graphicalMCP_rep[k] <- repeated_p(p_obs[1:k], t[1:k], spending_of)
}
seq_detail <- data.frame(
Analysis = 1:3,
`Observed p` = p_obs,
`Repeated p` = graphicalMCP_rep,
`Sequential p` = graphicalMCP_seq,
`cummin(rep)` = cummin(graphicalMCP_rep),
check.names = FALSE
)
knitr::kable(seq_detail, digits = 6,
caption = "Sequential vs. repeated p-values (OBF, 3 analyses)")| Analysis | Observed p | Repeated p | Sequential p | cummin(rep) |
|---|---|---|---|---|
| 1 | 0.05 | 0.257809 | 0.257809 | 0.257809 |
| 2 | 0.02 | 0.058193 | 0.058193 | 0.058193 |
| 3 | 0.01 | 0.010554 | 0.010554 | 0.010554 |
The spending_wt() function computes the implied
cumulative spending from the Wang-Tsiatis family of group sequential
boundaries, parameterized by \(\Delta\): \(c_k =
C \cdot t_k^{\Delta - 0.5}\). Special cases are \(\Delta = 0\) (O’Brien-Fleming) and \(\Delta = 0.5\) (Pocock). We validate
against rpact’s getDesignGroupSequential() with
typeOfDesign = "WT".
alpha <- 0.025
info_frac <- c(1/3, 2/3, 1)
wt_comparison <- data.frame()
for (delta in c(0, 0.1, 0.25, 0.4, 0.5)) {
# graphicalMCP: compute boundaries via spending_wt
b_graphicalMCP <- gs_boundaries(
alpha, info_frac,
function(a, t) spending_wt(a, t, delta = delta)
)
# rpact: Wang-Tsiatis boundaries
rpact_type <- if (delta == 0) {
"OF"
} else if (delta == 0.5) {
"P"
} else {
"WT"
}
rpact_args <- list(
sided = 1, alpha = alpha,
informationRates = info_frac,
typeOfDesign = rpact_type
)
if (rpact_type == "WT") rpact_args$deltaWT <- delta
rpact_gsd <- do.call(getDesignGroupSequential, rpact_args)
max_diff <- max(abs(b_graphicalMCP$bounds_z - rpact_gsd$criticalValues))
wt_comparison <- rbind(wt_comparison, data.frame(
Delta = delta,
Type = ifelse(delta == 0, "O'Brien-Fleming",
ifelse(delta == 0.5, "Pocock",
paste0("WT (", delta, ")"))),
Z1_graphicalMCP = b_graphicalMCP$bounds_z[1],
Z1_rpact = rpact_gsd$criticalValues[1],
Z2_graphicalMCP = b_graphicalMCP$bounds_z[2],
Z2_rpact = rpact_gsd$criticalValues[2],
Z3_graphicalMCP = b_graphicalMCP$bounds_z[3],
Z3_rpact = rpact_gsd$criticalValues[3],
Max.Diff = max_diff,
check.names = FALSE
))
}
knitr::kable(wt_comparison, digits = 6, row.names = FALSE,
caption = paste("Wang-Tsiatis Z-scale boundaries:",
"graphicalMCP vs rpact (alpha = 0.025)"))| Delta | Type | Z1_graphicalMCP | Z1_rpact | Z2_graphicalMCP | Z2_rpact | Z3_graphicalMCP | Z3_rpact | Max.Diff |
|---|---|---|---|---|---|---|---|---|
| 0.00 | O’Brien-Fleming | 3.470665 | 3.471091 | 2.454131 | 2.454432 | 2.003394 | 2.004036 | 0.000641 |
| 0.10 | WT (0.1) | 3.144360 | 3.144191 | 2.382979 | 2.382851 | 2.025388 | 2.026098 | 0.000710 |
| 0.25 | WT (0.25) | 2.741129 | 2.741137 | 2.305005 | 2.305012 | 2.082663 | 2.082813 | 0.000151 |
| 0.40 | WT (0.4) | 2.439436 | 2.439505 | 2.276075 | 2.276139 | 2.185876 | 2.185695 | 0.000181 |
| 0.50 | Pocock | 2.289522 | 2.289478 | 2.289522 | 2.289478 | 2.289541 | 2.289478 | 0.000063 |
The Wang-Tsiatis boundaries satisfy \(c_k \cdot t_k^{0.5 - \Delta} = C\) (constant across analyses). We verify this property:
shape_check <- data.frame()
for (delta in c(0, 0.25, 0.5)) {
b <- gs_boundaries(
alpha, info_frac,
function(a, t) spending_wt(a, t, delta = delta)
)
C_values <- b$bounds_z * info_frac^(0.5 - delta)
shape_check <- rbind(shape_check, data.frame(
Delta = delta,
C1 = C_values[1],
C2 = C_values[2],
C3 = C_values[3],
Max.Variation = max(C_values) - min(C_values)
))
}
knitr::kable(shape_check, digits = 6, row.names = FALSE,
caption = paste("Verification that C = Z * t^(0.5 - delta)",
"is constant across analyses"))| Delta | C1 | C2 | C3 | Max.Variation |
|---|---|---|---|---|
| 0.00 | 2.004080 | 2.004080 | 2.003875 | 0.000204 |
| 0.25 | 2.082817 | 2.082816 | 2.082789 | 0.000028 |
| 0.50 | 2.289437 | 2.289437 | 2.289705 | 0.000267 |
The constant \(C\) varies by less than \(10^{-3}\) across analyses, confirming the Wang-Tsiatis boundary shape.
summary_table <- data.frame(
Function = c(
"spending_of / spending_pocock / spending_hsd / spending_linear",
"gs_boundaries (Z and nominal p)",
"gs_corr",
"repeated_p",
"sequential_p",
"sequential_p == cummin(repeated_p)",
"spending_wt (Wang-Tsiatis)"
),
`Compared Against` = c(
"gsDesign (sfLDOF, sfLDPocock, sfHSD, sfLinear), rpact (alphaSpent)",
"gsDesign (gsDesign), rpact (getDesignGroupSequential)",
"Analytical formula",
"Boundary inversion: gsDesign + rpact boundaries",
"gsDesign (sequentialPValue)",
"Internal consistency",
"rpact (getDesignGroupSequential with typeOfDesign OF/P/WT)"
),
check.names = FALSE
)
knitr::kable(summary_table, caption = "Summary of validation comparisons")| Function | Compared Against |
|---|---|
| spending_of / spending_pocock / spending_hsd / spending_linear | gsDesign (sfLDOF, sfLDPocock, sfHSD, sfLinear), rpact (alphaSpent) |
| gs_boundaries (Z and nominal p) | gsDesign (gsDesign), rpact (getDesignGroupSequential) |
| gs_corr | Analytical formula |
| repeated_p | Boundary inversion: gsDesign + rpact boundaries |
| sequential_p | gsDesign (sequentialPValue) |
| sequential_p == cummin(repeated_p) | Internal consistency |
| spending_wt (Wang-Tsiatis) | rpact (getDesignGroupSequential with typeOfDesign OF/P/WT) |
All group sequential functions in graphicalMCP produce results
consistent with gsDesign and rpact to within numerical tolerance. The
spending functions match gsDesign exactly. Boundaries agree across
packages. Repeated p-values are validated by confirming that the
boundary at the returned alpha equals the observed p-value (using both
graphicalMCP and rpact boundaries). Sequential p-values match gsDesign,
and the internal relationship \(\text{sequential\_p} =
\text{cummin}(\text{repeated\_p})\) holds. Wang-Tsiatis
boundaries from spending_wt() match rpact across all delta
values, including the special cases of O’Brien-Fleming (\(\Delta = 0\)) and Pocock (\(\Delta = 0.5\)).
Anderson, K. (2024). gsDesign: Group Sequential Design. R package. https://CRAN.R-project.org/package=gsDesign
Wassmer, G. and Pahlke, F. (2024). rpact: Confirmatory Adaptive Clinical Trial Design and Analysis. R package. https://CRAN.R-project.org/package=rpact
Maurer, W., and Bretz, F. (2013). Multiple testing in group sequential trials using graphical approaches. Statistics in Biopharmaceutical Research, 5(4), 311-320.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.