Skip to content

Commit 50c866e

Browse files
committed
look back applicable to all hypotheses with data at current or any previous analysis
1 parent 6ba9311 commit 50c866e

4 files changed

Lines changed: 284 additions & 36 deletions

File tree

R/graph_test_shortcut_gsd.R

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -473,22 +473,36 @@ gsd_test <- function(graph, p, alpha, info_frac, spending_fn, look_back,
473473
tv_details <- if (test_values) vector("list", num_analyses)
474474

475475
for (k in seq_len(num_analyses)) {
476-
# Get active hypotheses: not rejected AND has data at analysis k
476+
# Get active hypotheses: not rejected AND has data at this analysis.
477+
# For look_back hypotheses, "has data" means data at any analysis up to k
478+
# (since the sequential p-value carries forward from prior analyses).
477479
has_data_k <- !is.na(p[, k])
478-
active <- !rejected & has_data_k
480+
has_prior_data <- vapply(seq_len(num_hyps), function(j) {
481+
any(!is.na(p[j, seq_len(k)]))
482+
}, logical(1))
483+
active_at_k <- ifelse(look_back, has_prior_data, has_data_k)
484+
active <- !rejected & active_at_k
479485

480486
if (!any(active)) next
481487

482488
# Construct the p-value vector for the shortcut: use sequential p-values
483489
# for hypotheses with look_back = TRUE, repeated p-values otherwise.
490+
# For look_back hypotheses without data at analysis k, use their most
491+
# recent sequential p-value (carried forward from the last available
492+
# analysis).
493+
last_seq_p <- vapply(seq_len(num_hyps), function(j) {
494+
available <- which(!is.na(seq_p_matrix[j, seq_len(k)]))
495+
if (length(available) == 0) NA_real_ else seq_p_matrix[j, max(available)]
496+
}, numeric(1))
497+
484498
p_for_shortcut <- ifelse(
485499
look_back,
486-
seq_p_matrix[, k],
500+
last_seq_p,
487501
rep_p_matrix[, k]
488502
)
489503

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().
504+
# For hypotheses that are not active, set p to 1 so they are never
505+
# selected by graph_test_shortcut().
492506
p_for_shortcut[!active] <- 1
493507

494508
# Apply shortcut to the current graph
@@ -568,18 +582,26 @@ gsd_test <- function(graph, p, alpha, info_frac, spending_fn, look_back,
568582
info_frac, spending_fn, hyp_names, w_at_rej
569583
)
570584

571-
# Set the analysis-k row's Reject to FALSE for this hypothesis
585+
# Check if this hypothesis has a standard row at analysis k
572586
hyp_row <- which(tv_details[[k]]$Hypothesis == hyp_name &
573587
tv_details[[k]]$Analysis == k)
574-
tv_details[[k]]$Reject[hyp_row] <- FALSE
575588

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]
589+
if (length(hyp_row) > 0) {
590+
# Has data at analysis k: set Reject to FALSE and insert
591+
# look_back rows after it
592+
tv_details[[k]]$Reject[hyp_row] <- FALSE
593+
before <- tv_details[[k]][seq_len(hyp_row), , drop = FALSE]
594+
after <- if (hyp_row < nrow(tv_details[[k]])) {
595+
tv_details[[k]][(hyp_row + 1):nrow(tv_details[[k]]), ,
596+
drop = FALSE]
597+
}
598+
tv_details[[k]] <- rbind(before, lb_rows, after)
599+
} else {
600+
# No data at analysis k (look_back-only): append look_back rows
601+
# at the position where this hypothesis was rejected in the
602+
# shortcut sequence
603+
tv_details[[k]] <- rbind(tv_details[[k]], lb_rows)
581604
}
582-
tv_details[[k]] <- rbind(before, lb_rows, after)
583605
}
584606
}
585607
}

R/print.gsd_graph_report.R

Lines changed: 12 additions & 10 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

@@ -188,10 +189,10 @@ print.gsd_graph_report <- function(x, ..., precision = 6, indent = 2) {
188189
# Remove the Look_back column from display
189190
detail$Look_back <- NULL
190191

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)
192+
# Format numeric columns with consistent fixed notation
193+
detail$Weight <- formatC(detail$Weight, format = "f", digits = precision)
194+
detail$p <- formatC(detail$p, format = "f", digits = precision)
195+
detail$Boundary <- formatC(detail$Boundary, format = "f", digits = precision)
195196

