สร้างแบบจำลองมาร์คอฟแบบกลุ่มประชากรทีละขั้น: ติดตามประชากรผ่านเวลา

Health EconomicsClinical Epidemiology ResearchMethodology and Research Design TH
สร้างแบบจำลองมาร์คอฟแบบกลุ่มประชากรทีละขั้น: ติดตามประชากรผ่านเวลา
On this page

Read the English version

บทคัดย่อ

คลินิกระดับอำเภอแห่งหนึ่งดูแลผู้ที่เป็นเบาหวาน 1,000 คนซึ่งยังไม่มีโรคไต สำนักงานสาธารณสุขต้องการคำพยากรณ์ล่วงหน้าสี่ปี ว่าเมื่อถึงตอนนั้นจะมีกี่คนที่เป็นโรคไต กี่คนที่เสียชีวิตไปแล้ว และกลุ่มนี้จะมีต้นทุนเท่าใดและจะมีผลทางสุขภาพเท่าใด บทความนี้สร้างการพยากรณ์นั้นทีละขั้น ขั้นแรก ตั้งชื่อสามสถานะ คือสุขภาพดี ป่วย และเสียชีวิต แล้วเขียนโอกาสของการย้ายแต่ละทางในหนึ่งปี จากนั้นย้ายคนทั้งกลุ่มทีละปี หลังปีแรกยังสุขภาพดี 800 คน ป่วย 150 คน และเสียชีวิต 50 คน โดยยอดรวมเท่าเดิม ขั้นสุดท้าย เลือกว่าจะนับค่าของแต่ละปีจากจำนวนคนตอนไหน ถ้านับเฉพาะตอนท้ายปีแรกจะได้ปีที่สุขภาพสมบูรณ์ 0.890 ปีต่อคน ถ้าเฉลี่ยตอนต้นกับตอนท้ายปีจะได้ 0.945 เพราะคนออกจากสถานะสุขภาพดีตลอดทั้งปี กล่อง R สั้น ๆ คำนวณตัวเลขทุกตัวซ้ำ


อ่านกรอบสามกรอบจากบนลงล่าง โอกาสรายปีย้ายคนทั้งกลุ่ม กลุ่มที่ย้ายแล้วให้จำนวนคนของแต่ละปี และค่าเฉลี่ยของจำนวนตอนต้นกับตอนท้ายปีคือค่าที่ใช้นับปีนั้น ตัวเลขทุกตัวมีที่มาในหัวข้อด้านล่าง

คลินิกหนึ่งแห่ง คน 1,000 คน และการพยากรณ์สี่ปี

คลินิกระดับอำเภอแห่งหนึ่งดูแลผู้ที่เป็นเบาหวานชนิดที่ 2 จำนวน 1,000 คนซึ่งยังไม่มีโรคไต สำนักงานสาธารณสุขอำเภอวางแผนงบประมาณล่วงหน้าสี่ปี และถามผู้จัดการคลินิกสามข้อ

ข้อแรก เมื่อครบสี่ปี คนกลุ่มนี้จะเป็นโรคไตกี่คนและเสียชีวิตไปแล้วกี่คน ข้อที่สอง คลินิกจะมีต้นทุนกับพวกเขาเท่าใด ข้อที่สาม กลุ่มนี้จะมีชีวิตที่สุขภาพดีเท่าใด

เวชระเบียนของคลินิกบอกโอกาสรายปี ซึ่งในที่นี้เป็นข้อมูลสมมติเพื่อการสอน ในแต่ละปี คนที่ยังไม่มีโรคไตจะเป็นโรคไตในสัดส่วน 0.15 และเสียชีวิตในสัดส่วน 0.05 ส่วนคนที่เป็นโรคไตแล้วจะเสียชีวิตในสัดส่วน 0.15 ต่อปี และไม่มีใครหายจากโรคไต

การตรวจรายปีทำให้คลินิกมีต้นทุนคนละ 100 บาทต่อปีสำหรับคนที่ไม่มีโรคไต และคนละ 400 บาทต่อปีสำหรับคนที่เป็นโรคไต การคูณคน 1,000 คนด้วยสี่ปีตอบคำถามเหล่านี้ไม่ได้ เพราะกลุ่มเปลี่ยนไปทุกปี บางคนย้ายจากสุขภาพดีไปป่วย และบางคนเสียชีวิต แบบจำลองจึงต้องย้ายคนก่อน แล้วจึงนับต้นทุนและสุขภาพของพวกเขาทีหลัง

ตั้งชื่อสถานะและโอกาสรายปี

เริ่มจากตั้งชื่อภาวะที่คนหนึ่งคนอยู่ได้ในแต่ละขณะ ในที่นี้มีสามภาวะ คือสุขภาพดี หมายถึงเป็นเบาหวานแต่ไม่มีโรคไต ป่วย หมายถึงเป็นเบาหวานและมีโรคไต และเสียชีวิต แต่ละคนอยู่ในภาวะใดภาวะหนึ่งเพียงภาวะเดียวในแต่ละขณะ และภาวะเหล่านี้คือสถานะสุขภาพ (health state) ของแบบจำลอง ชื่อสั้นสองชื่อแรกใช้เฉพาะในบทความนี้ สุขภาพดีไม่ได้แปลว่าหายจากเบาหวาน และป่วยหมายถึงมีโรคไตเพิ่มเข้ามา

แบบจำลองเดินทีละช่วงคงที่ช่วงละหนึ่งปี และแต่ละช่วงเรียกว่ารอบ (cycle) ในหนึ่งรอบ คนสุขภาพดีมีโอกาสคงสุขภาพดี 0.80 ป่วย 0.15 และเสียชีวิต 0.05 คนป่วยมีโอกาสคงป่วย 0.85 และเสียชีวิต 0.15 ส่วนคนที่เสียชีวิตก็อยู่ในสถานะนั้นต่อไป สถานะเสียชีวิตไม่มีทางออก จึงเรียกว่าสถานะดูดกลืน (absorbing state)

ความน่าจะเป็นของการเปลี่ยนสถานะ (transition probability) คือโอกาสย้ายระหว่างสองสถานะภายในหนึ่งรอบ ซึ่งแปลงมาจากอัตราโดยคงเวลาไว้ในการคำนวณ อัตรา 0.12 ต่อปีในรอบยาว 0.25 ปี ให้ 0.0296 ไม่ใช่ทางลัด 0.0300 อ่านบทเรียนเต็มได้ที่ อัตรา ความน่าจะเป็น และความน่าจะเป็นของการเปลี่ยนสถานะ: คำนวณโดยไม่หลงหน่วยเวลา ในที่นี้หนึ่งรอบคือหนึ่งปี และโอกาสของคลินิกเป็นรายปีอยู่แล้ว จึงไม่ต้องแปลงค่า

เขียนโอกาสเหล่านี้เป็นตาราง แต่ละแถวคือสถานะที่คนอยู่ตอนนี้ และแต่ละคอลัมน์คือสถานะที่คนอยู่เมื่อผ่านไปหนึ่งรอบ แถวหนึ่งแบ่งคนทุกคนที่เริ่มในสถานะนั้นออกไป โอกาสในแถวจึงต้องรวมได้ 1 คือ 0.80 + 0.15 + 0.05 สำหรับสุขภาพดี และ 0.85 + 0.15 สำหรับป่วย แถวที่รวมได้มากหรือน้อยกว่า 1 จะสร้างคนขึ้นมาหรือทำคนหายไป

โอกาสรายปี จากแต่ละสถานะ (แถว) ไปยังแต่ละสถานะ (คอลัมน์)

จากไปสุขภาพดีไปป่วยไปเสียชีวิต
สุขภาพดี0.800.150.05
ป่วย00.850.15
เสียชีวิต001
ไล่ดูลูกศรที่ออกจากแต่ละวง รวมลูกศรที่วนกลับเข้าวงเดิมด้วย โอกาสบนลูกศรทั้งหมดรวมกันได้ 1 ทุกคนในสถานะหนึ่งจึงไปที่ใดที่หนึ่ง ไม่มีใครหายไปหรือเกิดเพิ่ม ไม่มีลูกศรออกจากเสียชีวิตและไม่มีลูกศรกลับเข้าสุขภาพดี แปลว่าจำนวนผู้เสียชีวิตมีแต่เพิ่ม และจำนวนคนสุขภาพดีมีแต่ลดลง

ย้ายคนทั้งกลุ่มทีละปี

คนทั้ง 1,000 คนเริ่มที่สถานะสุขภาพดี ในปีแรก โอกาสในแถวสุขภาพดีแบ่งพวกเขาออกไป คือ 1,000 × 0.80 = 800 คนคงสุขภาพดี, 1,000 × 0.15 = 150 คนป่วย และ 1,000 × 0.05 = 50 คนเสียชีวิต จำนวนทั้งสามรวมกันได้ 1,000 อีกครั้ง จึงไม่มีใครหายไป

ปีที่สองเริ่มจากจำนวนเหล่านี้ และตอนนี้มีสองกลุ่มที่ย้ายพร้อมกัน สถานะป่วยรับคนจากสองทาง คือผู้ป่วยรายใหม่จากคนสุขภาพดี 800 คน และคนป่วยที่ยังป่วยอยู่ ตัวอย่างคำนวณด้านล่างรวมทั้งสองส่วนนี้

วิธีติดตามคนแบบนี้มีชื่อเรียก แบบจำลองมาร์คอฟ (Markov model) คือการติดตามกลุ่มคนที่ย้ายไปมาระหว่างสถานะสุขภาพไม่กี่สถานะ ทีละรอบ โดยที่คนจะไปไหนต่อขึ้นกับสถานะปัจจุบันเท่านั้น ไม่ขึ้นกับว่ามาถึงสถานะนั้นได้อย่างไร และเพราะติดตามคนทั้งกลุ่ม (cohort) ไม่ใช่ทีละคน จึงเรียกว่าแบบจำลองมาร์คอฟแบบกลุ่มประชากร (Markov cohort model)

ตัวอย่างคำนวณ: ปีที่สอง

