สร้างแผนภูมิต้นไม้การตัดสินใจทีละขั้น: ตามรอยผลลัพธ์ ต้นทุน และสุขภาพ

On this page
Read the English version
บทคัดย่อ
ทีมดูแลผู้ป่วยโรคหลอดเลือดสมองของโรงพยาบาลจังหวัดแห่งหนึ่งต้องตัดสินใจว่าการรักษาเฉียบพลันแบบใหม่ ซึ่งแพงกว่า 22,000 บาทต่อผู้ป่วย คุ้มที่จะให้หรือไม่ ถ้าดูแค่ราคาก็ดูแพง ถ้าดูแค่ผลที่ดีที่สุดก็ดูเหมือนต้องให้แน่นอน แผนภูมิต้นไม้การตัดสินใจจึงติดตามผู้ป่วยทุกคนไปจนถึงปลายทางแทน จากผู้ป่วย 100 คนที่ได้รับการดูแลมาตรฐาน มี 50 คนกลับบ้านโดยช่วยเหลือตนเองได้ มี 35 คนรอดชีวิตแต่ต้องพึ่งพาผู้อื่น และมี 15 คนเสียชีวิต ส่วนการรักษาใหม่ได้ 65, 25 และ 10 คนตามลำดับ แต่ละปลายทางมีต้นทุนตลอดชีวิตและสุขภาพตลอดชีวิต นับเป็นปีสุขภาวะ (QALYs) ซึ่งหมายถึงปีชีวิตที่ถ่วงน้ำหนักด้วยคุณภาพ เมื่อถ่วงน้ำหนักแต่ละปลายทางด้วยความน่าจะเป็นแล้วรวมกัน จะได้ค่าเฉลี่ยต่อผู้ป่วย คือ 29,700 บาทกับ 5.05 QALYs สำหรับการดูแลมาตรฐาน และ 46,600 บาทกับ 5.95 QALYs สำหรับการรักษาใหม่ ที่ค่าสมมติ 100,000 บาทต่อ QALY สุขภาพที่เพิ่ม 0.9 QALYs มีค่า 90,000 บาท ซึ่งเหลือ 73,100 บาทต่อผู้ป่วยหลังหักต้นทุนที่เพิ่ม 16,900 บาท
ทีมโรคหลอดเลือดสมองกับเงิน 22,000 บาท
ทีมดูแลผู้ป่วยโรคหลอดเลือดสมองของโรงพยาบาลจังหวัดแห่งหนึ่งดูแลผู้ใหญ่ที่เป็นโรคหลอดเลือดสมองเฉียบพลัน ซึ่งได้รับการดูแลมาตรฐานหรือการรักษาเฉียบพลันแบบใหม่ก็ได้ การรักษาใหม่เพิ่มต้นทุน 22,000 บาทให้ผู้ป่วยทุกคนที่ได้รับ เจ้าหน้าที่การเงินเห็นราคานี้ ส่วนแพทย์ระบบประสาทเห็นว่าผู้ป่วยกลับบ้านโดยดูแลตัวเองได้มากขึ้น
ในผู้ป่วยที่เหมาะสมจะได้รับการรักษาทุก 100 คน ถ้าได้รับการดูแลมาตรฐาน จะมี 50 คนกลับบ้านโดยช่วยเหลือตนเองได้ มี 35 คนรอดชีวิตแต่ต้องพึ่งพาผู้อื่นในชีวิตประจำวัน และมี 15 คนเสียชีวิต เมื่อได้รับการรักษาใหม่ ตัวเลขเป็น 65, 25 และ 10 คนตามลำดับ การรักษาใหม่จึงทำให้ผู้รอดชีวิตที่ช่วยเหลือตนเองได้เพิ่มขึ้น 15 คน ผู้รอดชีวิตที่ต้องพึ่งพาลดลง 10 คน และการเสียชีวิตลดลง 5 คนต่อผู้ป่วย 100 คน
มุมมองทั้งสองยังไม่ตอบคำถามจริงของทีม คือเงินที่จ่ายเพิ่มคุ้มกับสุขภาพที่ได้เพิ่มหรือไม่ การดูแค่ราคาละเลยการดูแลระยะยาวที่ลดลงเมื่อผู้รอดชีวิตที่ต้องพึ่งพามีน้อยลง การดูแค่กิ่งที่ดีที่สุดละเลยผู้ป่วยที่ยังต้องพึ่งพาหรือเสียชีวิต บทความนี้สร้างเครื่องมือที่ติดตามผู้ป่วยทุกคนจนถึงปลายทาง เรียกว่าแผนภูมิต้นไม้การตัดสินใจ (decision tree)
ตัวเลขทั้งหมดในบทความนี้สมมติขึ้นเพื่อการสอน ไม่ใช่หลักฐานเรื่องโรคหลอดเลือดสมองหรือคำแนะนำการรักษา งานประเมินทางเศรษฐศาสตร์ของไทยเรื่องการรักษาผ่านสายสวนหลอดเลือด (endovascular treatment) สำหรับโรคหลอดเลือดสมองตีบเฉียบพลันเคยถามคำถามคล้ายกันด้วยข้อมูลจริง (Rattanavipapong และคณะ)
วาดการเลือกก่อน แล้วจึงวาดโอกาส
เริ่มจากส่วนที่ทีมควบคุมได้ จุดตัดสินใจ (decision node) ซึ่งวาดเป็นรูปสี่เหลี่ยม คือการเลือกที่ทีมทำ ได้แก่ การดูแลมาตรฐานหรือการรักษาใหม่ แต่ละแผนที่ทีมเลือกได้เรียกว่าทางเลือก (strategy) หมายถึงแผนการดูแลที่ครบถ้วนสำหรับผู้ป่วยทุกคนที่เหมาะสมจะได้รับการรักษา ไม่ใช่เฉพาะคนที่ผลดี
ต่อมาคือส่วนที่ไม่มีใครควบคุมได้ จุดโอกาส (chance node) ซึ่งวาดเป็นรูปวงกลม แยกผู้ป่วยตามสิ่งที่เกิดขึ้นกับเขา และแต่ละกิ่งมีความน่าจะเป็นของตัวเอง ในการดูแลมาตรฐาน กิ่งต่าง ๆ คือ ช่วยเหลือตนเองได้ 0.50 ต้องพึ่งพา 0.35 และเสียชีวิต 0.15 ส่วนในการรักษาใหม่คือ 0.65, 0.25 และ 0.10 ค่าเหล่านี้คือจำนวนต่อผู้ป่วย 100 คนที่เขียนเป็นสัดส่วน
แต่ละเส้นทางสิ้นสุดที่จุดปลายทาง (terminal node) ซึ่งวาดเป็นรูปสามเหลี่ยม ได้แก่ ช่วยเหลือตนเองได้ ต้องพึ่งพา หรือเสียชีวิต โดยประเมิน ณ จุดเวลาที่กำหนดหลังเกิดโรค เช่น สามเดือน และผูกต้นทุนกับสุขภาพตลอดชีวิตที่ตามมาไว้กับปลายทางนั้น ผู้ป่วยแต่ละคนไปถึงปลายทางเดียวเท่านั้น และปลายทางทั้งสามครอบคลุมทุกคน ความน่าจะเป็นของแต่ละทางเลือกจึงรวมได้ 1 เช่นเดียวกับที่จำนวนคนทุกปลายทางรวมกันได้ครบ 100 คนของกลุ่ม
ใช้ลำดับของปลายทางเดียวกันในตัวเลขทุกชุด คือ ช่วยเหลือตนเองได้ ต้องพึ่งพา เสียชีวิต ความน่าจะเป็น ต้นทุน และค่าสุขภาพถูกพิมพ์เป็นชุดตัวเลขแยกกัน ชุดที่เรียงลำดับต่างออกไปจึงจับคู่ความน่าจะเป็นกับปลายทางผิดตัว
ตรวจจำนวนผู้ป่วยด้วย 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")
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
ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด building-a-decision-tree-step-by-step.R
สคริปต์ฉบับเต็ม
# 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")
ใส่ต้นทุนและค่าสุขภาพให้แต่ละปลายทาง
ต้นไม้ถามสองเรื่องจากแต่ละปลายทาง คือมีต้นทุนเท่าไร และมีสุขภาพอยู่เท่าไร ใช้มุมมองของระบบบริการสุขภาพ โดยนับสิ่งที่โรงพยาบาลและบริการดูแลระยะยาวจ่าย นับตลอดชีวิตที่เหลือของผู้ป่วยแต่ละคน ซึ่งเรียกว่ากรอบเวลา (time horizon) เพราะภาวะพึ่งพาหลังโรคหลอดเลือดสมองอยู่ไปอีกหลายปี
ต้นทุนของแต่ละปลายทางจึงรวมการนอนโรงพยาบาลระยะเฉียบพลันและการดูแลภายหลังทั้งหมด คือ 12,000 บาทสำหรับผู้รอดชีวิตที่ช่วยเหลือตนเองได้ 60,000 บาทสำหรับผู้รอดชีวิตที่ต้องพึ่งพา และ 18,000 บาทสำหรับการดูแลระยะเฉียบพลันของผู้ป่วยที่เสียชีวิต ต้นทุนปลายทางเหล่านี้ไม่รวมค่าการรักษาใหม่ 22,000 บาท ราคานี้เป็นของทางเลือก จึงบวกครั้งเดียวให้ผู้ป่วยทุกคนที่ได้รับ ไม่ว่าเขาจะไปถึงปลายทางใด
สุขภาพต้องมีหน่วยที่นับได้ทั้งความยืนยาวและคุณภาพชีวิต ปีสุขภาวะ (quality-adjusted life year, QALY) คือปีชีวิตที่ถ่วงน้ำหนักด้วยคุณภาพ ตั้งแต่ 1 สำหรับสุขภาพสมบูรณ์ถึง 0 สำหรับการเสียชีวิต ถ้าใช้ชีวิต 10 ปีในสภาพสุขภาพที่ให้ค่าคุณภาพ 0.70 จะนับเป็น 7.0 QALYs ถ้าสุขภาพสมบูรณ์ตลอด 10 ปีนั้นจะได้เพิ่มอีก 3.0 QALYs อ่านบทเรียนเต็มได้ที่บทความพื้นฐานเรื่องปีสุขภาวะ
ในกรณีนี้ แต่ละปลายทางมี QALYs ตลอดชีวิตต่อผู้ป่วย คือ 8 สำหรับการรอดชีวิตที่ช่วยเหลือตนเองได้ 3 สำหรับการรอดชีวิตที่ต้องพึ่งพา และ 0 สำหรับการเสียชีวิต ค่าแต่ละค่าเป็นยอดรวมตลอดชีวิตอยู่แล้ว จึงถ่วงน้ำหนักเพียงครั้งเดียว และไม่นำไปคูณด้วยจำนวนปีอีก ตัวอย่างเช่น 8 อาจมาจากชีวิตที่เหลือ 10 ปีที่ค่าคุณภาพ 0.8 และ 3 อาจมาจาก 10 ปีที่ค่าคุณภาพ 0.3 ซึ่งเป็นตัวเลขกลม ๆ ที่สมมติขึ้น
การย้ายผู้ป่วยหนึ่งคนจากต้องพึ่งพาเป็นช่วยเหลือตนเองได้ ช่วยประหยัดค่าดูแลภายหลัง 48,000 บาท และเพิ่ม 5 QALYs ค่าดูแลที่ประหยัดได้นี้คือเหตุที่การรักษาใหม่ชดเชยราคาของตัวเองได้บางส่วน
ข้อมูลนำเข้าของต้นไม้ เรียงตามลำดับผลลัพธ์เดียวกัน
| ปลายทาง | การดูแลมาตรฐาน: ความน่าจะเป็น | การรักษาใหม่: ความน่าจะเป็น | ต้นทุนตลอดชีวิต (บาท) | QALYs ตลอดชีวิต |
|---|---|---|---|---|
| รอดชีวิต ช่วยเหลือตนเองได้ | 0.50 | 0.65 | 12,000 | 8 |
| รอดชีวิต ต้องพึ่งพาผู้อื่น | 0.35 | 0.25 | 60,000 | 3 |
| เสียชีวิต | 0.15 | 0.10 | 18,000 | 0 |
ตรวจค่าปลายทางด้วย 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")
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
ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด building-a-decision-tree-step-by-step.R
สคริปต์ฉบับเต็ม
# 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")
คำนวณย้อนต้นไม้: ถ่วงน้ำหนักแต่ละปลายทางแล้วรวม
ตอนนี้พับต้นไม้กลับจากปลายทางมาที่จุดตัดสินใจ ทีละทางเลือก ลองนึกถึงผู้ป่วย 100 คนที่ได้รับการดูแลมาตรฐาน ผู้ที่ช่วยเหลือตนเองได้ 50 คน × 12,000 บาทต่อคน = 600,000 บาท ผู้ที่ต้องพึ่งพา 35 คน × 60,000 บาทต่อคน = 2,100,000 บาท และผู้ที่เสียชีวิต 15 คน × 18,000 บาทต่อคน = 270,000 บาท รวมเป็น 2,970,000 บาท หรือ 29,700 บาทต่อผู้ป่วย
การหารด้วย 100 ให้คำตอบเดียวกับการถ่วงน้ำหนักต้นทุนของแต่ละปลายทางด้วยความน่าจะเป็นแล้วรวมกัน คือ 0.50 × 12,000 + 0.35 × 60,000 + 0.15 × 18,000 = 29,700 บาท ค่าเฉลี่ยที่ถ่วงด้วยความน่าจะเป็นนี้เรียกว่าค่าคาดหมาย (expected value) ในที่นี้คือต้นทุนคาดหมายต่อผู้ป่วย การแทนจุดโอกาสแต่ละจุดด้วยค่าคาดหมายของมัน โดยไล่ย้อนจากปลายทาง เรียกว่าการคำนวณย้อนกลับต้นไม้ (rolling back)
-
ถ่วงน้ำหนักต้นทุนปลายทางของการรักษาใหม่
\[ 0.65\times 12000+0.25\times 60000+0.10\times 18000=24600 \]
ใช้ความน่าจะเป็นของการรักษาใหม่กับต้นทุนปลายทางชุดเดิม จะได้ค่าดูแลระยะเฉียบพลันและภายหลัง 24,600 บาท น้อยกว่าการดูแลมาตรฐาน 29,700 − 24,600 = 5,100 บาท เพราะผู้ป่วยที่ต้องพึ่งพามีน้อยลง
-
บวกค่าการรักษาครั้งเดียว
\[ 24600+22000=46600 \]
บวกค่าการรักษา 22,000 บาทครั้งเดียวต่อผู้ป่วย เพราะผู้ป่วยทุกคนที่ได้รับต้องจ่าย รวมทั้งผู้ที่เสียชีวิต ต้นทุนคาดหมายของการรักษาใหม่จึงเท่ากับ 46,600 บาท
-
ถ่วงน้ำหนักสุขภาพแยกต่างหาก
\[ 0.65\times 8+0.25\times 3+0.10\times 0=5.95 \]
ทำแบบเดียวกันกับ QALYs ตลอดชีวิต โดยไม่บวกราคาใด ๆ การดูแลมาตรฐานได้ 0.50 × 8 + 0.35 × 3 + 0.15 × 0 = 5.05 QALYs ต่อผู้ป่วย ส่วนการรักษาใหม่ได้ 5.95
-
หาผลต่าง
\[ 46600-29700=22000-5100=16900,\quad 5.95-5.05=0.9 \]
นำการรักษาใหม่ลบด้วยการดูแลมาตรฐานในแต่ละส่วน ได้ต้นทุนเพิ่ม 16,900 บาท ซึ่งคือราคา 22,000 บาท หักค่าดูแลภายหลังที่ประหยัดได้ 5,100 บาท และได้ QALYs เพิ่ม 0.9 ต่อผู้ป่วย
ผลลัพธ์: การดูแลมาตรฐาน: 29,700 บาทและ 5.05 QALYs ต่อผู้ป่วย การรักษาใหม่: 46,600 บาทและ 5.95 QALYs การรักษาใหม่แพงกว่า 16,900 บาท และเพิ่ม 0.9 QALYs
ตรวจการคำนวณย้อนกลับด้วย 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")
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
ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด building-a-decision-tree-step-by-step.R
สคริปต์ฉบับเต็ม
# 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")
0.9 QALYs คุ้มกับ 16,900 บาทหรือไม่
ต้นทุนกับสุขภาพยังอยู่คนละหน่วย ทีมจึงต้องมีอัตราแลกเปลี่ยน เกณฑ์ (threshold) คือจำนวนเงินสูงสุดที่ผู้จ่ายค่าบริการ (payer) เช่น กองทุนสุขภาพ ยอมจ่ายต่อหนึ่ง QALY ในที่นี้ใช้ 100,000 บาทต่อ QALY เพื่อแสดงวิธีคำนวณ ซึ่งเป็นตัวเลขกลม ๆ ที่สมมติขึ้น ไม่ใช่เกณฑ์เชิงนโยบาย ตัวอย่างร่วมของชุดบทความด้านล่างใช้ 160,000 บาทแทน และวิธีคิดเหมือนกันไม่ว่าเกณฑ์จะเป็นเท่าใด
ผลประโยชน์สุทธิในรูปตัวเงิน (net monetary benefit, NMB) คือสุขภาพที่ตีค่าด้วยเกณฑ์ลบด้วยต้นทุน ส่วนผลประโยชน์สุทธิส่วนเพิ่ม (incremental NMB, INMB) ใช้หลักเดียวกันกับส่วนต่างระหว่างสองทางเลือก คือสุขภาพที่ได้เพิ่มซึ่งตีค่าด้วยเกณฑ์ ลบด้วยต้นทุนที่เพิ่ม ถ้า INMB เป็นบวก แปลว่าต้นทุนที่เพิ่มคุ้มที่จะจ่าย ในตัวอย่างร่วมของชุดบทความ ที่เกณฑ์ 160,000 บาทต่อ QALY การได้เพิ่ม 1.00 QALY ด้วยต้นทุนที่มากกว่า 150,000 บาท ให้ INMB 10,000 บาท อ่านบทเรียนเต็มได้ที่บทความพื้นฐานเรื่องเกณฑ์และผลประโยชน์สุทธิ
สำหรับทีมโรคหลอดเลือดสมอง 0.9 QALYs × 100,000 บาท = สุขภาพมูลค่า 90,000 บาท เมื่อหักต้นทุนที่เพิ่ม 16,900 บาท จะเหลือ INMB 73,100 บาทต่อผู้ป่วย ค่านี้เป็นบวก ที่เกณฑ์นี้ต้นทุนที่เพิ่มจึงคุ้มที่จะจ่าย ราคาการรักษาขึ้นได้ถึง 95,100 บาทต่อผู้ป่วย คือราคาปัจจุบัน 22,000 บาท บวก INMB 73,100 บาท ก่อนที่ INMB จะลดลงเป็นศูนย์
INMB ที่เป็นบวกเป็นการเปรียบเทียบมูลค่าภายใต้ข้อมูลนำเข้าชุดนี้ ไม่ได้บอกว่าโรงพยาบาลมีเงินพอจ่าย การที่งบประมาณครอบคลุมผู้ป่วยโรคหลอดเลือดสมองทุกคนได้หรือไม่เป็นอีกคำถามหนึ่ง ซึ่งบทความตอนหลังของชุดนี้จะกล่าวถึง
ตรวจ INMB ด้วย 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")
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
ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด building-a-decision-tree-step-by-step.R
สคริปต์ฉบับเต็ม
# 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")
ข้อผิดพลาดสองแบบที่ต้นไม้ช่วยจับได้
ข้อผิดพลาดในต้นไม้ไม่ค่อยแสดงตัวออกมาให้เห็น แต่ให้ตัวเลขที่ดูน่าเชื่อ มีสองแบบที่พบบ่อยจนควรตรวจทุกครั้ง
แบบแรกคือชุดตัวเลขที่เรียงผิดลำดับ สมมติว่าพิมพ์ความน่าจะเป็นของการรักษาใหม่โดยเริ่มจากเสียชีวิต เป็น 0.10, 0.25 และ 0.65 แต่ต้นทุนเรียงโดยเริ่มจากช่วยเหลือตนเองได้ การรักษาใหม่จะดูเหมือนมีต้นทุน 0.10 × 12,000 + 0.25 × 60,000 + 0.65 × 18,000 + 22,000 = 49,900 บาท และได้เพียง 1.55 QALYs กลายเป็นผลเสียแทนผลดี การเขียนลำดับไว้ครั้งเดียวและใช้ลำดับนั้นในตัวเลขทุกชุดช่วยป้องกันเรื่องนี้
แบบที่สองคือคิดค่าการรักษาเฉพาะบางปลายทาง ถ้าบวก 22,000 บาทให้ปลายทางของผู้รอดชีวิตแต่ไม่บวกให้ผู้เสียชีวิต จะมีผู้ป่วยเพียง 0.65 + 0.25 = 0.9 ส่วนที่ถูกคิดค่าการรักษา ต้นทุนคาดหมายจึงเป็น 24,600 + 0.9 × 22,000 = 44,400 บาท แทนที่จะเป็น 46,600 บาท ผู้ป่วยที่เสียชีวิตก็ได้รับการรักษาแล้ว ราคานี้จึงเป็นของผู้ป่วยทุกคน
การบวกราคาให้ทุกปลายทางแล้วจึงถ่วงน้ำหนักนั้นปลอดภัย ได้ 46,600 บาทเท่าเดิม เพราะความน่าจะเป็นรวมได้ 1 การเขียนข้อมูลนำเข้าแต่ละค่า หน่วย และลำดับของมันไว้ในที่ที่ผู้อื่นตรวจได้ เป็นส่วนหนึ่งของแบบจำลองที่โปร่งใส (Eddy และคณะ; Briggs และคณะ)
ตรวจข้อผิดพลาดด้วย 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")
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
ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด building-a-decision-tree-step-by-step.R
สคริปต์ฉบับเต็ม
# 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")
คำศัพท์ตามที่ใช้ในบทความนี้
- Decision node
- จุดตัดสินใจ: จุดรูปสี่เหลี่ยมที่ผู้ตัดสินใจเลือกระหว่างทางเลือก
- Strategy
- ทางเลือก: แผนการดูแลที่ครบถ้วนสำหรับผู้ป่วยทุกคนที่เหมาะสมจะได้รับการรักษา เช่น การดูแลมาตรฐานหรือการรักษาใหม่
- Chance node
- จุดโอกาส: จุดรูปวงกลมที่ผู้ป่วยแยกตามสิ่งที่เกิดขึ้นกับเขา แต่ละกิ่งมีความน่าจะเป็นของตัวเอง
- Terminal node
- จุดปลายทาง: จุดรูปสามเหลี่ยมที่สิ้นสุดเส้นทาง และมีต้นทุนกับสุขภาพตลอดชีวิตของปลายทางนั้น
- Expected value
- ค่าคาดหมาย: ค่าเฉลี่ยของค่าที่ปลายทางซึ่งถ่วงด้วยความน่าจะเป็น คิดต่อผู้ป่วยหนึ่งคน
- Rolling back
- การคำนวณย้อนกลับ: การแทนจุดโอกาสแต่ละจุดด้วยค่าคาดหมาย โดยไล่จากปลายทางกลับไปที่การเลือก
- Time horizon
- กรอบเวลา: ระยะเวลาที่แบบจำลองนับต้นทุนและสุขภาพ ในที่นี้คือชีวิตที่เหลือของผู้ป่วยแต่ละคน
- Threshold
- เกณฑ์: จำนวนเงินสูงสุดที่ผู้จ่ายค่าบริการยอมจ่ายต่อหนึ่ง QALY
- INMB
- ผลประโยชน์สุทธิส่วนเพิ่ม: เกณฑ์คูณ QALYs ที่เพิ่ม ลบต้นทุนที่เพิ่ม ค่าบวกสนับสนุนทางเลือกใหม่
เชื่อมกับบทความพื้นฐาน
บทความพื้นฐานเรื่องต้นไม้การตัดสินใจและแบบจำลองกลุ่มประชากรแนะนำต้นไม้จากมุมเศรษฐศาสตร์ ส่วนบทความเรื่องเกณฑ์และผลประโยชน์สุทธิแสดงว่าเกณฑ์แปลงสุขภาพที่ได้เพิ่มเป็นเงินอย่างไร
ลองใช้ในแอป
เปิดแอปแผนภูมิต้นไม้การตัดสินใจของเราเพื่อลองเปลี่ยนค่าข้อมูลนำเข้าและดูผลลัพธ์เปลี่ยนตาม ต้องเข้าสู่ระบบและใช้แผน Pro หรือ Max
แอปเริ่มจากค่าตัวอย่างของแอปเอง ตัวเลขในแอปจึงไม่ตรงกับบทความนี้
เอกสารอ้างอิง
- 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
- 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
- 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
ลำดับการเรียนในชุดนี้
- คู่มือชุดบทความการสร้างแบบจำลองเศรษฐศาสตร์สุขภาพ: จากหลักฐานสู่การตัดสินใจ
- การเลือกแบบจำลองเพื่อการตัดสินใจ: ให้เส้นทางทางคลินิกกำหนดโครงสร้าง
- ข้อมูลนำเข้าของแบบจำลองที่ตรวจสอบได้: สร้างตารางพารามิเตอร์ที่สืบย้อนถึงที่มา
- การค้นหาข้อมูลนำเข้าของแบบจำลองจากวรรณกรรม: เลือกค่าประมาณให้ตรงกับคำถาม
- อัตรา ความน่าจะเป็น และความน่าจะเป็นของการเปลี่ยนสถานะ: คำนวณโดยไม่หลงหน่วยเวลา
- สร้างแผนภูมิต้นไม้การตัดสินใจทีละขั้น: ตามรอยผลลัพธ์ ต้นทุน และสุขภาพ (บทความนี้)
- สร้างแบบจำลองมาร์คอฟแบบกลุ่มประชากรทีละขั้น: ติดตามประชากรผ่านเวลา
- การวิเคราะห์ผลกระทบด้านงบประมาณในทางปฏิบัติ: นับคนให้ชัดก่อนคำนวณเงิน
- ICER และผลประโยชน์สุทธิในรูปตัวเงินเมื่อมีหลายทางเลือก: หาขอบเขตทางเลือกที่มีประสิทธิภาพ
- การวิเคราะห์ความไวแบบอาศัยความน่าจะเป็น: จากการแจกแจงพารามิเตอร์สู่ CEAC
- การประเมินความสมเหตุสมผลและตรวจสอบแบบจำลอง: ทดสอบคำถาม สมการ และโค้ด
- มูลค่าคาดหมายของข้อมูลสมบูรณ์: หลักฐานที่ดีขึ้นอาจมีมูลค่าเพียงใด
ประเด็นสำคัญ
- วาดการเลือกเป็นสี่เหลี่ยม โอกาสเป็นวงกลม และปลายทางเป็นสามเหลี่ยม และใช้ลำดับของปลายทางเดียวกันในตัวเลขทุกชุด
- ถ่วงน้ำหนักแต่ละปลายทางด้วยความน่าจะเป็นแล้วรวม โดยคิดต้นทุนกับ QALYs แยกกัน ได้ 29,700 บาทและ 5.05 QALYs เทียบกับ 46,600 บาทและ 5.95
- คิดค่าการรักษาครั้งเดียวให้ผู้ป่วยทุกคนที่ได้รับ รวมทั้งผู้ที่เสียชีวิต
- ตีค่า QALYs ที่เพิ่ม 0.9 ด้วยเกณฑ์แล้วหักต้นทุนที่เพิ่ม 16,900 บาท INMB 73,100 บาทต่อผู้ป่วยเป็นการเทียบมูลค่า ไม่ใช่ความสามารถในการจ่าย
อ่านต่อในวิกิ: ต้นไม้การตัดสินใจ แบบจำลองมาร์คอฟ และการวิเคราะห์ความไวเชิงความน่าจะเป็น: แบบจำลองแปลงหลักฐานเป็นการตัดสินใจได้อย่างไร · อัตราส่วนต้นทุนประสิทธิผลส่วนเพิ่มและระนาบต้นทุนประสิทธิผล: ทางเลือกที่ด้อยกว่า ผลประโยชน์สุทธิ และเกณฑ์ 160,000 บาทต่อปีสุขภาวะ