| Title: | Utility-Based Optimization for Basket Trial Designs |
|---|---|
| Description: | A unified framework for optimizing basket trial designs. To this end, the package supplies several utility functions and also a function for executing optimization algorithms on basket trial designs. The considered utility functions are discussed in Sauer et al. (2025) <doi:10.1371/journal.pone.0323097>. |
| Authors: | Lukas D Sauer [aut, cre] (ORCID: <https://orcid.org/0000-0002-1340-9994>) |
| Maintainer: | Lukas D Sauer <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 1.0.4 |
| Built: | 2026-06-06 07:11:33 UTC |
| Source: | https://github.com/lukasdsauer/baskoptr |
append_details takes the element details[["index"]] and appends it with
the list further. set_details takes the element details[["index"]] and
sets it to value.
append_details(details, index, further) set_details(details, index, value)append_details(details, index, further) set_details(details, index, value)
details |
A list of details to be requested from
|
index |
Indicates which element of |
further |
A list of further parameters to be appended. |
value |
A character string, |
The updated list of details.
Calculate the extreme borrowing cutoffs for the tuning parameters
and in Fujikawa's basket trial design.
epsilon_extreme(design, tau, detail_params) tau_extreme(design, epsilon, detail_params)epsilon_extreme(design, tau, detail_params) tau_extreme(design, epsilon, detail_params)
design |
An object of class |
tau |
The optimization parameter |
detail_params |
A named list of parameters containing per-stratum
sizes |
epsilon |
The optimization parameter |
The weights for the borrowing posterior in Fujikawa's design are calculated as
with , , and
.
Here, is the Jensen-Shannon divergence, and
is the beta-binomial conjugated posterior distribution in the -th stratum
for prior Beta parameters an , number of responses
and number of patients . In particular for
borrowing will only take place for baskets with completely identical response rates.
We call this boundary the extreme borrowing cutoff. The functions
epsilon_extreme and tau_extreme calculate the extreme borrowing cutoffs
and
, which are the boundaries of
and
The extreme borrowing cutoffs depend neither on p0 nor on p1. They only
depend on the number of baskets k and the number of patients per basket
n. The extreme borrowing cutoff is discussed in Sauer et al. (2025).
A numeric, the extreme borrowing cutoff for .
Sauer LD, Ritz A, Kieser M. Utility-based optimization of Fujikawa’s basket trial design – Pre-specified protocol of a comparison study. PLOS ONE. 2025;20(5):e0323097. doi:10.1371/journal.pone.0323097
design <- basksim::setup_fujikawa(k = 3, shape1 = 1, shape2 = 1, p0 = 0.2) detail_params <- list(n = 20, logbase = exp(1)) epsilon_extreme(design, tau = 0.5, detail_params) tau_extreme(design, epsilon = 2, detail_params)design <- basksim::setup_fujikawa(k = 3, shape1 = 1, shape2 = 1, p0 = 0.2) detail_params <- list(n = 20, logbase = exp(1)) epsilon_extreme(design, tau = 0.5, detail_params) tau_extreme(design, epsilon = 2, detail_params)
Internal helper function: Get details for two response scenarios
get_details_for_two_scenarios( design, x, detail_params, p1, p2, which_details_list = NULL )get_details_for_two_scenarios( design, x, detail_params, p1, p2, which_details_list = NULL )
design |
An object of class |
x |
A named list, the design's tuning parameters to be optimized. |
detail_params |
A named list of parameters that need to be supplied to
|
p1 |
A numeric, response scenario for calculating power resp. ECD. This
can also be left |
p2 |
A numeric, response scenario for calculating FWER, default is the global null scenario. |
which_details_list |
A list of two lists, |
A list of two lists containing return values of get_details calls.
This functions generates an array of possible outcome scenarios. Each row
of the array is a scenario vector. The array starts with k inactive
baskets and 0 active baskets and then increments in steps of by up to
k active baskets (or the next smaller multiple of by).
get_p1s(k, p0, p1, by = 1)get_p1s(k, p0, p1, by = 1)
k |
The number of baskets. |
p0 |
A common probability under the null hypothesis. |
p1 |
A numeric, the true response probability for the active baskets |
by |
A numeric, increment of the scenario sequence, default: 1. |
A numeric array.
p1s <- get_p1s(k = 3, p0 = 0.2, p1 = 0.5)p1s <- get_p1s(k = 3, p0 = 0.2, p1 = 0.5)
A wrapper of get_p1s() that also returns the input values k and
p0 as part of a list.
get_scenarios(k, p0, p1, by = 1)get_scenarios(k, p0, p1, by = 1)
k |
The number of baskets. |
p0 |
A common probability under the null hypothesis. |
p1 |
A numeric, the true response probability for the active baskets |
by |
A numeric, increment of the scenario sequence, default: 1. |
A list with components k, p0 and p1s,
where p1s is generated by the the function get_p1s.
scenario <- get_scenarios(k = 3, p0 = 0.2, p1 = 0.5)scenario <- get_scenarios(k = 3, p0 = 0.2, p1 = 0.5)
Internal helper function: Decide whether to record a trace and what path to use
get_trace_info(trace, type = "trace")get_trace_info(trace, type = "trace")
trace |
A parameter that can be |
type |
A character that designates the name of the input. |
A list with components rec and path.
Returns detail_params with checked element detail_params$p1 <- p1,
depending on whether p1 is not NULL or detail_params$p1 is not NULL.
Returns an error message if both are NULL.
io_val_p1(detail_params, p1)io_val_p1(detail_params, p1)
detail_params |
A named list of parameters that need to be supplied to
|
p1 |
A numeric, response scenario for calculating power resp. ECD. This
can also be left |
The updated list of detail_params.
Optimize the parameters of a basket trial design using a utility-based approach with a simulation algorithm of your choice.
opt_design_gen( design, utility, algorithm, detail_params, utility_params, algorithm_params, x_names = NULL, fn_name = "fn", trace = FALSE, trace_options = list(robust = FALSE), format_result = NULL, final_details = FALSE, final_details_utility_params = utility_params, progress_bar = NULL )opt_design_gen( design, utility, algorithm, detail_params, utility_params, algorithm_params, x_names = NULL, fn_name = "fn", trace = FALSE, trace_options = list(robust = FALSE), format_result = NULL, final_details = FALSE, final_details_utility_params = utility_params, progress_bar = NULL )
design |
An object of class |
utility |
A function returning the utility of a parameter combination
|
algorithm |
A function returning the optimization algorithm's results,
should have the form |
detail_params |
A named list of parameters that need to be supplied to
|
utility_params |
A named list of further parameters that need to be supplied to the utility function. |
algorithm_params |
A named list of further parameters that need to be supplied to the optimization algorithm. |
x_names |
A character vector containing the names of the utility functions
tuning parameters, automatically retrieved from |
fn_name |
The name of the function argument of |
trace |
A logical or a character string, should the trace of the
optimization algorithm be recorded (utility values, parameters vectors,
and random number seeds)? Default is |
trace_options |
A list, if |
format_result |
(Optional:) A function |
final_details |
A logical, if |
final_details_utility_params |
A list, only takes effect if
|
progress_bar |
Do you want a progress bar? This argument either takes
a numeric containing the number of steps or a |
a list consisting of the algorithm's output (usually the optimal
parameter vector and the resulting optimal utility value and some meta
information). If trace == TRUE, the trace of the optimization algorithm
can be found in the [["trace"]] entry of the list. (If the algorithm
function also outputs a trace, this will be saved in an additional
[["trace_alg"]] entry.)
# Optimizing a three-basket trial design using Fujikawa's beta-binomial # sharing approach design <- baskwrap::setup_fujikawa_x(k = 3, shape1 = 1, shape2 = 1, p0 = 0.2, backend = "exact") detail_params <- list(p1 = c(0.5, 0.2, 0.2), n = 20, weight_fun = baskwrap::weights_jsd, logbase = exp(1)) utility_params <- list(penalty = 1, thresh = 0.1) # Bounded simulated annealing with progress bar progressr::handlers(global = TRUE) opt_design_gen(design = design, utility = u_ewp, algorithm = optimizr::simann, detail_params = detail_params, utility_params = utility_params, algorithm_params = list(par = c(lambda = 0.99, epsilon = 2, tau = 0.5), lower = c(lambda = 0.001, epsilon = 1, tau = 0.001), upper = c(lambda = 0.999, epsilon = 10, tau = 0.999), control = list(maxit = 10, temp = 10, fnscale = -1)), trace = TRUE, progress_bar = 10 + 2)# Optimizing a three-basket trial design using Fujikawa's beta-binomial # sharing approach design <- baskwrap::setup_fujikawa_x(k = 3, shape1 = 1, shape2 = 1, p0 = 0.2, backend = "exact") detail_params <- list(p1 = c(0.5, 0.2, 0.2), n = 20, weight_fun = baskwrap::weights_jsd, logbase = exp(1)) utility_params <- list(penalty = 1, thresh = 0.1) # Bounded simulated annealing with progress bar progressr::handlers(global = TRUE) opt_design_gen(design = design, utility = u_ewp, algorithm = optimizr::simann, detail_params = detail_params, utility_params = utility_params, algorithm_params = list(par = c(lambda = 0.99, epsilon = 2, tau = 0.5), lower = c(lambda = 0.001, epsilon = 1, tau = 0.001), upper = c(lambda = 0.999, epsilon = 10, tau = 0.999), control = list(maxit = 10, temp = 10, fnscale = -1)), trace = TRUE, progress_bar = 10 + 2)
List of parameters used across the package
design |
An object of class |
x |
A named list, the design's tuning parameters to be optimized. |
detail_params |
A named list of parameters that need to be supplied to
|
report_details |
A logical, if |
List of parameters used for functions calling utility functions
utility |
A function returning the utility of a parameter combination
|
utility_params |
A named list of further parameters that need to be supplied to the utility function. |
These utility functions combine rewarding power and penalizing TOER by subtracting TOER from power. In addition, unacceptably high FWER above a certain threshold receives harsher penalty.
u_2ewp( design, x, detail_params, p1 = NULL, penalty1, penalty2, threshold, report_details = FALSE ) u_2pow( design, x, detail_params, p1 = NULL, penalty1, penalty2, threshold, report_details = FALSE )u_2ewp( design, x, detail_params, p1 = NULL, penalty1, penalty2, threshold, report_details = FALSE ) u_2pow( design, x, detail_params, p1 = NULL, penalty1, penalty2, threshold, report_details = FALSE )
design |
An object of class |
x |
A named list, the design's tuning parameters to be optimized. |
detail_params |
A named list of parameters that need to be supplied to
|
p1 |
A numeric, response scenario for calculating power and error rate. |
penalty1 |
A numeric, |
penalty2 |
A numeric, |
threshold |
A numeric, for high FWER above this threshold we impose a harsher penalty. |
report_details |
A logical, if |
The utility function is defined as
where is a threshold for imposing harsher FWER penalty.
The utility function is defined analogously as
where and are the sets of active and inactive strata,
respectively.
The (averaged) is defined in Jiang et al. (2021).
A numeric, the parameter combination's utility.
Jiang L, Nie L, Yan F, Yuan Y. Optimal Bayesian hierarchical model to accelerate the development of tissue-agnostic drugs and basket trials. Contemporary Clinical Trials. 2021;107:106460. doi:10.1016/j.cct.2021.106460
design <- baskwrap::setup_fujikawa_x(k = 3, shape1 = 1, shape2 = 1, p0 = 0.2, backend = "exact") u_2ewp(design, x = list(lambda = 0.99, epsilon = 2, tau = 0.5), detail_params = list(p1 = c(0.5, 0.2, 0.2), n = 20, weight_fun = baskwrap::weights_jsd, logbase = exp(1)), penalty1 = 1, penalty2 = 2, threshold = 0.1) u_2pow(design, x = list(lambda = 0.99, epsilon = 2, tau = 0.5), detail_params = list(p1 = c(0.5, 0.2, 0.2), n = 20, weight_fun = baskwrap::weights_jsd, logbase = exp(1)), penalty1 = 1, penalty2 = 2, threshold = 0.1)design <- baskwrap::setup_fujikawa_x(k = 3, shape1 = 1, shape2 = 1, p0 = 0.2, backend = "exact") u_2ewp(design, x = list(lambda = 0.99, epsilon = 2, tau = 0.5), detail_params = list(p1 = c(0.5, 0.2, 0.2), n = 20, weight_fun = baskwrap::weights_jsd, logbase = exp(1)), penalty1 = 1, penalty2 = 2, threshold = 0.1) u_2pow(design, x = list(lambda = 0.99, epsilon = 2, tau = 0.5), detail_params = list(p1 = c(0.5, 0.2, 0.2), n = 20, weight_fun = baskwrap::weights_jsd, logbase = exp(1)), penalty1 = 1, penalty2 = 2, threshold = 0.1)
For a utility function and a set of true scenarios
, calculate the weighted average utility function
for a set of weights with . By default,
for all .
The idea of averaging utility functions across a set of scenarios is taken
from Jiang et al. (2021).
u_avg( design, x, detail_params, utility, utility_params, p1s, weights_u = rep(1/nrow(p1s), nrow(p1s)), report_details = FALSE, penalty_maxtoer = NULL, threshold_maxtoer = NULL, use_future = FALSE )u_avg( design, x, detail_params, utility, utility_params, p1s, weights_u = rep(1/nrow(p1s), nrow(p1s)), report_details = FALSE, penalty_maxtoer = NULL, threshold_maxtoer = NULL, use_future = FALSE )
design |
An object of class |
x |
A named list, the design's tuning parameters to be optimized. |
detail_params |
A named list of parameters that need to be supplied to
|
utility |
A function returning the utility of a parameter combination
|
utility_params |
A named list of further parameters that need to be supplied to the utility function. |
p1s |
A numeric array in which each row defines a scenario of true response rates under the alternative hypothesis. |
weights_u |
A numeric vector of weights for calculating the weighted average. |
report_details |
A logical, if |
penalty_maxtoer |
A numeric, the penalty for punishing the maximal TOER across all considered scenarios and all strata. |
threshold_maxtoer |
A numeric, above this threshold maximal TOER is punished
by returning |
use_future |
A logical, should |
A numeric, the parameter combination's utility.
Jiang L, Nie L, Yan F, Yuan Y. Optimal Bayesian hierarchical model to accelerate the development of tissue-agnostic drugs and basket trials. Contemporary Clinical Trials. 2021;107:106460. doi:10.1016/j.cct.2021.106460
design <- baskwrap::setup_fujikawa_x(k = 3, shape1 = 1, shape2 = 1, p0 = 0.2, backend = "exact") x <- list(lambda = 0.99, epsilon = 2, tau = 0.5) detail_params <- list(n = 20, weight_fun = baskwrap::weights_jsd, logbase = exp(1)) p1s <- rbind(c(0.2,0.2,0.2), c(0.2,0.2,0.5), c(0.2,0.5,0.5), c(0.5,0.5,0.5)) # Averaging over u_ewp() u_avg(design, x = x, detail_params = detail_params, utility = u_ewp, utility_params = list(penalty = 1, threshold = 0.1), p1s = p1s ) # Averaging over u_2ewp() utility_params_2ewp <- list(penalty1 = 1, penalty2 = 2, threshold = 0.1) u_avg(design, x = x, detail_params = detail_params, utility = u_2ewp, utility_params = utility_params_2ewp, p1s = p1s ) # Punishing maximal TOER in all scenarios and all strata u_avg(design, x = x, detail_params = detail_params, utility = u_2ewp, utility_params = utility_params_2ewp, p1s = p1s, penalty_maxtoer = 1, threshold_maxtoer = 0.1 )design <- baskwrap::setup_fujikawa_x(k = 3, shape1 = 1, shape2 = 1, p0 = 0.2, backend = "exact") x <- list(lambda = 0.99, epsilon = 2, tau = 0.5) detail_params <- list(n = 20, weight_fun = baskwrap::weights_jsd, logbase = exp(1)) p1s <- rbind(c(0.2,0.2,0.2), c(0.2,0.2,0.5), c(0.2,0.5,0.5), c(0.5,0.5,0.5)) # Averaging over u_ewp() u_avg(design, x = x, detail_params = detail_params, utility = u_ewp, utility_params = list(penalty = 1, threshold = 0.1), p1s = p1s ) # Averaging over u_2ewp() utility_params_2ewp <- list(penalty1 = 1, penalty2 = 2, threshold = 0.1) u_avg(design, x = x, detail_params = detail_params, utility = u_2ewp, utility_params = utility_params_2ewp, p1s = p1s ) # Punishing maximal TOER in all scenarios and all strata u_avg(design, x = x, detail_params = detail_params, utility = u_2ewp, utility_params = utility_params_2ewp, p1s = p1s, penalty_maxtoer = 1, threshold_maxtoer = 0.1 )
This function manually implements boundaries for a given utility function
utility. If the vector x lies out of the lower and upper bounds, the
function returns NA_real_. Else it returns the utility functions value.
u_bnd( design, x, detail_params, utility, utility_params, lower, upper, report_details = FALSE )u_bnd( design, x, detail_params, utility, utility_params, lower, upper, report_details = FALSE )
design |
An object of class |
x |
A named list, the design's tuning parameters to be optimized. |
detail_params |
A named list of parameters that need to be supplied to
|
utility |
A function returning the utility of a parameter combination
|
utility_params |
A named list of further parameters that need to be supplied to the utility function. |
lower |
numerical, a vector of lower bounds of the parameters. |
upper |
numerical, a vector of upper bounds of the parameters. |
report_details |
A logical, if |
A numeric, the parameter combination's utility.
design <- baskwrap::setup_fujikawa_x(k = 3, shape1 = 1, shape2 = 1, p0 = 0.2, backend = "exact") lower <- list(lambda = 0, epsilon = 1, tau = 0) upper <- list(lambda = 1, epsilon = 10, tau = 1) utility_params <- list(penalty = 1, threshold = 0.1) detail_params <- list(p1 = c(0.5, 0.2, 0.2), n = 20, weight_fun = baskwrap::weights_jsd, logbase = exp(1)) # Out of bounds u_bnd(design = design, x = list(lambda = 1.3, epsilon = 2, tau = 0.5), detail_params = detail_params, utility = u_ewp, utility_params = utility_params, lower = lower, upper = upper) # Inside bounds, this is the same as u_ewp x <- list(lambda = 0.99, epsilon = 2, tau = 0.5) u_bnd(design = design, x = x, detail_params = detail_params, utility = u_ewp, utility_params = utility_params, lower = lower, upper = upper) u_ewp(design = design, x = x, detail_params = detail_params, penalty = 1, threshold = 0.1)design <- baskwrap::setup_fujikawa_x(k = 3, shape1 = 1, shape2 = 1, p0 = 0.2, backend = "exact") lower <- list(lambda = 0, epsilon = 1, tau = 0) upper <- list(lambda = 1, epsilon = 10, tau = 1) utility_params <- list(penalty = 1, threshold = 0.1) detail_params <- list(p1 = c(0.5, 0.2, 0.2), n = 20, weight_fun = baskwrap::weights_jsd, logbase = exp(1)) # Out of bounds u_bnd(design = design, x = list(lambda = 1.3, epsilon = 2, tau = 0.5), detail_params = detail_params, utility = u_ewp, utility_params = utility_params, lower = lower, upper = upper) # Inside bounds, this is the same as u_ewp x <- list(lambda = 0.99, epsilon = 2, tau = 0.5) u_bnd(design = design, x = x, detail_params = detail_params, utility = u_ewp, utility_params = utility_params, lower = lower, upper = upper) u_ewp(design = design, x = x, detail_params = detail_params, penalty = 1, threshold = 0.1)
These utility functions return experiment-wise power (EWP) or expected number of correct decisions (ECD) if the family-wise error rate (FWER) is low and the negative FWER multiplied by a penalty parameter if the FWER is high.
u_ewp( design, x, detail_params, p1 = NULL, p2 = rep(design$p0, design$k), threshold, penalty, report_details = FALSE, reduce_calculations = ifelse(design$backend == "exact", TRUE, FALSE) ) u_ecd( design, x, detail_params, p1 = NULL, p2 = rep(design$p0, design$k), penalty, threshold, report_details = FALSE, reduce_calculations = ifelse(design$backend == "exact", TRUE, FALSE) )u_ewp( design, x, detail_params, p1 = NULL, p2 = rep(design$p0, design$k), threshold, penalty, report_details = FALSE, reduce_calculations = ifelse(design$backend == "exact", TRUE, FALSE) ) u_ecd( design, x, detail_params, p1 = NULL, p2 = rep(design$p0, design$k), penalty, threshold, report_details = FALSE, reduce_calculations = ifelse(design$backend == "exact", TRUE, FALSE) )
design |
An object of class |
x |
A named list, the design's tuning parameters to be optimized. |
detail_params |
A named list of parameters that need to be supplied to
|
p1 |
A numeric, response scenario for calculating power resp. ECD. This
can also be left |
p2 |
A numeric, response scenario for calculating FWER, default is the global null scenario. |
threshold |
A numeric, for high FWER above this threshold we impose a penalty, default: 0.1. |
penalty |
A numeric, the scaling factor for FWER penalty, default: 1. |
report_details |
A logical, if |
reduce_calculations |
A logical, only takes effect for the
|
The utility function u_ewp is defined as
defined as
if the FWER fulfills , and
if . The parameter
is called the threshold, the parameter is called
the penalty.
The utility function u_ecd is defined analogously with the expected number
of correct decisions (ECD) instead of the experiment-wise power. The use of
ECD together with FWER constraints in the context of basket trials stems
from Broglio et al. (2020).
A numeric, the parameter combination's utility.
Broglio KR, Zhang F, Yu B, et al. A Comparison of Different Approaches to Bayesian Hierarchical Models in a Basket Trial to Evaluate the Benefits of Increasing Complexity. Statistics in Biopharmaceutical Research. 2022;14(3):324-333. doi:10.1080/19466315.2021.2008484
# Calculating the EWP utility using basksim as a backend design <- baskwrap::setup_fujikawa_x(k = 3, shape1 = 1, shape2 = 1, p0 = 0.2) u_ewp(design, x = list(lambda = 0.99, epsilon = 2, tau = 0.5), detail_params = list(p1 = c(0.5, 0.2, 0.2), n = 20, iter = 100, logbase = exp(1)), penalty = 1, threshold = 0.1) # Calculating the ECD utility using baskexact as a backend design_x <- baskwrap::setup_fujikawa_x(k = 3, shape1 = 1, shape2 = 1, p0 = 0.2, backend = "exact") u_ecd(design_x, x = list(lambda = 0.99, epsilon = 2, tau = 0.5), detail_params = list(p1 = c(0.5, 0.2, 0.2), n = 20, weight_fun = baskwrap::weights_jsd, logbase = exp(1)), penalty = 1, threshold = 0.1)# Calculating the EWP utility using basksim as a backend design <- baskwrap::setup_fujikawa_x(k = 3, shape1 = 1, shape2 = 1, p0 = 0.2) u_ewp(design, x = list(lambda = 0.99, epsilon = 2, tau = 0.5), detail_params = list(p1 = c(0.5, 0.2, 0.2), n = 20, iter = 100, logbase = exp(1)), penalty = 1, threshold = 0.1) # Calculating the ECD utility using baskexact as a backend design_x <- baskwrap::setup_fujikawa_x(k = 3, shape1 = 1, shape2 = 1, p0 = 0.2, backend = "exact") u_ecd(design_x, x = list(lambda = 0.99, epsilon = 2, tau = 0.5), detail_params = list(p1 = c(0.5, 0.2, 0.2), n = 20, weight_fun = baskwrap::weights_jsd, logbase = exp(1)), penalty = 1, threshold = 0.1)