Budget Impact Analysis in Practice: Count People Before Counting Money

Health EconomicsClinical Epidemiology ResearchMethodology and Research Design
Budget Impact Analysis in Practice: Count People Before Counting Money
On this page

อ่านฉบับภาษาไทย (Thai version)

Abstract

A district health fund must decide whether to pay for a home blood-pressure service for stroke survivors: a cuff at home and a monthly nurse call, costing 2,000 THB a year more than usual care for each user. Today 500 survivors qualify. The quick answer multiplies people by price by years, 500 × 2,000 × 3 = 3,000,000 THB. It is wrong, because the group changes. Each year 100 new survivors qualify, 10% of the group leaves at year end, and the share using the service rises from 20% to 40% to 60%. Counting people first gives 120, 256 and 405.6 users and extra cash of 240,000, 512,000 and 811,200 THB: 1,563,200 THB over three years. This year-by-year count of a payer's extra cash is called budget impact analysis. The article prices the years with and without the service, keeps each amount as cash in the year it is paid, and checks whether the nurses can serve the users the budget assumes.


Read the plate from the top: people are counted first, year by year, and only then turned into cash. Every number is worked out in the sections below, so the plate is the article's route in one place.

One district fund, 500 survivors and a 2,000 THB service

A district health fund in a Thai province pays for the care of local stroke survivors. Its clinics count 500 survivors who meet the rules for a new home blood-pressure service: a cuff to measure blood pressure at home and a monthly phone call from a nurse. Each survivor who uses the service costs the fund 2,000 THB a year more than usual care, in every year they stay on it.

The fund's board asks one question: how much extra cash must we find in each of the next three years? The quick answer multiplies the people by the price and the years: 500 × 2,000 × 3 = 3,000,000 THB. This article corrects that habit. The group of survivors changes every year, and not everyone starts the service at once.

Counting people first gives 1,563,200 THB over the three years, about half the quick answer. The numbers are illustrative, chosen to teach.

Who qualifies: a group that fills and drains

Before any money, the fund needs to know who could use the service in each year. That group is not fixed. New strokes bring new survivors into it, and some survivors leave it: they die, move away or stop meeting the rules.

Picture a bathtub. The water is everyone who qualifies at one moment, the tap is the 100 new survivors who qualify each year, and the drain is the 10% of the group who leave at each year end. The people who qualify at a moment are called the eligible stock, and the new arrivals each year are called the incident inflow.

Leaving covers every reason at once; this example does not split deaths from moves. A new survivor counts for the whole year they join, even if the stroke happened mid-year.

Who uses the service: a share that grows

Not everyone who qualifies will use the service in its first year. Nurses need training, cuffs must be ordered, and doctors have to start referring. The fund expects the share of survivors who use it to be 20% in year 1, 40% in year 2 and 60% in year 3, as the service settles in.

That yearly share is called uptake. It is a share of everyone who qualifies that year, not a share of the newcomers. In year 1 the 100 new survivors join the 500 at the start, so 600 qualify, and 600 × 0.20 = 120 use the service.

Everyone using the service in a year, whether they started this year or earlier, is called the treated prevalence. It is the number the fund pays for, because the 2,000 THB recurs for every user in every year.

The order inside one year

Each year runs in four steps: add the new survivors at the start, count who uses the service, cost each user at 2,000 THB, and only then let 10% leave at year end. Leavers still used the service during the year, so they are counted before they go.

The order changes the answer. In year 1 the 600 who qualify include the 60 who will leave by year end. If those 60 left first, only 540 would be counted: 540 × 0.20 = 108 users and 108 × 2,000 = 216,000 THB, instead of 120 × 2,000 = 240,000 THB. The fund would be short by the cost of survivors who really used the service.

Worked example: three years of people, then cash

Each step is one year, run in the order above.

  1. Year 1

    \[ (500 + 100) \times 0.20 = 120 \]

    The 100 new survivors join the 500 at the start, so 600 qualify and 120 use the service. The fund pays 120 × 2,000 = 240,000 THB. At year end 10% of the 600 leave, 60 people, and 540 stay.

  2. Year 2

    \[ (540 + 100) \times 0.40 = 256 \]

    So 640 qualify and 256 use the service. The fund pays 256 × 2,000 = 512,000 THB. At year end 64 of the 640 leave and 576 stay.

  3. Year 3

    \[ (576 + 100) \times 0.60 = 405.6 \]

    So 676 qualify and 405.6 use the service. The fund pays 405.6 × 2,000 = 811,200 THB. The 405.6 is an expected number of people, an average, so it keeps its decimal. At year end 10% of the 676 leave, 67.6 people, and 608.4 stay; the three-year count stops there.

  4. Add the years

    Add the three years: 240,000 + 512,000 + 811,200 = 1,563,200 THB. The running total is 240,000 THB after year 1 and 752,000 after year 2.

Result: Over three years the fund needs 1,563,200 THB of extra cash, not the 3,000,000 of the quick answer. The quick answer counted the cost of all 500 survivors from the first day, as if every one of them used the service in every year.

Read the bars from left to right: each bar is everyone who qualifies that year, and its blue part is the people using the home service, 120, 256 and 405.6 (an expected count, so it can carry a decimal). Between bars, the orange arrow is the 10% who leave and the black arrow those who stay, and 100 new survivors join at the top, so the bars grow from 600 to 640 to 676 while the blue part grows faster.
Start at the left bar and move right: the label on top of each bar is the running total, and each dark blue block is that year's own cash, 240,000, 512,000 and 811,200 THB, stacked on the pale block that earlier years already paid. The blocks grow because the users grow from 120 to 256 to 405.6, so the three-year total reaches 1,563,200 THB.

The name: budget impact analysis

This year-by-year count of extra cash for one payer is called budget impact analysis. The payer is whoever pays for the care, here the district fund. The analysis asks whether the payer can afford a change in each year, not whether the change is good value for money (Sullivan and colleagues).

In symbols, write $E_y$ for the eligible stock in year $y$ after the newcomers join, $I$ for the incident inflow (100), $x$ for the share leaving at year end (0.10), $u_y$ for that year's uptake and $c$ for the extra cost per user (2,000 THB). Year 1 starts from the 500 who qualify now, so $E_1 = 500 + I$; from year 2 on, $E_y = E_{y-1}(1-x) + I$. The year's extra cash is $B_y = E_y u_y c$, and the total adds up the $B_y$.

Check the people in R

R code population.R
# Illustrative district: stroke survivors who qualify for a home blood-pressure service.
# Writes: nothing to disk; the results print in the console.
prevalent <- 500
annual_incident <- 100
exit_fraction <- 0.10
uptake <- c(0.20,0.40,0.60)
eligible <- prevalent+annual_incident
for (year in 2:length(uptake)) eligible[year] <- eligible[year-1]*(1-exit_fraction)+annual_incident
treated <- eligible*uptake
cat("Year order:",seq_along(uptake),"\n")
cat("Eligible at year start (people):",eligible,"\n")
cat("Treated prevalence (people):",treated,"\n")
cat("Year-end exits (people):",eligible*exit_fraction,"\n")
cat("Remaining stock (people):",eligible*(1-exit_fraction),"\n")
Output of the run population.log
Year order: 1 2 3
Eligible at year start (people): 600 640 676
Treated prevalence (people): 120 256 405.6
Year-end exits (people): 60 64 67.6
Remaining stock (people): 540 576 608.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 budget-impact-analysis-in-practice.R