ปีที่สองเริ่มจากสุขภาพดี 800 คน ป่วย 150 คน และเสียชีวิต 50 คน แต่ละจำนวนถูกแบ่งตามแถวโอกาสของตัวเอง แล้วแต่ละสถานะรวมคนที่รับเข้ามา

  1. สถานะสุขภาพดีเก็บคนไว้ 0.80 ของจำนวนเดิม

    \[ 800 \times 0.80 = 640 \]

    ไม่มีใครกลับมาจากสถานะป่วย สถานะสุขภาพดีจึงมีแต่เสียคน เหลือ 640 คน

  2. สถานะป่วยรับผู้ป่วยรายใหม่และเก็บคนเดิมไว้ส่วนใหญ่

    \[ 800 \times 0.15 + 150 \times 0.85 = 120 + 127.5 = 247.5 \]

    คนสุขภาพดี 120 คนป่วยในปีนี้ และ 127.5 คนจาก 150 คนของปีก่อนยังป่วยอยู่

  3. สถานะเสียชีวิตเก็บทุกคนไว้และรับเพิ่มจากทั้งสองสถานะ

    \[ 50 + 800 \times 0.05 + 150 \times 0.15 = 50 + 40 + 22.5 = 112.5 \]

    50 คนที่เสียชีวิตในปีแรกยังอยู่ในสถานะเดิม และในปีที่สองคนสุขภาพดี 40 คนกับคนป่วย 22.5 คนเสียชีวิต

  4. ตรวจยอดรวม

    \[ 640 + 247.5 + 112.5 = 1000 \]

    ยอดรวมยังเท่ากับ 1,000 คน ปีนี้จึงย้ายคนโดยไม่ทำให้ใครหายไปหรือเกิดเพิ่ม

ผลลัพธ์: หลังสองปี คลินิกมีคนสุขภาพดี 640 คน ป่วย 247.5 คน และเสียชีวิต 112.5 คน จำนวนอย่าง 247.5 เป็นค่าเฉลี่ยของกลุ่มแบบนี้หลาย ๆ กลุ่ม ไม่ใช่คนครึ่งคน

ตารางจำนวนคนรายปี

ทำขั้นเดิมซ้ำ กลุ่มก็จะย้ายต่อไปเรื่อย ๆ ตารางจำนวนคนเมื่อสิ้นแต่ละปีเรียกว่า cohort trace ไล่ลงตามคอลัมน์สุขภาพดี จำนวนลดลงด้วยตัวคูณ 0.80 ทุกปี จาก 1,000 เหลือ 409.6 เมื่อครบสี่ปี สถานะป่วยเพิ่มเป็น 337.219 และสถานะเสียชีวิตมีแต่เพิ่ม เป็น 253.181 ขณะที่ทุกแถวยังรวมได้ 1,000 คน

cohort trace: จำนวนคนในแต่ละสถานะเมื่อสิ้นแต่ละปี

สิ้นปีที่สุขภาพดีป่วยเสียชีวิตรวม
0 (เริ่มต้น)1,000001,000
1800150501,000
2640247.5112.51,000
3512306.375181.6251,000
4409.6337.219253.1811,000

ตรวจ cohort trace ด้วย R

โค้ด R trace.R
# 1,000 healthy people move between Healthy, Sick and Dead, one year per cycle.
# Writes: nothing to disk; the results print in the console.
start <- c(1000, 0, 0)
transition <- c(0.80, 0.15, 0.05, 0, 0.85, 0.15, 0, 0, 1)
cycles <- 4
moves <- matrix(transition, nrow = 3, byrow = TRUE)
trace <- matrix(start, nrow = cycles + 1, ncol = 3, byrow = TRUE)
for (year in seq_len(cycles)) trace[year + 1, ] <- trace[year, ] %*% moves
flows <- trace[2, ] * moves
cat("Row sums of the chances:", rowSums(moves), "| totals, years 0 to 4:", rowSums(trace), "\n")
cat("Year 2 flows from Healthy | Sick | Dead:", flows[1, ], "|", flows[2, ], "|", flows[3, ], "\n")
cat("Healthy, years 0 to 4:", round(trace[, 1], 3), "\n")
cat("Sick, years 0 to 4:", round(trace[, 2], 3), "\n")
cat("Dead, years 0 to 4:", round(trace[, 3], 3), "\n")
ผลลัพธ์จากการรัน trace.log
Row sums of the chances: 1 1 1 | totals, years 0 to 4: 1000 1000 1000 1000 1000
Year 2 flows from Healthy | Sick | Dead: 640 120 40 | 0 127.5 22.5 | 0 0 50
Healthy, years 0 to 4: 1000 800 640 512 409.6
Sick, years 0 to 4: 0 150 247.5 306.375 337.219
Dead, years 0 to 4: 0 50 112.5 181.625 253.181

ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด building-a-markov-cohort-model-step-by-step.R

สคริปต์ฉบับเต็ม
# Building a Markov Cohort Model Step by Step: Follow the Cohort Through Time
# Follow 1,000 people through Healthy, Sick and Dead for four years, then count health and cost.
# ติดตามคน 1,000 คนผ่านสถานะสุขภาพดี ป่วย และเสียชีวิต สี่รอบรายปี แล้วนับสุขภาพและต้นทุน
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# ggplot2 4.0.3 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
library(tibble)  # Write the states and their yearly values as an explicit table.
library(dplyr)  # Add the per-cycle values to that table.
library(ggplot2)  # Display the trace without saving files.
stopifnot(getRversion() >= "4.5.2")

#%%@note 1. Define the illustrative inputs
# Every input is synthetic and written out once, in the order the article uses it.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
start <- c(1000, 0, 0)
transition <- c(0.80, 0.15, 0.05, 0, 0.85, 0.15, 0, 0, 1)
cycles <- 4
first_start <- c(1000, 0, 0)
first_end <- c(800, 150, 50)
health_value <- c(1, 0.6, 0)
yearly_cost <- c(100, 400, 0)
rate <- 0.03
average_by_year <- c(945, 839.25, 742.1625, 653.878125)
rate_per_year <- 0.12
cycle_years <- 0.25
years_in_state <- 10
state_value <- 0.70
future_cost <- 1834
cycle_length <- 1
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
states <- tibble::tibble(state = c("Healthy", "Sick", "Dead"), health = health_value,
  cost = yearly_cost) |>
  dplyr::mutate(health_per_cycle = health * cycle_length, cost_per_cycle = cost * cycle_length)
moves <- matrix(transition, nrow = 3, byrow = TRUE)
stopifnot(all(abs(rowSums(moves) - 1) < 1e-9))

#%%@note 2. Move the cohort one year at a time
# Multiply the counts by the chances each year, and check that the total stays at 1,000.
# คูณจำนวนคนด้วยโอกาสย้ายสถานะทีละปี และตรวจว่ายอดรวมยังเท่ากับ 1,000
trace <- matrix(start, nrow = cycles + 1, ncol = 3, byrow = TRUE)
for (year in seq_len(cycles)) trace[year + 1, ] <- trace[year, ] %*% moves
stopifnot(all(abs(rowSums(trace) - sum(start)) < 1e-9))
flows <- trace[2, ] * moves
people <- trace

#%%@note 3. Count each year at its start, at its end and as the average
# The end count of one year is the start count of the next, so the same trace gives all three.
# จำนวนตอนท้ายปีหนึ่งคือจำนวนตอนต้นปีถัดไป trace เดียวจึงให้การนับได้ทั้งสามแบบ
counts <- rbind(first_start, first_end)
health <- c(counts %*% health_value, mean(counts %*% health_value))
spend <- c(counts %*% yearly_cost, mean(counts %*% yearly_cost))
health_edges <- people %*% health_value
at_start <- health_edges[1:4]
at_end <- health_edges[2:5]
average <- (at_start + at_end) / 2

#%%@note 4. Bring later years back to today
# Divide each year's value by 1.03 once for every year that has passed by its end.
# หารค่าของแต่ละปีด้วย 1.03 หนึ่งครั้งต่อทุกปีที่ผ่านไปเมื่อสิ้นปีนั้น
factors <- (1 + rate)^(1:4)
rewards <- cbind(health = states$health_per_cycle, cost = states$cost_per_cycle)
edges <- people %*% rewards
ways <- list(start = edges[1:4, ], end = edges[2:5, ], average = (edges[1:4, ] + edges[2:5, ]) / 2)
today <- cbind(start = colSums(ways$start / factors), end = colSums(ways$end / factors),
  average = colSums(ways$average / factors))

#%%@note 5. Rework the three foundation examples
# A yearly rate becomes a chance per cycle, a weight turns years into health, a later cost shrinks.
# อัตรารายปีกลายเป็นโอกาสต่อรอบ ค่าน้ำหนักเปลี่ยนปีเป็นสุขภาพ และต้นทุนในอนาคตมีค่าน้อยลงในวันนี้
cycle_chance <- signif(1 - exp(-rate_per_year * cycle_years), 3)
shortcut <- formatC(rate_per_year * cycle_years, format = "f", digits = 4)
weighted_years <- formatC(years_in_state * c(state_value, 1 - state_value), format = "f",
  digits = 1)

#%%@note 6. Display the trace
# Plot the number of people in each state at the end of each year, no graphics file is saved.
# แสดงจำนวนคนในแต่ละสถานะเมื่อสิ้นแต่ละปี โดยไม่บันทึกไฟล์ภาพ
trace_table <- tibble::tibble(year = rep(0:cycles, times = 3),
  state = rep(states$state, each = cycles + 1), people = as.vector(trace))
print(ggplot2::ggplot(trace_table, ggplot2::aes(x = year, y = people, colour = state)) +
  ggplot2::geom_line())

#%%@note 7. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Row sums of the chances:", rowSums(moves), "| totals, years 0 to 4:", rowSums(trace), "\n")
cat("Year 2 flows from Healthy | Sick | Dead:", flows[1, ], "|", flows[2, ], "|", flows[3, ], "\n")
cat("Healthy, years 0 to 4:", round(trace[, 1], 3), "\n")
cat("Sick, years 0 to 4:", round(trace[, 2], 3), "\n")
cat("Dead, years 0 to 4:", round(trace[, 3], 3), "\n")
cat("Year 1 health, start | end | average:", health, "\n")
cat("Year 1 health per person:", sprintf("%.3f", health / 1000), "\n")
cat("Year 1 cost THB, start | end | average:", big(spend), "\n")
cat("Health by year, start:", at_start, "| total:", sum(at_start), "\n")
cat("Health by year, end:", round(at_end, 3), "| total:", round(sum(at_end), 3), "\n")
cat("Health by year, average:", round(average, 2), "| total:", round(sum(average), 2), "\n")
cat("Rate:", paste0(100 * rate, "%"), "| factors:",
  formatC(factors, format = "f", digits = 4), "\n")
cat("Average health by year, today's value:", round(average_by_year / factors, 1),
  "| total:", round(sum(average_by_year / factors), 3), "\n")
cat("Four-year health today, start | end | average:", sprintf("%.3f", today["health", ]), "\n")
cat("Four-year cost today THB, start | end | average:", round(today["cost", ]), "\n")
cat("Per person, average, health | cost THB:", sprintf("%.3f", today["health", "average"] / 1000),
  round(today["cost", "average"] / 1000), "\n")
