Bài 5: Giá trị gắn với lần chuyển, đường biên hiệu quả và xác minh mô hình

Hoàn thiện mô hình MDR/RR-TB, so sánh ba phác đồ và chứng minh code làm đúng điều ta định làm

Vì sao bạn nên quan tâm?

BPaL/M đã có trong hướng dẫn điều trị lao quốc gia và đang được mở rộng theo từng tỉnh (SCDI 2023). Một Chương trình Chống lao tỉnh đang dùng phác đồ ngắn 9 tháng cần biết: chuyển người bệnh sang BPaLM thì tốn thêm hay tiết kiệm, và được gì về sức khỏe? Theo mô hình của module, mỗi người bệnh dùng BPaLM thay cho phác đồ ngắn tiết kiệm 6.7 triệu VNĐ cho hệ thống y tế (tính cả đời, đã chiết khấu), bằng 1.3 tháng lương tối thiểu vùng I (Chính phủ 2025), và thêm 0.48 QALY. Nếu một tỉnh chuyển 100 người bệnh sang BPaLM, mức đó là 668 triệu VNĐ tính cả đời, chưa phải khoản chi trong một năm ngân sách (M10 bàn riêng). Bài này dạy cách đọc kết quả của ba phương án cùng lúc, thử những giả định có thể lật kết luận, và chứng minh code làm đúng trước khi đưa con số lên bàn họp.

Mục tiêu học tập

Sau bài này, bạn có thể:

  1. Tính chi phí và QALY gắn với biến cố và lần chuyển trạng thái (giá trị gắn với lần chuyển, transition reward).
  2. Lập bảng kết quả đầy đủ: chi phí, năm sống điều chỉnh theo chất lượng (QALY), năm sống, năm sống điều chỉnh theo bệnh tật (DALY) tránh được, tỷ số chi phí-hiệu quả gia tăng (ICER) và lợi ích ròng quy ra tiền (NMB).
  3. Xếp ba phương án trở lên trên đường biên hiệu quả (efficiency frontier), nhận ra lấn át tuyệt đối và lấn át mở rộng.
  4. Dùng phân tích kịch bản (scenario analysis) để tìm giả định chi phối kết quả.
  5. Chạy và mở rộng một bộ kiểm tra xác minh mô hình (verification) dạng hộp đen.

Ý chính

  • Một số giá trị gắn với sự kiện, không gắn với trạng thái: xử trí biến cố bất lợi nặng, đánh giá cuối điều trị.
  • Dòng chuyển \(i \to j\) ở chu kỳ \(t\) bằng \(s_{t-1,i}\,P_t[i,j]\). Nhân với giá trị mỗi sự kiện rồi chiết khấu đúng thời điểm.
  • Hai phương án: ICER luôn kèm góc phần tư; \(\Delta\text{NMB} > 0\) cho cùng quyết định ở mọi góc.
  • Ba phương án trở lên: sắp theo chi phí, loại phương án bị lấn át, loại phương án bị lấn át mở rộng, rồi tính ICER giữa các điểm còn lại trên đường biên. Phương án có NMB cao nhất ở ngưỡng \(\lambda\) luôn nằm trên đường biên.
  • So sánh từng cặp với một phương án “cũ” có thể gây hiểu lầm; luôn nhìn cả đường biên.
  • Phân tích kịch bản đổi từng giả định một và cho thấy kết luận dựa vào giả định nào.
  • Xác minh hỏi “code có làm đúng điều ta định làm không?”; thẩm định (validation) hỏi “mô hình có phản ánh thực tế không?” (Büyükkaramikli và c.s. 2019).

Nội dung

Giá trị gắn với lần chuyển

Vết đoàn hệ (Markov trace) cho biết bao nhiêu người ở đâu; nhiều chi phí lại phụ thuộc bao nhiêu người vừa chuyển. Với dòng chuyển \(F_t(i \to j) = s_{t-1,i}\,P_t[i,j]\) và giá trị mỗi sự kiện \(c_{ij}\):

\[ C_{\text{sự kiện}} = \sum_{t=1}^{n} F_t(i \to j)\; c_{ij}\; d_{\tau_t}, \]

trong đó \(d_{\tau_t}\) là hệ số chiết khấu ở thời điểm sự kiện: \(\tau_t = t - \tfrac12\) cho sự kiện rải rác trong chu kỳ, \(\tau_t = t\) cho lần chuyển xác định ra khỏi đường hầm. Tính dòng chuyển từ vết và ma trận, đừng lấy hiệu số giữa hai hàng của vết, vì một trạng thái có thể vừa nhận vào vừa mất đi trong cùng chu kỳ (Krijkamp và c.s. 2020).

  • Biến cố bất lợi nặng (độ 3 trở lên hoặc nghiêm trọng): 23% với BPaLM và 48% với chăm sóc chuẩn trong kết quả cuối của TB-PRACTECAL (Nyang’wa và c.s. 2024); mô hình dùng tỷ lệ của chăm sóc chuẩn cho cả phác đồ ngắn và phác đồ dài hạn, và gắn biến cố vào lúc bắt đầu điều trị (giả định). Chi phí mỗi biến cố bằng một đợt nằm viện: trung vị US$707.24 tại Bệnh viện Phổi Trung ương năm 2024 (Ngo và c.s. 2026), đổi về VNĐ bằng chính tỷ giá 25,000 VNĐ/USD mà bài báo dùng (để lấy lại số VNĐ gốc; tỷ giá 2024 của khóa học lệch 3.5% so với con số này) và quy về giá 2025 bằng CPI (×1.033) (World Bank 2026), được 18,266,240 VNĐ. Đây là đợt nằm viện vì lao kháng thuốc nói chung, và thuốc cùng dịch truyền chiếm 38% chi phí, nên có thể tính trùng tiền thuốc. Mỗi biến cố làm giảm thỏa dụng 0.15 trong 1 tháng (giá trị minh họa).
  • Đánh giá cuối điều trị: một lượt khám, X-quang ngực, soi đờm và nuôi cấy, 499,100 VNĐ theo giá BHYT (Bộ Y tế 2024), cho mỗi người hoàn thành một đợt điều trị, lần đầu hay điều trị lại.

Hai phương án: bốn góc phần tư