Full tidy script
# Budget Impact Analysis in Practice: Count People Before Counting Money
# Follow one district's stroke survivors from eligible people to the extra cash the fund pays.
# ติดตามผู้ป่วยหลังโรคหลอดเลือดสมองของอำเภอหนึ่ง จากผู้มีสิทธิจนถึงเงินสดส่วนเพิ่มที่กองทุนจ่าย
# 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 years as an explicit table.
library(dplyr)  # Calculate people and money for each year.
library(ggplot2)  # Display the yearly cash 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
prevalent <- 500
annual_incident <- 100
exit_fraction <- 0.10
uptake <- c(0.20,0.40,0.60)
annual_incremental_cost <- 2000
current_care_cost <- 4500
nurse_places <- 300
discount_rate <- 0.03
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")

#%%@note 2. Follow the eligible people year by year
# New survivors join at the year start, the year's users are counted, then exits leave at year end.
# ผู้มีสิทธิใหม่เข้าต้นปี นับผู้ใช้บริการของปีนั้น แล้วผู้ที่ออกจึงออกเมื่อสิ้นปี
stock <- rep(prevalent + annual_incident, length(uptake))
for (year_index in 2:length(uptake)) {
  stock[year_index] <- stock[year_index - 1] * (1 - exit_fraction) + annual_incident
}
eligible <- c(600,640,676)
stopifnot(all(abs(stock - eligible) < 1e-9))
years <- tibble::tibble(year = seq_along(uptake), eligible = stock, uptake = uptake) |>
  dplyr::mutate(treated = eligible * uptake, exits = eligible * exit_fraction,
    remaining = eligible * (1 - exit_fraction),
    annual_budget = treated * annual_incremental_cost,
    cumulative_budget = cumsum(annual_budget))
stopifnot(all(abs(years$exits + years$remaining - years$eligible) < 1e-9))
treated <- years$treated
budget <- years$annual_budget

#%%@note 3. Price the two scenarios
# Everyone costs current care in both; only the service users add the extra cost.
# ทุกคนมีต้นทุนการดูแลเดิมในทั้งสองสถานการณ์ เฉพาะผู้ใช้บริการที่มีต้นทุนเพิ่ม
current <- eligible * current_care_cost
new <- (eligible - treated) * current_care_cost +
  treated * (current_care_cost + annual_incremental_cost)
stopifnot(all(abs(new - current - budget) < 1e-6))

#%%@note 4. Check timing, rounding and the nurse places
# Wrong timing and early rounding change the cash; the nurse places cap year 3; uptake moves 0.10.
# ลำดับเวลาที่ผิดและการปัดเศษก่อนเวลาเปลี่ยนเงินสด จำนวนผู้ใช้ที่พยาบาลรับได้จำกัดปีที่ 3
exits_first <- treated[1] * c(1, 1 - exit_fraction) * annual_incremental_cost
rounded <- c(treated[3], round(treated[3]))
today_value <- 1834 / (1 + discount_rate)^3
capped <- pmin(treated, nurse_places)
shift <- 0.10 * sum(eligible)
users <- sum(eligible * uptake) + c(0, -1, 1) * shift

#%%@note 5. Display the model
# Plot the yearly extra cash; the script does not save a graphics file.
# แสดงเงินสดส่วนเพิ่มรายปี โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(years, ggplot2::aes(x = year, y = annual_budget)) +
  ggplot2::geom_col() + ggplot2::labs(x = "Year", y = "Extra cash in the year (THB)"))

#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Year order:",seq_along(uptake),"\n")
cat("Eligible at year start (people):",eligible,"\n")
cat("Treated prevalence (people):",treated,"\n")
cat("Year-end exits (people):",eligible*exit_fraction,"\n")
cat("Remaining stock (people):",eligible*(1-exit_fraction),"\n")
cat("Annual incremental budget (THB):",big(budget),"\n")
cat("Cumulative incremental budget (THB):",big(cumsum(budget)),"\n")
cat("Shortcut, 500 x 2,000 x 3 years (THB):",big(prevalent*annual_incremental_cost*3),"\n")
cat("Cost per person (THB):",big(current_care_cost+c(0,annual_incremental_cost)),"\n")
cat("Current-care scenario (THB):",big(current),"\n")
cat("New scenario (THB):",big(new),"\n")
cat("New minus current (THB):",big(new-current),"\n")
cat("Year 1 users, exits counted after and before:",treated[1]*c(1,1-exit_fraction),"\n")
cat("Year 1, exits counted after and before (THB):",big(exits_first),"\n")
cat("Year 3 people, expected and rounded:",rounded,"\n")
cat("Year 3 budget, expected and rounded (THB):",big(rounded*annual_incremental_cost),"\n")
cat("Yearly rate back to today (%):",100*discount_rate,"\n")
cat("1,834 THB due in year 3, worth today (THB):",big(today_value),"\n")
cat("Year 3 wanted, offered, waiting:",treated[3],nurse_places,treated[3]-nurse_places,"\n")
cat("Year 3 and three years within the places (THB):",
  big(c(capped[3],sum(capped))*annual_incremental_cost),"\n")
cat("Users over three years, shift of 0.10:",users[1],shift,"\n")
cat("Users, every share 0.10 lower and higher:",users[2:3],"\n")
cat("Three years, every share 0.10 lower and higher (THB):",
  big(users[2:3]*annual_incremental_cost),"\n")
Notice the first two lines: 600, 640 and 676 qualify, and 120, 256 and 405.6 use the service. The last two lines are the leavers and the stayers at each year end.

Check the cash in R

R code budget.R
# Illustrative district: the extra cash the fund pays each year for the home service.
# Writes: nothing to disk; the results print in the console.
prevalent <- 500
annual_incident <- 100
exit_fraction <- 0.10
uptake <- c(0.20,0.40,0.60)
annual_incremental_cost <- 2000
eligible <- prevalent+annual_incident
for (year in 2:length(uptake)) eligible[year] <- eligible[year-1]*(1-exit_fraction)+annual_incident
budget <- eligible*uptake*annual_incremental_cost
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
cat("Annual incremental budget (THB):",big(budget),"\n")
cat("Cumulative incremental budget (THB):",big(cumsum(budget)),"\n")
cat("Shortcut, 500 x 2,000 x 3 years (THB):",big(prevalent*annual_incremental_cost*3),"\n")
Output of the run budget.log
Annual incremental budget (THB): 240,000 512,000 811,200
Cumulative incremental budget (THB): 240,000 752,000 1,563,200
Shortcut, 500 x 2,000 x 3 years (THB): 3,000,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 budget-impact-analysis-in-practice.R

Full tidy script
# Budget Impact Analysis in Practice: Count People Before Counting Money
# Follow one district's stroke survivors from eligible people to the extra cash the fund pays.
# ติดตามผู้ป่วยหลังโรคหลอดเลือดสมองของอำเภอหนึ่ง จากผู้มีสิทธิจนถึงเงินสดส่วนเพิ่มที่กองทุนจ่าย
# 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 years as an explicit table.
library(dplyr)  # Calculate people and money for each year.
library(ggplot2)  # Display the yearly cash 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
prevalent <- 500
annual_incident <- 100
exit_fraction <- 0.10
uptake <- c(0.20,0.40,0.60)
annual_incremental_cost <- 2000
current_care_cost <- 4500
nurse_places <- 300
discount_rate <- 0.03
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")

