Session 4 (Bonus): DES vs BMS Using the rdecision Package

Same model, fewer lines — how dedicated HTA packages work

Why This Tab Exists

In Session 4, we built the DES vs BMS decision tree from scratch — counting pathways, computing cost vectors, QALY vectors, and the ICER step by step. That was deliberate: you need to understand what happens inside the model before you let a package do it for you.

Now we rebuild the exact same model using rdecision, a dedicated R package for decision-analytic modelling in health economics. The goal is to show:

  1. The results match — same parameters, same ICER
  2. The code is shorter — object-oriented tree construction replaces manual pathway arithmetic
  3. PSA is built in — you can attach distributions and run Monte Carlo with one method call
  4. When to use which — manual approach for learning and transparency; packages for production HTA
NoteInstall first

If you haven’t installed rdecision, run this once:

install.packages("rdecision")

1. How rdecision Thinks

The package uses an object-oriented (R6 class) approach. You build a decision tree from three types of objects:

Object What it represents Analogy
DecisionNode The point where we choose (DES or BMS) Square node in a tree diagram
ChanceNode A point where nature decides (success/failure, restenosis/not) Circle node
LeafNode A terminal outcome (well, MI, death) — carries utility Terminal box
Action An edge from a decision node — carries a cost (e.g., stent cost) Our choice
Reaction An edge from a chance node — carries a probability and optional cost Nature’s outcome

You connect these objects into a directed graph, hand it to DecisionTree$new(), and call evaluate(). The package automatically folds back the tree, computing expected costs and utilities per strategy.

TipMental model

Think of it like assembling a circuit board. Each node is a component, each edge is a wire carrying probability and cost information. Once assembled, electricity (= patients) flows through the tree and the package tells you where they end up and what it costs.

2. Parameters — Identical to Session 4

We use the exact same inputs so we can cross-validate the results.

Code
library(rdecision)
library(knitr)

# ============================================================
# PARAMETERS — same as manual model (Session 4)
# ============================================================

# Cohort & framework
n_cohort      <- 1000
time_horizon  <- 5       # years
discount_rate <- 0.03
discount_factor <- sum(1 / (1 + discount_rate)^(0:(time_horizon - 1)))

# --- Probabilities ---
p_success_des <- 0.97
p_success_bms <- 0.96

p_restenosis_des <- 0.08
p_restenosis_bms <- 0.20

p_mace_des_no_restenosis <- 0.08
p_mace_bms_no_restenosis <- 0.15

p_mace_des_restenosis <- 0.15
p_mace_bms_restenosis <- 0.25

p_mi_given_mace        <- 0.70
p_cardiac_death_given_mace <- 0.30

p_cabg_if_restenosis <- 0.20

# --- Costs (₹) ---
cost_des_stent <- 38933
cost_bms_stent <- 10693
cost_procedure <- 120000
cost_dapt_des_annual <- 8000
cost_dapt_bms_annual <- 3000
cost_followup_annual <- 5000
cost_repeat_pci <- 180000
cost_cabg       <- 250000
cost_mi_management <- 200000
cost_cardiac_death_cost <- 50000

# --- Utilities ---
u_well  <- 0.85
u_rest  <- 0.78
u_mi    <- 0.65
u_death <- 0.00
ImportantSame numbers, different approach

Every single parameter above is identical to Session 4. The only difference is how we feed them into the model. This lets us verify that the package gives the same answer.

3. Pre-computing Composite Costs and Utilities

rdecision assigns costs to edges and utilities to leaf nodes. Our model has composite costs (e.g., initial PCI + stent + medications over 5 years) that we need to pre-compute so they can be attached to the right edges and leaves.

Code
# ============================================================
# COMPOSITE COSTS FOR EDGES
# ============================================================

# Medication costs (discounted over time horizon) — differs by arm
med_cost_des <- cost_dapt_des_annual + cost_followup_annual * discount_factor
med_cost_bms <- cost_dapt_bms_annual + cost_followup_annual * discount_factor

# Initial PCI + stent + medications (per patient, successful PCI)
edge_cost_des <- cost_procedure + cost_des_stent + med_cost_des
edge_cost_bms <- cost_procedure + cost_bms_stent + med_cost_bms

# Procedural failure → CABG cost + medications
edge_cost_failure_des <- cost_cabg + med_cost_des
edge_cost_failure_bms <- cost_cabg + med_cost_bms

# Restenosis management (weighted: 80% repeat PCI, 20% CABG)
cost_restenosis_mgmt <- (1 - p_cabg_if_restenosis) * cost_repeat_pci +
                         p_cabg_if_restenosis * cost_cabg

# MACE costs at leaf level (weighted by MI vs death split)
# We'll handle MI/death split via separate branches

4. Composite Utilities for Leaf Nodes

In rdecision, leaf node utility must be a weight between 0 and 1 — it represents the average quality of life in that health state. The package then multiplies this weight by the interval (time spent in state) to compute QALYs. So we need two things per leaf: an average utility weight and an interval.

Since our patients have different health trajectories (e.g., well for 2 years then MI), we compute the time-weighted average utility for each terminal state and set the interval to our 5-year horizon.

Code
# ============================================================
# LEAF UTILITIES — average utility weight (must be ≤ 1)
# rdecision computes QALY = utility × interval (in years)
# So we use time-weighted average utility over the horizon
# ============================================================

# Interval for all leaves: 5 years (expressed as difftime)
leaf_interval <- as.difftime(time_horizon * 365.25, units = "days")

# Well (no restenosis, no MACE) — full 5 years at u_well
leaf_u_well <- u_well   # 0.85

# Restenosis but recovered (no MACE after repeat intervention)
# 6 months unwell pre-intervention, then recovered for 4.5 years
# Average: (0.85 × 0.5 + 0.78 × 4.5) / 5
leaf_u_rest_ok <- (u_well * 0.5 + u_rest * (time_horizon - 0.5)) / time_horizon

# MI survivor — ~2 years well before MI, then reduced utility
# Average: (0.85 × 2 + 0.65 × 3) / 5
leaf_u_mi <- (u_well * 2 + u_mi * (time_horizon - 2)) / time_horizon

# Cardiac death — well until midpoint, then dead (utility 0)
# Average: (0.85 × 2.5 + 0 × 2.5) / 5
leaf_u_death <- (u_well * (time_horizon / 2)) / time_horizon