\[ \text{ICER} = \frac{\Delta C}{\Delta Q}, \qquad \Delta \text{NMB} = \lambda\,\Delta Q - \Delta C . \]

Góc \(\Delta C\) \(\Delta Q\) Kết luận
Đông nam âm dương phương án mới chiếm ưu thế (dominant): chọn, không cần ICER
Tây bắc dương âm phương án mới bị lấn át (dominated): loại
Đông bắc dương dương đánh đổi: chọn nếu ICER < \(\lambda\)
Tây nam âm âm đánh đổi: chọn nếu ICER > \(\lambda\)

ICER dương không cho biết đang ở góc đông bắc hay tây nam; ICER âm không phân biệt “chiếm ưu thế” với “bị lấn át”. compare_regimens() trả về cả nhãn góc phần tư lẫn \(\Delta\text{NMB}\); báo cáo đầy đủ theo CHEERS 2022 (Husereau và c.s. 2022). Ngưỡng cơ sở của khóa học là 1 × GDP bình quân đầu người năm 2025, 125,500,000 VNĐ/QALY (Cục Thống kê (Bộ Tài chính) 2026), mốc mà nhiều đánh giá kinh tế ở Việt Nam đang dùng (Ong và c.s. 2026); ước tính theo chi phí cơ hội thường thấp hơn (Woods và c.s. 2016) (M01).

Ba phương án trở lên: đường biên hiệu quả

Với ba phác đồ, so sánh từng cặp có thể dẫn sai: một phương án có ICER “chấp nhận được” so với phương án tệ nhất vẫn có thể là lựa chọn kém. Quy trình chuẩn:

  1. Sắp các phương án theo chi phí tăng dần.
  2. Loại phương án bị lấn át: có phương án khác rẻ hơn (hoặc bằng) mà nhiều QALY hơn (hoặc bằng).
  3. Tính ICER giữa các phương án còn lại liền kề nhau. Nếu ICER của một phương án cao hơn ICER của phương án đắt hơn kế tiếp, phương án đó bị lấn át mở rộng (extended dominance): một tổ hợp của hai phương án lân cận cho kết quả tốt hơn nó. Loại và tính lại.
  4. Các phương án còn lại tạo thành đường biên hiệu quả; ICER tăng dần dọc theo đường biên. Ở ngưỡng \(\lambda\), chọn phương án đắt nhất có ICER (so với điểm trước nó) không vượt \(\lambda\).

Bước 4 luôn chọn đúng phương án có NMB cao nhất, nên trong thực hành có thể chọn thẳng theo NMB; đường biên giải thích vì sao. Hàm cea_frontier() cài bốn bước này; bộ xác minh kiểm tra nó trên một bảng giả lập có cả hai loại lấn át.

DALY trong mô hình Markov

\(\text{DALY} = \text{YLL} + \text{YLD}\). Số năm sống với bệnh tật (YLD) tích lũy theo thời gian ở các trạng thái mắc lao (đang điều trị, điều trị lại, thất bại, bỏ điều trị (lost to follow-up)) nhân với trọng số bệnh tật 0.333, giá trị GBD 2013 cho lao không nhiễm HIV (Salomon và c.s. 2015; Tomeny và c.s. 2024). Nếu số năm sống mất do tử vong sớm (YLL) đo bằng chính bảng sống của mô hình, thì DALY tránh được \(= \Delta(\text{LY} - \text{YLD}) = \Delta\text{HALY}\), với HALY là năm sống điều chỉnh sức khỏe. Mô hình chiết khấu HALY như QALY; GBD không chiết khấu và dùng bảng sống chuẩn, nên bảng kết quả báo cáo cả hai cách.

Xác minh mô hình

TECH-VER đề xuất kiểm tra theo thứ tự: hộp đen (kết quả có khớp kỳ vọng?), hộp trắng (đọc từng dòng code), rồi lập trình song song nếu vẫn chưa rõ (Büyükkaramikli và c.s. 2019). Hàm verify_model() gom các kiểm tra hộp đen:

Nhóm Kiểm tra
Tính hợp lệ, bảo toàn mọi ma trận ở mọi chu kỳ hợp lệ; tổng mỗi hàng của vết bằng 1 (ba phác đồ)
Tái tạo đầu vào kết cục cuối điều trị khớp số liệu đã dùng, kể cả ở tuổi bắt đầu khác; kết cục chung cuộc của điều trị lại khớp đầu vào
Tính tay chu kỳ 1 và số tháng thuốc được tính tiền khớp phép tính độc lập
Giá trị cực trị chi phí bằng 0; thỏa dụng bằng 1; chiết khấu bằng 0; không điều trị lại thì trạng thái ReTx rỗng
Đối xứng, độc lập hai phác đồ giống hệt cho chênh lệch bằng 0; không có rủi ro lao thì tuổi thọ khớp bảng sống
Chiều tác động thành công thấp hơn, hoặc RR lớn hơn, thì QALY ít hơn
Quy tắc quyết định \(\Delta\text{NMB}\) nhất quán; nhãn góc phần tư đúng; đường biên đúng trên bảng giả lập

Mỗi kiểm tra phải có thể thất bại. M11 chuyển các kiểm tra này thành bộ test testthat.

Thực hành

Nạp hàm và tham số. Kiểm tra: không có lỗi.

# Vietnamese text in figures needs a UTF-8 locale (Rscript may start in the "C" locale)
if (!isTRUE(l10n_info()[["UTF-8"]])) invisible(Sys.setlocale("LC_CTYPE", "en_US.UTF-8"))
source("../_shared/R/helpers.R")
source("../_shared/R/vn_data.R")    # course-wide values: threshold, prices, CPI
source("code/model.R")
library(ggplot2)
p <- m02_params()                    # params/mdrtb.csv + shared values
ptab <- attr(p, "table")
inp <- prepare_inputs(p)
ctx <- vn_context()
# The adverse-event cost converts a 2024 study cost back to VND with the study's own rate (25,000).
# Check that it stays within a few percent of the course-wide official 2024 rate (kb/_shared).
fx_2024 <- vn_cpi_fx()$fx_vnd_per_usd[vn_cpi_fx()$year == 2024]
stopifnot(abs(p$fx_vnd_usd / fx_2024 - 1) < 0.05)