#%%@note 2. Follow the eligible people year by year
# New survivors join at the year start, the year's users are counted, then exits leave at year end.
# ผู้มีสิทธิใหม่เข้าต้นปี นับผู้ใช้บริการของปีนั้น แล้วผู้ที่ออกจึงออกเมื่อสิ้นปี
stock <- rep(prevalent + annual_incident, length(uptake))
for (year_index in 2:length(uptake)) {
  stock[year_index] <- stock[year_index - 1] * (1 - exit_fraction) + annual_incident
}
eligible <- c(600,640,676)
stopifnot(all(abs(stock - eligible) < 1e-9))
years <- tibble::tibble(year = seq_along(uptake), eligible = stock, uptake = uptake) |>
  dplyr::mutate(treated = eligible * uptake, exits = eligible * exit_fraction,
    remaining = eligible * (1 - exit_fraction),
    annual_budget = treated * annual_incremental_cost,
    cumulative_budget = cumsum(annual_budget))
stopifnot(all(abs(years$exits + years$remaining - years$eligible) < 1e-9))
treated <- years$treated
budget <- years$annual_budget

#%%@note 3. Price the two scenarios
# Everyone costs current care in both; only the service users add the extra cost.
# ทุกคนมีต้นทุนการดูแลเดิมในทั้งสองสถานการณ์ เฉพาะผู้ใช้บริการที่มีต้นทุนเพิ่ม
current <- eligible * current_care_cost
new <- (eligible - treated) * current_care_cost +
  treated * (current_care_cost + annual_incremental_cost)
stopifnot(all(abs(new - current - budget) < 1e-6))

#%%@note 4. Check timing, rounding and the nurse places
# Wrong timing and early rounding change the cash; the nurse places cap year 3; uptake moves 0.10.
# ลำดับเวลาที่ผิดและการปัดเศษก่อนเวลาเปลี่ยนเงินสด จำนวนผู้ใช้ที่พยาบาลรับได้จำกัดปีที่ 3
exits_first <- treated[1] * c(1, 1 - exit_fraction) * annual_incremental_cost
rounded <- c(treated[3], round(treated[3]))
today_value <- 1834 / (1 + discount_rate)^3
capped <- pmin(treated, nurse_places)
shift <- 0.10 * sum(eligible)
users <- sum(eligible * uptake) + c(0, -1, 1) * shift

#%%@note 5. Display the model
# Plot the yearly extra cash; the script does not save a graphics file.
# แสดงเงินสดส่วนเพิ่มรายปี โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(years, ggplot2::aes(x = year, y = annual_budget)) +
  ggplot2::geom_col() + ggplot2::labs(x = "Year", y = "Extra cash in the year (THB)"))

#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Year order:",seq_along(uptake),"\n")
cat("Eligible at year start (people):",eligible,"\n")
cat("Treated prevalence (people):",treated,"\n")
cat("Year-end exits (people):",eligible*exit_fraction,"\n")
cat("Remaining stock (people):",eligible*(1-exit_fraction),"\n")
cat("Annual incremental budget (THB):",big(budget),"\n")
cat("Cumulative incremental budget (THB):",big(cumsum(budget)),"\n")
cat("Shortcut, 500 x 2,000 x 3 years (THB):",big(prevalent*annual_incremental_cost*3),"\n")
cat("Cost per person (THB):",big(current_care_cost+c(0,annual_incremental_cost)),"\n")
cat("Current-care scenario (THB):",big(current),"\n")
cat("New scenario (THB):",big(new),"\n")
cat("New minus current (THB):",big(new-current),"\n")
cat("Year 1 users, exits counted after and before:",treated[1]*c(1,1-exit_fraction),"\n")
cat("Year 1, exits counted after and before (THB):",big(exits_first),"\n")
cat("Year 3 people, expected and rounded:",rounded,"\n")
cat("Year 3 budget, expected and rounded (THB):",big(rounded*annual_incremental_cost),"\n")
cat("Yearly rate back to today (%):",100*discount_rate,"\n")
cat("1,834 THB due in year 3, worth today (THB):",big(today_value),"\n")
cat("Year 3 wanted, offered, waiting:",treated[3],nurse_places,treated[3]-nurse_places,"\n")
cat("Year 3 and three years within the places (THB):",
  big(c(capped[3],sum(capped))*annual_incremental_cost),"\n")
cat("Users over three years, shift of 0.10:",users[1],shift,"\n")
cat("Users, every share 0.10 lower and higher:",users[2:3],"\n")
cat("Three years, every share 0.10 lower and higher (THB):",
  big(users[2:3]*annual_incremental_cost),"\n")
Notice that the yearly amounts add up to 1,563,200 THB on the second line, while the quick answer on the last line is 3,000,000 THB.
The controls start at the district: 500 survivors who already qualify, 100 new each year, 0.10 leaving, shares of 0.20, 0.40 and 0.60 using the service, and 2,000 THB per user. On the left, grey bars are everyone who qualifies and blue is those using the service. Raise the share using the service in year 3 and only the last orange bar and the end of the green running-total line rise. Drag the share leaving up from 0.10, and once more people leave each year than the 100 who join, the grey bars shrink from year to year.

With and without the service: only the difference is the budget

The fund already pays for usual care: clinic visits and blood-pressure pills. Say usual care costs 4,500 THB per survivor a year, so a survivor on the home service costs 4,500 + 2,000 = 6,500 THB.

Now price the same year two ways. Without the service, all 600 survivors get usual care, and year 1 costs 600 × 4,500 = 2,700,000 THB. With it, every survivor still costs 4,500 for usual care, and the 120 users add 2,000 each, so spending is 2,700,000 + 240,000 = 2,940,000 THB. The difference is exactly the 240,000 THB counted above.

Years 2 and 3 work the same way: without the service, year 2 costs 640 × 4,500 = 2,880,000 and year 3 costs 676 × 4,500 = 3,042,000 THB, and with it they cost 512,000 and 811,200 THB more. Usual care cancels because it appears on both sides.

The two versions are called the current scenario and the new scenario, and the budget impact is the new minus the current, year by year. Read across a row of the table: the extra cash that year is the with column minus the without column.

Spending with and without the service, cash in the year paid

YearUsersWithout the service (THB)With the service (THB)Extra cash that year (THB)Running total (THB)
11202,700,0002,940,000240,000240,000
22562,880,0003,392,000512,000752,000
3405.63,042,0003,853,200811,2001,563,200

Check the two scenarios in R

R code scenarios.R
# Illustrative district: total spending with and without the home service, year by year.
# Writes: nothing to disk; the results print in the console.
eligible <- c(600,640,676)
uptake <- c(0.20,0.40,0.60)
annual_incremental_cost <- 2000
current_care_cost <- 4500
treated <- eligible*uptake
current <- eligible*current_care_cost
new <- (eligible-treated)*current_care_cost+treated*(current_care_cost+annual_incremental_cost)
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
cat("Cost per person (THB):",big(current_care_cost+c(0,annual_incremental_cost)),"\n")
cat("Current-care scenario (THB):",big(current),"\n")
cat("New scenario (THB):",big(new),"\n")
cat("New minus current (THB):",big(new-current),"\n")
Output of the run scenarios.log
Cost per person (THB): 4,500 6,500
Current-care scenario (THB): 2,700,000 2,880,000 3,042,000
New scenario (THB): 2,940,000 3,392,000 3,853,200
New minus current (THB): 240,000 512,000 811,200

Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download budget-impact-analysis-in-practice.R

Full tidy script
# Budget Impact Analysis in Practice: Count People Before Counting Money
# Follow one district's stroke survivors from eligible people to the extra cash the fund pays.
# ติดตามผู้ป่วยหลังโรคหลอดเลือดสมองของอำเภอหนึ่ง จากผู้มีสิทธิจนถึงเงินสดส่วนเพิ่มที่กองทุนจ่าย
# 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 years as an explicit table.
library(dplyr)  # Calculate people and money for each year.
library(ggplot2)  # Display the yearly cash 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
prevalent <- 500
annual_incident <- 100
exit_fraction <- 0.10
uptake <- c(0.20,0.40,0.60)
annual_incremental_cost <- 2000
current_care_cost <- 4500
nurse_places <- 300
discount_rate <- 0.03
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")