# Procedural failure → CABG → recovery (restenosis-like recovery)
leaf_u_failure <- u_rest   # 0.78
NoteWhy average utility?

rdecision uses QALY = utility × interval. Our manual model computed total QALYs directly (e.g., 0.85 × 2 + 0.65 × 3 = 3.65). The package achieves the same by: average utility = 3.65 / 5 = 0.73, interval = 5 years, so 0.73 × 5 = 3.65 QALYs. Same result, different route.

5. Building the Tree — Node by Node

This is where rdecision shines. We define nodes, connect them with edges, and the package handles the rollback arithmetic.

Code
# ============================================================
# STEP 1: CREATE ALL NODES
# ============================================================

# --- Decision node (root) ---
d_stent <- DecisionNode$new("PCI Stent Choice")

# --- DES arm: chance nodes ---
c_des_proc     <- ChanceNode$new("DES: Procedure")
c_des_rest     <- ChanceNode$new("DES: Restenosis?")
c_des_mace_r   <- ChanceNode$new("DES: MACE (post-restenosis)")
c_des_mace_nr  <- ChanceNode$new("DES: MACE (no restenosis)")
c_des_mace_type_r  <- ChanceNode$new("DES: MACE type (restenosis)")
c_des_mace_type_nr <- ChanceNode$new("DES: MACE type (no restenosis)")

# --- DES arm: leaf nodes (terminal outcomes with utility + interval) ---
l_des_failure     <- LeafNode$new("DES: Failure→CABG",        utility = leaf_u_failure,  interval = leaf_interval)
l_des_rest_well   <- LeafNode$new("DES: Restenosis→Well",     utility = leaf_u_rest_ok,  interval = leaf_interval)
l_des_rest_mi     <- LeafNode$new("DES: Restenosis→MI",       utility = leaf_u_mi,       interval = leaf_interval)
l_des_rest_death  <- LeafNode$new("DES: Restenosis→Death",    utility = leaf_u_death,    interval = leaf_interval)
l_des_norest_well <- LeafNode$new("DES: No Rest→Well",        utility = leaf_u_well,     interval = leaf_interval)
l_des_norest_mi   <- LeafNode$new("DES: No Rest→MI",          utility = leaf_u_mi,       interval = leaf_interval)
l_des_norest_death <- LeafNode$new("DES: No Rest→Death",      utility = leaf_u_death,    interval = leaf_interval)

# --- BMS arm: chance nodes ---
c_bms_proc     <- ChanceNode$new("BMS: Procedure")
c_bms_rest     <- ChanceNode$new("BMS: Restenosis?")
c_bms_mace_r   <- ChanceNode$new("BMS: MACE (post-restenosis)")
c_bms_mace_nr  <- ChanceNode$new("BMS: MACE (no restenosis)")
c_bms_mace_type_r  <- ChanceNode$new("BMS: MACE type (restenosis)")
c_bms_mace_type_nr <- ChanceNode$new("BMS: MACE type (no restenosis)")

# --- BMS arm: leaf nodes ---
l_bms_failure     <- LeafNode$new("BMS: Failure→CABG",        utility = leaf_u_failure,  interval = leaf_interval)
l_bms_rest_well   <- LeafNode$new("BMS: Restenosis→Well",     utility = leaf_u_rest_ok,  interval = leaf_interval)
l_bms_rest_mi     <- LeafNode$new("BMS: Restenosis→MI",       utility = leaf_u_mi,       interval = leaf_interval)
l_bms_rest_death  <- LeafNode$new("BMS: Restenosis→Death",    utility = leaf_u_death,    interval = leaf_interval)
l_bms_norest_well <- LeafNode$new("BMS: No Rest→Well",        utility = leaf_u_well,     interval = leaf_interval)
l_bms_norest_mi   <- LeafNode$new("BMS: No Rest→MI",          utility = leaf_u_mi,       interval = leaf_interval)
l_bms_norest_death <- LeafNode$new("BMS: No Rest→Death",      utility = leaf_u_death,    interval = leaf_interval)
NoteWhy so many nodes?

In the manual model, we handled MI vs cardiac death with a simple multiplication (total_mace * 0.70 for MI). In rdecision, every branch must be an explicit node-edge pair. This is more verbose but also more transparent — the tree diagram shows every possible pathway.

6. Wiring the Edges — Actions and Reactions

Code
# ============================================================
# STEP 2: CREATE ALL EDGES
# ============================================================

# --- Decision → DES or BMS (Actions carry the stent choice cost) ---
a_des <- Action$new(d_stent, c_des_proc, cost = 0, label = "DES")
a_bms <- Action$new(d_stent, c_bms_proc, cost = 0, label = "BMS")

# ================================================================
# DES ARM EDGES
# ================================================================

# Procedure success/failure
r_des_success <- Reaction$new(c_des_proc, c_des_rest,
                               p = p_success_des,
                               cost = edge_cost_des,
                               label = "Success")
r_des_failure <- Reaction$new(c_des_proc, l_des_failure,
                               p = NA_real_,
                               cost = edge_cost_failure_des,
                               label = "Failure→CABG")

# Restenosis or not (among successes)
r_des_rest    <- Reaction$new(c_des_rest, c_des_mace_r,
                               p = p_restenosis_des,
                               cost = cost_restenosis_mgmt,
                               label = "Restenosis")
r_des_norest  <- Reaction$new(c_des_rest, c_des_mace_nr,
                               p = NA_real_,
                               cost = 0,
                               label = "No Restenosis")

# MACE among restenosis patients
r_des_mace_r_yes <- Reaction$new(c_des_mace_r, c_des_mace_type_r,
                                  p = p_mace_des_restenosis,
                                  cost = 0,
                                  label = "MACE")
r_des_mace_r_no  <- Reaction$new(c_des_mace_r, l_des_rest_well,
                                  p = NA_real_,
                                  cost = 0,
                                  label = "No MACE")

# MACE type split (restenosis pathway)
r_des_rest_mi    <- Reaction$new(c_des_mace_type_r, l_des_rest_mi,
                                  p = p_mi_given_mace,
                                  cost = cost_mi_management,
                                  label = "MI")
r_des_rest_death <- Reaction$new(c_des_mace_type_r, l_des_rest_death,
                                  p = NA_real_,
                                  cost = cost_cardiac_death_cost,
                                  label = "Cardiac Death")

