Skip to content

Commit ddf3071

Browse files
committed
spending time correction
1 parent 0d3beed commit ddf3071

2 files changed

Lines changed: 140 additions & 63 deletions

File tree

R/spending_functions.R

Lines changed: 87 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -154,30 +154,36 @@ spending_linear <- function(alpha, info_frac) {
154154
#'
155155
#' @description
156156
#' Wraps an existing spending function to use a fixed **spending time** instead
157-
#' of the information fractions passed to it at runtime. This separates the
158-
#' alpha allocation schedule (determined by spending time) from the correlation
159-
#' structure (determined by information fractions in
160-
#' [graph_test_shortcut_gsd()]).
157+
#' of the information fractions passed to it at runtime. This controls only
158+
#' the alpha allocation schedule. The correlation structure of the test
159+
#' statistics is determined separately by the `info_frac` argument in
160+
#' [graph_test_shortcut_gsd()] (via [gs_corr()]), not by the spending
161+
#' function.
161162
#'
162163
#' This is useful in two common scenarios:
163-
#' * **Subgroup analyses**: all-subjects hypotheses use all-subjects event
164-
#' counts for the correlation structure but subgroup event counts for
165-
#' spending (see the spending time section of
166-
#' `vignette("group-sequential-testing")`).
164+
#' * **Subgroup analyses**: all-subjects hypotheses use subgroup event
165+
#' fractions as spending time (controlling how alpha is allocated across
166+
#' analyses), while `info_frac` in [graph_test_shortcut_gsd()] uses
167+
#' all-subjects event fractions (controlling the correlation structure).
167168
#' * **Monitoring with changed final information**: when the actual total
168169
#' information at the final analysis differs from the planned total, the
169170
#' planned information fractions are used as spending time to preserve
170-
#' boundaries at earlier analyses, while the actual information fractions
171-
#' are used for the correlation structure (see the monitoring section of
172-
#' `vignette("group-sequential-testing")`).
171+
#' the alpha allocation at earlier analyses, while `info_frac` in
172+
#' [graph_test_shortcut_gsd()] uses the actual information fractions
173+
#' for the correlation structure.
173174
#'
174175
#' @param spending_fn A spending function to wrap. Must accept two arguments:
175176
#' `alpha` (significance level) and `info_frac` (information fraction), and
176177
#' return the cumulative alpha spent.
177178
#' @param spending_time A numeric vector of spending time values. These replace
178-
#' the `info_frac` argument when the wrapped function is called. The vector
179-
#' is truncated to match the length of `info_frac` at runtime, which handles
180-
#' interim analyses where fewer analyses have been conducted.
179+
#' the `info_frac` argument when the wrapped function is called. May contain
180+
#' `NA` for analyses that are skipped (e.g., a hypothesis not tested at a
181+
#' particular analysis). The last non-`NA` value should be 1 if the final
182+
#' analysis has been specified.
183+
#' @param info_frac An optional numeric vector of information fractions with
184+
#' the same length as `spending_time`. If provided, the `NA` positions are
185+
#' validated to match those in `spending_time`. This ensures that the
186+
#' spending time and information fraction structures are consistent.
181187
#'
182188
#' @return A function with the same signature as `spending_fn` —
183189
#' `function(alpha, info_frac)` — that internally uses `spending_time`
@@ -191,38 +197,83 @@ spending_linear <- function(alpha, info_frac) {
191197
#' @export
192198
#'
193199
#' @examples
194-
#' # Subgroup spending time: use subgroup event fractions for spending
195-
#' # while info_frac uses all-subjects event fractions for correlation
196-
#' spending_h2 <- spending_with_time(
200+
#' # --- Subgroup spending time ---
201+
#' # Without spending_with_time, spending_of() uses info_frac for spending:
202+
#' info_frac_all <- c(529 / 800, 700 / 800, 1) # all-subjects fractions
203+
#' spending_of(0.01, info_frac_all)
204+
#'
205+
#' # With spending_with_time, spending uses subgroup fractions instead.
206+
#' # The info_frac passed at runtime is ignored by the spending function;
207+
#' # it is only used by gs_boundaries()/graph_test_shortcut_gsd() for
208+
#' # the correlation structure.
209+
#' spending_time_sub <- c(185 / 295, 245 / 295, 1) # subgroup fractions
210+
#' spending_with_time(spending_of, spending_time_sub)
211+
#'
212+
#' # --- Monitoring with changed final information ---
213+
#' # Planned: 295 OS events at 3 analyses (185, 245, 295 events).
214+
#' # spending_time uses planned fractions for interim analyses and 1
215+
#' # for the final analysis.
216+
#' spending_monitor <- spending_with_time(
197217
#' spending_of,
198218
#' spending_time = c(185 / 295, 245 / 295, 1)
199219
#' )
200220
#'
201-
#' # The wrapped function has the standard (alpha, info_frac) signature
202-
#' # but ignores info_frac and uses spending_time internally
203-
#' spending_h2(0.01, c(0.5, 0.8, 1))
204-
#'
205-
#' # Monitoring: use planned info fractions for spending
206-
#' # when actual final information differs from planned
207-
#' spending_monitor <- spending_with_time(
221+
#' # Overrunning (310 events) or underrunning (280 events):
222+
#' # spending_time is the same in both cases — it uses planned fractions
223+
#' # for interim analyses and 1 for the final analysis, because alpha
224+
#' # spent has been fixed for interim analyses. The actual info_frac
225+
#' # (which differs between overrunning and underrunning) only affects
226+
#' # the correlation structure in gs_boundaries()/graph_test_shortcut_gsd().
227+
#' spending_monitor(0.01, c(185 / 295, 245 / 295, 1))
228+
#'
229+
#' # --- Skipped analyses (NA in spending_time) ---
230+
#' # If a hypothesis is not tested at analysis 2, both spending_time and
231+
#' # info_frac have NA at that position. The output also has NA there.
232+
#' spending_skip <- spending_with_time(
208233
#' spending_of,
209-
#' spending_time = c(0.627, 0.831, 1) # planned
234+
#' spending_time = c(185 / 295, NA, 1),
235+
#' info_frac = c(185 / 295, NA, 1)
210236
#' )
211-
#' # Call with actual info fractions (for correlation structure)
212-
#' spending_monitor(0.01, c(0.597, 0.790, 1)) # actual
213-
spending_with_time <- function(spending_fn, spending_time) {
237+
#' spending_skip(0.01, c(185 / 295, NA, 1))
238+
spending_with_time <- function(spending_fn, spending_time, info_frac = NULL) {
214239
stopifnot(
215240
"spending_fn must be a function" = is.function(spending_fn),
216-
"spending_time must be a numeric vector" = is.numeric(spending_time),
217-
"spending_time must be non-negative" =
218-
all(spending_time >= 0),
219-
"At most one spending_time value can be >= 1" =
220-
sum(spending_time >= 1) <= 1
241+
"spending_time must be a numeric vector" = is.numeric(spending_time)
242+
)
243+
244+
# Validate non-NA spending_time values
245+
st_non_na <- spending_time[!is.na(spending_time)]
246+
stopifnot(
247+
"Non-NA spending_time values must be non-negative" =
248+
length(st_non_na) == 0 || all(st_non_na >= 0),
249+
"At most one non-NA spending_time value can be >= 1" =
250+
sum(st_non_na >= 1) <= 1
221251
)
222252

223-
function(alpha, info_frac) {
224-
st <- spending_time[seq_along(info_frac)]
225-
spending_fn(alpha, st)
253+
# If info_frac provided, validate NA positions match
254+
if (!is.null(info_frac)) {
255+
stopifnot(
256+
"spending_time and info_frac must have the same length" =
257+
length(spending_time) == length(info_frac),
258+
"NA positions in spending_time and info_frac must match" =
259+
identical(is.na(spending_time), is.na(info_frac))
260+
)
261+
}
262+
263+
function(alpha, info_frac_runtime) {
264+
non_na <- !is.na(info_frac_runtime)
265+
n_non_na <- sum(non_na)
266+
267+
# Use the first n_non_na entries of the non-NA spending_time
268+
st <- st_non_na[seq_len(n_non_na)]
269+
270+
# Compute spending for non-NA entries
271+
spent <- spending_fn(alpha, st)
272+
273+
# Build result with NAs in the same positions as info_frac_runtime
274+
result <- rep(NA_real_, length(info_frac_runtime))
275+
result[non_na] <- spent
276+
result
226277
}
227278
}
228279

man/spending_with_time.Rd

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

0 commit comments

Comments
 (0)