@@ -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
0 commit comments