cat("Rate 0.12, 0.25 years, chance | shortcut:", cycle_chance, shortcut, "\n")
cat("Ten years at 0.70 | gain from full health:", weighted_years, "\n")
cat("Cost due in year 3 at 3%, today THB:", round(future_cost / (1 + rate)^3), "\n")
กล่องนี้ทำขั้นรายปีซ้ำสี่ครั้ง บรรทัดแรกของผลลัพธ์ตรวจทุกแถวของโอกาสและยอดรวม 1,000 คนของทุกปี บรรทัดที่สองคือตัวอย่างปีที่สอง แยกตามแต่ละทางที่คนย้าย และสามบรรทัดสุดท้ายคือคอลัมน์ของ cohort trace

ปีหนึ่งควรนับด้วยจำนวนตอนไหน

คำถามสองข้อสุดท้ายของผู้จัดการต้องใช้ค่าของแต่ละปีที่คนหนึ่งคนอยู่ในแต่ละสถานะ ทั้งในรูปเงินและในรูปค่าสุขภาพ

ปีสุขภาวะ (quality-adjusted life year, QALY) คือปีชีวิตที่ถ่วงน้ำหนักด้วยคุณภาพ ตั้งแต่ 1 สำหรับสุขภาพสมบูรณ์ถึง 0 สำหรับการเสียชีวิต การอยู่ 10 ปีในสภาวะที่มีค่า 0.70 เท่ากับ 7.0 QALY การกลับมาสุขภาพสมบูรณ์จึงได้เพิ่ม 3.0 อ่านบทเรียนเต็มได้ที่ ปีสุขภาวะ ปีสุขภาวะที่สูญเสีย และค่าอรรถประโยชน์: เรากำลังวัดอะไรกันแน่

เพื่อการสอน คลินิกกำหนดให้ปีที่ไม่มีโรคไตมีค่า 1 QALY ซึ่งคือสุขภาพสมบูรณ์ ปีที่มีโรคไตมีค่า 0.6 QALY และปีที่เสียชีวิตมีค่า 0 QALY เมื่อรวมกับต้นทุนรายปี 100 บาทและ 400 บาท แต่ละสถานะจึงมีค่าสองค่าต่อคนต่อปี

ค่าของหนึ่งคนที่อยู่ครบหนึ่งปีในแต่ละสถานะ

สถานะต้นทุน (บาทต่อปี)สุขภาพ (QALY ต่อปี)
สุขภาพดี1001
ป่วย4000.6
เสียชีวิต00

ปีแรก นับสามแบบ

ทีนี้ดูปีแรก ตอนต้นปีคนทั้ง 1,000 คนสุขภาพดี นับได้ 1,000 × 1 = 1,000 QALY สำหรับทั้งกลุ่ม ตอนท้ายปีมีสุขภาพดี 800 คนและป่วย 150 คน นับได้ 800 × 1 + 150 × 0.6 = 890 QALY แต่ไม่มีค่าใดถูกต้อง เพราะคนที่ออกจากสถานะสุขภาพดีไม่ได้ออกกันหมดในวันแรกของปี และไม่ได้ออกกันหมดในวันสุดท้ายของปี

ถ้าคนออกอย่างสม่ำเสมอตลอดปี จำนวนคนเฉลี่ยตลอดปีก็อยู่กึ่งกลางระหว่างสองจำนวนนี้ จึงนำสองค่ามาเฉลี่ย (1,000 + 890) / 2 = 945 QALY หรือ 0.945 ต่อคน แทนที่จะเป็น 0.890 จากจำนวนตอนท้าย หรือ 1.000 จากจำนวนตอนต้น ต้นทุนใช้กฎเดียวกัน คือ 1,000 × 100 = 100,000 บาทเมื่อนับตอนต้น และ 800 × 100 + 150 × 400 = 140,000 บาทเมื่อนับตอนท้าย ค่าเฉลี่ยจึงเป็น (100,000 + 140,000) / 2 = 120,000 บาท

การเฉลี่ยแบบนี้มีชื่อเรียก การปรับครึ่งรอบ (half-cycle correction) คือการเฉลี่ยจำนวนคนตอนต้นและตอนท้ายของแต่ละรอบ เหมือนคนย้ายสถานะกลางรอบ (Briggs and colleagues) การปรับนี้เปลี่ยนค่าที่นับให้แต่ละปี ไม่ได้เปลี่ยนว่าใครไปอยู่สถานะไหน cohort trace ด้านบนจึงไม่เปลี่ยน บางรายงานเรียกการคำนวณเดียวกันนี้ว่าวิธีสี่เหลี่ยมคางหมู (trapezoidal method) เพราะพื้นที่ใต้เส้นสีน้ำเงินในรูปด้านล่างมีรูปสี่เหลี่ยมคางหมู

ดูเส้นสีน้ำเงิน ค่าสุขภาพของกลุ่มลดลงจาก 1,000 QALY ตอนต้นปีแรกเป็น 890 QALY ตอนท้ายปี และพื้นที่แรเงาใต้เส้นคือค่าที่กลุ่มมีจริงตลอดปี เส้นประสีแดงของจำนวนตอนต้น 1,000 อยู่เหนือเส้นนั้น และเส้นประสีส้มของจำนวนตอนท้าย 890 อยู่ใต้เส้นนั้น แต่ละค่าจึงคลาดไปทางเดียว เส้นสีเขียวที่ 945 ตัดผ่านกลางเส้นสีน้ำเงินพอดี และเท่ากับพื้นที่แรเงาถ้าคนออกอย่างสม่ำเสมอตลอดปี แกนตั้งเริ่มที่ 800 เพื่อให้เห็นการลดลงชัด

ตรวจปีแรกด้วย R

โค้ด R cycle-one.R
# Year one's cost and health, counted at the start, at the end and as the average of the two.
# Writes: nothing to disk; the results print in the console.
first_start <- c(1000, 0, 0)
first_end <- c(800, 150, 50)
health_value <- c(1, 0.6, 0)
yearly_cost <- c(100, 400, 0)
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
counts <- rbind(first_start, first_end)
health <- c(counts %*% health_value, mean(counts %*% health_value))
spend <- c(counts %*% yearly_cost, mean(counts %*% yearly_cost))
cat("Year 1 health, start | end | average:", health, "\n")
cat("Year 1 health per person:", sprintf("%.3f", health / 1000), "\n")
cat("Year 1 cost THB, start | end | average:", big(spend), "\n")
ผลลัพธ์จากการรัน cycle-one.log
Year 1 health, start | end | average: 1000 890 945
Year 1 health per person: 1.000 0.890 0.945
Year 1 cost THB, start | end | average: 100,000 140,000 120,000

ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด building-a-markov-cohort-model-step-by-step.R

สคริปต์ฉบับเต็ม
# Building a Markov Cohort Model Step by Step: Follow the Cohort Through Time
# Follow 1,000 people through Healthy, Sick and Dead for four years, then count health and cost.
# ติดตามคน 1,000 คนผ่านสถานะสุขภาพดี ป่วย และเสียชีวิต สี่รอบรายปี แล้วนับสุขภาพและต้นทุน
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# ggplot2 4.0.3 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
library(tibble)  # Write the states and their yearly values as an explicit table.
library(dplyr)  # Add the per-cycle values to that table.
library(ggplot2)  # Display the trace without saving files.
stopifnot(getRversion() >= "4.5.2")

#%%@note 1. Define the illustrative inputs
# Every input is synthetic and written out once, in the order the article uses it.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
start <- c(1000, 0, 0)
transition <- c(0.80, 0.15, 0.05, 0, 0.85, 0.15, 0, 0, 1)
cycles <- 4
first_start <- c(1000, 0, 0)
first_end <- c(800, 150, 50)
health_value <- c(1, 0.6, 0)
yearly_cost <- c(100, 400, 0)
rate <- 0.03
average_by_year <- c(945, 839.25, 742.1625, 653.878125)
rate_per_year <- 0.12
cycle_years <- 0.25
years_in_state <- 10
state_value <- 0.70
future_cost <- 1834
cycle_length <- 1
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
states <- tibble::tibble(state = c("Healthy", "Sick", "Dead"), health = health_value,
  cost = yearly_cost) |>
  dplyr::mutate(health_per_cycle = health * cycle_length, cost_per_cycle = cost * cycle_length)
moves <- matrix(transition, nrow = 3, byrow = TRUE)
stopifnot(all(abs(rowSums(moves) - 1) < 1e-9))

#%%@note 2. Move the cohort one year at a time
# Multiply the counts by the chances each year, and check that the total stays at 1,000.
# คูณจำนวนคนด้วยโอกาสย้ายสถานะทีละปี และตรวจว่ายอดรวมยังเท่ากับ 1,000
trace <- matrix(start, nrow = cycles + 1, ncol = 3, byrow = TRUE)
for (year in seq_len(cycles)) trace[year + 1, ] <- trace[year, ] %*% moves
stopifnot(all(abs(rowSums(trace) - sum(start)) < 1e-9))
flows <- trace[2, ] * moves
people <- trace

#%%@note 3. Count each year at its start, at its end and as the average
# The end count of one year is the start count of the next, so the same trace gives all three.
# จำนวนตอนท้ายปีหนึ่งคือจำนวนตอนต้นปีถัดไป trace เดียวจึงให้การนับได้ทั้งสามแบบ
counts <- rbind(first_start, first_end)
health <- c(counts %*% health_value, mean(counts %*% health_value))
spend <- c(counts %*% yearly_cost, mean(counts %*% yearly_cost))
health_edges <- people %*% health_value
at_start <- health_edges[1:4]
at_end <- health_edges[2:5]
average <- (at_start + at_end) / 2

#%%@note 4. Bring later years back to today
# Divide each year's value by 1.03 once for every year that has passed by its end.
# หารค่าของแต่ละปีด้วย 1.03 หนึ่งครั้งต่อทุกปีที่ผ่านไปเมื่อสิ้นปีนั้น
factors <- (1 + rate)^(1:4)
rewards <- cbind(health = states$health_per_cycle, cost = states$cost_per_cycle)
edges <- people %*% rewards
ways <- list(start = edges[1:4, ], end = edges[2:5, ], average = (edges[1:4, ] + edges[2:5, ]) / 2)
today <- cbind(start = colSums(ways$start / factors), end = colSums(ways$end / factors),
  average = colSums(ways$average / factors))

