Auditable Model Inputs: Build a Parameter Table You Can Trace

On this page
อ่านฉบับภาษาไทย (Thai version)
Abstract
A district hospital plans 400 elective bowel operations, and its finance committee asks what an average admission will cost. The analyst inherits a cost model with four bare numbers: 0.25, 12,000, 2,000 and 0.70. Read one way they give 4,500 CU per patient, read another way 5,000, and with the typed 0.70 they give 4,400. This article builds the record that settles it: a parameter table with one row per input. Each row names the exact quantity, the patients, the time span, the unit, the source and how the model uses the number. The table settles the case at 4,500 CU per patient, 1,800,000 CU for 400 operations. One shared risk feeds both paths, so the share without a complication is worked out, never typed. Last, it keeps how much patients differ apart from how sure we are about their average.
Four numbers and a committee waiting
A district hospital plans 400 elective bowel operations for next year. Its finance committee asks one question: what will an average admission cost? The analyst who built the cost model has left. The new analyst opens the file and finds four cells, 0.25, 12,000, 2,000 and 0.70, labelled only "risk", "cost high", "cost low" and "other risk".
Costs are in CU, an illustrative currency unit, per patient for the whole admission. The four cells can be read in three ways, and each way gives the committee a different total bill.
- If 12,000 is the average total cost of a patient who has a complication, and 2,000 the average for one who does not, the patients without a complication are the 0.75 left over from 0.25. An admission then costs 0.25 × 12,000 + 0.75 × 2,000 = 3,000 + 1,500 = 4,500 CU on average, or 1,800,000 CU for 400 operations.
- If 12,000 is the extra cost a complication adds on top of an ordinary 2,000 stay, the average is 2,000 + 0.25 × 12,000 = 5,000 CU, or 2,000,000 CU in all.
- If someone typed 0.70 into the cell by hand, instead of working it out from 0.25, and it is used as the share without a complication, the average is 0.25 × 12,000 + 0.70 × 2,000 = 4,400 CU, or 1,760,000 CU in all. The reason is that 0.25 and 0.70 add up to only 0.95, so some patients sit on no path and add nothing.
Nothing in the file says which reading is right, so the committee could be handed any of the three bills. The fix is not a better number but a written record of what each number means. That record is a parameter table: one row per input, saying what the number measures, for whom, over what time, where it came from and how the model uses it.
Name the quantity before entering its value
A parameter is one input of the model, such as the complication risk. Its estimand is the exact quantity the evidence should estimate, written out in words. For the risk here, that means naming the event (any complication after the operation), the population (adults having elective bowel surgery at this hospital), the setting (the surgical ward) and the period (from the operation to discharge) (Briggs and colleagues).
The two costs need the same care, and here the words decide the bill. "Mean total cost per patient among those with a complication, from admission to discharge" gives the first reading, 4,500 CU. "Mean extra cost that a complication adds" gives the second, 5,000 CU. The number 12,000 is the same in both; only the estimand differs.
Give each parameter a stable code name, its identifier, such as p_event, and use that name wherever the same quantity is needed. That way a change in one place reaches every calculation that uses it.
A value worked out from named inputs is a derived value, and the table records how it is worked out. The share without a complication is one: p_nonevent = 1 − p_event = 0.75, so the third reading cannot happen.
Keep the source value separate from the model value when a conversion is needed. A study may report a cost in an older price year, or a risk over a longer follow-up than the model uses. The row then keeps the original value and unit, where in the source it was found and the conversion, so a reviewer can follow the change without opening the model code (Eddy and colleagues). Here the finance office's costs are already in this year's prices and per admission, so the row records that no conversion was needed.
Together these give the fields every row should record, listed below. The base value is the central value the model starts from. One more field, a plausible low and high value, says how sure we are about the number; it is filled in later in this article, once the uncertainty is worked out.
The minimum audit fields
| Field | Record | Reason |
|---|---|---|
| Identifier and estimand | Stable name; the event, outcome or resource in words | Stops two quantities sharing a name |
| Population and time | Who is included, setting, comparator if any, and period | Shows whether the number fits this hospital |
| Unit and source | Denominator, currency and year, citation and where it was found | Lets a reviewer check units and go back to the source |
| Base value and range | Central value; plausible low and high values from the evidence | Shows how sure we are about the number, apart from what-if choices the analyst makes |
| Transformation and reuse | Equation, the inputs it uses and every calculation that uses it | Makes derived values reproducible |
A parameter table for one surgical admission
Here is the table the new analyst writes, after asking the surgical team and the finance office what each cell meant. Every row shares the same population, adults having elective bowel surgery, and the same period, the admission. Costs are what the hospital pays.
Two of the costs are conditional means: the average among patients on one path only, those who had a complication or those who did not. The model combines them into one unconditional mean, the average over all patients, which is the number the committee asked for. That mean is called the expected cost, written E[C] in a formula. The values are illustrative, and no range is given yet.
The figure after the table draws each path as one branch of a tree: one branch for patients with a complication and one for those without.
One row per input
| Identifier | Estimand and unit | Source | Base value | Transformation |
|---|---|---|---|---|
| p_event | Risk of any complication, operation to discharge (proportion) | Hospital audit of last year's operations | 0.25 | Direct input; used by both paths |
| cost_event | Mean total cost per patient with a complication (CU) | Hospital finance records, same patients | 12,000 | Direct conditional mean |
| cost_nonevent | Mean total cost per patient without a complication (CU) | Hospital finance records, same patients | 2,000 | Direct conditional mean |
| p_nonevent | Risk of no complication, operation to discharge (proportion) | Derived | 0.75 | 1 − p_event; never typed |
| expected_cost | Mean cost per patient over all patients (CU) | Derived | 4,500 | p_event × cost_event + p_nonevent × cost_nonevent |
Check the three readings in R
# Three readings of the same four cells, in illustrative CU per patient and for 400 operations.
p_event <- 0.25
cost_event <- 12000
cost_nonevent <- 2000
p_typed <- 0.70
ops <- 400
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
path_means <- p_event * cost_event + (1 - p_event) * cost_nonevent
extra_cost <- cost_nonevent + p_event * cost_event
typed <- p_event * cost_event + p_typed * cost_nonevent
cat("Path means (CU/patient; all):", big(path_means), ";", big(ops * path_means), "\n")
cat("Extra cost (CU/patient; all):", big(extra_cost), ";", big(ops * extra_cost), "\n")
cat("Typed (sum; CU/patient; all):", p_event + p_typed, ";", typed, ";", big(ops * typed), "\n")
Path means (CU/patient; all): 4,500 ; 1,800,000
Extra cost (CU/patient; all): 5,000 ; 2,000,000
Typed (sum; CU/patient; all): 0.95 ; 4400 ; 1,760,000
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download auditable-model-inputs-parameter-tables.R
Full tidy script
# Auditable Model Inputs: Build a Parameter Table You Can Trace
# Shows how a parameter table settles what four numbers mean, then gives the expected cost.
# แสดงว่าตารางพารามิเตอร์ช่วยตัดสินความหมายของตัวเลขสี่ตัว และให้ต้นทุนคาดหมายต่อผู้ป่วยหนึ่งคน
# 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 parameter table as an explicit input table.
library(dplyr) # Calculate each path's contribution from the table.
library(ggplot2) # Display the contributions without saving files.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The parameter table
# One row per input: the identifier, its value and what it means; the share without a complication
# is derived from p_event, never typed. All values are illustrative.
# หนึ่งแถวต่อข้อมูลนำเข้าหนึ่งค่า ระบุชื่อ ค่า และความหมาย ส่วนสัดส่วนที่ไม่มีภาวะแทรกซ้อนคำนวณจาก
# p_event เสมอ ไม่พิมพ์แยก ค่าทั้งหมดเป็นตัวอย่างสมมติ
p_event <- 0.25
cost_event <- 12000
cost_nonevent <- 2000
p_typed <- 0.70
ops <- 400
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
p_nonevent <- 1 - p_event
parameters <- tibble::tibble(
identifier = c("p_event", "cost_event", "cost_nonevent", "p_nonevent"),
value = c(p_event, cost_event, cost_nonevent, p_nonevent),
meaning = c("Risk of any complication, operation to discharge",
"Mean total cost per patient with a complication (CU)",
"Mean total cost per patient without a complication (CU)",
"Derived: 1 - p_event")
)
#%%@note 2. Three readings of the same four cells
# The same numbers give three bills, depending on what 12000 means and whether the share is typed.
# ตัวเลขชุดเดียวกันให้ค่าใช้จ่ายสามแบบ ขึ้นกับว่า 12000 หมายถึงอะไร และสัดส่วนถูกพิมพ์แยกหรือไม่
path_means <- p_event * cost_event + (1 - p_event) * cost_nonevent
extra_cost <- cost_nonevent + p_event * cost_event
typed <- p_event * cost_event + p_typed * cost_nonevent
#%%@note 3. The expected cost from the table
# Weight each path's mean cost by its probability; a probability has no unit, so the unit stays CU.
# ถ่วงต้นทุนเฉลี่ยของแต่ละเส้นทางด้วยความน่าจะเป็น ความน่าจะเป็นไม่มีหน่วย ผลจึงเป็น CU ต่อผู้ป่วย
paths <- tibble::tibble(
path = c("Complication", "No complication"),
probability = c(p_event, p_nonevent),
mean_cost = c(cost_event, cost_nonevent)
) |>
dplyr::mutate(contribution = probability * mean_cost)
stopifnot(abs(sum(paths$probability) - 1) < 1e-9)
from_event <- p_event * cost_event
from_nonevent <- p_nonevent * cost_nonevent
expected_cost <- from_event + from_nonevent
stopifnot(abs(sum(paths$contribution) - expected_cost) < 1e-9)
#%%@note 4. The bill for 400 operations, counted in patients
# Count patients on each path, then multiply by each path's mean cost.
# นับผู้ป่วยในแต่ละเส้นทาง แล้วคูณด้วยต้นทุนเฉลี่ยของเส้นทางนั้น
with_event <- ops * p_event
bill_event <- with_event * cost_event
bill_nonevent <- (ops - with_event) * cost_nonevent
total <- bill_event + bill_nonevent
stopifnot(abs(total / ops - expected_cost) < 1e-9)
#%%@note 5. Spread between patients versus uncertainty about the mean
# The SD of 36 patients' costs is the spread; the SE is how precisely the mean is known.
# SD ของต้นทุนผู้ป่วย 36 คนคือความผันแปรระหว่างคน ส่วน SE บอกว่ารู้ค่าเฉลี่ยได้แม่นเพียงใด
sd_cost <- 6000
n_event <- 36
se_count <- 2
se_cost <- sd_cost / sqrt(n_event)
cost_at <- function(cost) p_event * cost + (1 - p_event) * cost_nonevent
se_range <- cost_event + c(-se_count, se_count) * se_cost
sd_range <- cost_event + c(-se_count, se_count) * sd_cost
#%%@note 6. Display the contributions
# Plot each path's contribution to the expected cost; the script saves no file.
# แสดงส่วนที่แต่ละเส้นทางเพิ่มให้ต้นทุนคาดหมาย โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(paths, ggplot2::aes(x = path, y = contribution)) +
ggplot2::geom_col() + ggplot2::labs(y = "Contribution to expected cost (CU/patient)"))
#%%@note 7. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("Path means (CU/patient; all):", big(path_means), ";", big(ops * path_means), "\n")
cat("Extra cost (CU/patient; all):", big(extra_cost), ";", big(ops * extra_cost), "\n")
cat("Typed (sum; CU/patient; all):", p_event + p_typed, ";", typed, ";", big(ops * typed), "\n")
cat("p_nonevent (derived); probabilities add to:", p_nonevent, ";", p_event + p_nonevent, "\n")
cat("Path contributions (CU/patient):", from_event, "+", from_nonevent, "\n")
cat("Expected cost (CU/patient):", expected_cost, "\n")
cat("Patients with / without a complication:", with_event, "/", ops - with_event, "\n")
cat("Bill by path (CU):", big(bill_event), "+", big(bill_nonevent), "=", big(total), "\n")
cat("Share of the bill from complications (%):", round(100 * bill_event / total, 2), "\n")
cat("SE (CU); mean +/- 2 SE; expected cost:", se_cost, ";", se_range, ";", cost_at(se_range), "\n")
cat("SD by mistake: mean +/- 2 SD; expected cost:", sd_range, ";", cost_at(sd_range), "\n")
The expected cost, step by step
The calculation uses the identifiers from the table. Changing p_event updates both branch weights at once, so the two paths can never disagree.
-
Work out the share without a complication
\[ p_{nonevent}=1-0.25=0.75 \]
Use p_event = 0.25 to weight the complication path. What is left over, its complement p_nonevent, weights the path without a complication, so the two add up to 1.
-
Check units, then add
\[ E[C]=0.25\times 12000+0.75\times 2000=3000+1500=4500 \]
Weight 12,000 CU per patient and 2,000 CU per patient by their probabilities. A probability has no unit, so probability times cost per patient is still cost per patient. The sum is E[C], the expected cost.
Result: Expected cost is 4,500 CU per patient, averaged over everyone having the operation: the first reading, and the one the table records.
Check the numbers in R
# Expected cost per patient from the named inputs, in illustrative CU.
p_event <- 0.25
cost_event <- 12000
cost_nonevent <- 2000
p_nonevent <- 1 - p_event
from_event <- p_event * cost_event
from_nonevent <- p_nonevent * cost_nonevent
expected_cost <- from_event + from_nonevent
cat("p_nonevent (derived); probabilities add to:", p_nonevent, ";", p_event + p_nonevent, "\n")
cat("Path contributions (CU/patient):", from_event, "+", from_nonevent, "\n")
cat("Expected cost (CU/patient):", expected_cost, "\n")
p_nonevent (derived); probabilities add to: 0.75 ; 1
Path contributions (CU/patient): 3000 + 1500
Expected cost (CU/patient): 4500
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download auditable-model-inputs-parameter-tables.R
Full tidy script
# Auditable Model Inputs: Build a Parameter Table You Can Trace
# Shows how a parameter table settles what four numbers mean, then gives the expected cost.
# แสดงว่าตารางพารามิเตอร์ช่วยตัดสินความหมายของตัวเลขสี่ตัว และให้ต้นทุนคาดหมายต่อผู้ป่วยหนึ่งคน
# 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 parameter table as an explicit input table.
library(dplyr) # Calculate each path's contribution from the table.
library(ggplot2) # Display the contributions without saving files.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The parameter table
# One row per input: the identifier, its value and what it means; the share without a complication
# is derived from p_event, never typed. All values are illustrative.
# หนึ่งแถวต่อข้อมูลนำเข้าหนึ่งค่า ระบุชื่อ ค่า และความหมาย ส่วนสัดส่วนที่ไม่มีภาวะแทรกซ้อนคำนวณจาก
# p_event เสมอ ไม่พิมพ์แยก ค่าทั้งหมดเป็นตัวอย่างสมมติ
p_event <- 0.25
cost_event <- 12000
cost_nonevent <- 2000
p_typed <- 0.70
ops <- 400
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
p_nonevent <- 1 - p_event
parameters <- tibble::tibble(
identifier = c("p_event", "cost_event", "cost_nonevent", "p_nonevent"),
value = c(p_event, cost_event, cost_nonevent, p_nonevent),
meaning = c("Risk of any complication, operation to discharge",
"Mean total cost per patient with a complication (CU)",
"Mean total cost per patient without a complication (CU)",
"Derived: 1 - p_event")
)
#%%@note 2. Three readings of the same four cells
# The same numbers give three bills, depending on what 12000 means and whether the share is typed.
# ตัวเลขชุดเดียวกันให้ค่าใช้จ่ายสามแบบ ขึ้นกับว่า 12000 หมายถึงอะไร และสัดส่วนถูกพิมพ์แยกหรือไม่
path_means <- p_event * cost_event + (1 - p_event) * cost_nonevent
extra_cost <- cost_nonevent + p_event * cost_event
typed <- p_event * cost_event + p_typed * cost_nonevent
#%%@note 3. The expected cost from the table
# Weight each path's mean cost by its probability; a probability has no unit, so the unit stays CU.
# ถ่วงต้นทุนเฉลี่ยของแต่ละเส้นทางด้วยความน่าจะเป็น ความน่าจะเป็นไม่มีหน่วย ผลจึงเป็น CU ต่อผู้ป่วย
paths <- tibble::tibble(
path = c("Complication", "No complication"),
probability = c(p_event, p_nonevent),
mean_cost = c(cost_event, cost_nonevent)
) |>
dplyr::mutate(contribution = probability * mean_cost)
stopifnot(abs(sum(paths$probability) - 1) < 1e-9)
from_event <- p_event * cost_event
from_nonevent <- p_nonevent * cost_nonevent
expected_cost <- from_event + from_nonevent
stopifnot(abs(sum(paths$contribution) - expected_cost) < 1e-9)
#%%@note 4. The bill for 400 operations, counted in patients
# Count patients on each path, then multiply by each path's mean cost.
# นับผู้ป่วยในแต่ละเส้นทาง แล้วคูณด้วยต้นทุนเฉลี่ยของเส้นทางนั้น
with_event <- ops * p_event
bill_event <- with_event * cost_event
bill_nonevent <- (ops - with_event) * cost_nonevent
total <- bill_event + bill_nonevent
stopifnot(abs(total / ops - expected_cost) < 1e-9)
#%%@note 5. Spread between patients versus uncertainty about the mean
# The SD of 36 patients' costs is the spread; the SE is how precisely the mean is known.
# SD ของต้นทุนผู้ป่วย 36 คนคือความผันแปรระหว่างคน ส่วน SE บอกว่ารู้ค่าเฉลี่ยได้แม่นเพียงใด
sd_cost <- 6000
n_event <- 36
se_count <- 2
se_cost <- sd_cost / sqrt(n_event)
cost_at <- function(cost) p_event * cost + (1 - p_event) * cost_nonevent
se_range <- cost_event + c(-se_count, se_count) * se_cost
sd_range <- cost_event + c(-se_count, se_count) * sd_cost
#%%@note 6. Display the contributions
# Plot each path's contribution to the expected cost; the script saves no file.
# แสดงส่วนที่แต่ละเส้นทางเพิ่มให้ต้นทุนคาดหมาย โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(paths, ggplot2::aes(x = path, y = contribution)) +
ggplot2::geom_col() + ggplot2::labs(y = "Contribution to expected cost (CU/patient)"))
#%%@note 7. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("Path means (CU/patient; all):", big(path_means), ";", big(ops * path_means), "\n")
cat("Extra cost (CU/patient; all):", big(extra_cost), ";", big(ops * extra_cost), "\n")
cat("Typed (sum; CU/patient; all):", p_event + p_typed, ";", typed, ";", big(ops * typed), "\n")
cat("p_nonevent (derived); probabilities add to:", p_nonevent, ";", p_event + p_nonevent, "\n")
cat("Path contributions (CU/patient):", from_event, "+", from_nonevent, "\n")
cat("Expected cost (CU/patient):", expected_cost, "\n")
cat("Patients with / without a complication:", with_event, "/", ops - with_event, "\n")
cat("Bill by path (CU):", big(bill_event), "+", big(bill_nonevent), "=", big(total), "\n")
cat("Share of the bill from complications (%):", round(100 * bill_event / total, 2), "\n")
cat("SE (CU); mean +/- 2 SE; expected cost:", se_cost, ";", se_range, ";", cost_at(se_range), "\n")
cat("SD by mistake: mean +/- 2 SD; expected cost:", sd_range, ";", cost_at(sd_range), "\n")
Where the average comes from
The expected cost is a weighted average: each path's cost is weighted by the share of patients who take it. The complication path contributes 3,000 CU per patient and the other path 1,500 CU, which together make the 4,500 CU above.
Counting patients makes the same point for the committee. Of 400 operations, 100 patients (400 × 0.25) are expected to have a complication and 300 are not. Their bills are 100 × 12,000 = 1,200,000 CU and 300 × 2,000 = 600,000 CU. That is 1,800,000 CU in all, the same 4,500 CU per patient.
So 66.67% of the bill comes from the quarter of patients who have a complication. That is why the average sits well above the cost of an uncomplicated stay, and why an accurate complication risk matters so much. The explorer below draws each path's contribution as an area: the width is the share of patients, the height the mean cost on that path.
Check the split in R
# Split the bill for 400 operations by path, in illustrative CU.
p_event <- 0.25
cost_event <- 12000
cost_nonevent <- 2000
ops <- 400
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
with_event <- ops * p_event
bill_event <- with_event * cost_event
bill_nonevent <- (ops - with_event) * cost_nonevent
total <- bill_event + bill_nonevent
cat("Patients with / without a complication:", with_event, "/", ops - with_event, "\n")
cat("Bill by path (CU):", big(bill_event), "+", big(bill_nonevent), "=", big(total), "\n")
cat("Share of the bill from complications (%):", round(100 * bill_event / total, 2), "\n")
Patients with / without a complication: 100 / 300
Bill by path (CU): 1,200,000 + 600,000 = 1,800,000
Share of the bill from complications (%): 66.67
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download auditable-model-inputs-parameter-tables.R
Full tidy script
# Auditable Model Inputs: Build a Parameter Table You Can Trace
# Shows how a parameter table settles what four numbers mean, then gives the expected cost.
# แสดงว่าตารางพารามิเตอร์ช่วยตัดสินความหมายของตัวเลขสี่ตัว และให้ต้นทุนคาดหมายต่อผู้ป่วยหนึ่งคน
# 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 parameter table as an explicit input table.
library(dplyr) # Calculate each path's contribution from the table.
library(ggplot2) # Display the contributions without saving files.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The parameter table
# One row per input: the identifier, its value and what it means; the share without a complication
# is derived from p_event, never typed. All values are illustrative.
# หนึ่งแถวต่อข้อมูลนำเข้าหนึ่งค่า ระบุชื่อ ค่า และความหมาย ส่วนสัดส่วนที่ไม่มีภาวะแทรกซ้อนคำนวณจาก
# p_event เสมอ ไม่พิมพ์แยก ค่าทั้งหมดเป็นตัวอย่างสมมติ
p_event <- 0.25
cost_event <- 12000
cost_nonevent <- 2000
p_typed <- 0.70
ops <- 400
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
p_nonevent <- 1 - p_event
parameters <- tibble::tibble(
identifier = c("p_event", "cost_event", "cost_nonevent", "p_nonevent"),
value = c(p_event, cost_event, cost_nonevent, p_nonevent),
meaning = c("Risk of any complication, operation to discharge",
"Mean total cost per patient with a complication (CU)",
"Mean total cost per patient without a complication (CU)",
"Derived: 1 - p_event")
)
#%%@note 2. Three readings of the same four cells
# The same numbers give three bills, depending on what 12000 means and whether the share is typed.
# ตัวเลขชุดเดียวกันให้ค่าใช้จ่ายสามแบบ ขึ้นกับว่า 12000 หมายถึงอะไร และสัดส่วนถูกพิมพ์แยกหรือไม่
path_means <- p_event * cost_event + (1 - p_event) * cost_nonevent
extra_cost <- cost_nonevent + p_event * cost_event
typed <- p_event * cost_event + p_typed * cost_nonevent
#%%@note 3. The expected cost from the table
# Weight each path's mean cost by its probability; a probability has no unit, so the unit stays CU.
# ถ่วงต้นทุนเฉลี่ยของแต่ละเส้นทางด้วยความน่าจะเป็น ความน่าจะเป็นไม่มีหน่วย ผลจึงเป็น CU ต่อผู้ป่วย
paths <- tibble::tibble(
path = c("Complication", "No complication"),
probability = c(p_event, p_nonevent),
mean_cost = c(cost_event, cost_nonevent)
) |>
dplyr::mutate(contribution = probability * mean_cost)
stopifnot(abs(sum(paths$probability) - 1) < 1e-9)
from_event <- p_event * cost_event
from_nonevent <- p_nonevent * cost_nonevent
expected_cost <- from_event + from_nonevent
stopifnot(abs(sum(paths$contribution) - expected_cost) < 1e-9)
#%%@note 4. The bill for 400 operations, counted in patients
# Count patients on each path, then multiply by each path's mean cost.
# นับผู้ป่วยในแต่ละเส้นทาง แล้วคูณด้วยต้นทุนเฉลี่ยของเส้นทางนั้น
with_event <- ops * p_event
bill_event <- with_event * cost_event
bill_nonevent <- (ops - with_event) * cost_nonevent
total <- bill_event + bill_nonevent
stopifnot(abs(total / ops - expected_cost) < 1e-9)
#%%@note 5. Spread between patients versus uncertainty about the mean
# The SD of 36 patients' costs is the spread; the SE is how precisely the mean is known.
# SD ของต้นทุนผู้ป่วย 36 คนคือความผันแปรระหว่างคน ส่วน SE บอกว่ารู้ค่าเฉลี่ยได้แม่นเพียงใด
sd_cost <- 6000
n_event <- 36
se_count <- 2
se_cost <- sd_cost / sqrt(n_event)
cost_at <- function(cost) p_event * cost + (1 - p_event) * cost_nonevent
se_range <- cost_event + c(-se_count, se_count) * se_cost
sd_range <- cost_event + c(-se_count, se_count) * sd_cost
#%%@note 6. Display the contributions
# Plot each path's contribution to the expected cost; the script saves no file.
# แสดงส่วนที่แต่ละเส้นทางเพิ่มให้ต้นทุนคาดหมาย โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(paths, ggplot2::aes(x = path, y = contribution)) +
ggplot2::geom_col() + ggplot2::labs(y = "Contribution to expected cost (CU/patient)"))
#%%@note 7. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("Path means (CU/patient; all):", big(path_means), ";", big(ops * path_means), "\n")
cat("Extra cost (CU/patient; all):", big(extra_cost), ";", big(ops * extra_cost), "\n")
cat("Typed (sum; CU/patient; all):", p_event + p_typed, ";", typed, ";", big(ops * typed), "\n")
cat("p_nonevent (derived); probabilities add to:", p_nonevent, ";", p_event + p_nonevent, "\n")
cat("Path contributions (CU/patient):", from_event, "+", from_nonevent, "\n")
cat("Expected cost (CU/patient):", expected_cost, "\n")
cat("Patients with / without a complication:", with_event, "/", ops - with_event, "\n")
cat("Bill by path (CU):", big(bill_event), "+", big(bill_nonevent), "=", big(total), "\n")
cat("Share of the bill from complications (%):", round(100 * bill_event / total, 2), "\n")
cat("SE (CU); mean +/- 2 SE; expected cost:", se_cost, ";", se_range, ";", cost_at(se_range), "\n")
cat("SD by mistake: mean +/- 2 SD; expected cost:", sd_range, ";", cost_at(sd_range), "\n")
Keep types of uncertainty separate
The committee's next question is how sure the 4,500 is. Answering it means keeping two kinds of spread apart. Last year 36 patients had a complication, and their costs varied widely: a leak can cost far more than a minor wound infection. That difference between patients is variability, and the standard deviation (SD) measures it: here 6,000 CU around the mean of 12,000.
Variability does not shrink when a study enrols more patients, because patients really do differ. Uncertainty about the mean is different: it is how precisely the evidence pins down the average, and a larger study pins it down better. With 36 patients whose costs have an SD of 6,000 CU, the average is known to within about 6,000 divided by the square root of 36, which is 1,000 CU. That figure is the standard error (SE), SE = SD / √n for n patients, and it shrinks as the study grows (Briggs and colleagues).
The range field in the table describes uncertainty about the mean, so it comes from the SE. Two standard errors either side of the mean, 2 × 1,000 = 2,000 CU, give roughly the confidence interval a study report would show. So the mean cost with a complication runs from 12,000 − 2,000 = 10,000 to 12,000 + 2,000 = 14,000 CU.
Moving one input across its range while the others stay fixed is a one-way sensitivity analysis. Only cost_event moves here, and the path without a complication still contributes 1,500 CU. The expected cost is 0.25 × 10,000 + 1,500 = 4,000 CU at the low end and 0.25 × 14,000 + 1,500 = 5,000 CU at the high end.
Using twice the SD (2 × 6,000) by mistake gives 12,000 ± 12,000, a range of 0 to 24,000 CU. The expected cost then runs from 0.25 × 0 + 1,500 = 1,500 to 0.25 × 24,000 + 1,500 = 7,500 CU. That range describes single patients, not their average, so it makes the 4,500 look far less certain than it is.
A range says only where the plausible values lie. A later article in this series goes one step further: it gives each value in the range a probability, called a distribution, and reruns the model many times with drawn values. When the evidence gives no SD or SE, say so in the table. Any range the analyst then picks is a what-if choice, a scenario, not evidence.
Structural uncertainty is doubt about whether the model shows the patient pathway well enough. This model stops at discharge, so a readmission for a leak found later is left out. That choice is an assumption: label it in the table and explain why it is reasonable. Giving it a range would not turn it into uncertainty about a mean.
Check complements, units and every shared use after each revision, so the table and the model never drift apart (Eddy and colleagues).
Check the range in R
# Spread between patients (SD) versus uncertainty about the mean (SE), in illustrative CU.
p_event <- 0.25
cost_event <- 12000
cost_nonevent <- 2000
sd_cost <- 6000
n_event <- 36
se_count <- 2
se_cost <- sd_cost / sqrt(n_event)
cost_at <- function(cost) p_event * cost + (1 - p_event) * cost_nonevent
se_range <- cost_event + c(-se_count, se_count) * se_cost
sd_range <- cost_event + c(-se_count, se_count) * sd_cost
cat("SE (CU); mean +/- 2 SE; expected cost:", se_cost, ";", se_range, ";", cost_at(se_range), "\n")
cat("SD by mistake: mean +/- 2 SD; expected cost:", sd_range, ";", cost_at(sd_range), "\n")
SE (CU); mean +/- 2 SE; expected cost: 1000 ; 10000 14000 ; 4000 5000
SD by mistake: mean +/- 2 SD; expected cost: 0 24000 ; 1500 7500
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download auditable-model-inputs-parameter-tables.R
Full tidy script
# Auditable Model Inputs: Build a Parameter Table You Can Trace
# Shows how a parameter table settles what four numbers mean, then gives the expected cost.
# แสดงว่าตารางพารามิเตอร์ช่วยตัดสินความหมายของตัวเลขสี่ตัว และให้ต้นทุนคาดหมายต่อผู้ป่วยหนึ่งคน
# 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 parameter table as an explicit input table.
library(dplyr) # Calculate each path's contribution from the table.
library(ggplot2) # Display the contributions without saving files.
stopifnot(getRversion() >= "4.5.2")
#%%@note 1. The parameter table
# One row per input: the identifier, its value and what it means; the share without a complication
# is derived from p_event, never typed. All values are illustrative.
# หนึ่งแถวต่อข้อมูลนำเข้าหนึ่งค่า ระบุชื่อ ค่า และความหมาย ส่วนสัดส่วนที่ไม่มีภาวะแทรกซ้อนคำนวณจาก
# p_event เสมอ ไม่พิมพ์แยก ค่าทั้งหมดเป็นตัวอย่างสมมติ
p_event <- 0.25
cost_event <- 12000
cost_nonevent <- 2000
p_typed <- 0.70
ops <- 400
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
p_nonevent <- 1 - p_event
parameters <- tibble::tibble(
identifier = c("p_event", "cost_event", "cost_nonevent", "p_nonevent"),
value = c(p_event, cost_event, cost_nonevent, p_nonevent),
meaning = c("Risk of any complication, operation to discharge",
"Mean total cost per patient with a complication (CU)",
"Mean total cost per patient without a complication (CU)",
"Derived: 1 - p_event")
)
#%%@note 2. Three readings of the same four cells
# The same numbers give three bills, depending on what 12000 means and whether the share is typed.
# ตัวเลขชุดเดียวกันให้ค่าใช้จ่ายสามแบบ ขึ้นกับว่า 12000 หมายถึงอะไร และสัดส่วนถูกพิมพ์แยกหรือไม่
path_means <- p_event * cost_event + (1 - p_event) * cost_nonevent
extra_cost <- cost_nonevent + p_event * cost_event
typed <- p_event * cost_event + p_typed * cost_nonevent
#%%@note 3. The expected cost from the table
# Weight each path's mean cost by its probability; a probability has no unit, so the unit stays CU.
# ถ่วงต้นทุนเฉลี่ยของแต่ละเส้นทางด้วยความน่าจะเป็น ความน่าจะเป็นไม่มีหน่วย ผลจึงเป็น CU ต่อผู้ป่วย
paths <- tibble::tibble(
path = c("Complication", "No complication"),
probability = c(p_event, p_nonevent),
mean_cost = c(cost_event, cost_nonevent)
) |>
dplyr::mutate(contribution = probability * mean_cost)
stopifnot(abs(sum(paths$probability) - 1) < 1e-9)
from_event <- p_event * cost_event
from_nonevent <- p_nonevent * cost_nonevent
expected_cost <- from_event + from_nonevent
stopifnot(abs(sum(paths$contribution) - expected_cost) < 1e-9)
#%%@note 4. The bill for 400 operations, counted in patients
# Count patients on each path, then multiply by each path's mean cost.
# นับผู้ป่วยในแต่ละเส้นทาง แล้วคูณด้วยต้นทุนเฉลี่ยของเส้นทางนั้น
with_event <- ops * p_event
bill_event <- with_event * cost_event
bill_nonevent <- (ops - with_event) * cost_nonevent
total <- bill_event + bill_nonevent
stopifnot(abs(total / ops - expected_cost) < 1e-9)
#%%@note 5. Spread between patients versus uncertainty about the mean
# The SD of 36 patients' costs is the spread; the SE is how precisely the mean is known.
# SD ของต้นทุนผู้ป่วย 36 คนคือความผันแปรระหว่างคน ส่วน SE บอกว่ารู้ค่าเฉลี่ยได้แม่นเพียงใด
sd_cost <- 6000
n_event <- 36
se_count <- 2
se_cost <- sd_cost / sqrt(n_event)
cost_at <- function(cost) p_event * cost + (1 - p_event) * cost_nonevent
se_range <- cost_event + c(-se_count, se_count) * se_cost
sd_range <- cost_event + c(-se_count, se_count) * sd_cost
#%%@note 6. Display the contributions
# Plot each path's contribution to the expected cost; the script saves no file.
# แสดงส่วนที่แต่ละเส้นทางเพิ่มให้ต้นทุนคาดหมาย โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(paths, ggplot2::aes(x = path, y = contribution)) +
ggplot2::geom_col() + ggplot2::labs(y = "Contribution to expected cost (CU/patient)"))
#%%@note 7. Numbers the article quotes
# Print the short boxes' lines in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องโค้ดสั้นตามลำดับในบทความ
cat("Path means (CU/patient; all):", big(path_means), ";", big(ops * path_means), "\n")
cat("Extra cost (CU/patient; all):", big(extra_cost), ";", big(ops * extra_cost), "\n")
cat("Typed (sum; CU/patient; all):", p_event + p_typed, ";", typed, ";", big(ops * typed), "\n")
cat("p_nonevent (derived); probabilities add to:", p_nonevent, ";", p_event + p_nonevent, "\n")
cat("Path contributions (CU/patient):", from_event, "+", from_nonevent, "\n")
cat("Expected cost (CU/patient):", expected_cost, "\n")
cat("Patients with / without a complication:", with_event, "/", ops - with_event, "\n")
cat("Bill by path (CU):", big(bill_event), "+", big(bill_nonevent), "=", big(total), "\n")
cat("Share of the bill from complications (%):", round(100 * bill_event / total, 2), "\n")
cat("SE (CU); mean +/- 2 SE; expected cost:", se_cost, ";", se_range, ";", cost_at(se_range), "\n")
cat("SD by mistake: mean +/- 2 SD; expected cost:", sd_range, ";", cost_at(sd_range), "\n")
Terms to keep distinct
- Estimand
- The exact quantity the evidence should estimate, in words: event, population, setting and period.
- Derived value
- A quantity calculated from named inputs, with its calculation recorded, such as p_nonevent.
- Conditional mean
- The average among patients on one path only, such as those who had a complication.
- Expected (unconditional) mean
- The average over all patients: each conditional mean weighted by its probability; for cost, E[C].
- SD and SE
- SD is the spread between patients; SE is the uncertainty of the mean and shrinks as the study grows.
- Range and distribution
- Plausible low and high values of an uncertain input; a distribution also gives each value a probability.
- One-way sensitivity analysis
- Moving one input across its range while the others stay fixed, to see how far the result moves.
- Structural uncertainty and assumption
- Doubt about whether the model shows the pathway well enough, such as stopping at discharge; the assumption is the choice made.
Connect to the foundation articles
Revisit Parameter sourcing and source catalogues and Costing, perspective and time for the clinical and economic foundations.
Try it in the app
Open our economic evaluation app to change the inputs and watch the result update. It needs a login and a Pro or Max plan.
The app starts from its own example values, so its numbers will not match this article.
References
- Andrew Briggs, Karl Claxton, Mark Sculpher. Decision Modelling for Health Economic Evaluation (Handbooks for Health Economic Evaluation). Oxford University Press; 2006. ISBN 9780198526629. https://openlibrary.org/isbn/9780198526629
- Briggs Andrew H., Weinstein Milton C., Fenwick Elisabeth A.L., Karnon Jonathan, Sculpher Mark J., Paltiel A. David. Model Parameter Estimation and Uncertainty: A Report of the ISPOR-SMDM Modeling Good Research Practices Task Force-6. Value in Health. 2012;15(6):835-842. doi:10.1016/j.jval.2012.04.014. https://doi.org/10.1016/j.jval.2012.04.014
- 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
- Auditable Model Inputs: Build a Parameter Table You Can Trace (this article)
- 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 each input as a row: what it measures, for whom, over what time, in what unit, from where and how the model uses it. The same 12,000 gave 4,500 or 5,000 CU per patient until its row said which.
- Give a shared quantity one name and work out its complement: p_nonevent = 1 − p_event = 0.75, never a typed 0.70.
- Fill the range from the SE, not the SD: an SE of 1,000 CU gives 4,000 to 5,000 CU per patient, while the SD would give 1,500 to 7,500.
- Label structural choices and assumptions, such as stopping at discharge, apart from uncertainty about a mean.
Related in the wiki: Parameter Sourcing, Reference Cost Lists and Utility Catalogues When Local Data Are Sparse · Whose Cost, in Which Year? Perspective, Opportunity Cost, Inflation and Discounting