#%%@note 2. Follow the eligible people year by year
# New survivors join at the year start, the year's users are counted, then exits leave at year end.
# ผู้มีสิทธิใหม่เข้าต้นปี นับผู้ใช้บริการของปีนั้น แล้วผู้ที่ออกจึงออกเมื่อสิ้นปี
stock <- rep(prevalent + annual_incident, length(uptake))
for (year_index in 2:length(uptake)) {
  stock[year_index] <- stock[year_index - 1] * (1 - exit_fraction) + annual_incident
}
eligible <- c(600,640,676)
stopifnot(all(abs(stock - eligible) < 1e-9))
years <- tibble::tibble(year = seq_along(uptake), eligible = stock, uptake = uptake) |>
  dplyr::mutate(treated = eligible * uptake, exits = eligible * exit_fraction,
    remaining = eligible * (1 - exit_fraction),
    annual_budget = treated * annual_incremental_cost,
    cumulative_budget = cumsum(annual_budget))
stopifnot(all(abs(years$exits + years$remaining - years$eligible) < 1e-9))
treated <- years$treated
budget <- years$annual_budget

#%%@note 3. Price the two scenarios
# Everyone costs current care in both; only the service users add the extra cost.
# ทุกคนมีต้นทุนการดูแลเดิมในทั้งสองสถานการณ์ เฉพาะผู้ใช้บริการที่มีต้นทุนเพิ่ม
current <- eligible * current_care_cost
new <- (eligible - treated) * current_care_cost +
  treated * (current_care_cost + annual_incremental_cost)
stopifnot(all(abs(new - current - budget) < 1e-6))

#%%@note 4. Check timing, rounding and the nurse places
# Wrong timing and early rounding change the cash; the nurse places cap year 3; uptake moves 0.10.
# ลำดับเวลาที่ผิดและการปัดเศษก่อนเวลาเปลี่ยนเงินสด จำนวนผู้ใช้ที่พยาบาลรับได้จำกัดปีที่ 3
exits_first <- treated[1] * c(1, 1 - exit_fraction) * annual_incremental_cost
rounded <- c(treated[3], round(treated[3]))
today_value <- 1834 / (1 + discount_rate)^3
capped <- pmin(treated, nurse_places)
shift <- 0.10 * sum(eligible)
users <- sum(eligible * uptake) + c(0, -1, 1) * shift

#%%@note 5. Display the model
# Plot the yearly extra cash; the script does not save a graphics file.
# แสดงเงินสดส่วนเพิ่มรายปี โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(years, ggplot2::aes(x = year, y = annual_budget)) +
  ggplot2::geom_col() + ggplot2::labs(x = "Year", y = "Extra cash in the year (THB)"))

#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Year order:",seq_along(uptake),"\n")
cat("Eligible at year start (people):",eligible,"\n")
cat("Treated prevalence (people):",treated,"\n")
cat("Year-end exits (people):",eligible*exit_fraction,"\n")
cat("Remaining stock (people):",eligible*(1-exit_fraction),"\n")
cat("Annual incremental budget (THB):",big(budget),"\n")
cat("Cumulative incremental budget (THB):",big(cumsum(budget)),"\n")
cat("Shortcut, 500 x 2,000 x 3 years (THB):",big(prevalent*annual_incremental_cost*3),"\n")
cat("Cost per person (THB):",big(current_care_cost+c(0,annual_incremental_cost)),"\n")
cat("Current-care scenario (THB):",big(current),"\n")
cat("New scenario (THB):",big(new),"\n")
cat("New minus current (THB):",big(new-current),"\n")
cat("Year 1 users, exits counted after and before:",treated[1]*c(1,1-exit_fraction),"\n")
cat("Year 1, exits counted after and before (THB):",big(exits_first),"\n")
cat("Year 3 people, expected and rounded:",rounded,"\n")
cat("Year 3 budget, expected and rounded (THB):",big(rounded*annual_incremental_cost),"\n")
cat("Yearly rate back to today (%):",100*discount_rate,"\n")
cat("1,834 THB due in year 3, worth today (THB):",big(today_value),"\n")
cat("Year 3 wanted, offered, waiting:",treated[3],nurse_places,treated[3]-nurse_places,"\n")
cat("Year 3 and three years within the places (THB):",
  big(c(capped[3],sum(capped))*annual_incremental_cost),"\n")
cat("Users over three years, shift of 0.10:",users[1],shift,"\n")
cat("Users, every share 0.10 lower and higher:",users[2:3],"\n")
cat("Three years, every share 0.10 lower and higher (THB):",
  big(users[2:3]*annual_incremental_cost),"\n")
Notice that the last line, new minus current, repeats the yearly extra cash: the 4,500 THB of usual care has cancelled.

Pay for every user, every year, and keep the decimals

The 2,000 THB recurs: the cuff service and the monthly nurse calls repeat in every year a survivor stays on the service. So each year's cash covers every user that year, not only the newcomers or the growth since last year. In year 3 the fund pays for 405.6 users, including those who started in years 1 and 2 and stayed on.

A one-off cost works differently. If instead each new user were given a cuff to keep, bought once, the fund would need the number who start in each year. Uptake alone cannot give that: the 256 users of year 2 mix continuing users with new starters, and the inputs do not say how many of each. A one-time treatment is budgeted the same way: the endovascular stroke treatment evaluated in Thailand by Rattanavipapong and colleagues is given once, at the stroke, so its budget counts the new patients each year.

The 405.6 is an expected count, an average over the many ways the year could turn out, so it need not be a whole number. Keep it until the last step. Rounding it to 406 first would cost 812,000 THB instead of 811,200 THB, a change that no fact about the survivors caused.

Check the timing and the rounding in R

R code timing.R
# Illustrative district: what changes if exits leave too early or people are rounded too soon.
# Writes: nothing to disk; the results print in the console.
eligible <- c(600,640,676)
uptake <- c(0.20,0.40,0.60)
annual_incremental_cost <- 2000
exit_fraction <- 0.10
treated <- eligible*uptake
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
cat("Year 1 users, exits counted after and before:",treated[1]*c(1,1-exit_fraction),"\n")
cat("Year 1, exits counted after and before (THB):",
  big(treated[1]*c(1,1-exit_fraction)*annual_incremental_cost),"\n")
cat("Year 3 people, expected and rounded:",treated[3],round(treated[3]),"\n")
cat("Year 3 budget, expected and rounded (THB):",
  big(c(treated[3],round(treated[3]))*annual_incremental_cost),"\n")
Output of the run timing.log
Year 1 users, exits counted after and before: 120 108
Year 1, exits counted after and before (THB): 240,000 216,000
Year 3 people, expected and rounded: 405.6 406
Year 3 budget, expected and rounded (THB): 811,200 812,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 budget-impact-analysis-in-practice.R

Full tidy script
# Budget Impact Analysis in Practice: Count People Before Counting Money
# Follow one district's stroke survivors from eligible people to the extra cash the fund pays.
# ติดตามผู้ป่วยหลังโรคหลอดเลือดสมองของอำเภอหนึ่ง จากผู้มีสิทธิจนถึงเงินสดส่วนเพิ่มที่กองทุนจ่าย
# 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 years as an explicit table.
library(dplyr)  # Calculate people and money for each year.
library(ggplot2)  # Display the yearly cash 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
prevalent <- 500
annual_incident <- 100
exit_fraction <- 0.10
uptake <- c(0.20,0.40,0.60)
annual_incremental_cost <- 2000
current_care_cost <- 4500
nurse_places <- 300
discount_rate <- 0.03
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")

