มูลค่าคาดหมายของข้อมูลสมบูรณ์: หลักฐานที่ดีขึ้นอาจมีมูลค่าเพียงใด

On this page
Read the English version
บทคัดย่อ
คณะกรรมการยาของเครือข่ายโรงพยาบาลแห่งหนึ่งต้องเลือกยา A หรือยา B ให้ผู้ใหญ่ที่มีภาวะหัวใจล้มเหลวราวปีละ 2,000 คน การทดลองที่มีต้นทุน 40 ล้านบาทจะตอบได้ชัดว่ายาใดดีกว่า แบบจำลองของคณะกรรมการตีค่ายาแต่ละตัวต่อผู้ป่วยหนึ่งคนเป็นสุขภาพที่แปลงเป็นเงินลบด้วยต้นทุน โดยเฉลี่ย B มีค่า 120 พันบาทต่อผู้ป่วย และ A มีค่า 115 พันบาท วันนี้ B จึงชนะ แต่หลักฐานยังเข้ากันได้กับความจริงสี่แบบที่มีโอกาสเท่ากัน (เราเรียกความจริงแต่ละแบบว่าโลก) และในสองโลกนั้น A จะดีกว่า ถ้ารู้ความจริงก่อน คณะกรรมการจะเลือกยาที่ดีกว่าในแต่ละโลก และค่าของยาที่เลือกเหล่านั้นเฉลี่ยได้ 135 มูลค่าคาดหมายของข้อมูลสมบูรณ์ (expected value of perfect information, EVPI) คือผลได้เฉลี่ยจากการรู้ความจริงก่อนเลือก เท่ากับ 135 ลบ 120 พันบาท หรือ 15 พันบาทต่อผู้ป่วย เมื่อนับผู้ป่วยตลอดห้าปีและคิดเป็นเงินวันนี้ การตอบคำถามนี้ให้ชัดมีค่าได้ไม่เกิน 141.51 ล้านบาท การทดลองนี้จึงยังไม่ถูกตัดทิ้ง แต่ประโยชน์ที่แท้จริงของการทดลองจริงจะต่ำกว่าเพดานนี้
คณะกรรมการยาที่ต้องเลือกตอนนี้หรือรอผลการทดลอง
เครือข่ายโรงพยาบาลระดับจังหวัดแห่งหนึ่งเริ่มการรักษาระยะยาวให้ผู้ใหญ่ที่มีภาวะหัวใจล้มเหลวราวปีละ 2,000 คน คณะกรรมการยาของเครือข่ายต้องเลือกยาทางเลือกแรก (first-line) หนึ่งตัวให้ทุกคน คือ A ซึ่งเป็นยาที่ใช้อยู่ หรือ B ซึ่งเป็นยาใหม่ที่แพงกว่า การทดลองขนาดเล็กชี้ว่า B ป้องกันการนอนโรงพยาบาลได้มากกว่า แต่ช่วงความเชื่อมั่นกว้าง กลุ่มนักวิจัยจึงเสนอการทดลองที่ใหญ่กว่าซึ่งมีต้นทุน 40 ล้านบาทเพื่อตอบคำถามนี้ให้ชัด
การชั่งยาสองตัวต้องใช้หน่วยสุขภาพหน่วยเดียวที่นับทั้งความยืนยาวและคุณภาพชีวิต ปีสุขภาวะ (quality-adjusted life year, QALY) คือปีชีวิตที่ถ่วงน้ำหนักด้วยคุณภาพ ตั้งแต่ 1 สำหรับสุขภาพสมบูรณ์ถึง 0 สำหรับการเสียชีวิต การอยู่ 10 ปีในสภาวะที่มีค่า 0.70 เท่ากับ 7.0 QALY การมีสุขภาพสมบูรณ์ตลอดช่วงนั้นจะเพิ่มอีก 3.0 QALY อ่านบทเรียนเต็มได้ที่บทความพื้นฐานเรื่องปีสุขภาวะ
จากนั้นแบบจำลองแปลงสุขภาพเป็นเงินแล้วหักต้นทุน ผลประโยชน์สุทธิในรูปตัวเงิน (net monetary benefit, NMB) คือสุขภาพที่ตีค่าด้วยเกณฑ์ลบด้วยต้นทุน โดยเกณฑ์คือเงินสูงสุดที่ผู้จ่ายยอมจ่ายต่อหนึ่ง QALY ผลประโยชน์สุทธิส่วนเพิ่ม (incremental NMB, INMB) ที่เป็นบวกแปลว่าต้นทุนที่เพิ่มคุ้มที่จะจ่าย ที่เกณฑ์ 160,000 บาทต่อ QALY การได้เพิ่ม 1.00 QALY ด้วยต้นทุนที่สูงขึ้น 150,000 บาท ให้ INMB 10,000 บาท อ่านบทเรียนเต็มได้ที่บทความพื้นฐานเรื่องผลประโยชน์สุทธิและเกณฑ์
แบบจำลองของคณะกรรมการให้ NMB ต่อผู้ป่วยหนึ่งคนของยาแต่ละตัว โดยนับเป็นพันบาท B มีค่า 120 และ A มีค่า 115 พันบาท เมื่อเฉลี่ยตามทุกความเป็นไปได้ที่หลักฐานตอนนี้ยังเปิดไว้ (หัวข้อถัดไปจะอธิบายให้ชัด) B จึงดูดีกว่าเล็กน้อย ตัวเลขทั้งหมดในบทความนี้สมมติขึ้นเพื่อการสอน ไม่ใช่หลักฐานเรื่องยารักษาภาวะหัวใจล้มเหลวตัวใด
คำตอบที่ชวนให้เชื่อ: หลักฐานไม่แน่นอนจึงต้องรอ
หลายคนจะตอบทันทีว่า ช่วงความเชื่อมั่นกว้าง จึงควรรอผลการทดลอง นี่คือความเข้าใจผิดที่บทความนี้แก้ ความไม่แน่นอนคุ้มที่จะลดเฉพาะเมื่ออาจเปลี่ยนการเลือกได้ และคุ้มเพียงเท่าความสูญเสียที่เกิดจากการเลือกผิด Fenwick และคณะอธิบายวิธีประเมินมูลค่าของงานวิจัยจากการตัดสินใจที่งานวิจัยนั้นช่วยสนับสนุน
คณะกรรมการจึงต้องการคำตอบสามข้อ การเลือก B จะผิดบ่อยเพียงใด การเลือกผิดแต่ละครั้งสูญเสียเท่าใด และการเลือกนี้จะส่งผลถึงผู้ป่วยกี่คน ส่วนที่เหลือของบทความตอบคำถามเหล่านี้ด้วยตัวเลขของคณะกรรมการ
โลกที่เป็นไปได้สี่แบบ
ยังไม่มีใครรู้ว่า B ป้องกันการนอนโรงพยาบาลได้ดีเพียงใด ผู้ป่วยต้องนอนโรงพยาบาลบ่อยเพียงใด หรือการนอนโรงพยาบาลแต่ละครั้งมีต้นทุนเท่าใด แบบจำลองของคณะกรรมการจึงทำงานกับความจริงสี่แบบที่มีโอกาสเท่ากัน ซึ่งเราเรียกว่าโลกที่เป็นไปได้ โลกแต่ละแบบกำหนดค่าที่ยังไม่รู้ทุกตัวพร้อมกัน แล้วตีค่ายาทั้งสองตัวในโลกนั้น
การวิเคราะห์ความไวเชิงความน่าจะเป็น (probabilistic sensitivity analysis, PSA) คือวิธีที่แบบจำลองเต็มรูปแบบสร้างโลกได้มากกว่านี้มาก โดยรันแบบจำลองซ้ำหลายครั้งและสุ่มข้อมูลนำเข้าที่ไม่แน่นอนทุกตัวจากการแจกแจงของมัน ผลจึงเป็นสัดส่วนของการรัน ไม่ใช่ตัวเลขเดียว เมื่อผลประโยชน์สุทธิส่วนเพิ่มเฉลี่ย 10,000 บาท และส่วนเบี่ยงเบนมาตรฐาน 40,000 บาท จะมีราว 599 จาก 1,000 ครั้งที่การรักษาคุ้มค่า อ่านบทเรียนเต็มได้ที่ การวิเคราะห์ความไวแบบอาศัยความน่าจะเป็น: จากการแจกแจงพารามิเตอร์สู่ CEAC
โลกสี่แบบก็พอให้เห็นทุกขั้น ให้อ่านภาพด้านล่างทีละแถวจากซ้ายไปขวา ไม่ใช่ตามคอลัมน์ ในโลกที่หนึ่ง B มีค่า 120 และ A มีค่า 100 พันบาท B จึงดีกว่า ในโลกที่สอง A มีค่า 140 และ B มีค่า 110 พันบาท A จึงดีกว่า
ในโลกที่สาม B มีค่า 150 และ A มีค่า 90 พันบาท B จึงดีกว่า ในโลกที่สี่ A มีค่า 130 และ B มีค่า 100 พันบาท A จึงดีกว่า สรุปคือ B ดีกว่าในโลกที่หนึ่งและโลกที่สาม ส่วน A ดีกว่าในโลกที่สองและโลกที่สี่ แถวหนึ่งคือความจริงแบบหนึ่งสำหรับผู้ป่วยทุกคน ไม่ใช่ผู้ป่วยกลุ่มหนึ่งที่คณะกรรมการจะให้ยาต่างกันได้
ตรวจตัวอย่างของชุดบทความด้วย R
# Trace the series' shared examples that this article recaps before the committee's own numbers.
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
years_lived <- 10
state_weight <- 0.70
lambda <- 160000
mean_gain <- 10000
sd_gain <- 40000
runs <- 1000
cat("10 years at 0.70 =", formatC(years_lived * state_weight, format = "f", digits = 1),
"| full health adds", formatC(years_lived * 0.30, format = "f", digits = 1), "\n")
cat("Series example: 160000 x 1.00 - 150000 =", big(lambda * 1.00 - 150000), "THB\n")
cat("Average extra gain", big(mean_gain), "THB, spread", big(sd_gain), "THB | runs favouring it:",
round(runs * stats::pnorm(mean_gain / sd_gain)), "of", big(runs), "\n")
10 years at 0.70 = 7.0 | full health adds 3.0
Series example: 160000 x 1.00 - 150000 = 10,000 THB
Average extra gain 10,000 THB, spread 40,000 THB | runs favouring it: 599 of 1,000
ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · dampack 1.0.2.1000 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด expected-value-of-perfect-information.R
สคริปต์ฉบับเต็ม
# Two drugs, four possible worlds: what knowing the truth first is worth to a hospital network
# ยาสองตัว ความจริงที่เป็นไปได้สี่แบบ: การรู้ความจริงก่อนเลือกมีค่าเท่าใดต่อเครือข่ายโรงพยาบาล
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# dampack 1.0.2.1000 (stable)
# install.packages(c("tibble", "dplyr", "dampack"))
# update.packages(oldPkgs = c("tibble", "dplyr", "dampack"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("dampack", "1.0.2.1000")
library(tibble) # Write the four worlds as an explicit input table.
library(dplyr) # Add each world's best value and loss to the table.
library(dampack) # Cross-check the gain from knowing first with an independent routine.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The series' shared examples
# A health weight over ten years, the threshold, and the share of reruns that favour a treatment.
# ค่าน้ำหนักสุขภาพตลอดสิบปี เกณฑ์ และสัดส่วนของการรันซ้ำที่เอียงไปทางการรักษา
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
years_lived <- 10
state_weight <- 0.70
lambda <- 160000
mean_gain <- 10000
sd_gain <- 40000
runs <- 1000
stopifnot(round(runs * stats::pnorm(mean_gain / sd_gain)) == 599)
#%%@note 2. Four worlds, both drugs valued in the same world
# Each row is one equally likely version of the truth. Values are thousand THB per patient.
# แต่ละแถวคือความจริงแบบหนึ่งที่มีโอกาสเท่ากัน ค่าเป็นพันบาทต่อผู้ป่วย และเป็นตัวเลขสมมติ
nmb_a <- c(100, 140, 90, 130)
nmb_b <- c(120, 110, 150, 100)
worlds <- tibble::tibble(world = seq_along(nmb_a), drug_a = nmb_a, drug_b = nmb_b) |>
dplyr::mutate(best = pmax(drug_a, drug_b), loss_with_b = best - drug_b)
averages <- c(A = mean(nmb_a), B = mean(nmb_b))
choose_now <- max(averages)
row_best <- pmax(nmb_a, nmb_b)
know_first <- mean(row_best)
stopifnot(know_first - choose_now == mean(worlds$loss_with_b))
#%%@note 3. What choosing B loses, then two checks
# Every A value lower by 10 keeps the same losing worlds. Sorting each column breaks the worlds.
# ลด A ลง 10 ทุกแถวยังแพ้ในโลกเดิม ส่วนการเรียงแต่ละคอลัมน์แยกกันทำให้โลกแตกออกจากกัน
shift_a <- 10
loss_b <- pmax(nmb_a, nmb_b) - nmb_b
lower_a <- nmb_a - shift_a
loss_lower <- pmax(lower_a, nmb_b) - nmb_b
sorted_best <- pmax(sort(nmb_a), sort(nmb_b))
stopifnot(sum(loss_lower > 0) == sum(loss_b > 0), mean(sorted_best) < know_first)
#%%@note 4. Cross-check with an independent routine
# dampack values each strategy as effect x wtp - cost, so effect = value, cost = 0 and wtp = 1.
# dampack คิดค่าเป็น effect x wtp - cost จึงให้ effect เป็นค่า ให้ cost เป็น 0 และ wtp เป็น 1
zero_cost <- data.frame(A = 0 * nmb_a, B = 0 * nmb_b)
psa_obj <- dampack::make_psa_obj(cost = zero_cost, effectiveness = data.frame(A = nmb_a, B = nmb_b),
parameters = data.frame(world = seq_along(nmb_a)), strategies = c("A", "B"))
checked <- dampack::calc_evpi(psa_obj, wtp = 1)
stopifnot(abs(checked[[2]] - (know_first - choose_now)) < 1e-9)
#%%@note 5. From one patient to the network, in today's money
# Patients reached over the years the choice stands, then each year divided by 1.03 once more.
# จำนวนผู้ป่วยตลอดช่วงที่การเลือกนี้ใช้อยู่ แล้วหารค่าของแต่ละปีด้วย 1.03 เพิ่มอีกหนึ่งครั้ง
per_patient <- 15000
per_year <- 2000
years_used <- 5
reached <- per_year * years_used
yearly_factor <- 1.03
trial_cost <- 40
dividers <- yearly_factor^(seq_len(years_used) - 1)
today <- per_year * per_patient / 1e6 / dividers
stopifnot(per_patient == 1000 * (know_first - choose_now), trial_cost < sum(today))
#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("10 years at 0.70 =", formatC(years_lived * state_weight, format = "f", digits = 1),
"| full health adds", formatC(years_lived * 0.30, format = "f", digits = 1), "\n")
cat("Series example: 160000 x 1.00 - 150000 =", big(lambda * 1.00 - 150000), "THB\n")
cat("Average extra gain", big(mean_gain), "THB, spread", big(sd_gain), "THB | runs favouring it:",
round(runs * stats::pnorm(mean_gain / sd_gain)), "of", big(runs), "\n")
cat("Average value, A and B:", averages, "| choose now: drug", names(which.max(averages)), "\n")
cat("Better drug in each world:", ifelse(nmb_a > nmb_b, "A", "B"), "\n")
cat("Best value in each world:", row_best, "| their average:", know_first, "\n")
cat("Knowing first:", know_first, "- choosing now:", choose_now, "=", know_first - choose_now, "\n")
cat("Loss from choosing B in each world:", loss_b, "| average:", mean(loss_b), "\n")
cat("Worlds where B loses:", sum(loss_b > 0), "of", length(loss_b),
"| share (%):", 100 * mean(loss_b > 0), "\n")
cat("A lower by 10: average", mean(lower_a), "| losses", loss_lower, "| average:",
mean(loss_lower), "\n")
cat("Sorted A:", sort(nmb_a), "| sorted B:", sort(nmb_b), "| best in each row:", sorted_best, "\n")
cat("Their average:", mean(sorted_best), "| gap over choosing now:",
mean(sorted_best) - max(mean(nmb_a), mean(nmb_b)), "\n")
cat("Patients reached:", big(per_year), "x", years_used, "=", big(reached), "\n")
cat("Each year:", big(per_year), "x", big(per_patient), "THB =", big(per_year * per_patient / 1e6),
"million THB\n")
cat("Ceiling:", big(reached), "x", big(per_patient), "THB =", big(reached * per_patient / 1e6),
"million THB\n")
cat("Series: 1,834 THB due in 3 years at (%):", 3, "->", round(1834 / yearly_factor^3), "THB\n")
cat("Dividers, year by year:", formatC(dividers, format = "f", digits = 4), "\n")
cat("Each year in today's money (million THB):", formatC(today, format = "f", digits = 2), "\n")
cat("Ceiling today (million THB):", formatC(sum(today), format = "f", digits = 2), "\n")
cat("Trial (million THB):", trial_cost, "| below the ceiling:", trial_cost < sum(today), "\n")
เลือกตอนนี้ หรือเลือกหลังรู้ความจริง
ถ้าเลือกตอนนี้ คณะกรรมการบอกไม่ได้ว่าโลกใดเป็นความจริง และต้องให้ยาตัวเดียวกับผู้ป่วยทุกคน จึงตัดสินยาแต่ละตัวจากค่าเฉลี่ยของโลกทั้งสี่แบบ แล้วเลือกยาที่มีค่าเฉลี่ยสูงกว่า
ลองนึกว่าคณะกรรมการรู้ได้ก่อนว่าโลกใดเป็นความจริง ก็จะให้ยาที่ดีกว่าในโลกนั้น แต่ในตอนนี้ยังไม่รู้ว่าเป็นโลกใด และทุกโลกมีโอกาสเท่ากัน ค่าของการเลือกหลังรู้ความจริงจึงเท่ากับค่าเฉลี่ยของค่าที่ดีที่สุดในแต่ละโลก ตัวอย่างคำนวณด้านล่างแสดงทั้งสองลำดับ
เฉลี่ยก่อนแล้วเลือก หรือเลือกก่อนแล้วเฉลี่ย
-
เลือกตอนนี้: เฉลี่ยก่อนแล้วเลือก
\[ \max(115,\ 120) = 120 \]
ยา A มีค่าเฉลี่ย (100 + 140 + 90 + 130) / 4 = 115 พันบาทต่อผู้ป่วย ยา B มีค่าเฉลี่ย (120 + 110 + 150 + 100) / 4 = 120 พันบาท คณะกรรมการจึงเลือก B
-
รู้ความจริงก่อน: เลือกก่อนแล้วเฉลี่ย
\[ \frac{120 + 140 + 150 + 130}{4} = 135 \]
ในแต่ละโลกให้เลือกยาที่ดีกว่า คือ 120 ของ B, 140 ของ A, 150 ของ B และ 130 ของ A ค่าที่ดีที่สุดเหล่านี้เฉลี่ยได้ 135 พันบาทต่อผู้ป่วย
-
ผลได้จากการรู้ความจริงก่อน
\[ 135 - 120 = 15 \]
นำค่าของการเลือกตอนนี้ไปลบออกจากค่าของการรู้ความจริงก่อน
ผลลัพธ์: การรู้ว่าโลกใดเป็นความจริงก่อนเลือกมีค่าเฉลี่ย 15 พันบาทต่อผู้ป่วย
ช่องว่างนี้มีชื่อเรียก
ผลได้ 135 ลบ 120 = 15 พันบาทต่อผู้ป่วยนี้มีชื่อเรียกว่า มูลค่าคาดหมายของข้อมูลสมบูรณ์ (expected value of perfect information, EVPI) หมายถึงผลได้เฉลี่ยจากการรู้ค่าจริงของข้อมูลนำเข้าก่อนเลือก จึงเป็นมูลค่าสูงสุดที่การขจัดความไม่แน่นอนทั้งหมดจะมีให้ คำว่าคาดหมายหมายถึงเฉลี่ยตามโลกที่เป็นไปได้ และคำว่าสมบูรณ์หมายถึงรู้ค่าที่ยังไม่รู้ครบทุกตัว
พูดสั้น ๆ EVPI คือการนำค่าที่ดีที่สุดของแต่ละโลกมาเฉลี่ย (135) แล้วลบด้วยค่าเฉลี่ยของยาตัวที่ดีที่สุดเมื่อเลือกตอนนี้ (120) ค่านี้ไม่มีทางต่ำกว่าศูนย์ เพราะคณะกรรมการที่รู้ความจริงแล้วยังเลือกยาตัวเดิมที่เคยเลือกตอนไม่รู้ความจริงได้เสมอ
ตรวจสองลำดับด้วย R
# Four equally likely worlds. Each row holds both drugs' value in the same world.
# Values are thousand THB per patient, illustrative.
nmb_a <- c(100, 140, 90, 130)
nmb_b <- c(120, 110, 150, 100)
averages <- c(A = mean(nmb_a), B = mean(nmb_b))
choose_now <- max(averages)
row_best <- pmax(nmb_a, nmb_b)
know_first <- mean(row_best)
cat("Average value, A and B:", averages, "| choose now: drug", names(which.max(averages)), "\n")
cat("Better drug in each world:", ifelse(nmb_a > nmb_b, "A", "B"), "\n")
cat("Best value in each world:", row_best, "| their average:", know_first, "\n")
cat("Knowing first:", know_first, "- choosing now:", choose_now, "=", know_first - choose_now, "\n")
Average value, A and B: 115 120 | choose now: drug B
Better drug in each world: B A B A
Best value in each world: 120 140 150 130 | their average: 135
Knowing first: 135 - choosing now: 120 = 15
ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · dampack 1.0.2.1000 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด expected-value-of-perfect-information.R
สคริปต์ฉบับเต็ม
# Two drugs, four possible worlds: what knowing the truth first is worth to a hospital network
# ยาสองตัว ความจริงที่เป็นไปได้สี่แบบ: การรู้ความจริงก่อนเลือกมีค่าเท่าใดต่อเครือข่ายโรงพยาบาล
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# dampack 1.0.2.1000 (stable)
# install.packages(c("tibble", "dplyr", "dampack"))
# update.packages(oldPkgs = c("tibble", "dplyr", "dampack"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("dampack", "1.0.2.1000")
library(tibble) # Write the four worlds as an explicit input table.
library(dplyr) # Add each world's best value and loss to the table.
library(dampack) # Cross-check the gain from knowing first with an independent routine.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The series' shared examples
# A health weight over ten years, the threshold, and the share of reruns that favour a treatment.
# ค่าน้ำหนักสุขภาพตลอดสิบปี เกณฑ์ และสัดส่วนของการรันซ้ำที่เอียงไปทางการรักษา
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
years_lived <- 10
state_weight <- 0.70
lambda <- 160000
mean_gain <- 10000
sd_gain <- 40000
runs <- 1000
stopifnot(round(runs * stats::pnorm(mean_gain / sd_gain)) == 599)
#%%@note 2. Four worlds, both drugs valued in the same world
# Each row is one equally likely version of the truth. Values are thousand THB per patient.
# แต่ละแถวคือความจริงแบบหนึ่งที่มีโอกาสเท่ากัน ค่าเป็นพันบาทต่อผู้ป่วย และเป็นตัวเลขสมมติ
nmb_a <- c(100, 140, 90, 130)
nmb_b <- c(120, 110, 150, 100)
worlds <- tibble::tibble(world = seq_along(nmb_a), drug_a = nmb_a, drug_b = nmb_b) |>
dplyr::mutate(best = pmax(drug_a, drug_b), loss_with_b = best - drug_b)
averages <- c(A = mean(nmb_a), B = mean(nmb_b))
choose_now <- max(averages)
row_best <- pmax(nmb_a, nmb_b)
know_first <- mean(row_best)
stopifnot(know_first - choose_now == mean(worlds$loss_with_b))
#%%@note 3. What choosing B loses, then two checks
# Every A value lower by 10 keeps the same losing worlds. Sorting each column breaks the worlds.
# ลด A ลง 10 ทุกแถวยังแพ้ในโลกเดิม ส่วนการเรียงแต่ละคอลัมน์แยกกันทำให้โลกแตกออกจากกัน
shift_a <- 10
loss_b <- pmax(nmb_a, nmb_b) - nmb_b
lower_a <- nmb_a - shift_a
loss_lower <- pmax(lower_a, nmb_b) - nmb_b
sorted_best <- pmax(sort(nmb_a), sort(nmb_b))
stopifnot(sum(loss_lower > 0) == sum(loss_b > 0), mean(sorted_best) < know_first)
#%%@note 4. Cross-check with an independent routine
# dampack values each strategy as effect x wtp - cost, so effect = value, cost = 0 and wtp = 1.
# dampack คิดค่าเป็น effect x wtp - cost จึงให้ effect เป็นค่า ให้ cost เป็น 0 และ wtp เป็น 1
zero_cost <- data.frame(A = 0 * nmb_a, B = 0 * nmb_b)
psa_obj <- dampack::make_psa_obj(cost = zero_cost, effectiveness = data.frame(A = nmb_a, B = nmb_b),
parameters = data.frame(world = seq_along(nmb_a)), strategies = c("A", "B"))
checked <- dampack::calc_evpi(psa_obj, wtp = 1)
stopifnot(abs(checked[[2]] - (know_first - choose_now)) < 1e-9)
#%%@note 5. From one patient to the network, in today's money
# Patients reached over the years the choice stands, then each year divided by 1.03 once more.
# จำนวนผู้ป่วยตลอดช่วงที่การเลือกนี้ใช้อยู่ แล้วหารค่าของแต่ละปีด้วย 1.03 เพิ่มอีกหนึ่งครั้ง
per_patient <- 15000
per_year <- 2000
years_used <- 5
reached <- per_year * years_used
yearly_factor <- 1.03
trial_cost <- 40
dividers <- yearly_factor^(seq_len(years_used) - 1)
today <- per_year * per_patient / 1e6 / dividers
stopifnot(per_patient == 1000 * (know_first - choose_now), trial_cost < sum(today))
#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("10 years at 0.70 =", formatC(years_lived * state_weight, format = "f", digits = 1),
"| full health adds", formatC(years_lived * 0.30, format = "f", digits = 1), "\n")
cat("Series example: 160000 x 1.00 - 150000 =", big(lambda * 1.00 - 150000), "THB\n")
cat("Average extra gain", big(mean_gain), "THB, spread", big(sd_gain), "THB | runs favouring it:",
round(runs * stats::pnorm(mean_gain / sd_gain)), "of", big(runs), "\n")
cat("Average value, A and B:", averages, "| choose now: drug", names(which.max(averages)), "\n")
cat("Better drug in each world:", ifelse(nmb_a > nmb_b, "A", "B"), "\n")
cat("Best value in each world:", row_best, "| their average:", know_first, "\n")
cat("Knowing first:", know_first, "- choosing now:", choose_now, "=", know_first - choose_now, "\n")
cat("Loss from choosing B in each world:", loss_b, "| average:", mean(loss_b), "\n")
cat("Worlds where B loses:", sum(loss_b > 0), "of", length(loss_b),
"| share (%):", 100 * mean(loss_b > 0), "\n")
cat("A lower by 10: average", mean(lower_a), "| losses", loss_lower, "| average:",
mean(loss_lower), "\n")
cat("Sorted A:", sort(nmb_a), "| sorted B:", sort(nmb_b), "| best in each row:", sorted_best, "\n")
cat("Their average:", mean(sorted_best), "| gap over choosing now:",
mean(sorted_best) - max(mean(nmb_a), mean(nmb_b)), "\n")
cat("Patients reached:", big(per_year), "x", years_used, "=", big(reached), "\n")
cat("Each year:", big(per_year), "x", big(per_patient), "THB =", big(per_year * per_patient / 1e6),
"million THB\n")
cat("Ceiling:", big(reached), "x", big(per_patient), "THB =", big(reached * per_patient / 1e6),
"million THB\n")
cat("Series: 1,834 THB due in 3 years at (%):", 3, "->", round(1834 / yearly_factor^3), "THB\n")
cat("Dividers, year by year:", formatC(dividers, format = "f", digits = 4), "\n")
cat("Each year in today's money (million THB):", formatC(today, format = "f", digits = 2), "\n")
cat("Ceiling today (million THB):", formatC(sum(today), format = "f", digits = 2), "\n")
cat("Trial (million THB):", trial_cost, "| below the ceiling:", trial_cost < sum(today), "\n")
อ่านผลได้นี้เป็นความสูญเสียที่เลี่ยงได้
ยังมีอีกทางหนึ่งที่ได้ 15 การเลือก B ให้ทุกคนไม่สูญเสียอะไรในโลกที่หนึ่งและโลกที่สาม ซึ่ง B ดีกว่า แต่สูญเสีย 140 - 110 = 30 ในโลกที่สอง และสูญเสีย 130 - 100 = 30 ในโลกที่สี่ เมื่อเฉลี่ยทั้งสี่โลก (0 + 30 + 0 + 30) / 4 = 15 ได้ตัวเลขเดียวกัน
ดังนั้น EVPI คือความสูญเสียเฉลี่ยจากการเลือกที่ทำตอนนี้ ซึ่งความรู้ที่สมบูรณ์จะขจัดออกได้ ค่านี้ขึ้นกับสองสิ่งพร้อมกัน คือเลือกผิดบ่อยเพียงใด และการเลือกผิดแต่ละครั้งสูญเสียเท่าใด
ทีนี้ลองทดสอบคำตอบที่ชวนให้เชื่อ การเลือก B ผิดในสองจากสี่โลก คิดเป็นโอกาส 50% ลองสมมติว่าค่าของ A ต่ำลง 10 ในทุกโลก ค่าเฉลี่ยของ A จึงเหลือ 105 พันบาท การเลือก B ยังผิดในสองโลกเดิม แต่สูญเสียเพียง 130 - 110 = 20 ในโลกที่สอง (A เหลือ 140 - 10 = 130) และ 120 - 100 = 20 ในโลกที่สี่ (A เหลือ 130 - 10 = 120) ความสูญเสียเฉลี่ยจึงลดเหลือ (0 + 20 + 0 + 20) / 4 = 10
โอกาสเลือกผิดยังเป็น 50% เท่าเดิม แต่การรู้ความจริงก่อนมีค่าลดลงหนึ่งในสาม และถ้า B ดีกว่าในทุกโลก การรู้ความจริงก่อนจะไม่เปลี่ยนอะไรเลย EVPI จะเป็น 0 ไม่ว่าช่วงความเชื่อมั่นจะดูกว้างเพียงใด Briggs, Claxton และ Sculpher อธิบายการมองความไม่แน่นอนผ่านการตัดสินใจแบบนี้ไว้ในตำราของตน
ตรวจการอ่านเป็นความสูญเสียด้วย R
# What choosing B loses in each world, then a check with every A value lower by 10.
nmb_a <- c(100, 140, 90, 130)
nmb_b <- c(120, 110, 150, 100)
shift_a <- 10
loss_b <- pmax(nmb_a, nmb_b) - nmb_b
lower_a <- nmb_a - shift_a
loss_lower <- pmax(lower_a, nmb_b) - nmb_b
cat("Loss from choosing B in each world:", loss_b, "| average:", mean(loss_b), "\n")
cat("Worlds where B loses:", sum(loss_b > 0), "of", length(loss_b),
"| share (%):", 100 * mean(loss_b > 0), "\n")
cat("A lower by 10: average", mean(lower_a), "| losses", loss_lower, "| average:",
mean(loss_lower), "\n")
Loss from choosing B in each world: 0 30 0 30 | average: 15
Worlds where B loses: 2 of 4 | share (%): 50
A lower by 10: average 105 | losses 0 20 0 20 | average: 10
ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · dampack 1.0.2.1000 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด expected-value-of-perfect-information.R
สคริปต์ฉบับเต็ม
# Two drugs, four possible worlds: what knowing the truth first is worth to a hospital network
# ยาสองตัว ความจริงที่เป็นไปได้สี่แบบ: การรู้ความจริงก่อนเลือกมีค่าเท่าใดต่อเครือข่ายโรงพยาบาล
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# dampack 1.0.2.1000 (stable)
# install.packages(c("tibble", "dplyr", "dampack"))
# update.packages(oldPkgs = c("tibble", "dplyr", "dampack"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("dampack", "1.0.2.1000")
library(tibble) # Write the four worlds as an explicit input table.
library(dplyr) # Add each world's best value and loss to the table.
library(dampack) # Cross-check the gain from knowing first with an independent routine.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The series' shared examples
# A health weight over ten years, the threshold, and the share of reruns that favour a treatment.
# ค่าน้ำหนักสุขภาพตลอดสิบปี เกณฑ์ และสัดส่วนของการรันซ้ำที่เอียงไปทางการรักษา
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
years_lived <- 10
state_weight <- 0.70
lambda <- 160000
mean_gain <- 10000
sd_gain <- 40000
runs <- 1000
stopifnot(round(runs * stats::pnorm(mean_gain / sd_gain)) == 599)
#%%@note 2. Four worlds, both drugs valued in the same world
# Each row is one equally likely version of the truth. Values are thousand THB per patient.
# แต่ละแถวคือความจริงแบบหนึ่งที่มีโอกาสเท่ากัน ค่าเป็นพันบาทต่อผู้ป่วย และเป็นตัวเลขสมมติ
nmb_a <- c(100, 140, 90, 130)
nmb_b <- c(120, 110, 150, 100)
worlds <- tibble::tibble(world = seq_along(nmb_a), drug_a = nmb_a, drug_b = nmb_b) |>
dplyr::mutate(best = pmax(drug_a, drug_b), loss_with_b = best - drug_b)
averages <- c(A = mean(nmb_a), B = mean(nmb_b))
choose_now <- max(averages)
row_best <- pmax(nmb_a, nmb_b)
know_first <- mean(row_best)
stopifnot(know_first - choose_now == mean(worlds$loss_with_b))
#%%@note 3. What choosing B loses, then two checks
# Every A value lower by 10 keeps the same losing worlds. Sorting each column breaks the worlds.
# ลด A ลง 10 ทุกแถวยังแพ้ในโลกเดิม ส่วนการเรียงแต่ละคอลัมน์แยกกันทำให้โลกแตกออกจากกัน
shift_a <- 10
loss_b <- pmax(nmb_a, nmb_b) - nmb_b
lower_a <- nmb_a - shift_a
loss_lower <- pmax(lower_a, nmb_b) - nmb_b
sorted_best <- pmax(sort(nmb_a), sort(nmb_b))
stopifnot(sum(loss_lower > 0) == sum(loss_b > 0), mean(sorted_best) < know_first)
#%%@note 4. Cross-check with an independent routine
# dampack values each strategy as effect x wtp - cost, so effect = value, cost = 0 and wtp = 1.
# dampack คิดค่าเป็น effect x wtp - cost จึงให้ effect เป็นค่า ให้ cost เป็น 0 และ wtp เป็น 1
zero_cost <- data.frame(A = 0 * nmb_a, B = 0 * nmb_b)
psa_obj <- dampack::make_psa_obj(cost = zero_cost, effectiveness = data.frame(A = nmb_a, B = nmb_b),
parameters = data.frame(world = seq_along(nmb_a)), strategies = c("A", "B"))
checked <- dampack::calc_evpi(psa_obj, wtp = 1)
stopifnot(abs(checked[[2]] - (know_first - choose_now)) < 1e-9)
#%%@note 5. From one patient to the network, in today's money
# Patients reached over the years the choice stands, then each year divided by 1.03 once more.
# จำนวนผู้ป่วยตลอดช่วงที่การเลือกนี้ใช้อยู่ แล้วหารค่าของแต่ละปีด้วย 1.03 เพิ่มอีกหนึ่งครั้ง
per_patient <- 15000
per_year <- 2000
years_used <- 5
reached <- per_year * years_used
yearly_factor <- 1.03
trial_cost <- 40
dividers <- yearly_factor^(seq_len(years_used) - 1)
today <- per_year * per_patient / 1e6 / dividers
stopifnot(per_patient == 1000 * (know_first - choose_now), trial_cost < sum(today))
#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("10 years at 0.70 =", formatC(years_lived * state_weight, format = "f", digits = 1),
"| full health adds", formatC(years_lived * 0.30, format = "f", digits = 1), "\n")
cat("Series example: 160000 x 1.00 - 150000 =", big(lambda * 1.00 - 150000), "THB\n")
cat("Average extra gain", big(mean_gain), "THB, spread", big(sd_gain), "THB | runs favouring it:",
round(runs * stats::pnorm(mean_gain / sd_gain)), "of", big(runs), "\n")
cat("Average value, A and B:", averages, "| choose now: drug", names(which.max(averages)), "\n")
cat("Better drug in each world:", ifelse(nmb_a > nmb_b, "A", "B"), "\n")
cat("Best value in each world:", row_best, "| their average:", know_first, "\n")
cat("Knowing first:", know_first, "- choosing now:", choose_now, "=", know_first - choose_now, "\n")
cat("Loss from choosing B in each world:", loss_b, "| average:", mean(loss_b), "\n")
cat("Worlds where B loses:", sum(loss_b > 0), "of", length(loss_b),
"| share (%):", 100 * mean(loss_b > 0), "\n")
cat("A lower by 10: average", mean(lower_a), "| losses", loss_lower, "| average:",
mean(loss_lower), "\n")
cat("Sorted A:", sort(nmb_a), "| sorted B:", sort(nmb_b), "| best in each row:", sorted_best, "\n")
cat("Their average:", mean(sorted_best), "| gap over choosing now:",
mean(sorted_best) - max(mean(nmb_a), mean(nmb_b)), "\n")
cat("Patients reached:", big(per_year), "x", years_used, "=", big(reached), "\n")
cat("Each year:", big(per_year), "x", big(per_patient), "THB =", big(per_year * per_patient / 1e6),
"million THB\n")
cat("Ceiling:", big(reached), "x", big(per_patient), "THB =", big(reached * per_patient / 1e6),
"million THB\n")
cat("Series: 1,834 THB due in 3 years at (%):", 3, "->", round(1834 / yearly_factor^3), "THB\n")
cat("Dividers, year by year:", formatC(dividers, format = "f", digits = 4), "\n")
cat("Each year in today's money (million THB):", formatC(today, format = "f", digits = 2), "\n")
cat("Ceiling today (million THB):", formatC(sum(today), format = "f", digits = 2), "\n")
cat("Trial (million THB):", trial_cost, "| below the ceiling:", trial_cost < sum(today), "\n")
เก็บค่าของโลกเดียวกันไว้ด้วยกัน
การคำนวณนี้ใช้ได้เฉพาะเมื่อ A และ B ในแถวเดียวกันมาจากโลกเดียวกัน สมมติว่ามีคนเรียงแต่ละคอลัมน์จากน้อยไปมากแยกกัน A จะเป็น 90, 100, 130, 140 และ B จะเป็น 100, 110, 120, 150 พันบาท แถวจะจับ 90 คู่กับ 100, 100 คู่กับ 110, 130 คู่กับ 120 และ 140 คู่กับ 150 ซึ่งเป็นคู่ที่ไม่เคยเกิดขึ้นพร้อมกันในโลกใดเลย
ค่าที่ดีที่สุดของแต่ละแถวคือ 100, 110, 130 และ 150 พันบาท เฉลี่ยได้ 122.5 ช่องว่างเหนือการเลือกตอนนี้จะหดเหลือ 2.5 ซึ่งเป็นคำตอบที่ผิด ในแบบจำลองเต็มรูปแบบ ให้เก็บผลของยาทั้งสองตัวจากการรันครั้งเดียวกันไว้คู่กัน Briggs และคณะอธิบายว่าทำไมข้อมูลนำเข้าที่ทางเลือกใช้ร่วมกันต้องผูกกันไว้
ตรวจผลของการเรียงด้วย R
# The same arithmetic after sorting each column on its own, which breaks the worlds apart.
nmb_a <- c(100, 140, 90, 130)
nmb_b <- c(120, 110, 150, 100)
sorted_best <- pmax(sort(nmb_a), sort(nmb_b))
cat("Sorted A:", sort(nmb_a), "| sorted B:", sort(nmb_b), "| best in each row:", sorted_best, "\n")
cat("Their average:", mean(sorted_best), "| gap over choosing now:",
mean(sorted_best) - max(mean(nmb_a), mean(nmb_b)), "\n")
Sorted A: 90 100 130 140 | sorted B: 100 110 120 150 | best in each row: 100 110 130 150
Their average: 122.5 | gap over choosing now: 2.5
ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · dampack 1.0.2.1000 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด expected-value-of-perfect-information.R
สคริปต์ฉบับเต็ม
# Two drugs, four possible worlds: what knowing the truth first is worth to a hospital network
# ยาสองตัว ความจริงที่เป็นไปได้สี่แบบ: การรู้ความจริงก่อนเลือกมีค่าเท่าใดต่อเครือข่ายโรงพยาบาล
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# dampack 1.0.2.1000 (stable)
# install.packages(c("tibble", "dplyr", "dampack"))
# update.packages(oldPkgs = c("tibble", "dplyr", "dampack"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("dampack", "1.0.2.1000")
library(tibble) # Write the four worlds as an explicit input table.
library(dplyr) # Add each world's best value and loss to the table.
library(dampack) # Cross-check the gain from knowing first with an independent routine.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The series' shared examples
# A health weight over ten years, the threshold, and the share of reruns that favour a treatment.
# ค่าน้ำหนักสุขภาพตลอดสิบปี เกณฑ์ และสัดส่วนของการรันซ้ำที่เอียงไปทางการรักษา
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
years_lived <- 10
state_weight <- 0.70
lambda <- 160000
mean_gain <- 10000
sd_gain <- 40000
runs <- 1000
stopifnot(round(runs * stats::pnorm(mean_gain / sd_gain)) == 599)
#%%@note 2. Four worlds, both drugs valued in the same world
# Each row is one equally likely version of the truth. Values are thousand THB per patient.
# แต่ละแถวคือความจริงแบบหนึ่งที่มีโอกาสเท่ากัน ค่าเป็นพันบาทต่อผู้ป่วย และเป็นตัวเลขสมมติ
nmb_a <- c(100, 140, 90, 130)
nmb_b <- c(120, 110, 150, 100)
worlds <- tibble::tibble(world = seq_along(nmb_a), drug_a = nmb_a, drug_b = nmb_b) |>
dplyr::mutate(best = pmax(drug_a, drug_b), loss_with_b = best - drug_b)
averages <- c(A = mean(nmb_a), B = mean(nmb_b))
choose_now <- max(averages)
row_best <- pmax(nmb_a, nmb_b)
know_first <- mean(row_best)
stopifnot(know_first - choose_now == mean(worlds$loss_with_b))
#%%@note 3. What choosing B loses, then two checks
# Every A value lower by 10 keeps the same losing worlds. Sorting each column breaks the worlds.
# ลด A ลง 10 ทุกแถวยังแพ้ในโลกเดิม ส่วนการเรียงแต่ละคอลัมน์แยกกันทำให้โลกแตกออกจากกัน
shift_a <- 10
loss_b <- pmax(nmb_a, nmb_b) - nmb_b
lower_a <- nmb_a - shift_a
loss_lower <- pmax(lower_a, nmb_b) - nmb_b
sorted_best <- pmax(sort(nmb_a), sort(nmb_b))
stopifnot(sum(loss_lower > 0) == sum(loss_b > 0), mean(sorted_best) < know_first)
#%%@note 4. Cross-check with an independent routine
# dampack values each strategy as effect x wtp - cost, so effect = value, cost = 0 and wtp = 1.
# dampack คิดค่าเป็น effect x wtp - cost จึงให้ effect เป็นค่า ให้ cost เป็น 0 และ wtp เป็น 1
zero_cost <- data.frame(A = 0 * nmb_a, B = 0 * nmb_b)
psa_obj <- dampack::make_psa_obj(cost = zero_cost, effectiveness = data.frame(A = nmb_a, B = nmb_b),
parameters = data.frame(world = seq_along(nmb_a)), strategies = c("A", "B"))
checked <- dampack::calc_evpi(psa_obj, wtp = 1)
stopifnot(abs(checked[[2]] - (know_first - choose_now)) < 1e-9)
#%%@note 5. From one patient to the network, in today's money
# Patients reached over the years the choice stands, then each year divided by 1.03 once more.
# จำนวนผู้ป่วยตลอดช่วงที่การเลือกนี้ใช้อยู่ แล้วหารค่าของแต่ละปีด้วย 1.03 เพิ่มอีกหนึ่งครั้ง
per_patient <- 15000
per_year <- 2000
years_used <- 5
reached <- per_year * years_used
yearly_factor <- 1.03
trial_cost <- 40
dividers <- yearly_factor^(seq_len(years_used) - 1)
today <- per_year * per_patient / 1e6 / dividers
stopifnot(per_patient == 1000 * (know_first - choose_now), trial_cost < sum(today))
#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("10 years at 0.70 =", formatC(years_lived * state_weight, format = "f", digits = 1),
"| full health adds", formatC(years_lived * 0.30, format = "f", digits = 1), "\n")
cat("Series example: 160000 x 1.00 - 150000 =", big(lambda * 1.00 - 150000), "THB\n")
cat("Average extra gain", big(mean_gain), "THB, spread", big(sd_gain), "THB | runs favouring it:",
round(runs * stats::pnorm(mean_gain / sd_gain)), "of", big(runs), "\n")
cat("Average value, A and B:", averages, "| choose now: drug", names(which.max(averages)), "\n")
cat("Better drug in each world:", ifelse(nmb_a > nmb_b, "A", "B"), "\n")
cat("Best value in each world:", row_best, "| their average:", know_first, "\n")
cat("Knowing first:", know_first, "- choosing now:", choose_now, "=", know_first - choose_now, "\n")
cat("Loss from choosing B in each world:", loss_b, "| average:", mean(loss_b), "\n")
cat("Worlds where B loses:", sum(loss_b > 0), "of", length(loss_b),
"| share (%):", 100 * mean(loss_b > 0), "\n")
cat("A lower by 10: average", mean(lower_a), "| losses", loss_lower, "| average:",
mean(loss_lower), "\n")
cat("Sorted A:", sort(nmb_a), "| sorted B:", sort(nmb_b), "| best in each row:", sorted_best, "\n")
cat("Their average:", mean(sorted_best), "| gap over choosing now:",
mean(sorted_best) - max(mean(nmb_a), mean(nmb_b)), "\n")
cat("Patients reached:", big(per_year), "x", years_used, "=", big(reached), "\n")
cat("Each year:", big(per_year), "x", big(per_patient), "THB =", big(per_year * per_patient / 1e6),
"million THB\n")
cat("Ceiling:", big(reached), "x", big(per_patient), "THB =", big(reached * per_patient / 1e6),
"million THB\n")
cat("Series: 1,834 THB due in 3 years at (%):", 3, "->", round(1834 / yearly_factor^3), "THB\n")
cat("Dividers, year by year:", formatC(dividers, format = "f", digits = 4), "\n")
cat("Each year in today's money (million THB):", formatC(today, format = "f", digits = 2), "\n")
cat("Ceiling today (million THB):", formatC(sum(today), format = "f", digits = 2), "\n")
cat("Trial (million THB):", trial_cost, "| below the ceiling:", trial_cost < sum(today), "\n")
จากผู้ป่วยหนึ่งคนสู่ทั้งเครือข่าย
ค่า 15 พันบาท (15,000 บาท) เป็นค่าต่อผู้ป่วยหนึ่งคน แต่การทดลองจะช่วยการเลือกสำหรับผู้ป่วยทุกคนที่เริ่มรักษาในช่วงที่การเลือกนี้ยังใช้อยู่ เครือข่ายเริ่มรักษาผู้ป่วยปีละ 2,000 คน และคณะกรรมการคาดว่าจะใช้การเลือกนี้ 5 ปี รวมเป็น 2,000 × 5 = 10,000 คน ความรู้ที่สมบูรณ์จึงมีค่าได้ไม่เกิน 10,000 × 15,000 บาท = 150 ล้านบาท
เงินที่มาถึงในปีหลัง ๆ มีค่าน้อยลงเมื่อคิดเป็นเงินวันนี้ การปรับลดค่าตามเวลา (discounting) คือการแปลงต้นทุนหรือผลทางสุขภาพในอนาคตเป็นมูลค่าวันนี้ โดยลดลงตามอัตราการปรับลดในแต่ละปีที่ห่างออกไป ที่อัตรา 3% ต้นทุน 1,834 บาทที่ต้องจ่ายในอีก 3 ปีข้างหน้ามีค่าเท่ากับ 1,678 บาทในวันนี้ อ่านบทเรียนเต็มได้ที่บทความพื้นฐานเรื่องต้นทุนข้ามเวลา
ในกรณีนี้ ผู้ป่วยของแต่ละปีมีค่า 2,000 × 15,000 บาท = 30 ล้านบาท ผู้ป่วยของปีแรกเริ่มรักษาวันนี้ ปีแรกจึงนับเต็ม และปีที่สามของเครือข่ายห่างจากวันนี้เพียงสองปี ตัวอย่างคำนวณด้านล่างคิดค่าของแต่ละปีเป็นเงินวันนี้ที่อัตราเดียวกันคือ 3% ต่อปี
ผู้ป่วยห้าปีคิดเป็นเงินวันนี้
-
ตัวหาร
\[ 1.03 \times 1.03 = 1.0609 \]
ปีแรกนับเต็มเพราะเริ่มวันนี้ ปีที่สองหารด้วย 1.03 หนึ่งครั้ง (1.0300) ปีที่สามหารสองครั้ง (1.03 × 1.03 = 1.0609) และปีถัด ๆ ไปหารเพิ่มอีกครั้งละ 1.03 คือ 1.0609 × 1.03 = 1.0927 และ 1.0927 × 1.03 = 1.1255
-
ค่าของแต่ละปีเป็นเงินวันนี้
\[ \frac{30}{1.0609} = 28.28 \]
นำ 30 ล้านบาทหารด้วยตัวหารแต่ละตัว ได้ 30.00, 29.13, 28.28, 27.45 และ 26.65 ล้านบาท เช่น ปีที่สามคือ 30 / 1.0609 = 28.28
-
รวมทุกปี
\[ 30.00 + 29.13 + 28.28 + 27.45 + 26.65 = 141.51 \]
ห้าปีรวมกันได้ 141.51 ล้านบาท ต่ำกว่า 150 ล้านบาทที่นับโดยไม่ปรับตามเวลา
ผลลัพธ์: เมื่อคิดเป็นเงินวันนี้ การขจัดความไม่แน่นอนทั้งหมดมีค่าต่อเครือข่ายไม่เกิน 141.51 ล้านบาท
เพดานของงานวิจัย
ผลรวมนี้เรียกว่า EVPI ระดับประชากร (population EVPI) คือ EVPI ต่อผู้ป่วยที่คูณด้วยจำนวนผู้ป่วยทุกคนที่การเลือกนี้ส่งผลถึง และคิดเป็นเงินวันนี้ ค่านี้เป็นเพดาน คือค่าสูงสุดที่งานวิจัยใด ๆ เรื่องนี้จะมีต่อเครือข่ายได้
การทดลองมีต้นทุน 40 ล้านบาท ต่ำกว่าเพดาน 141.51 ล้านบาทอยู่มาก จึงยังไม่ถูกตัดทิ้ง ส่วนงานวิจัยที่มีต้นทุนสูงกว่าเพดานจะถูกตัดทิ้งไม่ว่าจะได้ผลอย่างไร เพราะแม้ความรู้ที่สมบูรณ์ก็ยังคืนทุนไม่ได้
นับผู้ป่วยด้วย R
# From one patient to the network: the patients the choice reaches, and the most knowing is worth.
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
per_patient <- 15000
per_year <- 2000
years_used <- 5
reached <- per_year * years_used
cat("Patients reached:", big(per_year), "x", years_used, "=", big(reached), "\n")
cat("Each year:", big(per_year), "x", big(per_patient), "THB =", big(per_year * per_patient / 1e6),
"million THB\n")
cat("Ceiling:", big(reached), "x", big(per_patient), "THB =", big(reached * per_patient / 1e6),
"million THB\n")
Patients reached: 2,000 x 5 = 10,000
Each year: 2,000 x 15,000 THB = 30 million THB
Ceiling: 10,000 x 15,000 THB = 150 million THB
ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · dampack 1.0.2.1000 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด expected-value-of-perfect-information.R
สคริปต์ฉบับเต็ม
# Two drugs, four possible worlds: what knowing the truth first is worth to a hospital network
# ยาสองตัว ความจริงที่เป็นไปได้สี่แบบ: การรู้ความจริงก่อนเลือกมีค่าเท่าใดต่อเครือข่ายโรงพยาบาล
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# dampack 1.0.2.1000 (stable)
# install.packages(c("tibble", "dplyr", "dampack"))
# update.packages(oldPkgs = c("tibble", "dplyr", "dampack"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("dampack", "1.0.2.1000")
library(tibble) # Write the four worlds as an explicit input table.
library(dplyr) # Add each world's best value and loss to the table.
library(dampack) # Cross-check the gain from knowing first with an independent routine.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The series' shared examples
# A health weight over ten years, the threshold, and the share of reruns that favour a treatment.
# ค่าน้ำหนักสุขภาพตลอดสิบปี เกณฑ์ และสัดส่วนของการรันซ้ำที่เอียงไปทางการรักษา
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
years_lived <- 10
state_weight <- 0.70
lambda <- 160000
mean_gain <- 10000
sd_gain <- 40000
runs <- 1000
stopifnot(round(runs * stats::pnorm(mean_gain / sd_gain)) == 599)
#%%@note 2. Four worlds, both drugs valued in the same world
# Each row is one equally likely version of the truth. Values are thousand THB per patient.
# แต่ละแถวคือความจริงแบบหนึ่งที่มีโอกาสเท่ากัน ค่าเป็นพันบาทต่อผู้ป่วย และเป็นตัวเลขสมมติ
nmb_a <- c(100, 140, 90, 130)
nmb_b <- c(120, 110, 150, 100)
worlds <- tibble::tibble(world = seq_along(nmb_a), drug_a = nmb_a, drug_b = nmb_b) |>
dplyr::mutate(best = pmax(drug_a, drug_b), loss_with_b = best - drug_b)
averages <- c(A = mean(nmb_a), B = mean(nmb_b))
choose_now <- max(averages)
row_best <- pmax(nmb_a, nmb_b)
know_first <- mean(row_best)
stopifnot(know_first - choose_now == mean(worlds$loss_with_b))
#%%@note 3. What choosing B loses, then two checks
# Every A value lower by 10 keeps the same losing worlds. Sorting each column breaks the worlds.
# ลด A ลง 10 ทุกแถวยังแพ้ในโลกเดิม ส่วนการเรียงแต่ละคอลัมน์แยกกันทำให้โลกแตกออกจากกัน
shift_a <- 10
loss_b <- pmax(nmb_a, nmb_b) - nmb_b
lower_a <- nmb_a - shift_a
loss_lower <- pmax(lower_a, nmb_b) - nmb_b
sorted_best <- pmax(sort(nmb_a), sort(nmb_b))
stopifnot(sum(loss_lower > 0) == sum(loss_b > 0), mean(sorted_best) < know_first)
#%%@note 4. Cross-check with an independent routine
# dampack values each strategy as effect x wtp - cost, so effect = value, cost = 0 and wtp = 1.
# dampack คิดค่าเป็น effect x wtp - cost จึงให้ effect เป็นค่า ให้ cost เป็น 0 และ wtp เป็น 1
zero_cost <- data.frame(A = 0 * nmb_a, B = 0 * nmb_b)
psa_obj <- dampack::make_psa_obj(cost = zero_cost, effectiveness = data.frame(A = nmb_a, B = nmb_b),
parameters = data.frame(world = seq_along(nmb_a)), strategies = c("A", "B"))
checked <- dampack::calc_evpi(psa_obj, wtp = 1)
stopifnot(abs(checked[[2]] - (know_first - choose_now)) < 1e-9)
#%%@note 5. From one patient to the network, in today's money
# Patients reached over the years the choice stands, then each year divided by 1.03 once more.
# จำนวนผู้ป่วยตลอดช่วงที่การเลือกนี้ใช้อยู่ แล้วหารค่าของแต่ละปีด้วย 1.03 เพิ่มอีกหนึ่งครั้ง
per_patient <- 15000
per_year <- 2000
years_used <- 5
reached <- per_year * years_used
yearly_factor <- 1.03
trial_cost <- 40
dividers <- yearly_factor^(seq_len(years_used) - 1)
today <- per_year * per_patient / 1e6 / dividers
stopifnot(per_patient == 1000 * (know_first - choose_now), trial_cost < sum(today))
#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("10 years at 0.70 =", formatC(years_lived * state_weight, format = "f", digits = 1),
"| full health adds", formatC(years_lived * 0.30, format = "f", digits = 1), "\n")
cat("Series example: 160000 x 1.00 - 150000 =", big(lambda * 1.00 - 150000), "THB\n")
cat("Average extra gain", big(mean_gain), "THB, spread", big(sd_gain), "THB | runs favouring it:",
round(runs * stats::pnorm(mean_gain / sd_gain)), "of", big(runs), "\n")
cat("Average value, A and B:", averages, "| choose now: drug", names(which.max(averages)), "\n")
cat("Better drug in each world:", ifelse(nmb_a > nmb_b, "A", "B"), "\n")
cat("Best value in each world:", row_best, "| their average:", know_first, "\n")
cat("Knowing first:", know_first, "- choosing now:", choose_now, "=", know_first - choose_now, "\n")
cat("Loss from choosing B in each world:", loss_b, "| average:", mean(loss_b), "\n")
cat("Worlds where B loses:", sum(loss_b > 0), "of", length(loss_b),
"| share (%):", 100 * mean(loss_b > 0), "\n")
cat("A lower by 10: average", mean(lower_a), "| losses", loss_lower, "| average:",
mean(loss_lower), "\n")
cat("Sorted A:", sort(nmb_a), "| sorted B:", sort(nmb_b), "| best in each row:", sorted_best, "\n")
cat("Their average:", mean(sorted_best), "| gap over choosing now:",
mean(sorted_best) - max(mean(nmb_a), mean(nmb_b)), "\n")
cat("Patients reached:", big(per_year), "x", years_used, "=", big(reached), "\n")
cat("Each year:", big(per_year), "x", big(per_patient), "THB =", big(per_year * per_patient / 1e6),
"million THB\n")
cat("Ceiling:", big(reached), "x", big(per_patient), "THB =", big(reached * per_patient / 1e6),
"million THB\n")
cat("Series: 1,834 THB due in 3 years at (%):", 3, "->", round(1834 / yearly_factor^3), "THB\n")
cat("Dividers, year by year:", formatC(dividers, format = "f", digits = 4), "\n")
cat("Each year in today's money (million THB):", formatC(today, format = "f", digits = 2), "\n")
cat("Ceiling today (million THB):", formatC(sum(today), format = "f", digits = 2), "\n")
cat("Trial (million THB):", trial_cost, "| below the ceiling:", trial_cost < sum(today), "\n")
คิดแต่ละปีเป็นเงินวันนี้ด้วย R
# Count later years in today's money at the series' rate, then hold the trial's cost against it.
yearly_factor <- 1.03
per_patient <- 15000
per_year <- 2000
years_used <- 5
trial_cost <- 40
dividers <- yearly_factor^(seq_len(years_used) - 1)
today <- per_year * per_patient / 1e6 / dividers
cat("Series: 1,834 THB due in 3 years at (%):", 3, "->", round(1834 / yearly_factor^3), "THB\n")
cat("Dividers, year by year:", formatC(dividers, format = "f", digits = 4), "\n")
cat("Each year in today's money (million THB):", formatC(today, format = "f", digits = 2), "\n")
cat("Ceiling today (million THB):", formatC(sum(today), format = "f", digits = 2), "\n")
cat("Trial (million THB):", trial_cost, "| below the ceiling:", trial_cost < sum(today), "\n")
Series: 1,834 THB due in 3 years at (%): 3 -> 1678 THB
Dividers, year by year: 1.0000 1.0300 1.0609 1.0927 1.1255
Each year in today's money (million THB): 30.00 29.13 28.28 27.45 26.65
Ceiling today (million THB): 141.51
Trial (million THB): 40 | below the ceiling: TRUE
ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · dampack 1.0.2.1000 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด expected-value-of-perfect-information.R
สคริปต์ฉบับเต็ม
# Two drugs, four possible worlds: what knowing the truth first is worth to a hospital network
# ยาสองตัว ความจริงที่เป็นไปได้สี่แบบ: การรู้ความจริงก่อนเลือกมีค่าเท่าใดต่อเครือข่ายโรงพยาบาล
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# dampack 1.0.2.1000 (stable)
# install.packages(c("tibble", "dplyr", "dampack"))
# update.packages(oldPkgs = c("tibble", "dplyr", "dampack"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("dampack", "1.0.2.1000")
library(tibble) # Write the four worlds as an explicit input table.
library(dplyr) # Add each world's best value and loss to the table.
library(dampack) # Cross-check the gain from knowing first with an independent routine.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The series' shared examples
# A health weight over ten years, the threshold, and the share of reruns that favour a treatment.
# ค่าน้ำหนักสุขภาพตลอดสิบปี เกณฑ์ และสัดส่วนของการรันซ้ำที่เอียงไปทางการรักษา
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
years_lived <- 10
state_weight <- 0.70
lambda <- 160000
mean_gain <- 10000
sd_gain <- 40000
runs <- 1000
stopifnot(round(runs * stats::pnorm(mean_gain / sd_gain)) == 599)
#%%@note 2. Four worlds, both drugs valued in the same world
# Each row is one equally likely version of the truth. Values are thousand THB per patient.
# แต่ละแถวคือความจริงแบบหนึ่งที่มีโอกาสเท่ากัน ค่าเป็นพันบาทต่อผู้ป่วย และเป็นตัวเลขสมมติ
nmb_a <- c(100, 140, 90, 130)
nmb_b <- c(120, 110, 150, 100)
worlds <- tibble::tibble(world = seq_along(nmb_a), drug_a = nmb_a, drug_b = nmb_b) |>
dplyr::mutate(best = pmax(drug_a, drug_b), loss_with_b = best - drug_b)
averages <- c(A = mean(nmb_a), B = mean(nmb_b))
choose_now <- max(averages)
row_best <- pmax(nmb_a, nmb_b)
know_first <- mean(row_best)
stopifnot(know_first - choose_now == mean(worlds$loss_with_b))
#%%@note 3. What choosing B loses, then two checks
# Every A value lower by 10 keeps the same losing worlds. Sorting each column breaks the worlds.
# ลด A ลง 10 ทุกแถวยังแพ้ในโลกเดิม ส่วนการเรียงแต่ละคอลัมน์แยกกันทำให้โลกแตกออกจากกัน
shift_a <- 10
loss_b <- pmax(nmb_a, nmb_b) - nmb_b
lower_a <- nmb_a - shift_a
loss_lower <- pmax(lower_a, nmb_b) - nmb_b
sorted_best <- pmax(sort(nmb_a), sort(nmb_b))
stopifnot(sum(loss_lower > 0) == sum(loss_b > 0), mean(sorted_best) < know_first)
#%%@note 4. Cross-check with an independent routine
# dampack values each strategy as effect x wtp - cost, so effect = value, cost = 0 and wtp = 1.
# dampack คิดค่าเป็น effect x wtp - cost จึงให้ effect เป็นค่า ให้ cost เป็น 0 และ wtp เป็น 1
zero_cost <- data.frame(A = 0 * nmb_a, B = 0 * nmb_b)
psa_obj <- dampack::make_psa_obj(cost = zero_cost, effectiveness = data.frame(A = nmb_a, B = nmb_b),
parameters = data.frame(world = seq_along(nmb_a)), strategies = c("A", "B"))
checked <- dampack::calc_evpi(psa_obj, wtp = 1)
stopifnot(abs(checked[[2]] - (know_first - choose_now)) < 1e-9)
#%%@note 5. From one patient to the network, in today's money
# Patients reached over the years the choice stands, then each year divided by 1.03 once more.
# จำนวนผู้ป่วยตลอดช่วงที่การเลือกนี้ใช้อยู่ แล้วหารค่าของแต่ละปีด้วย 1.03 เพิ่มอีกหนึ่งครั้ง
per_patient <- 15000
per_year <- 2000
years_used <- 5
reached <- per_year * years_used
yearly_factor <- 1.03
trial_cost <- 40
dividers <- yearly_factor^(seq_len(years_used) - 1)
today <- per_year * per_patient / 1e6 / dividers
stopifnot(per_patient == 1000 * (know_first - choose_now), trial_cost < sum(today))
#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("10 years at 0.70 =", formatC(years_lived * state_weight, format = "f", digits = 1),
"| full health adds", formatC(years_lived * 0.30, format = "f", digits = 1), "\n")
cat("Series example: 160000 x 1.00 - 150000 =", big(lambda * 1.00 - 150000), "THB\n")
cat("Average extra gain", big(mean_gain), "THB, spread", big(sd_gain), "THB | runs favouring it:",
round(runs * stats::pnorm(mean_gain / sd_gain)), "of", big(runs), "\n")
cat("Average value, A and B:", averages, "| choose now: drug", names(which.max(averages)), "\n")
cat("Better drug in each world:", ifelse(nmb_a > nmb_b, "A", "B"), "\n")
cat("Best value in each world:", row_best, "| their average:", know_first, "\n")
cat("Knowing first:", know_first, "- choosing now:", choose_now, "=", know_first - choose_now, "\n")
cat("Loss from choosing B in each world:", loss_b, "| average:", mean(loss_b), "\n")
cat("Worlds where B loses:", sum(loss_b > 0), "of", length(loss_b),
"| share (%):", 100 * mean(loss_b > 0), "\n")
cat("A lower by 10: average", mean(lower_a), "| losses", loss_lower, "| average:",
mean(loss_lower), "\n")
cat("Sorted A:", sort(nmb_a), "| sorted B:", sort(nmb_b), "| best in each row:", sorted_best, "\n")
cat("Their average:", mean(sorted_best), "| gap over choosing now:",
mean(sorted_best) - max(mean(nmb_a), mean(nmb_b)), "\n")
cat("Patients reached:", big(per_year), "x", years_used, "=", big(reached), "\n")
cat("Each year:", big(per_year), "x", big(per_patient), "THB =", big(per_year * per_patient / 1e6),
"million THB\n")
cat("Ceiling:", big(reached), "x", big(per_patient), "THB =", big(reached * per_patient / 1e6),
"million THB\n")
cat("Series: 1,834 THB due in 3 years at (%):", 3, "->", round(1834 / yearly_factor^3), "THB\n")
cat("Dividers, year by year:", formatC(dividers, format = "f", digits = 4), "\n")
cat("Each year in today's money (million THB):", formatC(today, format = "f", digits = 2), "\n")
cat("Ceiling today (million THB):", formatC(sum(today), format = "f", digits = 2), "\n")
cat("Trial (million THB):", trial_cost, "| below the ceiling:", trial_cost < sum(today), "\n")
ประโยชน์ของการทดลองจริงต่ำกว่าเพดาน
EVPI สมมติว่างานวิจัยจะเปิดเผยโลกที่เป็นความจริงได้ตรงทุกค่า การทดลองจริงที่มีขนาดจำกัดลดความไม่แน่นอนลงได้แต่ยังเหลืออยู่บ้าง คณะกรรมการจึงอาจยังเลือกผิดได้หลังได้ผลการทดลอง มูลค่าคาดหมายของข้อมูลจากตัวอย่าง (expected value of sample information, EVSI) คือผลได้เฉลี่ยจากการตัดสินใจตามผลที่เป็นไปได้ของงานวิจัยจริง สำหรับคำถามเดียวกัน ค่านี้ไม่มีทางสูงกว่า EVPI
การทดลองนี้จึงคุ้มที่จะทำเฉพาะเมื่อ EVSI ที่นับรวมผู้ป่วยกลุ่มเดียวกันสูงกว่าต้นทุน 40 ล้านบาท นอกจากนี้ผลยังมาถึงช้า ผู้ป่วยที่เริ่มรักษาระหว่างรอผลการทดลองจึงไม่ได้ประโยชน์จากผลนั้น และถ้าเครือข่ายเปลี่ยนยาไม่ได้อีกเมื่อเลือกไปแล้ว ผลที่มาถึงช้าก็จะนำไปใช้ไม่ได้เลย
การคำนวณแบบเดียวกันทำทีละข้อมูลนำเข้าได้ เช่น เฉพาะผลของ B ต่อการนอนโรงพยาบาล ซึ่งเรียกว่า EVPI บางส่วน (partial EVPI) และบอกว่าความไม่แน่นอนส่วนใดคุ้มที่จะศึกษา Jalal และคณะอธิบายวิธีทำการวิเคราะห์แบบนี้ใน R
สิ่งที่ตัวเลขนี้ไม่ได้นับ
EVPI นับเฉพาะความไม่แน่นอนที่อยู่ในแบบจำลอง ถ้าแบบจำลองไม่ได้รวมประเด็นที่ยาสองตัวอาจแตกต่างกัน ความไม่แน่นอนนั้นจะไม่ถูกนับ และ EVPI ไม่ได้บอกว่าเครือข่ายมีเงินพอจ่ายยา B หรือไม่ ซึ่งเป็นคำถามด้านงบประมาณที่มีบทความของตัวเองในชุดนี้
คำศัพท์ตามที่ใช้ในบทความนี้
- Possible world
- โลกที่เป็นไปได้: ความจริงแบบหนึ่งที่กำหนดค่าข้อมูลนำเข้าที่ยังไม่รู้ทุกตัวพร้อมกัน และตีค่าทุกทางเลือกในโลกนั้น
- Expected value of perfect information (EVPI)
- มูลค่าคาดหมายของข้อมูลสมบูรณ์: ผลได้เฉลี่ยจากการรู้ค่าจริงของข้อมูลนำเข้าก่อนเลือก ในที่นี้คือ 135 - 120 = 15 พันบาทต่อผู้ป่วย
- Loss from the current choice
- ความสูญเสียจากการเลือกตอนนี้: สิ่งที่การเลือกตอนนี้เสียไปในโลกที่ทางเลือกอื่นดีกว่า ค่าเฉลี่ยของมันเท่ากับ EVPI
- Population EVPI
- EVPI ระดับประชากร: EVPI ต่อผู้ป่วยคูณด้วยจำนวนผู้ป่วยทุกคนที่การเลือกส่งผลถึง คิดเป็นเงินวันนี้ และเป็นเพดานของงานวิจัย
- Expected value of sample information (EVSI)
- มูลค่าคาดหมายของข้อมูลจากตัวอย่าง: ผลได้เฉลี่ยจากการตัดสินใจตามผลที่เป็นไปได้ของงานวิจัยจริง ไม่มีทางสูงกว่า EVPI
- Partial EVPI
- EVPI บางส่วน: การคำนวณแบบเดียวกันทีละข้อมูลนำเข้า ซึ่งชี้ว่าความไม่แน่นอนส่วนใดคุ้มที่จะศึกษา
เชื่อมกับบทความพื้นฐาน
บทความพื้นฐานเรื่องแบบจำลองแปลงหลักฐานเป็นการตัดสินใจอย่างไร แสดงว่าโลกที่เป็นไปได้ของแบบจำลองเต็มรูปแบบมาจากไหน ส่วนบทความเรื่องการสื่อสารหลักฐานเศรษฐศาสตร์สุขภาพ แสดงวิธีนำตัวเลขอย่างเพดานนี้ไปเสนอต่อคณะกรรมการ
ลองใช้ในแอป
เปิดแอปแบบจำลองของเราเพื่อลองเปลี่ยนค่าข้อมูลนำเข้าและดูผลลัพธ์เปลี่ยนตาม ต้องเข้าสู่ระบบและใช้แผน Pro หรือ Max
แอปเริ่มจากค่าตัวอย่างของแอปเอง ตัวเลขในแอปจึงไม่ตรงกับบทความนี้
เอกสารอ้างอิง
- Fenwick Elisabeth, Steuten Lotte, Knies Saskia, Ghabri Salah, Basu Anirban, Murray James F., Koffijberg Hendrik (Erik), Strong Mark, Sanders Schmidler Gillian D., Rothery Claire. Value of Information Analysis for Research Decisions-An Introduction: Report 1 of the ISPOR Value of Information Analysis Emerging Good Practices Task Force. Value in Health. 2020;23(2):139-150. doi:10.1016/j.jval.2020.01.001. https://api.crossref.org/works/10.1016/j.jval.2020.01.001
- Briggs Andrew H., Weinstein Milton C., Fenwick Elisabeth A.L., Karnon Jonathan, Sculpher Mark J., Paltiel A. David. Model Parameter Estimation and Uncertainty: A Report of the ISPOR-SMDM Modeling Good Research Practices Task Force-6. Value in Health. 2012;15(6):835-842. doi:10.1016/j.jval.2012.04.014. https://api.crossref.org/works/10.1016/j.jval.2012.04.014
- 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.json
- Jalal Hawre, Pechlivanoglou Petros, Krijkamp Eline, Alarid-Escudero Fernando, Enns Eva, Hunink M. G. Myriam. An Overview of R in Health Decision Sciences. Medical Decision Making. 2017;37(7):735-746. doi:10.1177/0272989x16686559. https://api.crossref.org/works/10.1177/0272989x16686559
ลำดับการเรียนในชุดนี้
- คู่มือชุดบทความการสร้างแบบจำลองเศรษฐศาสตร์สุขภาพ: จากหลักฐานสู่การตัดสินใจ
- การเลือกแบบจำลองเพื่อการตัดสินใจ: ให้เส้นทางทางคลินิกกำหนดโครงสร้าง
- ข้อมูลนำเข้าของแบบจำลองที่ตรวจสอบได้: สร้างตารางพารามิเตอร์ที่สืบย้อนถึงที่มา
- การค้นหาข้อมูลนำเข้าของแบบจำลองจากวรรณกรรม: เลือกค่าประมาณให้ตรงกับคำถาม
- อัตรา ความน่าจะเป็น และความน่าจะเป็นของการเปลี่ยนสถานะ: คำนวณโดยไม่หลงหน่วยเวลา
- สร้างแผนภูมิต้นไม้การตัดสินใจทีละขั้น: ตามรอยผลลัพธ์ ต้นทุน และสุขภาพ
- สร้างแบบจำลองมาร์คอฟแบบกลุ่มประชากรทีละขั้น: ติดตามประชากรผ่านเวลา
- การวิเคราะห์ผลกระทบด้านงบประมาณในทางปฏิบัติ: นับคนให้ชัดก่อนคำนวณเงิน
- ICER และผลประโยชน์สุทธิในรูปตัวเงินเมื่อมีหลายทางเลือก: หาขอบเขตทางเลือกที่มีประสิทธิภาพ
- การวิเคราะห์ความไวแบบอาศัยความน่าจะเป็น: จากการแจกแจงพารามิเตอร์สู่ CEAC
- การประเมินความสมเหตุสมผลและตรวจสอบแบบจำลอง: ทดสอบคำถาม สมการ และโค้ด
- มูลค่าคาดหมายของข้อมูลสมบูรณ์: หลักฐานที่ดีขึ้นอาจมีมูลค่าเพียงใด (บทความนี้)
ประเด็นสำคัญ
- หลักฐานที่ไม่แน่นอนคุ้มที่จะหามาเฉพาะเมื่ออาจเปลี่ยนการเลือกได้ การเลือก B ผิดในสองจากสี่โลก และความสูญเสียจากการเลือกผิดเหล่านั้นเท่านั้นที่ทำให้งานวิจัยมีมูลค่า
- เฉลี่ยก่อนแล้วเลือกได้ 120 และเลือกก่อนแล้วเฉลี่ยได้ 135 พันบาทต่อผู้ป่วย ช่องว่าง 15 พันบาทต่อผู้ป่วยคือ EVPI
- EVPI ยังเท่ากับความสูญเสียเฉลี่ยจากการเลือกตอนนี้ด้วย การสูญเสีย 30 ในสองจากสี่โลกเฉลี่ยได้ 15 ถ้าโอกาสเลือกผิดยังเป็น 50% แต่แต่ละครั้งสูญเสียเพียง 20 EVPI จะเหลือ 10
- เก็บค่าของทุกทางเลือกจากโลกเดียวกันไว้คู่กัน การเรียงคอลัมน์แยกกันให้ 2.5 ซึ่งเป็นคำตอบที่ผิด
- นับผู้ป่วยทุกคนที่การเลือกส่งผลถึงและคิดเป็นเงินวันนี้ 141.51 ล้านบาทคือเพดาน การทดลอง 40 ล้านบาทจึงยังไม่ถูกตัดทิ้ง แต่ประโยชน์ที่แท้จริงของการทดลองจริงต่ำกว่าเพดาน
อ่านต่อในวิกิ: ต้นไม้การตัดสินใจ แบบจำลองมาร์คอฟ และการวิเคราะห์ความไวเชิงความน่าจะเป็น: แบบจำลองแปลงหลักฐานเป็นการตัดสินใจได้อย่างไร · การสื่อสารหลักฐานเศรษฐศาสตร์สุขภาพ: จากผลลัพธ์แบบจำลองสู่ข้อเสนอแนะเชิงนโยบาย