# MACE among no-restenosis patients
r_des_mace_nr_yes <- Reaction$new(c_des_mace_nr, c_des_mace_type_nr,
                                   p = p_mace_des_no_restenosis,
                                   cost = 0,
                                   label = "MACE")
r_des_mace_nr_no  <- Reaction$new(c_des_mace_nr, l_des_norest_well,
                                   p = NA_real_,
                                   cost = 0,
                                   label = "No MACE")

# MACE type split (no-restenosis pathway)
r_des_norest_mi    <- Reaction$new(c_des_mace_type_nr, l_des_norest_mi,
                                    p = p_mi_given_mace,
                                    cost = cost_mi_management,
                                    label = "MI")
r_des_norest_death <- Reaction$new(c_des_mace_type_nr, l_des_norest_death,
                                    p = NA_real_,
                                    cost = cost_cardiac_death_cost,
                                    label = "Cardiac Death")

# ================================================================
# BMS ARM EDGES (same structure, different probabilities and costs)
# ================================================================

r_bms_success <- Reaction$new(c_bms_proc, c_bms_rest,
                               p = p_success_bms,
                               cost = edge_cost_bms,
                               label = "Success")
r_bms_failure <- Reaction$new(c_bms_proc, l_bms_failure,
                               p = NA_real_,
                               cost = edge_cost_failure_bms,
                               label = "Failure→CABG")

r_bms_rest    <- Reaction$new(c_bms_rest, c_bms_mace_r,
                               p = p_restenosis_bms,
                               cost = cost_restenosis_mgmt,
                               label = "Restenosis")
r_bms_norest  <- Reaction$new(c_bms_rest, c_bms_mace_nr,
                               p = NA_real_,
                               cost = 0,
                               label = "No Restenosis")

r_bms_mace_r_yes <- Reaction$new(c_bms_mace_r, c_bms_mace_type_r,
                                  p = p_mace_bms_restenosis,
                                  cost = 0,
                                  label = "MACE")
r_bms_mace_r_no  <- Reaction$new(c_bms_mace_r, l_bms_rest_well,
                                  p = NA_real_,
                                  cost = 0,
                                  label = "No MACE")

r_bms_rest_mi    <- Reaction$new(c_bms_mace_type_r, l_bms_rest_mi,
                                  p = p_mi_given_mace,
                                  cost = cost_mi_management,
                                  label = "MI")
r_bms_rest_death <- Reaction$new(c_bms_mace_type_r, l_bms_rest_death,
                                  p = NA_real_,
                                  cost = cost_cardiac_death_cost,
                                  label = "Cardiac Death")

r_bms_mace_nr_yes <- Reaction$new(c_bms_mace_nr, c_bms_mace_type_nr,
                                   p = p_mace_bms_no_restenosis,
                                   cost = 0,
                                   label = "MACE")
r_bms_mace_nr_no  <- Reaction$new(c_bms_mace_nr, l_bms_norest_well,
                                   p = NA_real_,
                                   cost = 0,
                                   label = "No MACE")

r_bms_norest_mi    <- Reaction$new(c_bms_mace_type_nr, l_bms_norest_mi,
                                    p = p_mi_given_mace,
                                    cost = cost_mi_management,
                                    label = "MI")
r_bms_norest_death <- Reaction$new(c_bms_mace_type_nr, l_bms_norest_death,
                                    p = NA_real_,
                                    cost = cost_cardiac_death_cost,
                                    label = "Cardiac Death")
TipThe p = NA_real_ trick

When a chance node has two outcomes, you only need to specify the probability for one. Setting the other to NA_real_ tells rdecision to compute the complement automatically (so they sum to 1). This avoids errors from probabilities not adding up.

7. Assemble and Evaluate the Tree

Now we hand all nodes and edges to DecisionTree$new() and call evaluate().

Code
# ============================================================
# STEP 3: BUILD THE DECISION TREE
# ============================================================

# Collect all nodes
V <- list(
  d_stent,
  # DES nodes
  c_des_proc, c_des_rest,
  c_des_mace_r, c_des_mace_nr,
  c_des_mace_type_r, c_des_mace_type_nr,
  l_des_failure, l_des_rest_well, l_des_rest_mi, l_des_rest_death,
  l_des_norest_well, l_des_norest_mi, l_des_norest_death,
  # BMS nodes
  c_bms_proc, c_bms_rest,
  c_bms_mace_r, c_bms_mace_nr,
  c_bms_mace_type_r, c_bms_mace_type_nr,
  l_bms_failure, l_bms_rest_well, l_bms_rest_mi, l_bms_rest_death,
  l_bms_norest_well, l_bms_norest_mi, l_bms_norest_death
)

# Collect all edges
E <- list(
  a_des, a_bms,
  # DES edges
  r_des_success, r_des_failure,
  r_des_rest, r_des_norest,
  r_des_mace_r_yes, r_des_mace_r_no,
  r_des_rest_mi, r_des_rest_death,
  r_des_mace_nr_yes, r_des_mace_nr_no,
  r_des_norest_mi, r_des_norest_death,
  # BMS edges
  r_bms_success, r_bms_failure,
  r_bms_rest, r_bms_norest,
  r_bms_mace_r_yes, r_bms_mace_r_no,
  r_bms_rest_mi, r_bms_rest_death,
  r_bms_mace_nr_yes, r_bms_mace_nr_no,
  r_bms_norest_mi, r_bms_norest_death
)

# Build the tree
dt <- DecisionTree$new(V, E)

# ============================================================
# STEP 4: EVALUATE — one line does all the rollback!
# ============================================================
results <- dt$evaluate()

kable(results,
      caption = "rdecision output: expected per-patient cost and utility by strategy",
      digits = 2)
rdecision output: expected per-patient cost and utility by strategy
Run PCI.Stent.Choice Probability Cost Benefit Utility QALY
1 BMS 1 224594.8 0 0.80 4.02
1 DES 1 221174.9 0 0.83 4.13
ImportantOne line replaces 80 lines

The dt$evaluate() call replaces all of Sections 6–9 from the manual model. It traces every pathway, weights by probability, accumulates costs along edges, and accumulates utilities at leaves — automatically.

8. Cross-Validation — Does It Match?

Let’s compare the rdecision output against our manual calculation.

The evaluate() output is a long-format data frame — one row per strategy, identified by the PCI.Stent.Choice column (named after our decision node). Let’s look at it and then compare with the manual model:

