Building a Decision Tree Step by Step: Follow Outcomes, Costs and Health

Health EconomicsClinical Epidemiology ResearchMethodology and Research Design
Building a Decision Tree Step by Step: Follow Outcomes, Costs and Health
On this page

อ่านฉบับภาษาไทย (Thai version)

Abstract

A provincial hospital's stroke team must decide whether a new acute treatment, costing 22,000 THB more per patient, is worth offering. Judged by its price alone it looks expensive. Judged by its best outcome alone it looks obvious. A decision tree follows every patient to an end instead. Of 100 patients on standard care, 50 go home independent, 35 survive dependent on others and 15 die. With the new treatment the counts are 65, 25 and 10. Each end carries a lifetime cost and lifetime health in quality-adjusted life years (QALYs), meaning years of life weighted by their quality. Weighing each end by its probability and adding gives the average per patient: 29,700 THB and 5.05 QALYs for standard care, 46,600 THB and 5.95 QALYs for the new treatment. At an illustrative 100,000 THB per QALY, the extra 0.9 QALYs are worth 90,000 THB, which leaves 73,100 THB per patient after the extra 16,900 THB.


Read top to bottom: the choice, what happens to 100 patients, what each end is worth, and the average per patient. Every number is worked out in the sections below.

A stroke team weighing 22,000 THB

A provincial hospital's stroke team sees adults with acute stroke who could receive standard care or a new acute treatment. The new treatment adds 22,000 THB to the cost of every patient who receives it. The finance officer sees that price, and the neurologist sees more patients going home able to look after themselves.

Out of every 100 eligible patients on standard care, 50 go home independent, 35 survive but depend on others for daily care, and 15 die. With the new treatment the counts are 65, 25 and 10. So the new treatment gives 15 more independent survivors, 10 fewer dependent survivors and 5 fewer deaths per 100 patients.

Neither view answers the team's real question: is the extra spending worth the extra health? The price alone ignores the later care that fewer dependent survivors will need. The best branch alone ignores the patients who still end dependent or die. This article builds the tool that follows every patient to an end: a decision tree.

All numbers here are invented for teaching; they are not stroke evidence or a treatment recommendation. A Thai evaluation of endovascular treatment for acute ischaemic stroke asked a similar question with real data (Rattanavipapong and colleagues).

Draw the choice, then the chances

Start where the team has control. A decision node, drawn as a square, is the choice the team makes: standard care or the new treatment. Each option is a strategy, meaning a complete plan for every eligible patient, not just for those who do well.

Next comes what nobody controls. A chance node, drawn as a circle, splits patients by what happens to them, and each branch carries its probability. Under standard care the branches are 0.50 independent, 0.35 dependent and 0.15 died; under the new treatment they are 0.65, 0.25 and 0.10. These are the counts per 100 patients written as shares.

Each path stops at a terminal node, drawn as a triangle: independent, dependent or died, judged at a fixed point after the stroke, such as three months, with the lifetime cost and health that follow attached to that end. Every patient lands in exactly one end, and the three ends cover everyone. So each strategy's probabilities add to 1, just as its counts add up to all 100 patients in the group.

Keep one outcome order in every list: independent, dependent, died. Probabilities, costs and health values are typed as separate lists, so a list in another order pairs a probability with the wrong end.

Check the counts in R

R code case.R
# The stroke case: where every 100 eligible patients end up under each strategy.
# Outcome order everywhere: independent, dependent, died. All values are illustrative.
patients <- 100
p_standard <- c(0.50, 0.35, 0.15)
p_new <- c(0.65, 0.25, 0.10)
extra_cost <- 22000
cat("Per 100 on standard care (independent, dependent, died):", patients * p_standard, "\n")
cat("Per 100 on the new treatment:", patients * p_new, "\n")
cat("More independent, fewer dependent, fewer deaths per 100:",
    patients * (p_new - p_standard) * c(1, -1, -1), "\n")
cat("Each list adds to:", sum(p_standard), "and", sum(p_new), "\n")
cat("New treatment, extra cost per patient:", format(extra_cost, big.mark = ","), "THB\n")
Output of the run case.log
Per 100 on standard care (independent, dependent, died): 50 35 15
Per 100 on the new treatment: 65 25 10
More independent, fewer dependent, fewer deaths per 100: 15 10 5
Each list adds to: 1 and 1
New treatment, extra cost per patient: 22,000 THB

Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download building-a-decision-tree-step-by-step.R

Full tidy script
# Building a Decision Tree Step by Step: Follow Outcomes, Costs and Health
# Follows 100 stroke patients to the end of each path, then rolls cost and health back separately.
# ติดตามผู้ป่วยโรคหลอดเลือดสมอง 100 คนจนถึงปลายทางของแต่ละเส้นทาง แล้วคำนวณย้อนต้นทุนและสุขภาพแยกกัน
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# ggplot2 4.0.3 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
library(tibble) # Write the tree's ends as an explicit input table.
library(dplyr) # Roll each strategy back from the table.
library(ggplot2) # Display the rolled-back strategies without saving files.
stopifnot(getRversion() >= "4.5.2")

#%%@note 1. The inputs, in one outcome order
# Every list runs independent, dependent, died. The new treatment's extra cost is paid once per
# patient who receives it. All values are illustrative, not stroke evidence or policy.
# ทุกรายการเรียงเป็น ช่วยเหลือตนเองได้ ต้องพึ่งพา เสียชีวิต
# ค่าการรักษาใหม่จ่ายครั้งเดียวต่อผู้ป่วยหนึ่งคนที่ได้รับ
# ค่าทั้งหมดเป็นตัวอย่างสมมติ ไม่ใช่หลักฐานเรื่องโรคหลอดเลือดสมองหรือเกณฑ์นโยบาย
patients <- 100
p_standard <- c(0.50, 0.35, 0.15)
p_new <- c(0.65, 0.25, 0.10)
path_cost <- c(12000, 60000, 18000)
path_qaly <- c(8, 3, 0)
extra_cost <- 22000
lambda <- 100000
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
ends <- tibble::tibble(
  strategy = rep(c("Standard", "New"), each = 3),
  outcome = rep(c("Independent", "Dependent", "Died"), times = 2),
  probability = c(p_standard, p_new),
  path_cost = rep(path_cost, times = 2),
  path_qaly = rep(path_qaly, times = 2),
  treatment_cost = rep(c(0, extra_cost), each = 3)
)

#%%@note 2. Check the tree before rolling it back
# Each strategy's probabilities must add to 1, so every patient ends somewhere exactly once.
# ความน่าจะเป็นของแต่ละทางเลือกต้องรวมได้ 1 ผู้ป่วยทุกคนจึงไปถึงปลายทางเดียวเสมอ
check <- ends |>
  dplyr::group_by(strategy) |>
  dplyr::summarise(total = sum(probability), .groups = "drop")
stopifnot(all(abs(check$total - 1) < 1e-9))