#%%@note 2. Follow the eligible people year by year
# New survivors join at the year start, the year's users are counted, then exits leave at year end.
# ผู้มีสิทธิใหม่เข้าต้นปี นับผู้ใช้บริการของปีนั้น แล้วผู้ที่ออกจึงออกเมื่อสิ้นปี
stock <- rep(prevalent + annual_incident, length(uptake))
for (year_index in 2:length(uptake)) {
  stock[year_index] <- stock[year_index - 1] * (1 - exit_fraction) + annual_incident
}
eligible <- c(600,640,676)
stopifnot(all(abs(stock - eligible) < 1e-9))
years <- tibble::tibble(year = seq_along(uptake), eligible = stock, uptake = uptake) |>
  dplyr::mutate(treated = eligible * uptake, exits = eligible * exit_fraction,
    remaining = eligible * (1 - exit_fraction),
    annual_budget = treated * annual_incremental_cost,
    cumulative_budget = cumsum(annual_budget))
stopifnot(all(abs(years$exits + years$remaining - years$eligible) < 1e-9))
treated <- years$treated
budget <- years$annual_budget

#%%@note 3. Price the two scenarios
# Everyone costs current care in both; only the service users add the extra cost.
# ทุกคนมีต้นทุนการดูแลเดิมในทั้งสองสถานการณ์ เฉพาะผู้ใช้บริการที่มีต้นทุนเพิ่ม
current <- eligible * current_care_cost
new <- (eligible - treated) * current_care_cost +
  treated * (current_care_cost + annual_incremental_cost)
stopifnot(all(abs(new - current - budget) < 1e-6))

#%%@note 4. Check timing, rounding and the nurse places
# Wrong timing and early rounding change the cash; the nurse places cap year 3; uptake moves 0.10.
# ลำดับเวลาที่ผิดและการปัดเศษก่อนเวลาเปลี่ยนเงินสด จำนวนผู้ใช้ที่พยาบาลรับได้จำกัดปีที่ 3
exits_first <- treated[1] * c(1, 1 - exit_fraction) * annual_incremental_cost
rounded <- c(treated[3], round(treated[3]))
today_value <- 1834 / (1 + discount_rate)^3
capped <- pmin(treated, nurse_places)
shift <- 0.10 * sum(eligible)
users <- sum(eligible * uptake) + c(0, -1, 1) * shift

#%%@note 5. Display the model
# Plot the yearly extra cash; the script does not save a graphics file.
# แสดงเงินสดส่วนเพิ่มรายปี โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(years, ggplot2::aes(x = year, y = annual_budget)) +
  ggplot2::geom_col() + ggplot2::labs(x = "Year", y = "Extra cash in the year (THB)"))

#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Year order:",seq_along(uptake),"\n")
cat("Eligible at year start (people):",eligible,"\n")
cat("Treated prevalence (people):",treated,"\n")
cat("Year-end exits (people):",eligible*exit_fraction,"\n")
cat("Remaining stock (people):",eligible*(1-exit_fraction),"\n")
cat("Annual incremental budget (THB):",big(budget),"\n")
cat("Cumulative incremental budget (THB):",big(cumsum(budget)),"\n")
cat("Shortcut, 500 x 2,000 x 3 years (THB):",big(prevalent*annual_incremental_cost*3),"\n")
cat("Cost per person (THB):",big(current_care_cost+c(0,annual_incremental_cost)),"\n")
cat("Current-care scenario (THB):",big(current),"\n")
cat("New scenario (THB):",big(new),"\n")
cat("New minus current (THB):",big(new-current),"\n")
cat("Year 1 users, exits counted after and before:",treated[1]*c(1,1-exit_fraction),"\n")
cat("Year 1, exits counted after and before (THB):",big(exits_first),"\n")
cat("Year 3 people, expected and rounded:",rounded,"\n")
cat("Year 3 budget, expected and rounded (THB):",big(rounded*annual_incremental_cost),"\n")
cat("Yearly rate back to today (%):",100*discount_rate,"\n")
cat("1,834 THB due in year 3, worth today (THB):",big(today_value),"\n")
cat("Year 3 wanted, offered, waiting:",treated[3],nurse_places,treated[3]-nurse_places,"\n")
cat("Year 3 and three years within the places (THB):",
  big(c(capped[3],sum(capped))*annual_incremental_cost),"\n")
cat("Users over three years, shift of 0.10:",users[1],shift,"\n")
cat("Users, every share 0.10 lower and higher:",users[2:3],"\n")
cat("Three years, every share 0.10 lower and higher (THB):",
  big(users[2:3]*annual_incremental_cost),"\n")
Notice the first two lines, 108 users and 216,000 THB when the leavers go first, against 120 and 240,000, and the 406 that early rounding makes of 405.6.

Cash in the year it is paid, not value for money

Economic evaluations often shrink future amounts to their value today, because people and payers prefer to pay later and benefit sooner. Discounting is converting a future cost or health gain into its value today, shrinking it by the discount rate for each year away. At 3% a cost of 1,834 THB due in year 3 is worth 1,678 THB today; the full lesson is in Whose Cost, in Which Year? Perspective, Opportunity Cost, Inflation and Discounting.

A budget does not do this. In year 3 the fund must pay 811,200 THB in cash, whatever that sum is worth today, so every amount in this article is cash in the year it is paid.

A budget answers whether the fund can pay; it does not say whether the service is good value for money. That question weighs the extra cost against the health gained, which this calculation does not count (Drummond and colleagues). A service can be good value and still not fit in year 3's budget.

Check the recap's numbers in R

R code cash.R
# Illustrative district: what a future cost is worth today, for contrast with cash as paid.
# Writes: nothing to disk; the results print in the console.
discount_rate <- 0.03
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
cat("Yearly rate back to today (%):",100*discount_rate,"\n")
cat("1,834 THB due in year 3, worth today (THB):",big(1834/(1+discount_rate)^3),"\n")
Output of the run cash.log
Yearly rate back to today (%): 3
1,834 THB due in year 3, worth today (THB): 1,678

Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download budget-impact-analysis-in-practice.R

Full tidy script
# Budget Impact Analysis in Practice: Count People Before Counting Money
# Follow one district's stroke survivors from eligible people to the extra cash the fund pays.
# ติดตามผู้ป่วยหลังโรคหลอดเลือดสมองของอำเภอหนึ่ง จากผู้มีสิทธิจนถึงเงินสดส่วนเพิ่มที่กองทุนจ่าย
# 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 years as an explicit table.
library(dplyr)  # Calculate people and money for each year.
library(ggplot2)  # Display the yearly cash 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
prevalent <- 500
annual_incident <- 100
exit_fraction <- 0.10
uptake <- c(0.20,0.40,0.60)
annual_incremental_cost <- 2000
current_care_cost <- 4500
nurse_places <- 300
discount_rate <- 0.03
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")

#%%@note 2. Follow the eligible people year by year
# New survivors join at the year start, the year's users are counted, then exits leave at year end.
# ผู้มีสิทธิใหม่เข้าต้นปี นับผู้ใช้บริการของปีนั้น แล้วผู้ที่ออกจึงออกเมื่อสิ้นปี
stock <- rep(prevalent + annual_incident, length(uptake))
for (year_index in 2:length(uptake)) {
  stock[year_index] <- stock[year_index - 1] * (1 - exit_fraction) + annual_incident
}
eligible <- c(600,640,676)
stopifnot(all(abs(stock - eligible) < 1e-9))
years <- tibble::tibble(year = seq_along(uptake), eligible = stock, uptake = uptake) |>
  dplyr::mutate(treated = eligible * uptake, exits = eligible * exit_fraction,
    remaining = eligible * (1 - exit_fraction),
    annual_budget = treated * annual_incremental_cost,
    cumulative_budget = cumsum(annual_budget))