#%%@note 5. Rework the three foundation examples
# A yearly rate becomes a chance per cycle, a weight turns years into health, a later cost shrinks.
# อัตรารายปีกลายเป็นโอกาสต่อรอบ ค่าน้ำหนักเปลี่ยนปีเป็นสุขภาพ และต้นทุนในอนาคตมีค่าน้อยลงในวันนี้
cycle_chance <- signif(1 - exp(-rate_per_year * cycle_years), 3)
shortcut <- formatC(rate_per_year * cycle_years, format = "f", digits = 4)
weighted_years <- formatC(years_in_state * c(state_value, 1 - state_value), format = "f",
  digits = 1)

#%%@note 6. Display the trace
# Plot the number of people in each state at the end of each year, no graphics file is saved.
# แสดงจำนวนคนในแต่ละสถานะเมื่อสิ้นแต่ละปี โดยไม่บันทึกไฟล์ภาพ
trace_table <- tibble::tibble(year = rep(0:cycles, times = 3),
  state = rep(states$state, each = cycles + 1), people = as.vector(trace))
print(ggplot2::ggplot(trace_table, ggplot2::aes(x = year, y = people, colour = state)) +
  ggplot2::geom_line())

#%%@note 7. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Row sums of the chances:", rowSums(moves), "| totals, years 0 to 4:", rowSums(trace), "\n")
cat("Year 2 flows from Healthy | Sick | Dead:", flows[1, ], "|", flows[2, ], "|", flows[3, ], "\n")
cat("Healthy, years 0 to 4:", round(trace[, 1], 3), "\n")
cat("Sick, years 0 to 4:", round(trace[, 2], 3), "\n")
cat("Dead, years 0 to 4:", round(trace[, 3], 3), "\n")
cat("Year 1 health, start | end | average:", health, "\n")
cat("Year 1 health per person:", sprintf("%.3f", health / 1000), "\n")
cat("Year 1 cost THB, start | end | average:", big(spend), "\n")
cat("Health by year, start:", at_start, "| total:", sum(at_start), "\n")
cat("Health by year, end:", round(at_end, 3), "| total:", round(sum(at_end), 3), "\n")
cat("Health by year, average:", round(average, 2), "| total:", round(sum(average), 2), "\n")
cat("Rate:", paste0(100 * rate, "%"), "| factors:",
  formatC(factors, format = "f", digits = 4), "\n")
cat("Average health by year, today's value:", round(average_by_year / factors, 1),
  "| total:", round(sum(average_by_year / factors), 3), "\n")
cat("Four-year health today, start | end | average:", sprintf("%.3f", today["health", ]), "\n")
cat("Four-year cost today THB, start | end | average:", round(today["cost", ]), "\n")
cat("Per person, average, health | cost THB:", sprintf("%.3f", today["health", "average"] / 1000),
  round(today["cost", "average"] / 1000), "\n")
cat("Rate 0.12, 0.25 years, chance | shortcut:", cycle_chance, shortcut, "\n")
cat("Ten years at 0.70 | gain from full health:", weighted_years, "\n")
cat("Cost due in year 3 at 3%, today THB:", round(future_cost / (1 + rate)^3), "\n")
สามบรรทัดพิมพ์สุขภาพของทั้งกลุ่ม สุขภาพต่อคน และต้นทุน โดยแต่ละค่านับตอนต้น ตอนท้าย และเป็นค่าเฉลี่ย

สี่ปี นับด้วยวิธีเดียวกัน

ปีต่อ ๆ ไปคิดแบบเดียวกัน และ cohort trace มีจำนวนที่ต้องใช้ครบแล้ว ตอนท้ายของปีหนึ่งคือตอนต้นของปีถัดไป ปีที่สองจึงเริ่มที่ 890 จบที่ 640 × 1 + 247.5 × 0.6 = 788.5 QALY และมีค่าเฉลี่ย (890 + 788.5) / 2 = 839.25 ปีที่สามและปีที่สี่ทำขั้นเดิมกับสองแถวถัดไปของ cohort trace และตารางวางวิธีนับทั้งสามแบบไว้เทียบกัน

QALY ของทั้งกลุ่มในแต่ละปี นับสามแบบ (ยังไม่แปลงเป็นมูลค่าวันนี้)

ปีที่นับตอนต้นปีนับตอนท้ายปีค่าเฉลี่ยของสองค่า
11,000890945
2890788.5839.25
3788.5695.825742.16
4695.825611.931653.88
รวมสี่ปี3,374.3252,986.2563,180.29

คอลัมน์บอกอะไร

ไล่ลงตามคอลัมน์ คอลัมน์ตอนต้นนับทุกปีเหมือนยังไม่มีใครออก ส่วนคอลัมน์ตอนท้ายนับเหมือนทุกคนที่ออกได้ออกไปตั้งแต่วันแรก ตลอดสี่ปี การนับตอนต้นให้ 3,374.325 QALY และการนับตอนท้ายให้ 2,986.256 ส่วนค่าเฉลี่ย 3,180.29 อยู่กึ่งกลางระหว่างสองค่านี้

จำนวนที่ขอบทั้งสองต่างกันมากเพราะกลุ่มเปลี่ยนเร็วภายในปีเดียว ถ้าจำนวนแทบไม่เปลี่ยนภายในปี ทั้งสามคอลัมน์ก็จะตรงกัน แต่เมื่อจำนวนเปลี่ยนเร็ว การใช้รอบที่สั้นลง เช่นสามเดือน น่าเชื่อถือกว่ากฎการเฉลี่ยใด ๆ

ตรวจสี่ปีด้วย R

โค้ด R four-years.R
# Four years of health from the same trace, counted at the start, at the end and as the average.
# Writes: nothing to disk; the results print in the console.
transition <- c(0.80, 0.15, 0.05, 0, 0.85, 0.15, 0, 0, 1)
health_value <- c(1, 0.6, 0)
people <- matrix(c(1000, 0, 0), nrow = 5, ncol = 3, byrow = TRUE)
for (year in 1:4) people[year + 1, ] <- people[year, ] %*% matrix(transition, 3, byrow = TRUE)
health_edges <- people %*% health_value
at_start <- health_edges[1:4]
at_end <- health_edges[2:5]
average <- (at_start + at_end) / 2
cat("Health by year, start:", at_start, "| total:", sum(at_start), "\n")
cat("Health by year, end:", round(at_end, 3), "| total:", round(sum(at_end), 3), "\n")
cat("Health by year, average:", round(average, 2), "| total:", round(sum(average), 2), "\n")
ผลลัพธ์จากการรัน four-years.log
Health by year, start: 1000 890 788.5 695.825 | total: 3374.325
Health by year, end: 890 788.5 695.825 611.931 | total: 2986.256
Health by year, average: 945 839.25 742.16 653.88 | total: 3180.29

ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด building-a-markov-cohort-model-step-by-step.R

สคริปต์ฉบับเต็ม
# Building a Markov Cohort Model Step by Step: Follow the Cohort Through Time
# Follow 1,000 people through Healthy, Sick and Dead for four years, then count health and cost.
# ติดตามคน 1,000 คนผ่านสถานะสุขภาพดี ป่วย และเสียชีวิต สี่รอบรายปี แล้วนับสุขภาพและต้นทุน
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# ggplot2 4.0.3 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
library(tibble)  # Write the states and their yearly values as an explicit table.
library(dplyr)  # Add the per-cycle values to that table.
library(ggplot2)  # Display the trace without saving files.
stopifnot(getRversion() >= "4.5.2")

#%%@note 1. Define the illustrative inputs
# Every input is synthetic and written out once, in the order the article uses it.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
start <- c(1000, 0, 0)
transition <- c(0.80, 0.15, 0.05, 0, 0.85, 0.15, 0, 0, 1)
cycles <- 4
first_start <- c(1000, 0, 0)
first_end <- c(800, 150, 50)
health_value <- c(1, 0.6, 0)
yearly_cost <- c(100, 400, 0)
rate <- 0.03
average_by_year <- c(945, 839.25, 742.1625, 653.878125)
rate_per_year <- 0.12
cycle_years <- 0.25
years_in_state <- 10
state_value <- 0.70
future_cost <- 1834
cycle_length <- 1
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
states <- tibble::tibble(state = c("Healthy", "Sick", "Dead"), health = health_value,
  cost = yearly_cost) |>
  dplyr::mutate(health_per_cycle = health * cycle_length, cost_per_cycle = cost * cycle_length)
moves <- matrix(transition, nrow = 3, byrow = TRUE)
stopifnot(all(abs(rowSums(moves) - 1) < 1e-9))

#%%@note 2. Move the cohort one year at a time
# Multiply the counts by the chances each year, and check that the total stays at 1,000.
# คูณจำนวนคนด้วยโอกาสย้ายสถานะทีละปี และตรวจว่ายอดรวมยังเท่ากับ 1,000
trace <- matrix(start, nrow = cycles + 1, ncol = 3, byrow = TRUE)
for (year in seq_len(cycles)) trace[year + 1, ] <- trace[year, ] %*% moves
stopifnot(all(abs(rowSums(trace) - sum(start)) < 1e-9))
flows <- trace[2, ] * moves
people <- trace

#%%@note 3. Count each year at its start, at its end and as the average
# The end count of one year is the start count of the next, so the same trace gives all three.
# จำนวนตอนท้ายปีหนึ่งคือจำนวนตอนต้นปีถัดไป trace เดียวจึงให้การนับได้ทั้งสามแบบ
counts <- rbind(first_start, first_end)
health <- c(counts %*% health_value, mean(counts %*% health_value))
spend <- c(counts %*% yearly_cost, mean(counts %*% yearly_cost))
health_edges <- people %*% health_value
at_start <- health_edges[1:4]
at_end <- health_edges[2:5]
average <- (at_start + at_end) / 2

#%%@note 4. Bring later years back to today
# Divide each year's value by 1.03 once for every year that has passed by its end.
# หารค่าของแต่ละปีด้วย 1.03 หนึ่งครั้งต่อทุกปีที่ผ่านไปเมื่อสิ้นปีนั้น
factors <- (1 + rate)^(1:4)
rewards <- cbind(health = states$health_per_cycle, cost = states$cost_per_cycle)
edges <- people %*% rewards
ways <- list(start = edges[1:4, ], end = edges[2:5, ], average = (edges[1:4, ] + edges[2:5, ]) / 2)
today <- cbind(start = colSums(ways$start / factors), end = colSums(ways$end / factors),
  average = colSums(ways$average / factors))