#%%@note 3. Roll back cost and health separately
# Weight each end by its probability and add, and the treatment cost is added once per strategy.
# ถ่วงแต่ละปลายทางด้วยความน่าจะเป็นแล้วรวม โดยบวกค่าการรักษาครั้งเดียวต่อทางเลือก
rolled <- ends |>
  dplyr::group_by(strategy) |>
  dplyr::summarise(
    expected_cost = sum(probability * path_cost) + unique(treatment_cost),
    expected_qaly = sum(probability * path_qaly), .groups = "drop"
  ) |>
  dplyr::arrange(match(strategy, c("Standard", "New")))
cost <- rolled$expected_cost
qaly <- rolled$expected_qaly
bill <- patients * p_standard * path_cost
stopifnot(abs(sum(bill) / patients - cost[1]) < 1e-9)

#%%@note 4. Value the gain and look for slips
# Health gained, valued at the illustrative threshold, minus the extra cost, then two common slips.
# มูลค่าสุขภาพที่เพิ่มตามเกณฑ์สมมติ ลบต้นทุนที่เพิ่ม แล้วดูข้อผิดพลาดที่พบบ่อยสองแบบ
delta_cost <- sum((p_new - p_standard) * path_cost) + extra_cost
delta_qaly <- sum((p_new - p_standard) * path_qaly)
inmb <- lambda * delta_qaly - delta_cost
stopifnot(abs(delta_cost - diff(cost)) < 1e-9, abs(delta_qaly - diff(qaly)) < 1e-9)
typed_reversed <- rev(p_new)

#%%@note 5. Display the two strategies
# Plot expected cost against expected health per patient. The script saves no file.
# แสดงต้นทุนคาดหมายเทียบกับสุขภาพคาดหมายต่อผู้ป่วย โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(rolled,
  ggplot2::aes(x = expected_qaly, y = expected_cost, label = strategy)) +
  ggplot2::geom_point() + ggplot2::geom_text(nudge_y = 1000) +
  ggplot2::labs(x = "Expected health per patient", y = "Expected cost per patient (THB)"))

#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("Per 100 on standard care (independent, dependent, died):", patients * p_standard, "\n")
cat("Per 100 on the new treatment:", patients * p_new, "\n")
cat("More independent, fewer dependent, fewer deaths per 100:",
    patients * (p_new - p_standard) * c(1, -1, -1), "\n")
cat("Each list adds to:", sum(p_standard), "and", sum(p_new), "\n")
cat("New treatment, extra cost per patient:", format(extra_cost, big.mark = ","), "THB\n")
cat("10 years at 0.70 =", formatC(10 * 0.70, format = "f", digits = 1),
    "years of full health, so full health adds", formatC(10 * 0.30, format = "f", digits = 1), "\n")
cat("Lifetime cost per end (independent, dependent, died):", big(path_cost), "THB\n")
cat("Lifetime health per end:", path_qaly, "\n")
cat("For example: 10 years at", 0.8, "=", 10 * 0.8, "| 10 years at", 0.3, "=", 10 * 0.3, "\n")
cat("One patient moved from dependent to independent:", big(path_cost[2] - path_cost[1]),
    "THB less, health up by", path_qaly[1] - path_qaly[2], "\n")
cat("Standard care, 100 patients by end:", big(bill), "| all:", big(sum(bill)), "THB\n")
cat("New care, later care:", big(sum(p_new * path_cost)), "| expected cost:", big(cost), "THB\n")
cat("Expected health:", qaly, "| difference:", big(diff(cost)), "THB, health", diff(qaly), "\n")
cat("Series example: 1.00 x 160000 - 150000 =", 1.00 * 160000 - 150000, "THB\n")
cat("Later care saved:", sum((p_standard - p_new) * path_cost), "| extra cost:", delta_cost, "\n")
cat("Gain valued:", lambda * delta_qaly, "| net gain:", inmb, "THB\n")
cat("Price where the net gain reaches 0:", extra_cost + inmb, "THB\n")
cat("Probabilities typed death first:", sum(typed_reversed * path_cost) + extra_cost, "THB,",
    "health", sum(typed_reversed * path_qaly), "\n")
cat("Treatment charged to survivors only:", sum(p_new * path_cost), "+", sum(p_new[1:2]),
    "x", extra_cost, "=", sum(p_new * path_cost) + sum(p_new[1:2]) * extra_cost, "THB\n")
cat("Treatment added to every end, then weighted:", sum(p_new * (path_cost + extra_cost)), "THB\n")
The counts per 100 patients under each strategy, and a check that each list of probabilities adds to 1.

Put a cost and a health value on each end

The tree asks two things of each end: what it costs and how much health it holds. Take the health service's view, counting what the hospital and later care services pay. Count over each patient's remaining life, the time horizon, because dependence after a stroke lasts for years.

So each end's cost covers the acute admission and all later care: 12,000 THB for an independent survivor, 60,000 THB for a dependent one and 18,000 THB for the acute care of a patient who dies. These end costs leave out the new treatment's 22,000 THB. That price belongs to the strategy, so it is added once for every patient who receives it, whichever end they reach.

Health needs a unit that counts both length and quality of life. A QALY, a quality-adjusted life year, is a year of life weighted by its quality, from 1 for full health to 0 for death. Over 10 years, a health state valued at 0.70 of full health counts as 7.0 QALYs, so full health over those same 10 years would gain 3.0; the full lesson is in the foundation article on quality-adjusted life years.

In this case each end carries lifetime QALYs per patient: 8 for independent survival, 3 for dependent survival and 0 for death. Each value is already a lifetime total, so it is weighted once and never multiplied by years again. For example, 8 could be 10 remaining years at 0.8, and 3 could be 10 years at 0.3; these are invented round values.

Moving one patient from dependent to independent survival saves 48,000 THB of later care and adds 5 QALYs. That saving is how the new treatment wins back part of its price.

The tree's inputs, in one outcome order

EndStandard care: probabilityNew treatment: probabilityLifetime cost (THB)Lifetime QALYs
Independent survival0.500.6512,0008
Dependent survival0.350.2560,0003
Died0.150.1018,0000

Check the ends in R

R code ends.R
# What each end of the tree carries: lifetime cost (THB) and lifetime health (QALYs) per patient.
# The first line is the series' QALY example, the rest are this case's values.
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
path_cost <- c(12000, 60000, 18000)
path_qaly <- c(8, 3, 0)
cat("10 years at 0.70 =", formatC(10 * 0.70, format = "f", digits = 1),
    "years of full health, so full health adds", formatC(10 * 0.30, format = "f", digits = 1), "\n")
cat("Lifetime cost per end (independent, dependent, died):", big(path_cost), "THB\n")
cat("Lifetime health per end:", path_qaly, "\n")
cat("For example: 10 years at", 0.8, "=", 10 * 0.8, "| 10 years at", 0.3, "=", 10 * 0.3, "\n")
cat("One patient moved from dependent to independent:", big(path_cost[2] - path_cost[1]),
    "THB less, health up by", path_qaly[1] - path_qaly[2], "\n")