Code
# ============================================================
# EXTRACT rdecision RESULTS
# ============================================================
# evaluate() returns long format: 2 rows × 7 cols
# Columns: Run, PCI.Stent.Choice, Probability, Cost, Benefit, Utility, QALY

des_row <- results[results$PCI.Stent.Choice == "DES", ]
bms_row <- results[results$PCI.Stent.Choice == "BMS", ]

cost_des_pkg <- des_row$Cost
cost_bms_pkg <- bms_row$Cost
qaly_des_pkg <- des_row$QALY
qaly_bms_pkg <- bms_row$QALY

icer_pkg <- (cost_des_pkg - cost_bms_pkg) / (qaly_des_pkg - qaly_bms_pkg)

# ============================================================
# MANUAL MODEL (compact version from Session 4 run_model)
# ============================================================

des_s   <- n_cohort * p_success_des
des_r   <- des_s * p_restenosis_des
des_nr  <- des_s * (1 - p_restenosis_des)
des_mac <- des_r * p_mace_des_restenosis + des_nr * p_mace_des_no_restenosis
des_m   <- des_mac * p_mi_given_mace
des_d   <- des_mac * p_cardiac_death_given_mace

bms_s   <- n_cohort * p_success_bms
bms_r   <- bms_s * p_restenosis_bms
bms_nr  <- bms_s * (1 - p_restenosis_bms)
bms_mac <- bms_r * p_mace_bms_restenosis + bms_nr * p_mace_bms_no_restenosis
bms_m   <- bms_mac * p_mi_given_mace
bms_d   <- bms_mac * p_cardiac_death_given_mace

tc_des <- des_s * (cost_procedure + cost_des_stent) +
          (n_cohort - des_s) * cost_cabg +
          n_cohort * cost_dapt_des_annual +
          n_cohort * cost_followup_annual * discount_factor +
          des_r * cost_restenosis_mgmt +
          des_m * cost_mi_management + des_d * cost_cardiac_death_cost

tc_bms <- bms_s * (cost_procedure + cost_bms_stent) +
          (n_cohort - bms_s) * cost_cabg +
          n_cohort * cost_dapt_bms_annual +
          n_cohort * cost_followup_annual * discount_factor +
          bms_r * cost_restenosis_mgmt +
          bms_m * cost_mi_management + bms_d * cost_cardiac_death_cost

tq_des <- (des_nr - des_nr * p_mace_des_no_restenosis) * u_well * time_horizon +
          (des_r - des_r * p_mace_des_restenosis) * (u_well * 0.5 + u_rest * (time_horizon - 0.5)) +
          des_m * (u_well * 2 + u_mi * 3) +
          des_d * u_well * (time_horizon / 2) +
          (n_cohort - des_s) * u_rest * time_horizon

tq_bms <- (bms_nr - bms_nr * p_mace_bms_no_restenosis) * u_well * time_horizon +
          (bms_r - bms_r * p_mace_bms_restenosis) * (u_well * 0.5 + u_rest * (time_horizon - 0.5)) +
          bms_m * (u_well * 2 + u_mi * 3) +
          bms_d * u_well * (time_horizon / 2) +
          (n_cohort - bms_s) * u_rest * time_horizon

icer_manual <- (tc_des - tc_bms) / (tq_des - tq_bms)

# ============================================================
# COMPARISON TABLE
# ============================================================
fmt <- function(x) paste0("₹", format(round(x), big.mark = ","))

comparison <- data.frame(
  Metric = c("Cost per patient (DES)",
             "Cost per patient (BMS)",
             "QALYs per patient (DES)",
             "QALYs per patient (BMS)",
             "ICER (₹/QALY)"),
  Manual = c(fmt(tc_des / n_cohort),
             fmt(tc_bms / n_cohort),
             round(tq_des / n_cohort, 4),
             round(tq_bms / n_cohort, 4),
             fmt(icer_manual)),
  rdecision = c(fmt(cost_des_pkg),
                fmt(cost_bms_pkg),
                round(qaly_des_pkg, 4),
                round(qaly_bms_pkg, 4),
                fmt(icer_pkg))
)

kable(comparison, col.names = c("Metric", "Manual (Session 4)", "rdecision Package"),
      caption = "Cross-validation: Manual vs rdecision results",
      align = c("l", "r", "r"))
Cross-validation: Manual vs rdecision results
Metric Manual (Session 4) rdecision Package
Cost per patient (DES) ₹221,175 ₹221,175
Cost per patient (BMS) ₹224,595 ₹224,595
QALYs per patient (DES) 4.1309 4.1309
QALYs per patient (BMS) 4.0181 4.0181
ICER (₹/QALY) ₹-30,302 ₹-30,302
WarningSmall differences are expected

The manual model and rdecision may show minor rounding differences (a few rupees). This is normal — the approaches compute the same expected values but through different arithmetic paths. What matters is that the ICER and the qualitative conclusion (dominant, cost-effective, or not) are the same.

9. Built-in Feature: Drawing the Decision Tree

One advantage of rdecision is that the tree structure is stored as a formal graph object. The package provides a draw() method to visualise it directly — no need for separate DiagrammeR code.

Code
# rdecision can draw the tree it built — one line!
dt$draw(border = TRUE)
Figure 1: Decision tree drawn by rdecision’s built-in draw() method
TipModel and diagram stay in sync

When you build the tree manually with DiagrammeR::grViz() (as in Session 4), the diagram is separate from the model — you could accidentally make the diagram show different probabilities than the code uses. With rdecision, the diagram is the model. Change a probability and the diagram updates automatically.

The package also exports to GraphViz DOT notation for publication-quality figures:

