Skip to content

Commit c440263

Browse files
committed
updated repeated_p and sequential_p
1 parent 321ae24 commit c440263

6 files changed

Lines changed: 295 additions & 17 deletions

File tree

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,6 @@ export(spending_hsd)
5555
export(spending_linear)
5656
export(spending_of)
5757
export(spending_pocock)
58+
export(spending_with_time)
5859
export(three_doses_two_primary_two_secondary)
5960
export(two_doses_two_primary_two_secondary)

R/print.gsd_graph_report.R

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,21 @@ print.gsd_graph_report <- function(x, ..., precision = 6, indent = 2) {
208208
}
209209
}
210210

211+
# Repeated and sequential p-values (verbose) --------------------------------
212+
if (!is.null(x$boundary_table)) {
213+
section_break("Repeated p-values ($outputs$repeated_p)")
214+
rep_p_display <- x$outputs$repeated_p
215+
rep_p_display[] <- format(rep_p_display, digits = precision)
216+
print(as.data.frame(rep_p_display))
217+
218+
cat("\n")
219+
section_break("Sequential p-values ($outputs$sequential_p)")
220+
seq_p_display <- x$outputs$sequential_p
221+
seq_p_display[] <- format(seq_p_display, digits = precision)
222+
print(as.data.frame(seq_p_display))
223+
cat("\n")
224+
}
225+
211226
# Boundary table (verbose) ---------------------------------------------------
212227
if (!is.null(x$boundary_table)) {
213228
section_break("Boundary table ($boundary_table)")

R/spending_functions.R

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,78 @@ spending_linear <- function(alpha, info_frac) {
141141
)
142142
alpha * info_frac
143143
}
144+
145+
146+
#' Create a spending function with a custom spending time
147+
#'
148+
#' @description
149+
#' Wraps an existing spending function to use a fixed **spending time** instead
150+
#' of the information fractions passed to it at runtime. This separates the
151+
#' alpha allocation schedule (determined by spending time) from the correlation
152+
#' structure (determined by information fractions in
153+
#' [graph_test_shortcut_gsd()]).
154+
#'
155+
#' This is useful in two common scenarios:
156+
#' * **Subgroup analyses**: all-subjects hypotheses use all-subjects event
157+
#' counts for the correlation structure but subgroup event counts for
158+
#' spending (see the spending time section of
159+
#' `vignette("group-sequential-testing")`).
160+
#' * **Monitoring with changed final information**: when the actual total
161+
#' information at the final analysis differs from the planned total, the
162+
#' planned information fractions are used as spending time to preserve
163+
#' boundaries at earlier analyses, while the actual information fractions
164+
#' are used for the correlation structure (see the monitoring section of
165+
#' `vignette("group-sequential-testing")`).
166+
#'
167+
#' @param spending_fn A spending function to wrap. Must accept two arguments:
168+
#' `alpha` (significance level) and `info_frac` (information fraction), and
169+
#' return the cumulative alpha spent.
170+
#' @param spending_time A numeric vector of spending time values. These replace
171+
#' the `info_frac` argument when the wrapped function is called. The vector
172+
#' is truncated to match the length of `info_frac` at runtime, which handles
173+
#' interim analyses where fewer analyses have been conducted.
174+
#'
175+
#' @return A function with the same signature as `spending_fn` —
176+
#' `function(alpha, info_frac)` — that internally uses `spending_time`
177+
#' instead of `info_frac` for alpha allocation.
178+
#'
179+
#' @seealso [spending_of()], [spending_pocock()], [spending_hsd()],
180+
#' [spending_linear()] for built-in spending functions,
181+
#' [graph_test_shortcut_gsd()] for the graphical procedure with group
182+
#' sequential designs.
183+
#'
184+
#' @export
185+
#'
186+
#' @examples
187+
#' # Subgroup spending time: use subgroup event fractions for spending
188+
#' # while info_frac uses all-subjects event fractions for correlation
189+
#' spending_h2 <- spending_with_time(
190+
#' spending_of,
191+
#' spending_time = c(185 / 295, 245 / 295, 1)
192+
#' )
193+
#'
194+
#' # The wrapped function has the standard (alpha, info_frac) signature
195+
#' # but ignores info_frac and uses spending_time internally
196+
#' spending_h2(0.01, c(0.5, 0.8, 1))
197+
#'
198+
#' # Monitoring: use planned info fractions for spending
199+
#' # when actual final information differs from planned
200+
#' spending_monitor <- spending_with_time(
201+
#' spending_of,
202+
#' spending_time = c(0.627, 0.831, 1) # planned
203+
#' )
204+
#' # Call with actual info fractions (for correlation structure)
205+
#' spending_monitor(0.01, c(0.597, 0.790, 1)) # actual
206+
spending_with_time <- function(spending_fn, spending_time) {
207+
stopifnot(
208+
"spending_fn must be a function" = is.function(spending_fn),
209+
"spending_time must be a numeric vector" = is.numeric(spending_time),
210+
"spending_time must be in [0, 1]" =
211+
all(spending_time >= 0 & spending_time <= 1)
212+
)
213+
214+
function(alpha, info_frac) {
215+
st <- spending_time[seq_along(info_frac)]
216+
spending_fn(alpha, st)
217+
}
218+
}