Output of the run ends.log
10 years at 0.70 = 7.0 years of full health, so full health adds 3.0
Lifetime cost per end (independent, dependent, died): 12,000 60,000 18,000 THB
Lifetime health per end: 8 3 0
For example: 10 years at 0.8 = 8 | 10 years at 0.3 = 3
One patient moved from dependent to independent: 48,000 THB less, health up by 5

Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download building-a-decision-tree-step-by-step.R

Full tidy script
# Building a Decision Tree Step by Step: Follow Outcomes, Costs and Health
# Follows 100 stroke patients to the end of each path, then rolls cost and health back separately.
# ติดตามผู้ป่วยโรคหลอดเลือดสมอง 100 คนจนถึงปลายทางของแต่ละเส้นทาง แล้วคำนวณย้อนต้นทุนและสุขภาพแยกกัน
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# ggplot2 4.0.3 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
library(tibble) # Write the tree's ends as an explicit input table.
library(dplyr) # Roll each strategy back from the table.
library(ggplot2) # Display the rolled-back strategies without saving files.
stopifnot(getRversion() >= "4.5.2")

#%%@note 1. The inputs, in one outcome order
# Every list runs independent, dependent, died. The new treatment's extra cost is paid once per
# patient who receives it. All values are illustrative, not stroke evidence or policy.
# ทุกรายการเรียงเป็น ช่วยเหลือตนเองได้ ต้องพึ่งพา เสียชีวิต
# ค่าการรักษาใหม่จ่ายครั้งเดียวต่อผู้ป่วยหนึ่งคนที่ได้รับ
# ค่าทั้งหมดเป็นตัวอย่างสมมติ ไม่ใช่หลักฐานเรื่องโรคหลอดเลือดสมองหรือเกณฑ์นโยบาย
patients <- 100
p_standard <- c(0.50, 0.35, 0.15)
p_new <- c(0.65, 0.25, 0.10)
path_cost <- c(12000, 60000, 18000)
path_qaly <- c(8, 3, 0)
extra_cost <- 22000
lambda <- 100000
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
ends <- tibble::tibble(
  strategy = rep(c("Standard", "New"), each = 3),
  outcome = rep(c("Independent", "Dependent", "Died"), times = 2),
  probability = c(p_standard, p_new),
  path_cost = rep(path_cost, times = 2),
  path_qaly = rep(path_qaly, times = 2),
  treatment_cost = rep(c(0, extra_cost), each = 3)
)

#%%@note 2. Check the tree before rolling it back
# Each strategy's probabilities must add to 1, so every patient ends somewhere exactly once.
# ความน่าจะเป็นของแต่ละทางเลือกต้องรวมได้ 1 ผู้ป่วยทุกคนจึงไปถึงปลายทางเดียวเสมอ
check <- ends |>
  dplyr::group_by(strategy) |>
  dplyr::summarise(total = sum(probability), .groups = "drop")
stopifnot(all(abs(check$total - 1) < 1e-9))

#%%@note 3. Roll back cost and health separately
# Weight each end by its probability and add, and the treatment cost is added once per strategy.
# ถ่วงแต่ละปลายทางด้วยความน่าจะเป็นแล้วรวม โดยบวกค่าการรักษาครั้งเดียวต่อทางเลือก
rolled <- ends |>
  dplyr::group_by(strategy) |>
  dplyr::summarise(
    expected_cost = sum(probability * path_cost) + unique(treatment_cost),
    expected_qaly = sum(probability * path_qaly), .groups = "drop"
  ) |>
  dplyr::arrange(match(strategy, c("Standard", "New")))
cost <- rolled$expected_cost
qaly <- rolled$expected_qaly
bill <- patients * p_standard * path_cost
stopifnot(abs(sum(bill) / patients - cost[1]) < 1e-9)

#%%@note 4. Value the gain and look for slips
# Health gained, valued at the illustrative threshold, minus the extra cost, then two common slips.
# มูลค่าสุขภาพที่เพิ่มตามเกณฑ์สมมติ ลบต้นทุนที่เพิ่ม แล้วดูข้อผิดพลาดที่พบบ่อยสองแบบ
delta_cost <- sum((p_new - p_standard) * path_cost) + extra_cost
delta_qaly <- sum((p_new - p_standard) * path_qaly)
inmb <- lambda * delta_qaly - delta_cost
stopifnot(abs(delta_cost - diff(cost)) < 1e-9, abs(delta_qaly - diff(qaly)) < 1e-9)
typed_reversed <- rev(p_new)

#%%@note 5. Display the two strategies
# Plot expected cost against expected health per patient. The script saves no file.
# แสดงต้นทุนคาดหมายเทียบกับสุขภาพคาดหมายต่อผู้ป่วย โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(rolled,
  ggplot2::aes(x = expected_qaly, y = expected_cost, label = strategy)) +
  ggplot2::geom_point() + ggplot2::geom_text(nudge_y = 1000) +
  ggplot2::labs(x = "Expected health per patient", y = "Expected cost per patient (THB)"))

#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("Per 100 on standard care (independent, dependent, died):", patients * p_standard, "\n")
cat("Per 100 on the new treatment:", patients * p_new, "\n")
cat("More independent, fewer dependent, fewer deaths per 100:",
    patients * (p_new - p_standard) * c(1, -1, -1), "\n")
cat("Each list adds to:", sum(p_standard), "and", sum(p_new), "\n")
cat("New treatment, extra cost per patient:", format(extra_cost, big.mark = ","), "THB\n")
cat("10 years at 0.70 =", formatC(10 * 0.70, format = "f", digits = 1),
    "years of full health, so full health adds", formatC(10 * 0.30, format = "f", digits = 1), "\n")
cat("Lifetime cost per end (independent, dependent, died):", big(path_cost), "THB\n")
cat("Lifetime health per end:", path_qaly, "\n")
cat("For example: 10 years at", 0.8, "=", 10 * 0.8, "| 10 years at", 0.3, "=", 10 * 0.3, "\n")
cat("One patient moved from dependent to independent:", big(path_cost[2] - path_cost[1]),
    "THB less, health up by", path_qaly[1] - path_qaly[2], "\n")
cat("Standard care, 100 patients by end:", big(bill), "| all:", big(sum(bill)), "THB\n")
cat("New care, later care:", big(sum(p_new * path_cost)), "| expected cost:", big(cost), "THB\n")
cat("Expected health:", qaly, "| difference:", big(diff(cost)), "THB, health", diff(qaly), "\n")
cat("Series example: 1.00 x 160000 - 150000 =", 1.00 * 160000 - 150000, "THB\n")
cat("Later care saved:", sum((p_standard - p_new) * path_cost), "| extra cost:", delta_cost, "\n")
cat("Gain valued:", lambda * delta_qaly, "| net gain:", inmb, "THB\n")
cat("Price where the net gain reaches 0:", extra_cost + inmb, "THB\n")
cat("Probabilities typed death first:", sum(typed_reversed * path_cost) + extra_cost, "THB,",
    "health", sum(typed_reversed * path_qaly), "\n")