Tính chi phí một biến cố bất lợi, rồi chạy mô hình đầy đủ cho ba phác đồ và lấy các giá trị gắn với sự kiện. Kiểm tra: số người hoàn thành điều trị lần đầu (dòng TxD → Khỏi bệnh) bằng 1 trừ tổng kết cục bất lợi, sai lệch dưới 1e-4 (stopifnot).

# Event costs: one severe adverse event = one hospital episode (2024 USD -> 2025 VND)
c_ae <- ae_cost_vnd(p)
c_ae
[1] 18266240
ev <- list()
for (g in REGIMENS) ev[[g]] <- evaluate_regimen(p, g)
event_tab <- data.frame(
  regimen = REGIMENS,
  p_ae = inp$p_ae[REGIMENS],
  cost_ae_million = sapply(ev, function(e) e$totals[["cost_ae"]]) / 1e6,
  qaly_ae = sapply(ev, function(e) e$totals[["qaly_ae"]]),
  completed = sapply(ev, function(e) e$totals[["n_completed"]]),
  entered_retx = sapply(ev, function(e) e$totals[["n_retx"]]),
  cost_eot_million = sapply(ev, function(e) e$totals[["cost_eot"]]) / 1e6
)
print(event_tab, digits = 3, row.names = FALSE)
 regimen p_ae cost_ae_million  qaly_ae completed entered_retx cost_eot_million
    long 0.48            8.77 -0.00600     0.706       0.1029            0.369
   short 0.48            8.77 -0.00600     0.789       0.0737            0.408
   bpalm 0.23            4.20 -0.00288     0.902       0.0258            0.452
# Completions (flow TxD -> Cured) must equal 1 minus the unfavourable outcomes
for (g in REGIMENS) {
  stopifnot(abs(ev[[g]]$totals[["n_completed"]] - (1 - sum(inp$q_unfav[[g]]))) < 1e-4)
}

Chi phí biến cố bất lợi kỳ vọng mỗi người bệnh là 8.8 triệu VNĐ với phác đồ ngắn, so với 4.2 triệu VNĐ với BPaLM. 90.2% người dùng BPaLM hoàn thành đợt đầu, so với 78.9% với phác đồ ngắn và 70.6% với phác đồ dài hạn.

Lập bảng kết quả đầy đủ cho ba phác đồ, với cột chênh lệch cho câu hỏi chính sách: BPaLM so với phác đồ ngắn. Kiểm tra: tổng các cấu phần chi phí bằng chi phí toàn phần (stopifnot); HALY nằm giữa QALY và năm sống.

res <- run_cea(p)
# The cost components must add up to the total cost; retreatment is part of cost_post
stopifnot(all(abs(rowSums(res[, c("cost_tx", "cost_post", "cost_ae", "cost_eot")]) - res$cost) < 1e-6),
          all(res$cost_retx <= res$cost_post + 1e-6))
# The policy question: BPaLM versus the 9-month short regimen (Viet Nam's current standard)
cmp <- compare_regimens(res, new = "bpalm", ref = "short", wtp = p$wtp)
status_vi <- c(dominant = "chiếm ưu thế (rẻ hơn và hiệu quả hơn)",
               dominated = "bị lấn át (đắt hơn và kém hiệu quả hơn)",
               ne = "đắt hơn và hiệu quả hơn: chọn nếu ICER thấp hơn ngưỡng",
               sw = "rẻ hơn nhưng kém hiệu quả hơn: chọn nếu ICER cao hơn ngưỡng",
               equivalent = "tương đương (không có chênh lệch)")
icer_cell <- if (is.na(cmp$icer)) paste("BPaLM", status_vi[[cmp$status]]) else
  paste0(fmt_num(cmp$icer), " (", status_vi[[cmp$status]], ")")
fm <- function(x) fmt_num(x / 1e6, 2)      # million VND
fq <- function(x) fmt_num(x, 3)
col_g <- function(g) c(fm(res[g, "cost"]), fq(res[g, "qaly"]), fq(res[g, "ly"]),
                       fq(res[g, "haly"]), fm(res[g, "nmb"]), "", "", "")
results_tab <- data.frame(
  outcome = c("Chi phí (triệu VNĐ)", "QALY", "Năm sống (LY)", "Năm sống điều chỉnh sức khỏe (HALY)",
              "NMB (triệu VNĐ)", "DALY tránh được (chiết khấu)", "DALY tránh được (không chiết khấu)",
              "ICER (VNĐ/QALY) hoặc trạng thái"),
  long = col_g("long"), short = col_g("short"), bpalm = col_g("bpalm"),
  difference = c(fm(cmp$d_cost), fq(cmp$d_qaly), fq(cmp$d_ly), fq(cmp$dalys_averted), fm(cmp$d_nmb),
                 fq(cmp$dalys_averted), fq(cmp$dalys_averted_undisc), icer_cell)
)
knitr::kable(results_tab, align = "lrrrr",
             col.names = c("Kết quả (chiết khấu, trừ khi ghi khác)", "Phác đồ dài hạn",
                           "Phác đồ ngắn 9 tháng", "BPaLM", "BPaLM − phác đồ ngắn"))
Kết quả (chiết khấu, trừ khi ghi khác) Phác đồ dài hạn Phác đồ ngắn 9 tháng BPaLM BPaLM − phác đồ ngắn
Chi phí (triệu VNĐ) 29.32 21.69 15.02 -6.68
QALY 12.489 12.911 13.390 0.479
Năm sống (LY) 14.038 14.444 14.930 0.486
Năm sống điều chỉnh sức khỏe (HALY) 13.379 14.050 14.720 0.670
NMB (triệu VNĐ) 1,538.11 1,598.62 1,665.42 66.81
DALY tránh được (chiết khấu) 0.670
DALY tránh được (không chiết khấu) 0.934
ICER (VNĐ/QALY) hoặc trạng thái BPaLM chiếm ưu thế (rẻ hơn và hiệu quả hơn)
cmp[c("status", "icer", "d_nmb", "adopt_new")]
$status
[1] "dominant"

$icer
[1] NA

$d_nmb
[1] 66807607

$adopt_new
[1] TRUE

