Rates, Probabilities and Transition Probabilities: Keep Time in the Calculation

On this page
อ่านฉบับภาษาไทย (Thai version)
Abstract
A kidney clinic counted 120 first starts of dialysis over 1,000 person-years among adults with stage 4 kidney disease: a rate of 0.12 per person-year. Its model moves patients every 3 months, a cycle of 0.25 years, and needs the chance of starting dialysis within one cycle. The tempting shortcut multiplies the two, 0.12 × 0.25 = 0.0300. It overshoots, because patients who start dialysis leave the group still at risk. The exact chance is one minus the share still free, 0.0296. That chance of moving from one state to another within one cycle is called a transition probability. Over five years the gap grows: 45.12% against 60.00%. A five-year risk must go back through the rate, never be divided by the years, and a treatment effect multiplies the rate, not the chance. When death competes with dialysis, both exits draw on the same patients, so the chances of staying, starting dialysis and dying add up to one.
One clinic, one rate, a model that moves every 3 months
A kidney clinic in a provincial hospital follows adults with stage 4 chronic kidney disease, the stage before kidney failure. Its records add up to 1,000 person-years of follow-up, where one patient watched for one year counts as one person-year. In that time 120 patients started dialysis for the first time. Divide the events by the time at risk: 120 / 1,000 = 0.12 starts per person-year.
The clinic team is building a model that sorts patients into a few health states: still at stage 4, on dialysis, and dead. The model moves patients between states every 3 months, the clinic's visit interval. That fixed step of time is called a cycle, here 0.25 years long. For each cycle the model needs one number: the chance that a patient at stage 4 starts dialysis before the next visit.
The tempting answer multiplies the rate by the cycle: 0.12 × 0.25 = 0.0300, or 30 of every 1,000 patients. This article corrects that habit. A rate and a chance are different quantities, and time has to stay inside the conversion. The numbers are illustrative, chosen to teach.
A rate is a speed; a chance is a share
A rate counts how fast events happen: events divided by the time people were at risk, 0.12 starts per person-year here. A chance, also called a probability or a risk, is a share of a group: the fraction who have the event by the end of a stated interval. A share can never pass 1 because a group cannot have more first starts than patients. A rate has no such ceiling.
So multiplying a rate by a time does not give a share. Over 10 years the shortcut gives 0.12 × 10 = 1.20, more first starts than the clinic has patients. The rate is still a good speed; it is just not a share.
The rate at one instant, among patients still free of the event, is called the hazard. This article assumes the hazard is constant: the clinic's 0.12 holds in every year of follow-up. That is an assumption to check against the data, not a fact (Fleurence and Hollenbeak).
Why rate × time overshoots
Each patient can start dialysis for the first time only once. A patient who starts leaves the group still at risk, so the same rate acts on a slightly smaller group every week. The shortcut ignores this and keeps adding new starts as if nobody ever left. The exact route follows the share still free instead.
That share does not fall in a straight line. In every equal step of time it loses the same fraction of whoever is left, so each step takes a little less than the one before. A calculator does this with its ex key, the exponential function (Briggs and colleagues). The worked example below puts the clinic's numbers through it.
Worked example: the clinic's chance per 3-month cycle
Take the clinic's rate, 0.12 starts per person-year, and one cycle of 0.25 years.
-
Multiply rate by time
\[ 0.12 \times 0.25 = 0.03 \]
This product is the amount of rate one cycle carries, not yet a chance. The shortcut stops here and calls 0.0300 the chance.
-
Find the share still free
\[ e^{-0.03} = 0.9704 \]
Losing the same fraction of whoever is left, step after tiny step, multiplies out to e raised to minus the rate one cycle carries: the e^x key with −0.03. Of 1,000 patients at stage 4 when the cycle begins, 970.4 are still free of dialysis at the next visit.
-
Take the rest
\[ 1 - 0.9704 = 0.0296 \]
So 29.6 of every 1,000 patients start dialysis in the cycle, against the shortcut's 30.
-
Check by going back
\[ -\ln(0.9704) / 0.25 \approx 0.12 \]
The ln key undoes e^x. The natural logarithm of the share still free, with its sign changed and divided by the cycle length, returns the clinic's rate, 0.12 to two decimals; R gets exactly 0.12 from the unrounded share. That checks the arithmetic.
Result: The model's chance per cycle is 0.0296, a little below the shortcut's 0.0300. Over one short cycle the shortcut is close; the next sections show where it is not.
The name: transition probability
The clinic's 0.0296 is the number its model needs: the chance of moving from one state (stage 4) to another (dialysis) within one cycle of 0.25 years. That chance is called a transition probability. It is converted from a rate, which counts events per unit of time, in a way that keeps time inside the calculation (Siebert and colleagues).
In symbols, write $S$ for the share still free after a time $t$ at a constant rate $r$: $S = e^{-rt}$. A rate over a cycle of length $t$ then gives the transition probability $p = 1 - e^{-rt}$, and a probability goes back to its rate as $r = -\ln(1 - p) / t$.
Check the cycle numbers in R
# Illustrative clinic counts: first starts of dialysis among people with stage 4 kidney disease.
# Writes: nothing to disk; the results print in the console.
starts <- 120
person_years <- 1000
cycle_years <- 0.25
rate <- starts / person_years
still_free <- exp(-rate * cycle_years)
per_thousand <- 1000 * c(rate * cycle_years, 1 - still_free, still_free)
cat("Rate per person-year:", rate, " shortcut over 10 years:", sprintf("%.2f", rate * 10), "\n")
cat("Shortcut for one 3-month cycle:", formatC(rate * cycle_years, format = "f", digits = 4), "\n")
cat("Share still free, exp(-0.03):", signif(still_free, 4), "\n")
cat("Cycle probability, 1 minus that share:", signif(1 - still_free, 3), "\n")
cat("Of 1,000 at stage 4, shortcut, starts, still free:", round(per_thousand, 1), "\n")
cat("Rate recovered from the probability:", -log(still_free) / cycle_years, "\n")
Rate per person-year: 0.12 shortcut over 10 years: 1.20
Shortcut for one 3-month cycle: 0.0300
Share still free, exp(-0.03): 0.9704
Cycle probability, 1 minus that share: 0.0296
Of 1,000 at stage 4, shortcut, starts, still free: 30 29.6 970.4
Rate recovered from the probability: 0.12
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download rates-probabilities-and-transition-probabilities.R
Full tidy script
# Rates, Probabilities and Transition Probabilities: Keep Time in the Calculation
# Follow one kidney clinic from a counted rate to the chances a 3-month model needs.
# ติดตามคลินิกโรคไตหนึ่งแห่ง จากอัตราที่นับได้ไปสู่ความน่าจะเป็นที่แบบจำลองรอบละ 3 เดือนต้องใช้
# 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 inputs and the exits as explicit tables.
library(dplyr) # Calculate the converted probabilities.
library(ggplot2) # Display the exact curve against the shortcut 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
starts <- 120
person_years <- 1000
cycle_years <- 0.25
rate <- 0.12
years <- 5
hazard_ratio <- 0.70
rate_dialysis <- 0.12
rate_death <- 0.08
stopifnot(abs(starts / person_years - rate) < 1e-12)
decimals <- function(value, digits) formatC(value, format = "f", digits = digits)
percent <- function(value, digits) paste0(formatC(100 * value, format = "f", digits = digits), "%")
#%%@note 2. Convert the rate over one cycle and over five years
# Survival falls as exp(-rate x time); the probability is one minus that survival.
# สัดส่วนที่ยังไม่เกิดเหตุการณ์ลดลงแบบ exp(-อัตรา x เวลา) ความน่าจะเป็นคือหนึ่งลบสัดส่วนนั้น
still_free <- exp(-rate * cycle_years)
per_thousand <- 1000 * c(rate * cycle_years, 1 - still_free, still_free)
risk_five <- 1 - exp(-rate * years)
rate_back <- -log(1 - risk_five) / years
stopifnot(abs(rate_back - rate) < 1e-12)
yearly <- c(risk_five / years, 1 - exp(-rate_back))
per_cycle <- c(risk_five / 20, 1 - exp(-rate_back * cycle_years))
#%%@note 3. Apply the treatment effect to the rate, then convert
# The trial's ratio multiplies the rate, never the probability; the foundation uses survival 0.60.
# อัตราส่วนจากการทดลองคูณกับอัตรา ไม่ได้คูณกับความน่าจะเป็น ตัวอย่างพื้นฐานใช้การรอดชีพ 0.60
risk_hr <- 1 - 0.60^c(1, hazard_ratio)
treated <- rate * hazard_ratio
five_year <- 1 - exp(-c(rate, treated) * years)
#%%@note 4. Share one survival between two exits
# Add the exit rates, find who stays, then split the leavers by each rate's share.
# รวมอัตราของทางออกทั้งสอง หาสัดส่วนที่อยู่ต่อ แล้วแบ่งผู้ที่ออกตามสัดส่วนของแต่ละอัตรา
exits <- tibble::tibble(exit = c("dialysis", "death"), rate = c(rate_dialysis, rate_death)) |>
dplyr::mutate(separate = 1 - exp(-rate * cycle_years))
rate_total <- sum(exits$rate)
stay <- exp(-rate_total * cycle_years)
row_joint <- c(stay, (1 - stay) * exits$rate / rate_total)
separate <- exits$separate
stopifnot(abs(sum(row_joint) - 1) < 1e-12)
#%%@note 5. Display the model
# Plot the exact risk against rate x time over ten years; no graphics file is saved.
# แสดงความเสี่ยงที่ถูกต้องเทียบกับอัตราคูณเวลาในสิบปี โดยไม่บันทึกไฟล์ภาพ
curve <- tibble::tibble(time = seq(0, 10, by = 0.25)) |>
dplyr::mutate(exact = 1 - exp(-rate * time), shortcut = rate * time)
print(ggplot2::ggplot(curve, ggplot2::aes(x = time)) +
ggplot2::geom_line(ggplot2::aes(y = exact)) +
ggplot2::geom_line(ggplot2::aes(y = shortcut), linetype = "dashed") +
ggplot2::labs(x = "Years", y = "Share who started dialysis"))
#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Rate per person-year:", rate, " shortcut over 10 years:", sprintf("%.2f", rate * 10), "\n")
cat("Shortcut for one 3-month cycle:", formatC(rate * cycle_years, format = "f", digits = 4), "\n")
cat("Share still free, exp(-0.03):", signif(still_free, 4), "\n")
cat("Cycle probability, 1 minus that share:", signif(1 - still_free, 3), "\n")
cat("Of 1,000 at stage 4, shortcut, starts, still free:", round(per_thousand, 1), "\n")
cat("Rate recovered from the probability:", -log(still_free) / cycle_years, "\n")
cat("Five-year risk, exact and rate x time:", percent(c(risk_five, rate * years), 2), "\n")
cat("Five-year risk and share still free:", signif(c(risk_five, 1 - risk_five), 4), "\n")
cat("Rate recovered from the share:", rate_back, "\n")
cat("One year, divided by 5 and through the rate:", percent(yearly, 2), "\n")
cat("One cycle, divided by 20 and through the rate:", signif(per_cycle, 3), "\n")
cat("Survival 0.60, risks and ratio:", percent(risk_hr, 1), round(risk_hr[2] / risk_hr[1], 2), "\n")
cat("Treated rate, cycle probability:", treated, signif(1 - exp(-treated * cycle_years), 3), "\n")
cat("Five-year risks, control and treated:", percent(five_year, 2), "\n")
cat("Five-year risk ratio:", sprintf("%.3f", five_year[2] / five_year[1]), "\n")
cat("Total rate, each exit's share:", rate_total, c(rate_dialysis, rate_death) / rate_total, "\n")
cat("Stay, dialysis, death:", decimals(row_joint, 4), " row sum:", sum(row_joint), "\n")
cat("Leave in one cycle, joint and separate:", decimals(c(1 - stay, sum(separate)), 4), "\n")
cat("Separate dialysis and death:", decimals(separate, 4), "\n")
Change the interval through the rate, not by division
Over one cycle the shortcut is close: 30 against 29.6 per 1,000 patients. Stretch the time and the gap opens. Over 5 years the exact share who start dialysis is $1 - e^{-(0.12 \times 5)}$ = 45.12%, while rate × time claims 60.00%.
A reported five-year risk, brought back to one cycle
Now run the problem the other way. Suppose the clinic had no counts of its own, and a national registry reported only that 45.12% of patients at stage 4 started dialysis within 5 years of follow-up. We chose that figure to match the clinic's rate, so the two routes can be compared. Dividing by the 20 cycles in 5 years gives 0.0226 per cycle.
The correct route goes back through the rate. First turn the five-year risk into a rate. The share still free after 5 years is 1 − 0.4512 = 0.5488; take its natural logarithm, change the sign and divide by 5: $-\ln(0.5488) / 5$ = 0.12 per person-year. Then convert that rate over one cycle as before, which gives 0.0296.
Division understated the chance by almost a quarter. Why? The rate acts only on patients still free of dialysis, and that group keeps shrinking, so the early years carry more of the starts than the late ones. Spreading 45.12% evenly hands every year the same share and gives the first year too little: 9.02% against the exact 11.31%.
Check the five-year numbers in R
# The same illustrative rate read over five years, then brought back to one year and one cycle.
# A long-interval risk is converted back through the rate, never divided by the years.
rate <- 0.12
years <- 5
percent <- function(value, digits) paste0(formatC(100 * value, format = "f", digits = digits), "%")
risk_five <- 1 - exp(-rate * years)
rate_back <- -log(1 - risk_five) / years
cat("Five-year risk, exact and rate x time:", percent(c(risk_five, rate * years), 2), "\n")
cat("Five-year risk and share still free:", signif(c(risk_five, 1 - risk_five), 4), "\n")
cat("Rate recovered from the share:", rate_back, "\n")
yearly <- c(risk_five / years, 1 - exp(-rate_back))
cat("One year, divided by 5 and through the rate:", percent(yearly, 2), "\n")
per_cycle <- c(risk_five / 20, 1 - exp(-rate_back * 0.25))
cat("One cycle, divided by 20 and through the rate:", signif(per_cycle, 3), "\n")
Five-year risk, exact and rate x time: 45.12% 60.00%
Five-year risk and share still free: 0.4512 0.5488
Rate recovered from the share: 0.12
One year, divided by 5 and through the rate: 9.02% 11.31%
One cycle, divided by 20 and through the rate: 0.0226 0.0296
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download rates-probabilities-and-transition-probabilities.R
Full tidy script
# Rates, Probabilities and Transition Probabilities: Keep Time in the Calculation
# Follow one kidney clinic from a counted rate to the chances a 3-month model needs.
# ติดตามคลินิกโรคไตหนึ่งแห่ง จากอัตราที่นับได้ไปสู่ความน่าจะเป็นที่แบบจำลองรอบละ 3 เดือนต้องใช้
# 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 inputs and the exits as explicit tables.
library(dplyr) # Calculate the converted probabilities.
library(ggplot2) # Display the exact curve against the shortcut 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
starts <- 120
person_years <- 1000
cycle_years <- 0.25
rate <- 0.12
years <- 5
hazard_ratio <- 0.70
rate_dialysis <- 0.12
rate_death <- 0.08
stopifnot(abs(starts / person_years - rate) < 1e-12)
decimals <- function(value, digits) formatC(value, format = "f", digits = digits)
percent <- function(value, digits) paste0(formatC(100 * value, format = "f", digits = digits), "%")
#%%@note 2. Convert the rate over one cycle and over five years
# Survival falls as exp(-rate x time); the probability is one minus that survival.
# สัดส่วนที่ยังไม่เกิดเหตุการณ์ลดลงแบบ exp(-อัตรา x เวลา) ความน่าจะเป็นคือหนึ่งลบสัดส่วนนั้น
still_free <- exp(-rate * cycle_years)
per_thousand <- 1000 * c(rate * cycle_years, 1 - still_free, still_free)
risk_five <- 1 - exp(-rate * years)
rate_back <- -log(1 - risk_five) / years
stopifnot(abs(rate_back - rate) < 1e-12)
yearly <- c(risk_five / years, 1 - exp(-rate_back))
per_cycle <- c(risk_five / 20, 1 - exp(-rate_back * cycle_years))
#%%@note 3. Apply the treatment effect to the rate, then convert
# The trial's ratio multiplies the rate, never the probability; the foundation uses survival 0.60.
# อัตราส่วนจากการทดลองคูณกับอัตรา ไม่ได้คูณกับความน่าจะเป็น ตัวอย่างพื้นฐานใช้การรอดชีพ 0.60
risk_hr <- 1 - 0.60^c(1, hazard_ratio)
treated <- rate * hazard_ratio
five_year <- 1 - exp(-c(rate, treated) * years)
#%%@note 4. Share one survival between two exits
# Add the exit rates, find who stays, then split the leavers by each rate's share.
# รวมอัตราของทางออกทั้งสอง หาสัดส่วนที่อยู่ต่อ แล้วแบ่งผู้ที่ออกตามสัดส่วนของแต่ละอัตรา
exits <- tibble::tibble(exit = c("dialysis", "death"), rate = c(rate_dialysis, rate_death)) |>
dplyr::mutate(separate = 1 - exp(-rate * cycle_years))
rate_total <- sum(exits$rate)
stay <- exp(-rate_total * cycle_years)
row_joint <- c(stay, (1 - stay) * exits$rate / rate_total)
separate <- exits$separate
stopifnot(abs(sum(row_joint) - 1) < 1e-12)
#%%@note 5. Display the model
# Plot the exact risk against rate x time over ten years; no graphics file is saved.
# แสดงความเสี่ยงที่ถูกต้องเทียบกับอัตราคูณเวลาในสิบปี โดยไม่บันทึกไฟล์ภาพ
curve <- tibble::tibble(time = seq(0, 10, by = 0.25)) |>
dplyr::mutate(exact = 1 - exp(-rate * time), shortcut = rate * time)
print(ggplot2::ggplot(curve, ggplot2::aes(x = time)) +
ggplot2::geom_line(ggplot2::aes(y = exact)) +
ggplot2::geom_line(ggplot2::aes(y = shortcut), linetype = "dashed") +
ggplot2::labs(x = "Years", y = "Share who started dialysis"))
#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Rate per person-year:", rate, " shortcut over 10 years:", sprintf("%.2f", rate * 10), "\n")
cat("Shortcut for one 3-month cycle:", formatC(rate * cycle_years, format = "f", digits = 4), "\n")
cat("Share still free, exp(-0.03):", signif(still_free, 4), "\n")
cat("Cycle probability, 1 minus that share:", signif(1 - still_free, 3), "\n")
cat("Of 1,000 at stage 4, shortcut, starts, still free:", round(per_thousand, 1), "\n")
cat("Rate recovered from the probability:", -log(still_free) / cycle_years, "\n")
cat("Five-year risk, exact and rate x time:", percent(c(risk_five, rate * years), 2), "\n")
cat("Five-year risk and share still free:", signif(c(risk_five, 1 - risk_five), 4), "\n")
cat("Rate recovered from the share:", rate_back, "\n")
cat("One year, divided by 5 and through the rate:", percent(yearly, 2), "\n")
cat("One cycle, divided by 20 and through the rate:", signif(per_cycle, 3), "\n")
cat("Survival 0.60, risks and ratio:", percent(risk_hr, 1), round(risk_hr[2] / risk_hr[1], 2), "\n")
cat("Treated rate, cycle probability:", treated, signif(1 - exp(-treated * cycle_years), 3), "\n")
cat("Five-year risks, control and treated:", percent(five_year, 2), "\n")
cat("Five-year risk ratio:", sprintf("%.3f", five_year[2] / five_year[1]), "\n")
cat("Total rate, each exit's share:", rate_total, c(rate_dialysis, rate_death) / rate_total, "\n")
cat("Stay, dialysis, death:", decimals(row_joint, 4), " row sum:", sum(row_joint), "\n")
cat("Leave in one cycle, joint and separate:", decimals(c(1 - stay, sum(separate)), 4), "\n")
cat("Separate dialysis and death:", decimals(separate, 4), "\n")
A treatment slows the rate, so apply it to the rate
A hazard ratio compares how fast events happen among people still event-free, moment by moment; it is not a ratio of risks over a period. With control survival of 0.60, six in ten control patients alive after five years, and a hazard ratio of 0.70, the five-year risks of death are 40.0% and 30.1%, a risk ratio of 0.75; the full lesson is in The Hazard Ratio Is Not a Risk Ratio: Proportional Hazards, Restricted Mean Survival Time and Model Inputs. That example has a different group and outcome, so its 0.75 is not the clinic's number; the clinic's own figures follow.
Trials of time to an event usually report their effect this way. Suppose a trial of a kidney-protecting drug reports a hazard ratio of 0.70. The ratio scales the speed, so it multiplies the rate: 0.12 × 0.70 = 0.084 starts per person-year for treated patients. Only then convert: over one cycle the treated chance is $1 - e^{-(0.084 \times 0.25)}$ = 0.0208.
Do not multiply a chance or a long risk by 0.70. With the clinic's rate of 0.12, the exact five-year risks are 45.12% untreated and 34.30% treated, a risk ratio of 0.760, not 0.70. The two ratios drift apart because the untreated group runs out of patients still at risk faster. Over one short cycle they nearly agree, which is why treating one as the other is easy to miss.
Check the treatment numbers in R
# Apply an illustrative hazard ratio to the rate first, then convert; synthetic numbers.
# Writes: nothing to disk; the results print in the console.
rate <- 0.12
hazard_ratio <- 0.70
cycle_years <- 0.25
years <- 5
percent <- function(value, digits) paste0(formatC(100 * value, format = "f", digits = digits), "%")
risk_hr <- 1 - 0.60^c(1, hazard_ratio)
treated <- rate * hazard_ratio
five_year <- 1 - exp(-c(rate, treated) * years)
cat("Survival 0.60, risks and ratio:", percent(risk_hr, 1), round(risk_hr[2] / risk_hr[1], 2), "\n")
cat("Treated rate, cycle probability:", treated, signif(1 - exp(-treated * cycle_years), 3), "\n")
cat("Five-year risks, control and treated:", percent(five_year, 2), "\n")
cat("Five-year risk ratio:", sprintf("%.3f", five_year[2] / five_year[1]), "\n")
Survival 0.60, risks and ratio: 40.0% 30.1% 0.75
Treated rate, cycle probability: 0.084 0.0208
Five-year risks, control and treated: 45.12% 34.30%
Five-year risk ratio: 0.760
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download rates-probabilities-and-transition-probabilities.R
Full tidy script
# Rates, Probabilities and Transition Probabilities: Keep Time in the Calculation
# Follow one kidney clinic from a counted rate to the chances a 3-month model needs.
# ติดตามคลินิกโรคไตหนึ่งแห่ง จากอัตราที่นับได้ไปสู่ความน่าจะเป็นที่แบบจำลองรอบละ 3 เดือนต้องใช้
# 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 inputs and the exits as explicit tables.
library(dplyr) # Calculate the converted probabilities.
library(ggplot2) # Display the exact curve against the shortcut 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
starts <- 120
person_years <- 1000
cycle_years <- 0.25
rate <- 0.12
years <- 5
hazard_ratio <- 0.70
rate_dialysis <- 0.12
rate_death <- 0.08
stopifnot(abs(starts / person_years - rate) < 1e-12)
decimals <- function(value, digits) formatC(value, format = "f", digits = digits)
percent <- function(value, digits) paste0(formatC(100 * value, format = "f", digits = digits), "%")
#%%@note 2. Convert the rate over one cycle and over five years
# Survival falls as exp(-rate x time); the probability is one minus that survival.
# สัดส่วนที่ยังไม่เกิดเหตุการณ์ลดลงแบบ exp(-อัตรา x เวลา) ความน่าจะเป็นคือหนึ่งลบสัดส่วนนั้น
still_free <- exp(-rate * cycle_years)
per_thousand <- 1000 * c(rate * cycle_years, 1 - still_free, still_free)
risk_five <- 1 - exp(-rate * years)
rate_back <- -log(1 - risk_five) / years
stopifnot(abs(rate_back - rate) < 1e-12)
yearly <- c(risk_five / years, 1 - exp(-rate_back))
per_cycle <- c(risk_five / 20, 1 - exp(-rate_back * cycle_years))
#%%@note 3. Apply the treatment effect to the rate, then convert
# The trial's ratio multiplies the rate, never the probability; the foundation uses survival 0.60.
# อัตราส่วนจากการทดลองคูณกับอัตรา ไม่ได้คูณกับความน่าจะเป็น ตัวอย่างพื้นฐานใช้การรอดชีพ 0.60
risk_hr <- 1 - 0.60^c(1, hazard_ratio)
treated <- rate * hazard_ratio
five_year <- 1 - exp(-c(rate, treated) * years)
#%%@note 4. Share one survival between two exits
# Add the exit rates, find who stays, then split the leavers by each rate's share.
# รวมอัตราของทางออกทั้งสอง หาสัดส่วนที่อยู่ต่อ แล้วแบ่งผู้ที่ออกตามสัดส่วนของแต่ละอัตรา
exits <- tibble::tibble(exit = c("dialysis", "death"), rate = c(rate_dialysis, rate_death)) |>
dplyr::mutate(separate = 1 - exp(-rate * cycle_years))
rate_total <- sum(exits$rate)
stay <- exp(-rate_total * cycle_years)
row_joint <- c(stay, (1 - stay) * exits$rate / rate_total)
separate <- exits$separate
stopifnot(abs(sum(row_joint) - 1) < 1e-12)
#%%@note 5. Display the model
# Plot the exact risk against rate x time over ten years; no graphics file is saved.
# แสดงความเสี่ยงที่ถูกต้องเทียบกับอัตราคูณเวลาในสิบปี โดยไม่บันทึกไฟล์ภาพ
curve <- tibble::tibble(time = seq(0, 10, by = 0.25)) |>
dplyr::mutate(exact = 1 - exp(-rate * time), shortcut = rate * time)
print(ggplot2::ggplot(curve, ggplot2::aes(x = time)) +
ggplot2::geom_line(ggplot2::aes(y = exact)) +
ggplot2::geom_line(ggplot2::aes(y = shortcut), linetype = "dashed") +
ggplot2::labs(x = "Years", y = "Share who started dialysis"))
#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Rate per person-year:", rate, " shortcut over 10 years:", sprintf("%.2f", rate * 10), "\n")
cat("Shortcut for one 3-month cycle:", formatC(rate * cycle_years, format = "f", digits = 4), "\n")
cat("Share still free, exp(-0.03):", signif(still_free, 4), "\n")
cat("Cycle probability, 1 minus that share:", signif(1 - still_free, 3), "\n")
cat("Of 1,000 at stage 4, shortcut, starts, still free:", round(per_thousand, 1), "\n")
cat("Rate recovered from the probability:", -log(still_free) / cycle_years, "\n")
cat("Five-year risk, exact and rate x time:", percent(c(risk_five, rate * years), 2), "\n")
cat("Five-year risk and share still free:", signif(c(risk_five, 1 - risk_five), 4), "\n")
cat("Rate recovered from the share:", rate_back, "\n")
cat("One year, divided by 5 and through the rate:", percent(yearly, 2), "\n")
cat("One cycle, divided by 20 and through the rate:", signif(per_cycle, 3), "\n")
cat("Survival 0.60, risks and ratio:", percent(risk_hr, 1), round(risk_hr[2] / risk_hr[1], 2), "\n")
cat("Treated rate, cycle probability:", treated, signif(1 - exp(-treated * cycle_years), 3), "\n")
cat("Five-year risks, control and treated:", percent(five_year, 2), "\n")
cat("Five-year risk ratio:", sprintf("%.3f", five_year[2] / five_year[1]), "\n")
cat("Total rate, each exit's share:", rate_total, c(rate_dialysis, rate_death) / rate_total, "\n")
cat("Stay, dialysis, death:", decimals(row_joint, 4), " row sum:", sum(row_joint), "\n")
cat("Leave in one cycle, joint and separate:", decimals(c(1 - stay, sum(separate)), 4), "\n")
cat("Separate dialysis and death:", decimals(separate, 4), "\n")
Two exits share the same patients
Some patients at stage 4 die before they ever need dialysis. Death is a competing exit: once a patient leaves through it, dialysis can no longer come first. Picture a waiting room with two doors, dialysis and death. Both doors take patients from the same group over the same 3 months.
A model must say where every stage 4 patient is at the next visit: still at stage 4, on dialysis, or dead. Those three chances sit together in one row of the model's table. Because every patient ends up in exactly one place, the row must add up to 1 for each starting state.
Say the clinic's death rate before dialysis is 0.08 per person-year. Converting each rate on its own gives 0.0296 for dialysis and 0.0198 for death, 0.0494 leaving in all. Each of those calculations treats its door as the only one, so it still counts patients who have already left through the other door.
The joint route adds the two rates first, 0.12 + 0.08 = 0.20 per person-year, and finds the share who stay, neither on dialysis nor dead: $e^{-(0.20 \times 0.25)}$ = 0.9512. The 0.0488 who leave are split by each rate's share of the total, 0.12 / 0.20 = 0.60 for dialysis and 0.40 for death: 0.0488 × 0.60 = 0.0293 and 0.0488 × 0.40 = 0.0195. The row adds up exactly, 0.9512 + 0.0293 + 0.0195 = 1, as every row must.
The separate route's 0.0494 leavers are more than the 0.0488 who really leave.
So once death is in the model, its dialysis chance is 0.0293, not 0.0296; the 0.0296 holds only when death is ignored.
This split holds when both rates are constant and nobody moves again within the cycle. If patients can leave and come back, or pass through another state within one cycle, use a model that allows those moves. Then check that every row still adds up to 1 (Siebert and colleagues; Briggs and colleagues).
Check the two exits in R
# Two exits share the same people: dialysis first or death first; synthetic rates per year.
# Writes: nothing to disk; the results print in the console.
rate_dialysis <- 0.12
rate_death <- 0.08
cycle_years <- 0.25
decimals <- function(value, digits) formatC(value, format = "f", digits = digits)
rate_total <- rate_dialysis + rate_death
stay <- exp(-rate_total * cycle_years)
row_joint <- c(stay, (1 - stay) * c(rate_dialysis, rate_death) / rate_total)
separate <- 1 - exp(-c(rate_dialysis, rate_death) * cycle_years)
cat("Total rate, each exit's share:", rate_total, c(rate_dialysis, rate_death) / rate_total, "\n")
cat("Stay, dialysis, death:", decimals(row_joint, 4), " row sum:", sum(row_joint), "\n")
cat("Leave in one cycle, joint and separate:", decimals(c(1 - stay, sum(separate)), 4), "\n")
cat("Separate dialysis and death:", decimals(separate, 4), "\n")
Total rate, each exit's share: 0.2 0.6 0.4
Stay, dialysis, death: 0.9512 0.0293 0.0195 row sum: 1
Leave in one cycle, joint and separate: 0.0488 0.0494
Separate dialysis and death: 0.0296 0.0198
Requires R ≥ 4.5.2 · tibble 3.3.1 · dplyr 1.2.1 · ggplot2 4.0.3 · works in R 4.6.1 · Download rates-probabilities-and-transition-probabilities.R
Full tidy script
# Rates, Probabilities and Transition Probabilities: Keep Time in the Calculation
# Follow one kidney clinic from a counted rate to the chances a 3-month model needs.
# ติดตามคลินิกโรคไตหนึ่งแห่ง จากอัตราที่นับได้ไปสู่ความน่าจะเป็นที่แบบจำลองรอบละ 3 เดือนต้องใช้
# 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 inputs and the exits as explicit tables.
library(dplyr) # Calculate the converted probabilities.
library(ggplot2) # Display the exact curve against the shortcut 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.
# ค่าตั้งต้นทั้งหมดเป็นข้อมูลสมมติ เขียนไว้ครั้งเดียวตามลำดับที่บทความใช้
starts <- 120
person_years <- 1000
cycle_years <- 0.25
rate <- 0.12
years <- 5
hazard_ratio <- 0.70
rate_dialysis <- 0.12
rate_death <- 0.08
stopifnot(abs(starts / person_years - rate) < 1e-12)
decimals <- function(value, digits) formatC(value, format = "f", digits = digits)
percent <- function(value, digits) paste0(formatC(100 * value, format = "f", digits = digits), "%")
#%%@note 2. Convert the rate over one cycle and over five years
# Survival falls as exp(-rate x time); the probability is one minus that survival.
# สัดส่วนที่ยังไม่เกิดเหตุการณ์ลดลงแบบ exp(-อัตรา x เวลา) ความน่าจะเป็นคือหนึ่งลบสัดส่วนนั้น
still_free <- exp(-rate * cycle_years)
per_thousand <- 1000 * c(rate * cycle_years, 1 - still_free, still_free)
risk_five <- 1 - exp(-rate * years)
rate_back <- -log(1 - risk_five) / years
stopifnot(abs(rate_back - rate) < 1e-12)
yearly <- c(risk_five / years, 1 - exp(-rate_back))
per_cycle <- c(risk_five / 20, 1 - exp(-rate_back * cycle_years))
#%%@note 3. Apply the treatment effect to the rate, then convert
# The trial's ratio multiplies the rate, never the probability; the foundation uses survival 0.60.
# อัตราส่วนจากการทดลองคูณกับอัตรา ไม่ได้คูณกับความน่าจะเป็น ตัวอย่างพื้นฐานใช้การรอดชีพ 0.60
risk_hr <- 1 - 0.60^c(1, hazard_ratio)
treated <- rate * hazard_ratio
five_year <- 1 - exp(-c(rate, treated) * years)
#%%@note 4. Share one survival between two exits
# Add the exit rates, find who stays, then split the leavers by each rate's share.
# รวมอัตราของทางออกทั้งสอง หาสัดส่วนที่อยู่ต่อ แล้วแบ่งผู้ที่ออกตามสัดส่วนของแต่ละอัตรา
exits <- tibble::tibble(exit = c("dialysis", "death"), rate = c(rate_dialysis, rate_death)) |>
dplyr::mutate(separate = 1 - exp(-rate * cycle_years))
rate_total <- sum(exits$rate)
stay <- exp(-rate_total * cycle_years)
row_joint <- c(stay, (1 - stay) * exits$rate / rate_total)
separate <- exits$separate
stopifnot(abs(sum(row_joint) - 1) < 1e-12)
#%%@note 5. Display the model
# Plot the exact risk against rate x time over ten years; no graphics file is saved.
# แสดงความเสี่ยงที่ถูกต้องเทียบกับอัตราคูณเวลาในสิบปี โดยไม่บันทึกไฟล์ภาพ
curve <- tibble::tibble(time = seq(0, 10, by = 0.25)) |>
dplyr::mutate(exact = 1 - exp(-rate * time), shortcut = rate * time)
print(ggplot2::ggplot(curve, ggplot2::aes(x = time)) +
ggplot2::geom_line(ggplot2::aes(y = exact)) +
ggplot2::geom_line(ggplot2::aes(y = shortcut), linetype = "dashed") +
ggplot2::labs(x = "Years", y = "Share who started dialysis"))
#%%@note 6. Numbers the article quotes
# Print exactly the short boxes in their reading order.
# พิมพ์ผลลัพธ์ตรงกับกล่องตัวอย่างตามลำดับในบทความ
cat("Rate per person-year:", rate, " shortcut over 10 years:", sprintf("%.2f", rate * 10), "\n")
cat("Shortcut for one 3-month cycle:", formatC(rate * cycle_years, format = "f", digits = 4), "\n")
cat("Share still free, exp(-0.03):", signif(still_free, 4), "\n")
cat("Cycle probability, 1 minus that share:", signif(1 - still_free, 3), "\n")
cat("Of 1,000 at stage 4, shortcut, starts, still free:", round(per_thousand, 1), "\n")
cat("Rate recovered from the probability:", -log(still_free) / cycle_years, "\n")
cat("Five-year risk, exact and rate x time:", percent(c(risk_five, rate * years), 2), "\n")
cat("Five-year risk and share still free:", signif(c(risk_five, 1 - risk_five), 4), "\n")
cat("Rate recovered from the share:", rate_back, "\n")
cat("One year, divided by 5 and through the rate:", percent(yearly, 2), "\n")
cat("One cycle, divided by 20 and through the rate:", signif(per_cycle, 3), "\n")
cat("Survival 0.60, risks and ratio:", percent(risk_hr, 1), round(risk_hr[2] / risk_hr[1], 2), "\n")
cat("Treated rate, cycle probability:", treated, signif(1 - exp(-treated * cycle_years), 3), "\n")
cat("Five-year risks, control and treated:", percent(five_year, 2), "\n")
cat("Five-year risk ratio:", sprintf("%.3f", five_year[2] / five_year[1]), "\n")
cat("Total rate, each exit's share:", rate_total, c(rate_dialysis, rate_death) / rate_total, "\n")
cat("Stay, dialysis, death:", decimals(row_joint, 4), " row sum:", sum(row_joint), "\n")
cat("Leave in one cycle, joint and separate:", decimals(c(1 - stay, sum(separate)), 4), "\n")
cat("Separate dialysis and death:", decimals(separate, 4), "\n")
Check the process, not only the formula
Before a converted number enters the model, write down its starting state, its destination, the cycle length and the other exits. Reverse the conversion and check that the rate comes back, as the clinic's 0.12 did. Then check that every row of chances adds up to 1 before the model runs.
These checks catch slips in the arithmetic, not wrong assumptions. A constant rate is a claim about the disease: if starts of dialysis speed up in later years, one rate of 0.12 is wrong however carefully it is converted. The same holds for a hazard ratio that does not stay at 0.70 over the whole follow-up. Shorter cycles shrink the shortcut's error, but they cannot repair a wrong event definition or the wrong population.
Terms to keep distinct
- Rate (อัตรา)
- Events per unit of time at risk, such as 0.12 starts of dialysis per person-year; it has no ceiling.
- Hazard (ฮาซาร์ด)
- The rate at one instant among people still free of the event.
- Probability (risk) (ความน่าจะเป็น (ความเสี่ยง))
- The share of a group who have the event by the end of a stated interval, never above 1 for any group.
- Cycle (รอบ)
- The fixed step of time at which the model moves patients between states; 0.25 years here.
- Transition probability (ความน่าจะเป็นของการเปลี่ยนสถานะ)
- The chance of moving from one state to another within one cycle, converted from a rate: 0.0296 here.
- Hazard ratio (อัตราส่วนฮาซาร์ด)
- Treated rate divided by untreated rate; multiply the rate by it, then convert.
- Competing exit (ทางออกที่แข่งขันกัน)
- Another event, such as death, that removes a patient from the starting state first.
Connect to the foundation articles
The foundation article on hazard ratios and proportional hazards shows how a ratio of 0.70 becomes a risk ratio of 0.75 over five years. The one on risk ratios and risk differences explains the risk side. Later parts of this series put these chances into a cohort model, one cycle at a time.
Try it in the app
Open our cohort model 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
- Fleurence Rachael L, Hollenbeak Christopher S. Rates and Probabilities in Economic Modelling. PharmacoEconomics. 2007;25(1):3-6. doi:10.2165/00019053-200725010-00002. https://doi.org/10.2165/00019053-200725010-00002
- Siebert Uwe, Alagoz Oguzhan, Bayoumi Ahmed M., Jahn Beate, Owens Douglas K., Cohen David J., Kuntz Karen M.. State-Transition Modeling: A Report of the ISPOR-SMDM Modeling Good Research Practices Task Force-3. Value in Health. 2012;15(6):812-820. doi:10.1016/j.jval.2012.06.014. https://doi.org/10.1016/j.jval.2012.06.014
- 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
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
- Finding Model Inputs in the Literature: Match the Estimate to the Question
- Rates, Probabilities and Transition Probabilities: Keep Time in the Calculation (this article)
- 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
- A rate is a speed, 0.12 starts per person-year; a probability is a share, which never passes 1 for any group.
- Keep time inside the conversion: $1 - e^{-(0.12 \times 0.25)}$ = 0.0296 per 3-month cycle when dialysis is the only exit, not the shortcut 0.0300.
- Move a long risk to a new interval through the rate: 45.12% over 5 years gives 0.0296 per cycle, not 0.0226 by division.
- Apply a hazard ratio to the rate, then convert: 0.12 × 0.70 = 0.084 gives 0.0208 per cycle, and over 5 years the risk ratio is 0.760, not 0.70.
- Let competing exits draw on the same patients so the row adds up to 1: 0.9512 stay, 0.0293 start dialysis and 0.0195 die.
Related in the wiki: The Hazard Ratio Is Not a Risk Ratio: Proportional Hazards, Restricted Mean Survival Time and Model Inputs · Risk Ratio, Odds Ratio, Risk Difference and NNT: One 2×2 Table, Four Answers