cat("Treatment charged to survivors only:", sum(p_new * path_cost), "+", sum(p_new[1:2]),
    "x", extra_cost, "=", sum(p_new * path_cost) + sum(p_new[1:2]) * extra_cost, "THB\n")
cat("Treatment added to every end, then weighted:", sum(p_new * (path_cost + extra_cost)), "THB\n")
The series' QALY example, each end's lifetime cost and QALYs, and what moving one patient from dependent to independent survival changes.

Roll the tree back: weigh each end, then add

Now fold the tree back from the ends to the choice, one strategy at a time. Picture 100 patients on standard care: 50 patients × 12,000 THB = 600,000 THB for the independent ends, 35 patients × 60,000 THB = 2,100,000 THB for the dependent ends and 15 patients × 18,000 THB = 270,000 THB for the deaths. Together that is 2,970,000 THB, or 29,700 THB per patient.

Dividing by 100 gives the same answer as weighing each end's cost by its probability and adding: 0.50 × 12,000 + 0.35 × 60,000 + 0.15 × 18,000 = 29,700 THB. That probability-weighted average is called the expected value, here the expected cost per patient. Replacing each chance node by its expected value, working back from the ends, is called rolling back the tree.

  1. Weigh the new treatment's end costs

    \[ 0.65\times 12000+0.25\times 60000+0.10\times 18000=24600 \]

    Use the new treatment's probabilities with the same end costs. That gives 24,600 THB of acute and later care: 29,700 − 24,600 = 5,100 THB less than standard care, because fewer patients end dependent.

  2. Add the treatment once

    \[ 24600+22000=46600 \]

    Add the 22,000 THB treatment once per patient, because every patient who receives it pays it, including those who die. The expected cost of the new treatment is 46,600 THB.

  3. Weigh health on its own track

    \[ 0.65\times 8+0.25\times 3+0.10\times 0=5.95 \]

    Do the same with lifetime QALYs, with no price added. Standard care gives 0.50 × 8 + 0.35 × 3 + 0.15 × 0 = 5.05 QALYs per patient; the new treatment gives 5.95.

  4. Take the differences

    \[ 46600-29700=22000-5100=16900,\quad 5.95-5.05=0.9 \]

    Subtract standard care from the new treatment on each track: 16,900 THB more, which is the 22,000 THB price minus the 5,100 THB of later care saved, and 0.9 QALYs more per patient.

Result: Standard care: 29,700 THB and 5.05 QALYs per patient. New treatment: 46,600 THB and 5.95 QALYs. The new treatment costs 16,900 THB more and adds 0.9 QALYs.

Read the tree from right to left. Look at the triangles: each end's lifetime cost and QALYs. Follow the probabilities on each strategy's branches back to its orange box, so the whole rollback above sits in one picture; the new treatment's box includes its 22,000 THB.

Check the rollback in R

R code rollback.R
# Roll back: weight each end by its probability, then add, with cost and health on separate tracks.
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
p_standard <- c(0.50, 0.35, 0.15)
p_new <- c(0.65, 0.25, 0.10)
path_cost <- c(12000, 60000, 18000)
path_qaly <- c(8, 3, 0)
extra_cost <- 22000
bill <- 100 * p_standard * path_cost
cost <- c(sum(p_standard * path_cost), sum(p_new * path_cost) + extra_cost)
qaly <- c(sum(p_standard * path_qaly), sum(p_new * path_qaly))
cat("Standard care, 100 patients by end:", big(bill), "| all:", big(sum(bill)), "THB\n")
cat("New care, later care:", big(sum(p_new * path_cost)), "| expected cost:", big(cost), "THB\n")
cat("Expected health:", qaly, "| difference:", big(diff(cost)), "THB, health", diff(qaly), "\n")
Output of the run rollback.log
Standard care, 100 patients by end: 600,000 2,100,000 270,000 | all: 2,970,000 THB
New care, later care: 24,600 | expected cost: 29,700 46,600 THB
Expected health: 5.05 5.95 | difference: 16,900 THB, health 0.9

Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download building-a-decision-tree-step-by-step.R

Full tidy script
# Building a Decision Tree Step by Step: Follow Outcomes, Costs and Health
# Follows 100 stroke patients to the end of each path, then rolls cost and health back separately.
# ติดตามผู้ป่วยโรคหลอดเลือดสมอง 100 คนจนถึงปลายทางของแต่ละเส้นทาง แล้วคำนวณย้อนต้นทุนและสุขภาพแยกกัน
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# ggplot2 4.0.3 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
library(tibble) # Write the tree's ends as an explicit input table.
library(dplyr) # Roll each strategy back from the table.
library(ggplot2) # Display the rolled-back strategies without saving files.
stopifnot(getRversion() >= "4.5.2")

#%%@note 1. The inputs, in one outcome order
# Every list runs independent, dependent, died. The new treatment's extra cost is paid once per
# patient who receives it. All values are illustrative, not stroke evidence or policy.
# ทุกรายการเรียงเป็น ช่วยเหลือตนเองได้ ต้องพึ่งพา เสียชีวิต
# ค่าการรักษาใหม่จ่ายครั้งเดียวต่อผู้ป่วยหนึ่งคนที่ได้รับ
# ค่าทั้งหมดเป็นตัวอย่างสมมติ ไม่ใช่หลักฐานเรื่องโรคหลอดเลือดสมองหรือเกณฑ์นโยบาย
patients <- 100
p_standard <- c(0.50, 0.35, 0.15)
p_new <- c(0.65, 0.25, 0.10)
path_cost <- c(12000, 60000, 18000)
path_qaly <- c(8, 3, 0)
extra_cost <- 22000
lambda <- 100000
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
ends <- tibble::tibble(
  strategy = rep(c("Standard", "New"), each = 3),
  outcome = rep(c("Independent", "Dependent", "Died"), times = 2),
  probability = c(p_standard, p_new),
  path_cost = rep(path_cost, times = 2),
  path_qaly = rep(path_qaly, times = 2),
  treatment_cost = rep(c(0, extra_cost), each = 3)
)

#%%@note 2. Check the tree before rolling it back
# Each strategy's probabilities must add to 1, so every patient ends somewhere exactly once.
# ความน่าจะเป็นของแต่ละทางเลือกต้องรวมได้ 1 ผู้ป่วยทุกคนจึงไปถึงปลายทางเดียวเสมอ
check <- ends |>
  dplyr::group_by(strategy) |>
  dplyr::summarise(total = sum(probability), .groups = "drop")
stopifnot(all(abs(check$total - 1) < 1e-9))

#%%@note 3. Roll back cost and health separately
# Weight each end by its probability and add, and the treatment cost is added once per strategy.
# ถ่วงแต่ละปลายทางด้วยความน่าจะเป็นแล้วรวม โดยบวกค่าการรักษาครั้งเดียวต่อทางเลือก
rolled <- ends |>
  dplyr::group_by(strategy) |>
  dplyr::summarise(
    expected_cost = sum(probability * path_cost) + unique(treatment_cost),
    expected_qaly = sum(probability * path_qaly), .groups = "drop"
  ) |>
  dplyr::arrange(match(strategy, c("Standard", "New")))