stopifnot(all(abs(years$exits + years$remaining - years$eligible) < 1e-9))
treated <- years$treated
budget <- years$annual_budget

#%%@note 3. Price the two scenarios
# Everyone costs current care in both; only the service users add the extra cost.
# ทุกคนมีต้นทุนการดูแลเดิมในทั้งสองสถานการณ์ เฉพาะผู้ใช้บริการที่มีต้นทุนเพิ่ม
current <- eligible * current_care_cost
new <- (eligible - treated) * current_care_cost +
  treated * (current_care_cost + annual_incremental_cost)
stopifnot(all(abs(new - current - budget) < 1e-6))

#%%@note 4. Check timing, rounding and the nurse places
# Wrong timing and early rounding change the cash; the nurse places cap year 3; uptake moves 0.10.
# ลำดับเวลาที่ผิดและการปัดเศษก่อนเวลาเปลี่ยนเงินสด จำนวนผู้ใช้ที่พยาบาลรับได้จำกัดปีที่ 3
exits_first <- treated[1] * c(1, 1 - exit_fraction) * annual_incremental_cost
rounded <- c(treated[3], round(treated[3]))
today_value <- 1834 / (1 + discount_rate)^3
capped <- pmin(treated, nurse_places)
shift <- 0.10 * sum(eligible)
users <- sum(eligible * uptake) + c(0, -1, 1) * shift

#%%@note 5. Display the model
# Plot the yearly extra cash; the script does not save a graphics file.
# แสดงเงินสดส่วนเพิ่มรายปี โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(years, ggplot2::aes(x = year, y = annual_budget)) +
  ggplot2::geom_col() + ggplot2::labs(x = "Year", y = "Extra cash in the year (THB)"))

#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Year order:",seq_along(uptake),"\n")
cat("Eligible at year start (people):",eligible,"\n")
cat("Treated prevalence (people):",treated,"\n")
cat("Year-end exits (people):",eligible*exit_fraction,"\n")
cat("Remaining stock (people):",eligible*(1-exit_fraction),"\n")
cat("Annual incremental budget (THB):",big(budget),"\n")
cat("Cumulative incremental budget (THB):",big(cumsum(budget)),"\n")
cat("Shortcut, 500 x 2,000 x 3 years (THB):",big(prevalent*annual_incremental_cost*3),"\n")
cat("Cost per person (THB):",big(current_care_cost+c(0,annual_incremental_cost)),"\n")
cat("Current-care scenario (THB):",big(current),"\n")
cat("New scenario (THB):",big(new),"\n")
cat("New minus current (THB):",big(new-current),"\n")
cat("Year 1 users, exits counted after and before:",treated[1]*c(1,1-exit_fraction),"\n")
cat("Year 1, exits counted after and before (THB):",big(exits_first),"\n")
cat("Year 3 people, expected and rounded:",rounded,"\n")
cat("Year 3 budget, expected and rounded (THB):",big(rounded*annual_incremental_cost),"\n")
cat("Yearly rate back to today (%):",100*discount_rate,"\n")
cat("1,834 THB due in year 3, worth today (THB):",big(today_value),"\n")
cat("Year 3 wanted, offered, waiting:",treated[3],nurse_places,treated[3]-nurse_places,"\n")
cat("Year 3 and three years within the places (THB):",
  big(c(capped[3],sum(capped))*annual_incremental_cost),"\n")
cat("Users over three years, shift of 0.10:",users[1],shift,"\n")
cat("Users, every share 0.10 lower and higher:",users[2:3],"\n")
cat("Three years, every share 0.10 lower and higher (THB):",
  big(users[2:3]*annual_incremental_cost),"\n")
Notice the two lines: the 3% rate and the 1,678 THB that a cost of 1,834 due in year 3 is worth today.

Can the nurses deliver the uptake?

A budget assumes the service can reach everyone it counts. Say the district's nurses can follow at most 300 users a year. The plan for year 3 has 405.6 users, so 405.6 − 300 = 105.6 survivors would have to wait.

Then the year 3 cash is 300 × 2,000 = 600,000 THB, not 811,200 THB, and the three years add 240,000 + 512,000 + 600,000 = 1,352,000 THB. Report both numbers: the cash the fund would pay and the survivors the service cannot reach yet, so the board sees the unmet need and not only a smaller bill.

Check cuffs, referral routes and nurse time before trusting a rising uptake. Uptake itself is a forecast, so report a range. The plan's users add up to 120 + 256 + 405.6 = 781.6 over the three years, and moving every year's share down by 0.10 removes 0.10 × (600 + 640 + 676) = 191.6 of them: 590 users and 590 × 2,000 = 1,180,000 THB. Moving every share up by 0.10 adds the same 191.6, giving 973.2 users and 1,946,400 THB before the nurse limit.

Check the nurse limit in R

R code capacity.R
# Illustrative district: the users the nurses can follow in year 3.
# Writes: nothing to disk; the results print in the console.
eligible <- c(600,640,676)
uptake <- c(0.20,0.40,0.60)
annual_incremental_cost <- 2000
nurse_places <- 300
treated <- eligible*uptake
capped <- pmin(treated,nurse_places)
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
cat("Year 3 wanted, offered, waiting:",treated[3],nurse_places,treated[3]-nurse_places,"\n")
cat("Year 3 and three years within the places (THB):",
  big(c(capped[3],sum(capped))*annual_incremental_cost),"\n")
Output of the run capacity.log
Year 3 wanted, offered, waiting: 405.6 300 105.6
Year 3 and three years within the places (THB): 600,000 1,352,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 budget-impact-analysis-in-practice.R

Full tidy script
# Budget Impact Analysis in Practice: Count People Before Counting Money
# Follow one district's stroke survivors from eligible people to the extra cash the fund pays.
# ติดตามผู้ป่วยหลังโรคหลอดเลือดสมองของอำเภอหนึ่ง จากผู้มีสิทธิจนถึงเงินสดส่วนเพิ่มที่กองทุนจ่าย
# 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 years as an explicit table.
library(dplyr)  # Calculate people and money for each year.
library(ggplot2)  # Display the yearly cash 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
prevalent <- 500
annual_incident <- 100
exit_fraction <- 0.10
uptake <- c(0.20,0.40,0.60)
annual_incremental_cost <- 2000
current_care_cost <- 4500
nurse_places <- 300
discount_rate <- 0.03
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")

#%%@note 2. Follow the eligible people year by year
# New survivors join at the year start, the year's users are counted, then exits leave at year end.
# ผู้มีสิทธิใหม่เข้าต้นปี นับผู้ใช้บริการของปีนั้น แล้วผู้ที่ออกจึงออกเมื่อสิ้นปี
stock <- rep(prevalent + annual_incident, length(uptake))
for (year_index in 2:length(uptake)) {
  stock[year_index] <- stock[year_index - 1] * (1 - exit_fraction) + annual_incident
}
eligible <- c(600,640,676)
stopifnot(all(abs(stock - eligible) < 1e-9))
years <- tibble::tibble(year = seq_along(uptake), eligible = stock, uptake = uptake) |>
  dplyr::mutate(treated = eligible * uptake, exits = eligible * exit_fraction,
    remaining = eligible * (1 - exit_fraction),
    annual_budget = treated * annual_incremental_cost,
    cumulative_budget = cumsum(annual_budget))