Code
# Export tree to DOT format (for GraphViz rendering or LaTeX)
dot_code <- dt$as_DOT(rankdir = "LR")
cat(substr(dot_code, 1, 500), "...\n")  # show first 500 chars
digraph rdecision {   graph [rankdir="LR", size="7,7"]   1 [label="DES: MACE (post-restenosis)"]   2 [label="DES: MACE type (restenosis)"]   3 [label="DES..Restenosis.MI"]   4 [label="DES..Failure.CABG"]   5 [label="BMS: MACE type (no restenosis)"]   6 [label="DES..No.Rest.MI"]   7 [label="BMS..Restenosis.Well"]   8 [label="BMS: Procedure"]   9 [label="BMS: MACE (no restenosis)"]   10 [label="BMS: MACE type (restenosis)"]   11 [label="BMS: Restenosis?"]   12 [label="BMS..No.Rest.Well"]   13 [label="DES..Restenosis.Well"]   14 [label="BMS: MACE (post-restenosis)"]   15 [label="BMS..Restenosis.Death"]   16 [label="DES: Restenosis?"]   17 [label="DES: MACE type (no restenosis)"]   18 [label="DES..No.Rest.Well"]   19 [label="DES..No.Rest.Death"]   20 [label="BMS..Failure.CABG"]   21 [label="BMS..No.Rest.Death"]   22 [label="BMS..Restenosis.MI"]   23 [label="PCI.Stent.Choice"]   24 [label="BMS..No.Rest.MI"]   25 [label="DES: Procedure"]   26 [label="DES: MACE (no restenosis)"]   27 [label="DES..Restenosis.Death"]   1 -> 2 [label="MACE"]   23 -> 8 [label="BMS"]   10 -> 15 [label="Cardiac Death"]   5 -> 21 [label="Cardiac Death"]   23 -> 25 [label="DES"]   14 -> 10 [label="MACE"]   26 -> 17 [label="MACE"]   2 -> 3 [label="MI"]   9 -> 12 [label="No MACE"]   2 -> 27 [label="Cardiac Death"]   5 -> 24 [label="MI"]   11 -> 9 [label="No Restenosis"]   26 -> 18 [label="No MACE"]   1 -> 13 [label="No MACE"]   16 -> 1 [label="Restenosis"]   25 -> 16 [label="Success"]   17 -> 6 [label="MI"]   25 -> 4 [label="Failure→CABG"]   14 -> 7 [label="No MACE"]   10 -> 22 [label="MI"]   16 -> 26 [label="No Restenosis"]   8 -> 20 [label="Failure→CABG"]   8 -> 11 [label="Success"]   9 -> 5 [label="MACE"]   11 -> 14 [label="Restenosis"]   17 -> 19 [label="Cardiac Death"] } ...

10. Built-in Feature: Tornado Diagram (One-way DSA)

The tornado() method varies each uncertain parameter over its 95% confidence interval — one at a time — and shows which parameters have the biggest impact on the result. This is deterministic sensitivity analysis (DSA), automated.

To use this, we need to rebuild the tree with model variables (ModVar objects) instead of fixed numbers. This tells rdecision the uncertainty distribution for each parameter.

Code
# ============================================================
# REBUILD TREE WITH UNCERTAINTY DISTRIBUTIONS
# ============================================================
# BetaModVar  → probabilities (bounded 0–1)
# GammaModVar → costs (non-negative, right-skewed)

# --- Probability ModVars ---
# Beta distribution: alpha = successes, beta = failures
# Mean = alpha/(alpha+beta)

mv_rest_des <- BetaModVar$new(
  description = "P(restenosis|DES)", units = "", alpha = 8, beta = 92)

mv_rest_bms <- BetaModVar$new(
  description = "P(restenosis|BMS)", units = "", alpha = 20, beta = 80)

mv_mace_des_r <- BetaModVar$new(
  description = "P(MACE|DES,restenosis)", units = "", alpha = 15, beta = 85)

mv_mace_bms_r <- BetaModVar$new(
  description = "P(MACE|BMS,restenosis)", units = "", alpha = 25, beta = 75)

mv_mace_des_nr <- BetaModVar$new(
  description = "P(MACE|DES,no restenosis)", units = "", alpha = 8, beta = 92)

mv_mace_bms_nr <- BetaModVar$new(
  description = "P(MACE|BMS,no restenosis)", units = "", alpha = 15, beta = 85)

mv_p_mi <- BetaModVar$new(
  description = "P(MI|MACE)", units = "", alpha = 70, beta = 30)

mv_p_success_des <- BetaModVar$new(
  description = "P(success|DES)", units = "", alpha = 97, beta = 3)

mv_p_success_bms <- BetaModVar$new(
  description = "P(success|BMS)", units = "", alpha = 96, beta = 4)

# --- Cost ModVars (Gamma distribution) ---
# Gamma: mean = shape × scale; shape = 25 gives ~20% CV
# units = "INR" for Indian Rupees

mv_cost_restenosis <- GammaModVar$new(
  description = "Cost of restenosis mgmt", units = "INR",
  shape = 25, scale = cost_restenosis_mgmt / 25)

mv_cost_mi <- GammaModVar$new(
  description = "Cost of MI management", units = "INR",
  shape = 25, scale = cost_mi_management / 25)

mv_cost_death <- GammaModVar$new(
  description = "Cost of cardiac death", units = "INR",
  shape = 25, scale = cost_cardiac_death_cost / 25)

# ============================================================
# REBUILD NODES (same structure, but with ModVar probabilities)
# ============================================================

# Decision node
d2 <- DecisionNode$new("PCI Stent Choice")

# --- DES arm ---
c2_des_proc    <- ChanceNode$new("DES: Procedure")
c2_des_rest    <- ChanceNode$new("DES: Restenosis?")
c2_des_mace_r  <- ChanceNode$new("DES: MACE (post-restenosis)")
c2_des_mace_nr <- ChanceNode$new("DES: MACE (no restenosis)")
c2_des_mtype_r <- ChanceNode$new("DES: MACE type (rest)")
c2_des_mtype_nr <- ChanceNode$new("DES: MACE type (no rest)")

l2_des_fail      <- LeafNode$new("DES: Fail→CABG",       utility = leaf_u_failure,  interval = leaf_interval)
l2_des_rest_well <- LeafNode$new("DES: Rest→Well",       utility = leaf_u_rest_ok,  interval = leaf_interval)
l2_des_rest_mi   <- LeafNode$new("DES: Rest→MI",         utility = leaf_u_mi,       interval = leaf_interval)
l2_des_rest_death <- LeafNode$new("DES: Rest→Death",     utility = leaf_u_death,    interval = leaf_interval)
l2_des_nr_well   <- LeafNode$new("DES: NoRest→Well",     utility = leaf_u_well,     interval = leaf_interval)
l2_des_nr_mi     <- LeafNode$new("DES: NoRest→MI",       utility = leaf_u_mi,       interval = leaf_interval)
l2_des_nr_death  <- LeafNode$new("DES: NoRest→Death",    utility = leaf_u_death,    interval = leaf_interval)