Với bộ tham số cơ sở, so với phác đồ ngắn, BPaLM chiếm ưu thế (rẻ hơn và hiệu quả hơn). Mỗi người bệnh, BPaLM tiết kiệm 6.7 triệu VNĐ, thêm 0.48 QALY và tránh được 0.67 DALY khi chiết khấu (0.93 DALY nếu không chiết khấu). Ở ngưỡng cơ sở, NMB gia tăng là 66.8 triệu VNĐ. Kết quả cùng chiều với đánh giá kinh tế dựa trên TB-PRACTECAL, vốn thấy các phác đồ dựa trên BPaL tiết kiệm chi phí ở các điểm nghiên cứu (Sweeney và c.s. 2025).

Xếp cả ba phác đồ lên đường biên hiệu quả. Kiểm tra: phương án có NMB cao nhất nằm trên đường biên (stopifnot).

# All three strategies at once: dominance, extended dominance, ICERs along the frontier
fr <- cea_frontier(res, wtp = p$wtp)
frontier_vi <- c(frontier = "trên đường biên", dominated = "bị lấn át",
                 ext_dominated = "bị lấn át mở rộng")
fr_show <- data.frame(
  strategy = REGIMEN_LABELS[fr$strategy], cost_million = fr$cost / 1e6, qaly = fr$qaly,
  status = frontier_vi[fr$status], icer = ifelse(is.na(fr$icer), "", fmt_num(fr$icer)),
  nmb_million = fr$nmb / 1e6, optimal = ifelse(fr$optimal, "có NMB cao nhất", "")
)
knitr::kable(fr_show, digits = c(0, 2, 3, 0, 0, 1, 0), row.names = FALSE,
             col.names = c("Chiến lược", "Chi phí (triệu VNĐ)", "QALY", "Trạng thái", "ICER so với điểm trước",
                           "NMB (triệu VNĐ)", "Lựa chọn"))
Chiến lược Chi phí (triệu VNĐ) QALY Trạng thái ICER so với điểm trước NMB (triệu VNĐ) Lựa chọn
BPaLM 15.02 13.390 trên đường biên 1665.4 có NMB cao nhất
Phác đồ ngắn 9 tháng 21.69 12.911 bị lấn át 1598.6
Phác đồ dài hạn 29.32 12.489 bị lấn át 1538.1
best <- fr$strategy[fr$optimal]
n_frontier <- sum(fr$status == "frontier")
# The earlier switch from the long to the short regimen, as a pair
cmp_short_long <- compare_regimens(res, new = "short", ref = "long", wtp = p$wtp)
cmp_short_long[c("status", "d_cost", "d_qaly", "d_nmb")]
$status
[1] "dominant"

$d_cost
[1] -7621728

$d_qaly
[1] 0.4214131

$d_nmb
[1] 60509069
stopifnot(length(best) == 1, best == rownames(res)[which.max(res$nmb)],
          fr[best, "status"] == "frontier")

Đường biên có 1 phương án. Chỉ BPaLM nằm trên đường biên: nó vừa rẻ nhất vừa nhiều QALY nhất, nên lấn át tuyệt đối cả hai phương án kia và không cần ICER nào. Khi chỉ so hai phác đồ cũ, phác đồ ngắn cũng chiếm ưu thế so với phác đồ dài hạn: theo mô hình, việc chuyển sang phác đồ ngắn giai đoạn 2021–2022 là bước đúng hướng. Bài tập 2 cho thấy đường biên thay đổi thế nào khi BPaLM đắt lên.

plane <- data.frame(strategy = factor(REGIMEN_LABELS[rownames(res)], levels = REGIMEN_LABELS),
                    cost = res$cost / 1e6, qaly = res$qaly)
on_frontier <- fr[fr$status == "frontier", ]
fig_plane <- ggplot(plane, aes(qaly, cost))
if (nrow(on_frontier) > 1) {        # the frontier is a line only if two or more strategies are on it
  fig_plane <- fig_plane + geom_line(data = data.frame(qaly = on_frontier$qaly, cost = on_frontier$cost / 1e6),
                                     colour = "grey40", linetype = "22")
}
fig_plane <- fig_plane +
  geom_point(aes(colour = strategy), size = 4) +
  geom_text(aes(label = strategy), vjust = -1.1, size = 3.5) +
  scale_colour_manual(values = pal_course[c(2, 4, 1)], guide = "none") +
  scale_y_continuous(expand = expansion(mult = 0.15)) +
  scale_x_continuous(expand = expansion(mult = 0.15)) +
  labs(x = "QALY mỗi người bệnh (chiết khấu)", y = "Chi phí mỗi người bệnh (triệu VNĐ, 2025)",
       title = "Ba chiến lược trên mặt phẳng chi phí-hiệu quả",
       subtitle = "Góc dưới bên phải là tốt nhất: rẻ hơn và nhiều QALY hơn") +
  theme_course()
save_fig(fig_plane, "l05-plane.png")

Ba chiến lược trên mặt phẳng chi phí-hiệu quả

Tách chi phí theo cấu phần để hiểu kết quả đến từ đâu. Kiểm tra: chiều cao mỗi cột bằng chi phí trong bảng kết quả (stopifnot).

comp_names <- c(cost_tx = "Điều trị lần đầu (thuốc + theo dõi)", cost_retx = "Điều trị lại",
                cost_oth = "Chăm sóc khi thất bại/bỏ điều trị", cost_ae = "Biến cố bất lợi",
                cost_eot = "Đánh giá cuối điều trị")
res$cost_oth <- res$cost_post - res$cost_retx
cost_df <- NULL
for (g in REGIMENS) {
  cost_df <- rbind(cost_df, data.frame(
    regimen = factor(REGIMEN_LABELS[[g]], levels = REGIMEN_LABELS),
    component = factor(comp_names, levels = comp_names),
    million = as.numeric(res[g, names(comp_names)]) / 1e6
  ))
}
stopifnot(all(abs(tapply(cost_df$million, cost_df$regimen, sum) - res$cost / 1e6) < 1e-9))
# Where does the cost difference between the short regimen and BPaLM come from?
comp_diff <- unlist(res["short", names(comp_names)] - res["bpalm", names(comp_names)])
comp_diff <- comp_diff[order(abs(comp_diff), decreasing = TRUE)]
round(comp_diff / 1e6, 2)
  cost_ae   cost_tx cost_retx  cost_eot  cost_oth 
     4.57      1.46      0.69     -0.04      0.00 