196197
detail_out <- utils::capture.output(
197198
print(detail, row.names = FALSE)
@@ -212,13 +213,13 @@ print.gsd_graph_report <- function(x, ..., precision = 6, indent = 2) {
212213
if (!is.null(x$boundary_table)) {
213214
section_break("Repeated p-values ($outputs$repeated_p)")
214215
rep_p_display <- x$outputs$repeated_p
215-
rep_p_display[] <- format(rep_p_display, digits = precision)
216+
rep_p_display[] <- formatC(rep_p_display, format = "f", digits = precision)
216217
print(as.data.frame(rep_p_display))
217218

218219
cat("\n")
219220
section_break("Sequential p-values ($outputs$sequential_p)")
220221
seq_p_display <- x$outputs$sequential_p
221-
seq_p_display[] <- format(seq_p_display, digits = precision)
222+
seq_p_display[] <- formatC(seq_p_display, format = "f", digits = precision)
222223
print(as.data.frame(seq_p_display))
223224
cat("\n")
224225
}
@@ -236,9 +237,10 @@ print.gsd_graph_report <- function(x, ..., precision = 6, indent = 2) {
236237
cat(pad, hyp, "\n", sep = "")
237238
bt <- x$boundary_table[[hyp]]
238239
bt_display <- bt
239-
# Format numeric columns
240+
# Format numeric columns with consistent fixed notation
240241
for (col in names(bt_display)) {
241-
bt_display[[col]] <- format(bt_display[[col]], digits = precision)
242+
bt_display[[col]] <- formatC(bt_display[[col]],
243+
format = "f", digits = precision)
242244
}
243245
bt_out <- utils::capture.output(print(bt_display, row.names = FALSE))
244246
cat(paste0(pad, bt_out), sep = "\n")

tests/testthat/test-graph_test_shortcut_gsd.R

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -999,3 +999,213 @@ test_that("print method shows rejection sequence", {
999999

10001000
expect_output(print(result), "Rejection sequence")
10011001
})
1002+
1003+
# =============================================================================
1004+
# Look_back tests: H4-type (data at both analyses, crosses only at earlier)
1005+
# and H5-type (data at analysis 1 only, look_back from later analysis)
1006+
# =============================================================================
1007+
1008+
# Helper: oncology-like graph for look_back tests
1009+
gsd_onc_graph <- function() {
1010+
alpha_allocation <- c(0.01, 0.01, 0.004, 0, 0.0005, 0.0005)
1011+
hypotheses <- alpha_allocation / sum(alpha_allocation)
1012+
names(hypotheses) <- c("H1", "H2", "H3", "H4", "H5", "H6")
1013+
transitions <- rbind(
1014+
c(0, 1, 0, 0, 0, 0),
1015+
c(0, 0, 0.5, 0.5, 0, 0),
1016+
c(0, 0, 0, 1, 0, 0),
1017+
c(0, 0, 0, 0, 0.5, 0.5),
1018+
c(0, 0, 0, 0, 0, 1),
1019+
c(0.5, 0.5, 0, 0, 0, 0)
1020+
)
1021+
graph_create(hypotheses, transitions)
1022+
}
1023+
1024+
gsd_onc_info_frac <- function() {
1025+
rbind(
1026+
H1 = c(185 / 295, 245 / 295, 1),
1027+
H2 = c(529 / 800, 700 / 800, 1),
1028+
H3 = c(265 / 310, 1, NA),
1029+
H4 = c(675 / 750, 1, NA),
1030+
H5 = c(1, NA, NA),
1031+
H6 = c(1, NA, NA)
1032+
)
1033+
}
1034+
1035+
test_that("H4-type look_back: data at both analyses, crosses only at earlier", {
1036+
g <- gsd_onc_graph()
1037+
info_frac <- gsd_onc_info_frac()
1038+
1039+
# H4 has strong evidence at analysis 1, weak at analysis 2
1040+
# H4 starts with weight 0, gets weight after H3 rejection at analysis 2
1041+
p <- rbind(
1042+
H1 = c(0.03, 0.0001, 0.000001),
1043+
H2 = c(0.2, 0.15, 0.1),
1044+
H3 = c(0.2, 0.001, NA),
1045+
H4 = c(0.0001, 0.02, NA),
1046+
H5 = c(0.00001, NA, NA),
1047+
H6 = c(0.1, NA, NA)
1048+
)
1049+
1050+
r_no <- graph_test_shortcut_gsd(
1051+
g, p, alpha = 0.025, info_frac = info_frac,
1052+
spending_fn = spending_of, look_back = FALSE
1053+
)
1054+
r_yes <- graph_test_shortcut_gsd(
1055+
g, p, alpha = 0.025, info_frac = info_frac,
1056+
spending_fn = spending_of, look_back = TRUE, test_values = TRUE
1057+
)
1058+
1059+
# H4 not rejected without look_back
1060+
expect_false(r_no$outputs$rejected[["H4"]])
1061+
1062+
# H4 rejected with look_back, attributed to analysis 1
1063+
expect_true(r_yes$outputs$rejected[["H4"]])
1064+
expect_equal(r_yes$outputs$decision_at[["H4"]], 2L)
1065+
expect_equal(r_yes$outputs$first_rejected_at[["H4"]], 1L)
1066+
1067+
# test_values at analysis 2: H4 has standard row (Reject=FALSE) and
1068+
# look_back row (Reject=TRUE)
1069+
tv2 <- r_yes$test_values[[2]]
1070+
h4_standard <- tv2[tv2$Hypothesis == "H4" & !tv2$Look_back, ]
1071+
h4_lb <- tv2[tv2$Hypothesis == "H4" & tv2$Look_back, ]
1072+
1073+
expect_equal(nrow(h4_standard), 1)
1074+
expect_false(h4_standard$Reject)
1075+
expect_equal(h4_standard$Analysis, 2L)
1076+
1077+
expect_equal(nrow(h4_lb), 1)
1078+
expect_true(h4_lb$Reject)
1079+
expect_equal(h4_lb$Analysis, 1L)
1080+
})
1081+
1082+
test_that("H5-type look_back: single-analysis hypothesis, rejected via look_back at later analysis", {
1083+
g <- gsd_onc_graph()
1084+
info_frac <- gsd_onc_info_frac()
1085+
1086+
# H5 has 1 analysis, p=0.0008 > initial boundary (0.0005)
1087+
# After graph propagation at analysis 2, H5 gets more weight and
1088+
# look_back finds p=0.0008 crosses the new boundary
1089+
p <- rbind(
1090+
H1 = c(0.03, 0.0001, 0.000001),
1091+
H2 = c(0.2, 0.15, 0.1),
1092+
H3 = c(0.2, 0.001, NA),
1093+
H4 = c(0.0001, 0.02, NA),
1094+
H5 = c(0.0008, NA, NA),
1095+
H6 = c(0.1, NA, NA)
1096+
)
1097+
1098+
r_no <- graph_test_shortcut_gsd(
1099+
g, p, alpha = 0.025, info_frac = info_frac,
1100+
spending_fn = spending_of, look_back = FALSE
1101+
)
1102+
r_yes <- graph_test_shortcut_gsd(
1103+
g, p, alpha = 0.025, info_frac = info_frac,
1104+
spending_fn = spending_of, look_back = TRUE, test_values = TRUE
1105+
)
1106+
1107+
# H5 not rejected without look_back
1108+
expect_false(r_no$outputs$rejected[["H5"]])
1109+
1110+
# H5 rejected with look_back, attributed to analysis 1
1111+
expect_true(r_yes$outputs$rejected[["H5"]])
1112+
expect_equal(r_yes$outputs$decision_at[["H5"]], 2L)
1113+
expect_equal(r_yes$outputs$first_rejected_at[["H5"]], 1L)
1114+
1115+
# test_values at analysis 2: H5 has a standard row with p=NA (no data)
1116+
# and a look_back row at analysis 1 (Reject=TRUE)
1117+
tv2 <- r_yes$test_values[[2]]
1118+
h5_standard <- tv2[tv2$Hypothesis == "H5" & !tv2$Look_back, ]
1119+
h5_lb <- tv2[tv2$Hypothesis == "H5" & tv2$Look_back, ]
1120+
1121+
# Standard row: p is NA, Reject is FALSE
1122+
expect_equal(nrow(h5_standard), 1)
1123+
expect_true(is.na(h5_standard$p))
1124+
expect_false(h5_standard$Reject)
1125+
1126+
# Look_back row: analysis 1, p=0.0008, Reject=TRUE
1127+
expect_equal(nrow(h5_lb), 1)
1128+
expect_true(h5_lb$Reject)
1129+
expect_equal(h5_lb$Analysis, 1L)
1130+
expect_equal(h5_lb$p, 0.0008)
1131+
})
1132+
1133+
test_that("H5-type: without look_back, single-analysis hypothesis not reconsidered", {
1134+
g <- gsd_onc_graph()
1135+
info_frac <- gsd_onc_info_frac()
1136+
1137+
p <- rbind(
1138+
H1 = c(0.03, 0.0001, 0.000001),
1139+
H2 = c(0.2, 0.15, 0.1),
1140+
H3 = c(0.2, 0.001, NA),
1141+
H4 = c(0.0001, 0.02, NA),
1142+
H5 = c(0.0008, NA, NA),
1143+
H6 = c(0.1, NA, NA)
1144+
)
1145+
1146+
result <- graph_test_shortcut_gsd(
1147+
g, p, alpha = 0.025, info_frac = info_frac,
1148+
spending_fn = spending_of, look_back = FALSE, test_values = TRUE
1149+
)
1150+
1151+
# H5 not rejected
1152+
expect_false(result$outputs$rejected[["H5"]])
1153+
# H5 decision_at should be 1 (only analysis with data)
1154+
expect_equal(result$outputs$decision_at[["H5"]], 1L)
1155+
1156+
# H5 should not appear in test_values at analysis 2
1157+
tv2 <- result$test_values[[2]]
1158+
expect_false("H5" %in% tv2$Hypothesis)
1159+
})
1160+
1161+
test_that("look_back with single-analysis hypothesis: not rejected when p too large", {
1162+
g <- gsd_onc_graph()
1163+
info_frac <- gsd_onc_info_frac()
1164+
1165+
# H5 p=0.01 is too large even after propagation
1166+
p <- rbind(
1167+
H1 = c(0.03, 0.0001, 0.000001),
1168+
H2 = c(0.2, 0.15, 0.1),
1169+
H3 = c(0.2, 0.001, NA),
1170+
H4 = c(0.0001, 0.02, NA),
1171+
H5 = c(0.01, NA, NA),
1172+
H6 = c(0.1, NA, NA)
1173+
)
1174+
1175+
result <- graph_test_shortcut_gsd(
1176+
g, p, alpha = 0.025, info_frac = info_frac,
1177+
spending_fn = spending_of, look_back = TRUE
1178+
)
1179+
1180+
# H5 still not rejected — p=0.01 too large even with increased weight
1181+
expect_false(result$outputs$rejected[["H5"]])
1182+
})
1183+
1184+
test_that("look_back carries forward sequential p for hypotheses with no current data", {
1185+
g <- gsd_onc_graph()
1186+
info_frac <- gsd_onc_info_frac()
1187+
1188+
p <- rbind(
1189+
H1 = c(0.03, 0.0001, 0.000001),
1190+
H2 = c(0.2, 0.15, 0.1),
1191+
H3 = c(0.2, 0.001, NA),
1192+
H4 = c(0.0001, 0.02, NA),
1193+
H5 = c(0.0008, NA, NA),
1194+
H6 = c(0.1, NA, NA)
1195+
)
1196+
1197+
result <- graph_test_shortcut_gsd(
1198+
g, p, alpha = 0.025, info_frac = info_frac,
1199+
spending_fn = spending_of, look_back = TRUE
1200+
)
1201+
1202+
# Sequential p for H5 should be the same at analysis 1
1203+
# (only one analysis with data)
1204+
expect_equal(
1205+
result$outputs$sequential_p["H5", 1],
1206+
result$outputs$repeated_p["H5", 1]
1207+
)
1208+
# Analysis 2 and 3 should be NA for H5
1209+
expect_true(is.na(result$outputs$sequential_p["H5", 2]))
1210+
expect_true(is.na(result$outputs$sequential_p["H5", 3]))
1211+
})

0 commit comments

Comments
 (0)