#%%@note 5. Rework the three foundation examples
# A yearly rate becomes a chance per cycle, a weight turns years into health, a later cost shrinks.
# อัตรารายปีกลายเป็นโอกาสต่อรอบ ค่าน้ำหนักเปลี่ยนปีเป็นสุขภาพ และต้นทุนในอนาคตมีค่าน้อยลงในวันนี้
cycle_chance <- signif(1 - exp(-rate_per_year * cycle_years), 3)
shortcut <- formatC(rate_per_year * cycle_years, format = "f", digits = 4)
weighted_years <- formatC(years_in_state * c(state_value, 1 - state_value), format = "f",
  digits = 1)

#%%@note 6. Display the trace
# Plot the number of people in each state at the end of each year, no graphics file is saved.
# แสดงจำนวนคนในแต่ละสถานะเมื่อสิ้นแต่ละปี โดยไม่บันทึกไฟล์ภาพ
trace_table <- tibble::tibble(year = rep(0:cycles, times = 3),
  state = rep(states$state, each = cycles + 1), people = as.vector(trace))
print(ggplot2::ggplot(trace_table, ggplot2::aes(x = year, y = people, colour = state)) +
  ggplot2::geom_line())

#%%@note 7. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Row sums of the chances:", rowSums(moves), "| totals, years 0 to 4:", rowSums(trace), "\n")
cat("Year 2 flows from Healthy | Sick | Dead:", flows[1, ], "|", flows[2, ], "|", flows[3, ], "\n")
cat("Healthy, years 0 to 4:", round(trace[, 1], 3), "\n")
cat("Sick, years 0 to 4:", round(trace[, 2], 3), "\n")
cat("Dead, years 0 to 4:", round(trace[, 3], 3), "\n")
cat("Year 1 health, start | end | average:", health, "\n")
cat("Year 1 health per person:", sprintf("%.3f", health / 1000), "\n")
cat("Year 1 cost THB, start | end | average:", big(spend), "\n")
cat("Health by year, start:", at_start, "| total:", sum(at_start), "\n")
cat("Health by year, end:", round(at_end, 3), "| total:", round(sum(at_end), 3), "\n")
cat("Health by year, average:", round(average, 2), "| total:", round(sum(average), 2), "\n")
cat("Rate:", paste0(100 * rate, "%"), "| factors:",
  formatC(factors, format = "f", digits = 4), "\n")
cat("Average health by year, today's value:", round(average_by_year / factors, 1),
  "| total:", round(sum(average_by_year / factors), 3), "\n")
cat("Four-year health today, start | end | average:", sprintf("%.3f", today["health", ]), "\n")
cat("Four-year cost today THB, start | end | average:", round(today["cost", ]), "\n")
cat("Per person, average, health | cost THB:", sprintf("%.3f", today["health", "average"] / 1000),
  round(today["cost", "average"] / 1000), "\n")
cat("Rate 0.12, 0.25 years, chance | shortcut:", cycle_chance, shortcut, "\n")
cat("Ten years at 0.70 | gain from full health:", weighted_years, "\n")
cat("Cost due in year 3 at 3%, today THB:", round(future_cost / (1 + rate)^3), "\n")
แต่ละบรรทัดคือหนึ่งคอลัมน์ของตาราง คือค่ารายปีสี่ค่า แล้วตามด้วยยอดรวมสี่ปี

แปลงปีหลัง ๆ กลับเป็นมูลค่าวันนี้

ยังเหลืออีกหนึ่งขั้นก่อนการพยากรณ์จะครบ การปรับลดค่าตามเวลา (discounting) คือการแปลงต้นทุนหรือผลทางสุขภาพในอนาคตเป็นมูลค่าวันนี้ โดยลดลงตามอัตราการปรับลดในแต่ละปี ที่อัตรา 3% ต้นทุน 1,834 บาทที่ต้องจ่ายในปีที่ 3 มีค่าเท่ากับ 1,678 บาทในวันนี้ อ่านบทเรียนเต็มได้ที่ ต้นทุนของใคร ณ ปีใด: มุมมองการวิเคราะห์ ต้นทุนค่าเสียโอกาส การปรับค่าเงินเฟ้อ และการปรับลดค่าตามเวลา

คลินิกใช้อัตรา 3% ต่อปีทั้งกับต้นทุนและ QALY และนับค่าของแต่ละปีที่ตอนท้ายปีนั้น ค่าของปีแรกหารด้วย 1.03 ค่าของปีที่สองหารด้วย 1.03 × 1.03 = 1.0609 ค่าของปีที่สามหารด้วย 1.0927 และค่าของปีที่สี่หารด้วย 1.1255 สำหรับการนับแบบค่าเฉลี่ยจึงได้ 945 / 1.03 = 917.5, 839.25 / 1.0609 = 791.1, 742.16 / 1.0927 = 679.2 และ 653.88 / 1.1255 = 581.0 QALY ในมูลค่าวันนี้ เมื่อรวมก่อนปัดเศษ สี่ค่านี้ได้ 2,968.695 QALY

ตรวจตัวหารรายปีด้วย R

โค้ด R today.R
# Each year's averaged health brought back to today at 3% a year, counted at the end of the year.
# Writes: nothing to disk; the results print in the console.
rate <- 0.03
average_by_year <- c(945, 839.25, 742.1625, 653.878125)
factors <- (1 + rate)^(1:4)
cat("Rate:", paste0(100 * rate, "%"), "| factors:",
  formatC(factors, format = "f", digits = 4), "\n")
cat("Average health by year, today's value:", round(average_by_year / factors, 1),
  "| total:", round(sum(average_by_year / factors), 3), "\n")
ผลลัพธ์จากการรัน today.log
Rate: 3% | factors: 1.0300 1.0609 1.0927 1.1255
Average health by year, today's value: 917.5 791.1 679.2 581 | total: 2968.695

ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด building-a-markov-cohort-model-step-by-step.R

สคริปต์ฉบับเต็ม
# Building a Markov Cohort Model Step by Step: Follow the Cohort Through Time
# Follow 1,000 people through Healthy, Sick and Dead for four years, then count health and cost.
# ติดตามคน 1,000 คนผ่านสถานะสุขภาพดี ป่วย และเสียชีวิต สี่รอบรายปี แล้วนับสุขภาพและต้นทุน
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# ggplot2 4.0.3 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
library(tibble)  # Write the states and their yearly values as an explicit table.
library(dplyr)  # Add the per-cycle values to that table.
library(ggplot2)  # Display the trace without saving files.
stopifnot(getRversion() >= "4.5.2")

#%%@note 1. Define the illustrative inputs
# Every input is synthetic and written out once, in the order the article uses it.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
start <- c(1000, 0, 0)
transition <- c(0.80, 0.15, 0.05, 0, 0.85, 0.15, 0, 0, 1)
cycles <- 4
first_start <- c(1000, 0, 0)
first_end <- c(800, 150, 50)
health_value <- c(1, 0.6, 0)
yearly_cost <- c(100, 400, 0)
rate <- 0.03
average_by_year <- c(945, 839.25, 742.1625, 653.878125)
rate_per_year <- 0.12
cycle_years <- 0.25
years_in_state <- 10
state_value <- 0.70
future_cost <- 1834
cycle_length <- 1
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
states <- tibble::tibble(state = c("Healthy", "Sick", "Dead"), health = health_value,
  cost = yearly_cost) |>
  dplyr::mutate(health_per_cycle = health * cycle_length, cost_per_cycle = cost * cycle_length)
moves <- matrix(transition, nrow = 3, byrow = TRUE)
stopifnot(all(abs(rowSums(moves) - 1) < 1e-9))

#%%@note 2. Move the cohort one year at a time
# Multiply the counts by the chances each year, and check that the total stays at 1,000.
# คูณจำนวนคนด้วยโอกาสย้ายสถานะทีละปี และตรวจว่ายอดรวมยังเท่ากับ 1,000
trace <- matrix(start, nrow = cycles + 1, ncol = 3, byrow = TRUE)
for (year in seq_len(cycles)) trace[year + 1, ] <- trace[year, ] %*% moves
stopifnot(all(abs(rowSums(trace) - sum(start)) < 1e-9))
flows <- trace[2, ] * moves
people <- trace

#%%@note 3. Count each year at its start, at its end and as the average
# The end count of one year is the start count of the next, so the same trace gives all three.
# จำนวนตอนท้ายปีหนึ่งคือจำนวนตอนต้นปีถัดไป trace เดียวจึงให้การนับได้ทั้งสามแบบ
counts <- rbind(first_start, first_end)
health <- c(counts %*% health_value, mean(counts %*% health_value))
spend <- c(counts %*% yearly_cost, mean(counts %*% yearly_cost))
health_edges <- people %*% health_value
at_start <- health_edges[1:4]
at_end <- health_edges[2:5]
average <- (at_start + at_end) / 2

#%%@note 4. Bring later years back to today
# Divide each year's value by 1.03 once for every year that has passed by its end.
# หารค่าของแต่ละปีด้วย 1.03 หนึ่งครั้งต่อทุกปีที่ผ่านไปเมื่อสิ้นปีนั้น
factors <- (1 + rate)^(1:4)
rewards <- cbind(health = states$health_per_cycle, cost = states$cost_per_cycle)
edges <- people %*% rewards
ways <- list(start = edges[1:4, ], end = edges[2:5, ], average = (edges[1:4, ] + edges[2:5, ]) / 2)
today <- cbind(start = colSums(ways$start / factors), end = colSums(ways$end / factors),
  average = colSums(ways$average / factors))

#%%@note 5. Rework the three foundation examples
# A yearly rate becomes a chance per cycle, a weight turns years into health, a later cost shrinks.
# อัตรารายปีกลายเป็นโอกาสต่อรอบ ค่าน้ำหนักเปลี่ยนปีเป็นสุขภาพ และต้นทุนในอนาคตมีค่าน้อยลงในวันนี้
cycle_chance <- signif(1 - exp(-rate_per_year * cycle_years), 3)
shortcut <- formatC(rate_per_year * cycle_years, format = "f", digits = 4)
weighted_years <- formatC(years_in_state * c(state_value, 1 - state_value), format = "f",
  digits = 1)

#%%@note 6. Display the trace
# Plot the number of people in each state at the end of each year, no graphics file is saved.
# แสดงจำนวนคนในแต่ละสถานะเมื่อสิ้นแต่ละปี โดยไม่บันทึกไฟล์ภาพ
trace_table <- tibble::tibble(year = rep(0:cycles, times = 3),
  state = rep(states$state, each = cycles + 1), people = as.vector(trace))
print(ggplot2::ggplot(trace_table, ggplot2::aes(x = year, y = people, colour = state)) +
  ggplot2::geom_line())