_pkgdown.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ reference:
4545
- repeated_p
4646
- sequential_p
4747
- spending_of
48+
- spending_with_time
4849
- gs_boundaries
4950
- gs_corr
5051
- title: Power simulation

man/spending_with_time.Rd

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

vignettes/group-sequential-testing.Rmd

Lines changed: 132 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -805,21 +805,8 @@ subgroup is a subset of the all-subjects population, and the subgroup events
805805
may better reflect the information available for the treatment effect
806806
comparison.
807807

808-
We define a helper function that creates a spending function with a custom
809-
spending time:
810-
811-
```{r spending-time-helper}
812-
# Factory function: create a spending function that uses spending_time
813-
# instead of info_frac for alpha allocation
814-
make_spending_with_time <- function(base_spending_fn, spending_time) {
815-
function(alpha, info_frac) {
816-
# Use spending_time for alpha allocation, ignoring info_frac
817-
# Truncate to match length (handles interim stops)
818-
st <- spending_time[seq_along(info_frac)]
819-
base_spending_fn(alpha, st)
820-
}
821-
}
822-
```
808+
The `spending_with_time()` function creates a spending function that uses
809+
a fixed spending time instead of the information fractions passed at runtime:
823810

824811
For the oncology trial, $H_2$ (OS, all subjects) uses all-subjects OS events
825812
(`r paste(c(529, 700, 800), collapse = ", ")`) for the correlation structure
@@ -828,13 +815,13 @@ For the oncology trial, $H_2$ (OS, all subjects) uses all-subjects OS events
828815

