Before we begin
Odds and odds ratios; incidence rates and person-time; Kaplan-Meier curves.
We will run a few chunks at a time as we reach each topic in class.
Open BTE3207_Advanced_Biostatistics.Rproj, then open this
week’s .Rmd file. Run the chunks from the top, or click
Knit to make the whole document.
Before begin..
This week, we will calculate odds and odds ratios, then add follow-up time to our data. After that, let’s draw a Kaplan-Meier curve!
The examples follow the Week 3 lecture. Run one section at a time when we reach the same example in class.
Going back to the case-control study
We recruited 100 lung cancer cases and 100 controls. Here is the table from the lecture.
dataset_lung <- data.frame(
group = c("Case", "Control"),
smoker = c(90, 60),
non_smoker = c(10, 40)
)
dataset_lung
We chose how many cases and controls to recruit. So the disease proportions in this table do not estimate the population risks. We cannot calculate the population RR from this table alone.
What is odds?
Let’s go back to the ART example. The outcome here is response to therapy.
dataset_art <- data.frame(
CD4 = c("<250", ">=250"),
response = c(127, 79),
total = c(503, 497)
)
dataset_art$no_response <- dataset_art$total - dataset_art$response
dataset_art$p_hat <- dataset_art$response / dataset_art$total
dataset_art
Odds are the probability of having the event divided by the
probability of not having the event. So, we use
p / (1 - p).
dataset_art$odds <- dataset_art$p_hat / (1 - dataset_art$p_hat)
dataset_art
# We can also calculate odds directly from the counts.
dataset_art$response / dataset_art$no_response
## [1] 0.3377660 0.1889952
See? Both calculations give the same results. An odds of 1 means that the event and no event are equally likely. Odds can be greater than 1, so they are not probabilities.
Odds ratio
To compare two groups, divide the odds of one group by the odds of the other group. Let’s keep the direction as CD4 <250 compared to CD4 >=250.
risk_difference <- dataset_art$p_hat[1] - dataset_art$p_hat[2]
relative_risk <- dataset_art$p_hat[1] / dataset_art$p_hat[2]
odds_ratio <- dataset_art$odds[1] / dataset_art$odds[2]
data.frame(RD = risk_difference,
RR = relative_risk,
OR = odds_ratio)
The odds of responding are about 1.79 times as high in the CD4 <250 group. The relative risk is about 1.59. They point in the same direction, but their values are different.
We use the original counts for each calculation and round the final answer. Rounding the proportions first can change the answer quite a bit.
Going back to the case-control study
Here is the case-control table we made at the beginning.
However, we can compare the odds of smoking among cases and controls.
odds_case <- 90 / 10
odds_control <- 60 / 40
odds_ratio_smoking <- odds_case / odds_control
odds_ratio_smoking
## [1] 6
# Cross-products give the same OR.
(90 * 40) / (10 * 60)
## [1] 6
The odds of smoking among cases are 6 times those among controls. With appropriate case-control sampling, this estimates the disease odds ratio for smokers compared to non-smokers. This does not mean 6 times the risk.
What happens if we recruit 1,000 controls with the same smoking proportion?
odds_control_more <- 600 / 400
odds_case / odds_control_more
## [1] 6
The OR stays the same! This calculation does not remove confounding or selection bias; the way we recruit and measure the subjects still matters.
Question
The maternal/infant HIV transmission example has 13 transmissions among 180 infants in the AZT group and 40 among 183 in the placebo group.
Calculate RD, RR, and OR for AZT compared to placebo. Try it before running the next chunk.
p_azt <- 13 / 180
p_placebo <- 40 / 183
rd_azt <- p_azt - p_placebo
rr_azt <- p_azt / p_placebo
or_azt <- (p_azt / (1 - p_azt)) /
(p_placebo / (1 - p_placebo))
data.frame(RD = rd_azt,
RR = rr_azt,
OR = or_azt)
The risk is about 14.6 percentage points lower in the AZT group. The RR is about 0.33, and the OR is about 0.28. Be careful when choosing between the words risk and odds.
Log transformation
What happens if we reverse the comparison?
data.frame(
comparison = c("AZT / Placebo", "Placebo / AZT"),
RR = c(rr_azt, 1 / rr_azt),
log_RR = log(c(rr_azt, 1 / rr_azt)),
OR = c(or_azt, 1 / or_azt),
log_OR = log(c(or_azt, 1 / or_azt))
)
In R, log() means the natural logarithm. The log ratios
have the same magnitude with opposite signs. The reference value is 1
for a ratio, and 0 for a log ratio.
To go back to the original ratio, use exp().
exp(log(rr_azt))
## [1] 0.3304167
Question
Calculate log(2), log(1 / 2), and
log(1). Can you explain the three answers? Here we use
positive, finite ratios; log(0) is -Inf.
Incidence rate
Now we need one more thing: time.
An incidence rate is the number of new events divided by the total person-time at risk. Let’s use the Pennsylvania lung cancer example.
cases_pa <- 10279
person_years_pa <- 12281054
ir_pa <- cases_pa / person_years_pa
ir_pa
## [1] 0.0008369803
# Express the same rate per 10,000 person-years.
ir_pa * 10000
## [1] 8.369803
This is about 8.37 new cases per 10,000 person-years. The lecture uses one year per resident as an approximation to the total person-time. Always write the time unit with the answer!
Different follow-up times
The three-subject PBC example uses follow-up times of 7, 2, and 3 years on the study clock. Only the first subject died during follow-up.
dataset_follow_up <- data.frame(
subject = 1:3,
time_years = c(7, 2, 3),
death = c(1, 0, 0)
)
dataset_follow_up
sum(dataset_follow_up$death) / sum(dataset_follow_up$time_years)
## [1] 0.08333333
The rate is 1 death per 12 person-years, or about 0.083 deaths per person-year. The two censored subjects still contribute their observed follow-up time.
mean(dataset_follow_up$time_years)
## [1] 4
mean(dataset_follow_up$death)
## [1] 0.3333333
These are the mean follow-up time and the observed fraction who died. The first is not the mean time to death. The second ignores the different lengths of follow-up. Censored subjects are event-free up to their last observation; we do not know what happened after that.
Changing time units
The Nepal infant mortality example has 644 deaths and 1,627,725 person-days of follow-up.
deaths_nepal <- 644
person_days_nepal <- 1627725
ir_nepal_day <- deaths_nepal / person_days_nepal
ir_nepal_year <- ir_nepal_day * 365
data.frame(deaths_per_person_day = ir_nepal_day,
deaths_per_person_year = ir_nepal_year,
deaths_per_500_person_years = ir_nepal_year * 500)
Here, 365 days is our conversion for one year. Expressing the rate per person-year does not turn the six-month study into a one-year study, or give us a one-year probability of death.
Question
Express the Nepal rate per 1,000 person-days. Do you get the same answer if you convert back from the rate per person-year?
Incidence rate ratio
Let’s compare females to males in the Pennsylvania example.
dataset_pa <- data.frame(
sex = c("Female", "Male"),
cases = c(4587, 5692),
person_years = c(6351391, 5929663)
)
dataset_pa$incidence_rate <- dataset_pa$cases / dataset_pa$person_years
dataset_pa
irr_female_to_male <- dataset_pa$incidence_rate[1] /
dataset_pa$incidence_rate[2]
irr_female_to_male
## [1] 0.7523588
The incidence rate for females is about 0.75 times the rate for males, or about 25% lower. An IRR compares rates; an RR compares risks over a specified period.
PBC trial
Now, use the PBC totals in the lecture. Keep the direction as DPCA compared to placebo.
dataset_pbc_summary <- data.frame(
treatment = c("DPCA", "Placebo"),
deaths = c(65, 60),
person_years = c(872.5, 842.5)
)
dataset_pbc_summary$incidence_rate <- dataset_pbc_summary$deaths /
dataset_pbc_summary$person_years
dataset_pbc_summary
irr_dpca_to_placebo <- dataset_pbc_summary$incidence_rate[1] /
dataset_pbc_summary$incidence_rate[2]
irr_dpca_to_placebo
## [1] 1.046084
From these totals, the IRR is about 1.046: the observed death rate in the DPCA group is about 4.6% higher. This is a descriptive comparison; we have not measured its uncertainty yet.
The hazard ratio mentioned in the ART paper compares instantaneous event rates. It is not generally the same quantity as the overall IRR.
Kaplan-Meier curve
An incidence rate summarizes the whole follow-up period with one number. But when did the events happen? Let’s draw a curve.
Smoking cessation data
These are the 12 subjects from the lecture. The event is
quitting smoking. event = 1 means that the
subject quit, and event = 0 means that the observation was
censored.
dataset_smoking <- data.frame(
time = c(2, 3, 6, 8, 9, 10, 15, 16, 18, 24, 27, 30),
event = c(1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0)
)
dataset_smoking
Here, “survival” means remaining event-free, or not having quit smoking yet. It does not always mean staying alive.
Calculate by hand first
At day 2, all 12 subjects are at risk and one quits. At day 6, only 10 subjects are at risk, because one quit at day 2 and one was censored at day 3.
s_day2 <- (12 - 1) / 12
s_day6 <- s_day2 * (10 - 1) / 10
s_day8 <- s_day6 * (9 - 1) / 9
c(day2 = s_day2, day6 = s_day6, day8 = s_day8)
## day2 day6 day8
## 0.9166667 0.8250000 0.7333333
At each event time, multiply the previous estimate by the fraction of the subjects at risk who remain event-free. Censoring changes later risk sets, but it does not make the curve drop at the censoring time.
Now, use R
Surv() puts time and event status together.
survfit() estimates the Kaplan-Meier curve. The
~ 1 means that we want one curve for the whole sample.
km_smoking <- survival::survfit(
survival::Surv(time, event) ~ 1,
data = dataset_smoking
)
km_summary <- summary(km_smoking)
data.frame(time = km_summary$time,
at_risk = km_summary$n.risk,
events = km_summary$n.event,
survival = km_summary$surv)
Check the first three rows against our calculations above.
plot(km_smoking,
conf.int = FALSE,
mark.time = TRUE,
lwd = 2,
xlab = "Time after workshop (days)",
ylab = "Estimated proportion not yet quit",
main = "Smoking cessation: Kaplan-Meier curve",
xlim = c(0, 30),
ylim = c(0, 1))
The curve is a step function. The marks show censoring times. We use both event and censored observations, assuming censoring is unrelated to the future event time within the group being analyzed.
Reading the curve
Let’s look at days 8 and 18.
summary(km_smoking, times = c(8, 18))$surv
## [1] 0.7333333 0.3771429
# First time the estimated event-free proportion reaches 0.5 or lower.
km_smoking$time[which(km_smoking$surv <= 0.5)[1]]
## [1] 18
The estimated median time to quitting is 18 days. At day 16 the estimate is still slightly above 0.5, even though it rounds to 0.50 in the lecture table. Use the unrounded values when finding the median.
The other way to show the same curve
1 - S(t) shows the estimated proportion who have had the
event by time t. In this example, it is the estimated
proportion who have quit.
plot(km_smoking,
fun = "event",
conf.int = FALSE,
mark.time = TRUE,
lwd = 2,
xlab = "Time after workshop (days)",
ylab = "Estimated proportion who have quit",
main = "Smoking cessation: 1 - S(t)",
xlim = c(0, 30),
ylim = c(0, 1))
Comparing two groups
The survival package includes individual records from
the PBC trial. Let’s select the 312 randomized subjects and draw one
curve per treatment. Here, status = 2 means death;
transplantation and other censored observations are treated as censoring
for this example.
dataset_pbc <- subset(survival::pbc, !is.na(trt))
dataset_pbc$time_years <- dataset_pbc$time / 365
dataset_pbc$death <- as.integer(dataset_pbc$status == 2)
dataset_pbc$treatment <- factor(dataset_pbc$trt,
levels = c(1, 2),
labels = c("DPCA", "Placebo"))
table(dataset_pbc$treatment, dataset_pbc$death)
##
## 0 1
## DPCA 93 65
## Placebo 94 60
Replace ~ 1 with ~ treatment to estimate a
curve for each group.
km_pbc <- survival::survfit(
survival::Surv(time_years, death) ~ treatment,
data = dataset_pbc
)
plot(km_pbc,
conf.int = FALSE,
mark.time = TRUE,
col = c("blue", "red"),
lwd = 2,
xlab = "Follow-up time (years)",
ylab = "Estimated survival probability",
main = "PBC trial: Kaplan-Meier curves",
ylim = c(0, 1))
legend("bottomleft",
legend = levels(dataset_pbc$treatment),
col = c("blue", "red"),
lwd = 2,
bty = "n")
These curves assume that censored subjects, including those receiving a transplant, would have the same future death experience as subjects remaining under observation in their treatment group.
Question
Why does the smoking curve not drop at day 3? Why can the curve stay
above zero even when few subjects are still under observation? Try
adding fun = "event" to the PBC plot and change the y-axis
label to match.
Bibliography
R Core Team (2024). R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing, Vienna, Austria. https://www.R-project.org/.
Xie Y (2025). knitr: A General-Purpose Package for Dynamic Report Generation in R. R package version 1.50, https://yihui.org/knitr/.
Xie Y (2015). Dynamic Documents with R and knitr, 2nd edition. Chapman and Hall/CRC, Boca Raton, Florida. ISBN 978-1498716963, https://yihui.org/knitr/.
Xie Y (2014). “knitr: A Comprehensive Tool for Reproducible Research in R.” In Stodden V, Leisch F, Peng RD (eds.), Implementing Reproducible Computational Research. Chapman and Hall/CRC. ISBN 978-1466561595.
Allaire J, Xie Y, Dervieux C, McPherson J, Luraschi J, Ushey K, Atkins A, Wickham H, Cheng J, Chang W, Iannone R (2025). rmarkdown: Dynamic Documents for R. R package version 2.30, https://github.com/rstudio/rmarkdown.
Xie Y, Allaire J, Grolemund G (2018). R Markdown: The Definitive Guide. Chapman and Hall/CRC, Boca Raton, Florida. ISBN 9781138359338, https://bookdown.org/yihui/rmarkdown.
Xie Y, Dervieux C, Riederer E (2020). R Markdown Cookbook. Chapman and Hall/CRC, Boca Raton, Florida. ISBN 9780367563837, https://bookdown.org/yihui/rmarkdown-cookbook.
Barnier J (2022). rmdformats: HTML Output Formats and Templates for ‘rmarkdown’ Documents. R package version 1.0.4, https://CRAN.R-project.org/package=rmdformats.
Therneau T (2024). A Package for Survival Analysis in R. R package version 3.8-3, https://CRAN.R-project.org/package=survival.
Terry M. Therneau, Patricia M. Grambsch (2000). Modeling Survival Data: Extending the Cox Model. Springer, New York. ISBN 0-387-98784-3.