#%%@note 7. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Row sums of the chances:", rowSums(moves), "| totals, years 0 to 4:", rowSums(trace), "\n")
cat("Year 2 flows from Healthy | Sick | Dead:", flows[1, ], "|", flows[2, ], "|", flows[3, ], "\n")
cat("Healthy, years 0 to 4:", round(trace[, 1], 3), "\n")
cat("Sick, years 0 to 4:", round(trace[, 2], 3), "\n")
cat("Dead, years 0 to 4:", round(trace[, 3], 3), "\n")
cat("Year 1 health, start | end | average:", health, "\n")
cat("Year 1 health per person:", sprintf("%.3f", health / 1000), "\n")
cat("Year 1 cost THB, start | end | average:", big(spend), "\n")
cat("Health by year, start:", at_start, "| total:", sum(at_start), "\n")
cat("Health by year, end:", round(at_end, 3), "| total:", round(sum(at_end), 3), "\n")
cat("Health by year, average:", round(average, 2), "| total:", round(sum(average), 2), "\n")
cat("Rate:", paste0(100 * rate, "%"), "| factors:",
  formatC(factors, format = "f", digits = 4), "\n")
cat("Average health by year, today's value:", round(average_by_year / factors, 1),
  "| total:", round(sum(average_by_year / factors), 3), "\n")
cat("Four-year health today, start | end | average:", sprintf("%.3f", today["health", ]), "\n")
cat("Four-year cost today THB, start | end | average:", round(today["cost", ]), "\n")
cat("Per person, average, health | cost THB:", sprintf("%.3f", today["health", "average"] / 1000),
  round(today["cost", "average"] / 1000), "\n")
cat("Rate 0.12, 0.25 years, chance | shortcut:", cycle_chance, shortcut, "\n")
cat("Ten years at 0.70 | gain from full health:", weighted_years, "\n")
cat("Cost due in year 3 at 3%, today THB:", round(future_cost / (1 + rate)^3), "\n")
บรรทัดแรกพิมพ์ตัวหารของปีที่หนึ่งถึงสี่ บรรทัดที่สองหาร QALY แบบค่าเฉลี่ยของแต่ละปีด้วยตัวหารของปีนั้นแล้วรวมกัน

ยอดรวมสี่ปี

ทำแบบเดียวกันกับการนับตอนต้นและตอนท้าย และกับต้นทุน วิธีนับทั้งสามแบบจึงให้ยอดรวมสี่ปีในมูลค่าวันนี้ตามตารางนี้ ค่าเฉลี่ยยังอยู่ระหว่างจำนวนที่ขอบทั้งสอง ทั้งในด้านต้นทุนและด้านสุขภาพ การนับตอนท้ายทำให้ต้นทุนรวมสูงที่สุด เพราะนับแต่ละปีด้วยจำนวนคนตอนท้ายปี ซึ่งมีคนย้ายเข้าสถานะป่วยที่มีต้นทุนสูงมากกว่าแล้ว

สี่ปีในมูลค่าวันนี้ สำหรับทั้งกลุ่ม

วิธีนับแต่ละปีต้นทุน (บาท)QALY
ตอนต้นปี532,5943,149.605
ตอนท้ายปี604,8102,787.786
ค่าเฉลี่ย (การปรับครึ่งรอบ)568,7022,968.695

คำตอบของการพยากรณ์

ผู้จัดการจึงตอบได้แล้ว เมื่อนับแต่ละปีที่กลางปี คน 1,000 คนจะทำให้คลินิกมีต้นทุน 568,702 บาทในมูลค่าวันนี้ ตลอดสี่ปี และกลุ่มนี้จะมีชีวิตที่ปรับด้วยคุณภาพรวม 2,968.695 QALY คิดเป็น 569 บาทและ 2.969 QALY ต่อคน ส่วน cohort trace ตอบคำถามข้อแรก คือเมื่อครบสี่ปี จำนวนคาดหมายคือคนที่ยังมีชีวิตอยู่และเป็นโรคไต 337.219 คน และผู้เสียชีวิตสะสม 253.181 คน ซึ่งเป็นค่าเฉลี่ยของกลุ่มแบบนี้หลาย ๆ กลุ่ม

ตรวจยอดรวมสี่ปีด้วย R

โค้ด R totals.R
# Four years of health and cost at today's value, for each way of counting the year.
# Writes: nothing to disk; the results print in the console.
rate <- 0.03
transition <- c(0.80, 0.15, 0.05, 0, 0.85, 0.15, 0, 0, 1)
rewards <- cbind(health = c(1, 0.6, 0), cost = c(100, 400, 0))
people <- matrix(c(1000, 0, 0), nrow = 5, ncol = 3, byrow = TRUE)
for (year in 1:4) people[year + 1, ] <- people[year, ] %*% matrix(transition, 3, byrow = TRUE)
edges <- people %*% rewards
ways <- list(start = edges[1:4, ], end = edges[2:5, ], average = (edges[1:4, ] + edges[2:5, ]) / 2)
today <- sapply(ways, function(rows) colSums(rows / (1 + rate)^(1:4)))
cat("Four-year health today, start | end | average:", sprintf("%.3f", today["health", ]), "\n")
cat("Four-year cost today THB, start | end | average:", round(today["cost", ]), "\n")
cat("Per person, average, health | cost THB:", sprintf("%.3f", today["health", "average"] / 1000),
  round(today["cost", "average"] / 1000), "\n")
ผลลัพธ์จากการรัน totals.log
Four-year health today, start | end | average: 3149.605 2787.786 2968.695
Four-year cost today THB, start | end | average: 532594 604810 568702
Per person, average, health | cost THB: 2.969 569

ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด building-a-markov-cohort-model-step-by-step.R

สคริปต์ฉบับเต็ม
# Building a Markov Cohort Model Step by Step: Follow the Cohort Through Time
# Follow 1,000 people through Healthy, Sick and Dead for four years, then count health and cost.
# ติดตามคน 1,000 คนผ่านสถานะสุขภาพดี ป่วย และเสียชีวิต สี่รอบรายปี แล้วนับสุขภาพและต้นทุน
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# ggplot2 4.0.3 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
library(tibble)  # Write the states and their yearly values as an explicit table.
library(dplyr)  # Add the per-cycle values to that table.
library(ggplot2)  # Display the trace without saving files.
stopifnot(getRversion() >= "4.5.2")

#%%@note 1. Define the illustrative inputs
# Every input is synthetic and written out once, in the order the article uses it.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
start <- c(1000, 0, 0)
transition <- c(0.80, 0.15, 0.05, 0, 0.85, 0.15, 0, 0, 1)
cycles <- 4
first_start <- c(1000, 0, 0)
first_end <- c(800, 150, 50)
health_value <- c(1, 0.6, 0)
yearly_cost <- c(100, 400, 0)
rate <- 0.03
average_by_year <- c(945, 839.25, 742.1625, 653.878125)
rate_per_year <- 0.12
cycle_years <- 0.25
years_in_state <- 10
state_value <- 0.70
future_cost <- 1834
cycle_length <- 1
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
states <- tibble::tibble(state = c("Healthy", "Sick", "Dead"), health = health_value,
  cost = yearly_cost) |>
  dplyr::mutate(health_per_cycle = health * cycle_length, cost_per_cycle = cost * cycle_length)
moves <- matrix(transition, nrow = 3, byrow = TRUE)
stopifnot(all(abs(rowSums(moves) - 1) < 1e-9))

#%%@note 2. Move the cohort one year at a time
# Multiply the counts by the chances each year, and check that the total stays at 1,000.
# คูณจำนวนคนด้วยโอกาสย้ายสถานะทีละปี และตรวจว่ายอดรวมยังเท่ากับ 1,000
trace <- matrix(start, nrow = cycles + 1, ncol = 3, byrow = TRUE)
for (year in seq_len(cycles)) trace[year + 1, ] <- trace[year, ] %*% moves
stopifnot(all(abs(rowSums(trace) - sum(start)) < 1e-9))
flows <- trace[2, ] * moves
people <- trace

#%%@note 3. Count each year at its start, at its end and as the average
# The end count of one year is the start count of the next, so the same trace gives all three.
# จำนวนตอนท้ายปีหนึ่งคือจำนวนตอนต้นปีถัดไป trace เดียวจึงให้การนับได้ทั้งสามแบบ
counts <- rbind(first_start, first_end)
health <- c(counts %*% health_value, mean(counts %*% health_value))
spend <- c(counts %*% yearly_cost, mean(counts %*% yearly_cost))
health_edges <- people %*% health_value
at_start <- health_edges[1:4]
at_end <- health_edges[2:5]
average <- (at_start + at_end) / 2

#%%@note 4. Bring later years back to today
# Divide each year's value by 1.03 once for every year that has passed by its end.
# หารค่าของแต่ละปีด้วย 1.03 หนึ่งครั้งต่อทุกปีที่ผ่านไปเมื่อสิ้นปีนั้น
factors <- (1 + rate)^(1:4)
rewards <- cbind(health = states$health_per_cycle, cost = states$cost_per_cycle)
edges <- people %*% rewards
ways <- list(start = edges[1:4, ], end = edges[2:5, ], average = (edges[1:4, ] + edges[2:5, ]) / 2)
today <- cbind(start = colSums(ways$start / factors), end = colSums(ways$end / factors),
  average = colSums(ways$average / factors))

#%%@note 5. Rework the three foundation examples
# A yearly rate becomes a chance per cycle, a weight turns years into health, a later cost shrinks.
# อัตรารายปีกลายเป็นโอกาสต่อรอบ ค่าน้ำหนักเปลี่ยนปีเป็นสุขภาพ และต้นทุนในอนาคตมีค่าน้อยลงในวันนี้
cycle_chance <- signif(1 - exp(-rate_per_year * cycle_years), 3)
shortcut <- formatC(rate_per_year * cycle_years, format = "f", digits = 4)
weighted_years <- formatC(years_in_state * c(state_value, 1 - state_value), format = "f",
  digits = 1)

#%%@note 6. Display the trace
# Plot the number of people in each state at the end of each year, no graphics file is saved.
# แสดงจำนวนคนในแต่ละสถานะเมื่อสิ้นแต่ละปี โดยไม่บันทึกไฟล์ภาพ
trace_table <- tibble::tibble(year = rep(0:cycles, times = 3),
  state = rep(states$state, each = cycles + 1), people = as.vector(trace))
print(ggplot2::ggplot(trace_table, ggplot2::aes(x = year, y = people, colour = state)) +
  ggplot2::geom_line())