# The components add up to the total difference; and the difference without adverse-event costs
stopifnot(abs(sum(comp_diff) + cmp$d_cost) < 1e-6)
d_cost_no_ae <- cmp$d_cost + comp_diff[["cost_ae"]]     # BPaLM minus short regimen, adverse events left out
round(c(d_cost_million = cmp$d_cost / 1e6, d_cost_without_ae_million = d_cost_no_ae / 1e6), 2)
           d_cost_million d_cost_without_ae_million 
                    -6.68                     -2.11 
fig_costs <- ggplot(cost_df, aes(regimen, million, fill = component)) +
  geom_col(width = 0.6) +
  scale_fill_manual(values = pal_course[c(1, 5, 2, 4, 3)], name = NULL) +
  labs(x = NULL, y = "Triệu VNĐ (2025, đã chiết khấu)",
       title = "Chi phí kỳ vọng mỗi người bệnh, theo cấu phần") +
  guides(fill = guide_legend(nrow = 2)) +
  theme_course()
save_fig(fig_costs, "l05-costs.png")

Chi phí kỳ vọng mỗi người bệnh, theo cấu phần

So với phác đồ ngắn, chênh lệch chi phí của BPaLM đến nhiều nhất từ “Biến cố bất lợi” (4.6 triệu VNĐ), rồi đến “Điều trị lần đầu (thuốc + theo dõi)” (1.5 triệu VNĐ); số dương là phần BPaLM rẻ hơn. Điều trị lại chiếm 1.5 triệu VNĐ mỗi người với phác đồ dài hạn và 0.4 triệu VNĐ với BPaLM. Phần chi phí biến cố bất lợi dựa trên một giả định mạnh (một trung vị nằm viện cho mọi biến cố). Nếu bỏ hẳn khoản này, BPaLM vẫn rẻ hơn phác đồ ngắn 2.1 triệu VNĐ mỗi người bệnh.

Vẽ NMB của từng phác đồ theo ngưỡng: ở mỗi ngưỡng, đường cao nhất là lựa chọn tốt nhất. Kiểm tra: mỗi đường cắt trục tung tại \(-C\) và có độ dốc bằng QALY của phác đồ đó.

# NMB of each strategy as the threshold changes: the highest line is the best choice
ctx_tab <- attr(ctx, "table")
wtp_max <- ctx_tab$high[ctx_tab$name == "wtp"]        # upper end of the shared threshold range (3 x GDP per capita)
wtp_grid <- seq(0, wtp_max, length.out = 61)
nmb_df <- NULL
for (g in REGIMENS) {
  nmb_df <- rbind(nmb_df, data.frame(
    wtp_million = wtp_grid / 1e6, strategy = factor(REGIMEN_LABELS[[g]], levels = REGIMEN_LABELS),
    nmb_million = (res[g, "qaly"] * wtp_grid - res[g, "cost"]) / 1e6))
}
best_at_grid <- sapply(wtp_grid, function(w) rownames(res)[which.max(res$qaly * w - res$cost)])
fig_nmb <- ggplot(nmb_df, aes(wtp_million, nmb_million, colour = strategy)) +
  geom_vline(xintercept = p$wtp / 1e6, linetype = "22", colour = "grey40") +
  geom_line(linewidth = 1) +
  scale_colour_manual(values = pal_course[c(2, 4, 1)], name = NULL) +
  labs(x = "Ngưỡng chi phí-hiệu quả (triệu VNĐ/QALY)", y = "NMB mỗi người bệnh (triệu VNĐ)",
       title = "Lợi ích ròng quy ra tiền của ba chiến lược theo ngưỡng",
       subtitle = "Đường đứt: ngưỡng cơ sở = 1 × GDP bình quân đầu người năm 2025") +
  theme_course()
save_fig(fig_nmb, "l05-nmb.png")
table(best_at_grid)
best_at_grid
bpalm 
   61 

NMB của ba phác đồ theo ngưỡng chi phí-hiệu quả

BPaLM có NMB cao nhất ở mọi ngưỡng từ 0 đến 376 triệu VNĐ/QALY.

Phân tích kịch bản đổi từng giả định đã nêu ở Bài 1 và Bài 2: cách áp RR cho tử vong trong điều trị, tử vong bị báo cáo thiếu, đường mô hình hóa tử vong sau khi khỏi, điều trị lại, tử vong sau bỏ điều trị, chi phí ở trạng thái Thất bại, và chi phí biến cố bất lợi khi bỏ phần tiền thuốc. M03 sẽ khảo sát bất định một cách hệ thống. Kiểm tra: dòng “Phân tích cơ sở” trùng bảng kết quả ở trên (stopifnot).

# One assumption changed at a time; results are BPaLM minus the 9-month short regimen
scen <- list()
scen$base <- p
s <- p; s$bpalm_death_rule <- 1; scen$rr_death <- s
s <- p; s$bpalm_death_rule <- 2; scen$same_death <- s
s <- p; s$vn_death <- p$vn_death / (1 - p$death_unreported); scen$underreport <- s
s <- p; s$relapse_years <- ptab$high[ptab$name == "relapse_years"]; s$smr_post <- 1; scen$relapse_explicit <- s
s <- p; s$relapse_years <- p$horizon_age - p$age_start; scen$double_count <- s
s <- p; s$retx_rate_failed <- 0; s$retx_rate_ltfu <- 0; scen$no_retx <- s
s <- p; s$retx_outcome_rule <- 1; scen$retx_georgia <- s
s <- p; s$ltfu_mort_rate <- p$cf_untreated / p$dur_untreated; scen$ltfu_untreated <- s
s <- p; s$c_failed <- 0; scen$c_failed_zero <- s
s <- p; s$c_hosp_usd <- p$c_hosp_usd * (1 - p$hosp_drug_share); scen$ae_no_drug <- s
scen_label <- c(
  base = "Phân tích cơ sở",
  rr_death = "RR áp cho cả tử vong trong điều trị",
  same_death = "Tỷ lệ tử vong tích lũy như phác đồ ngắn",
  underreport = "Tử vong chương trình bị báo cáo thiếu",
  relapse_explicit = paste0("Tái phát tường minh ", ptab$high[ptab$name == "relapse_years"], " năm, SMR = 1"),
  double_count = "Tái phát suốt đời cộng SMR (đếm hai lần)",
  no_retx = "Không có điều trị lại (cấu trúc cũ)",
  retx_georgia = "Điều trị lại kém hiệu quả (Georgia 2011-2014)",
  ltfu_untreated = "Bỏ điều trị tử vong như lao không điều trị",
  c_failed_zero = "Không có chi phí ở trạng thái Thất bại",
  ae_no_drug = "Chi phí biến cố bất lợi không gồm tiền thuốc"
)
status_short <- c(dominant = "chiếm ưu thế", dominated = "bị lấn át", ne = "đắt hơn, hiệu quả hơn",
                  sw = "rẻ hơn, kém hiệu quả hơn", equivalent = "tương đương")
