Skip to content

Commit c106485

Browse files
authored
Merge branch 'gsd' into modify_rpact_wording
2 parents 141b4c0 + 7c5ec68 commit c106485

18 files changed

Lines changed: 1590 additions & 159 deletions

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,7 @@ export(spending_hsd)
5555
export(spending_linear)
5656
export(spending_of)
5757
export(spending_pocock)
58+
export(spending_with_time)
59+
export(spending_wt)
5860
export(three_doses_two_primary_two_secondary)
5961
export(two_doses_two_primary_two_secondary)

R/graph_test_shortcut_gsd.R

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@
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:
@@ -101,6 +104,11 @@
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))),

R/print.graph_report.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ print.graph_report <- function(x, ..., precision = 4, indent = 2, rows = 10) {
121121

122122
df_summary <- data.frame(
123123
Hypothesis = formatC(hyp_names, width = hyp_width),
124-
`Adj. P-value` = adjusted_p,
124+
Adj.p = adjusted_p,
125125
Reject = x$outputs$rejected,
126126
check.names = FALSE
127127
)

R/print.gsd_graph_report.R

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ print.gsd_graph_report <- function(x, ..., precision = 6, indent = 2) {
7878
cat("\n", pad, "P-values\n", sep = "")
7979
p_df <- as.data.frame(x$inputs$p, row.names = hyp_names)
8080
colnames(p_df) <- analysis_names
81-
p_df[] <- lapply(p_df, function(col) format(col, digits = precision))
81+
p_df[] <- lapply(p_df, function(col) formatC(col, format = "f", digits = precision))
8282
print(p_df)
8383

8484
# Spending functions
@@ -124,7 +124,8 @@ print.gsd_graph_report <- function(x, ..., precision = 6, indent = 2) {
124124
exceed_1 <- adj_p > 1
125125
adj_p_format <- character(length(adj_p))
126126
adj_p_format[exceed_1] <- gsub(".00000001", "+", adj_p[exceed_1])
127-
adj_p_format[!exceed_1] <- format(adj_p[!exceed_1], digits = precision)
127+
adj_p_format[!exceed_1] <- formatC(adj_p[!exceed_1], format = "f",
128+
digits = precision)
128129

129130
decision_at <- x$outputs$decision_at
130131

@@ -134,20 +135,32 @@ print.gsd_graph_report <- function(x, ..., precision = 6, indent = 2) {
134135
as.character(x$outputs$first_rejected_at)
135136
)
136137

138+
last_rej_display <- ifelse(
139+
is.na(x$outputs$last_rejected_at),
140+
"--",
141+
as.character(x$outputs$last_rejected_at)
142+
)
143+
137144
df_summary <- data.frame(
138145
Hypothesis = formatC(hyp_names, width = hyp_width),
139146
Adj.P = adj_p_format,
140147
Reject = x$outputs$rejected,
141-
Decision.at = as.character(decision_at),
148+
Tested.at = as.character(decision_at),
142149
First.Rej.at = first_rej_display,
150+
Last.Rej.at = last_rej_display,
143151
Look.back = look_back,
144152
check.names = FALSE
145153
)
146154
names(df_summary)[[1]] <- formatC("Hypothesis", width = hyp_width)
147-
names(df_summary)[[2]] <- "Adj.P-value"
155+
names(df_summary)[[2]] <- "Adj.p*"
148156

149157
print(df_summary, row.names = FALSE)
150158

159+
cat(pad, "(*) Adjusted p-values account for both the group sequential",
160+
" design and the\n", pad, " graphical multiple comparison procedure.",
161+
" Based on repeated p-values when\n", pad, " look_back = FALSE,",
162+
" and sequential p-values when look_back = TRUE.\n", sep = "")
163+
151164
# Rejection sequence
152165
rej_seq <- x$outputs$rejection_sequence
153166
if (length(rej_seq) > 0) {
@@ -188,10 +201,10 @@ print.gsd_graph_report <- function(x, ..., precision = 6, indent = 2) {
188201
# Remove the Look_back column from display
189202
detail$Look_back <- NULL
190203

191-
# Format numeric columns
192-
detail$Weight <- format(detail$Weight, digits = precision)
193-
detail$p <- format(detail$p, digits = precision)
194-
detail$Boundary <- format(detail$Boundary, digits = precision)
204+
# Format numeric columns with consistent fixed notation
205+
detail$Weight <- formatC(detail$Weight, format = "f", digits = precision)
206+
detail$p <- formatC(detail$p, format = "f", digits = precision)
207+
detail$Boundary <- formatC(detail$Boundary, format = "f", digits = precision)
195208

196209
detail_out <- utils::capture.output(
197210
print(detail, row.names = FALSE)
@@ -200,14 +213,30 @@ print.gsd_graph_report <- function(x, ..., precision = 6, indent = 2) {
200213

201214
# Print footnote for look_back hypotheses
202215
if (has_look_back) {
203-
cat(pad, "(*) Rejected via look_back: nominal p-value did not cross",
204-
" the boundary at the\n", pad, " current analysis, but",
205-
" crossed the boundary at an earlier analysis.\n", sep = "")
216+
cat(pad, "(*) Rejected via look_back: the nominal p-value crossed",
217+
" the boundary at an\n", pad, " earlier analysis with the",
218+
" hypothesis weight updated via graph propagation.\n",
219+
sep = "")
206220
}
207221
cat("\n")
208222
}
209223
}
210224

225+
# Repeated and sequential p-values (verbose) --------------------------------
226+
if (!is.null(x$boundary_table)) {
227+
section_break("Repeated p-values ($outputs$repeated_p)")
228+
rep_p_display <- x$outputs$repeated_p
229+
rep_p_display[] <- formatC(rep_p_display, format = "f", digits = precision)
230+
print(as.data.frame(rep_p_display))
231+
232+
cat("\n")
233+
section_break("Sequential p-values ($outputs$sequential_p)")
234+
seq_p_display <- x$outputs$sequential_p
235+
seq_p_display[] <- formatC(seq_p_display, format = "f", digits = precision)
236+
print(as.data.frame(seq_p_display))
237+
cat("\n")
238+
}
239+
211240
# Boundary table (verbose) ---------------------------------------------------
212241
if (!is.null(x$boundary_table)) {
213242
section_break("Boundary table ($boundary_table)")
@@ -221,9 +250,10 @@ print.gsd_graph_report <- function(x, ..., precision = 6, indent = 2) {
221250
cat(pad, hyp, "\n", sep = "")
222251
bt <- x$boundary_table[[hyp]]
223252
bt_display <- bt
224-
# Format numeric columns
253+
# Format numeric columns with consistent fixed notation
225254
for (col in names(bt_display)) {
226-
bt_display[[col]] <- format(bt_display[[col]], digits = precision)
255+
bt_display[[col]] <- formatC(bt_display[[col]],
256+
format = "f", digits = precision)
227257
}
228258
bt_out <- utils::capture.output(print(bt_display, row.names = FALSE))
229259
cat(paste0(pad, bt_out), sep = "\n")

0 commit comments

Comments
 (0)