# --- BMS arm ---
c2_bms_proc    <- ChanceNode$new("BMS: Procedure")
c2_bms_rest    <- ChanceNode$new("BMS: Restenosis?")
c2_bms_mace_r  <- ChanceNode$new("BMS: MACE (post-restenosis)")
c2_bms_mace_nr <- ChanceNode$new("BMS: MACE (no restenosis)")
c2_bms_mtype_r <- ChanceNode$new("BMS: MACE type (rest)")
c2_bms_mtype_nr <- ChanceNode$new("BMS: MACE type (no rest)")

l2_bms_fail      <- LeafNode$new("BMS: Fail→CABG",       utility = leaf_u_failure,  interval = leaf_interval)
l2_bms_rest_well <- LeafNode$new("BMS: Rest→Well",       utility = leaf_u_rest_ok,  interval = leaf_interval)
l2_bms_rest_mi   <- LeafNode$new("BMS: Rest→MI",         utility = leaf_u_mi,       interval = leaf_interval)
l2_bms_rest_death <- LeafNode$new("BMS: Rest→Death",     utility = leaf_u_death,    interval = leaf_interval)
l2_bms_nr_well   <- LeafNode$new("BMS: NoRest→Well",     utility = leaf_u_well,     interval = leaf_interval)
l2_bms_nr_mi     <- LeafNode$new("BMS: NoRest→MI",       utility = leaf_u_mi,       interval = leaf_interval)
l2_bms_nr_death  <- LeafNode$new("BMS: NoRest→Death",    utility = leaf_u_death,    interval = leaf_interval)

# ============================================================
# REBUILD EDGES with ModVar probabilities and costs
# ============================================================

# Decision actions
a2_des <- Action$new(d2, c2_des_proc, cost = 0, label = "DES")
a2_bms <- Action$new(d2, c2_bms_proc, cost = 0, label = "BMS")

# --- DES edges ---
e2_des_succ  <- Reaction$new(c2_des_proc, c2_des_rest,    p = mv_p_success_des, cost = edge_cost_des,         label = "Success")
e2_des_fail  <- Reaction$new(c2_des_proc, l2_des_fail,    p = NA_real_,         cost = edge_cost_failure_des,  label = "Failure")

e2_des_rest  <- Reaction$new(c2_des_rest, c2_des_mace_r,  p = mv_rest_des,      cost = mv_cost_restenosis,    label = "Restenosis")
e2_des_nrest <- Reaction$new(c2_des_rest, c2_des_mace_nr, p = NA_real_,         cost = 0,                     label = "No Restenosis")

e2_des_mace_r_y <- Reaction$new(c2_des_mace_r, c2_des_mtype_r,   p = mv_mace_des_r, cost = 0, label = "MACE")
e2_des_mace_r_n <- Reaction$new(c2_des_mace_r, l2_des_rest_well, p = NA_real_,       cost = 0, label = "No MACE")

e2_des_r_mi    <- Reaction$new(c2_des_mtype_r, l2_des_rest_mi,    p = mv_p_mi,   cost = mv_cost_mi,    label = "MI")
e2_des_r_death <- Reaction$new(c2_des_mtype_r, l2_des_rest_death, p = NA_real_,  cost = mv_cost_death,  label = "Death")

e2_des_mace_nr_y <- Reaction$new(c2_des_mace_nr, c2_des_mtype_nr,  p = mv_mace_des_nr, cost = 0, label = "MACE")
e2_des_mace_nr_n <- Reaction$new(c2_des_mace_nr, l2_des_nr_well,   p = NA_real_,        cost = 0, label = "No MACE")

e2_des_nr_mi    <- Reaction$new(c2_des_mtype_nr, l2_des_nr_mi,    p = mv_p_mi,   cost = mv_cost_mi,    label = "MI")
e2_des_nr_death <- Reaction$new(c2_des_mtype_nr, l2_des_nr_death, p = NA_real_,  cost = mv_cost_death,  label = "Death")

# --- BMS edges ---
e2_bms_succ  <- Reaction$new(c2_bms_proc, c2_bms_rest,    p = mv_p_success_bms, cost = edge_cost_bms,         label = "Success")
e2_bms_fail  <- Reaction$new(c2_bms_proc, l2_bms_fail,    p = NA_real_,         cost = edge_cost_failure_bms,  label = "Failure")

e2_bms_rest  <- Reaction$new(c2_bms_rest, c2_bms_mace_r,  p = mv_rest_bms,      cost = mv_cost_restenosis,    label = "Restenosis")
e2_bms_nrest <- Reaction$new(c2_bms_rest, c2_bms_mace_nr, p = NA_real_,         cost = 0,                     label = "No Restenosis")

e2_bms_mace_r_y <- Reaction$new(c2_bms_mace_r, c2_bms_mtype_r,   p = mv_mace_bms_r, cost = 0, label = "MACE")
e2_bms_mace_r_n <- Reaction$new(c2_bms_mace_r, l2_bms_rest_well, p = NA_real_,       cost = 0, label = "No MACE")

e2_bms_r_mi    <- Reaction$new(c2_bms_mtype_r, l2_bms_rest_mi,    p = mv_p_mi,   cost = mv_cost_mi,    label = "MI")
e2_bms_r_death <- Reaction$new(c2_bms_mtype_r, l2_bms_rest_death, p = NA_real_,  cost = mv_cost_death,  label = "Death")

e2_bms_mace_nr_y <- Reaction$new(c2_bms_mace_nr, c2_bms_mtype_nr,  p = mv_mace_bms_nr, cost = 0, label = "MACE")
e2_bms_mace_nr_n <- Reaction$new(c2_bms_mace_nr, l2_bms_nr_well,   p = NA_real_,        cost = 0, label = "No MACE")

e2_bms_nr_mi    <- Reaction$new(c2_bms_mtype_nr, l2_bms_nr_mi,    p = mv_p_mi,   cost = mv_cost_mi,    label = "MI")
e2_bms_nr_death <- Reaction$new(c2_bms_mtype_nr, l2_bms_nr_death, p = NA_real_,  cost = mv_cost_death,  label = "Death")

# ============================================================
# ASSEMBLE the PSA-ready tree
# ============================================================

