BTE3207 week 3

Minsik Kim

2026 Fall

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.