#%%@note 7. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Row sums of the chances:", rowSums(moves), "| totals, years 0 to 4:", rowSums(trace), "\n")
cat("Year 2 flows from Healthy | Sick | Dead:", flows[1, ], "|", flows[2, ], "|", flows[3, ], "\n")
cat("Healthy, years 0 to 4:", round(trace[, 1], 3), "\n")
cat("Sick, years 0 to 4:", round(trace[, 2], 3), "\n")
cat("Dead, years 0 to 4:", round(trace[, 3], 3), "\n")
cat("Year 1 health, start | end | average:", health, "\n")
cat("Year 1 health per person:", sprintf("%.3f", health / 1000), "\n")
cat("Year 1 cost THB, start | end | average:", big(spend), "\n")
cat("Health by year, start:", at_start, "| total:", sum(at_start), "\n")
cat("Health by year, end:", round(at_end, 3), "| total:", round(sum(at_end), 3), "\n")
cat("Health by year, average:", round(average, 2), "| total:", round(sum(average), 2), "\n")
cat("Rate:", paste0(100 * rate, "%"), "| factors:",
  formatC(factors, format = "f", digits = 4), "\n")
cat("Average health by year, today's value:", round(average_by_year / factors, 1),
  "| total:", round(sum(average_by_year / factors), 3), "\n")
cat("Four-year health today, start | end | average:", sprintf("%.3f", today["health", ]), "\n")
cat("Four-year cost today THB, start | end | average:", round(today["cost", ]), "\n")
cat("Per person, average, health | cost THB:", sprintf("%.3f", today["health", "average"] / 1000),
  round(today["cost", "average"] / 1000), "\n")
cat("Rate 0.12, 0.25 years, chance | shortcut:", cycle_chance, shortcut, "\n")
cat("Ten years at 0.70 | gain from full health:", weighted_years, "\n")
cat("Cost due in year 3 at 3%, today THB:", round(future_cost / (1 + rate)^3), "\n")
สองบรรทัดแรกคือตารางด้านบน บรรทัดสุดท้ายหารแถวค่าเฉลี่ยด้วยคน 1,000 คน
แถบเลื่อนเริ่มที่โอกาสของคลินิก คือ 0.15 จากสุขภาพดีไปป่วย 0.05 จากสุขภาพดีไปเสียชีวิต และ 0.15 จากป่วยไปเสียชีวิต ตลอดสี่รอบรายปีที่อัตรา 3% ต่อปี ลองเลื่อนสุขภาพดีไปป่วยให้สูงขึ้น โอกาสคงสุขภาพดีจะลดลงเพื่อให้แถวยังรวมได้ 1 แล้วดูเส้นสีเขียวของสุขภาพดีลดลงเร็วขึ้น ขณะที่แท่งสามแท่งทางขวาห่างกันมากขึ้น เพราะกลุ่มที่เปลี่ยนเร็วกว่าทำให้จำนวนตอนต้นกับตอนท้ายต่างกันมากกว่า เมื่อเปลี่ยนกฎการนับ cohort trace ทางซ้ายไม่ขยับเลย มีเพียงยอดรวมที่เปลี่ยน

ตรวจตัวอย่างที่ยืมมาทั้งสามด้วย R

โค้ด R foundations.R
# The three foundation examples behind the recaps: a yearly rate, a health weight and a later cost.
# Writes: nothing to disk; the results print in the console.
rate_per_year <- 0.12
cycle_years <- 0.25
years_in_state <- 10
state_value <- 0.70
future_cost <- 1834
rate <- 0.03
cat("Rate 0.12, 0.25 years, chance | shortcut:", signif(1 - exp(-rate_per_year * cycle_years), 3),
  formatC(rate_per_year * cycle_years, format = "f", digits = 4), "\n")
cat("Ten years at 0.70 | gain from full health:",
  formatC(years_in_state * c(state_value, 1 - state_value), format = "f", digits = 1), "\n")
cat("Cost due in year 3 at 3%, today THB:", round(future_cost / (1 + rate)^3), "\n")
ผลลัพธ์จากการรัน foundations.log
Rate 0.12, 0.25 years, chance | shortcut: 0.0296 0.0300
Ten years at 0.70 | gain from full health: 7.0 3.0
Cost due in year 3 at 3%, today THB: 1678

ต้องใช้ R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · ใช้ได้กับ R 4.6.1 · ดาวน์โหลด building-a-markov-cohort-model-step-by-step.R

สคริปต์ฉบับเต็ม
# Building a Markov Cohort Model Step by Step: Follow the Cohort Through Time
# Follow 1,000 people through Healthy, Sick and Dead for four years, then count health and cost.
# ติดตามคน 1,000 คนผ่านสถานะสุขภาพดี ป่วย และเสียชีวิต สี่รอบรายปี แล้วนับสุขภาพและต้นทุน
# Requires: R >= 4.5.2
# Tested: works in R 4.6.1
# tibble 3.3.1 (stable)
# dplyr 1.2.1 (stable)
# ggplot2 4.0.3 (stable)
# install.packages(c("tibble", "dplyr", "ggplot2"))
# update.packages(oldPkgs = c("tibble", "dplyr", "ggplot2"))
# remotes::install_version("tibble", "3.3.1")
# remotes::install_version("dplyr", "1.2.1")
# remotes::install_version("ggplot2", "4.0.3")
library(tibble)  # Write the states and their yearly values as an explicit table.
library(dplyr)  # Add the per-cycle values to that table.
library(ggplot2)  # Display the trace without saving files.
stopifnot(getRversion() >= "4.5.2")

#%%@note 1. Define the illustrative inputs
# Every input is synthetic and written out once, in the order the article uses it.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
start <- c(1000, 0, 0)
transition <- c(0.80, 0.15, 0.05, 0, 0.85, 0.15, 0, 0, 1)
cycles <- 4
first_start <- c(1000, 0, 0)
first_end <- c(800, 150, 50)
health_value <- c(1, 0.6, 0)
yearly_cost <- c(100, 400, 0)
rate <- 0.03
average_by_year <- c(945, 839.25, 742.1625, 653.878125)
rate_per_year <- 0.12
cycle_years <- 0.25
years_in_state <- 10
state_value <- 0.70
future_cost <- 1834
cycle_length <- 1
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
states <- tibble::tibble(state = c("Healthy", "Sick", "Dead"), health = health_value,
  cost = yearly_cost) |>
  dplyr::mutate(health_per_cycle = health * cycle_length, cost_per_cycle = cost * cycle_length)
moves <- matrix(transition, nrow = 3, byrow = TRUE)
stopifnot(all(abs(rowSums(moves) - 1) < 1e-9))

#%%@note 2. Move the cohort one year at a time
# Multiply the counts by the chances each year, and check that the total stays at 1,000.
# คูณจำนวนคนด้วยโอกาสย้ายสถานะทีละปี และตรวจว่ายอดรวมยังเท่ากับ 1,000
trace <- matrix(start, nrow = cycles + 1, ncol = 3, byrow = TRUE)
for (year in seq_len(cycles)) trace[year + 1, ] <- trace[year, ] %*% moves
stopifnot(all(abs(rowSums(trace) - sum(start)) < 1e-9))
flows <- trace[2, ] * moves
people <- trace

#%%@note 3. Count each year at its start, at its end and as the average
# The end count of one year is the start count of the next, so the same trace gives all three.
# จำนวนตอนท้ายปีหนึ่งคือจำนวนตอนต้นปีถัดไป trace เดียวจึงให้การนับได้ทั้งสามแบบ
counts <- rbind(first_start, first_end)
health <- c(counts %*% health_value, mean(counts %*% health_value))
spend <- c(counts %*% yearly_cost, mean(counts %*% yearly_cost))
health_edges <- people %*% health_value
at_start <- health_edges[1:4]
at_end <- health_edges[2:5]
average <- (at_start + at_end) / 2

#%%@note 4. Bring later years back to today
# Divide each year's value by 1.03 once for every year that has passed by its end.
# หารค่าของแต่ละปีด้วย 1.03 หนึ่งครั้งต่อทุกปีที่ผ่านไปเมื่อสิ้นปีนั้น
factors <- (1 + rate)^(1:4)
rewards <- cbind(health = states$health_per_cycle, cost = states$cost_per_cycle)
edges <- people %*% rewards
ways <- list(start = edges[1:4, ], end = edges[2:5, ], average = (edges[1:4, ] + edges[2:5, ]) / 2)
today <- cbind(start = colSums(ways$start / factors), end = colSums(ways$end / factors),
  average = colSums(ways$average / factors))

#%%@note 5. Rework the three foundation examples
# A yearly rate becomes a chance per cycle, a weight turns years into health, a later cost shrinks.
# อัตรารายปีกลายเป็นโอกาสต่อรอบ ค่าน้ำหนักเปลี่ยนปีเป็นสุขภาพ และต้นทุนในอนาคตมีค่าน้อยลงในวันนี้
cycle_chance <- signif(1 - exp(-rate_per_year * cycle_years), 3)
shortcut <- formatC(rate_per_year * cycle_years, format = "f", digits = 4)
weighted_years <- formatC(years_in_state * c(state_value, 1 - state_value), format = "f",
  digits = 1)

#%%@note 6. Display the trace
# Plot the number of people in each state at the end of each year, no graphics file is saved.
# แสดงจำนวนคนในแต่ละสถานะเมื่อสิ้นแต่ละปี โดยไม่บันทึกไฟล์ภาพ
trace_table <- tibble::tibble(year = rep(0:cycles, times = 3),
  state = rep(states$state, each = cycles + 1), people = as.vector(trace))
print(ggplot2::ggplot(trace_table, ggplot2::aes(x = year, y = people, colour = state)) +
  ggplot2::geom_line())

#%%@note 7. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Row sums of the chances:", rowSums(moves), "| totals, years 0 to 4:", rowSums(trace), "\n")
cat("Year 2 flows from Healthy | Sick | Dead:", flows[1, ], "|", flows[2, ], "|", flows[3, ], "\n")
cat("Healthy, years 0 to 4:", round(trace[, 1], 3), "\n")
cat("Sick, years 0 to 4:", round(trace[, 2], 3), "\n")
cat("Dead, years 0 to 4:", round(trace[, 3], 3), "\n")
cat("Year 1 health, start | end | average:", health, "\n")
cat("Year 1 health per person:", sprintf("%.3f", health / 1000), "\n")
cat("Year 1 cost THB, start | end | average:", big(spend), "\n")
cat("Health by year, start:", at_start, "| total:", sum(at_start), "\n")
cat("Health by year, end:", round(at_end, 3), "| total:", round(sum(at_end), 3), "\n")
cat("Health by year, average:", round(average, 2), "| total:", round(sum(average), 2), "\n")
cat("Rate:", paste0(100 * rate, "%"), "| factors:",
  formatC(factors, format = "f", digits = 4), "\n")
