Choosing a Decision-Analytic Model: Let the Clinical Pathway Decide

On this page
อ่านฉบับภาษาไทย (Thai version)
Abstract
An emergency doctor sees an adult with cellulitis of the leg and must choose antibiotic tablets at home or admission for drip antibiotics. Before any software, the question shapes the model: which patients, which complete care plans, how long consequences count and whose costs count. For one episode over two weeks a decision tree is enough. If tablets work with a chance of 0.8, giving 10 symptom-free days, and the other patients are admitted and get 2, the plan averages 8.4 days. Ask instead about a year in which the infection can return every month, and the tree doubles its branch ends every month, to 4,096, while two reusable states, well and active cellulitis, do the same job. When the risk depends on past episodes, the states must remember them: ignoring that history halves the benefit of prevention after a recurrence, from 7.5 to 3.75 episodes avoided a month. Let the pathway choose the structure.
One patient, two plans
An adult comes to a district hospital's emergency department with a red, hot, swollen lower leg: cellulitis, an infection of the skin. The doctor can send the patient home on antibiotic tablets or admit them for antibiotics through a drip. The team's illustrative estimate is that tablets work at home for 0.8 of such patients; the rest come back and are admitted. The team counts symptom-free days in the two weeks after the visit: 10 if the tablets work and 2 if the patient ends up admitted.
Before choosing any model, write the question down. A strategy is a complete care plan, not a drug name: tablets at home include admission if the tablets fail. The time horizon is how long consequences are counted, here two weeks, because the question is how to treat this one episode. The perspective says whose costs and outcomes count; here it is the hospital's, which counts beds and drugs, while a wider view of society would also count days off work (Briggs and colleagues).
Then sketch with the clinicians what can happen. An event is something that happens at one moment, such as the tablets failing. A state is a condition a patient stays in for a while, such as being well or having active infection. Mixing the two can count a cost twice: the admission is an event, while the days in a hospital bed belong to a state.
One episode: a decision tree is enough
Over two weeks this pathway has a clear start and end. Each patient on tablets takes exactly one branch: the tablets work, or they fail and the patient is admitted. The branches cannot both happen and together cover every patient, so their chances add up to 1. To get the plan's average, weight each branch's days by its chance and add the results.
Tablets at home: weight each branch, then add
Synthetic numbers for one patient started on tablets at home, counted over two weeks.
-
List the branches
\[ 1-0.8=0.2 \]
Tablets work with chance 0.8 and give 10 symptom-free days. The patient is admitted with the remaining chance, 0.2, and gets 2 days.
-
Weight each branch
\[ 0.8\times 10=8 \]
\[ 0.2\times 2=0.4 \]
The branch where the tablets work contributes 8 days; the admission branch contributes 0.4 of a day.
-
Add the weighted branches
\[ 8+0.4=8.4 \]
The two contributions add up to the plan's average per patient.
Result: Tablets at home give 8.4 symptom-free days per patient on average over the two weeks.
The name: a decision tree, rolled back
This drawing is called a decision tree: a square for the choice, a circle where chance takes over, and days at the branch ends. Working from the ends back to the square, as in the steps above, is called rolling the tree back. The result, 8.4 days, is the plan's expected value: its average per patient. The admission plan is rolled back the same way with its own chances and days; this article follows only the tablets plan, and later parts of the series compare whole plans.
# Illustrative symptom-free days for one leg cellulitis episode; synthetic numbers.
# Writes: nothing to disk; the results print in the console.
p_success <- 0.8
value_success <- 10
value_failure <- 2
p_failure <- 1 - p_success
weighted_success <- p_success * value_success
weighted_failure <- p_failure * value_failure
expected_days <- weighted_success + weighted_failure
cat("Success, failure probability:", p_success, p_failure, "\n")
cat("Weighted days, success and failure:", weighted_success, weighted_failure, "\n")
cat("Expected symptom-free days:", expected_days, "\n")
Success, failure probability: 0.8 0.2
Weighted days, success and failure: 8 0.4
Expected symptom-free days: 8.4
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download choosing-a-decision-analytic-model.R
Full tidy script
# Choosing a Decision-Analytic Model: Let the Clinical Pathway Decide
# Follow one leg cellulitis case from a two-week tree to monthly states with memory.
# ติดตามผู้ป่วยเซลลูไลติสที่ขาจากต้นไม้สองสัปดาห์ ไปสู่ภาวะรายเดือนที่จำอดีตได้
# 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 model inputs as explicit tables.
library(dplyr) # Calculate the weighted and split quantities.
library(ggplot2) # Display the tree size against the reused states 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
p_success <- 0.8
value_success <- 10
value_failure <- 2
periods <- c(1, 2, 3, 10, 12)
states <- c("well", "active cellulitis")
cohort <- 1000
risk_month <- 0.03
well_first <- 750
well_again <- 250
risk_first <- 0.02
risk_again <- 0.06
prevent_cut <- 0.5
recovered <- 1000
risk_month1 <- 0.10
risk_month2 <- 0.05
#%%@note 2. Roll back the two-week tree
# Weight each branch's symptom-free days by its chance, then add the two products.
# ถ่วงวันที่ไม่มีอาการของแต่ละกิ่งด้วยโอกาสของกิ่งนั้น แล้วรวมสองผลคูณ
branches <- tibble::tibble(
branch = c("tablets work", "admitted"), probability = c(p_success, 1 - p_success),
days = c(value_success, value_failure)
) |>
dplyr::mutate(weighted_days = probability * days)
stopifnot(abs(sum(branches$probability) - 1) < 1e-9)
p_failure <- 1 - p_success
weighted_success <- branches$weighted_days[1]
weighted_failure <- branches$weighted_days[2]
expected_days <- weighted_success + weighted_failure
#%%@note 3. Count tree endpoints and take one monthly state step
# A tree doubles its endpoints every month; a state model reuses the same two states.
# ต้นไม้เพิ่มปลายกิ่งเป็นสองเท่าทุกเดือน ส่วนแบบจำลองภาวะใช้สองภาวะเดิมซ้ำ
endpoints <- 2^periods
comma_format <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
month_step <- cohort * c(1, risk_month, 1 - risk_month)
growth <- tibble::tibble(month = periods, tree_endpoints = endpoints,
states_reused = length(states))
#%%@note 4. Split the well state by history and by time since recovery
# History states keep 0.02 and 0.06 apart; tunnel states keep the first two months apart.
# ภาวะที่เก็บประวัติแยกความเสี่ยง 0.02 กับ 0.06 ส่วนภาวะอุโมงค์แยกสองเดือนแรกหลังหาย
groups <- tibble::tibble(group = c("no recurrence", "after a recurrence"),
people = c(well_first, well_again), risk = c(risk_first, risk_again)) |>
dplyr::mutate(new_episodes = people * risk)
new_first <- groups$new_episodes[1]
new_again <- groups$new_episodes[2]
average_risk <- (new_first + new_again) / (well_first + well_again)
tunnel_free <- recovered * (1 - risk_month1) * (1 - risk_month2)
flat_free <- recovered * (1 - risk_month)^2
flat_returns <- recovered - flat_free
tunnel_returns <- recovered - tunnel_free
one_decimal <- function(value) formatC(value, format = "f", digits = 1)
#%%@note 5. Display the model
# Plot tree endpoints against the reused states on a log scale; no graphics file is saved.
# แสดงจำนวนปลายกิ่งเทียบกับภาวะที่ใช้ซ้ำบนสเกลลอการิทึม โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(growth, ggplot2::aes(x = month)) +
ggplot2::geom_col(ggplot2::aes(y = tree_endpoints)) +
ggplot2::geom_point(ggplot2::aes(y = states_reused)) +
ggplot2::scale_y_log10() + ggplot2::labs(x = "Monthly periods", y = "Count (log scale)"))
#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Success, failure probability:", p_success, p_failure, "\n")
cat("Weighted days, success and failure:", weighted_success, weighted_failure, "\n")
cat("Expected symptom-free days:", expected_days, "\n")
cat("Monthly periods:", periods, "\n")
cat("Tree endpoints:", comma_format(endpoints), "\n")
cat("States reused every month:", length(states), "\n")
cat("Well now, new episodes, still well:", comma_format(month_step), "\n")
cat("New episodes next month, no recurrence and after one:", new_first, new_again, "\n")
cat("Average monthly risk in one well state:", average_risk, "\n")
cat("Episodes avoided a month, history states:", new_again * prevent_cut, "\n")
cat("Episodes avoided a month, one well state:", well_again * average_risk * prevent_cut, "\n")
cat("Free of a new episode after month 1, tunnel:", recovered * (1 - risk_month1), "\n")
cat("Free after month 2, tunnel and flat 0.03:", tunnel_free, one_decimal(flat_free), "\n")
cat("Returns within two months, tunnel and flat:", tunnel_returns, one_decimal(flat_returns), "\n")
When episodes return, use states
Now change the question. Many patients get cellulitis again, and the hospital asks whether patients who recover should take a preventive antibiotic for a year. A year in which the infection can return every month does not fit a short tree. The monthly risks from here on are invented for teaching; a real model takes them from studies.
Suppose a new episode can start in any month. A tree cannot loop back to an earlier branch, so it must split every month into new episode or no new episode. Its endpoints double: 2 after one month, 4 after two, 8 after three and 4,096 after 12 months. Each endpoint also needs its own days and costs, which no team can fill in by hand.
A state model avoids the growth. It sorts patients into a few states, each patient in exactly one at a time, here well and active cellulitis, and moves them between the states once per cycle, a fixed time step of one month. Of 1,000 patients well this month, at a monthly risk of 0.03, 30 start a new episode next month and 970 stay well. The same two states are reused every month, however long the horizon.
Following a whole group this way, as shares spread across states, is called a cohort state-transition model. It needs a monthly risk of a new episode and a monthly chance of recovery, not a separate value for each of the 4,096 paths.
# Illustrative tree size and one monthly state step when cellulitis can return; synthetic numbers.
# Writes: nothing to disk; the results print in the console.
periods <- c(1, 2, 3, 10, 12)
states <- c("well", "active cellulitis")
cohort <- 1000
risk_month <- 0.03
endpoints <- 2^periods
month_step <- cohort * c(1, risk_month, 1 - risk_month)
comma_format <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
cat("Monthly periods:", periods, "\n")
cat("Tree endpoints:", comma_format(endpoints), "\n")
cat("States reused every month:", length(states), "\n")
cat("Well now, new episodes, still well:", comma_format(month_step), "\n")
Monthly periods: 1 2 3 10 12
Tree endpoints: 2 4 8 1,024 4,096
States reused every month: 2
Well now, new episodes, still well: 1,000 30 970
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download choosing-a-decision-analytic-model.R
Full tidy script
# Choosing a Decision-Analytic Model: Let the Clinical Pathway Decide
# Follow one leg cellulitis case from a two-week tree to monthly states with memory.
# ติดตามผู้ป่วยเซลลูไลติสที่ขาจากต้นไม้สองสัปดาห์ ไปสู่ภาวะรายเดือนที่จำอดีตได้
# 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 model inputs as explicit tables.
library(dplyr) # Calculate the weighted and split quantities.
library(ggplot2) # Display the tree size against the reused states 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
p_success <- 0.8
value_success <- 10
value_failure <- 2
periods <- c(1, 2, 3, 10, 12)
states <- c("well", "active cellulitis")
cohort <- 1000
risk_month <- 0.03
well_first <- 750
well_again <- 250
risk_first <- 0.02
risk_again <- 0.06
prevent_cut <- 0.5
recovered <- 1000
risk_month1 <- 0.10
risk_month2 <- 0.05
#%%@note 2. Roll back the two-week tree
# Weight each branch's symptom-free days by its chance, then add the two products.
# ถ่วงวันที่ไม่มีอาการของแต่ละกิ่งด้วยโอกาสของกิ่งนั้น แล้วรวมสองผลคูณ
branches <- tibble::tibble(
branch = c("tablets work", "admitted"), probability = c(p_success, 1 - p_success),
days = c(value_success, value_failure)
) |>
dplyr::mutate(weighted_days = probability * days)
stopifnot(abs(sum(branches$probability) - 1) < 1e-9)
p_failure <- 1 - p_success
weighted_success <- branches$weighted_days[1]
weighted_failure <- branches$weighted_days[2]
expected_days <- weighted_success + weighted_failure
#%%@note 3. Count tree endpoints and take one monthly state step
# A tree doubles its endpoints every month; a state model reuses the same two states.
# ต้นไม้เพิ่มปลายกิ่งเป็นสองเท่าทุกเดือน ส่วนแบบจำลองภาวะใช้สองภาวะเดิมซ้ำ
endpoints <- 2^periods
comma_format <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
month_step <- cohort * c(1, risk_month, 1 - risk_month)
growth <- tibble::tibble(month = periods, tree_endpoints = endpoints,
states_reused = length(states))
#%%@note 4. Split the well state by history and by time since recovery
# History states keep 0.02 and 0.06 apart; tunnel states keep the first two months apart.
# ภาวะที่เก็บประวัติแยกความเสี่ยง 0.02 กับ 0.06 ส่วนภาวะอุโมงค์แยกสองเดือนแรกหลังหาย
groups <- tibble::tibble(group = c("no recurrence", "after a recurrence"),
people = c(well_first, well_again), risk = c(risk_first, risk_again)) |>
dplyr::mutate(new_episodes = people * risk)
new_first <- groups$new_episodes[1]
new_again <- groups$new_episodes[2]
average_risk <- (new_first + new_again) / (well_first + well_again)
tunnel_free <- recovered * (1 - risk_month1) * (1 - risk_month2)
flat_free <- recovered * (1 - risk_month)^2
flat_returns <- recovered - flat_free
tunnel_returns <- recovered - tunnel_free
one_decimal <- function(value) formatC(value, format = "f", digits = 1)
#%%@note 5. Display the model
# Plot tree endpoints against the reused states on a log scale; no graphics file is saved.
# แสดงจำนวนปลายกิ่งเทียบกับภาวะที่ใช้ซ้ำบนสเกลลอการิทึม โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(growth, ggplot2::aes(x = month)) +
ggplot2::geom_col(ggplot2::aes(y = tree_endpoints)) +
ggplot2::geom_point(ggplot2::aes(y = states_reused)) +
ggplot2::scale_y_log10() + ggplot2::labs(x = "Monthly periods", y = "Count (log scale)"))
#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Success, failure probability:", p_success, p_failure, "\n")
cat("Weighted days, success and failure:", weighted_success, weighted_failure, "\n")
cat("Expected symptom-free days:", expected_days, "\n")
cat("Monthly periods:", periods, "\n")
cat("Tree endpoints:", comma_format(endpoints), "\n")
cat("States reused every month:", length(states), "\n")
cat("Well now, new episodes, still well:", comma_format(month_step), "\n")
cat("New episodes next month, no recurrence and after one:", new_first, new_again, "\n")
cat("Average monthly risk in one well state:", average_risk, "\n")
cat("Episodes avoided a month, history states:", new_again * prevent_cut, "\n")
cat("Episodes avoided a month, one well state:", well_again * average_risk * prevent_cut, "\n")
cat("Free of a new episode after month 1, tunnel:", recovered * (1 - risk_month1), "\n")
cat("Free after month 2, tunnel and flat 0.03:", tunnel_free, one_decimal(flat_free), "\n")
cat("Returns within two months, tunnel and flat:", tunnel_returns, one_decimal(flat_returns), "\n")
When the risk remembers, split the states
A plain two-state model has no memory. It assumes the chance of moving next depends only on the state a patient is in now, not on how they got there; we call this the no-memory rule (Siebert and colleagues). That is a choice the modeller makes, not a fact about cellulitis. Each episode can damage the leg's lymph drainage, so a patient who has already had a recurrence is more likely to have another.
Say 1,000 patients are well: 750 have never had a recurrence, with a monthly risk of 0.02, and 250 have had one, with a risk of 0.06. Next month 750 × 0.02 = 15 and 250 × 0.06 = 15 new episodes start, 30 in all. That is an average risk of 0.03, so a single well state at 0.03 gets this month right.
The trouble starts when the decision depends on that history. Suppose the preventive antibiotic halves the risk and is offered only to the 250 patients after a recurrence. With their own risk of 0.06 they have 15 episodes a month, so halving it avoids 7.5. A single well state can only give them the average, so this group has 250 × 0.03 = 7.5 new episodes a month; halving that seems to avoid only 3.75, half the true benefit.
Splitting a state by what happened before is called using history states: here well with no previous recurrence and well after a recurrence, each with its own risk. Now the model can tell who gets prevention. It also needs evidence on risk by past episodes, the 0.02 and the 0.06, not just their average.
# Illustrative monthly risk of a new episode split by past recurrence; synthetic numbers.
# Writes: nothing to disk; the results print in the console.
well_first <- 750
well_again <- 250
risk_first <- 0.02
risk_again <- 0.06
prevent_cut <- 0.5
new_first <- well_first * risk_first
new_again <- well_again * risk_again
average_risk <- (new_first + new_again) / (well_first + well_again)
cat("New episodes next month, no recurrence and after one:", new_first, new_again, "\n")
cat("Average monthly risk in one well state:", average_risk, "\n")
cat("Episodes avoided a month, history states:", new_again * prevent_cut, "\n")
cat("Episodes avoided a month, one well state:", well_again * average_risk * prevent_cut, "\n")
New episodes next month, no recurrence and after one: 15 15
Average monthly risk in one well state: 0.03
Episodes avoided a month, history states: 7.5
Episodes avoided a month, one well state: 3.75
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download choosing-a-decision-analytic-model.R
Full tidy script
# Choosing a Decision-Analytic Model: Let the Clinical Pathway Decide
# Follow one leg cellulitis case from a two-week tree to monthly states with memory.
# ติดตามผู้ป่วยเซลลูไลติสที่ขาจากต้นไม้สองสัปดาห์ ไปสู่ภาวะรายเดือนที่จำอดีตได้
# 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 model inputs as explicit tables.
library(dplyr) # Calculate the weighted and split quantities.
library(ggplot2) # Display the tree size against the reused states 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
p_success <- 0.8
value_success <- 10
value_failure <- 2
periods <- c(1, 2, 3, 10, 12)
states <- c("well", "active cellulitis")
cohort <- 1000
risk_month <- 0.03
well_first <- 750
well_again <- 250
risk_first <- 0.02
risk_again <- 0.06
prevent_cut <- 0.5
recovered <- 1000
risk_month1 <- 0.10
risk_month2 <- 0.05
#%%@note 2. Roll back the two-week tree
# Weight each branch's symptom-free days by its chance, then add the two products.
# ถ่วงวันที่ไม่มีอาการของแต่ละกิ่งด้วยโอกาสของกิ่งนั้น แล้วรวมสองผลคูณ
branches <- tibble::tibble(
branch = c("tablets work", "admitted"), probability = c(p_success, 1 - p_success),
days = c(value_success, value_failure)
) |>
dplyr::mutate(weighted_days = probability * days)
stopifnot(abs(sum(branches$probability) - 1) < 1e-9)
p_failure <- 1 - p_success
weighted_success <- branches$weighted_days[1]
weighted_failure <- branches$weighted_days[2]
expected_days <- weighted_success + weighted_failure
#%%@note 3. Count tree endpoints and take one monthly state step
# A tree doubles its endpoints every month; a state model reuses the same two states.
# ต้นไม้เพิ่มปลายกิ่งเป็นสองเท่าทุกเดือน ส่วนแบบจำลองภาวะใช้สองภาวะเดิมซ้ำ
endpoints <- 2^periods
comma_format <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
month_step <- cohort * c(1, risk_month, 1 - risk_month)
growth <- tibble::tibble(month = periods, tree_endpoints = endpoints,
states_reused = length(states))
#%%@note 4. Split the well state by history and by time since recovery
# History states keep 0.02 and 0.06 apart; tunnel states keep the first two months apart.
# ภาวะที่เก็บประวัติแยกความเสี่ยง 0.02 กับ 0.06 ส่วนภาวะอุโมงค์แยกสองเดือนแรกหลังหาย
groups <- tibble::tibble(group = c("no recurrence", "after a recurrence"),
people = c(well_first, well_again), risk = c(risk_first, risk_again)) |>
dplyr::mutate(new_episodes = people * risk)
new_first <- groups$new_episodes[1]
new_again <- groups$new_episodes[2]
average_risk <- (new_first + new_again) / (well_first + well_again)
tunnel_free <- recovered * (1 - risk_month1) * (1 - risk_month2)
flat_free <- recovered * (1 - risk_month)^2
flat_returns <- recovered - flat_free
tunnel_returns <- recovered - tunnel_free
one_decimal <- function(value) formatC(value, format = "f", digits = 1)
#%%@note 5. Display the model
# Plot tree endpoints against the reused states on a log scale; no graphics file is saved.
# แสดงจำนวนปลายกิ่งเทียบกับภาวะที่ใช้ซ้ำบนสเกลลอการิทึม โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(growth, ggplot2::aes(x = month)) +
ggplot2::geom_col(ggplot2::aes(y = tree_endpoints)) +
ggplot2::geom_point(ggplot2::aes(y = states_reused)) +
ggplot2::scale_y_log10() + ggplot2::labs(x = "Monthly periods", y = "Count (log scale)"))
#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Success, failure probability:", p_success, p_failure, "\n")
cat("Weighted days, success and failure:", weighted_success, weighted_failure, "\n")
cat("Expected symptom-free days:", expected_days, "\n")
cat("Monthly periods:", periods, "\n")
cat("Tree endpoints:", comma_format(endpoints), "\n")
cat("States reused every month:", length(states), "\n")
cat("Well now, new episodes, still well:", comma_format(month_step), "\n")
cat("New episodes next month, no recurrence and after one:", new_first, new_again, "\n")
cat("Average monthly risk in one well state:", average_risk, "\n")
cat("Episodes avoided a month, history states:", new_again * prevent_cut, "\n")
cat("Episodes avoided a month, one well state:", well_again * average_risk * prevent_cut, "\n")
cat("Free of a new episode after month 1, tunnel:", recovered * (1 - risk_month1), "\n")
cat("Free after month 2, tunnel and flat 0.03:", tunnel_free, one_decimal(flat_free), "\n")
cat("Returns within two months, tunnel and flat:", tunnel_returns, one_decimal(flat_returns), "\n")
The usual name for the no-memory rule: the Markov assumption
The name in this heading belongs to the no-memory rule above. History states do not break the rule; they move the memory into the state itself, so the next move still depends only on the state a patient is in now. A later part of this series builds such a model cycle by cycle.
Memory of time: tunnel states
Memory of time works the same way. After an episode clears, the risk of another is highest at first: say 0.10 in the first month, 0.05 in the second, then settling at 0.03 a month from month 3. Tunnel states are a fixed chain of temporary well states, one per month after recovery, each with its own risk. Patients pass through them in order and then join the usual well state.
Of 1,000 patients who have just recovered, 1,000 × (1 − 0.10) = 900 are still free of a new episode after month 1, and 900 × (1 − 0.05) = 855 after month 2. So 145 return within two months. A single well state at 0.03 predicts 1,000 × (1 − 0.03) × (1 − 0.03) = 940.9 still free, only 59.1 returns: it misses more than half of the early returns.
When the memory needed grows too detailed for states, such as the number of past episodes and the months since the last one together, follow patients one at a time instead. This is called individual simulation: it needs the same risks, recorded against each patient's own history.
# Illustrative risk of a new episode in the months after recovery; synthetic numbers.
# Writes: nothing to disk; the results print in the console.
recovered <- 1000
risk_month1 <- 0.10
risk_month2 <- 0.05
risk_month <- 0.03
tunnel_free <- recovered * (1 - risk_month1) * (1 - risk_month2)
flat_free <- recovered * (1 - risk_month)^2
flat_returns <- recovered - flat_free
tunnel_returns <- recovered - tunnel_free
one_decimal <- function(value) formatC(value, format = "f", digits = 1)
cat("Free of a new episode after month 1, tunnel:", recovered * (1 - risk_month1), "\n")
cat("Free after month 2, tunnel and flat 0.03:", tunnel_free, one_decimal(flat_free), "\n")
cat("Returns within two months, tunnel and flat:", tunnel_returns, one_decimal(flat_returns), "\n")
Free of a new episode after month 1, tunnel: 900
Free after month 2, tunnel and flat 0.03: 855 940.9
Returns within two months, tunnel and flat: 145 59.1
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download choosing-a-decision-analytic-model.R
Full tidy script
# Choosing a Decision-Analytic Model: Let the Clinical Pathway Decide
# Follow one leg cellulitis case from a two-week tree to monthly states with memory.
# ติดตามผู้ป่วยเซลลูไลติสที่ขาจากต้นไม้สองสัปดาห์ ไปสู่ภาวะรายเดือนที่จำอดีตได้
# 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 model inputs as explicit tables.
library(dplyr) # Calculate the weighted and split quantities.
library(ggplot2) # Display the tree size against the reused states 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
p_success <- 0.8
value_success <- 10
value_failure <- 2
periods <- c(1, 2, 3, 10, 12)
states <- c("well", "active cellulitis")
cohort <- 1000
risk_month <- 0.03
well_first <- 750
well_again <- 250
risk_first <- 0.02
risk_again <- 0.06
prevent_cut <- 0.5
recovered <- 1000
risk_month1 <- 0.10
risk_month2 <- 0.05
#%%@note 2. Roll back the two-week tree
# Weight each branch's symptom-free days by its chance, then add the two products.
# ถ่วงวันที่ไม่มีอาการของแต่ละกิ่งด้วยโอกาสของกิ่งนั้น แล้วรวมสองผลคูณ
branches <- tibble::tibble(
branch = c("tablets work", "admitted"), probability = c(p_success, 1 - p_success),
days = c(value_success, value_failure)
) |>
dplyr::mutate(weighted_days = probability * days)
stopifnot(abs(sum(branches$probability) - 1) < 1e-9)
p_failure <- 1 - p_success
weighted_success <- branches$weighted_days[1]
weighted_failure <- branches$weighted_days[2]
expected_days <- weighted_success + weighted_failure
#%%@note 3. Count tree endpoints and take one monthly state step
# A tree doubles its endpoints every month; a state model reuses the same two states.
# ต้นไม้เพิ่มปลายกิ่งเป็นสองเท่าทุกเดือน ส่วนแบบจำลองภาวะใช้สองภาวะเดิมซ้ำ
endpoints <- 2^periods
comma_format <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
month_step <- cohort * c(1, risk_month, 1 - risk_month)
growth <- tibble::tibble(month = periods, tree_endpoints = endpoints,
states_reused = length(states))
#%%@note 4. Split the well state by history and by time since recovery
# History states keep 0.02 and 0.06 apart; tunnel states keep the first two months apart.
# ภาวะที่เก็บประวัติแยกความเสี่ยง 0.02 กับ 0.06 ส่วนภาวะอุโมงค์แยกสองเดือนแรกหลังหาย
groups <- tibble::tibble(group = c("no recurrence", "after a recurrence"),
people = c(well_first, well_again), risk = c(risk_first, risk_again)) |>
dplyr::mutate(new_episodes = people * risk)
new_first <- groups$new_episodes[1]
new_again <- groups$new_episodes[2]
average_risk <- (new_first + new_again) / (well_first + well_again)
tunnel_free <- recovered * (1 - risk_month1) * (1 - risk_month2)
flat_free <- recovered * (1 - risk_month)^2
flat_returns <- recovered - flat_free
tunnel_returns <- recovered - tunnel_free
one_decimal <- function(value) formatC(value, format = "f", digits = 1)
#%%@note 5. Display the model
# Plot tree endpoints against the reused states on a log scale; no graphics file is saved.
# แสดงจำนวนปลายกิ่งเทียบกับภาวะที่ใช้ซ้ำบนสเกลลอการิทึม โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(growth, ggplot2::aes(x = month)) +
ggplot2::geom_col(ggplot2::aes(y = tree_endpoints)) +
ggplot2::geom_point(ggplot2::aes(y = states_reused)) +
ggplot2::scale_y_log10() + ggplot2::labs(x = "Monthly periods", y = "Count (log scale)"))
#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Success, failure probability:", p_success, p_failure, "\n")
cat("Weighted days, success and failure:", weighted_success, weighted_failure, "\n")
cat("Expected symptom-free days:", expected_days, "\n")
cat("Monthly periods:", periods, "\n")
cat("Tree endpoints:", comma_format(endpoints), "\n")
cat("States reused every month:", length(states), "\n")
cat("Well now, new episodes, still well:", comma_format(month_step), "\n")
cat("New episodes next month, no recurrence and after one:", new_first, new_again, "\n")
cat("Average monthly risk in one well state:", average_risk, "\n")
cat("Episodes avoided a month, history states:", new_again * prevent_cut, "\n")
cat("Episodes avoided a month, one well state:", well_again * average_risk * prevent_cut, "\n")
cat("Free of a new episode after month 1, tunnel:", recovered * (1 - risk_month1), "\n")
cat("Free after month 2, tunnel and flat 0.03:", tunnel_free, one_decimal(flat_free), "\n")
cat("Returns within two months, tunnel and flat:", tunnel_returns, one_decimal(flat_returns), "\n")
When patients affect each other
Some questions need more than memory. If vaccinating one person lowers another person's risk of influenza, each person's risk depends on how many others are infected, and the model must link them. Shared capacity works the same way: when the ward's beds are full, admitting one patient with cellulitis delays the next. A tree or a state model treats each patient alone, so these questions need a transmission model or a queue model, built on contact patterns or on arrival and waiting times.
Cellulitis does not spread between patients this way, so the model leaves interaction out on purpose and says so. Bed pressure could matter if the admission plan fills the ward, and the team records that as a known limit.
Each structure asks for different evidence
The structure decides which numbers the team must find, and the table lists them for each kind of pathway. Only a study that reports recurrence by past episodes can give the 0.02 and 0.06 that history states need. So choose the structure from the pathway first, then search for evidence that fits it; a more complex model is not better in itself, it only asks more of the evidence.
Match the pathway to a structure and its evidence
| What the pathway does | Structure | Evidence it needs | In the cellulitis case |
|---|---|---|---|
| One bounded episode, no return that matters | Decision tree | Branch chances and the days or costs at each end | 0.8, with 10 and 2 days |
| Episodes return over time | States moved once per cycle | Monthly risk of moving and the days or costs of each state | 0.03 a month |
| Risk depends on past episodes | History states, or individual simulation | Risk split by history | 0.02 and 0.06 |
| Risk depends on time since an episode | Tunnel states | Risk by month after recovery | 0.10, then 0.05 |
| Patients affect each other or share beds | Transmission or queue model | Contact patterns, or arrival and waiting times | Left out, recorded as a limit |
Arithmetic cannot choose the structure
The 8.4 days checks the tree's arithmetic. It does not show that returns, death or long-term harm are safe to leave out; leaving them out is acceptable here only because the question covers one episode over two weeks. The year-long prevention question needs states, and the 4,096 endpoints show why.
Ask a clinician to trace a patient who does not fit the drawing, such as one whose infection returns a week after the tablets end. Ask whether adding that path could change the choice between tablets and admission. Record the answer and the reason for each thing left out, so others can check and test the model (Eddy and colleagues).
Terms as we use them
- Strategy (กลยุทธ์)
- A complete care plan being compared, such as tablets at home including admission if they fail.
- Time horizon (ช่วงเวลาที่นับผล)
- How long consequences are counted: two weeks for one episode, a year for prevention.
- Perspective (มุมมอง)
- Whose costs and outcomes count, such as the hospital's or society's.
- Event (เหตุการณ์)
- Something that happens at one moment, such as the tablets failing.
- State (ภาวะ)
- A condition a patient stays in for a while, such as well or active cellulitis; each patient is in exactly one at a time.
- Cycle (รอบ)
- The fixed time step at which a state model moves patients; one month here.
- Decision tree (ต้นไม้การตัดสินใจ)
- Branches from a choice through chance to the days or costs at each end; it cannot loop back in time.
- Expected value (ค่าคาดหมาย)
- Each end's days times its chance, added up: 0.8 × 10 + 0.2 × 2 = 8.4 days.
- No-memory rule (กฎไม่จำอดีต)
- The next move depends only on the current state, not on earlier history.
- History state (ภาวะที่เก็บประวัติ)
- A state split by what happened before, such as well after a recurrence at 0.06 a month.
- Tunnel state (ภาวะอุโมงค์)
- One of a fixed chain of temporary states that records time since an event, such as the first month after recovery at 0.10.
- Individual simulation (การจำลองรายบุคคล)
- Following patients one at a time, so each carries their own history.
Connect to the foundation articles
The foundation article on study design, estimands and effect measures shows how to state the question before the numbers. The one on decision trees and cohort models revisits both structures from the economic side. Later parts of this series build the tree step by step and then the monthly state model.
Try it in the app
Open our decision tree app to change the inputs and watch the result update. It needs a login and a Pro or Max plan.
The app starts from its own example values, so its numbers will not match this article.
References
- 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
- 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
- Eddy David M., Hollingworth William, Caro J. Jaime, Tsevat Joel, McDonald Kathryn M., Wong John B.. Model Transparency and Validation: A Report of the ISPOR-SMDM Modeling Good Research Practices Task Force-7. Value in Health. 2012;15(6):843-850. doi:10.1016/j.jval.2012.04.012. https://doi.org/10.1016/j.jval.2012.04.012
The series, in learning order
- Health Economic Modelling Series Guide: From Evidence to a Decision
- Choosing a Decision-Analytic Model: Let the Clinical Pathway Decide (this article)
- Auditable Model Inputs: Build a Parameter Table You Can Trace
- Finding Model Inputs in the Literature: Match the Estimate to the Question
- Rates, Probabilities and Transition Probabilities: Keep Time in the Calculation
- Building a Decision Tree Step by Step: Follow Outcomes, Costs and Health
- Building a Markov Cohort Model Step by Step: Follow the Cohort Through Time
- Budget Impact Analysis in Practice: Count People Before Counting Money
- ICER and Net Monetary Benefit Across Strategies: Find the Relevant Frontier
- Probabilistic Sensitivity Analysis: From Parameter Distributions to CEAC
- Model Validation and Verification: Test the Question, Equations and Code
- Expected Value of Perfect Information: What Could Better Evidence Be Worth?
Key takeaways
- Write the decision first: the patients, the complete care plans, the time horizon and the perspective decide the structure before any software does.
- For one bounded episode a tree is enough: 0.8 × 10 + 0.2 × 2 = 8.4 symptom-free days over two weeks.
- When episodes return, use states: a year of monthly returns needs 4,096 tree endpoints but only 2 reusable states.
- When the risk remembers the past, split the states: with history states, prevention after a recurrence avoids 7.5 episodes a month, while one well state shows only 3.75.
- Each structure needs its own evidence, and the arithmetic checks the sums, not whether the structure fits the disease.
Related in the wiki: Design, Model and Estimand: What Each Study Design Can Actually Estimate · Decision Trees, Markov Cohorts and Probabilistic Sensitivity Analysis: How a Model Turns Evidence into a Decision