Add harm bound to AHR designs#640
Conversation
This reverts commit 9b59c45.
The helper-support-as_rtf.R is auto-sourced by testit, so the explicit source() call failed during R CMD check. Also update as_gt and as_rtf snapshots to reflect the new bound ordering. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Run roxygenize to sync Rd files with code (fixes WARNING) - Remove test-independent-as_rtf.R (the .md snapshot is sufficient) - Replace all() with bare logical vector in test assertions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…alone Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Note on snapshot test changesThe This means every analysis section in both The other visible change in |
|
Correction on the alignment change: the |
|
Thank you @yihui for helping with the |
| harm_bound <- x |> dplyr::filter(bound == "harm") |> dplyr::pull(z) | ||
| futility_bound <- x |> dplyr::filter(bound == "lower") |> dplyr::pull(z) | ||
| (harm_bound <= futility_bound) | ||
| }) |
There was a problem hiding this comment.
Please instruct the AI to write unit tests for every function that it edits
|
|
||
| # One-sided design should not include Futility column | ||
| if (all(is.na(out[["Futility"]]))) out[["Futility"]] <- NULL | ||
| if (all(is.na(out[["Harm"]]))) out[["Harm"]] <- NULL |
There was a problem hiding this comment.
Changes look good when I tested locally, but this needs some unit tests for long-term maintenance. For example, confirm that a one-sided design does not return the column Harm
| upper = gs_b, upar = -qnorm(0.025), test_upper = TRUE, | ||
| lower = gs_b, lpar = -1, test_lower = TRUE, | ||
| harm = gs_b, hpar = -2, test_harm = TRUE | ||
| ) |
There was a problem hiding this comment.
Does it make sense to compute and return a futility or harm bound when there is only a single analysis?
If I run this with test_harm = FALSE, it only returns an efficacy bound for the single analysis. But with test_harm = TRUE, it returns all 3 bounds (efficacy, futility, harm).
There was a problem hiding this comment.
Could you please provide your code example?
There was a problem hiding this comment.
I was using the example immediately above. Here is more minimal code to reproduce what I observe:
library("gsDesign2")
# Returns upper bound
gs_design_ahr(analysis_time = 40)$bound
# Returns upper and lower bounds
gs_design_ahr(analysis_time = 40, test_harm = TRUE)$bound
# Returns upper, lower, and harm bounds
gs_design_ahr(analysis_time = 40, hpar = -2, test_harm = TRUE)$boundThis matters because the bounds return by gs_design_ahr() are directly used and reported by gs_bound_summary(). Does it make sense to include columns for Futility and Harm for a trial with a single analysis time?
x <- gs_design_ahr(analysis_time = 40, hpar = -2, test_harm = TRUE)
gs_bound_summary(x)
## Analysis Value Efficacy Futility Harm
## 1 Final Z 1.9600 1.9444 -2.0000
## 2 N: 428 p (1-sided) 0.0250 0.0259 0.9772
## 3 Events: 280 ~HR at bound 0.7911 0.7925 1.2702
## 4 Month: 40 P(Cross) if HR=1 0.0250 0.9741 0.0228
## 5 P(Cross) if AHR=0.68 0.9000 0.0973 0.0000There was a problem hiding this comment.
These are really good questions. I confirmed with Keaven:
- When it is a fixed design, there is no harm bound.
- When it is a group sequential design, the harm bound and lower bound are relatively independent. However, when the lower bound exists, the harm bound is always below the harm bound.
I will test the these 2 bullets in a coming commit!
yihui
left a comment
There was a problem hiding this comment.
I only looked at the test part as I don't have expertise on the design part.
|
|
||
| ```{r} | ||
| candidate_harm_bounds <- lapply(astar_candidates, function(astar_candidate) { | ||
| gs_harm <- gsDesign::gsSurv( |
There was a problem hiding this comment.
The pkgdown workflow failed with the error ! unused arguments (sfharm = gsDesign::sfHSD, sfharmparam = -2) because this new article requires the dev version of {gsDesign}
| } | ||
| if (n_analysis == 1 && test_harm) { | ||
| stop("gs_design_ahr() harm bound cannot be tested if there is only one analysis.") | ||
| } |
There was a problem hiding this comment.
What about gs_design_npe() and gs_power_npe()? They still return the harm bound for a fixed design when test_harm = TRUE.
library("gsDesign2")
gs_design_npe()
## # A tibble: 1 × 10
## analysis bound z probability probability0 theta info info0 info1 info_frac
## <dbl> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1 upper 1.96 0.9 0.025 0.1 1051. 1051. 1051. 1
gs_design_npe(test_harm = TRUE)
## analysis bound z probability probability0 theta info_frac info info0 info1
## 1 1 upper 1.959964 0.9 0.025 0.1 1 1050.742 1050.742 1050.742
## 2 1 lower -Inf 0.0 0.000 0.1 1 1050.742 1050.742 1050.742
## 3 1 harm -Inf 0.0 0.000 0.1 1 1050.742 1050.742 1050.742
gs_power_npe()
## analysis bound z probability theta theta1 info_frac info info0 info1
## 1 1 upper 1.959964 0.03144531 0.1 0.1 1 1 1 1
## 2 1 lower -Inf 0.00000000 0.1 0.1 1 1 1 1
> gs_power_npe(test_harm = TRUE)
## analysis bound z probability theta theta1 info_frac info info0 info1
## 1 1 upper 1.959964 0.03144531 0.1 0.1 1 1 1 1
## 2 1 lower -Inf 0.00000000 0.1 0.1 1 1 1 1
## 3 1 harm -Inf 0.00000000 0.1 0.1 1 1 1 1| (filter(x$bound, bound == "lower")$spending_time %==% expected) | ||
| }) | ||
|
|
||
| assert("Harm bound is not provided when it is a fixed design", { |
There was a problem hiding this comment.
I'm confused. All of the calls below use the default of test_harm = FALSE. Thus the harm bound is not returned because of this argument, and has nothing to do with the fact that these are fixed designs.
| upper = gs_b, upar = qnorm(1 - 0.025), test_upper = TRUE, | ||
| lower = gs_b, lpar = -Inf, test_lower = FALSE) | ||
|
|
||
| (all.equal(x1$bound[abs(x1$z) != Inf], "upper")) |
There was a problem hiding this comment.
all.equal() should be used for numeric comparisons. I think == should be sufficient for comparing strings. Alternatively you could use identical().
| (all.equal(x1$bound[abs(x1$z) != Inf], "upper")) | ||
| (all.equal(x2$bound[abs(x2$z) != Inf], "upper")) | ||
| (all.equal(x3$bound$bound[abs(x3$bound$z) != Inf], "upper")) | ||
| (all.equal(x4$bound$bound[abs(x4$bound$z) != Inf], "upper")) |
There was a problem hiding this comment.
Using != Inf seems like cheating here. Don't you want to confirm that the bound table does not include a row for the harm bound at all?
Only x1 (produced by gs_power_npe()) returns a lower bound with a z of -Inf. Given that test_lower = FALSE, shouldn't the function gs_power_npe() instead be updated to not return this lower bound?
| (all(x2$bound$z[x2$bound$bound == "lower"] - x2$bound$z[x2$bound$bound == "harm"] >= 0)) | ||
| }) | ||
|
|
||
| assert("Harm bound is always below the lower bound in a group sequential design", { |
There was a problem hiding this comment.
How is this test different than the one above? The comment used to describe them is identical.
| upper = gs_spending_bound, | ||
| upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025), | ||
| test_upper = TRUE, | ||
| test_lower = FALSE, |
There was a problem hiding this comment.
No lower bound is returned because test_lower = FALSE
| harm = gs_spending_bound, | ||
| hpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -4), | ||
| test_harm = TRUE) | ||
| x3 <- gs_design_ahr(analysis_time = c(12, 24, 36), info_frac = NULL, |
There was a problem hiding this comment.
This x3 object is never used
| test_harm = TRUE) | ||
|
|
||
| (all(x1$bound$z[x1$bound$bound == "lower"] - x1$bound$z[x1$bound$bound == "harm"] >= 0)) | ||
| (all(x2$bound$z[x2$bound$bound == "lower"] - x2$bound$z[x2$bound$bound == "harm"] >= 0)) |
There was a problem hiding this comment.
These tests are meaningless. Because test_lower = FALSE, there is no lower bound.
x1$bound
## analysis bound probability probability0 z ~hr at bound nominal p spending_time
## 1 1 upper 0.002264189 5.380432e-05 3.872763 0.4485851 5.380432e-05 0.3080415
## 2 2 upper 0.550377366 9.209304e-03 2.357870 0.7299828 9.190059e-03 0.7407917
## 3 3 upper 0.899999844 2.500000e-02 2.009598 0.7938368 2.223685e-02 1.0000000
x1$bound$z[x1$bound$bound == "lower"]
## numeric(0)
x1$bound$z[x1$bound$bound == "harm"]
## numeric(0)
x1$bound$z[x1$bound$bound == "lower"] - x1$bound$z[x1$bound$bound == "harm"] >= 0
## logical(0)
all(x1$bound$z[x1$bound$bound == "lower"] - x1$bound$z[x1$bound$bound == "harm"] >= 0)
## [1] TRUE
| test_lower = c(TRUE, TRUE, FALSE), | ||
| harm = gs_spending_bound, | ||
| hpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -4, timing = NULL), | ||
| test_harm = c(TRUE, TRUE, TRUE) |
There was a problem hiding this comment.
Does it make sense for test_harm to be TRUE for the 3rd analysis but test_lower be FALSE?
To solve issue #618.
@jdblischak: In addition to "Efficacy" and "Futility" bound, I added "Harm" bound, which sequentially leads to some changes in
gs_bound_summary(). The changes ings_bound_summary()is suggested by GPT5.5. Could you please review if these AI-suggested changes looks good to you?