cost <- rolled$expected_cost
qaly <- rolled$expected_qaly
bill <- patients * p_standard * path_cost
stopifnot(abs(sum(bill) / patients - cost[1]) < 1e-9)

#%%@note 4. Value the gain and look for slips
# Health gained, valued at the illustrative threshold, minus the extra cost, then two common slips.
# มูลค่าสุขภาพที่เพิ่มตามเกณฑ์สมมติ ลบต้นทุนที่เพิ่ม แล้วดูข้อผิดพลาดที่พบบ่อยสองแบบ
delta_cost <- sum((p_new - p_standard) * path_cost) + extra_cost
delta_qaly <- sum((p_new - p_standard) * path_qaly)
inmb <- lambda * delta_qaly - delta_cost
stopifnot(abs(delta_cost - diff(cost)) < 1e-9, abs(delta_qaly - diff(qaly)) < 1e-9)
typed_reversed <- rev(p_new)

#%%@note 5. Display the two strategies
# Plot expected cost against expected health per patient. The script saves no file.
# แสดงต้นทุนคาดหมายเทียบกับสุขภาพคาดหมายต่อผู้ป่วย โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(rolled,
  ggplot2::aes(x = expected_qaly, y = expected_cost, label = strategy)) +
  ggplot2::geom_point() + ggplot2::geom_text(nudge_y = 1000) +
  ggplot2::labs(x = "Expected health per patient", y = "Expected cost per patient (THB)"))

#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("Per 100 on standard care (independent, dependent, died):", patients * p_standard, "\n")
cat("Per 100 on the new treatment:", patients * p_new, "\n")
cat("More independent, fewer dependent, fewer deaths per 100:",
    patients * (p_new - p_standard) * c(1, -1, -1), "\n")
cat("Each list adds to:", sum(p_standard), "and", sum(p_new), "\n")
cat("New treatment, extra cost per patient:", format(extra_cost, big.mark = ","), "THB\n")
cat("10 years at 0.70 =", formatC(10 * 0.70, format = "f", digits = 1),
    "years of full health, so full health adds", formatC(10 * 0.30, format = "f", digits = 1), "\n")
cat("Lifetime cost per end (independent, dependent, died):", big(path_cost), "THB\n")
cat("Lifetime health per end:", path_qaly, "\n")
cat("For example: 10 years at", 0.8, "=", 10 * 0.8, "| 10 years at", 0.3, "=", 10 * 0.3, "\n")
cat("One patient moved from dependent to independent:", big(path_cost[2] - path_cost[1]),
    "THB less, health up by", path_qaly[1] - path_qaly[2], "\n")
cat("Standard care, 100 patients by end:", big(bill), "| all:", big(sum(bill)), "THB\n")
cat("New care, later care:", big(sum(p_new * path_cost)), "| expected cost:", big(cost), "THB\n")
cat("Expected health:", qaly, "| difference:", big(diff(cost)), "THB, health", diff(qaly), "\n")
cat("Series example: 1.00 x 160000 - 150000 =", 1.00 * 160000 - 150000, "THB\n")
cat("Later care saved:", sum((p_standard - p_new) * path_cost), "| extra cost:", delta_cost, "\n")
cat("Gain valued:", lambda * delta_qaly, "| net gain:", inmb, "THB\n")
cat("Price where the net gain reaches 0:", extra_cost + inmb, "THB\n")
cat("Probabilities typed death first:", sum(typed_reversed * path_cost) + extra_cost, "THB,",
    "health", sum(typed_reversed * path_qaly), "\n")
cat("Treatment charged to survivors only:", sum(p_new * path_cost), "+", sum(p_new[1:2]),
    "x", extra_cost, "=", sum(p_new * path_cost) + sum(p_new[1:2]) * extra_cost, "THB\n")
cat("Treatment added to every end, then weighted:", sum(p_new * (path_cost + extra_cost)), "THB\n")
The bill for 100 patients on standard care, each strategy's expected cost and QALYs, and the differences.

Is 0.9 QALYs worth 16,900 THB?

Cost and health are still in different units, so the team needs an exchange rate. A threshold is the most a payer, such as a health fund, is willing to pay for one QALY. Here we use 100,000 THB per QALY to show the arithmetic; it is a round illustrative figure, not a policy threshold. The series' shared example below uses 160,000 THB instead, and the method is the same at any threshold.

Net monetary benefit (NMB) is health valued at the threshold, minus cost. The incremental net monetary benefit (INMB) applies that to the difference between two strategies: the extra health valued at the threshold, minus the extra cost. A positive INMB means the extra cost is worth paying. In the series' shared example, at 160,000 THB per QALY, 1.00 extra QALY for 150,000 THB more gives an INMB of 10,000 THB; the full lesson is in the foundation article on thresholds and net benefit.

For the stroke team, 0.9 QALYs × 100,000 THB = 90,000 THB of health, and subtracting the 16,900 THB extra cost leaves an INMB of 73,100 THB per patient. The value is positive, so at this threshold the extra cost is worth paying. The treatment's price could rise to 95,100 THB per patient, the current 22,000 THB plus the 73,100 THB of INMB, before the INMB reached zero.

A positive INMB compares value under these inputs; it does not say the hospital can afford the treatment. Whether the budget can cover every stroke patient is a separate question, taken up in a later part of this series.

Compare the two bars. The blue bar is the 0.9 QALY gain valued at 100,000 THB per QALY, 90,000 THB; the orange bar is the extra cost of 16,900 THB. The gap between their ends is the INMB of 73,100 THB per patient, which is why the new treatment comes out ahead.

Check the INMB in R

R code value.R
# Value the health gain at the threshold and subtract the extra cost (THB per patient).
p_standard <- c(0.50, 0.35, 0.15)
p_new <- c(0.65, 0.25, 0.10)
path_cost <- c(12000, 60000, 18000)
path_qaly <- c(8, 3, 0)
extra_cost <- 22000
lambda <- 100000
delta_cost <- sum((p_new - p_standard) * path_cost) + extra_cost
delta_qaly <- sum((p_new - p_standard) * path_qaly)
cat("Series example: 1.00 x 160000 - 150000 =", 1.00 * 160000 - 150000, "THB\n")
cat("Later care saved:", sum((p_standard - p_new) * path_cost), "| extra cost:", delta_cost, "\n")
cat("Gain valued:", lambda * delta_qaly, "| net gain:", lambda * delta_qaly - delta_cost, "THB\n")
cat("Price where the net gain reaches 0:", extra_cost + lambda * delta_qaly - delta_cost, "THB\n")
Output of the run value.log
Series example: 1.00 x 160000 - 150000 = 10000 THB
Later care saved: 5100 | extra cost: 16900
Gain valued: 90000 | net gain: 73100 THB
Price where the net gain reaches 0: 95100 THB

Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download building-a-decision-tree-step-by-step.R