V2 <- list(
  d2,
  c2_des_proc, c2_des_rest, c2_des_mace_r, c2_des_mace_nr, c2_des_mtype_r, c2_des_mtype_nr,
  l2_des_fail, l2_des_rest_well, l2_des_rest_mi, l2_des_rest_death,
  l2_des_nr_well, l2_des_nr_mi, l2_des_nr_death,
  c2_bms_proc, c2_bms_rest, c2_bms_mace_r, c2_bms_mace_nr, c2_bms_mtype_r, c2_bms_mtype_nr,
  l2_bms_fail, l2_bms_rest_well, l2_bms_rest_mi, l2_bms_rest_death,
  l2_bms_nr_well, l2_bms_nr_mi, l2_bms_nr_death
)

E2 <- list(
  a2_des, a2_bms,
  e2_des_succ, e2_des_fail, e2_des_rest, e2_des_nrest,
  e2_des_mace_r_y, e2_des_mace_r_n, e2_des_r_mi, e2_des_r_death,
  e2_des_mace_nr_y, e2_des_mace_nr_n, e2_des_nr_mi, e2_des_nr_death,
  e2_bms_succ, e2_bms_fail, e2_bms_rest, e2_bms_nrest,
  e2_bms_mace_r_y, e2_bms_mace_r_n, e2_bms_r_mi, e2_bms_r_death,
  e2_bms_mace_nr_y, e2_bms_mace_nr_n, e2_bms_nr_mi, e2_bms_nr_death
)

dt_psa <- DecisionTree$new(V2, E2)
ImportantSame tree, now with uncertainty

The tree structure is identical to Section 5–6. The only difference is that probabilities and costs are now ModVar objects (Beta and Gamma distributions) instead of fixed numbers. This is all rdecision needs to run both DSA and PSA.

Now the tornado diagram — one line:

Code
# Tornado: vary each parameter over its 95% CI
# index = DES strategy, ref = BMS strategy
to <- dt_psa$tornado(
  index = a2_des,
  ref   = a2_bms,
  outcome = "ICER",
  draw = TRUE
)
Figure 2: Tornado diagram: which parameters matter most for the ICER?
NoteReading the tornado

Each bar shows how the ICER (DES vs BMS) changes when that one parameter is varied from its lower to upper 95% confidence limit, with all other parameters held at their mean. The longest bars are the parameters that matter most — these are where you should focus your evidence gathering.

Code
# The tornado also returns a data frame with the numeric values
kable(to, digits = 0,
      caption = "Tornado diagram values: ICER sensitivity to each parameter")
Tornado diagram values: ICER sensitivity to each parameter
Description Units LL UL outcome.min outcome.max
12 P(restenosis|BMS) 0 0 124011 -143953
10 P(restenosis|DES) 0 0 -96046 92118
9 Cost of restenosis mgmt INR 125547 277110 39085 -114546
3 P(MACE|DES,no restenosis) 0 0 -61816 87823
8 P(MACE|BMS,no restenosis) 0 0 65137 -71365
1 Cost of MI management INR 129429 285681 4788 -72905
6 P(MACE|BMS,restenosis) 0 0 -10399 -48319
11 P(success|DES) 1 1 -8360 -41318
7 P(success|BMS) 1 1 -49880 -16977
2 P(MI|MACE) 1 1 -18537 -43407
5 P(MACE|DES,restenosis) 0 0 -35833 -23111
4 Cost of cardiac death INR 32357 71420 -26543 -34867

11. Built-in Feature: Probabilistic Sensitivity Analysis (PSA)

This is where rdecision truly earns its keep. Instead of writing sampling loops, we call evaluate() with setvars = "random" and get 1,000 Monte Carlo iterations — automatically.

Code
# ============================================================
# RUN PSA — 1,000 iterations, one line!
# ============================================================
set.seed(42)  # for reproducibility
N_psa <- 1000L

psa_results <- dt_psa$evaluate(setvars = "random", by = "run", N = N_psa)

cat("PSA returned:", nrow(psa_results), "rows x", ncol(psa_results), "columns\n")
PSA returned: 1000 rows x 11 columns
Code
cat("Column names:", paste(names(psa_results), collapse = ", "), "\n")
Column names: Run, Probability.BMS, Cost.BMS, Benefit.BMS, Utility.BMS, QALY.BMS, Probability.DES, Cost.DES, Benefit.DES, Utility.DES, QALY.DES 
Code
kable(head(psa_results, 5), digits = 2,
      caption = "First 5 PSA iterations (of 1,000)")
First 5 PSA iterations (of 1,000)
Run Probability.BMS Cost.BMS Benefit.BMS Utility.BMS QALY.BMS Probability.DES Cost.DES Benefit.DES Utility.DES QALY.DES
1 1 232944.4 0 0.81 4.04 1 235578.0 0 0.82 4.11
2 1 230753.0 0 0.80 4.02 1 234159.9 0 0.82 4.08
3 1 215902.5 0 0.81 4.03 1 209339.2 0 0.83 4.17
4 1 223728.0 0 0.80 4.00 1 226469.4 0 0.82 4.10
5 1 243772.0 0 0.80 4.00 1 229296.5 0 0.82 4.12
WarningCompare with the manual approach

To do the same thing manually, you would write a for-loop that samples from rbeta() and rgamma(), calls run_model() 1,000 times, and binds the results into a data frame — about 25–30 lines of code. Here it’s one line: dt_psa$evaluate(setvars = "random", by = "run", N = 1000).

12. Visualising PSA Results: CE Plane and CEAC

With 1,000 iterations in hand, we can build the two most important PSA visualisations.

12a. Cost-Effectiveness Plane

Code
library(ggplot2)
Warning: package 'ggplot2' was built under R version 4.5.2
Code
# PSA with by="run" returns WIDE format: one row per iteration
# Columns: Run, Cost.DES, Cost.BMS, QALY.DES, QALY.BMS, etc.
inc_cost <- psa_results$Cost.DES - psa_results$Cost.BMS
inc_qaly <- psa_results$QALY.DES - psa_results$QALY.BMS

ce_data <- data.frame(
  inc_cost = inc_cost,
  inc_qaly = inc_qaly
)

# WTP threshold line (1× GDP per capita India)
wtp <- 170000