stopifnot(all(abs(years$exits + years$remaining - years$eligible) < 1e-9))
treated <- years$treated
budget <- years$annual_budget

#%%@note 3. Price the two scenarios
# Everyone costs current care in both; only the service users add the extra cost.
# ทุกคนมีต้นทุนการดูแลเดิมในทั้งสองสถานการณ์ เฉพาะผู้ใช้บริการที่มีต้นทุนเพิ่ม
current <- eligible * current_care_cost
new <- (eligible - treated) * current_care_cost +
  treated * (current_care_cost + annual_incremental_cost)
stopifnot(all(abs(new - current - budget) < 1e-6))

#%%@note 4. Check timing, rounding and the nurse places
# Wrong timing and early rounding change the cash; the nurse places cap year 3; uptake moves 0.10.
# ลำดับเวลาที่ผิดและการปัดเศษก่อนเวลาเปลี่ยนเงินสด จำนวนผู้ใช้ที่พยาบาลรับได้จำกัดปีที่ 3
exits_first <- treated[1] * c(1, 1 - exit_fraction) * annual_incremental_cost
rounded <- c(treated[3], round(treated[3]))
today_value <- 1834 / (1 + discount_rate)^3
capped <- pmin(treated, nurse_places)
shift <- 0.10 * sum(eligible)
users <- sum(eligible * uptake) + c(0, -1, 1) * shift

#%%@note 5. Display the model
# Plot the yearly extra cash; the script does not save a graphics file.
# แสดงเงินสดส่วนเพิ่มรายปี โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(years, ggplot2::aes(x = year, y = annual_budget)) +
  ggplot2::geom_col() + ggplot2::labs(x = "Year", y = "Extra cash in the year (THB)"))

#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Year order:",seq_along(uptake),"\n")
cat("Eligible at year start (people):",eligible,"\n")
cat("Treated prevalence (people):",treated,"\n")
cat("Year-end exits (people):",eligible*exit_fraction,"\n")
cat("Remaining stock (people):",eligible*(1-exit_fraction),"\n")
cat("Annual incremental budget (THB):",big(budget),"\n")
cat("Cumulative incremental budget (THB):",big(cumsum(budget)),"\n")
cat("Shortcut, 500 x 2,000 x 3 years (THB):",big(prevalent*annual_incremental_cost*3),"\n")
cat("Cost per person (THB):",big(current_care_cost+c(0,annual_incremental_cost)),"\n")
cat("Current-care scenario (THB):",big(current),"\n")
cat("New scenario (THB):",big(new),"\n")
cat("New minus current (THB):",big(new-current),"\n")
cat("Year 1 users, exits counted after and before:",treated[1]*c(1,1-exit_fraction),"\n")
cat("Year 1, exits counted after and before (THB):",big(exits_first),"\n")
cat("Year 3 people, expected and rounded:",rounded,"\n")
cat("Year 3 budget, expected and rounded (THB):",big(rounded*annual_incremental_cost),"\n")
cat("Yearly rate back to today (%):",100*discount_rate,"\n")
cat("1,834 THB due in year 3, worth today (THB):",big(today_value),"\n")
cat("Year 3 wanted, offered, waiting:",treated[3],nurse_places,treated[3]-nurse_places,"\n")
cat("Year 3 and three years within the places (THB):",
  big(c(capped[3],sum(capped))*annual_incremental_cost),"\n")
cat("Users over three years, shift of 0.10:",users[1],shift,"\n")
cat("Users, every share 0.10 lower and higher:",users[2:3],"\n")
cat("Three years, every share 0.10 lower and higher (THB):",
  big(users[2:3]*annual_incremental_cost),"\n")
Notice the 105.6 survivors waiting and the 600,000 and 1,352,000 THB within the nurse limit.

Check the uptake range in R

R code range.R
# Illustrative district: the three-year cash if every year's uptake is 0.10 lower or higher.
# Writes: nothing to disk; the results print in the console.
eligible <- c(600,640,676)
uptake <- c(0.20,0.40,0.60)
annual_incremental_cost <- 2000
shift <- 0.10*sum(eligible)
users <- sum(eligible*uptake)+c(0,-1,1)*shift
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")
cat("Users over three years, shift of 0.10:",users[1],shift,"\n")
cat("Users, every share 0.10 lower and higher:",users[2:3],"\n")
cat("Three years, every share 0.10 lower and higher (THB):",
  big(users[2:3]*annual_incremental_cost),"\n")
Output of the run range.log
Users over three years, shift of 0.10: 781.6 191.6
Users, every share 0.10 lower and higher: 590 973.2
Three years, every share 0.10 lower and higher (THB): 1,180,000 1,946,400

Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download budget-impact-analysis-in-practice.R

Full tidy script
# Budget Impact Analysis in Practice: Count People Before Counting Money
# Follow one district's stroke survivors from eligible people to the extra cash the fund pays.
# ติดตามผู้ป่วยหลังโรคหลอดเลือดสมองของอำเภอหนึ่ง จากผู้มีสิทธิจนถึงเงินสดส่วนเพิ่มที่กองทุนจ่าย
# 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 years as an explicit table.
library(dplyr)  # Calculate people and money for each year.
library(ggplot2)  # Display the yearly cash 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
prevalent <- 500
annual_incident <- 100
exit_fraction <- 0.10
uptake <- c(0.20,0.40,0.60)
annual_incremental_cost <- 2000
current_care_cost <- 4500
nurse_places <- 300
discount_rate <- 0.03
big <- function(value) formatC(value, format = "f", digits = 0, big.mark = ",")

#%%@note 2. Follow the eligible people year by year
# New survivors join at the year start, the year's users are counted, then exits leave at year end.
# ผู้มีสิทธิใหม่เข้าต้นปี นับผู้ใช้บริการของปีนั้น แล้วผู้ที่ออกจึงออกเมื่อสิ้นปี
stock <- rep(prevalent + annual_incident, length(uptake))
for (year_index in 2:length(uptake)) {
  stock[year_index] <- stock[year_index - 1] * (1 - exit_fraction) + annual_incident
}
eligible <- c(600,640,676)
stopifnot(all(abs(stock - eligible) < 1e-9))
years <- tibble::tibble(year = seq_along(uptake), eligible = stock, uptake = uptake) |>
  dplyr::mutate(treated = eligible * uptake, exits = eligible * exit_fraction,
    remaining = eligible * (1 - exit_fraction),
    annual_budget = treated * annual_incremental_cost,
    cumulative_budget = cumsum(annual_budget))
stopifnot(all(abs(years$exits + years$remaining - years$eligible) < 1e-9))
treated <- years$treated
budget <- years$annual_budget

#%%@note 3. Price the two scenarios
# Everyone costs current care in both; only the service users add the extra cost.
# ทุกคนมีต้นทุนการดูแลเดิมในทั้งสองสถานการณ์ เฉพาะผู้ใช้บริการที่มีต้นทุนเพิ่ม
current <- eligible * current_care_cost
new <- (eligible - treated) * current_care_cost +
  treated * (current_care_cost + annual_incremental_cost)
stopifnot(all(abs(new - current - budget) < 1e-6))

#%%@note 4. Check timing, rounding and the nurse places
# Wrong timing and early rounding change the cash; the nurse places cap year 3; uptake moves 0.10.
# ลำดับเวลาที่ผิดและการปัดเศษก่อนเวลาเปลี่ยนเงินสด จำนวนผู้ใช้ที่พยาบาลรับได้จำกัดปีที่ 3
exits_first <- treated[1] * c(1, 1 - exit_fraction) * annual_incremental_cost
rounded <- c(treated[3], round(treated[3]))
today_value <- 1834 / (1 + discount_rate)^3
capped <- pmin(treated, nurse_places)
shift <- 0.10 * sum(eligible)
users <- sum(eligible * uptake) + c(0, -1, 1) * shift