Full tidy script
# Building a Decision Tree Step by Step: Follow Outcomes, Costs and Health
# Follows 100 stroke patients to the end of each path, then rolls cost and health back separately.
# ติดตามผู้ป่วยโรคหลอดเลือดสมอง 100 คนจนถึงปลายทางของแต่ละเส้นทาง แล้วคำนวณย้อนต้นทุนและสุขภาพแยกกัน
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# ggplot2 4.0.3 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
library(tibble) # Write the tree's ends as an explicit input table.
library(dplyr) # Roll each strategy back from the table.
library(ggplot2) # Display the rolled-back strategies without saving files.
stopifnot(getRversion() >= "4.5.2")

#%%@note 1. The inputs, in one outcome order
# Every list runs independent, dependent, died. The new treatment's extra cost is paid once per
# patient who receives it. All values are illustrative, not stroke evidence or policy.
# ทุกรายการเรียงเป็น ช่วยเหลือตนเองได้ ต้องพึ่งพา เสียชีวิต
# ค่าการรักษาใหม่จ่ายครั้งเดียวต่อผู้ป่วยหนึ่งคนที่ได้รับ
# ค่าทั้งหมดเป็นตัวอย่างสมมติ ไม่ใช่หลักฐานเรื่องโรคหลอดเลือดสมองหรือเกณฑ์นโยบาย
patients <- 100
p_standard <- c(0.50, 0.35, 0.15)
p_new <- c(0.65, 0.25, 0.10)
path_cost <- c(12000, 60000, 18000)
path_qaly <- c(8, 3, 0)
extra_cost <- 22000
lambda <- 100000
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
ends <- tibble::tibble(
  strategy = rep(c("Standard", "New"), each = 3),
  outcome = rep(c("Independent", "Dependent", "Died"), times = 2),
  probability = c(p_standard, p_new),
  path_cost = rep(path_cost, times = 2),
  path_qaly = rep(path_qaly, times = 2),
  treatment_cost = rep(c(0, extra_cost), each = 3)
)

#%%@note 2. Check the tree before rolling it back
# Each strategy's probabilities must add to 1, so every patient ends somewhere exactly once.
# ความน่าจะเป็นของแต่ละทางเลือกต้องรวมได้ 1 ผู้ป่วยทุกคนจึงไปถึงปลายทางเดียวเสมอ
check <- ends |>
  dplyr::group_by(strategy) |>
  dplyr::summarise(total = sum(probability), .groups = "drop")
stopifnot(all(abs(check$total - 1) < 1e-9))

#%%@note 3. Roll back cost and health separately
# Weight each end by its probability and add, and the treatment cost is added once per strategy.
# ถ่วงแต่ละปลายทางด้วยความน่าจะเป็นแล้วรวม โดยบวกค่าการรักษาครั้งเดียวต่อทางเลือก
rolled <- ends |>
  dplyr::group_by(strategy) |>
  dplyr::summarise(
    expected_cost = sum(probability * path_cost) + unique(treatment_cost),
    expected_qaly = sum(probability * path_qaly), .groups = "drop"
  ) |>
  dplyr::arrange(match(strategy, c("Standard", "New")))
cost <- rolled$expected_cost
qaly <- rolled$expected_qaly
bill <- patients * p_standard * path_cost
stopifnot(abs(sum(bill) / patients - cost[1]) < 1e-9)

#%%@note 4. Value the gain and look for slips
# Health gained, valued at the illustrative threshold, minus the extra cost, then two common slips.
# มูลค่าสุขภาพที่เพิ่มตามเกณฑ์สมมติ ลบต้นทุนที่เพิ่ม แล้วดูข้อผิดพลาดที่พบบ่อยสองแบบ
delta_cost <- sum((p_new - p_standard) * path_cost) + extra_cost
delta_qaly <- sum((p_new - p_standard) * path_qaly)
inmb <- lambda * delta_qaly - delta_cost
stopifnot(abs(delta_cost - diff(cost)) < 1e-9, abs(delta_qaly - diff(qaly)) < 1e-9)
typed_reversed <- rev(p_new)

#%%@note 5. Display the two strategies
# Plot expected cost against expected health per patient. The script saves no file.
# แสดงต้นทุนคาดหมายเทียบกับสุขภาพคาดหมายต่อผู้ป่วย โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(rolled,
  ggplot2::aes(x = expected_qaly, y = expected_cost, label = strategy)) +
  ggplot2::geom_point() + ggplot2::geom_text(nudge_y = 1000) +
  ggplot2::labs(x = "Expected health per patient", y = "Expected cost per patient (THB)"))

#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("Per 100 on standard care (independent, dependent, died):", patients * p_standard, "\n")
cat("Per 100 on the new treatment:", patients * p_new, "\n")
cat("More independent, fewer dependent, fewer deaths per 100:",
    patients * (p_new - p_standard) * c(1, -1, -1), "\n")
cat("Each list adds to:", sum(p_standard), "and", sum(p_new), "\n")
cat("New treatment, extra cost per patient:", format(extra_cost, big.mark = ","), "THB\n")
cat("10 years at 0.70 =", formatC(10 * 0.70, format = "f", digits = 1),
    "years of full health, so full health adds", formatC(10 * 0.30, format = "f", digits = 1), "\n")
cat("Lifetime cost per end (independent, dependent, died):", big(path_cost), "THB\n")
cat("Lifetime health per end:", path_qaly, "\n")
cat("For example: 10 years at", 0.8, "=", 10 * 0.8, "| 10 years at", 0.3, "=", 10 * 0.3, "\n")
cat("One patient moved from dependent to independent:", big(path_cost[2] - path_cost[1]),
    "THB less, health up by", path_qaly[1] - path_qaly[2], "\n")
cat("Standard care, 100 patients by end:", big(bill), "| all:", big(sum(bill)), "THB\n")
cat("New care, later care:", big(sum(p_new * path_cost)), "| expected cost:", big(cost), "THB\n")
cat("Expected health:", qaly, "| difference:", big(diff(cost)), "THB, health", diff(qaly), "\n")
cat("Series example: 1.00 x 160000 - 150000 =", 1.00 * 160000 - 150000, "THB\n")
cat("Later care saved:", sum((p_standard - p_new) * path_cost), "| extra cost:", delta_cost, "\n")
cat("Gain valued:", lambda * delta_qaly, "| net gain:", inmb, "THB\n")
cat("Price where the net gain reaches 0:", extra_cost + inmb, "THB\n")
cat("Probabilities typed death first:", sum(typed_reversed * path_cost) + extra_cost, "THB,",
    "health", sum(typed_reversed * path_qaly), "\n")
cat("Treatment charged to survivors only:", sum(p_new * path_cost), "+", sum(p_new[1:2]),
    "x", extra_cost, "=", sum(p_new * path_cost) + sum(p_new[1:2]) * extra_cost, "THB\n")
