--- title: "Calibrating `prob_ha` for type I error control" output: rmarkdown::html_vignette: toc: true toc_depth: 2 vignette: > %\VignetteIndexEntry{Calibrating prob_ha for type I error control} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} options(rmarkdown.html_vignette.check_title = FALSE) knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4.8 ) load("prob-ha-calibration.rda") ``` ```{r setup, message = FALSE} library(goldilocks) ``` The decision thresholds in a Goldilocks design are not guarantees of its frequentist operating characteristics. In particular, setting `prob_ha = 0.975` does not by itself guarantee that the complete adaptive design has one-sided type I error no greater than 0.025. Interim analyses, predictive stopping, the enrollment and follow-up processes, and the Monte Carlo settings all contribute to the probability of a successful trial under the null hypothesis. This vignette demonstrates a focused calibration procedure: 1. Prespecify a null data-generating scenario and a target type I error. 2. Screen a finite grid of `prob_ha` values while holding the rest of the design fixed. 3. Use Monte Carlo intervals to distinguish controlled, uncontrolled, and inconclusive candidates. 4. Validate the selected candidate with more simulations and a fresh seed. The result is evidence about the simulated scenarios, not a mathematical guarantee over every possible data-generating process. A design with a composite null may require several prespecified nuisance-parameter scenarios. ## Design and null scenario The example uses a two-arm design with a one-sided log-rank analysis. Both arms have a 30% event probability by 12 months under the null. The design permits futility stopping and stopping accrual for expected success, but does not permit an immediate success claim at an interim look because `Qn = 1`. ```{r design} target_type1 <- 0.025 prob_ha_grid <- seq(0.965, 0.995, by = 0.005) null_hazard <- prop_to_haz(0.30, endtime = 12) calibration_design <- list( hazard_treatment = null_hazard, hazard_control = null_hazard, cutpoints = NULL, N_total = 300, lambda = 5, lambda_time = NULL, interim_look = seq(100, 275, 25), end_of_study = 12, prior_surv = c(0.1, 0.1), block = 2, rand_ratio = c(control = 1, treatment = 1), prop_loss = 0, alternative = "less", h0 = 0, Fn = rep(0.10, 8), Sn = c(1, rep(0.90, 7)), Qn = 1, N_impute = 100, method = "logrank", return_trace = FALSE, ncores = 8 ) ``` Only `prob_ha` changes across candidates. In particular, `N_impute` is fixed: it is part of the implemented interim decision algorithm, not merely a precision setting for this calibration exercise. The number of simulated trials, `N_trials`, controls the Monte Carlo precision of the estimated operating characteristics and can be increased during validation. ## Screening a candidate grid The following code runs 2,000 null trials for every candidate. These calculations are performed offline for the built vignette; their compact summaries are loaded from the accompanying data file. ```{r screening-code, eval = FALSE} screening_seed <- 67201 screening_results <- lapply(prob_ha_grid, function(threshold) { do.call( sim_trials, c( calibration_design, list( prob_ha = threshold, N_trials = 2000, seed = screening_seed ) ) ) }) names(screening_results) <- sprintf("prob_ha = %.3f", prob_ha_grid) screening_summary <- summarise_sims(screening_results) screening_summary$prob_ha <- prob_ha_grid ``` The same seed is deliberately used for each candidate. Because the candidates differ only in `prob_ha`, this applies common random numbers and reduces irrelevant Monte Carlo variation in the screening curve. Candidate results are therefore correlated, and their marginal Monte Carlo intervals should not be interpreted as confidence intervals for pairwise differences. Under the null, the `power` column returned by `summarise_sims()` is the estimated type I error. Its `power_mc_lower` and `power_mc_upper` columns form a 95% Wilson Monte Carlo interval. We apply the following deliberately conservative screening rule: - **controlled**: the upper Monte Carlo bound is at or below the target; - **not controlled**: the lower bound is above the target; and - **inconclusive**: the interval overlaps the target. ```{r classify} classify_type1 <- function(summary, target) { ifelse( summary$power_mc_upper <= target, "controlled", ifelse( summary$power_mc_lower > target, "not controlled", "inconclusive" ) ) } calibration_screening$type1_status <- classify_type1( calibration_screening, target_type1 ) screening_display <- calibration_screening[c( "prob_ha", "n_used", "power", "power_mc_lower", "power_mc_upper", "type1_status" )] names(screening_display)[3:5] <- c( "type1_error", "type1_mc_lower", "type1_mc_upper" ) knitr::kable(screening_display, digits = 3) ``` ## Plotting the calibration curve The shaded area is at or below the target type I error. A candidate is classified as controlled only when its entire displayed Monte Carlo interval falls in that region. ```{r calibration-plot, fig.alt="Estimated type I error and 95% Monte Carlo intervals across seven prob_ha candidates. The intervals for candidates 0.985, 0.990, and 0.995 fall below the horizontal 0.025 target."} screening_plot <- calibration_screening[ order(calibration_screening$prob_ha), ] status_colours <- c( "controlled" = "#009E73", "inconclusive" = "#E69F00", "not controlled" = "#D55E00" ) point_colours <- unname(status_colours[screening_plot$type1_status]) y_max <- max(screening_plot$power_mc_upper, target_type1) * 1.08 plot( screening_plot$prob_ha, screening_plot$power, type = "n", ylim = c(0, y_max), xlab = expression(prob[ha]), ylab = "Null rejection probability (estimated type I error)", main = "Screening prob_ha against a 0.025 target" ) plot_region <- par("usr") rect( plot_region[1], 0, plot_region[2], target_type1, col = grDevices::adjustcolor("#009E73", alpha.f = 0.10), border = NA ) abline(h = target_type1, col = "#0072B2", lty = 2, lwd = 2) lines(screening_plot$prob_ha, screening_plot$power, col = "#555555") arrows( screening_plot$prob_ha, screening_plot$power_mc_lower, screening_plot$prob_ha, screening_plot$power_mc_upper, angle = 90, code = 3, length = 0.04, col = point_colours ) points( screening_plot$prob_ha, screening_plot$power, pch = 19, col = point_colours ) legend( "topright", legend = c(names(status_colours), "target"), col = c(unname(status_colours), "#0072B2"), pch = c(19, 19, 19, NA), lty = c(NA, NA, NA, 2), bty = "n" ) ``` The grid is discrete, so this plot does not justify interpolation to an untested value. Nor is the curve required to be perfectly monotone: changing `prob_ha` changes completed-data success classifications, predictive stopping, and potentially the final sample size. Here we select the smallest screened value classified as controlled, rather than claiming it is the unique or optimal threshold. ```{r selected-candidate} controlled_candidates <- calibration_screening$prob_ha[ calibration_screening$type1_status == "controlled" ] if (length(controlled_candidates) == 0L) { stop("No screened candidate meets the Monte Carlo criterion; revise the grid or simulation size.") } selected_prob_ha <- min(controlled_candidates) selected_prob_ha ``` ## Independent validation Selecting a candidate because it performed well in the screening simulations introduces selection bias. The selected design should therefore be rerun with a fresh simulation seed. The validation below keeps all prespecified decision settings fixed and increases only `N_trials`, from 2,000 to 10,000. ```{r validation-code, eval = FALSE} validation_result <- do.call( sim_trials, c( calibration_design, list( prob_ha = selected_prob_ha, N_trials = 10000, seed = 67202 ) ) ) validation_summary <- summarise_sims(list( "fresh-seed validation" = validation_result )) validation_summary$prob_ha <- selected_prob_ha validation_summary$type1_status <- classify_type1( validation_summary, target_type1 ) ``` ```{r validation-result} validation_display <- calibration_validation[c( "prob_ha", "n_used", "power", "power_mc_lower", "power_mc_upper", "type1_status" )] names(validation_display)[3:5] <- c( "type1_error", "type1_mc_lower", "type1_mc_upper" ) knitr::kable(validation_display, digits = 4) ``` The fresh-seed validation is the relevant assessment of the selected candidate. If it is inconclusive or not controlled, the candidate set or simulation size should be reconsidered and another independent validation planned; repeatedly selecting against the same validation simulations would turn them into an extension of the screening data. ## Extending the calibration For a target of 0.05, change `target_type1` and choose a grid appropriate to that target. Do not assume that `prob_ha = 1 - target_type1` controls the type I error of the full adaptive design. When the null contains nuisance parameters, repeat the candidate grid for each prespecified null scenario. A simple conservative requirement is that the upper Monte Carlo bound be below the target in every scenario. This establishes simulation evidence only on that finite scenario set; identifying a least-favourable configuration and accounting for searches over many scenarios remain design-specific statistical responsibilities. After calibrating type I error, simulate the selected design under one or more clinically relevant alternative scenarios to estimate power and expected sample size. Those simulations should retain the selected `prob_ha` and the same prespecified values of `N_impute` and `N_mcmc` intended for the trial. ### Calibrating an RMST design The workflow also applies to `method = "rmst"`, but the null is defined on the treatment-minus-control RMST scale. The following specification keeps the equal-hazard null from the example and targets a difference of zero through month nine, with follow-up continuing through month twelve: ```{r rmst-calibration-design} rmst_calibration_design <- modifyList(calibration_design, list( method = "rmst", rmst_tau = 9, alternative = "greater", h0 = 0 )) ``` Repeat screening and independent validation with this design in place of `calibration_design`; the stored log-rank results and selected threshold do not calibrate RMST. Keep `rmst_tau` fixed throughout the search. When allowing a loss of `m` months for non-inferiority, use `h0 = -m` and generate boundary-null scenarios with a true RMST difference of `-m` at that horizon. Equal survival distributions provide one zero-difference null scenario. Crossing survival curves can also have equal RMST, so evaluate those scenarios when relevant, together with dropout and discrepancies between `generation_cutpoints` and the predictive `cutpoints`. Inspect failure counts as well as rejection rates: inadequate support through the fixed horizon or zero total variance can make an RMST analysis non-estimable. The [RMST vignette](rmst.html) describes these requirements and gives executable trial simulations.