ICER and Net Monetary Benefit Across Strategies: Find the Relevant Frontier

On this page
อ่านฉบับภาษาไทย (Thai version)
Abstract
A regional hospital's heart team must choose one follow-up plan for patients leaving hospital after a heart attack. Compared with usual clinic visits (A), monthly nurse calls (B) add 8,000 THB and 0.08 years of full health per patient. Home rehabilitation (C) adds 16,000 THB and 0.18 years, and hospital rehabilitation (D) 19,000 THB and 0.17 years. Against usual visits alone, every new plan costs less per year of full health than the 160,000 THB the fund will pay, so that shortcut cannot choose. The fix takes three steps. First, set D aside, because C costs less and gives more. Next, compare each plan with the one just below it in health: B to C buys health more cheaply than A to B, so a mix of A and C beats B. Last, A to C costs 88,888.89 THB per year of full health, below 160,000 THB. Valuing health at 160,000 THB and subtracting cost picks C too.
A heart team choosing one plan out of four
A regional hospital's heart team follows up every adult who leaves hospital after a heart attack. The team must settle on one follow-up plan as its standard, because the plans share the same nurses and clinic time. Plan A is usual clinic visits. Plan B adds monthly nurse phone calls, plan C a home-based cardiac rehabilitation programme, and plan D the same programme in the hospital gym.
To compare the plans, health needs one unit that counts both length and quality of life. A QALY, a quality-adjusted life year, is a year of life weighted by its quality, from 1 for full health to 0 for death. Over 10 years, a health state valued at 0.70 counts as 7.0 QALYs, so full health over those years would be worth 3.0 QALYs more. The full lesson is in the foundation article on quality-adjusted life years.
Every number below is per patient and counted as the extra over plan A, so plan A sits at 0 THB and 0 QALYs. Nurse calls (B) add 8,000 THB and 0.08 QALY. Home rehabilitation (C) adds 16,000 THB and 0.18 QALY. Hospital rehabilitation (D) adds 19,000 THB and 0.17 QALY, because staff and travel cost more and fewer patients finish it.
The fund that pays for the team's care will pay up to 160,000 THB for one QALY, the series' illustrative threshold. All numbers here are invented for teaching; they are not evidence about cardiac rehabilitation or a Thai payer's rule.
The four plans per patient, over plan A
| Plan | Extra cost (THB) | Extra QALYs |
|---|---|---|
| A: usual clinic visits | 0 | 0 |
| B: monthly nurse calls | 8,000 | 0.08 |
| C: home rehabilitation | 16,000 | 0.18 |
| D: hospital rehabilitation | 19,000 | 0.17 |
The shortcut: compare every plan with usual visits
For one new option and one comparator, the usual tool is a ratio. The incremental cost-effectiveness ratio (ICER) is the extra cost divided by the extra health gained against the next best option. In the series' shared example, an option that costs 150,000 THB more for 1.00 extra QALY has an ICER of 150,000 THB per QALY, below a threshold of 160,000 THB. The full lesson is in the foundation article on the ICER and the threshold.
The tempting shortcut divides each plan's extra cost by its extra QALYs over plan A. Plan B gives 8,000 / 0.08 = 100,000 THB per QALY, plan C gives 88,888.89 THB and plan D gives 111,764.71 THB. All three sit below 160,000 THB, so the shortcut says every plan is worth buying. It cannot say which one to choose, and it even passes plan D, which is never the right choice here.
This is the misconception the article corrects: a plan that passes against usual care is not, for that reason, a good choice. Each plan has to be compared with its nearest cheaper rival, after removing the plans that are beaten. Drummond and colleagues, and Briggs, Claxton and Sculpher, describe this ordered comparison in their textbooks.
Check the shortcut in R
# The heart team's four follow-up plans: extra cost and extra health per patient over plan A.
# Order A, B, C, D. Health is in quality-weighted years. All values are illustrative.
cost <- c(0, 8000, 16000, 19000)
effect <- c(0, 0.08, 0.18, 0.17)
lambda <- 160000
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
cat("10 years at 0.70 =", formatC(10 * 0.70, format = "f", digits = 1),
"| full health over those years adds", formatC(10 * 0.30, format = "f", digits = 1), "\n")
cat("Series example: 150000 / 1.00 =", big(150000 / 1.00), "THB | threshold:", big(lambda), "\n")
cat("Each plan against A, THB per unit of health (B, C, D):", big(cost[-1] / effect[-1]), "\n")
cat("Below the threshold (B, C, D):", cost[-1] / effect[-1] < lambda, "\n")
10 years at 0.70 = 7.0 | full health over those years adds 3.0
Series example: 150000 / 1.00 = 150,000 THB | threshold: 160,000
Each plan against A, THB per unit of health (B, C, D): 100,000 88,888.89 111,764.71
Below the threshold (B, C, D): TRUE TRUE TRUE
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · dampack 1.0.2.1000 · works in R 4.6.1 · Download icer-and-net-monetary-benefit-across-strategies.R
Full tidy script
# Four follow-up plans after a heart attack: set aside the beaten plans, then find the best buy
# แผนติดตามผู้ป่วยหลังกล้ามเนื้อหัวใจขาดเลือดสี่แบบ: กันแผนที่แพ้ออก แล้วหาแผนที่คุ้มที่สุด
# 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)
# dampack 1.0.2.1000 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2", "dampack"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2", "dampack"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
# remotes::install_version("dampack", "1.0.2.1000")
library(tibble) # Write the four plans as an explicit input table.
library(dplyr) # Add each plan's net gain to the table.
library(ggplot2) # Display the plans without saving files.
library(dampack) # Cross-check which plans drop out with an independent routine.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The four plans, as extra cost and extra health over plan A
# Order A, B, C, D. Cost is THB per patient and health is quality-weighted years per patient.
# All values are illustrative, not cardiac rehabilitation evidence or a payer's rule.
# เรียงเป็น A, B, C, D ต้นทุนเป็นบาทต่อผู้ป่วย สุขภาพเป็นปีที่ถ่วงด้วยคุณภาพต่อผู้ป่วย
# ค่าทั้งหมดเป็นตัวอย่างสมมติ ไม่ใช่หลักฐานเรื่องการฟื้นฟูหัวใจหรือเกณฑ์ของผู้จ่าย
cost <- c(0, 8000, 16000, 19000)
effect <- c(0, 0.08, 0.18, 0.17)
lambda <- 160000
strategy <- c("A", "B", "C", "D")
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
two <- function(value) formatC(value, format = "f", digits = 2)
plans <- tibble::tibble(strategy = strategy, cost = cost, effect = effect)
#%%@note 2. Set aside the plan beaten outright, then compare neighbours
# D costs more than C and gives less health, so it leaves first. The steps then run A to B to C.
# D แพงกว่า C และได้สุขภาพน้อยกว่า จึงออกก่อน แล้วจึงไล่ขั้นจาก A ไป B ไป C
stopifnot(cost[4] > cost[3], effect[4] < effect[3])
step_cost <- diff(cost[1:3])
step_effect <- diff(effect[1:3])
stopifnot(step_cost[1] / step_effect[1] > step_cost[2] / step_effect[2])
checked <- dampack::calculate_icers(cost = cost, effect = effect, strategies = strategy)
stopifnot(identical(checked$Status[match(strategy, checked$Strategy)], c("ND", "ED", "ND", "D")))
#%%@note 3. Price the mix of A and C, then the step from A to C
# The mix gives C to a share of patients and A to the rest, so its average health equals B's.
# ส่วนผสมให้ C แก่ผู้ป่วยส่วนหนึ่งและให้ A แก่ที่เหลือ สุขภาพเฉลี่ยจึงเท่ากับของ B
share_c <- (effect[2] - effect[1]) / (effect[3] - effect[1])
mix_cost <- (1 - share_c) * cost[1] + share_c * cost[3]
mix_effect <- (1 - share_c) * effect[1] + share_c * effect[3]
ratio <- (cost[3] - cost[1]) / (effect[3] - effect[1])
stopifnot(mix_cost < cost[2], abs(mix_effect - effect[2]) < 1e-9, ratio < lambda)
#%%@note 4. Value health at the threshold and subtract cost
# Each plan's extra health is priced at the threshold, then its extra cost is taken away.
# ตีค่าสุขภาพที่เพิ่มของแต่ละแผนด้วยเกณฑ์ แล้วหักต้นทุนที่เพิ่มออก
valued <- lambda * effect
net <- valued - cost
plans <- plans |>
dplyr::mutate(net = lambda * effect - cost)
stopifnot(strategy[which.max(net)] == "C", all(abs(plans$net - net) < 1e-9))
#%%@note 5. Display the four plans
# Plot extra cost against extra health per patient, with the line from A to C. No file is saved.
# แสดงต้นทุนที่เพิ่มเทียบกับสุขภาพที่เพิ่มต่อผู้ป่วย พร้อมเส้นจาก A ไป C โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(plans, ggplot2::aes(x = effect, y = cost, label = strategy)) +
ggplot2::geom_point() + ggplot2::geom_text(nudge_y = 700) +
ggplot2::geom_line(data = dplyr::filter(plans, strategy %in% c("A", "C"))) +
ggplot2::labs(x = "Extra QALYs per patient", y = "Extra cost per patient (THB)"))
#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("10 years at 0.70 =", formatC(10 * 0.70, format = "f", digits = 1),
"| full health over those years adds", formatC(10 * 0.30, format = "f", digits = 1), "\n")
cat("Series example: 150000 / 1.00 =", big(150000 / 1.00), "THB | threshold:", big(lambda), "\n")
cat("Each plan against A, THB per unit of health (B, C, D):", big(cost[-1] / effect[-1]), "\n")
cat("Below the threshold (B, C, D):", cost[-1] / effect[-1] < lambda, "\n")
cat("Series: 270000 - 250000 =", big(270000 - 250000), "| 6.00 - 5.90 =", two(6.00 - 5.90), "\n")
cat("C against D: saves", big(cost[4] - cost[3]), "THB | gains", two(effect[3] - effect[4]), "\n")
cat("D against C:", big(cost[4] - cost[3]), "/", two(effect[4] - effect[3]), "=",
big((cost[4] - cost[3]) / (effect[4] - effect[3])), "\n")
cat("Steps A to B, B to C: cost", big(step_cost), "THB | health", two(step_effect), "\n")
cat("THB per unit of health, A to B then B to C:", big(step_cost / step_effect), "\n")
cat("Share given C | given A:", paste0(two(100 * c(share_c, 1 - share_c)), "%"), "\n")
cat("Per 100 patients, on C:", two(100 * share_c), "\n")
cat("Health of the mix:", two(mix_effect), "| plan B:", two(effect[2]), "\n")
cat("Cost of the mix:", big(mix_cost), "THB | plan B:", big(cost[2]), "THB | saving:",
big(cost[2] - mix_cost), "THB\n")
cat("A to C: cost", big(cost[3] - cost[1]), "THB | health", two(effect[3] - effect[1]), "\n")
cat("A to C, THB per unit of health:", big(ratio), "\n")
cat("Below the threshold of", big(lambda), "THB:", ratio < lambda, "\n")
cat("Series example: 160000 x 1.00 - 150000 =", big(160000 * 1.00 - 150000), "THB\n")
cat("Health valued at the threshold (A, B, C, D):", big(valued), "THB\n")
cat("Net gain over A (A, B, C, D):", big(net), "THB | largest:", strategy[which.max(net)], "\n")
cat("A and C tie at", big(cost[3] / effect[3]), "| B beats A above", big(cost[2] / effect[2]),
"| B beats C below", big((cost[3] - cost[2]) / (effect[3] - effect[2])), "\n")
First, set aside any plan that another beats outright
Start with the easiest check, whether any plan is beaten on both cost and health. Dominance is the rule that removes an option when another costs less and gives at least as much health. In the series' example, an option costing 270,000 THB for 5.90 QALYs is beaten by one costing 250,000 THB for 6.00 QALYs. It costs 20,000 THB more and gives 0.10 QALY less, so the cheaper option dominates it; the full lesson is in the foundation article on dominance.
In the heart team's case, plan D costs 3,000 THB more than plan C and gives 0.01 QALY less. However much the fund values a QALY, C beats D, so C dominates D and D leaves the comparison. A plan that is both dearer and less effective than another is strictly dominated.
Do not try to rescue D with a ratio. D against C gives 3,000 / -0.01 = -300,000 THB per QALY, a negative number that looks like a bargain. A negative ratio can mean cheaper and better, or dearer and worse as here. Read the signs of the cost and health differences before the ratio.
Next, compare each plan with its neighbour
With D gone, rank the remaining plans by health: A (0), B (0.08) and C (0.18). Compare each with the plan just below it in health, not with A. The step from A to B costs 8,000 THB for 0.08 QALY, which is 100,000 THB per QALY. The step from B to C costs another 8,000 THB for 0.10 QALY, only 80,000 THB per QALY.
The second step buys health more cheaply than the first. A fund willing to pay 100,000 THB per QALY to reach B would gladly pay 80,000 to go on to C. So B is a poor place to stop.
Try a thought experiment: split similar patients into two groups, giving C to one group and leaving the other on A. This is a mix of A and C, and its share can be set so that the group's average health equals B's. The card below works out what that mix costs.
Match plan B's health with a mix of A and C
-
Share of patients given C
\[ \frac{0.08}{0.18} \times 100 = 44.44 \]
Give C to just enough patients that the group's average health equals B's 0.08 QALY. Each patient on C gains 0.18 QALY, so the share given C is 0.08 / 0.18: 44.44 of every 100 patients, or 44.44%. The other 55.56% stay on plan A.
-
Cost of the mix
\[ \frac{0.08}{0.18} \times 16000 = 7111.11 \]
A patient on C costs 16,000 THB extra and a patient on A costs nothing extra. The mix therefore costs the same share of 16,000 THB per patient on average.
-
Compare with plan B
\[ 8000 - 7111.11 = 888.89 \]
The mix gives the same 0.08 QALY as B for 7,111.11 THB instead of 8,000 THB, a saving of 888.89 THB per patient.
Result: A mix of A and C gives B's 0.08 QALY for 888.89 THB less per patient. So B is never the plan to choose.
A plan beaten by a mix
So B is beaten, not by A or C alone, but by a mix of the two. B gives more health than A and costs less than C, so no single plan dominates it. Being beaten by a mix of the two neighbouring plans is called extended dominance. It shows up whenever the cost per QALY falls from one step to the next, as it does here from 100,000 to 80,000 THB.
The mix is a yardstick, not a fifth plan on the table. It asks what B's health would cost if the team could split similar patients between A and C, and no patient receives part of a treatment. Even if the team never splits patients, the money check later in this article never picks B.
Check the first two steps in R
# Step 1: remove the plan beaten outright. Step 2: compare each remaining plan with its neighbour.
# Neighbour steps run in health order over A, B, C. Values are per patient and illustrative.
cost <- c(0, 8000, 16000, 19000)
effect <- c(0, 0.08, 0.18, 0.17)
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
two <- function(value) formatC(value, format = "f", digits = 2)
step_cost <- diff(cost[1:3])
step_effect <- diff(effect[1:3])
cat("Series: 270000 - 250000 =", big(270000 - 250000), "| 6.00 - 5.90 =", two(6.00 - 5.90), "\n")
cat("C against D: saves", big(cost[4] - cost[3]), "THB | gains", two(effect[3] - effect[4]), "\n")
cat("D against C:", big(cost[4] - cost[3]), "/", two(effect[4] - effect[3]), "=",
big((cost[4] - cost[3]) / (effect[4] - effect[3])), "\n")
cat("Steps A to B, B to C: cost", big(step_cost), "THB | health", two(step_effect), "\n")
cat("THB per unit of health, A to B then B to C:", big(step_cost / step_effect), "\n")
Series: 270000 - 250000 = 20,000 | 6.00 - 5.90 = 0.10
C against D: saves 3,000 THB | gains 0.01
D against C: 3,000 / -0.01 = -300,000
Steps A to B, B to C: cost 8,000 8,000 THB | health 0.08 0.10
THB per unit of health, A to B then B to C: 100,000 80,000
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · dampack 1.0.2.1000 · works in R 4.6.1 · Download icer-and-net-monetary-benefit-across-strategies.R
Full tidy script
# Four follow-up plans after a heart attack: set aside the beaten plans, then find the best buy
# แผนติดตามผู้ป่วยหลังกล้ามเนื้อหัวใจขาดเลือดสี่แบบ: กันแผนที่แพ้ออก แล้วหาแผนที่คุ้มที่สุด
# 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)
# dampack 1.0.2.1000 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2", "dampack"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2", "dampack"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
# remotes::install_version("dampack", "1.0.2.1000")
library(tibble) # Write the four plans as an explicit input table.
library(dplyr) # Add each plan's net gain to the table.
library(ggplot2) # Display the plans without saving files.
library(dampack) # Cross-check which plans drop out with an independent routine.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The four plans, as extra cost and extra health over plan A
# Order A, B, C, D. Cost is THB per patient and health is quality-weighted years per patient.
# All values are illustrative, not cardiac rehabilitation evidence or a payer's rule.
# เรียงเป็น A, B, C, D ต้นทุนเป็นบาทต่อผู้ป่วย สุขภาพเป็นปีที่ถ่วงด้วยคุณภาพต่อผู้ป่วย
# ค่าทั้งหมดเป็นตัวอย่างสมมติ ไม่ใช่หลักฐานเรื่องการฟื้นฟูหัวใจหรือเกณฑ์ของผู้จ่าย
cost <- c(0, 8000, 16000, 19000)
effect <- c(0, 0.08, 0.18, 0.17)
lambda <- 160000
strategy <- c("A", "B", "C", "D")
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
two <- function(value) formatC(value, format = "f", digits = 2)
plans <- tibble::tibble(strategy = strategy, cost = cost, effect = effect)
#%%@note 2. Set aside the plan beaten outright, then compare neighbours
# D costs more than C and gives less health, so it leaves first. The steps then run A to B to C.
# D แพงกว่า C และได้สุขภาพน้อยกว่า จึงออกก่อน แล้วจึงไล่ขั้นจาก A ไป B ไป C
stopifnot(cost[4] > cost[3], effect[4] < effect[3])
step_cost <- diff(cost[1:3])
step_effect <- diff(effect[1:3])
stopifnot(step_cost[1] / step_effect[1] > step_cost[2] / step_effect[2])
checked <- dampack::calculate_icers(cost = cost, effect = effect, strategies = strategy)
stopifnot(identical(checked$Status[match(strategy, checked$Strategy)], c("ND", "ED", "ND", "D")))
#%%@note 3. Price the mix of A and C, then the step from A to C
# The mix gives C to a share of patients and A to the rest, so its average health equals B's.
# ส่วนผสมให้ C แก่ผู้ป่วยส่วนหนึ่งและให้ A แก่ที่เหลือ สุขภาพเฉลี่ยจึงเท่ากับของ B
share_c <- (effect[2] - effect[1]) / (effect[3] - effect[1])
mix_cost <- (1 - share_c) * cost[1] + share_c * cost[3]
mix_effect <- (1 - share_c) * effect[1] + share_c * effect[3]
ratio <- (cost[3] - cost[1]) / (effect[3] - effect[1])
stopifnot(mix_cost < cost[2], abs(mix_effect - effect[2]) < 1e-9, ratio < lambda)
#%%@note 4. Value health at the threshold and subtract cost
# Each plan's extra health is priced at the threshold, then its extra cost is taken away.
# ตีค่าสุขภาพที่เพิ่มของแต่ละแผนด้วยเกณฑ์ แล้วหักต้นทุนที่เพิ่มออก
valued <- lambda * effect
net <- valued - cost
plans <- plans |>
dplyr::mutate(net = lambda * effect - cost)
stopifnot(strategy[which.max(net)] == "C", all(abs(plans$net - net) < 1e-9))
#%%@note 5. Display the four plans
# Plot extra cost against extra health per patient, with the line from A to C. No file is saved.
# แสดงต้นทุนที่เพิ่มเทียบกับสุขภาพที่เพิ่มต่อผู้ป่วย พร้อมเส้นจาก A ไป C โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(plans, ggplot2::aes(x = effect, y = cost, label = strategy)) +
ggplot2::geom_point() + ggplot2::geom_text(nudge_y = 700) +
ggplot2::geom_line(data = dplyr::filter(plans, strategy %in% c("A", "C"))) +
ggplot2::labs(x = "Extra QALYs per patient", y = "Extra cost per patient (THB)"))
#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("10 years at 0.70 =", formatC(10 * 0.70, format = "f", digits = 1),
"| full health over those years adds", formatC(10 * 0.30, format = "f", digits = 1), "\n")
cat("Series example: 150000 / 1.00 =", big(150000 / 1.00), "THB | threshold:", big(lambda), "\n")
cat("Each plan against A, THB per unit of health (B, C, D):", big(cost[-1] / effect[-1]), "\n")
cat("Below the threshold (B, C, D):", cost[-1] / effect[-1] < lambda, "\n")
cat("Series: 270000 - 250000 =", big(270000 - 250000), "| 6.00 - 5.90 =", two(6.00 - 5.90), "\n")
cat("C against D: saves", big(cost[4] - cost[3]), "THB | gains", two(effect[3] - effect[4]), "\n")
cat("D against C:", big(cost[4] - cost[3]), "/", two(effect[4] - effect[3]), "=",
big((cost[4] - cost[3]) / (effect[4] - effect[3])), "\n")
cat("Steps A to B, B to C: cost", big(step_cost), "THB | health", two(step_effect), "\n")
cat("THB per unit of health, A to B then B to C:", big(step_cost / step_effect), "\n")
cat("Share given C | given A:", paste0(two(100 * c(share_c, 1 - share_c)), "%"), "\n")
cat("Per 100 patients, on C:", two(100 * share_c), "\n")
cat("Health of the mix:", two(mix_effect), "| plan B:", two(effect[2]), "\n")
cat("Cost of the mix:", big(mix_cost), "THB | plan B:", big(cost[2]), "THB | saving:",
big(cost[2] - mix_cost), "THB\n")
cat("A to C: cost", big(cost[3] - cost[1]), "THB | health", two(effect[3] - effect[1]), "\n")
cat("A to C, THB per unit of health:", big(ratio), "\n")
cat("Below the threshold of", big(lambda), "THB:", ratio < lambda, "\n")
cat("Series example: 160000 x 1.00 - 150000 =", big(160000 * 1.00 - 150000), "THB\n")
cat("Health valued at the threshold (A, B, C, D):", big(valued), "THB\n")
cat("Net gain over A (A, B, C, D):", big(net), "THB | largest:", strategy[which.max(net)], "\n")
cat("A and C tie at", big(cost[3] / effect[3]), "| B beats A above", big(cost[2] / effect[2]),
"| B beats C below", big((cost[3] - cost[2]) / (effect[3] - effect[2])), "\n")
Price the mix in R
# Price a mix of plans A and C that gives the patient group the same average health as plan B.
cost <- c(0, 8000, 16000, 19000)
effect <- c(0, 0.08, 0.18, 0.17)
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
two <- function(value) formatC(value, format = "f", digits = 2)
share_c <- (effect[2] - effect[1]) / (effect[3] - effect[1])
mix_cost <- (1 - share_c) * cost[1] + share_c * cost[3]
mix_effect <- (1 - share_c) * effect[1] + share_c * effect[3]
cat("Share given C | given A:", paste0(two(100 * c(share_c, 1 - share_c)), "%"), "\n")
cat("Per 100 patients, on C:", two(100 * share_c), "\n")
cat("Health of the mix:", two(mix_effect), "| plan B:", two(effect[2]), "\n")
cat("Cost of the mix:", big(mix_cost), "THB | plan B:", big(cost[2]), "THB | saving:",
big(cost[2] - mix_cost), "THB\n")
Share given C | given A: 44.44% 55.56%
Per 100 patients, on C: 44.44
Health of the mix: 0.08 | plan B: 0.08
Cost of the mix: 7,111.11 THB | plan B: 8,000 THB | saving: 888.89 THB
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · dampack 1.0.2.1000 · works in R 4.6.1 · Download icer-and-net-monetary-benefit-across-strategies.R
Full tidy script
# Four follow-up plans after a heart attack: set aside the beaten plans, then find the best buy
# แผนติดตามผู้ป่วยหลังกล้ามเนื้อหัวใจขาดเลือดสี่แบบ: กันแผนที่แพ้ออก แล้วหาแผนที่คุ้มที่สุด
# 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)
# dampack 1.0.2.1000 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2", "dampack"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2", "dampack"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
# remotes::install_version("dampack", "1.0.2.1000")
library(tibble) # Write the four plans as an explicit input table.
library(dplyr) # Add each plan's net gain to the table.
library(ggplot2) # Display the plans without saving files.
library(dampack) # Cross-check which plans drop out with an independent routine.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The four plans, as extra cost and extra health over plan A
# Order A, B, C, D. Cost is THB per patient and health is quality-weighted years per patient.
# All values are illustrative, not cardiac rehabilitation evidence or a payer's rule.
# เรียงเป็น A, B, C, D ต้นทุนเป็นบาทต่อผู้ป่วย สุขภาพเป็นปีที่ถ่วงด้วยคุณภาพต่อผู้ป่วย
# ค่าทั้งหมดเป็นตัวอย่างสมมติ ไม่ใช่หลักฐานเรื่องการฟื้นฟูหัวใจหรือเกณฑ์ของผู้จ่าย
cost <- c(0, 8000, 16000, 19000)
effect <- c(0, 0.08, 0.18, 0.17)
lambda <- 160000
strategy <- c("A", "B", "C", "D")
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
two <- function(value) formatC(value, format = "f", digits = 2)
plans <- tibble::tibble(strategy = strategy, cost = cost, effect = effect)
#%%@note 2. Set aside the plan beaten outright, then compare neighbours
# D costs more than C and gives less health, so it leaves first. The steps then run A to B to C.
# D แพงกว่า C และได้สุขภาพน้อยกว่า จึงออกก่อน แล้วจึงไล่ขั้นจาก A ไป B ไป C
stopifnot(cost[4] > cost[3], effect[4] < effect[3])
step_cost <- diff(cost[1:3])
step_effect <- diff(effect[1:3])
stopifnot(step_cost[1] / step_effect[1] > step_cost[2] / step_effect[2])
checked <- dampack::calculate_icers(cost = cost, effect = effect, strategies = strategy)
stopifnot(identical(checked$Status[match(strategy, checked$Strategy)], c("ND", "ED", "ND", "D")))
#%%@note 3. Price the mix of A and C, then the step from A to C
# The mix gives C to a share of patients and A to the rest, so its average health equals B's.
# ส่วนผสมให้ C แก่ผู้ป่วยส่วนหนึ่งและให้ A แก่ที่เหลือ สุขภาพเฉลี่ยจึงเท่ากับของ B
share_c <- (effect[2] - effect[1]) / (effect[3] - effect[1])
mix_cost <- (1 - share_c) * cost[1] + share_c * cost[3]
mix_effect <- (1 - share_c) * effect[1] + share_c * effect[3]
ratio <- (cost[3] - cost[1]) / (effect[3] - effect[1])
stopifnot(mix_cost < cost[2], abs(mix_effect - effect[2]) < 1e-9, ratio < lambda)
#%%@note 4. Value health at the threshold and subtract cost
# Each plan's extra health is priced at the threshold, then its extra cost is taken away.
# ตีค่าสุขภาพที่เพิ่มของแต่ละแผนด้วยเกณฑ์ แล้วหักต้นทุนที่เพิ่มออก
valued <- lambda * effect
net <- valued - cost
plans <- plans |>
dplyr::mutate(net = lambda * effect - cost)
stopifnot(strategy[which.max(net)] == "C", all(abs(plans$net - net) < 1e-9))
#%%@note 5. Display the four plans
# Plot extra cost against extra health per patient, with the line from A to C. No file is saved.
# แสดงต้นทุนที่เพิ่มเทียบกับสุขภาพที่เพิ่มต่อผู้ป่วย พร้อมเส้นจาก A ไป C โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(plans, ggplot2::aes(x = effect, y = cost, label = strategy)) +
ggplot2::geom_point() + ggplot2::geom_text(nudge_y = 700) +
ggplot2::geom_line(data = dplyr::filter(plans, strategy %in% c("A", "C"))) +
ggplot2::labs(x = "Extra QALYs per patient", y = "Extra cost per patient (THB)"))
#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("10 years at 0.70 =", formatC(10 * 0.70, format = "f", digits = 1),
"| full health over those years adds", formatC(10 * 0.30, format = "f", digits = 1), "\n")
cat("Series example: 150000 / 1.00 =", big(150000 / 1.00), "THB | threshold:", big(lambda), "\n")
cat("Each plan against A, THB per unit of health (B, C, D):", big(cost[-1] / effect[-1]), "\n")
cat("Below the threshold (B, C, D):", cost[-1] / effect[-1] < lambda, "\n")
cat("Series: 270000 - 250000 =", big(270000 - 250000), "| 6.00 - 5.90 =", two(6.00 - 5.90), "\n")
cat("C against D: saves", big(cost[4] - cost[3]), "THB | gains", two(effect[3] - effect[4]), "\n")
cat("D against C:", big(cost[4] - cost[3]), "/", two(effect[4] - effect[3]), "=",
big((cost[4] - cost[3]) / (effect[4] - effect[3])), "\n")
cat("Steps A to B, B to C: cost", big(step_cost), "THB | health", two(step_effect), "\n")
cat("THB per unit of health, A to B then B to C:", big(step_cost / step_effect), "\n")
cat("Share given C | given A:", paste0(two(100 * c(share_c, 1 - share_c)), "%"), "\n")
cat("Per 100 patients, on C:", two(100 * share_c), "\n")
cat("Health of the mix:", two(mix_effect), "| plan B:", two(effect[2]), "\n")
cat("Cost of the mix:", big(mix_cost), "THB | plan B:", big(cost[2]), "THB | saving:",
big(cost[2] - mix_cost), "THB\n")
cat("A to C: cost", big(cost[3] - cost[1]), "THB | health", two(effect[3] - effect[1]), "\n")
cat("A to C, THB per unit of health:", big(ratio), "\n")
cat("Below the threshold of", big(lambda), "THB:", ratio < lambda, "\n")
cat("Series example: 160000 x 1.00 - 150000 =", big(160000 * 1.00 - 150000), "THB\n")
cat("Health valued at the threshold (A, B, C, D):", big(valued), "THB\n")
cat("Net gain over A (A, B, C, D):", big(net), "THB | largest:", strategy[which.max(net)], "\n")
cat("A and C tie at", big(cost[3] / effect[3]), "| B beats A above", big(cost[2] / effect[2]),
"| B beats C below", big((cost[3] - cost[2]) / (effect[3] - effect[2])), "\n")
Then, walk the efficient frontier
Set B aside as well and compare C directly with A. The step costs 16,000 THB for 0.18 QALY, which is 88,888.89 THB per QALY. That is below 160,000 THB, so the team should move from A all the way to C.
Picture each plan as a point, with extra health to the right and extra cost upward; this picture is the cost-effectiveness plane. The line from A to C is the cheapest way to buy each amount of health with these plans, since no plan lies below it. That line is called the efficient frontier, and only moves along it need a cost per QALY.
Reading the frontier is not hard. Start at the cheapest plan and keep stepping along the frontier while each step costs less per QALY than the threshold. Here there is one step, A to C, and 88,888.89 THB is below 160,000 THB, so the answer is C.
Each step's cost per QALY
| Step | Extra cost (THB) | Extra QALYs | THB per QALY |
|---|---|---|---|
| A to B | 8,000 | 0.08 | 100,000 |
| B to C | 8,000 | 0.10 | 80,000 |
| A to C (the frontier) | 16,000 | 0.18 | 88,888.89 |
Check the frontier step in R
# Step 3: with B and D set aside, compare C directly with A and judge the step at the threshold.
cost <- c(0, 8000, 16000, 19000)
effect <- c(0, 0.08, 0.18, 0.17)
lambda <- 160000
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
two <- function(value) formatC(value, format = "f", digits = 2)
ratio <- (cost[3] - cost[1]) / (effect[3] - effect[1])
cat("A to C: cost", big(cost[3] - cost[1]), "THB | health", two(effect[3] - effect[1]), "\n")
cat("A to C, THB per unit of health:", big(ratio), "\n")
cat("Below the threshold of", big(lambda), "THB:", ratio < lambda, "\n")
A to C: cost 16,000 THB | health 0.18
A to C, THB per unit of health: 88,888.89
Below the threshold of 160,000 THB: TRUE
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · dampack 1.0.2.1000 · works in R 4.6.1 · Download icer-and-net-monetary-benefit-across-strategies.R
Full tidy script
# Four follow-up plans after a heart attack: set aside the beaten plans, then find the best buy
# แผนติดตามผู้ป่วยหลังกล้ามเนื้อหัวใจขาดเลือดสี่แบบ: กันแผนที่แพ้ออก แล้วหาแผนที่คุ้มที่สุด
# 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)
# dampack 1.0.2.1000 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2", "dampack"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2", "dampack"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
# remotes::install_version("dampack", "1.0.2.1000")
library(tibble) # Write the four plans as an explicit input table.
library(dplyr) # Add each plan's net gain to the table.
library(ggplot2) # Display the plans without saving files.
library(dampack) # Cross-check which plans drop out with an independent routine.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The four plans, as extra cost and extra health over plan A
# Order A, B, C, D. Cost is THB per patient and health is quality-weighted years per patient.
# All values are illustrative, not cardiac rehabilitation evidence or a payer's rule.
# เรียงเป็น A, B, C, D ต้นทุนเป็นบาทต่อผู้ป่วย สุขภาพเป็นปีที่ถ่วงด้วยคุณภาพต่อผู้ป่วย
# ค่าทั้งหมดเป็นตัวอย่างสมมติ ไม่ใช่หลักฐานเรื่องการฟื้นฟูหัวใจหรือเกณฑ์ของผู้จ่าย
cost <- c(0, 8000, 16000, 19000)
effect <- c(0, 0.08, 0.18, 0.17)
lambda <- 160000
strategy <- c("A", "B", "C", "D")
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
two <- function(value) formatC(value, format = "f", digits = 2)
plans <- tibble::tibble(strategy = strategy, cost = cost, effect = effect)
#%%@note 2. Set aside the plan beaten outright, then compare neighbours
# D costs more than C and gives less health, so it leaves first. The steps then run A to B to C.
# D แพงกว่า C และได้สุขภาพน้อยกว่า จึงออกก่อน แล้วจึงไล่ขั้นจาก A ไป B ไป C
stopifnot(cost[4] > cost[3], effect[4] < effect[3])
step_cost <- diff(cost[1:3])
step_effect <- diff(effect[1:3])
stopifnot(step_cost[1] / step_effect[1] > step_cost[2] / step_effect[2])
checked <- dampack::calculate_icers(cost = cost, effect = effect, strategies = strategy)
stopifnot(identical(checked$Status[match(strategy, checked$Strategy)], c("ND", "ED", "ND", "D")))
#%%@note 3. Price the mix of A and C, then the step from A to C
# The mix gives C to a share of patients and A to the rest, so its average health equals B's.
# ส่วนผสมให้ C แก่ผู้ป่วยส่วนหนึ่งและให้ A แก่ที่เหลือ สุขภาพเฉลี่ยจึงเท่ากับของ B
share_c <- (effect[2] - effect[1]) / (effect[3] - effect[1])
mix_cost <- (1 - share_c) * cost[1] + share_c * cost[3]
mix_effect <- (1 - share_c) * effect[1] + share_c * effect[3]
ratio <- (cost[3] - cost[1]) / (effect[3] - effect[1])
stopifnot(mix_cost < cost[2], abs(mix_effect - effect[2]) < 1e-9, ratio < lambda)
#%%@note 4. Value health at the threshold and subtract cost
# Each plan's extra health is priced at the threshold, then its extra cost is taken away.
# ตีค่าสุขภาพที่เพิ่มของแต่ละแผนด้วยเกณฑ์ แล้วหักต้นทุนที่เพิ่มออก
valued <- lambda * effect
net <- valued - cost
plans <- plans |>
dplyr::mutate(net = lambda * effect - cost)
stopifnot(strategy[which.max(net)] == "C", all(abs(plans$net - net) < 1e-9))
#%%@note 5. Display the four plans
# Plot extra cost against extra health per patient, with the line from A to C. No file is saved.
# แสดงต้นทุนที่เพิ่มเทียบกับสุขภาพที่เพิ่มต่อผู้ป่วย พร้อมเส้นจาก A ไป C โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(plans, ggplot2::aes(x = effect, y = cost, label = strategy)) +
ggplot2::geom_point() + ggplot2::geom_text(nudge_y = 700) +
ggplot2::geom_line(data = dplyr::filter(plans, strategy %in% c("A", "C"))) +
ggplot2::labs(x = "Extra QALYs per patient", y = "Extra cost per patient (THB)"))
#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("10 years at 0.70 =", formatC(10 * 0.70, format = "f", digits = 1),
"| full health over those years adds", formatC(10 * 0.30, format = "f", digits = 1), "\n")
cat("Series example: 150000 / 1.00 =", big(150000 / 1.00), "THB | threshold:", big(lambda), "\n")
cat("Each plan against A, THB per unit of health (B, C, D):", big(cost[-1] / effect[-1]), "\n")
cat("Below the threshold (B, C, D):", cost[-1] / effect[-1] < lambda, "\n")
cat("Series: 270000 - 250000 =", big(270000 - 250000), "| 6.00 - 5.90 =", two(6.00 - 5.90), "\n")
cat("C against D: saves", big(cost[4] - cost[3]), "THB | gains", two(effect[3] - effect[4]), "\n")
cat("D against C:", big(cost[4] - cost[3]), "/", two(effect[4] - effect[3]), "=",
big((cost[4] - cost[3]) / (effect[4] - effect[3])), "\n")
cat("Steps A to B, B to C: cost", big(step_cost), "THB | health", two(step_effect), "\n")
cat("THB per unit of health, A to B then B to C:", big(step_cost / step_effect), "\n")
cat("Share given C | given A:", paste0(two(100 * c(share_c, 1 - share_c)), "%"), "\n")
cat("Per 100 patients, on C:", two(100 * share_c), "\n")
cat("Health of the mix:", two(mix_effect), "| plan B:", two(effect[2]), "\n")
cat("Cost of the mix:", big(mix_cost), "THB | plan B:", big(cost[2]), "THB | saving:",
big(cost[2] - mix_cost), "THB\n")
cat("A to C: cost", big(cost[3] - cost[1]), "THB | health", two(effect[3] - effect[1]), "\n")
cat("A to C, THB per unit of health:", big(ratio), "\n")
cat("Below the threshold of", big(lambda), "THB:", ratio < lambda, "\n")
cat("Series example: 160000 x 1.00 - 150000 =", big(160000 * 1.00 - 150000), "THB\n")
cat("Health valued at the threshold (A, B, C, D):", big(valued), "THB\n")
cat("Net gain over A (A, B, C, D):", big(net), "THB | largest:", strategy[which.max(net)], "\n")
cat("A and C tie at", big(cost[3] / effect[3]), "| B beats A above", big(cost[2] / effect[2]),
"| B beats C below", big((cost[3] - cost[2]) / (effect[3] - effect[2])), "\n")
Check the answer with net monetary benefit
Ratios mislead when negative, and they cannot be formed when two plans give the same health, because the divisor is zero. A second tool needs no division: turn health into money by multiplying the QALYs gained by the threshold, then take away the cost. In the series' shared example, 1.00 extra QALY at 160,000 THB per QALY is worth 160,000 THB, and taking away the extra 150,000 THB leaves 10,000 THB, so the extra cost is worth paying. This is called net monetary benefit (NMB), and between two options it is the incremental NMB (INMB); the full lesson is in the foundation article on net benefit.
Here each plan's NMB over A is its INMB against usual visits. Plan B's 0.08 QALY is worth 0.08 × 160,000 = 12,800 THB, and taking away its 8,000 THB leaves an NMB of 4,800 THB. In the same way, C's NMB is 28,800 minus 16,000 = 12,800 THB. D's is 27,200 minus 19,000 = 8,200 THB, and A's is 0.
The rule is to choose the plan with the largest NMB, here C at 12,800 THB per patient. It matches the frontier, and always will: the plan with the largest NMB at any threshold lies on the efficient frontier. D's NMB is positive, yet C's is higher at every threshold, because C costs less and gives more.
The threshold decides where the answer changes, and each break point is a cost per QALY already met. A and C tie where C's NMB reaches zero, when 0.18 × threshold equals 16,000 THB, that is at 16,000 / 0.18 = 88,888.89 THB per QALY, the A to C step. Below that threshold A has the larger NMB, and above it C does.
B beats A only when 0.08 × threshold is more than 8,000 THB, so above 8,000 / 0.08 = 100,000 THB per QALY, the A to B step. B beats C only when C's extra 0.10 QALY is worth less than its extra 8,000 THB, so below 8,000 / 0.10 = 80,000 THB per QALY. No threshold does both, so B never comes first; that is extended dominance seen through NMB.
Value each plan in R
# Value each plan's extra health at the threshold, subtract its extra cost, and pick the largest.
cost <- c(0, 8000, 16000, 19000)
effect <- c(0, 0.08, 0.18, 0.17)
lambda <- 160000
strategy <- c("A", "B", "C", "D")
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
valued <- lambda * effect
net <- valued - cost
cat("Series example: 160000 x 1.00 - 150000 =", big(160000 * 1.00 - 150000), "THB\n")
cat("Health valued at the threshold (A, B, C, D):", big(valued), "THB\n")
cat("Net gain over A (A, B, C, D):", big(net), "THB | largest:", strategy[which.max(net)], "\n")
cat("A and C tie at", big(cost[3] / effect[3]), "| B beats A above", big(cost[2] / effect[2]),
"| B beats C below", big((cost[3] - cost[2]) / (effect[3] - effect[2])), "\n")
Series example: 160000 x 1.00 - 150000 = 10,000 THB
Health valued at the threshold (A, B, C, D): 0 12,800 28,800 27,200 THB
Net gain over A (A, B, C, D): 0 4,800 12,800 8,200 THB | largest: C
A and C tie at 88,888.89 | B beats A above 100,000 | B beats C below 80,000
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · dampack 1.0.2.1000 · works in R 4.6.1 · Download icer-and-net-monetary-benefit-across-strategies.R
Full tidy script
# Four follow-up plans after a heart attack: set aside the beaten plans, then find the best buy
# แผนติดตามผู้ป่วยหลังกล้ามเนื้อหัวใจขาดเลือดสี่แบบ: กันแผนที่แพ้ออก แล้วหาแผนที่คุ้มที่สุด
# 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)
# dampack 1.0.2.1000 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2", "dampack"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2", "dampack"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
# remotes::install_version("dampack", "1.0.2.1000")
library(tibble) # Write the four plans as an explicit input table.
library(dplyr) # Add each plan's net gain to the table.
library(ggplot2) # Display the plans without saving files.
library(dampack) # Cross-check which plans drop out with an independent routine.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The four plans, as extra cost and extra health over plan A
# Order A, B, C, D. Cost is THB per patient and health is quality-weighted years per patient.
# All values are illustrative, not cardiac rehabilitation evidence or a payer's rule.
# เรียงเป็น A, B, C, D ต้นทุนเป็นบาทต่อผู้ป่วย สุขภาพเป็นปีที่ถ่วงด้วยคุณภาพต่อผู้ป่วย
# ค่าทั้งหมดเป็นตัวอย่างสมมติ ไม่ใช่หลักฐานเรื่องการฟื้นฟูหัวใจหรือเกณฑ์ของผู้จ่าย
cost <- c(0, 8000, 16000, 19000)
effect <- c(0, 0.08, 0.18, 0.17)
lambda <- 160000
strategy <- c("A", "B", "C", "D")
big <- function(value) prettyNum(round(value, 2), big.mark = ",", digits = 10, scientific = FALSE)
two <- function(value) formatC(value, format = "f", digits = 2)
plans <- tibble::tibble(strategy = strategy, cost = cost, effect = effect)
#%%@note 2. Set aside the plan beaten outright, then compare neighbours
# D costs more than C and gives less health, so it leaves first. The steps then run A to B to C.
# D แพงกว่า C และได้สุขภาพน้อยกว่า จึงออกก่อน แล้วจึงไล่ขั้นจาก A ไป B ไป C
stopifnot(cost[4] > cost[3], effect[4] < effect[3])
step_cost <- diff(cost[1:3])
step_effect <- diff(effect[1:3])
stopifnot(step_cost[1] / step_effect[1] > step_cost[2] / step_effect[2])
checked <- dampack::calculate_icers(cost = cost, effect = effect, strategies = strategy)
stopifnot(identical(checked$Status[match(strategy, checked$Strategy)], c("ND", "ED", "ND", "D")))
#%%@note 3. Price the mix of A and C, then the step from A to C
# The mix gives C to a share of patients and A to the rest, so its average health equals B's.
# ส่วนผสมให้ C แก่ผู้ป่วยส่วนหนึ่งและให้ A แก่ที่เหลือ สุขภาพเฉลี่ยจึงเท่ากับของ B
share_c <- (effect[2] - effect[1]) / (effect[3] - effect[1])
mix_cost <- (1 - share_c) * cost[1] + share_c * cost[3]
mix_effect <- (1 - share_c) * effect[1] + share_c * effect[3]
ratio <- (cost[3] - cost[1]) / (effect[3] - effect[1])
stopifnot(mix_cost < cost[2], abs(mix_effect - effect[2]) < 1e-9, ratio < lambda)
#%%@note 4. Value health at the threshold and subtract cost
# Each plan's extra health is priced at the threshold, then its extra cost is taken away.
# ตีค่าสุขภาพที่เพิ่มของแต่ละแผนด้วยเกณฑ์ แล้วหักต้นทุนที่เพิ่มออก
valued <- lambda * effect
net <- valued - cost
plans <- plans |>
dplyr::mutate(net = lambda * effect - cost)
stopifnot(strategy[which.max(net)] == "C", all(abs(plans$net - net) < 1e-9))
#%%@note 5. Display the four plans
# Plot extra cost against extra health per patient, with the line from A to C. No file is saved.
# แสดงต้นทุนที่เพิ่มเทียบกับสุขภาพที่เพิ่มต่อผู้ป่วย พร้อมเส้นจาก A ไป C โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(plans, ggplot2::aes(x = effect, y = cost, label = strategy)) +
ggplot2::geom_point() + ggplot2::geom_text(nudge_y = 700) +
ggplot2::geom_line(data = dplyr::filter(plans, strategy %in% c("A", "C"))) +
ggplot2::labs(x = "Extra QALYs per patient", y = "Extra cost per patient (THB)"))
#%%@note 6. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("10 years at 0.70 =", formatC(10 * 0.70, format = "f", digits = 1),
"| full health over those years adds", formatC(10 * 0.30, format = "f", digits = 1), "\n")
cat("Series example: 150000 / 1.00 =", big(150000 / 1.00), "THB | threshold:", big(lambda), "\n")
cat("Each plan against A, THB per unit of health (B, C, D):", big(cost[-1] / effect[-1]), "\n")
cat("Below the threshold (B, C, D):", cost[-1] / effect[-1] < lambda, "\n")
cat("Series: 270000 - 250000 =", big(270000 - 250000), "| 6.00 - 5.90 =", two(6.00 - 5.90), "\n")
cat("C against D: saves", big(cost[4] - cost[3]), "THB | gains", two(effect[3] - effect[4]), "\n")
cat("D against C:", big(cost[4] - cost[3]), "/", two(effect[4] - effect[3]), "=",
big((cost[4] - cost[3]) / (effect[4] - effect[3])), "\n")
cat("Steps A to B, B to C: cost", big(step_cost), "THB | health", two(step_effect), "\n")
cat("THB per unit of health, A to B then B to C:", big(step_cost / step_effect), "\n")
cat("Share given C | given A:", paste0(two(100 * c(share_c, 1 - share_c)), "%"), "\n")
cat("Per 100 patients, on C:", two(100 * share_c), "\n")
cat("Health of the mix:", two(mix_effect), "| plan B:", two(effect[2]), "\n")
cat("Cost of the mix:", big(mix_cost), "THB | plan B:", big(cost[2]), "THB | saving:",
big(cost[2] - mix_cost), "THB\n")
cat("A to C: cost", big(cost[3] - cost[1]), "THB | health", two(effect[3] - effect[1]), "\n")
cat("A to C, THB per unit of health:", big(ratio), "\n")
cat("Below the threshold of", big(lambda), "THB:", ratio < lambda, "\n")
cat("Series example: 160000 x 1.00 - 150000 =", big(160000 * 1.00 - 150000), "THB\n")
cat("Health valued at the threshold (A, B, C, D):", big(valued), "THB\n")
cat("Net gain over A (A, B, C, D):", big(net), "THB | largest:", strategy[which.max(net)], "\n")
cat("A and C tie at", big(cost[3] / effect[3]), "| B beats A above", big(cost[2] / effect[2]),
"| B beats C below", big((cost[3] - cost[2]) / (effect[3] - effect[2])), "\n")
What the answer does not say
The comparison is about value for money per patient. It does not say whether the fund can afford plan C for every patient this year. That budget question has its own article in this series.
Every input here is a single best guess, so the frontier does not show how sure the team can be that C is best. Briggs and colleagues describe how to carry the uncertainty in each input through to the decision, and later parts of this series show how.
Terms as we use them
- Cost-effectiveness plane
- A picture of each plan as a point, extra health to the right and extra cost upward.
- Dominance
- The rule that removes a plan when another costs less and gives at least as much health.
- Strict dominance
- One plan costs more and gives less health than another, as D does against C; it is set aside first.
- Extended dominance
- A mix of two neighbouring plans gives a plan's health for less money, as the A-C mix does for B.
- Efficient frontier
- The line through the plans that buy each level of health most cheaply; here A and C.
- Threshold
- The most the fund will pay for one QALY; here an illustrative 160,000 THB.
- Net monetary benefit (NMB)
- Health valued at the threshold minus cost; the plan with the largest NMB is chosen.
Connect to the foundation articles
The foundation article on the ICER, dominance and thresholds compares two options at a time; this article extends it to several. The one on early HTA and headroom pricing uses the same threshold to ask what price a new plan could carry.
Try it in the app
Open our economic evaluation app to change the inputs and watch the result update. It needs a login and a Pro or Max plan.
The app starts from its own example values, so its numbers will not match this article.
References
- Michael F. Drummond, Mark J. Sculpher, Karl Claxton, Greg L. Stoddart, George W. Torrance. Methods for the Economic Evaluation of Health Care Programmes. Oxford University Press; 2015. ISBN 9780199665884. https://openlibrary.org/isbn/9780199665884
- 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
- 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://doi.org/10.1016/j.jval.2012.04.014
The series, in learning order
- Health Economic Modelling Series Guide: From Evidence to a Decision
- Choosing a Decision-Analytic Model: Let the Clinical Pathway Decide
- Auditable Model Inputs: Build a Parameter Table You Can Trace
- Finding Model Inputs in the Literature: Match the Estimate to the Question
- Rates, Probabilities and Transition Probabilities: Keep Time in the Calculation
- Building a Decision Tree Step by Step: Follow Outcomes, Costs and Health
- Building a Markov Cohort Model Step by Step: Follow the Cohort Through Time
- Budget Impact Analysis in Practice: Count People Before Counting Money
- ICER and Net Monetary Benefit Across Strategies: Find the Relevant Frontier (this article)
- Probabilistic Sensitivity Analysis: From Parameter Distributions to CEAC
- Model Validation and Verification: Test the Question, Equations and Code
- Expected Value of Perfect Information: What Could Better Evidence Be Worth?
Key takeaways
- Passing the threshold against usual care does not make a plan the right choice: B (100,000), C (88,888.89) and D (111,764.71 THB per QALY) all pass, yet only C is the best buy.
- Set aside any plan another beats outright: C dominates D, which costs 3,000 THB more for 0.01 QALY less.
- Rank the rest by health and compare each plan with its neighbour, not with usual care; a cost per QALY that falls from one step to the next (100,000 then 80,000 THB) marks a plan beaten by a mix.
- Walk the efficient frontier while each step costs less per QALY than the threshold: A to C at 88,888.89 THB is below 160,000 THB.
- The largest NMB gives the same answer without dividing: plan C, at 12,800 THB per patient.
Related in the wiki: ICER, Dominance and the Cost-Effectiveness Plane: Reading the Thai Threshold of 160,000 Baht per QALY · Early Health Technology Assessment, Headroom Pricing and Benefit-Package Decisions