ggplot(ce_data, aes(x = inc_qaly, y = inc_cost)) +
  geom_point(alpha = 0.3, colour = "#4e79a7", size = 1.5) +
  geom_hline(yintercept = 0, linetype = "dashed", colour = "grey50") +
  geom_vline(xintercept = 0, linetype = "dashed", colour = "grey50") +
  geom_abline(intercept = 0, slope = wtp, linetype = "dotted",
              colour = "#e15759", linewidth = 1) +
  annotate("text", x = max(inc_qaly) * 0.7, y = wtp * max(inc_qaly) * 0.7,
           label = paste0("WTP = ₹", format(wtp, big.mark = ",")),
           colour = "#e15759", hjust = 0, size = 3.5) +
  labs(x = "Incremental QALYs (DES − BMS)",
       y = "Incremental Cost (DES − BMS, ₹)",
       title = "Cost-Effectiveness Plane",
       subtitle = "Each dot = one PSA iteration (N = 1,000)") +
  theme_minimal()
Figure 3: Cost-effectiveness plane: 1,000 PSA iterations (DES vs BMS)
TipReading the CE plane

Southeast quadrant (right, below zero) = DES is dominant (better and cheaper). Northeast below the WTP line = DES is cost-effective. Points above the WTP line = DES is not cost-effective at that threshold. The cloud of points shows the uncertainty in the conclusion.

12b. Cost-Effectiveness Acceptability Curve (CEAC)

The CEAC answers: “At any given WTP threshold, what is the probability that DES is cost-effective?”

Code
# Generate CEAC: sweep WTP from 0 to ₹5,00,000
wtp_range <- seq(0, 500000, by = 5000)

# For each WTP, compute proportion of iterations where DES is cost-effective
# DES is CE if: inc_cost / inc_qaly < WTP, or equivalently: inc_cost - WTP * inc_qaly < 0
prob_ce <- sapply(wtp_range, function(w) {
  nmb <- inc_qaly * w - inc_cost  # net monetary benefit
  mean(nmb > 0)
})

ceac_data <- data.frame(WTP = wtp_range, Prob_CE = prob_ce)

ggplot(ceac_data, aes(x = WTP, y = Prob_CE)) +
  geom_line(colour = "#4e79a7", linewidth = 1.2) +
  geom_hline(yintercept = 0.5, linetype = "dashed", colour = "grey60") +
  geom_vline(xintercept = 170000, linetype = "dotted", colour = "#e15759") +
  annotate("text", x = 175000, y = 0.1,
           label = "1× GDP\nper capita",
           colour = "#e15759", hjust = 0, size = 3) +
  geom_vline(xintercept = 510000, linetype = "dotted", colour = "#e67e22") +
  annotate("text", x = 490000, y = 0.1,
           label = "3× GDP\nper capita",
           colour = "#e67e22", hjust = 1, size = 3) +
  scale_x_continuous(labels = function(x) paste0("₹", format(x, big.mark = ","))) +
  scale_y_continuous(labels = scales::percent_format(), limits = c(0, 1)) +
  labs(x = "Willingness-to-Pay Threshold (₹/QALY)",
       y = "Probability DES is Cost-Effective",
       title = "Cost-Effectiveness Acceptability Curve (CEAC)",
       subtitle = "Based on 1,000 PSA iterations") +
  theme_minimal()
Figure 4: Cost-Effectiveness Acceptability Curve: probability of DES being cost-effective
ImportantThe policy answer

A single ICER says “DES is cost-effective.” The CEAC says “DES is cost-effective with X% probability at threshold Y.” This is what decision-makers actually need. Notice how we generated this entire analysis — PSA, CE plane, CEAC — with rdecision doing the heavy lifting.

13. Summary: Package Features at a Glance

rdecision built-in features demonstrated in this document
Feature Where Shown Manual Equivalent
Tree structure as code Demonstrated above (Sections 5–7) ~50 lines of R arithmetic
Automatic rollback (evaluate) Demonstrated above (Section 7) ~80 lines (Sections 6–9 in Session 4)
Tree diagram (draw) Demonstrated above (Section 9) Separate DiagrammeR code
DOT/GraphViz export Demonstrated above (Section 9) Not available without extra packages
Tornado diagram (DSA) Demonstrated above (Section 10) Write your own loop + plotting code
PSA with ModVar distributions Demonstrated above (Section 11) ~25–30 lines (for-loop + rbeta/rgamma)
CE plane scatter plot Demonstrated above (Section 12a) Manual ggplot from loop results
CEAC curve Demonstrated above (Section 12b) Manual ggplot from loop results
Semi-Markov models Available — not shown (Day 2 topic) Requires custom state-transition code

14. When to Use Which Approach

Choosing between manual and package-based modelling
Scenario Recommended Approach
Learning HTA / teaching students Manual first, then rdecision
Small decision tree (< 10 pathways) Either — manual is fine
Complex tree with many branches rdecision package
Production HTA submission rdecision (or heemod for Markov)
Need PSA / VOI analysis rdecision — built-in support
Need full audit trail rdecision — formal model structure
Quick one-off analysis Manual — faster to set up
TipThe learning progression
  1. Session 4: Build from scratch → understand every calculation
  2. This tab: Same model in rdecision → see how packages structure it, then use built-in DSA, PSA, CEAC
  3. DSA/PSA sessions: Deep dive into sensitivity analysis theory
  4. Your own HTA: Choose the right tool for the job

Key Takeaways

The manual approach and the rdecision package are complementary, not competing. The manual model teaches you what is being calculated; the package handles how to do it efficiently at scale.

What we demonstrated in this document:

  1. Same model, same results — manual and rdecision cross-validate
  2. Tree drawingdt$draw() produces a diagram directly from the model
  3. Tornado diagramdt$tornado() does one-way DSA in one line
  4. PSAdt$evaluate(setvars = "random", N = 1000) runs 1,000 Monte Carlo iterations
  5. CE plane and CEAC — built from PSA results with standard ggplot2

The entire analysis — from model definition to policy-relevant uncertainty quantification — is reproducible and contained in a single .qmd file.

References

  • Sims A (2024). rdecision: Decision Analytic Modelling in Health Economics. R package. CRAN
  • Jenks M et al. (2016). Clinical and economic burden of surgical site infection. Journal of Hospital Infection.
  • Briggs A, Sculpher M, Claxton K (2006). Decision Modelling for Health Economic Evaluation. Oxford University Press.
  • R-HTA Consortium. R for Health Technology Assessment

Back to Session 4: The manual step-by-step model is in des-bms-decision-tree.qmd. Use it as your reference for understanding; use this tab for seeing how packages streamline the process.