scen_tab <- data.frame(scenario = scen_label[names(scen)], d_cost_million = NA, d_qaly = NA,
                       d_nmb_million = NA, status = NA, best = NA, row.names = names(scen))
scen_codes <- character(0)
for (k in names(scen)) {
  r_k <- run_cea(scen[[k]])
  c_k <- compare_regimens(r_k, new = "bpalm", ref = "short", wtp = p$wtp)
  scen_tab[k, c("d_cost_million", "d_qaly", "d_nmb_million")] <- c(c_k$d_cost / 1e6, c_k$d_qaly, c_k$d_nmb / 1e6)
  scen_tab[k, "status"] <- status_short[[c_k$status]]
  scen_tab[k, "best"] <- REGIMEN_LABELS[[rownames(r_k)[which.max(r_k$nmb)]]]
  scen_codes[k] <- c_k$status
}
all_dominant <- all(scen_codes == "dominant")
# Scenarios that move the QALY gain furthest from the base case
dev <- abs(scen_tab$d_qaly - scen_tab["base", "d_qaly"])
names(dev) <- rownames(scen_tab)
top2 <- names(sort(dev, decreasing = TRUE))[1:2]
round(sort(dev, decreasing = TRUE), 3)
  ltfu_untreated         rr_death       same_death relapse_explicit 
           0.346            0.336            0.268            0.203 
    retx_georgia          no_retx      underreport     double_count 
           0.159            0.140            0.084            0.017 
            base    c_failed_zero       ae_no_drug 
           0.000            0.000            0.000 
# index.qmd and the lesson text say these two assumptions move the result most
stopifnot(setequal(top2, c("rr_death", "ltfu_untreated")))
all_best_bpalm <- all(scen_tab$best == REGIMEN_LABELS[["bpalm"]])
knitr::kable(scen_tab, digits = c(0, 2, 3, 1, 0, 0), row.names = FALSE,
             col.names = c("Kịch bản", "ΔC (triệu VNĐ)", "ΔQALY", "ΔNMB (triệu VNĐ)", "Trạng thái", "NMB cao nhất"))
Kịch bản ΔC (triệu VNĐ) ΔQALY ΔNMB (triệu VNĐ) Trạng thái NMB cao nhất
Phân tích cơ sở -6.68 0.479 66.8 chiếm ưu thế BPaLM
RR áp cho cả tử vong trong điều trị -6.56 0.816 108.9 chiếm ưu thế BPaLM
Tỷ lệ tử vong tích lũy như phác đồ ngắn -6.77 0.211 33.2 chiếm ưu thế BPaLM
Tử vong chương trình bị báo cáo thiếu -6.55 0.563 77.2 chiếm ưu thế BPaLM
Tái phát tường minh 5 năm, SMR = 1 -6.61 0.682 92.2 chiếm ưu thế BPaLM
Tái phát suốt đời cộng SMR (đếm hai lần) -6.51 0.462 64.5 chiếm ưu thế BPaLM
Không có điều trị lại (cấu trúc cũ) -6.00 0.619 83.7 chiếm ưu thế BPaLM
Điều trị lại kém hiệu quả (Georgia 2011-2014) -6.35 0.638 86.4 chiếm ưu thế BPaLM
Bỏ điều trị tử vong như lao không điều trị -6.51 0.825 110.0 chiếm ưu thế BPaLM
Không có chi phí ở trạng thái Thất bại -6.67 0.479 66.8 chiếm ưu thế BPaLM
Chi phí biến cố bất lợi không gồm tiền thuốc -4.94 0.479 65.1 chiếm ưu thế BPaLM
stopifnot(abs(scen_tab["base", "d_qaly"] - cmp$d_qaly) < 1e-12,
          scen_tab["no_retx", "d_qaly"] > scen_tab["base", "d_qaly"],          # retreatment narrows the gain
          scen_tab["ltfu_untreated", "d_qaly"] > scen_tab["base", "d_qaly"],
          scen_tab["same_death", "d_qaly"] < scen_tab["base", "d_qaly"],
          scen_tab["rr_death", "d_qaly"] > scen_tab["base", "d_qaly"])

Chênh lệch QALY của BPaLM so với phác đồ ngắn đi từ 0.21 đến 0.82. Hai kịch bản kéo kết quả xa phân tích cơ sở nhất là “Bỏ điều trị tử vong như lao không điều trị” và “RR áp cho cả tử vong trong điều trị”. Riêng cách áp RR cho tử vong trong điều trị đã tạo ra cả một dải: 0.21 QALY nếu BPaLM giữ tỷ lệ tử vong tích lũy của phác đồ ngắn, 0.48 ở phân tích cơ sở, 0.82 khi áp RR cho cả tử vong. Bỏ nhánh điều trị lại làm lợi ích tăng lên 0.62 QALY: điều trị lại “cứu” một phần người thất bại hoặc bỏ dở phác đồ ngắn, nên mô hình không có nhánh này phóng đại lợi ích của phác đồ đầu tiên tốt hơn. Cho người bỏ điều trị tử vong như lao không điều trị cũng phóng đại lợi ích (0.82 QALY). Trong mọi kịch bản, BPaLM vẫn chiếm ưu thế so với phác đồ ngắn: chênh lệch chi phí luôn âm, từ -6.8 đến -4.9 triệu VNĐ, vì khoản tiết kiệm chủ yếu đến từ tháng thuốc, theo dõi và biến cố bất lợi, những thứ mà hầu hết các giả định này không chạm tới.