cat("Treatment added to every end, then weighted:", sum(p_new * (path_cost + extra_cost)), "THB\n")
The series' example, then the stroke case: the valued gain, the extra cost, the INMB and the price at which the INMB reaches zero.
The sliders start at the worked example. Lower the new treatment's independent share and raise its dependent share by the same amount: the expected cost rises and the QALYs fall, because a dependent end costs more and holds less health. In the right chart the orange bar is the extra cost and the green bar the valued health gain; in the tree, orange lines are standard care and blue lines the new treatment. Raise the treatment cost and watch the orange bar grow: near 95,100 THB per patient it matches the green bar, so the INMB falls to zero.

Two slips the tree catches

Errors in a tree rarely announce themselves; they produce plausible numbers. Two are common enough to check for every time.

First, a list in the wrong order. Suppose the new treatment's probabilities are typed death first, as 0.10, 0.25 and 0.65, against costs listed independent first. The new treatment then appears to cost 0.10 × 12,000 + 0.25 × 60,000 + 0.65 × 18,000 + 22,000 = 49,900 THB and give only 1.55 QALYs, a harm instead of a gain. Writing the order once and keeping it in every list prevents this.

Second, charging the treatment to some ends only. If the 22,000 THB is added to the survivors' ends but not to the deaths, only 0.65 + 0.25 = 0.9 of the patients pay it. The expected cost becomes 24,600 + 0.9 × 22,000 = 44,400 THB instead of 46,600 THB. Patients who die still received the treatment, so the price belongs to every one of them.

Adding the price to every end and then weighing is safe: it gives 46,600 THB again, because the probabilities add to 1. Writing each input, its unit and its order where others can check it is part of a transparent model (Eddy and colleagues; Briggs and colleagues).

Check the slips in R

R code slips.R
# Two slips and one safe variant, each on the new treatment's expected cost per patient (THB).
p_new <- c(0.65, 0.25, 0.10)
path_cost <- c(12000, 60000, 18000)
path_qaly <- c(8, 3, 0)
extra_cost <- 22000
typed_reversed <- rev(p_new)
cat("Probabilities typed death first:", sum(typed_reversed * path_cost) + extra_cost, "THB,",
    "health", sum(typed_reversed * path_qaly), "\n")
cat("Treatment charged to survivors only:", sum(p_new * path_cost), "+", sum(p_new[1:2]),
    "x", extra_cost, "=", sum(p_new * path_cost) + sum(p_new[1:2]) * extra_cost, "THB\n")
cat("Treatment added to every end, then weighted:", sum(p_new * (path_cost + extra_cost)), "THB\n")
Output of the run slips.log
Probabilities typed death first: 49900 THB, health 1.55
Treatment charged to survivors only: 24600 + 0.9 x 22000 = 44400 THB
Treatment added to every end, then weighted: 46600 THB

Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download building-a-decision-tree-step-by-step.R

Full tidy script
# Building a Decision Tree Step by Step: Follow Outcomes, Costs and Health
# Follows 100 stroke patients to the end of each path, then rolls cost and health back separately.
# ติดตามผู้ป่วยโรคหลอดเลือดสมอง 100 คนจนถึงปลายทางของแต่ละเส้นทาง แล้วคำนวณย้อนต้นทุนและสุขภาพแยกกัน
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# ggplot2 4.0.3 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
library(tibble) # Write the tree's ends as an explicit input table.
library(dplyr) # Roll each strategy back from the table.
library(ggplot2) # Display the rolled-back strategies without saving files.
stopifnot(getRversion() >= "4.5.2")

#%%@note 1. The inputs, in one outcome order
# Every list runs independent, dependent, died. The new treatment's extra cost is paid once per
# patient who receives it. All values are illustrative, not stroke evidence or policy.
# ทุกรายการเรียงเป็น ช่วยเหลือตนเองได้ ต้องพึ่งพา เสียชีวิต
# ค่าการรักษาใหม่จ่ายครั้งเดียวต่อผู้ป่วยหนึ่งคนที่ได้รับ
# ค่าทั้งหมดเป็นตัวอย่างสมมติ ไม่ใช่หลักฐานเรื่องโรคหลอดเลือดสมองหรือเกณฑ์นโยบาย
patients <- 100
p_standard <- c(0.50, 0.35, 0.15)
p_new <- c(0.65, 0.25, 0.10)
path_cost <- c(12000, 60000, 18000)
path_qaly <- c(8, 3, 0)
extra_cost <- 22000
lambda <- 100000
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
ends <- tibble::tibble(
  strategy = rep(c("Standard", "New"), each = 3),
  outcome = rep(c("Independent", "Dependent", "Died"), times = 2),
  probability = c(p_standard, p_new),
  path_cost = rep(path_cost, times = 2),
  path_qaly = rep(path_qaly, times = 2),
  treatment_cost = rep(c(0, extra_cost), each = 3)
)

#%%@note 2. Check the tree before rolling it back
# Each strategy's probabilities must add to 1, so every patient ends somewhere exactly once.
# ความน่าจะเป็นของแต่ละทางเลือกต้องรวมได้ 1 ผู้ป่วยทุกคนจึงไปถึงปลายทางเดียวเสมอ
check <- ends |>
  dplyr::group_by(strategy) |>
  dplyr::summarise(total = sum(probability), .groups = "drop")
stopifnot(all(abs(check$total - 1) < 1e-9))

#%%@note 3. Roll back cost and health separately
# Weight each end by its probability and add, and the treatment cost is added once per strategy.
# ถ่วงแต่ละปลายทางด้วยความน่าจะเป็นแล้วรวม โดยบวกค่าการรักษาครั้งเดียวต่อทางเลือก
rolled <- ends |>
  dplyr::group_by(strategy) |>
  dplyr::summarise(
    expected_cost = sum(probability * path_cost) + unique(treatment_cost),
    expected_qaly = sum(probability * path_qaly), .groups = "drop"
  ) |>
  dplyr::arrange(match(strategy, c("Standard", "New")))
cost <- rolled$expected_cost
qaly <- rolled$expected_qaly
bill <- patients * p_standard * path_cost
stopifnot(abs(sum(bill) / patients - cost[1]) < 1e-9)

#%%@note 4. Value the gain and look for slips
# Health gained, valued at the illustrative threshold, minus the extra cost, then two common slips.
# มูลค่าสุขภาพที่เพิ่มตามเกณฑ์สมมติ ลบต้นทุนที่เพิ่ม แล้วดูข้อผิดพลาดที่พบบ่อยสองแบบ
delta_cost <- sum((p_new - p_standard) * path_cost) + extra_cost
delta_qaly <- sum((p_new - p_standard) * path_qaly)
inmb <- lambda * delta_qaly - delta_cost
stopifnot(abs(delta_cost - diff(cost)) < 1e-9, abs(delta_qaly - diff(qaly)) < 1e-9)
typed_reversed <- rev(p_new)

