@@ -660,7 +660,11 @@ if (requireNamespace("gsDesign", quietly = TRUE)) {
660660
661661Any of ` gsDesign ` 's spending functions can be wrapped this way. For spending
662662functions 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
762766flows from OS to PFS to ORR, and ORR recycles to OS. Between populations,
763767the 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 }
766770onc_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
777781label_x <- rep(NA, 9)
778782label_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
782790plot(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