Skip to content

Commit 45a948d

Browse files
committed
Updated group-sequential-testing.rmd
1 parent 5aa2984 commit 45a948d

1 file changed

Lines changed: 115 additions & 54 deletions

File tree

vignettes/group-sequential-testing.Rmd

Lines changed: 115 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,11 @@ if (requireNamespace("gsDesign", quietly = TRUE)) {
660660

661661
Any of `gsDesign`'s spending functions can be wrapped this way. For spending
662662
functions with additional parameters (like `sfHSD`), simply bind the
663-
parameter in the wrapper as shown above.
663+
parameter in the wrapper as shown above. A more advanced use of custom
664+
spending functions — including the separation of *spending time* from
665+
*information fraction* — is illustrated in the
666+
[Customizing Spending Functions: Spending Time] section of the oncology
667+
case study below.
664668

665669
**rpact.** The `rpact` package computes group sequential designs via
666670
`getDesignGroupSequential()` but does not expose standalone spending
@@ -762,12 +766,12 @@ The transition structure follows the hierarchy: within each population, alpha
762766
flows from OS to PFS to ORR, and ORR recycles to OS. Between populations,
763767
the all-subjects hypotheses share alpha with the subgroup hypotheses.
764768

765-
```{r oncology-graph-plot, eval = requireNamespace("igraph", quietly = TRUE), fig.height=6, fig.width=6}
769+
```{r oncology-graph-plot, eval = requireNamespace("igraph", quietly = TRUE), fig.height=6, fig.width=7}
766770
onc_layout <- rbind(
767771
c(0, 3), # H1_OS_S
768772
c(2, 3), # H2_OS_A
769773
c(0, 1.8), # H3_PFS_S
770-
c(1.3, 1.8), # H4_PFS_A
774+
c(3, 1.8), # H4_PFS_A
771775
c(0, 0.5), # H5_ORR_S
772776
c(2, 0.5) # H6_ORR_A
773777
)
@@ -776,21 +780,20 @@ onc_layout <- rbind(
776780
# 6=H3->H4, 7=H4->H5, 8=H4->H6, 9=H5->H6
777781
label_x <- rep(NA, 9)
778782
label_y <- rep(NA, 9)
779-
label_x[1] <- 1.35; label_y[1] <- 1.1 # H6->H1: on the curved edge
780-
label_x[7] <- 0.65; label_y[7] <- 1.1 # H4->H5: between nodes, near arrow
783+
label_x[1] <- 0.4; label_y[1] <- 2.5 # H6->H1: toward arrow (H1)
784+
label_x[3] <- 2.0; label_y[3] <- 2.375 # H6->H2: on the edge, toward arrow
785+
label_x[4] <- 1.5; label_y[4] <- 2.7 # H2->H3: toward tail (H2)
786+
label_x[6] <- 0.75; label_y[6] <- 1.8 # H3->H4: toward tail (H3)
787+
label_x[7] <- 0.9; label_y[7] <- 0.89 # H4->H5: toward arrow (H5)
788+
label_x[8] <- 2.5; label_y[8] <- 1.15 # H4->H6: on the edge, midway
781789
782790
plot(g_onc, layout = onc_layout, vertex.size = 60, asp = 1,
783791
vertex.label.cex = 0.7,
784792
rescale = FALSE,
785-
xlim = c(-1.2, 3.5),
793+
xlim = c(-0.8, 4.0),
786794
ylim = c(-0.2, 3.8),
787795
edge.label.x = label_x,
788-
edge.label.y = label_y,
789-
edge_curves = c("H6_ORR_A|H2_OS_A" = 0,
790-
"H6_ORR_A|H1_OS_S" = 0.2,
791-
"H4_PFS_A|H6_ORR_A" = 0,
792-
"H4_PFS_A|H5_ORR_S" = 0,
793-
"H3_PFS_S|H4_PFS_A" = 0))
796+
edge.label.y = label_y)
794797
```
795798

796799
### P-values and Information Fractions
@@ -939,62 +942,120 @@ knitr::kable(onc_summary_lb, row.names = FALSE,
939942
caption = "Oncology case study (look_back = TRUE): rejection decisions")
940943
```
941944

942-
### Repeated P-values (look_back = FALSE)
945+
This case study demonstrates that `graph_test_shortcut_gsd()` handles trials
946+
where different endpoints have different numbers of analyses — a common
947+
situation in oncology trials with OS, PFS, and ORR endpoints.
948+
949+
### Customizing Spending Functions: Spending Time
950+
951+
Some group sequential frameworks (e.g., gMCPLite via gsDesign) separate
952+
*spending time* from *information fraction*. The information fraction
953+
determines the correlation structure of the test statistics, while the
954+
spending time determines how alpha is allocated across analyses via the
955+
spending function. The two can differ when, for example, all-subjects
956+
hypotheses use all-subjects event counts for the correlation but subgroup
957+
event counts for spending.
958+
959+
In `graphicalMCP`, the `info_frac` argument is used for both purposes by
960+
default. However, the spending time behavior can be achieved without any
961+
API changes by defining a custom spending function that internally maps
962+
the information fractions to spending times.
963+
964+
Consider the oncology trial above. The all-subjects hypotheses ($H_2$ and
965+
$H_4$) use all-subjects event counts for their information fractions (which
966+
determine the correlation structure), but one might want to use the
967+
corresponding subgroup event counts as the spending time (which determines
968+
how aggressively alpha is spent at each analysis). This is because the
969+
subgroup is a subset of the all-subjects population, and the subgroup events
970+
may better reflect the information available for the treatment effect
971+
comparison.
972+
973+
We define a helper function that creates a spending function with a custom
974+
spending time:
975+
976+
```{r spending-time-helper}
977+
# Factory function: create a spending function that uses spending_time
978+
# instead of info_frac for alpha allocation
979+
make_spending_with_time <- function(base_spending_fn, spending_time) {
980+
function(alpha, info_frac) {
981+
# Use spending_time for alpha allocation, ignoring info_frac
982+
# Truncate to match length (handles interim stops)
983+
st <- spending_time[seq_along(info_frac)]
984+
base_spending_fn(alpha, st)
985+
}
986+
}
987+
```
988+
989+
For the oncology trial, $H_2$ (OS, all subjects) uses all-subjects OS events
990+
(`r paste(c(529, 700, 800), collapse = ", ")`) for the correlation structure
991+
(via `info_frac`), but subgroup OS events
992+
(`r paste(c(185, 245, 295), collapse = ", ")`) for spending:
993+
994+
```{r spending-time-setup}
995+
# Spending time = subgroup event fractions (same as H1_OS_S)
996+
spending_h2 <- make_spending_with_time(
997+
spending_of,
998+
spending_time = c(185 / 295, 245 / 295, 1)
999+
)
1000+
1001+
# Similarly, H4 (PFS, all subjects) uses subgroup PFS event fractions
1002+
spending_h4 <- make_spending_with_time(
1003+
spending_of,
1004+
spending_time = c(265 / 310, 1)
1005+
)
1006+
1007+
# Build per-hypothesis spending function list
1008+
spending_fn_onc <- list(
1009+
spending_of, # H1_OS_S: standard (info_frac = spending time)
1010+
spending_h2, # H2_OS_A: spending time = subgroup OS events
1011+
spending_of, # H3_PFS_S: standard
1012+
spending_h4, # H4_PFS_A: spending time = subgroup PFS events
1013+
spending_of, # H5_ORR_S: standard (single analysis)
1014+
spending_of # H6_ORR_A: standard (single analysis)
1015+
)
1016+
```
9431017

944-
For comparison, we also run the procedure with `look_back = FALSE`, which
945-
uses repeated p-values at each analysis. This is the default mode and does
946-
not look back at evidence from prior analyses:
1018+
Now `info_frac_onc` continues to use all-subjects event counts for $H_2$ and
1019+
$H_4$ (determining the correlation structure), while the custom spending
1020+
functions use subgroup event counts for alpha allocation:
9471021

948-
```{r oncology-run-no-lb}
949-
result_onc <- graph_test_shortcut_gsd(
1022+
```{r spending-time-run}
1023+
result_onc_st <- graph_test_shortcut_gsd(
9501024
graph = g_onc,
9511025
p = p_onc,
9521026
alpha = alpha_onc,
9531027
info_frac = info_frac_onc,
954-
spending_fn = spending_of,
955-
look_back = FALSE
1028+
spending_fn = spending_fn_onc,
1029+
look_back = TRUE
9561030
)
1031+
print(result_onc_st)
9571032
```
9581033

959-
```{r oncology-compare}
960-
onc_comparison <- data.frame(
1034+
```{r spending-time-compare}
1035+
st_comparison <- data.frame(
9611036
Hypothesis = hyp_names_onc,
962-
`Rejected (LB)` = result_onc_lb$outputs$rejected,
963-
`At (LB)` = ifelse(
964-
is.na(result_onc_lb$outputs$rejected_at), "—",
965-
as.character(result_onc_lb$outputs$rejected_at)
966-
),
967-
`Rejected (no LB)` = result_onc$outputs$rejected,
968-
`At (no LB)` = ifelse(
969-
is.na(result_onc$outputs$rejected_at), "—",
970-
as.character(result_onc$outputs$rejected_at)
971-
),
1037+
`Rejected (info fraction)` = result_onc_lb$outputs$rejected,
1038+
`Adj. P (info fraction)` = round(result_onc_lb$outputs$adjusted_p, 6),
1039+
`Rejected (spending time)` = result_onc_st$outputs$rejected,
1040+
`Adj. P (spending time)` = round(result_onc_st$outputs$adjusted_p, 6),
9721041
check.names = FALSE
9731042
)
974-
knitr::kable(onc_comparison, row.names = FALSE,
975-
caption = "Comparison: look_back = TRUE vs. FALSE (oncology case study)")
1043+
knitr::kable(st_comparison, row.names = FALSE,
1044+
caption = "Effect of spending time on rejection decisions")
9761045
```
9771046

978-
For this example, both modes produce the same rejection decisions. This is
979-
because the evidence at the rejection analyses is strong enough that looking
980-
back at earlier analyses does not change the outcome.
981-
982-
**Note on differences from gMCPLite.** This case study is adapted from the
983-
[gMCPLite vignette](https://cran.r-project.org/web/packages/gMCPLite/vignettes/huyett-burnett-example.html).
984-
The rejection decisions (H1, H3, H5 rejected; H2, H4, H6 not rejected) agree
985-
between the two implementations. However, sequential p-values may differ
986-
slightly for some hypotheses. The reason is that gMCPLite (via gsDesign)
987-
separates *spending time* from *information fraction*: for all-subjects
988-
hypotheses (H2 and H4), gMCPLite uses the subgroup event counts as the
989-
spending time while using the all-subjects event counts for the correlation
990-
structure. In contrast, `graphicalMCP` uses `info_frac` for both alpha
991-
spending and the correlation structure. This difference affects the group
992-
sequential boundaries and hence the sequential p-values, but in this example
993-
it does not change which hypotheses are rejected.
994-
995-
This case study demonstrates that `graph_test_shortcut_gsd()` handles trials
996-
where different endpoints have different numbers of analyses — a common
997-
situation in oncology trials with OS, PFS, and ORR endpoints.
1047+
The spending time adjustment affects the sequential p-values for $H_2$ and
1048+
$H_4$ because it changes how alpha is allocated across their analyses. The
1049+
subgroup event fractions are smaller than the all-subjects event fractions at
1050+
early analyses (e.g., `r round(185/295, 3)` vs. `r round(529/800, 3)` at
1051+
analysis 1 for OS), meaning the spending function allocates less alpha to
1052+
early analyses — the boundaries become more conservative at interim analyses
1053+
and more liberal at the final analysis.
1054+
1055+
This approach illustrates a general principle: because `spending_fn` accepts
1056+
any function with the signature `function(alpha, info_frac)`, users can
1057+
encode arbitrary spending behaviors — including spending time separation —
1058+
without requiring changes to the `graph_test_shortcut_gsd()` interface.
9981059

9991060
## Summary
10001061

0 commit comments

Comments
 (0)