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

Health EconomicsClinical Epidemiology ResearchMethodology and Research Design
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.


Read the steps from the top: each one turns the clinic's rate into a number its model can use. Every number is worked out in the sections below, so the drawing is the article's route in one place.

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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

R code example.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")
Output of the run example.log
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")
Notice the second and fourth lines: the shortcut's 0.0300 sits just above the cycle probability of 0.0296.

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%.

Follow the two lines from the start: blue, labelled exact risk, is the share who started dialysis, and orange is rate × time. Over one 0.25-year cycle, too thin a slice to see at this scale, they almost agree, 0.0296 against 0.0300; at 5 years the curve is at 45.12% and the line at 60.00%. Later the orange line crosses the dashed everyone line, a share no group can reach, so the shortcut is only safe for short cycles.

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%.

Compare the two lines at one year: the blue curve, the exact share who started dialysis, stands at 11.31%, while the orange dashed line, 45.12% spread evenly over 5 years, stands at 9.02%. The curve rises fastest early, while everyone is still at risk, which is why division understates the first year.

Check the five-year numbers in R

R code interval.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")
Output of the run interval.log
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")
Notice that the rate comes back as 0.12, and only the route through it gives the cycle's 0.0296.

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.

The controls start at the clinic: 0.12 a year, cycles of 0.25 years, 5 years and a hazard ratio of 0.70 (HR in the widget). The first card shows the chance per cycle untreated and treated, the article's 0.0296 and 0.0208 as percentages, with the shortcut in brackets; drag the cycle length up to 1 year and the bracket reads 12.00% against the exact 11.31%. On the left, follow the orange dashed rate × time line pulling away from the blue exact curve, with the green treated curve and the red dotted line, the five-year risk divided evenly; on the right, the risk ratio drifts above 0.70 as the years pass. The cycle slider leaves the risk ratio alone, but raising the rate pushes it further from 0.70, so the conversion matters most for long cycles and common events.

Check the treatment numbers in R

R code effect.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")
Output of the run effect.log
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")
Notice the order: the 0.70 multiplies the rate before the conversion, and the five-year risk ratio comes out at 0.760.

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

R code compete.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")
Output of the run compete.log
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")
Notice the first line, the shares 0.6 and 0.4, then the row sum of 1 for the joint split against the separate exits' 0.0494.

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

  1. 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
  2. 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
  3. 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

  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 (this article)
  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
  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

  • 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

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