#%%@note 5. Display the model
# Plot the yearly extra cash; the script does not save a graphics file.
# แสดงเงินสดส่วนเพิ่มรายปี โดยไม่บันทึกไฟล์ภาพ
print(ggplot2::ggplot(years, ggplot2::aes(x = year, y = annual_budget)) +
  ggplot2::geom_col() + ggplot2::labs(x = "Year", y = "Extra cash in the year (THB)"))

#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Year order:",seq_along(uptake),"\n")
cat("Eligible at year start (people):",eligible,"\n")
cat("Treated prevalence (people):",treated,"\n")
cat("Year-end exits (people):",eligible*exit_fraction,"\n")
cat("Remaining stock (people):",eligible*(1-exit_fraction),"\n")
cat("Annual incremental budget (THB):",big(budget),"\n")
cat("Cumulative incremental budget (THB):",big(cumsum(budget)),"\n")
cat("Shortcut, 500 x 2,000 x 3 years (THB):",big(prevalent*annual_incremental_cost*3),"\n")
cat("Cost per person (THB):",big(current_care_cost+c(0,annual_incremental_cost)),"\n")
cat("Current-care scenario (THB):",big(current),"\n")
cat("New scenario (THB):",big(new),"\n")
cat("New minus current (THB):",big(new-current),"\n")
cat("Year 1 users, exits counted after and before:",treated[1]*c(1,1-exit_fraction),"\n")
cat("Year 1, exits counted after and before (THB):",big(exits_first),"\n")
cat("Year 3 people, expected and rounded:",rounded,"\n")
cat("Year 3 budget, expected and rounded (THB):",big(rounded*annual_incremental_cost),"\n")
cat("Yearly rate back to today (%):",100*discount_rate,"\n")
cat("1,834 THB due in year 3, worth today (THB):",big(today_value),"\n")
cat("Year 3 wanted, offered, waiting:",treated[3],nurse_places,treated[3]-nurse_places,"\n")
cat("Year 3 and three years within the places (THB):",
  big(c(capped[3],sum(capped))*annual_incremental_cost),"\n")
cat("Users over three years, shift of 0.10:",users[1],shift,"\n")
cat("Users, every share 0.10 lower and higher:",users[2:3],"\n")
cat("Three years, every share 0.10 lower and higher (THB):",
  big(users[2:3]*annual_incremental_cost),"\n")
Notice the 781.6 users of the plan and the 191.6 that a shift of 0.10 moves, then the range from 1,180,000 to 1,946,400 THB.

Terms to keep distinct

Budget impact analysis (การวิเคราะห์ผลกระทบด้านงบประมาณ)
The extra cash one payer needs in each year if a change is adopted; 1,563,200 THB over three years here.
Eligible stock (กลุ่มผู้มีสิทธิ)
Everyone who qualifies at one moment; 600 at the start of the first year.
Incident inflow (ผู้มีสิทธิรายใหม่)
The new people who qualify each year; 100 a year here.
Uptake (สัดส่วนผู้ใช้บริการ)
The share of everyone who qualifies that year who uses the service; 20%, 40% and 60% here.
Treated prevalence (จำนวนผู้ใช้บริการทั้งหมดในปี)
Everyone using the service in a year, new or continuing; year 3 has 405.6 users.
Recurring and one-off cost (ต้นทุนที่เกิดซ้ำและต้นทุนครั้งเดียว)
A recurring cost repeats in every year on the service; a one-off cost is paid once, when a user starts.
Current and new scenario (สถานการณ์ปัจจุบันและสถานการณ์ใหม่)
Spending without and with the change; the budget impact is the new minus the current.

Connect to the foundation articles

The foundation article on budget impact and fairness shows why a service that is good value can still be unaffordable. The one on early assessment and price setting works out the most a payer could pay before the evidence is complete. Later parts of this series compare several strategies at once.

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

  1. Sullivan Sean D., Mauskopf Josephine A., Augustovski Federico, Jaime Caro J., Lee Karen M., Minchin Mark, Orlewska Ewa, Penna Pete, Rodriguez Barrios Jose-Manuel, Shau Wen-Yi. Budget Impact Analysis-Principles of Good Practice: Report of the ISPOR 2012 Budget Impact Analysis Good Practice II Task Force. Value in Health. 2014;17(1):5-14. doi:10.1016/j.jval.2013.08.2291. https://doi.org/10.1016/j.jval.2013.08.2291
  2. Michael F. Drummond, Mark J. Sculpher, Karl Claxton, Greg L. Stoddart, George W. Torrance. Methods for the Economic Evaluation of Health Care Programmes. Oxford University Press; 2015. ISBN 9780199665884. https://openlibrary.org/isbn/9780199665884
  3. Rattanavipapong Waranya, Worakijthamrongchai Thanaboon, Soboon Budsadee, Luankongsomchit Vilawan, Kongmuangpuk Mananchaya, Isaranuwatchai Wanrudee, Teerawattananon Yot, Nilanont Yongchai. Economic evaluation of endovascular treatment for acute ischaemic stroke in Thailand. BMJ Open. 2022;12(9):e064403. doi:10.1136/bmjopen-2022-064403. https://doi.org/10.1136/bmjopen-2022-064403

The series, in learning order

  1. Health Economic Modelling Series Guide: From Evidence to a Decision
  2. Choosing a Decision-Analytic Model: Let the Clinical Pathway Decide
  3. Auditable Model Inputs: Build a Parameter Table You Can Trace
  4. Finding Model Inputs in the Literature: Match the Estimate to the Question
  5. Rates, Probabilities and Transition Probabilities: Keep Time in the Calculation
  6. Building a Decision Tree Step by Step: Follow Outcomes, Costs and Health
  7. Building a Markov Cohort Model Step by Step: Follow the Cohort Through Time
  8. Budget Impact Analysis in Practice: Count People Before Counting Money (this article)
  9. ICER and Net Monetary Benefit Across Strategies: Find the Relevant Frontier
  10. Probabilistic Sensitivity Analysis: From Parameter Distributions to CEAC
  11. Model Validation and Verification: Test the Question, Equations and Code
  12. Expected Value of Perfect Information: What Could Better Evidence Be Worth?

Key takeaways

  • Count people before money: 500 survivors who qualify become 600, 640 and 676 as 100 join and 10% leave each year.
  • Apply uptake to everyone who qualifies that year, 120, 256 and 405.6 users, and count the recurring 2,000 THB for each one.
  • Inside a year, add the newcomers, count the users and their cost, then remove the leavers; the other order cuts year 1 from 240,000 to 216,000 THB.
  • The budget is the new scenario minus the current one, in cash for the year paid: 1,563,200 THB over three years, not 3,000,000 THB.
  • Check capacity: with 300 places in year 3 the cash is 600,000 THB, 1,352,000 over three years, and 105.6 survivors wait.

Related in the wiki: Cost-Effective Is Not Affordable, and Efficient Is Not Fair: Budget Impact and Equity · Early Health Technology Assessment, Headroom Pricing and Benefit-Package Decisions

0
Message for International and Thai ReadersUnderstanding My Medical Context in ThailandRead more →Message for International and Thai ReadersUnderstanding My Broader Content Beyond MedicineRead more →

Comments

No comments yet. Be the first to share your thoughts.

Sign in to comment