Chạy bộ kiểm tra xác minh. Kiểm tra: mọi dòng có pass = TRUE; stopifnot dừng script nếu có dòng sai.

vt <- verify_model(p)
knitr::kable(vt, col.names = c("Kiểm tra", "Đạt", "Cách kiểm"))
Kiểm tra Đạt Cách kiểm
Ma trận hợp lệ ở mọi chu kỳ: long TRUE 0 <= p <= 1, tổng hàng = 1
Bảo toàn đoàn hệ: long TRUE tổng mỗi hàng của vết = 1
Tái tạo kết quả cuối điều trị: long TRUE dòng ra khỏi Tx1..TxD = tỷ lệ đầu vào
Tái tạo kết quả khi tuổi bắt đầu + 4: long TRUE tử vong nền đổi nhóm tuổi trong khi điều trị
Ma trận hợp lệ ở mọi chu kỳ: short TRUE 0 <= p <= 1, tổng hàng = 1
Bảo toàn đoàn hệ: short TRUE tổng mỗi hàng của vết = 1
Tái tạo kết quả cuối điều trị: short TRUE dòng ra khỏi Tx1..TxD = tỷ lệ đầu vào
Tái tạo kết quả khi tuổi bắt đầu + 4: short TRUE tử vong nền đổi nhóm tuổi trong khi điều trị
Ma trận hợp lệ ở mọi chu kỳ: bpalm TRUE 0 <= p <= 1, tổng hàng = 1
Bảo toàn đoàn hệ: bpalm TRUE tổng mỗi hàng của vết = 1
Tái tạo kết quả cuối điều trị: bpalm TRUE dòng ra khỏi Tx1..TxD = tỷ lệ đầu vào
Tái tạo kết quả khi tuổi bắt đầu + 4: bpalm TRUE tử vong nền đổi nhóm tuổi trong khi điều trị
Chu kỳ 1 khớp phép tính tay TRUE Tx1 -> Tx2 và Tx1 -> Thất bại tính từ tham số
Số tháng thuốc được tính tiền: long TRUE = 18 tháng trừ tử vong nền
Số tháng thuốc được tính tiền: short TRUE = 9 tháng trừ tử vong nền
Số tháng thuốc được tính tiền: bpalm TRUE = 6 tháng trừ tử vong nền
Mọi chi phí = 0 thì tổng chi phí = 0 TRUE giá trị cực trị
Thỏa dụng = 1 thì QALY = năm sống TRUE giá trị cực trị
Chiết khấu 0% thì có chiết khấu = không chiết khấu TRUE chi phí, QALY và năm sống
Hai phác đồ giống hệt thì chênh lệch = 0 TRUE phác đồ dài hạn = phác đồ ngắn
Không có rủi ro lao: tuổi thọ = tính độc lập từ bảng sống TRUE so với tính tay từ bảng sống
Thành công thấp hơn thì QALY thấp hơn TRUE kiểm tra chiều tác động
RR bất lợi lớn hơn thì BPaLM có ít QALY hơn TRUE kiểm tra chiều tác động
Điều trị lại tái tạo tỷ lệ kết cục đầu vào TRUE xác suất rời ReTx theo từng lối ra
Không điều trị lại thì không ai vào ReTx TRUE vết của ReTx = 0
NMB gia tăng nhất quán TRUE ΔNMB = λΔQ − ΔC
Nhãn góc phần tư đúng cho 5 bảng giả lập TRUE ICER và NMB cho cùng quyết định
Đường biên hiệu quả đúng trên bảng giả lập TRUE lấn át, lấn át mở rộng; ICER đường biên và NMB chọn cùng phương án
stopifnot(all(vt$pass))

Cả 28 kiểm tra đều đạt. Điều này nói rằng code làm đúng thiết kế, không nói rằng thiết kế đúng: thẩm định cần dữ liệu độc lập (Bài 4, bài tập 3) và phân tích bất định (M03).

Mẹo thực hành
  • Tách chi phí theo cấu phần ngay trong hàm đánh giá: bảng cấu phần giúp bắt lỗi và giải thích kết quả cho người ra quyết định.
  • Với ba phương án trở lên, trình bày đường biên hiệu quả trước, rồi mới đến từng cặp so sánh mà người ra quyết định quan tâm.
  • Gom các kiểm tra vào một hàm (verify_model()) và chạy lại sau mỗi lần sửa mô hình. Mỗi khi tìm được một lỗi, thêm một kiểm tra bắt được lỗi đó.
  • Đánh dấu giá trị minh họa ngay trong bảng tham số; khi trình bày, nói rõ kết quả nào phụ thuộc vào chúng.
Lỗi thường gặp
  • So mọi phương án với phương án tệ nhất: phác đồ nào cũng trông “đáng tiền” so với phác đồ dài hạn. Dùng đường biên.
  • Đọc ICER không kèm góc phần tư: ở góc tây nam, quy tắc quyết định đảo ngược.
  • Đếm sự kiện hai lần: vừa gắn chi phí biến cố vào trạng thái vừa gắn vào lần chuyển; hoặc tính tiền thuốc cả trong chi phí nằm viện lẫn trong chi phí điều trị hằng tháng.
  • Dùng trung vị chi phí thay cho trung bình: đánh giá kinh tế cần giá trị kỳ vọng; với chi phí lệch phải, trung vị thường thấp hơn trung bình. Chi phí biến cố bất lợi của module là một trung vị như vậy.

Bài tập

  1. (Dễ) Ở ngưỡng bằng 0, NMB gia tăng của BPaLM so với phác đồ ngắn bằng bao nhiêu? Diễn giải.
# Exercise 1: at a threshold of zero, the NMB difference is minus the cost difference
d_nmb_0 <- cmp$d_qaly * 0 - cmp$d_cost
round(c(d_nmb_at_0_million = d_nmb_0 / 1e6, minus_d_cost_million = -cmp$d_cost / 1e6), 3)
  d_nmb_at_0_million minus_d_cost_million 
               6.677                6.677 