cat("Average health by year, today's value:", round(average_by_year / factors, 1),
  "| total:", round(sum(average_by_year / factors), 3), "\n")
cat("Four-year health today, start | end | average:", sprintf("%.3f", today["health", ]), "\n")
cat("Four-year cost today THB, start | end | average:", round(today["cost", ]), "\n")
cat("Per person, average, health | cost THB:", sprintf("%.3f", today["health", "average"] / 1000),
  round(today["cost", "average"] / 1000), "\n")
cat("Rate 0.12, 0.25 years, chance | shortcut:", cycle_chance, shortcut, "\n")
cat("Ten years at 0.70 | gain from full health:", weighted_years, "\n")
cat("Cost due in year 3 at 3%, today THB:", round(future_cost / (1 + rate)^3), "\n")
กล่องนี้คำนวณตัวอย่างทั้งสามจากส่วนทบทวนใหม่ คืออัตราที่แปลงเป็นโอกาสต่อรอบ ปีที่ถ่วงน้ำหนัก และต้นทุนที่ต้องจ่ายในปีที่ 3

ตรวจก่อนเชื่อตัวเลข

การตรวจสามข้อจับความผิดพลาดทางเลขคณิตส่วนใหญ่ในแบบจำลองแบบนี้ได้ ทุกแถวของโอกาสรวมได้ 1 ยอดรวมของทุกปียังเป็น 1,000 คน และจำนวนผู้เสียชีวิตไม่เคยลดลง เพราะไม่มีใครออกจากสถานะนี้

การตรวจข้อที่สี่เป็นเรื่องทางคลินิก ไม่ใช่เลขคณิต แบบจำลองจำได้เพียงว่าคนอยู่ที่ไหนตอนนี้ คนที่เพิ่งป่วยเมื่อปีที่แล้วจึงมีโอกาสเสียชีวิต 0.15 เท่ากับคนที่ป่วยมาสิบปี ถ้าโอกาสนี้ขึ้นกับระยะเวลาที่ป่วย ให้แยกสถานะป่วยเป็นสถานะปีแรกกับสถานะปีต่อ ๆ ไป เพื่อให้แบบจำลองจำเวลาได้ (Siebert and colleagues)

เขียนกฎเรื่องเวลาไว้ข้างผลลัพธ์ ในที่นี้หนึ่งรอบเท่ากับหนึ่งปี และโอกาสย้ายคนหนึ่งครั้งต่อรอบ แต่ละปีนับเป็นค่าเฉลี่ยของตอนต้นกับตอนท้ายปี และแปลงค่าของปีนั้นเป็นมูลค่าวันนี้ที่ตอนท้ายปี

นักวิเคราะห์สองคนที่ใช้โอกาสชุดเดียวกันแต่กฎการนับต่างกันจะรายงานยอดรวมต่างกัน ดังที่ตารางยอดรวมสี่ปีแสดง กล่องสั้นในบทความนี้ใช้เพียง base R ตามแนวทางเรียบง่ายที่ Jalal and colleagues อธิบายไว้สำหรับแบบจำลองการตัดสินใจทางสุขภาพ ส่วนสคริปต์ฉบับเต็มที่ดาวน์โหลดได้เพิ่ม tibble, dplyr และ ggplot2 เพื่อวาด cohort trace

คำศัพท์ตามที่แบบจำลองนี้ใช้

Health state (สถานะสุขภาพ)
ภาวะที่คนหนึ่งคนอยู่ได้ในแต่ละขณะ ในที่นี้คือสุขภาพดี ป่วย หรือเสียชีวิต
Cycle (รอบ)
ช่วงเวลาคงที่ที่แบบจำลองเดินไปทีละช่วง ในที่นี้คือหนึ่งปี
Transition probability (ความน่าจะเป็นของการเปลี่ยนสถานะ)
โอกาสย้ายจากสถานะหนึ่งไปอีกสถานะหนึ่งภายในหนึ่งรอบ โดยแต่ละแถวรวมได้ 1
Absorbing state (สถานะดูดกลืน)
สถานะที่เมื่อเข้าไปแล้วไม่มีใครออก เช่นเสียชีวิต
Markov model (แบบจำลองมาร์คอฟ)
แบบจำลองที่ติดตามกลุ่มคนระหว่างสถานะสุขภาพไม่กี่สถานะทีละรอบ โดยที่คนจะไปไหนต่อขึ้นกับสถานะปัจจุบันเท่านั้น
Cohort trace (ตารางจำนวนคนรายรอบ)
ตารางจำนวนคนในแต่ละสถานะเมื่อสิ้นแต่ละรอบ
Half-cycle correction (การปรับครึ่งรอบ)
การนับแต่ละรอบด้วยค่าเฉลี่ยของจำนวนตอนต้นกับตอนท้ายรอบ เหมือนคนย้ายสถานะกลางรอบ

เชื่อมกับบทความพื้นฐาน

บทความพื้นฐานเรื่อง “ต้นไม้การตัดสินใจและแบบจำลองกลุ่มประชากรเปลี่ยนหลักฐานเป็นการตัดสินใจอย่างไร” แสดงว่าแบบจำลองนี้อยู่ตรงไหนเมื่อเทียบกับแบบจำลองชนิดอื่น บทความเรื่อง “QALY และค่าน้ำหนักเบื้องหลัง” แสดงว่าค่าน้ำหนักอย่าง 0.6 มาจากไหน และบทความเรื่อง “การคิดต้นทุนและมูลค่าวันนี้” คำนวณอัตรา 3% อย่างละเอียด

ลองใช้ในแอป

เปิดแอปแบบจำลองมาร์คอฟของเราเพื่อลองเปลี่ยนค่าข้อมูลนำเข้าและดูผลลัพธ์เปลี่ยนตาม ต้องเข้าสู่ระบบและใช้แผน Pro หรือ Max

แอปเริ่มจากค่าตัวอย่างของแอปเอง ตัวเลขในแอปจึงไม่ตรงกับบทความนี้

เอกสารอ้างอิง

  1. Siebert Uwe, Alagoz Oguzhan, Bayoumi Ahmed M., Jahn Beate, Owens Douglas K., Cohen David J., Kuntz Karen M.. State-Transition Modeling: A Report of the ISPOR-SMDM Modeling Good Research Practices Task Force-3. Value in Health. 2012;15(6):812-820. doi:10.1016/j.jval.2012.06.014. https://doi.org/10.1016/j.jval.2012.06.014
  2. 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
  3. 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://doi.org/10.1177/0272989x16686559

ลำดับการเรียนในชุดนี้

  1. คู่มือชุดบทความการสร้างแบบจำลองเศรษฐศาสตร์สุขภาพ: จากหลักฐานสู่การตัดสินใจ
  2. การเลือกแบบจำลองเพื่อการตัดสินใจ: ให้เส้นทางทางคลินิกกำหนดโครงสร้าง
  3. ข้อมูลนำเข้าของแบบจำลองที่ตรวจสอบได้: สร้างตารางพารามิเตอร์ที่สืบย้อนถึงที่มา
  4. การค้นหาข้อมูลนำเข้าของแบบจำลองจากวรรณกรรม: เลือกค่าประมาณให้ตรงกับคำถาม
  5. อัตรา ความน่าจะเป็น และความน่าจะเป็นของการเปลี่ยนสถานะ: คำนวณโดยไม่หลงหน่วยเวลา
  6. สร้างแผนภูมิต้นไม้การตัดสินใจทีละขั้น: ตามรอยผลลัพธ์ ต้นทุน และสุขภาพ
  7. สร้างแบบจำลองมาร์คอฟแบบกลุ่มประชากรทีละขั้น: ติดตามประชากรผ่านเวลา (บทความนี้)
  8. การวิเคราะห์ผลกระทบด้านงบประมาณในทางปฏิบัติ: นับคนให้ชัดก่อนคำนวณเงิน
  9. ICER และผลประโยชน์สุทธิในรูปตัวเงินเมื่อมีหลายทางเลือก: หาขอบเขตทางเลือกที่มีประสิทธิภาพ
  10. การวิเคราะห์ความไวแบบอาศัยความน่าจะเป็น: จากการแจกแจงพารามิเตอร์สู่ CEAC
  11. การประเมินความสมเหตุสมผลและตรวจสอบแบบจำลอง: ทดสอบคำถาม สมการ และโค้ด
  12. มูลค่าคาดหมายของข้อมูลสมบูรณ์: หลักฐานที่ดีขึ้นอาจมีมูลค่าเพียงใด

ประเด็นสำคัญ

  • ตั้งชื่อสถานะก่อน แล้วจึงเขียนโอกาสของการย้ายแต่ละทางในหนึ่งรอบ โดยแต่ละแถวของโอกาสต้องรวมได้ 1
  • ย้ายคนทั้งกลุ่มทีละรอบ คนสุขภาพดี 1,000 คนกลายเป็นสุขภาพดี 800 คน ป่วย 150 คน และเสียชีวิต 50 คน โดยยอดรวมยังเป็น 1,000 คนทุกปี
  • นับแต่ละรอบที่กลางรอบ การเฉลี่ยตอนต้นกับตอนท้ายปีแรกให้ 0.945 QALY ต่อคน ไม่ใช่ 0.890 จากการนับตอนท้าย
  • เขียนกฎเรื่องเวลาไว้ข้างยอดรวม เพราะ cohort trace เดียวกันให้ 2,787.786 ถึง 3,149.605 QALY ในมูลค่าวันนี้ ขึ้นกับวิธีนับแต่ละปี

อ่านต่อในวิกิ: ต้นไม้การตัดสินใจ แบบจำลองมาร์คอฟ และการวิเคราะห์ความไวเชิงความน่าจะเป็น: แบบจำลองแปลงหลักฐานเป็นการตัดสินใจได้อย่างไร · ปีสุขภาวะ ปีสุขภาวะที่สูญเสีย และค่าอรรถประโยชน์: เรากำลังวัดอะไรกันแน่

0
ถึงนักอ่านชาวไทยและต่างชาติทำความเข้าใจบริบททางการแพทย์ของผมอ่านต่อ →ถึงนักอ่านชาวไทยและต่างชาติทำความเข้าใจเนื้อหาของผมที่นอกเหนือจากการแพทย์อ่านต่อ →

ความคิดเห็น

ยังไม่มีความคิดเห็น มาเป็นคนแรกกันเลย

เข้าสู่ระบบเพื่อแสดงความคิดเห็น