M0_getting_started_markov_gmod.Rmd
The purpose of this vignette is to demonstrate how gmod can be used to build common features of a Markov model. For decision modeling, please check the getting started with decision modeling vignette.
For a graphical interface to build the gmod syntax, please check https://www.dashlab.ca/projects/decision_twig/.
This vignette only explains the features one at a time to emphasize each individual feature. For examples on how to build a complete Markov model or a decision model, please refer to the detailed introductory and advanced Markov tutorial vignettes for Markov models, and the Herpes Simplex Virus (HSV) and the Doubilet vignettes for decision tree examples.
gmod is built on the grammar of modeling principle which aims to describe a model in terms of its main components. The idea is to reduce the complexity of a decision/Markov model to it’s principal components (Twigs). gmod is inspired by the grammer of graphics (ggplot) and the grammar of data wranggling (dplyr) package for data wrangling. Similar to ggplot which builds a graph by combining layers, gmod then writes the model code for you!
gmod has two main external functions. The first function is
gmod()
which instantiates the basic gmod object that can be
supplemented with one or more twigs containing the components of the
Markov model.
a gmod syntax for a Markov model currently consist of the following twigs:
decisions
: a single twig that lists the decisions or
strategies that we want to comparestates
: a single twig that describes the Markov states,
their initial probabitlities and the number of tunnel cycles (defaults
to 1).event
: one or more twig describing, how events control
transition among states. These transitions can be conditional on the
decisions, Markov states, previous events, number of cycles, and number
of cycles in a state (i.e., tunnels).payoffs
: a single payoff twig, such as costs,
effectiveness, utility, … etc. Similar to the events, these can also
depend on the decisions, Markov states, events, number of cycles, and
number of cycles in a state (i.e., tunnels).The second function is gmod_gen_model_function()
which
returns mainly creates a new function named my_markov_model
that represents the model code in standard R that can be run
independently of gmod. This function also returns other useful
information, such as the model structure and the equations involved in
the transition probabilities and payoffs …etc.
The my_markov_model()
represents the entire model
structure, and can be called directly. This function takes in a
parameter list param
which include the model parameter
values, and returns the Markov results, such as the discounted lifetime
costs and quality adjusted life-years (QALYs). It is important to pass
the number of cycles n_cycles
as a parameter as we
discussed below. As described below, in addition to the main payoffs,
this function can also return other intermediate outputs, such as the
transitional probability matrices, the cohort’s Markov trace over time,
and others.
We will start with building a minimal Markov model to illustrate the
basic principles of gmod
. Then, we will introduce how to
add other functionality, such as cycle dependency, tunnels…etc. For each
feature, we will only show how to add that feature to reduce complexity.
For full models that contain many of these features in the same model,
please refer to the introductory and advanced Markov tutorials
vignettes.
We will adapt the sick-sicker model (ref) that we often use in our DARTH materials and teachings.
First, install gmod using devtools. Also, make sure to enable use_vignettes so that vignettes render correctly.
#library(devtools)
#install_github("hjalal/gmod", use_vignettes = TRUE, force = TRUE)
Then, load the gmod
library and the
magrittr
library needed for the pipe %>%
operator.
rm(list = ls())
library(gmod)
#> Please note that active development of gmod is now moved to twig. Please install twig https://hjalal.github.io/twig/ and review the vignettes for the latest features.
library(magrittr)
We will start with a basic gmod()
syntax looks like
this:
mygmod <- gmod() +
decisions("StandardOfCare", "StrategyA") +
states(names = c("H", "S", "D"), init_probs = c(1,0,0)) +
event(name = "only_event",
scenarios = c("get_sick", "die", "stay"),
probs = c(pGetSick(state), pDie(decision, state), Inf),
outcomes = c("S", "D", "curr_state")) +
payoffs(cost = compute_cost(decision, state))
To have a graphical representation, we can copy this code into Decision Twigs to generate the twigs:
Here are the explanation of what each twig does:
decisions("StandardOfCare", "StrategyA")
: define the
first twig which is a decision with two strategies involved:
StandardOfCare and StrategyAstates(...)
: defines the Markov twig. There must be
only one of these per model. The states funciton takes in 3 arguments:
event(...)
: is the most powerful feature in gmod and it
governs how the states are linked through events. In the Grammar of
Modeling we distinguish between a state and an event.
We use a noun for a state
to indicate where the cohort
stays a full cycle (e.g., one year), and use a verb to refer to an
event
, which in a Markov world is instantaneous and doesn’t
take any time at all (e.g., get_sick
for getting sick).
There has to be at least one event
twig, but there can be
more events that can be chained together as we explain below. The event
mapping takes in four arguments: name, scenarios, outcomes, and probs.
We will explain each of these separately:
event = "only_event"
: defines the event name. Each
event must have a unique name that can be referenced by other events’s
probability statements or payoff equations as described below.scenarios = c("get_sick", "die", "stay")
: defines the
event scenarios: here the event can only be one of these: getting sick,
dying or staying in the current state.probs = c(pGetSick(state), pDie(decision, state), Inf))
:
these are the probabilities of the correspondign scenarios occuring. A
powerful feature of gmod is that it allows these probabilities to be
conditional on other model components. Here we define the probability of
only_event==get_sick by calling pGetSick(state) which is dependent on
the state. Similarly, the probability of only_event==die is defined by
calling pDie(decision, state)
which we tell gmod that is
dependent on the state and the decision. pGetSick
and
pDie
are user-defined functions that we describe below. The
Inf
is used to tell gmod to compute the complementary
probability for the probability of staying in the same state: (i.e.,
1-pGetSick(state)-pDie(state)) to reduce the amount of typing
needed.outcomes = c("S", "D", "curr_state")
: defines the
outcomes of each one of the event values. getting sick ends up in the S
state, dying ends up in the D state, and staying will return to
whichever state the cohort was in using the special keyword
"curr_state"
.payoffs(cost = cost(decision, state))
: defines a single
payoff hers. Here we only have a cost payoff that is dependent on the
state and decision. More than one payoff can be described here, but only
a single payoff twig is allowed. And, similar to the probs argument
these can be a function of other model components, such as decision,
state, cycles, events, tunnels, …etcIn this section, we define the three functions:
pGetSick
, pDie
and cost
that we
use above in our event_mapping
and the
payoffs()
layers above:
pGetSick <- function(state){
if(state=="H") 0.1 else 0
}
pDie <- function(decision, state){
switch(state, "H"=0.01,
"S"= if (decision=="StrategyA") 0.02 else 0.05,
"D"=0)
}
compute_cost <- function(decision, state){
cost_state <- switch(state, "H"=1000, "S"=5000, "D"=0)
cost_decision <- if(decision=="StrategyA" & state=="S") 2000 else 0
cost_state+cost_decision
}
pGetSick is a simple function with a single if else statement. Probability of getting sick is 0.1 if one is Healthy, otherwise is 0.
pDie is a function of bothe the decision and state. Probability of death is 0.01 if Healthy, 0.02 if sick (+TreatmentA), 0.05 if Sick+StandardOfCare, and 0 if already dead.
compute_cost calculates the cost of the disease by combining the cost of the state $1000 if Healthy, $5000 if sick and $0 if Dead. In addition, it computes the cost of the decision as an additional $2000 if one receives TreatmentA compared to the standard of care.
As your functions get more complex, we recommended that you test them to make sure they return outcomes for all possible combinations of states and decisions and other input arguments. For example, the simple code below tests the functions for all possible combinations of decisions and states.
Evaluate the functions
gmod_expand_functions(mygmod)
#> Note: The dataset df_compute_cost created for function compute_cost.
#> Note: The dataset df_pDie created for function pDie.
#> Note: The dataset df_pGetSick created for function pGetSick.
head(df_pDie)
#> decision state pDie
#> 1 StandardOfCare H 0.01
#> 2 StrategyA H 0.01
#> 3 StandardOfCare S 0.05
#> 4 StrategyA S 0.02
#> 5 StandardOfCare D 0.00
#> 6 StrategyA D 0.00
for (decision in c("StandardOfCare", "StrategyA")){
for (state in c("H", "S", "D")){
cat("decision:", decision, " state:", state, " pGetSick:", pGetSick(state),
" pDie:", pDie(decision, state), " cost:", compute_cost(decision, state), "\n")
}
}
#> decision: StandardOfCare state: H pGetSick: 0.1 pDie: 0.01 cost: 1000
#> decision: StandardOfCare state: S pGetSick: 0 pDie: 0.05 cost: 5000
#> decision: StandardOfCare state: D pGetSick: 0 pDie: 0 cost: 0
#> decision: StrategyA state: H pGetSick: 0.1 pDie: 0.01 cost: 1000
#> decision: StrategyA state: S pGetSick: 0 pDie: 0.02 cost: 7000
#> decision: StrategyA state: D pGetSick: 0 pDie: 0 cost: 0
The functions seem to be outputting the correct numbers. switch is similar to if else, but can be more compact than multiple if else statements.
Now we have all the ingredients to run the model.
First, we need to generate the model function code
model_struc <- gmod_gen_model_function(mygmod)
#> Warning in gmod_gen_model_function.gmod_markov(mygmod): Model function my_markov_model is generated. It can be run by calling it directly, for example this function returns the summary results:
#> my_markov_model(model_struc,params,return_transition_prob=FALSE,return_state_payoffs=FALSE,return_trace=FALSE,return_cycle_payoffs=FALSE,return_payoff_summary=TRUE)
This will create the model_struc
which contains the
model equations and formulae and can be examined, and as stated in the
generated note, this function also produces a function representation of
the Markov model. The default name of this function is
my_markov_model.
Let’s examine the content of these two object. First, let’s check the model structure
model_struc # returns the model structure including the formulae before evaluation
#> $is_cycle_dep
#> [1] FALSE
#>
#> $decisions
#> [1] "StandardOfCare" "StrategyA"
#>
#> $n_decisions
#> [1] 2
#>
#> $states
#> [1] "H" "S" "D"
#>
#> $n_states
#> [1] 3
#>
#> $states_expanded
#> [1] "H" "S" "D"
#>
#> $n_states_expanded
#> [1] 3
#>
#> $n_events
#> [1] 1
#>
#> $p0
#> $p0$StandardOfCare
#> H S D
#> 1 0 0
#>
#> $p0$StrategyA
#> H S D
#> 1 0 0
#>
#>
#> $events
#> [1] "only_event"
#>
#> $events_df
#> type event values probs outcomes
#> 1 event only_event get_sick pGetSick(state) S
#> 2 event only_event die pDie(decision,state) D
#> 3 event only_event stay 1-(pGetSick(state)+pDie(decision,state)) curr_state
#> id
#> 1 1
#> 2 2
#> 3 3
#>
#> $payoffs
#> $payoffs$cost
#> compute_cost(decision, state)
#>
#>
#> $payoff_names
#> [1] "cost"
#>
#> $n_payoffs
#> [1] 1
#>
#> $discounts
#> cost
#> 0
#>
#> $model_equations
#> # A tibble: 3 × 3
#> dest probs cost
#> <chr> <chr> <chr>
#> 1 D (pDie(decision,state)) (pDie(decision,state))*…
#> 2 S (pGetSick(state)) (pGetSick(state))*compu…
#> 3 curr_state (1-(pGetSick(state)+pDie(decision,state))) (1-(pGetSick(state)+pDi…
#>
#> attr(,"class")
#> [1] "gmod_markov"
examining the content of this structure can be helpful to debug a model by examining the individual layers and formulae. For example $model_equations returns the probability and cost formulae as plain text. These can be evaluated by calling them directly if needed. For example, if we want to know what is the probability of staying at the Healthy state we can just copy the content of the probs column corresponding to curr_state and evaluate it with the corresponding parameters:
(1-(pGetSick(state="H")+pDie(decision="StandardOfCare",state="H")))
#> [1] 0.89
Next, we can also examine the contents of the my_markov_model function which generates the text.
my_markov_model #without parenthesis returns the function text
#> function (model_struc, params = NULL, return_transition_prob = FALSE,
#> return_state_payoffs = FALSE, return_trace = FALSE, return_cycle_payoffs = FALSE,
#> return_payoff_summary = TRUE)
#> {
#> if (!is.null(params))
#> list2env(params, envir = .GlobalEnv)
#> decisions <- model_struc$decisions
#> tunnel_lengths <- model_struc$tunnel_lengths
#> tunnel_states <- model_struc$tunnel_states
#> tunnel_lengths[is.infinite(tunnel_lengths)] <- n_cycles
#> n_decisions <- model_struc$n_decisions
#> cycles <- 1:n_cycles
#> states_expanded <- model_struc$states_expanded
#> n_states_expanded <- model_struc$n_states_expanded
#> payoff_names <- model_struc$payoff_names
#> n_payoffs <- model_struc$n_payoffs
#> discounts <- eval(model_struc$discounts)
#> P <- array(0, dim = c(n_states_expanded, n_states_expanded,
#> n_decisions), dimnames = list(states_expanded, states_expanded,
#> decisions))
#> state_payoffs <- array(0, dim = c(n_states_expanded, n_decisions,
#> n_payoffs), dimnames = list(states_expanded, decisions,
#> payoff_names))
#> dest <- "D"
#> for (state_expanded in states_expanded) {
#> state <- tunnel2state(state_expanded)$state
#> for (decision in decisions) {
#> P[state_expanded, dest, decision] <- (pDie(decision,
#> state))
#> }
#> }
#> dest <- "S"
#> for (state_expanded in states_expanded) {
#> state <- tunnel2state(state_expanded)$state
#> P[state_expanded, dest, ] <- (pGetSick(state))
#> }
#> dest <- "curr_state"
#> for (state_expanded in states_expanded) {
#> state <- tunnel2state(state_expanded)$state
#> for (decision in decisions) {
#> dest <- state_expanded
#> P[state_expanded, dest, decision] <- P[state_expanded,
#> dest, decision] + (1 - (pGetSick(state) + pDie(decision,
#> state)))
#> }
#> }
#> for (state_expanded in states_expanded) {
#> state <- state_expanded
#> for (decision in decisions) {
#> state_payoffs[state_expanded, decision, "cost"] <- (pDie(decision,
#> state)) * compute_cost(decision, state) + (pGetSick(state)) *
#> compute_cost(decision, state) + (1 - (pGetSick(state) +
#> pDie(decision, state))) * compute_cost(decision,
#> state)
#> }
#> }
#> Trace <- array(NA, dim = c(n_cycles, n_states_expanded, n_decisions),
#> dimnames = list(cycles, states_expanded, decisions))
#> cycle_payoffs <- array(0, dim = c(n_cycles, n_states_expanded,
#> n_decisions, n_payoffs), dimnames = list(cycles, states_expanded,
#> decisions, payoff_names))
#> for (decision in decisions) {
#> Trace[1, , decision] <- model_struc$p0[[decision]]
#> for (i in 2:n_cycles) {
#> P_temp <- P[, , decision]
#> Trace[i, , decision] <- Trace[i - 1, , decision] %*%
#> P_temp
#> }
#> }
#> cycle_ones <- matrix(1, nrow = n_cycles, ncol = 1)
#> state_ones <- matrix(1, nrow = 1, ncol = n_states_expanded)
#> summary_payoffs <- matrix(0, nrow = n_decisions, ncol = n_payoffs,
#> dimnames = list(decisions, payoff_names))
#> for (decision in decisions) {
#> for (payoff_name in payoff_names) {
#> mat_discounts <- matrix(1/(1 + discounts[payoff_name])^(cycles -
#> 1), ncol = 1) %*% state_ones
#> Payoff_temp <- cycle_ones %*% state_payoffs[, decision,
#> payoff_name]
#> cycle_payoffs[, , decision, payoff_name] <- Trace[,
#> , decision] * Payoff_temp * mat_discounts
#> summary_payoffs[decision, payoff_name] <- sum(cycle_payoffs[,
#> , decision, payoff_name])
#> }
#> }
#> model_results <- list()
#> if (return_transition_prob)
#> model_results$P <- P
#> if (return_state_payoffs)
#> model_results$state_payoffs <- state_payoffs
#> if (return_trace)
#> model_results$Trace <- Trace
#> if (return_cycle_payoffs)
#> model_results$cycle_payoffs <- cycle_payoffs
#> if (return_payoff_summary)
#> model_results$summary_payoffs <- summary_payoffs
#> return(model_results)
#> }
#> <environment: 0x126f50e18>
The generated code is build on our introductory and advanced Markov tutorials in Medical Decision Making. For further information, please refer to these tutorials.
Now, let’s run the model function and generate our summary results of running the model for 5 cycles and computing the cumulative cost for our cohort.
n_cycles <- 10
my_markov_model(model_struc)
#> $summary_payoffs
#> cost
#> StandardOfCare 20998.32
#> StrategyA 28735.17
You can also retrieve the other components of the Markov model run, such as the Markov trace which shows the proportion of the cohort in each of the states for each of the 5 cycles.
model_results <- my_markov_model(model_struc, return_trace = T)
model_results$Trace
#> , , StandardOfCare
#>
#> H S D
#> 1 1.0000000 0.0000000 0.00000000
#> 2 0.8900000 0.1000000 0.01000000
#> 3 0.7921000 0.1840000 0.02390000
#> 4 0.7049690 0.2540100 0.04102100
#> 5 0.6274224 0.3118064 0.06077119
#> 6 0.5584059 0.3589583 0.08263573
#> 7 0.4969813 0.3968510 0.10616771
#> 8 0.4423133 0.4267066 0.13098007
#> 9 0.3936589 0.4496026 0.15673853
#> 10 0.3503564 0.4664883 0.18315525
#>
#> , , StrategyA
#>
#> H S D
#> 1 1.0000000 0.0000000 0.00000000
#> 2 0.8900000 0.1000000 0.01000000
#> 3 0.7921000 0.1870000 0.02090000
#> 4 0.7049690 0.2624700 0.03256100
#> 5 0.6274224 0.3277175 0.04486009
#> 6 0.5584059 0.3839054 0.05768866
#> 7 0.4969813 0.4320679 0.07095083
#> 8 0.4423133 0.4731246 0.08456200
#> 9 0.3936589 0.5078935 0.09844763
#> 10 0.3503564 0.5371015 0.11254209
We start by modifying some of the basic features of our markov model. We will explore how to change the number of Markov cycles, the initial probabilities, and the discount rates.
Changing the number of Markov cycles is straightforward by changing
the n_cycles
parameter in the gmod
function.
For example, below we change the number of cycles from 5 to 2:
mygmod <- gmod()
We can set the initial probabilities in various ways. Most of the time we start the cohort in the same health state. Above we showed how to set it up when everyone starts at the Healthy state.
states(names = c("H","S","D"), init_probs = c(1,0,0))
#> [[1]]
#> [[1]]$type
#> [1] "states"
#>
#> [[1]]$states
#> [1] "H" "S" "D"
#>
#>
#> [[2]]
#> [[2]]$type
#> [1] "initial_prob"
#>
#> [[2]]$states
#> [1] "H" "S" "D"
#>
#> [[2]]$probs
#> [1] 1 0 0
However, we can do any distribution we want. For example, we can define the starting probabilities for all the states.
states(names = c("H","S","D"), init_probs = c(0.7,0.2,0.1))
#> [[1]]
#> [[1]]$type
#> [1] "states"
#>
#> [[1]]$states
#> [1] "H" "S" "D"
#>
#>
#> [[2]]
#> [[2]]$type
#> [1] "initial_prob"
#>
#> [[2]]$states
#> [1] "H" "S" "D"
#>
#> [[2]]$probs
#> [1] 0.7 0.2 0.1
Or, we can even use a combination of variables and Inf as a placeholder for the complementary probabilities.
pH0 <- 0.7
pS0 <- 0.2
states(names = c("H","S","D"), init_probs = c(pH0, pS0, Inf))
#> [[1]]
#> [[1]]$type
#> [1] "states"
#>
#> [[1]]$states
#> [1] "H" "S" "D"
#>
#>
#> [[2]]
#> [[2]]$type
#> [1] "initial_prob"
#>
#> [[2]]$states
#> [1] "H" "S" "D"
#>
#> [[2]]$probs
#> [1] 0.7 0.2 Inf
To run the model with this updated probabilities, we need to add it
as a new init_prob
layer to the gmod
code.
mygmod <- gmod() +
decisions("StandardOfCare", "StrategyA") +
states(names = c("H","S","D"), init_probs = c(pH0, pS0, Inf)) +
event(name = "only_event",
scenarios = c("get_sick", "die", "stay"),
probs = c(pGetSick(state), pDie(decision, state), Inf),
outcomes = c("S", "D", "curr_state")) +
payoffs(cost = compute_cost(decision, state))
# creating the updated function and evaluating it
model_struc <- gmod_gen_model_function(mygmod)
#> Warning in gmod_gen_model_function.gmod_markov(mygmod): Model function my_markov_model is generated. It can be run by calling it directly, for example this function returns the summary results:
#> my_markov_model(model_struc,params,return_transition_prob=FALSE,return_state_payoffs=FALSE,return_trace=FALSE,return_cycle_payoffs=FALSE,return_payoff_summary=TRUE)
model_results <- my_markov_model(model_struc, return_trace = T)
model_results$Trace
#> , , StandardOfCare
#>
#> H S D
#> 1 0.7000000 0.2000000 0.1000000
#> 2 0.6230000 0.2600000 0.1170000
#> 3 0.5544700 0.3093000 0.1362300
#> 4 0.4934783 0.3492820 0.1572397
#> 5 0.4391957 0.3811657 0.1796386
#> 6 0.3908842 0.4060270 0.2030888
#> 7 0.3478869 0.4248141 0.2272990
#> 8 0.3096193 0.4383621 0.2520186
#> 9 0.2755612 0.4474059 0.2770329
#> 10 0.2452495 0.4525917 0.3021588
#>
#> , , StrategyA
#>
#> H S D
#> 1 0.7000000 0.2000000 0.1000000
#> 2 0.6230000 0.2660000 0.1110000
#> 3 0.5544700 0.3229800 0.1225500
#> 4 0.4934783 0.3719674 0.1345543
#> 5 0.4391957 0.4138759 0.1469284
#> 6 0.3908842 0.4495179 0.1595979
#> 7 0.3478869 0.4796160 0.1724971
#> 8 0.3096193 0.5048124 0.1855683
#> 9 0.2755612 0.5256780 0.1987607
#> 10 0.2452495 0.5427206 0.2120299
We can see from the first line of the Trace that H=0.7, S=0.2 and D=0.1.
gmod tries to check the composition of the probability vector and returns an error if there are any inconsistencies.
Changing the discount rate is similar to setting the initial probabilities. These can be set for all the payoffs. To illustrate, we will add an additional payoff (effectiveness), and use a discount rate of 3% per cycle for both costs and utilities.
mygmod <- gmod() +
decisions("StandardOfCare", "StrategyA") +
states(names = c("H", "S", "D"), init_probs = c(1,0,0)) +
event(name = "only_event",
scenarios = c("get_sick", "die", "stay"),
probs = c(pGetSick(state), pDie(decision, state), Inf),
outcomes = c("S", "D", "curr_state")) +
payoffs(cost = compute_cost(decision, state),
effectiveness = compute_effectiveness(state),
discount_rates = c(0.03, 0.03))
# define the effectiveness function, the rest of the function will be just like before
compute_effectiveness <- function(state){
switch(state, "H"=1, "S"=0.7, "D"=0)
}
# creating the updated function and evaluating it
model_struc <- gmod_gen_model_function(mygmod)
#> Warning in gmod_gen_model_function.gmod_markov(mygmod): Model function my_markov_model is generated. It can be run by calling it directly, for example this function returns the summary results:
#> my_markov_model(model_struc,params,return_transition_prob=FALSE,return_state_payoffs=FALSE,return_trace=FALSE,return_cycle_payoffs=FALSE,return_payoff_summary=TRUE)
# we look at the cycle payoffs which apply the discounts to the cycle payoffs.
my_markov_model(model_struc, return_cycle_payoffs=TRUE)
#> $cycle_payoffs
#> , , StandardOfCare, cost
#>
#> H S D
#> 1 1000.0000 0.0000 0
#> 2 864.0777 485.4369 0
#> 3 746.6302 867.1882 0
#> 4 645.1465 1162.2757 0
#> 5 557.4567 1385.1797 0
#> 6 481.6859 1548.2030 0
#> 7 416.2140 1661.7823 0
#> 8 359.6412 1734.7575 0
#> 9 310.7580 1774.6022 0
#> 10 268.5190 1787.6224 0
#>
#> , , StrategyA, cost
#>
#> H S D
#> 1 1000.0000 0.0000 0
#> 2 864.0777 679.6117 0
#> 3 746.6302 1233.8580 0
#> 4 645.1465 1681.3806 0
#> 5 557.4567 2038.2093 0
#> 6 481.6859 2318.1211 0
#> 7 416.2140 2532.9503 0
#> 8 359.6412 2692.8555 0
#> 9 310.7580 2806.5507 0
#> 10 268.5190 2881.5051 0
#>
#> , , StandardOfCare, effectiveness
#>
#> H S D
#> 1 1.0000000 0.00000000 0
#> 2 0.8640777 0.06796117 0
#> 3 0.7466302 0.12140635 0
#> 4 0.6451465 0.16271859 0
#> 5 0.5574567 0.19392516 0
#> 6 0.4816859 0.21674842 0
#> 7 0.4162140 0.23264952 0
#> 8 0.3596412 0.24286605 0
#> 9 0.3107580 0.24844430 0
#> 10 0.2685190 0.25026713 0
#>
#> , , StrategyA, effectiveness
#>
#> H S D
#> 1 1.0000000 0.00000000 0
#> 2 0.8640777 0.06796117 0
#> 3 0.7466302 0.12338580 0
#> 4 0.6451465 0.16813806 0
#> 5 0.5574567 0.20382093 0
#> 6 0.4816859 0.23181211 0
#> 7 0.4162140 0.25329503 0
#> 8 0.3596412 0.26928555 0
#> 9 0.3107580 0.28065507 0
#> 10 0.2685190 0.28815051 0
#>
#>
#> $summary_payoffs
#> cost effectiveness
#> StandardOfCare 18057.18 7.387116
#> StrategyA 24515.17 7.536633
Sometimes we want a probability of transition from one state to another, or a payoff’s value to depend on the cohort’s age. We can achieve this by using the special variable cycle for both transition probabilities and payoffs.
The code below shows the previous code in a single block where we modify the probability of death and cost payoff to depend on cycle. See if you can spot the difference:
mygmod <- gmod() +
decisions("StandardOfCare", "StrategyA") +
states(names = c("H", "S", "D"), init_probs = c(1,0,0)) +
event(name = "only_event",
scenarios = c("get_sick", "die", "stay"),
probs = c(pGetSick(state), pDie(decision, state, cycle), Inf),
outcomes = c("S", "D", "curr_state")) +
payoffs(cost = compute_cost(decision, state, cycle))
pGetSick <- function(state){
if(state=="H") 0.1 else 0
}
pDie <- function(decision, state, cycle){
switch(state, "H"=0.01,
"S"= if (decision=="StrategyA") 0.02 else 0.05,
"D"=0) * cycle
}
compute_cost <- function(decision, state, cycle){
cost_state <- switch(state, "H"=1000, "S"=5000, "D"=0) * cycle
cost_decision <- if(decision=="StrategyA" & state=="S") 2000 else 0
cost_state+cost_decision
}
model_struc <- gmod_gen_model_function(mygmod)
#> Warning in gmod_gen_model_function.gmod_markov(mygmod): Model function my_markov_model is generated. It can be run by calling it directly, for example this function returns the summary results:
#> my_markov_model(model_struc,params,return_transition_prob=FALSE,return_state_payoffs=FALSE,return_trace=FALSE,return_cycle_payoffs=FALSE,return_payoff_summary=TRUE)
Of course, we only need to add the variable cycle to the function
definitions pDie
and compute_cost
and also
remember to add them to the function calls in the gmod
function. For illustration we assume that pDie
and the
cost
of the states both increased as a linear function of
the Markov cycles, 1X, 2X, …, 5X for cycles 1 through 5, respectively.
Let’s check first to see that our functions are correct, and then we
will run the model and inspect the outputs.
for (decision in c("StandardOfCare", "StrategyA")){
for (state in c("H", "S", "D")){
for (cycle in 1:2){ # just print the first two cycles
cat("decision:", decision, " state:", state, " cycle:", cycle, " pGetSick:", pGetSick(state),
" pDie:", pDie(decision, state, cycle), " cost:", compute_cost(decision, state, cycle), "\n")
}
}
}
#> decision: StandardOfCare state: H cycle: 1 pGetSick: 0.1 pDie: 0.01 cost: 1000
#> decision: StandardOfCare state: H cycle: 2 pGetSick: 0.1 pDie: 0.02 cost: 2000
#> decision: StandardOfCare state: S cycle: 1 pGetSick: 0 pDie: 0.05 cost: 5000
#> decision: StandardOfCare state: S cycle: 2 pGetSick: 0 pDie: 0.1 cost: 10000
#> decision: StandardOfCare state: D cycle: 1 pGetSick: 0 pDie: 0 cost: 0
#> decision: StandardOfCare state: D cycle: 2 pGetSick: 0 pDie: 0 cost: 0
#> decision: StrategyA state: H cycle: 1 pGetSick: 0.1 pDie: 0.01 cost: 1000
#> decision: StrategyA state: H cycle: 2 pGetSick: 0.1 pDie: 0.02 cost: 2000
#> decision: StrategyA state: S cycle: 1 pGetSick: 0 pDie: 0.02 cost: 7000
#> decision: StrategyA state: S cycle: 2 pGetSick: 0 pDie: 0.04 cost: 12000
#> decision: StrategyA state: D cycle: 1 pGetSick: 0 pDie: 0 cost: 0
#> decision: StrategyA state: D cycle: 2 pGetSick: 0 pDie: 0 cost: 0
Now we can examine the impact of this on the model results
model_struc <- gmod_gen_model_function(mygmod)
#> Warning in gmod_gen_model_function.gmod_markov(mygmod): Model function my_markov_model is generated. It can be run by calling it directly, for example this function returns the summary results:
#> my_markov_model(model_struc,params,return_transition_prob=FALSE,return_state_payoffs=FALSE,return_trace=FALSE,return_cycle_payoffs=FALSE,return_payoff_summary=TRUE)
my_markov_model(model_struc)
#> $summary_payoffs
#> cost
#> StandardOfCare 64950.86
#> StrategyA 100340.56
Sometimes we have a different situation where we want to keep track of how long the cohort has been in a particular state to model history of a disease. We usually do this by creating what is called tunnel states and expanding the number of states for example S1, S2, …, etc. where a transition to S2 is only possible from S1 and to S3 from S2, … etc.
Here we show how to do this again for pDie and compute_cost. We are only interested of a history of 3 years. Again - can you spot the difference compared to the minimal model?
mygmod <- gmod() +
decisions("StandardOfCare", "StrategyA") +
states(names = c("H", "S", "D"), init_probs = c(1,0,0), max_cycle_in_states = c(1,3,1)) + #
event(name = "only_event",
scenarios = c("get_sick", "die", "stay"),
probs = c(pGetSick(state), pDie(decision, state, cycle_in_state), Inf),
outcomes = c("S", "D", "curr_state")) + #
payoffs(cost = compute_cost(decision, state, cycle_in_state)) #
pGetSick <- function(state){
if(state=="H") 0.1 else 0
}
pDie <- function(decision, state, cycle_in_state){ #
switch(state, "H"=0.01,
"S"= cycle_in_state * if (decision=="StrategyA") 0.02 else 0.05,
"D"=0)
}
compute_cost <- function(decision, state, cycle_in_state){ #
cost_state <- switch(state, "H"=1000, "S"=5000 * cycle_in_state, "D"=0) #
cost_decision <- if(decision=="StrategyA" & state=="S") 2000 else 0
cost_state+cost_decision
}
model_struc <- gmod_gen_model_function(mygmod)
#> Warning in gmod_gen_model_function.gmod_markov(mygmod): Model function my_markov_model is generated. It can be run by calling it directly, for example this function returns the summary results:
#> my_markov_model(model_struc,params,return_transition_prob=FALSE,return_state_payoffs=FALSE,return_trace=FALSE,return_cycle_payoffs=FALSE,return_payoff_summary=TRUE)
It is quite simple to add tunnel dependency by adding the
cycle_in_state
variable.
IMPORTANT: when a model has at least on state that has time
dependency, the cycle_in_state
variable becomes avaialble
for all the states. States that are not defined in the
tunnels()
layer will always return
cycle_in_state=1
Similar to above, we assume that the probability of death is a linear function of the number of cycles in S. For the cost, we also assume that the longer you stay in S the higher the cost will be - also linearly related.
The other important change is that we have to add a tunnels() layer specifying the states that have residency time history dependency. Here we assume only S has time dependency for 3 cycles only. This can be expanded to multiple states with various tunnel lengths.
Let’s check first to see that our functions are correct, and then we will run the model and inspect the outputs.
for (decision in c("StandardOfCare", "StrategyA")){
for (state in c("H", "S", "D")){
for (cycle_in_state in 1:2){ # just print the first two cycles
cat("decision:", decision, " state:", state, " cycle_in_state:", cycle_in_state, " pGetSick:", pGetSick(state),
" pDie:", pDie(decision, state, cycle_in_state), " cost:", compute_cost(decision, state, cycle_in_state), "\n")
}
}
}
#> decision: StandardOfCare state: H cycle_in_state: 1 pGetSick: 0.1 pDie: 0.01 cost: 1000
#> decision: StandardOfCare state: H cycle_in_state: 2 pGetSick: 0.1 pDie: 0.01 cost: 1000
#> decision: StandardOfCare state: S cycle_in_state: 1 pGetSick: 0 pDie: 0.05 cost: 5000
#> decision: StandardOfCare state: S cycle_in_state: 2 pGetSick: 0 pDie: 0.1 cost: 10000
#> decision: StandardOfCare state: D cycle_in_state: 1 pGetSick: 0 pDie: 0 cost: 0
#> decision: StandardOfCare state: D cycle_in_state: 2 pGetSick: 0 pDie: 0 cost: 0
#> decision: StrategyA state: H cycle_in_state: 1 pGetSick: 0.1 pDie: 0.01 cost: 1000
#> decision: StrategyA state: H cycle_in_state: 2 pGetSick: 0.1 pDie: 0.01 cost: 1000
#> decision: StrategyA state: S cycle_in_state: 1 pGetSick: 0 pDie: 0.02 cost: 7000
#> decision: StrategyA state: S cycle_in_state: 2 pGetSick: 0 pDie: 0.04 cost: 12000
#> decision: StrategyA state: D cycle_in_state: 1 pGetSick: 0 pDie: 0 cost: 0
#> decision: StrategyA state: D cycle_in_state: 2 pGetSick: 0 pDie: 0 cost: 0
Now we can examine the impact of this on the model results
model_struc <- gmod_gen_model_function(mygmod)
#> Warning in gmod_gen_model_function.gmod_markov(mygmod): Model function my_markov_model is generated. It can be run by calling it directly, for example this function returns the summary results:
#> my_markov_model(model_struc,params,return_transition_prob=FALSE,return_state_payoffs=FALSE,return_trace=FALSE,return_cycle_payoffs=FALSE,return_payoff_summary=TRUE)
model_results <- my_markov_model(model_struc, return_transition_prob = T, return_state_payoffs = T)
Let’s examine the transition probability matrix structure with the tunnel state
model_results$P # shows transition probabilities to Death
#> , , StandardOfCare
#>
#> H S_tnl1 S_tnl2 S_tnl3 D
#> H 0.89 0.1 0.00 0.00 0.01
#> S_tnl1 0.00 0.0 0.95 0.00 0.05
#> S_tnl2 0.00 0.0 0.00 0.90 0.10
#> S_tnl3 0.00 0.0 0.00 0.85 0.15
#> D 0.00 0.0 0.00 0.00 1.00
#>
#> , , StrategyA
#>
#> H S_tnl1 S_tnl2 S_tnl3 D
#> H 0.89 0.1 0.00 0.00 0.01
#> S_tnl1 0.00 0.0 0.98 0.00 0.02
#> S_tnl2 0.00 0.0 0.00 0.96 0.04
#> S_tnl3 0.00 0.0 0.00 0.94 0.06
#> D 0.00 0.0 0.00 0.00 1.00
This returns a list of two 5x5 matrices for the transition probabilities of the two strategies. These matrices describe the probability of transition from an origin state (row) to a destination state (column). Now notice that S is being replaced by S_tnl1, S_tnl2 and S_tnl3 indicating the three cycles in S as separate states. Also notice the special way transitions occur among these states. i.e., each tunnel state goes to the next one, but never back or stay in the same state except for the last state in the tunnel, here S_tnl3. Since we made the probability of death a function of the tunnel state, we can see that the probabiltiy of death for StandardOfCare (last column) for S_tnl1 = 0.05, S_tnl2=0.10 and S_tnl3=0.15 as we specified in our function. Similarly, the probability of death under StrategyA also incrases linearly from 0.02 for S_tnl1, to 0.04 for S_tnl2 and 0.06 for S_tnl3.
And, let’s examine the cycle payoffs.
model_results$state_payoffs # shows cycle payoffs of the Sick state
#> , , cost
#>
#> StandardOfCare StrategyA
#> H 1000 1000
#> S_tnl1 5000 7000
#> S_tnl2 10000 12000
#> S_tnl3 15000 17000
#> D 0 0
Here you can see that for each year in S, the cost increases by $5000.
It is relatively easy to add additional events to a Markov model. Please keep in mind that Events are quite flexible and are central to the Grammar of Modeling framework. While users are free to make branching events, we recommend to use a single linear sequence of events, and call previous event values to make the same sequence apply to all decisions and states. For example, if an event doesn’t happen for a particular decision (e.g., getting a side effect), then one can make the get_side_effect event a function of the decision, and only have a non-zero probability for that particular treatment. The Grammer of Modeling is designed aroudn the concept of a single sequence of events as it makes the code more concise and easier to manage.
Below we slightly modify the minimal example by converting the Sick
state to an event. Assume that Sick is an acute disease that only takes
a few days rather than a full cycle. We will have a sequence of events
get_sick
followed by die
. Because Sick is no
longer a state, we will use these two health states Alive and Dead and
show how the probability of death can be a function of the event
get_sick occuring before the event die. In addition, we will show how
the cost can also be a function of the value of the sick_event. We will
also look at one strategy “StandardOfCare to simplify the
illustration.
mygmod <- gmod() +
decisions("StandardOfCare") +
states(names = c("Alive", "Dead"), init_probs = c(1,0)) +
event(name = "get_sick",
scenarios = c(TRUE, FALSE),
probs = c(0.05, Inf),
outcomes = c("die", "die")) +
event(name = "die",
scenarios = c(TRUE, FALSE),
probs = c(pDie(state), Inf),
outcomes = c("Dead", "curr_state")) +
payoffs(cost = compute_cost(state))
pDie <- function(state){
# if the cohort gets sick, the probability of death = 0.01 otherwise it is 0.1
switch(state, "Alive"=0.01,
"Dead"=0)
}
compute_cost <- function(state){
cost_state <- switch(state, "Alive"=1000, "Dead"=0)
# add 2000 if someone in the Alive state gets sick
cost_state
}
model_struc <- gmod_gen_model_function(mygmod)
#> Warning in gmod_gen_model_function.gmod_markov(mygmod): Model function my_markov_model is generated. It can be run by calling it directly, for example this function returns the summary results:
#> my_markov_model(model_struc,params,return_transition_prob=FALSE,return_state_payoffs=FALSE,return_trace=FALSE,return_cycle_payoffs=FALSE,return_payoff_summary=TRUE)
my_markov_model(model_struc,return_transition_prob=TRUE,return_state_payoffs=TRUE,return_trace=TRUE,return_cycle_payoffs=TRUE,return_payoff_summary=TRUE)
#> $P
#> , , StandardOfCare
#>
#> Alive Dead
#> Alive 0.99 0.01
#> Dead 0.00 1.00
#>
#>
#> $state_payoffs
#> , , cost
#>
#> StandardOfCare
#> Alive 1000
#> Dead 0
#>
#>
#> $Trace
#> , , StandardOfCare
#>
#> Alive Dead
#> 1 1.0000000 0.00000000
#> 2 0.9900000 0.01000000
#> 3 0.9801000 0.01990000
#> 4 0.9702990 0.02970100
#> 5 0.9605960 0.03940399
#> 6 0.9509900 0.04900995
#> 7 0.9414801 0.05851985
#> 8 0.9320653 0.06793465
#> 9 0.9227447 0.07725531
#> 10 0.9135172 0.08648275
#>
#>
#> $cycle_payoffs
#> , , StandardOfCare, cost
#>
#> Alive Dead
#> 1 1000.0000 0
#> 2 990.0000 0
#> 3 980.1000 0
#> 4 970.2990 0
#> 5 960.5960 0
#> 6 950.9900 0
#> 7 941.4801 0
#> 8 932.0653 0
#> 9 922.7447 0
#> 10 913.5172 0
#>
#>
#> $summary_payoffs
#> cost
#> StandardOfCare 9561.792
Notice that now we have two sequential events. The first event is get_sick, the second one is die. Here get_sick doesn’t do much because if the cohort gets sick or not, they face the next event die. Next, we will show how to make the probabilit of death a function of the event get_sick and also make the cost also a function of this event.
In the previous section, we illustrated how to add multiple sequential events. gmod can also use these prior events to influence the transition probabilities downstream and the payoffs. This can be particularly important to put all the events on a single sequence across decisions and states. Traditional models often duplicate (copy or clone) these events which creates a nested branching problem that can be difficult to manage and troubleshoot.
The other advantage of being able to refer to any event value is the ability to associate payoffs to events. This is often referred to as transitional rewards or payoffs because these payoffs occur when an event occurs. These payoffs are quite different from state payoffs as state payoffs are associated with spending time in states compared to event (transitional) payoffs which are associated with the events that are considered instantaneous and only happens for a portion of the cohort residing in a state.
Below we modify the previous example by making the probability of death a function of the event get_sick occurring upstream. In addition, we will show how the cost can also be a function of the value of the sick_event. We will also look at one strategy “StandardOfCare to simplify the illustration.
mygmod <- gmod() +
decisions("StandardOfCare") +
states(names = c("Alive", "Dead"), init_probs = c(1,0)) +
event(name = "get_sick",
scenarios = c(TRUE, FALSE),
probs = c(0.05, Inf),
outcomes = c("die", "die")) +
event(name = "die",
scenarios = c(TRUE, FALSE),
probs = c(pDie(state, get_sick), Inf),
outcomes = c("Dead", "curr_state")) +
payoffs(cost = compute_cost(state, get_sick))
pDie <- function(state, get_sick){
# if the cohort gets sick, the probability of death = 0.01 otherwise it is 0.1
switch(state, "Alive"= if(get_sick==TRUE) 0.1 else 0.01,
"Dead"=0)
}
compute_cost <- function(state, get_sick){
cost_state <- switch(state, "Alive"=1000, "Dead"=0)
# add 2000 if someone in the Alive state gets sick
cost_get_sick <- if(get_sick==TRUE & state=="Alive") 2000 else 0
cost_state+cost_get_sick
}
Notice that now we have two sequential events. The first event is die, the second one is get_sick. Now, if die=TRUE, then the cohort moves to the state D, but if D=FALSE, then the cohort moves to the next event get_sick. Then, if get_sick=TRUE, then the cohort moves to the state S, but if not, they will stay in their current state.
The rest of the user-functions can stay the same. Below we show how to use prior events to influence the probability of transition and the payoffs.
for (state in c("Alive", "Dead")){
for (get_sick in c(TRUE, FALSE)){ # just print the first two cycles
cat(" state:", state, " get_sick:", get_sick, #
" pDie:", pDie(state, get_sick), " cost:", compute_cost(state, get_sick), "\n")
}
}
#> state: Alive get_sick: TRUE pDie: 0.1 cost: 3000
#> state: Alive get_sick: FALSE pDie: 0.01 cost: 1000
#> state: Dead get_sick: TRUE pDie: 0 cost: 0
#> state: Dead get_sick: FALSE pDie: 0 cost: 0
Now we can examine the impact of this on the model results
model_struc <- gmod_gen_model_function(mygmod)
#> Warning in gmod_gen_model_function.gmod_markov(mygmod): Model function my_markov_model is generated. It can be run by calling it directly, for example this function returns the summary results:
#> my_markov_model(model_struc,params,return_transition_prob=FALSE,return_state_payoffs=FALSE,return_trace=FALSE,return_cycle_payoffs=FALSE,return_payoff_summary=TRUE)
my_markov_model(model_struc,return_transition_prob=TRUE,return_state_payoffs=TRUE,return_trace=TRUE,return_cycle_payoffs=TRUE,return_payoff_summary=TRUE)
#> $P
#> , , StandardOfCare
#>
#> Alive Dead
#> Alive 0.9855 0.0145
#> Dead 0.0000 1.0000
#>
#>
#> $state_payoffs
#> , , cost
#>
#> StandardOfCare
#> Alive 1100
#> Dead 0
#>
#>
#> $Trace
#> , , StandardOfCare
#>
#> Alive Dead
#> 1 1.0000000 0.00000000
#> 2 0.9855000 0.01450000
#> 3 0.9712103 0.02878975
#> 4 0.9571277 0.04287230
#> 5 0.9432493 0.05675065
#> 6 0.9295722 0.07042777
#> 7 0.9160934 0.08390656
#> 8 0.9028101 0.09718992
#> 9 0.8897193 0.11028066
#> 10 0.8768184 0.12318159
#>
#>
#> $cycle_payoffs
#> , , StandardOfCare, cost
#>
#> Alive Dead
#> 1 1100.0000 0
#> 2 1084.0500 0
#> 3 1068.3313 0
#> 4 1052.8405 0
#> 5 1037.5743 0
#> 6 1022.5295 0
#> 7 1007.7028 0
#> 8 993.0911 0
#> 9 978.6913 0
#> 10 964.5002 0
#>
#>
#> $summary_payoffs
#> cost
#> StandardOfCare 10309.31
gmod allows using variables inside the custom functions. These values must be defined in the global environment so that they are accessible within all the functions. To illustrate the use of parameters, we can begin by creating a new list (params) that contains 4 parameters as shown below. In the functions below we replace the values with the variable names we use in the params list. For example, we will use pDieSick in the params = 0.1, and replace the 0.1 we used in the pDie function with the variable name pDieSick. We do the same for the other variables.
params <- list(pDieSick = 0.1,
pDieNotSick = 0.01,
costAlive = 1000,
costGetSick = 2000)
mygmod <- gmod() +
decisions("StandardOfCare") +
states(names = c("Alive", "Dead"), init_probs = c(1,0)) +
event(name = "get_sick",
scenarios = c(TRUE, FALSE),
probs = c(0.05, Inf),
outcomes = c("die", "die")) +
event(name = "die",
scenarios = c(TRUE, FALSE),
probs = c(pDie(state, get_sick), Inf),
outcomes = c("Dead", "curr_state")) +
payoffs(cost = compute_cost(state, get_sick))
pDie <- function(state, get_sick){
# if the cohort gets sick, the probability of death = 0.01 otherwise it is 0.1
switch(state, "Alive"= if(get_sick==TRUE) pDieSick else pDieNotSick,
"Dead"=0)
}
compute_cost <- function(state, get_sick){
cost_state <- switch(state, "Alive"=costAlive, "Dead"=0)
# add 2000 if someone in the Alive state gets sick
cost_get_sick <- if(get_sick==TRUE & state=="Alive") costGetSick else 0
cost_state+cost_get_sick
}
model_struc <- gmod_gen_model_function(mygmod)
#> Warning in gmod_gen_model_function.gmod_markov(mygmod): Model function my_markov_model is generated. It can be run by calling it directly, for example this function returns the summary results:
#> my_markov_model(model_struc,params,return_transition_prob=FALSE,return_state_payoffs=FALSE,return_trace=FALSE,return_cycle_payoffs=FALSE,return_payoff_summary=TRUE)
my_markov_model(model_struc,params=params)
#> $summary_payoffs
#> cost
#> StandardOfCare 10309.31
gmod allows passing parameter values as a list. The values passed in the list replace the parameter values in the global environment.
Now, we can simply change the values of the parameter list and rerun the model by simply calling it with a different parameter values. For example, we change the value of pDieSick to 0.3 from 0.1, and change the costGetSick to 10000 from 2000.
params <- list(pDieSick = 0.3, # was 0.1
pDieNotSick = 0.01,
costAlive = 1000,
costGetSick = 10000) # was 2000
my_markov_model(model_struc,params=params)
#> $summary_payoffs
#> cost
#> StandardOfCare 13449.8
Being able to pass parameters as a list can be helpful when we conduct analyses using the gmod structure. For example, if we want to run a probabilistic sensitivity analysis, deterministic analyses, do value of information, or calibrate our model.
In this Vignette we went over the basic features of the gmod library for building and running Markov models. We illustrated these features one at a time, and show how to define decisions, states, events, payoffs. In addition, we show how to change the number of cycles, the discounting rates. Furthermore, we show how to make transition probabilities and payoffs a function of decisions, states cycles, number of cycles in a state (tunnel) and previous events.
For further details on gmod, please refer to the other vignettes, mainly the introductory and advanced Markov model tutorial vignettes that replicate our anlayses published in Medical Decision Making.