#%%@note 5. Display the two strategies
# Plot expected cost against expected health per patient. The script saves no file.
# แสดงต้นทุนคาดหมายเทียบกับสุขภาพคาดหมายต่อผู้ป่วย โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(rolled,
  ggplot2::aes(x = expected_qaly, y = expected_cost, label = strategy)) +
  ggplot2::geom_point() + ggplot2::geom_text(nudge_y = 1000) +
  ggplot2::labs(x = "Expected health per patient", y = "Expected cost per patient (THB)"))

#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("Per 100 on standard care (independent, dependent, died):", patients * p_standard, "\n")
cat("Per 100 on the new treatment:", patients * p_new, "\n")
cat("More independent, fewer dependent, fewer deaths per 100:",
    patients * (p_new - p_standard) * c(1, -1, -1), "\n")
cat("Each list adds to:", sum(p_standard), "and", sum(p_new), "\n")
cat("New treatment, extra cost per patient:", format(extra_cost, big.mark = ","), "THB\n")
cat("10 years at 0.70 =", formatC(10 * 0.70, format = "f", digits = 1),
    "years of full health, so full health adds", formatC(10 * 0.30, format = "f", digits = 1), "\n")
cat("Lifetime cost per end (independent, dependent, died):", big(path_cost), "THB\n")
cat("Lifetime health per end:", path_qaly, "\n")
cat("For example: 10 years at", 0.8, "=", 10 * 0.8, "| 10 years at", 0.3, "=", 10 * 0.3, "\n")
cat("One patient moved from dependent to independent:", big(path_cost[2] - path_cost[1]),
    "THB less, health up by", path_qaly[1] - path_qaly[2], "\n")
cat("Standard care, 100 patients by end:", big(bill), "| all:", big(sum(bill)), "THB\n")
cat("New care, later care:", big(sum(p_new * path_cost)), "| expected cost:", big(cost), "THB\n")
cat("Expected health:", qaly, "| difference:", big(diff(cost)), "THB, health", diff(qaly), "\n")
cat("Series example: 1.00 x 160000 - 150000 =", 1.00 * 160000 - 150000, "THB\n")
cat("Later care saved:", sum((p_standard - p_new) * path_cost), "| extra cost:", delta_cost, "\n")
cat("Gain valued:", lambda * delta_qaly, "| net gain:", inmb, "THB\n")
cat("Price where the net gain reaches 0:", extra_cost + inmb, "THB\n")
cat("Probabilities typed death first:", sum(typed_reversed * path_cost) + extra_cost, "THB,",
    "health", sum(typed_reversed * path_qaly), "\n")
cat("Treatment charged to survivors only:", sum(p_new * path_cost), "+", sum(p_new[1:2]),
    "x", extra_cost, "=", sum(p_new * path_cost) + sum(p_new[1:2]) * extra_cost, "THB\n")
cat("Treatment added to every end, then weighted:", sum(p_new * (path_cost + extra_cost)), "THB\n")
The list typed death first, the treatment charged to survivors only, and the safe variant, each on the new treatment's expected cost per patient.

Terms as we use them

Decision node
A square where the decision maker chooses between strategies.
Strategy
A complete plan for every eligible patient, such as standard care or the new treatment.
Chance node
A circle where patients split by what happens to them; each branch has a probability.
Terminal node
A triangle that ends a path; it carries that end's lifetime cost and health.
Expected value
The probability-weighted average of the ends' values, per patient.
Rolling back
Replacing each chance node by its expected value, working from the ends back to the choice.
Time horizon
How far ahead the model counts costs and health; here, each patient's remaining life.
Threshold
The most a payer, such as a health fund, is willing to pay for one QALY.
INMB
Threshold times the QALY gain, minus the extra cost; a positive value favours the new strategy.

Connect to the foundation articles

The foundation article on decision trees and cohort models introduces the tree from the economic side. The one on thresholds and net benefit shows how a threshold turns a health gain into money.

Try it in the app

Open our decision tree app to change the inputs and watch the result update. It needs a login and a Pro or Max plan.

The app starts from its own example values, so its numbers will not match this article.

References

  1. Andrew Briggs, Karl Claxton, Mark Sculpher. Decision Modelling for Health Economic Evaluation (Handbooks for Health Economic Evaluation). Oxford University Press; 2006. ISBN 9780198526629. https://openlibrary.org/isbn/9780198526629
  2. Rattanavipapong Waranya, Worakijthamrongchai Thanaboon, Soboon Budsadee, Luankongsomchit Vilawan, Kongmuangpuk Mananchaya, Isaranuwatchai Wanrudee, Teerawattananon Yot, Nilanont Yongchai. Economic evaluation of endovascular treatment for acute ischaemic stroke in Thailand. BMJ Open. 2022;12(9):e064403. doi:10.1136/bmjopen-2022-064403. https://doi.org/10.1136/bmjopen-2022-064403
  3. Eddy David M., Hollingworth William, Caro J. Jaime, Tsevat Joel, McDonald Kathryn M., Wong John B.. Model Transparency and Validation: A Report of the ISPOR-SMDM Modeling Good Research Practices Task Force-7. Value in Health. 2012;15(6):843-850. doi:10.1016/j.jval.2012.04.012. https://doi.org/10.1016/j.jval.2012.04.012

The series, in learning order

  1. Health Economic Modelling Series Guide: From Evidence to a Decision
  2. Choosing a Decision-Analytic Model: Let the Clinical Pathway Decide
  3. Auditable Model Inputs: Build a Parameter Table You Can Trace
  4. Finding Model Inputs in the Literature: Match the Estimate to the Question
  5. Rates, Probabilities and Transition Probabilities: Keep Time in the Calculation
  6. Building a Decision Tree Step by Step: Follow Outcomes, Costs and Health (this article)
  7. Building a Markov Cohort Model Step by Step: Follow the Cohort Through Time
  8. Budget Impact Analysis in Practice: Count People Before Counting Money
  9. ICER and Net Monetary Benefit Across Strategies: Find the Relevant Frontier
  10. Probabilistic Sensitivity Analysis: From Parameter Distributions to CEAC
  11. Model Validation and Verification: Test the Question, Equations and Code
  12. Expected Value of Perfect Information: What Could Better Evidence Be Worth?

Key takeaways

  • Draw the choice as a square, the chances as circles and the ends as triangles, and keep one outcome order in every list.
  • Weigh each end by its probability and add, with cost and QALYs on separate tracks: 29,700 THB and 5.05 QALYs against 46,600 THB and 5.95.
  • Charge a treatment once to every patient who receives it, including those who die.
  • Value the 0.9 QALY gain at the threshold and subtract the 16,900 THB extra cost: an INMB of 73,100 THB per patient compares value, not affordability.

Related in the wiki: Decision Trees, Markov Cohorts and Probabilistic Sensitivity Analysis: How a Model Turns Evidence into a Decision · ICER, Dominance and the Cost-Effectiveness Plane: Reading the Thai Threshold of 160,000 Baht per QALY

0
Message for International and Thai ReadersUnderstanding My Medical Context in ThailandRead more →Message for International and Thai ReadersUnderstanding My Broader Content Beyond MedicineRead more →

Comments

No comments yet. Be the first to share your thoughts.

Sign in to comment