829816
```{r spending-time-setup}
830817
# Spending time = subgroup event fractions (same as H1_OS_S)
831-
spending_h2 <- make_spending_with_time(
818+
spending_h2 <- spending_with_time(
832819
spending_of,
833820
spending_time = c(185 / 295, 245 / 295, 1)
834821
)
835822
836823
# Similarly, H4 (PFS, all subjects) uses subgroup PFS event fractions
837-
spending_h4 <- make_spending_with_time(
824+
spending_h4 <- spending_with_time(
838825
spending_of,
839826
spending_time = c(265 / 310, 1)
840827
)
@@ -892,6 +879,134 @@ any function with the signature `function(alpha, info_frac)`, users can
892879
encode arbitrary spending behaviors — including spending time separation —
893880
without requiring changes to the `graph_test_shortcut_gsd()` interface.
894881

882+
### Monitoring: Adjusting for Changed Final Information
883+
884+
In practice, the total information (e.g., total number of events) at the
885+
final analysis may differ from what was planned at the design stage. When
886+
this happens, the information fractions at earlier analyses change
887+
retroactively — not because the data changed, but because the denominator
888+
(planned total) is now different. This creates a challenge: the boundaries
889+
at earlier analyses were already computed using the **planned** information
890+
fractions, but the correlation structure at the current analysis should
891+
reflect the **actual** information fractions.
892+
893+
The spending time approach handles this naturally:
894+
895+
- **Spending time**: use the planned information fractions to preserve the
896+
boundaries at analyses 1 and 2 (which have already been applied).
897+
- **Information fraction** (`info_frac`): use the actual information fractions
898+
for the correlation structure, since this reflects the true joint
899+
distribution of the test statistics.
900+
901+
Consider the OS subgroup hypothesis ($H_1$) in the oncology trial. The
902+
trial was designed with a planned maximum of 295 OS events in the subgroup,
903+
giving planned information fractions of
904+
(`r paste(round(c(185, 245, 295) / 295, 3), collapse = ", ")`).
905+
Suppose that by the time of the final analysis, 310 events have been
906+
observed instead of 295. The actual information fractions are now
907+
(`r paste(round(c(185, 245, 310) / 310, 3), collapse = ", ")`):
908+
909+
```{r monitoring-setup}
910+
# Planned info fractions (used for boundaries at analyses 1 and 2)
911+
planned_if_h1 <- c(185 / 295, 245 / 295, 1)
912+
913+
# Actual info fractions (more events than planned at final analysis)
914+
actual_if_h1 <- c(185 / 310, 245 / 310, 1)
915+
916+
cat("Planned:", round(planned_if_h1, 4), "\n")
917+
cat("Actual: ", round(actual_if_h1, 4), "\n")
918+
```
919+
920+
We create a spending function that uses the planned information fractions
921+
for alpha allocation, while the procedure uses the actual information
922+
fractions for the correlation structure:
923+
924+
```{r monitoring-spending}
925+
# Spending function using planned info fractions
926+
spending_h1_monitor <- spending_with_time(
927+
spending_of,
928+
spending_time = planned_if_h1
929+
)
930+
```
931+
932+
To illustrate, we compare the boundaries computed under three scenarios:
933+
934+
1. **Planned**: both spending and correlation use planned info fractions
935+
(the original design).
936+
2. **Naive update**: both spending and correlation use actual info fractions
937+
(ignores that analyses 1 and 2 used planned boundaries).
938+
3. **Correct monitoring**: spending uses planned info fractions, correlation
939+
uses actual info fractions.
940+
941+
```{r monitoring-comparison}
942+
alpha_h1 <- 0.01 # H1's allocated alpha
943+
944+
# Scenario 1: Planned (original design)
945+
bounds_planned <- gs_boundaries(alpha_h1, planned_if_h1, spending_of)
946+
947+
# Scenario 2: Naive update (both use actual)
948+
bounds_naive <- gs_boundaries(alpha_h1, actual_if_h1, spending_of)
949+
950+
# Scenario 3: Correct monitoring (spending=planned, correlation=actual)
951+
bounds_monitor <- gs_boundaries(alpha_h1, actual_if_h1, spending_h1_monitor)
952+
953+
monitor_table <- data.frame(
954+
Analysis = 1:3,
955+
Planned.IF = round(planned_if_h1, 4),
956+
Actual.IF = round(actual_if_h1, 4),
957+
Boundary.Planned = bounds_planned$bounds_nominal,
958+
Boundary.Naive = bounds_naive$bounds_nominal,
959+
Boundary.Monitor = bounds_monitor$bounds_nominal
960+
)
961+
knitr::kable(monitor_table, digits = 6,
962+
caption = paste("Boundaries under three scenarios",
963+
"(H1 with alpha = 0.01)"))
964+
```
965+
966+
The key observations:
967+
968+
- **Analyses 1 and 2**: the monitoring boundaries differ from the planned
969+
boundaries because the correlation structure has changed (the actual
970+
info fractions are smaller). However, the spending at these analyses is
971+
preserved — the same cumulative alpha is allocated.
972+
- **Analysis 3**: the monitoring boundary reflects both the preserved
973+
spending schedule and the updated correlation structure.
974+
- **Naive update**: changes the spending at all analyses, which is
975+
inconsistent with the boundaries already applied at analyses 1 and 2.
976+
977+
This approach extends naturally to the full graphical procedure. For
978+
monitoring at analysis 3, use the actual information fractions in
979+
`info_frac` and per-hypothesis spending functions with planned information
980+
fractions:
981+
982+
```{r monitoring-full}
983+
# Actual info fractions for all hypotheses
984+
# (only OS hypotheses affected; PFS and ORR are complete)
985+
actual_if_onc <- info_frac_onc
986+
actual_if_onc["H1_OS_S", ] <- c(185 / 310, 245 / 310, 1)
987+
actual_if_onc["H2_OS_A", ] <- c(529 / 830, 700 / 830, 1)
988+
989+
# Spending functions using planned info fractions for OS hypotheses
990+
spending_fn_monitor <- list(
991+
spending_with_time(spending_of, info_frac_onc["H1_OS_S", ]),
992+
spending_with_time(spending_of, info_frac_onc["H2_OS_A", ]),
993+
spending_of, # H3: PFS complete, no change
994+
spending_of, # H4: PFS complete, no change
995+
spending_of, # H5: ORR complete
996+
spending_of # H6: ORR complete
997+
)
998+
999+
result_monitor <- graph_test_shortcut_gsd(
1000+
graph = g_onc,
1001+
p = p_onc,
1002+
alpha = alpha_onc,
1003+
info_frac = actual_if_onc,
1004+
spending_fn = spending_fn_monitor,
1005+
look_back = TRUE
1006+
)
1007+
print(result_monitor)
1008+
```
1009+
8951010
## Additional Examples
8961011

8971012
### Different Spending Functions per Hypothesis

0 commit comments

Comments
 (0)