Khi \(\lambda = 0\), sức khỏe không được định giá, nên \(\Delta\text{NMB} = -\Delta C\): phương án rẻ hơn thắng. Đây là góc nhìn thuần ngân sách.

  1. (Vừa) Giá thuốc BPaLM mỗi tháng phải cao đến đâu thì BPaLM không còn tốt hơn phác đồ ngắn ở ngưỡng cơ sở? Dùng uniroot(). Sau đó vẽ lại đường biên khi giá thuốc BPaLM gấp ba.
# Exercise 2: threshold price — monthly BPaLM drug cost at which BPaLM stops beating the
# short regimen at the base threshold
d_nmb_at_price <- function(price) {
  q <- p
  q$c_drug_bpalm <- price
  compare_regimens(run_cea(q, regimens = c("short", "bpalm")), new = "bpalm", ref = "short", wtp = p$wtp)$d_nmb
}
price_threshold <- uniroot(d_nmb_at_price, lower = 0, upper = 100 * p$c_drug_bpalm)$root
round(c(base_price = p$c_drug_bpalm, threshold_price = price_threshold,
        ratio = price_threshold / p$c_drug_bpalm,
        course_usd_at_threshold = price_threshold * p$dur_bpalm / ctx$fx_vnd_per_usd), 2)
             base_price         threshold_price                   ratio 
             1181913.33             12869341.69                   10.89 
course_usd_at_threshold 
                3092.35 
# The frontier when BPaLM drugs cost three times the GDF price
p3x <- p
p3x$c_drug_bpalm <- 3 * p$c_drug_bpalm
fr3 <- cea_frontier(run_cea(p3x), wtp = p$wtp)
fr3[, c("strategy", "cost", "qaly", "status", "compared_with", "icer", "optimal")]
      strategy     cost     qaly    status compared_with     icer optimal
short    short 21693905 12.91084  frontier          <NA>       NA   FALSE
bpalm    bpalm 28528782 13.38997  frontier         short 14265282    TRUE
long      long 29315633 12.48943 dominated          <NA>       NA   FALSE

Giá ngưỡng là 12,869,342 VNĐ mỗi tháng, gấp 10.9 lần giá của Global Drug Facility, tức khoảng US$3,092 cho cả đợt. Khoảng đệm lớn như vậy vì BPaLM tiết kiệm tháng theo dõi, biến cố bất lợi và điều trị lại, đồng thời thêm QALY được định giá ở 125,500,000 VNĐ/QALY. Khi giá gấp ba, đường biên gồm Phác đồ ngắn 9 tháng và BPaLM; ICER của BPaLM so với phác đồ ngắn là 14.3 triệu VNĐ/QALY, thấp hơn ngưỡng, nên BPaLM vẫn được chọn.

  1. (Khó) Viết một kiểm tra hộp đen của riêng bạn. Người vào trạng thái Thất bại có thể đến từ các tháng điều trị lần đầu, từ điều trị lại và, chỉ khi bật tái phát, từ Khỏi bệnh. Dòng vào từ các tháng điều trị lần đầu phải bằng đại lượng nào? Dòng từ Khỏi bệnh phải bằng bao nhiêu ở phân tích cơ sở, và khi bật tái phát suốt đời?
# Exercise 3: your own black-box test. Failed can be entered from first-line treatment
# (Tx1..TxD), from retreatment (ReTx) and, only when relapse is switched on, from Cured.
# Inflow from the first-line months must equal the failure input; inflow from Cured must
# be zero in the base case and positive with lifetime relapse.
inflow_failed <- function(e, from) {
  total <- 0
  for (s in from) total <- total + sum(transition_flow(e$trace, e$P, s, "Failed"))
  total
}
tx_states <- paste0("Tx", seq_len(p$dur_short))
ev_rel <- evaluate_regimen(scen$double_count, "short")
in_tab <- c(from_tx_base = inflow_failed(ev$short, tx_states),
            failure_input = inp$q_unfav$short[["failure"]],
            from_retx_base = inflow_failed(ev$short, "ReTx"),
            from_cured_base = inflow_failed(ev$short, "Cured"),
            from_cured_relapse = inflow_failed(ev_rel, "Cured"))
round(in_tab, 5)
      from_tx_base      failure_input     from_retx_base    from_cured_base 
           0.03071            0.03071            0.00316            0.00000 
from_cured_relapse 
           0.19376 
stopifnot(abs(in_tab[["from_tx_base"]] - in_tab[["failure_input"]]) < 1e-4,
          in_tab[["from_cured_base"]] == 0, in_tab[["from_cured_relapse"]] > 0)

Với phác đồ ngắn, dòng vào Thất bại từ các tháng điều trị lần đầu là 3.07%, đúng bằng tỷ lệ thất bại đầu vào; điều trị lại thêm 0.32%. Dòng từ Khỏi bệnh bằng 0 ở phân tích cơ sở và là 19.4% khi bật tái phát suốt đời. Một kiểm tra như vậy bắt được lỗi “rò rỉ” vào một trạng thái từ nơi không có mũi tên trên sơ đồ.

Tài liệu tham khảo

# Slide Lời giảng chính Hình/Code
1 Câu hỏi của tỉnh phác đồ ngắn hay BPaLM; 6.7 triệu VNĐ mỗi người bệnh callout
2 Trạng thái và sự kiện dòng chuyển \(F_t = s_{t-1,i}P_t[i,j]\); chiết khấu đúng thời điểm công thức
3 Biến cố bất lợi 23% so với 48%; một đợt nằm viện 18,266,240 VNĐ chunk l05-events
4 Bảng kết quả ba phác đồ; cột BPaLM − phác đồ ngắn chunk l05-results
5 Bốn góc phần tư ICER luôn kèm góc; NMB đúng ở mọi góc bảng góc phần tư
6 Đường biên hiệu quả sắp theo chi phí; lấn át; lấn át mở rộng; ICER tăng dần figs/l05-plane.png
7 Chi phí đến từ đâu thuốc, theo dõi, biến cố bất lợi, điều trị lại figs/l05-costs.png
8 NMB theo ngưỡng đường cao nhất thắng figs/l05-nmb.png
9 Kịch bản cách áp RR cho tử vong; điều trị lại làm lợi ích nhỏ lại chunk l05-scenarios
10 Xác minh mô hình 28 kiểm tra hộp đen; kiểm tra phải có thể thất bại chunk l05-verify
11 Chuyển tiếp kết quả chắc chắn đến đâu? → M03 —