dampack
has the functionality to interface with a user-defined decision model in R
to conduct a deterministic sensitivity analysis (DSA) on parameters of interest. DSA is a method for assessing how the results of a decision analytic model change as a parameter of interest in varied over a specified range of values. This includes both how model outcomes change (e.g. the expected cost of each strategy) as well as how the decision changes (e.g. which strategy is the most cost-effective) with the parameter of interest.
In a DSA, the model is run for each value of the parameter of interest over the specified range, while holding all other parameter values fixed. If one parameter is varied, this is called a one-way DSA. If two parameters are varied, it is called a two-way DSA. We typically do not vary more than two parameters at a time, as the output becomes difficult to visualize and interpret. A more comprehensive assessment of how model outptus depend on model parameters can be done through a probabilistic sensitivity analysis (PSA) (type vignette("psa_generation", package = "dampack")
in the console after installing the dampack
package to see a vignette describing how to use dampack
for PSA).
dampack
includes functionality to generate DSA results for any user-defined decision analytic model that is written in R
. The user-defined model must be written as a function takes in a list of all model parameter values as its first argument and outputs a data frame of modeled outcomes (e.g. costs, QALYs, etc.) for each strategy being evaluated by the model. The first column of the output data frame must be the strategy names (as strings) with any number of additional outcomes (costs, QALYs, infections averted, etc.) stored in subsequent columns. Thus, each row of the output data frame consists of the strategy’s name followed by the corresponding outcome values for that strategy. The user-defined model function may have additional required or optional input arguments; values for these additional arguments will need to be passed to the function through the dampack
DSA functions.
When using a decision analytic model to compare different potential strategies, it is often helpful to construct two functions: the model function that reflects the dynamic process being modeled and a wrapper function that runs the model with parameters that are modified to reflect the impact of different strategies. The example below will illustrate this kind of set up.
In this example, we consider a disease that can be represented as a four-state cohort state transition model (also known as a Markov model) with the health states “Healthy”, “Sick”, “Sicker”, and “Dead”. We aptly call this model the Sick-Sicker model. Individuals who are initially healthy are at risk of becomnig sick (transition to the “Sick” state). In the “Sick” state, individuals can either recover back to “Healthy” or progress to the worse “Sicker” health state, which has a lower utility and higher costs than the “Sick” and “Healthy” states. Once in the “Sicker” health states, individuals cannot recover. In every health state, individuals have a probability of dying, which is elevated for individuals in the “Sick” and “Sicker” states relative to the “Healthy” state. Individuals who die transition to the “Dead” state. For a deeper discusison of state transition models and more complex variations of this state transition model, see Alarid-Escudero F, Krijkamp EM, Enns EA, Yang A, Hunink MGM, Pechlivanoglou P, Jalal H. Cohort state-transition models in R: A Tutorial. arXiv:200107824v1. 2020:1-31.
The Sick-Sicker model is implemented as the function run_sick_sicker_model
below:
<- function(l_params, verbose = FALSE) {
run_sick_sicker_model with(as.list(l_params), {
# l_params must include:
# -- disease progression parameters (annual): r_HD, p_S1S2, hr_S1D, hr_S2D,
# -- initial cohort distribution: v_s_init
# -- vector of annual state utilities: v_state_utility = c(u_H, u_S1, u_S2, u_D)
# -- vector of annual state costs: v_state_cost = c(c_H, c_S1, c_S2, c_D)
# -- time horizon (in annual cycles): n_cyles
# -- annual discount rate: r_disc
####### SET INTERNAL PARAMETERS #########################################
# state names
<- c("H", "S1", "S2", "D")
v_names_states <- length(v_names_states)
n_states
# vector of discount weights
<- 1 / ((1 + r_disc) ^ (0:n_cycles))
v_dw
# state rewards
<- c("H" = c_H, "S1" = c_S1, "S2" = c_S2, "D" = c_D)
v_state_cost <- c("H" = u_H, "S1" = u_S1, "S2" = u_S2, "D" = u_D)
v_state_utility
# transition probability values
<- hr_S1D * r_HD # rate of death in sick state
r_S1D <- hr_S2D * r_HD # rate of death in sicker state
r_S2D <- 1 - exp(-r_S1D) # probability of dying when sick
p_S1D <- 1 - exp(-r_S2D) # probability of dying when sicker
p_S2D <- 1 - exp(-r_HD) # probability of dying when healthy
p_HD
## Initialize transition probability matrix
# all transitions to a non-death state are assumed to be conditional on survival
<- matrix(0,
m_P nrow = n_states, ncol = n_states,
dimnames = list(v_names_states, v_names_states)) # define row and column names
## Fill in matrix
# From H
"H", "H"] <- (1 - p_HD) * (1 - p_HS1)
m_P["H", "S1"] <- (1 - p_HD) * p_HS1
m_P["H", "D"] <- p_HD
m_P[# From S1
"S1", "H"] <- (1 - p_S1D) * p_S1H
m_P["S1", "S1"] <- (1 - p_S1D) * (1 - (p_S1H + p_S1S2))
m_P["S1", "S2"] <- (1 - p_S1D) * p_S1S2
m_P["S1", "D"] <- p_S1D
m_P[# From S2
"S2", "S2"] <- 1 - p_S2D
m_P["S2", "D"] <- p_S2D
m_P[# From D
"D", "D"] <- 1
m_P[
# check that all transition matrix entries are between 0 and 1
if(!all(m_P <= 1 & m_P >= 0)){
stop("This is not a valid transition matrix (entries are not between 0 and 1")
else
} # check transition matrix rows add up to 1
if (!all.equal(as.numeric(rowSums(m_P)),rep(1,n_states))){
stop("This is not a valid transition matrix (rows do not sum to 1)")
}
####### INITIALIZATION #########################################
# create the cohort trace
<- matrix(NA, nrow = n_cycles + 1 ,
m_Trace ncol = n_states,
dimnames = list(0:n_cycles, v_names_states)) # create Markov trace
# create vectors of costs and QALYs
<- v_Q <- numeric(length = n_cycles + 1)
v_C
############# PROCESS ###########################################
1, ] <- v_s_init # initialize Markov trace
m_Trace[1] <- 0 # no upfront costs
v_C[1] <- 0 # no upfront QALYs
v_Q[
for (t in 1:n_cycles){ # throughout the number of cycles
+ 1, ] <- m_Trace[t, ] %*% m_P # calculate trace for cycle (t + 1) based on cycle t
m_Trace[t
+ 1] <- m_Trace[t + 1, ] %*% v_state_cost
v_C[t
+ 1] <- m_Trace[t + 1, ] %*% v_state_utility
v_Q[t
}
############# PRIMARY ECONOMIC OUTPUTS #########################
# Total discounted costs
<- t(v_C) %*% v_dw
n_tot_cost
# Total discounted QALYs
<- t(v_Q) %*% v_dw
n_tot_qaly
############# OTHER OUTPUTS ###################################
# Total discounted life-years (sometimes used instead of QALYs)
<- t(m_Trace %*% c(1, 1, 1, 0)) %*% v_dw
n_tot_ly
####### RETURN OUTPUT ###########################################
<- list(m_Trace = m_Trace,
out m_P = m_P,
l_params,n_tot_cost = n_tot_cost,
n_tot_qaly = n_tot_qaly,
n_tot_ly = n_tot_ly)
return(out)
}
) }
Note that this function outputs costs, QALYs, and LYs for a given set of inputs (passed as the list l_params
). Different strategies can be modeled by calling run_sick_sicker_model
with different parameter values reflect the impacts of those strategies. In this example, we will use the Sick-Sicker model to evaluate three strateges: “No_Treatment”, “Treatment_A”, and “Treatment_B”. The “No_Treatment” strategy is just the Sick-Sicker natural history, while both “Treatment_A” and “Treatment_B” improve some aspect of the disease. These treatments are taken whenever someone is ill (Sick or Sicker), which increases the cost of being in those states by the cost of treatment.“Treatment_A” improves the utility for those in the Sick states (but not in the Sicker state), while “Treatment_B” reduces the rate of progressing from the Sick state to the Sicker state. Note that we assume that treatment cannot be targeted to just the Sick state because we assume that for this disease it is difficult to differentiate between patients in the Sick and Sicker states. Thus treatment costs are incurred by those in the Sicker state even if they do not benefit from it.
We define a second function, simulate_strategies
that calls the Sick-Sicker model with the approrpiate inputs values for these three strategies. The simulate_strategies
function also takes a list of parameter values (l_params
), which must include both parameters for the markov model and any parameters governing the three strategies to be evaluated. simulate_strategies
outputs a data frame with costs, QALYs, life-years (LYs), and net monetary benefit (NMB) for each of the three strategies. It is this function that we will pass to dampack
’s internal functions for conducting DSA.
<- function(l_params, wtp = 100000){
simulate_strategies # l_params_all must include:
# -- *** Model parameters ***
# -- disease progression parameters (annual): r_HD, p_S1S2, hr_S1D, hr_S2D,
# -- initial cohort distribution: v_s_init
# -- vector of annual state utilities: v_state_utility = c(u_H, u_S1, u_S2, u_D)
# -- vector of annual state costs: v_state_cost = c(c_H, c_S1, c_S2, c_D)
# -- time horizon (in annual cycles): n_cyles
# -- annual discount rate: r_disc
# -- *** Strategy specific parameters ***
# -- treartment costs (applied to Sick and Sicker states): c_trtA, c_trtB
# -- utility with Treatment_A (for Sick state only): u_trtA
# -- hazard ratio of progression with Treatment_B: hr_S1S1_trtB
with(as.list(l_params), {
####### SET INTERNAL PARAMETERS #########################################
# Strategy names
<- c("No_Treatment", "Treatment_A", "Treatment_B")
v_names_strat # Number of strategies
<- length(v_names_strat)
n_strat
## Treatment_A
# utility impacts
<- u_trtA
u_S1_trtA # include treatment costs
<- c_S1 + c_trtA
c_S1_trtA <- c_S2 + c_trtA
c_S2_trtA
## Treatment_B
# progression impacts
<- -log(1 - p_S1S2) * hr_S1S2_trtB
r_S1S2_trtB <- 1 - exp(-r_S1S2_trtB)
p_S1S2_trtB # include treatment costs
<- c_S1 + c_trtB
c_S1_trtB <- c_S2 + c_trtB
c_S2_trtB
####### INITIALIZATION #########################################
# Create cost-effectiveness results data frame
<- data.frame(Strategy = v_names_strat,
df_ce Cost = numeric(n_strat),
QALY = numeric(n_strat),
LY = numeric(n_strat),
stringsAsFactors = FALSE)
######### PROCESS ##############################################
for (i in 1:n_strat){
<- list(n_cycles = n_cycles, r_disc = r_disc, v_s_init = v_s_init,
l_params_markov c_H = c_H, c_S1 = c_S2, c_S2 = c_S1, c_D = c_D,
u_H = u_H, u_S1 = u_S2, u_S2 = u_S1, u_D = u_D,
r_HD = r_HD, hr_S1D = hr_S1D, hr_S2D = hr_S2D,
p_HS1 = p_HS1, p_S1H = p_S1H, p_S1S2 = p_S1S2)
if (v_names_strat[i] == "Treatment_A"){
$u_S1 <- u_S1_trtA
l_params_markov$c_S1 <- c_S1_trtA
l_params_markov$c_S2 <- c_S2_trtA
l_params_markov
else if(v_names_strat[i] == "Treatment_B"){
} $p_S1S2 <- p_S1S2_trtB
l_params_markov$c_S1 <- c_S1_trtB
l_params_markov$c_S2 <- c_S2_trtB
l_params_markov
}
<- run_sick_sicker_model(l_params_markov)
l_result
c("Cost", "QALY", "LY")] <- c(l_result$n_tot_cost,
df_ce[i, $n_tot_qaly,
l_result$n_tot_ly)
l_result"NMB"] <- l_result$n_tot_qaly * wtp - l_result$n_tot_cost
df_ce[i,
}
return(df_ce)
}) }
To demonstrate the simulate_strategies
function, we define a list of model and strategy parameters. We call this our basecase list of parameters, as these will be the default parameter values used when conducting the DSA.
<- list(p_HS1 = 0.15,
my_params_basecase p_S1H = 0.5,
p_S1S2 = 0.105,
r_HD = 0.002,
hr_S1D = 3,
hr_S2D = 10,
hr_S1S2_trtB = 0.6,
c_H = 2000,
c_S1 = 4000,
c_S2 = 15000,
c_D = 0,
c_trtA = 12000,
c_trtB = 13000,
u_H = 1,
u_S1 = 0.75,
u_S2 = 0.5,
u_D = 0,
u_trtA = 0.95,
n_cycles = 75,
v_s_init = c(1, 0, 0, 0),
r_disc = 0.03)
If we then call simulate_strategies
with the list my_params_basecase
, we will get a data frame of strategy outcomes:
<- simulate_strategies(my_params_basecase)
df_ce
df_ce#> Strategy Cost QALY LY NMB
#> 1 No_Treatment 112005.4 21.66567 26.24113 2054562
#> 2 Treatment_A 276350.4 23.31164 26.24113 2054814
#> 3 Treatment_B 248990.2 22.49849 26.95238 2000859
Next, we will use the functionality within dampack
to generate one- and two-way DSAs using our user-defined function, simulate_strategies
. A simple one-way DSA starts with choosing a model parameter to be investigated. Next, the modeler specifies a range for this parameter and the number of evenly spaced points along this range at which to evaluate model outcomes. The model is then run for each element of the vector of parameter values by setting the parameter of interest to the value, holding all other model parameters at their default base case values.
The dampack
functions run_owsa_det
and run_twsa_det
are used to execute one- and two-way DSA, respectively.
To execute a one-way DSA, we first create a data frame that contains the list of parameter names to be varied (first column) and the minimum (second column) and maximum (third column) values of the range of values for each parameter. In the example below, we will conduct four one-way DSAs separately on four different parameters: the utility benefit of Treatment_A; the cost of Treatment_A; the reduction, as a hazard ratio, on the rate of progressing from Sick to Sicker with Treatment_B; and the rate of dying from the Healthy state.
<- data.frame(pars = c("u_trtA", "c_trtA", "hr_S1S2_trtB", "r_HD"),
my_owsa_params_range min = c(0.9, 9000, 0.3, 0.001),
max = c(1, 24000, 0.9, 0.003))
To conduct the one-way DSA, we then call the function run_owsa_det
with my_owsa_params_range
, specifying the parameters to be varied and over which ranges, and my_params_basecase
as the fixed parameter values to be used in the model when a parameter is not being explicitly varied in the one-way sensitivity analysis. Note that the parameters specified in my_owsa_params_range$pars
are a subset of the parameters listed in my_params_basecase
.
Additional inputs are the number of equally-spaced samples (nsamp
) to be used between the specified minimum and maximum of each range, the user-defined function (FUN
) to be called to generate model outcomes for each strategy, the vector of outcomes to be stored (must be outcomes generated by the data frame output of the function passed in FUN
), and the vector of strategy names to be evaluated.
The input progress = TRUE
allows the user to see a progress bar as the DSA is conducted. When many parameters are being varied, nsamp
is large, and/or the user-defined function is computationally burdensome, the DSA may take a noticeable amount of time to compute and the progress display is recommended.
library(dampack)
<- run_owsa_det(params_range = my_owsa_params_range,
l_owsa_det params_basecase = my_params_basecase,
nsamp = 100,
FUN = simulate_strategies,
outcomes = c("Cost", "QALY", "LY", "NMB"),
strategies = c("No_Treatment", "Treatment_A", "Treatment_B"),
progress = FALSE)
Because we have defined multiple parameters in my_owsa_params_range
, we have instructed run_owsa_det
to execute a series of separate one-way sensitivity analyses and compile the results into a single owsa
object for each requested outcome
. When only one outcome is specified run_owsa_det
returns a owsa
data frame. When more than one outcome is specified, run_owsa_det
returns a list containing one owsa
data frame for each outcome. To access the owsa
object corresponding to a given outcome, one can select the list item with the name “owsa_owsa
object associated with the NMB
outcome can be accessed as l_owsa_det$owsa_NMB
.
Each owsa
object returned by run_owsa_det
is a data frame with four columns: parameter
, strategy
, param_val
, and outcome_val
. For each row, param_val
is the value used for the parameter listed in parameter
and outcome_val
is the value of the specified outcome for the strategy listed in strategy
. The owsa
object can now be visualized using the functionality within dampack
, which includes an owsa
plot method and a visualization of the optimal strategy over each parameter range (function owsa_opt_strat
).
# Select the net monetary benefit (NMB) owsa object
<- l_owsa_det$owsa_NMB
my_owsa_NMB
# Plot outcome of each strategy over each parameter range
plot(my_owsa_NMB,
n_x_ticks = 3)
# Visualize optimal strategy (max NMB) over each parameter range
owsa_opt_strat(my_owsa_NMB)
A two-way DSA is used to assess how model outcomes vary over specified ranges of two model parameters jointly. Similar to a one-way DSA, we first create a data frame with the (two) parameters that we wish to vary and their individual ranges. This data frame must have exactly two rows since the two-way DSA requires exactly two parameters.
<- data.frame(pars = c("hr_S1S2_trtB", "r_HD"),
my_twsa_params_range min = c(0.3, 0.001),
max = c(0.9, 0.003))
To conduct the two-way DSA, we then call the function run_twsa_det
with my_twsa_params_range
. The general format of the function arguments for run_twsa_det
are the same as those for run_owsa_det
. In run_twsa_det
, equally spaced sequences of length nsamp
are created for the two parameters based on the inputs provided in the params_range
argument. These two sequences of parameter values define an nsamp
by nsamp
grid over which FUN
is applied to produce outcomes for every combination of the two parameters.
<- run_twsa_det(params_range = my_twsa_params_range,
l_twsa_det params_basecase = my_params_basecase,
nsamp = 50,
FUN = simulate_strategies,
outcomes = c("Cost", "QALY", "NMB"),
strategies = c("No_Treatment", "Treatment_A", "Treatment_B"),
progress = FALSE)
Like in the one-way DSA, if only one outcome is specified, then run_twsa_det
will return a twsa
data frame. If more than one outcome is specified, run_twsa_det
returns a list containing one twsa
object for each outcome. To access the twsa
object corresponding to a given outcome, one can select the list item with the name “twsa_twsa
object associated with the NMB
outcome can be accessed as l_twsa_det$owsa_NMB
.
Each twsa
object returned by run_twsa_det
is a data frame with four columns. The first two columns are named according to the two parameters specified in params_range
. Each row is the value of that parameter corresponding to the outcome value in the outcome_val
(fourth) column for the strategy listed in the strategy
(third) column. The twsa
object can now be visualized using the functionality within dampack
. The twsa
plot method shows the optimal strategy as a function of the two parameters being varied in the two-way DSA, which is the primary way that two-way analyses are reported.
<- l_twsa_det$twsa_NMB
my_twsa_NMB
# plot optimal strategy (max NMB) as a function of the two parameters varied in the two-way DSA
plot(my_twsa_NMB)