4444# ' `NA` padding, `info_frac` must be a matrix with `NA` in the same
4545# ' positions as `p`.
4646# '
47- # ' Non-`NA` values must be in (0, 1] and monotonically non-decreasing per
48- # ' hypothesis. The last non-`NA` value does not need to be 1, allowing
47+ # ' Non-`NA` values must be positive and monotonically non-decreasing per
48+ # ' hypothesis. Values greater than 1 are allowed (e.g., when more
49+ # ' information is collected than planned). The spending functions cap
50+ # ' the cumulative spending at `alpha` for information fractions at or
51+ # ' above 1. The last non-`NA` value does not need to be 1, allowing
4952# ' the procedure to be applied up to an interim analysis.
5053# ' @param spending_fn Spending function(s) for computing group sequential
5154# ' boundaries. Can be:
101104# ' hypotheses, this is `NA`. When `look_back = TRUE`, this may be
102105# ' earlier than `decision_at` if a hypothesis crossed its boundary at
103106# ' a prior analysis but only became testable at a later analysis,
107+ # ' * `last_rejected_at` - Integer vector indicating the latest analysis
108+ # ' at which each hypothesis's boundary was crossed. For non-rejected
109+ # ' hypotheses, this is `NA`. Comparing `first_rejected_at` and
110+ # ' `last_rejected_at` shows whether the rejection is supported by
111+ # ' data at multiple analyses or only at a single analysis,
104112# ' * `rejection_sequence` - Character vector giving the order in which
105113# ' hypotheses were rejected across all analyses,
106114# ' * `graph` - Updated graph after removing all rejected hypotheses.
@@ -330,6 +338,7 @@ graph_test_shortcut_gsd <- function(graph,
330338 rejected = result $ rejected ,
331339 decision_at = result $ decision_at ,
332340 first_rejected_at = result $ first_rejected_at ,
341+ last_rejected_at = result $ last_rejected_at ,
333342 rejection_sequence = result $ rejection_sequence ,
334343 graph = if (any(result $ rejected )) {
335344 graph_update(graph , result $ rejected )$ updated_graph
@@ -464,6 +473,7 @@ gsd_test <- function(graph, p, alpha, info_frac, spending_fn, look_back,
464473
465474 # Process analyses sequentially
466475 rejected <- structure(rep(FALSE , num_hyps ), names = hyp_names )
476+ last_rejected_at <- structure(rep(NA_integer_ , num_hyps ), names = hyp_names )
467477 decision_at <- structure(rep(NA_integer_ , num_hyps ), names = hyp_names )
468478 first_rejected_at <- structure(rep(NA_integer_ , num_hyps ), names = hyp_names )
469479 adjusted_p <- structure(rep(NA_real_ , num_hyps ), names = hyp_names )
@@ -473,22 +483,36 @@ gsd_test <- function(graph, p, alpha, info_frac, spending_fn, look_back,
473483 tv_details <- if (test_values ) vector(" list" , num_analyses )
474484
475485 for (k in seq_len(num_analyses )) {
476- # Get active hypotheses: not rejected AND has data at analysis k
486+ # Get active hypotheses: not rejected AND has data at this analysis.
487+ # For look_back hypotheses, "has data" means data at any analysis up to k
488+ # (since the sequential p-value carries forward from prior analyses).
477489 has_data_k <- ! is.na(p [, k ])
478- active <- ! rejected & has_data_k
490+ has_prior_data <- vapply(seq_len(num_hyps ), function (j ) {
491+ any(! is.na(p [j , seq_len(k )]))
492+ }, logical (1 ))
493+ active_at_k <- ifelse(look_back , has_prior_data , has_data_k )
494+ active <- ! rejected & active_at_k
479495
480496 if (! any(active )) next
481497
482498 # Construct the p-value vector for the shortcut: use sequential p-values
483499 # for hypotheses with look_back = TRUE, repeated p-values otherwise.
500+ # For look_back hypotheses without data at analysis k, use their most
501+ # recent sequential p-value (carried forward from the last available
502+ # analysis).
503+ last_seq_p <- vapply(seq_len(num_hyps ), function (j ) {
504+ available <- which(! is.na(seq_p_matrix [j , seq_len(k )]))
505+ if (length(available ) == 0 ) NA_real_ else seq_p_matrix [j , max(available )]
506+ }, numeric (1 ))
507+
484508 p_for_shortcut <- ifelse(
485509 look_back ,
486- seq_p_matrix [, k ] ,
510+ last_seq_p ,
487511 rep_p_matrix [, k ]
488512 )
489513
490- # For hypotheses without data at analysis k or already rejected,
491- # set p to 1 so they are never selected by graph_test_shortcut().
514+ # For hypotheses that are not active, set p to 1 so they are never
515+ # selected by graph_test_shortcut().
492516 p_for_shortcut [! active ] <- 1
493517
494518 # Apply shortcut to the current graph
@@ -525,13 +549,19 @@ gsd_test <- function(graph, p, alpha, info_frac, spending_fn, look_back,
525549 shortcut_k $ details $ results [[rej_idx ]]$ hypotheses [hyp_name ]
526550 allocated_alpha <- w_at_rejection * alpha
527551
528- # Look back: earliest analysis where sequential p <= allocated alpha
552+ # Look back: earliest and latest analysis where boundary is crossed.
553+ # Check repeated p-values (not sequential) at each analysis against
554+ # the allocated alpha — a repeated p <= allocated alpha means the
555+ # boundary at that specific analysis is crossed.
529556 j <- which(hyp_names == hyp_name )
530- earliest <- which(seq_p_matrix [j , 1 : k ] < = allocated_alpha )[ 1 ]
557+ crossed <- which(rep_p_matrix [j , 1 : k ] < = allocated_alpha )
531558 first_rejected_at [hyp_name ] <-
532- if (! is.na(earliest )) earliest else k
559+ if (length(crossed ) > 0 ) crossed [1 ] else k
560+ last_rejected_at [hyp_name ] <-
561+ if (length(crossed ) > 0 ) crossed [length(crossed )] else k
533562 } else {
534563 first_rejected_at [hyp_name ] <- k
564+ last_rejected_at [hyp_name ] <- k
535565 }
536566 }
537567
@@ -548,7 +578,7 @@ gsd_test <- function(graph, p, alpha, info_frac, spending_fn, look_back,
548578 if (test_values ) {
549579 tv_details [[k ]] <- gsd_test_values_details(
550580 step_graph , p , k , alpha , info_frac , spending_fn ,
551- newly_in_order , hyp_names , rejected , has_data_k
581+ newly_in_order , hyp_names , rejected , active_at_k
552582 )
553583
554584 # Add Look_back column (FALSE for all standard rows)
@@ -568,18 +598,31 @@ gsd_test <- function(graph, p, alpha, info_frac, spending_fn, look_back,
568598 info_frac , spending_fn , hyp_names , w_at_rej
569599 )
570600
571- # Set the analysis-k row's Reject to FALSE for this hypothesis
601+ # Check if this hypothesis has a standard row at analysis k
572602 hyp_row <- which(tv_details [[k ]]$ Hypothesis == hyp_name &
573603 tv_details [[k ]]$ Analysis == k )
574- tv_details [[k ]]$ Reject [hyp_row ] <- FALSE
575604
576- # Insert look_back rows immediately after the hypothesis's row
577- before <- tv_details [[k ]][seq_len(hyp_row ), , drop = FALSE ]
578- after <- if (hyp_row < nrow(tv_details [[k ]])) {
579- tv_details [[k ]][(hyp_row + 1 ): nrow(tv_details [[k ]]), ,
580- drop = FALSE ]
605+ if (length(hyp_row ) > 0 ) {
606+ # Has data at analysis k: check if the nominal p-value at
607+ # analysis k also crosses the boundary. If not, set Reject
608+ # to FALSE (the rejection is only via look_back).
609+ p_at_k <- tv_details [[k ]]$ p [hyp_row ]
610+ b_at_k <- tv_details [[k ]]$ Boundary [hyp_row ]
611+ if (is.na(p_at_k ) || p_at_k > b_at_k ) {
612+ tv_details [[k ]]$ Reject [hyp_row ] <- FALSE
613+ }
614+ before <- tv_details [[k ]][seq_len(hyp_row ), , drop = FALSE ]
615+ after <- if (hyp_row < nrow(tv_details [[k ]])) {
616+ tv_details [[k ]][(hyp_row + 1 ): nrow(tv_details [[k ]]), ,
617+ drop = FALSE ]
618+ }
619+ tv_details [[k ]] <- rbind(before , lb_rows , after )
620+ } else {
621+ # No data at analysis k (look_back-only): append look_back rows
622+ # at the position where this hypothesis was rejected in the
623+ # shortcut sequence
624+ tv_details [[k ]] <- rbind(tv_details [[k ]], lb_rows )
581625 }
582- tv_details [[k ]] <- rbind(before , lb_rows , after )
583626 }
584627 }
585628 }
@@ -597,6 +640,7 @@ gsd_test <- function(graph, p, alpha, info_frac, spending_fn, look_back,
597640 rejected = rejected ,
598641 decision_at = decision_at ,
599642 first_rejected_at = first_rejected_at ,
643+ last_rejected_at = last_rejected_at ,
600644 rejection_sequence = rejection_sequence ,
601645 test_values = tv_details
602646 )
@@ -800,8 +844,8 @@ gsd_input_val <- function(graph, p, alpha, info_frac, spending_fn, look_back,
800844 " Information fractions must have the same number of columns as p" =
801845 ncol(info_frac ) == num_analyses ,
802846 " Information fractions must be numeric" = is.numeric(info_frac ),
803- " Non-NA information fractions must be in (0, 1] " =
804- length(if_non_na ) == 0 || all(if_non_na > 0 & if_non_na < = 1 ),
847+ " Non-NA information fractions must be positive " =
848+ length(if_non_na ) == 0 || all(if_non_na > 0 ),
805849 " Spending functions must be a list of functions" =
806850 is.list(spending_fn ) &&
807851 all(vapply(spending_fn , is.function , logical (1 ))),
0 commit comments