Section 1 of 9

1 Before you start

Before you begin, take a few minutes to check what you already know and how confident you feel. You will see the same questions again at the end of the module — this helps both you and us measure what you have learned. Click an option for every question and confidence rating, then click Next to continue.

Pre-test

You have a fitted logistic model fit and want each patient's estimated probability of the event. Which call returns probabilities rather than log-odds?

Pre-test

You run predict(fit) on a logistic model without setting type. What scale are the returned values on?

Pre-test

A colleague reports that their prediction model has an AUC of 0.50. How should you read that?

Pre-test

Which pair of calls measures a logistic model's discrimination, given the true 0/1 outcome and the predicted probabilities?

Pre-test

A patient reaches the end of the study still alive, so their time to death is unknown. In survival analysis this observation is described as

Pre-test

The lung dataset codes its status column as 1 = censored and 2 = dead. Before building a Surv() outcome, why must you recode it so 1 = event?

Pre-test

Which function estimates the Kaplan-Meier survival curve, split by treatment group?

Pre-test

A log-rank test comparing survival in two arms returns p = 0.002. What may you conclude?

Pre-confidence

I can generate predicted probabilities from a logistic model with predict(type = "response") and explain why the default returns log-odds instead.

Not at all confident
Fully confident
Pre-confidence

I can measure a model's discrimination by building an ROC curve with roc() and reading its AUC, and I know that 0.5 is a coin flip and 1.0 is perfect.

Not at all confident
Fully confident
Pre-confidence

I can code a censored time-to-event outcome with Surv(time, event), draw a Kaplan-Meier curve, and compare two groups with the log-rank test via survdiff() while knowing its limits.

Not at all confident
Fully confident
Section 2 of 9

2 Introduction

In Part III you fit a logistic model and turned its coefficients into adjusted odds ratios. A fitted model can do more than report effects — it can predict. This part asks each patient's estimated risk, checks how well those risks separate patients who had the event from those who did not, and then steps into a different kind of question altogether: not whether an event happens, but when.

This part of the module covers two skills:

  • Predicted probabilities and discrimination — read each patient's estimated risk with predict() and type = "response", then measure how well those risks separate the groups with an ROC curve and its AUC.
  • A taste of survival analysis — handle censoring, pack time and status into an outcome with Surv(), draw a Kaplan-Meier curve, and compare two groups with the log-rank test.

By the end of this part you will be able to generate predicted probabilities from a logistic model, quantify its discrimination with an ROC AUC and read that number correctly, code a censored time-to-event outcome with Surv(), produce and interpret a Kaplan-Meier survival curve, and compare two groups with a log-rank test — while knowing exactly what that test can and cannot tell you.

Try every snippet in the R Scratchpad on the right. The discrimination examples build a small simulated cohort with set.seed() and score it with pROC; the survival examples use the lung dataset that ships inside the survival package.

Section 3 of 9

3 Predicted probabilities and discrimination

A fitted model can also predict. Feed it the patients and ask for each one's estimated probability of the event with predict() and type = "response". The type = "response" part is essential: without it predict() returns log-odds, not probabilities.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
set.seed(2025)
n <- 300
age <- round(rnorm(n, mean = 60, sd = 10))
smoker <- rbinom(n, 1, 0.35)
complication <- rbinom(n, 1, 1 / (1 + exp(-(-6 + 0.07 * age + 0.8 * smoker))))
cohort <- data.frame(age, smoker, complication)
fit <- glm(complication ~ age + smoker, family = binomial, data = cohort)
cohort$prob <- predict(fit, type = "response")
summary(cohort$prob)
On a fitted logistic model predict() returns log-odds by default (here -1.0), and type = "response" passes that value through the logistic curve to give the patient's event probability in [0, 1] (here 0.27).
On a fitted logistic model predict() returns log-odds by default (here -1.0), and type = "response" passes that value through the logistic curve to give the patient's event probability in [0, 1] (here 0.27).

These probabilities let you close the loop with the diagnostics module. A good model gives higher probabilities to patients who actually had the event. We measure that separation — called discrimination — with a ROC curve and its area under the curve, the AUC. An AUC of 0.5 is a coin flip; 1.0 is perfect; clinical models often land between 0.7 and 0.85.

The pROC package builds the curve. Give roc() the true 0/1 outcome and the predicted probabilities; then auc() reads off the area. Plotting the roc object draws the curve.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
library(pROC)
set.seed(2025)
n <- 300
age <- round(rnorm(n, mean = 60, sd = 10))
smoker <- rbinom(n, 1, 0.35)
complication <- rbinom(n, 1, 1 / (1 + exp(-(-6 + 0.07 * age + 0.8 * smoker))))
cohort <- data.frame(age, smoker, complication)
fit <- glm(complication ~ age + smoker, family = binomial, data = cohort)
cohort$prob <- predict(fit, type = "response")
roc_obj <- roc(cohort$complication, cohort$prob)
auc(roc_obj)
plot(roc_obj)
AUC measures how well a model's predicted risk separates patients who had the event from those who did not, so as the two groups pull apart the ROC bows toward the corner and the AUC climbs from 0.5 (coin flip) toward 1.0, with real clinical models usually landing around 0.70 to 0.85.
AUC measures how well a model's predicted risk separates patients who had the event from those who did not, so as the two groups pull apart the ROC bows toward the corner and the AUC climbs from 0.5 (coin flip) toward 1.0, with real clinical models usually landing around 0.70 to 0.85.

Practise predicting probabilities and scoring them in the example below:

Worked example · Predicted probabilities and AUC

Work through this example in three stages. You unlock each stage only after the tutor confirms the previous one. Each stage removes more of the scaffolding — by the end you are writing it yourself.

Problem: Fit a logistic model, get each patient's predicted probability with predict(type = "response"), and measure discrimination with an ROC AUC.

Stage 1 · Study the solved example
Fully solved solution
library(pROC)
set.seed(2025)
age <- round(rnorm(100, 60, 10))
complication <- rbinom(100, 1, plogis(-6 + 0.1 * age))
fit <- glm(complication ~ age, family = binomial)
prob <- predict(fit, type = "response")
auc(roc(complication, prob))
Walk-through
  1. predict(type = "response") gives probabilities, not log-odds
  2. roc() compares them to the true outcome
  3. auc() summarises discrimination
AUC is the probability that the model gives a higher predicted risk to a patient who had the complication than to one who did not, so 0.5 is a coin flip and 1.0 is perfect ranking.
AUC is the probability that the model gives a higher predicted risk to a patient who had the complication than to one who did not, so 0.5 is a coin flip and 1.0 is perfect ranking.
Section 4 of 9

4 A taste of survival analysis

Sometimes the question is not whether an event happens, but when. How long until relapse, until discharge, until death? That is survival analysis, and it needs its own tools because of one feature: censoring.

A patient is censored when the study ends, or they are lost to follow-up, before the event happens. You know they survived at least that long, but not their full time to event. You cannot just drop censored patients or treat their last-seen time as the event time — both throw away real information and bias the result.

A censored observation is a floor on survival time (the patient lasted at least that long), so it must be kept in the analysis rather than dropped or mistaken for the event itself.
A censored observation is a floor on survival time (the patient lasted at least that long), so it must be kept in the analysis rather than dropped or mistaken for the event itself.
Section 4.1 of 9

4.1 Building the outcome with Surv()

The survival package packs time and status into one outcome with Surv(time, event). The second argument is the event indicator: 1 marks that the event happened; 0 marks a censored observation.

Here is the trap that silently ruins survival analyses. If you code the event indicator backwards — 0 for the event and 1 for censored — every result inverts and no error warns you. Always confirm that 1 = event before you trust a curve.

We will use lung, a cohort of advanced lung-cancer patients that ships inside the survival package. Its time column is survival in days and its status column codes 1 for censored and 2 for dead, so we recode death to a clean 1.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
library(survival)
lung$died <- ifelse(lung$status == 2, 1, 0)
lung$surv <- Surv(lung$time, lung$died)
head(lung$surv)
In Surv(time, event) the indicator is directional, so coding it backwards does not throw an error, it silently turns a real survival curve into a falsely optimistic wrong one, which is why you must confirm that 1 means the event before trusting any result.
In Surv(time, event) the indicator is directional, so coding it backwards does not throw an error, it silently turns a real survival curve into a falsely optimistic wrong one, which is why you must confirm that 1 means the event before trusting any result.

A plus sign next to a time in the printed Surv object marks a censored patient. A bare number marks a patient who had the event. That little plus is your visual check that censoring was coded the right way round.

Section 4.2 of 9

4.2 The Kaplan-Meier curve

The Kaplan-Meier curve estimates the probability of surviving past each time point, stepping down as events occur and accounting for censoring along the way. You fit it with survfit() and a formula whose right side is the grouping — use ~ 1 for the whole cohort, or ~ sex to split by a group.

This course plots survival curves with ggsurvfit. You pass the survfit() object to ggsurvfit() and get a ggplot you can style. Do not reach for survminer — this course standardises on ggsurvfit, and mixing the two only creates confusion.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
library(survival)
library(ggsurvfit)
lung$died <- ifelse(lung$status == 2, 1, 0)
km_fit <- survfit(Surv(time, died) ~ sex, data = lung)
ggsurvfit(km_fit)
The Kaplan-Meier estimate steps down only when someone dies, by the fraction (n-1)/n of those still at risk, while a censored subject merely leaves the risk set, so each later death moves the curve more.
The Kaplan-Meier estimate steps down only when someone dies, by the fraction (n-1)/n of those still at risk, while a censored subject merely leaves the risk set, so each later death moves the curve more.

Read the curve at two kinds of landmark. The median survival is the time where the curve crosses 0.5 — half the group has had the event by then. A landmark estimate reads survival at a fixed time, say the one-year probability. Printing the survfit object reports the median for each group.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
library(survival)
lung$died <- ifelse(lung$status == 2, 1, 0)
km_fit <- survfit(Surv(time, died) ~ sex, data = lung)
km_fit
A Kaplan-Meier curve is a two-way lookup table, where reading across from S = 0.50 gives the median survival time and reading up from a fixed time gives that time's survival probability.
A Kaplan-Meier curve is a two-way lookup table, where reading across from S = 0.50 gives the median survival time and reading up from a fixed time gives that time's survival probability.
Section 4.3 of 9

4.3 Comparing two groups: the log-rank test

To ask whether two survival curves differ, use the log-rank test. It compares the observed events in each group with what you would expect if the groups shared one survival curve, and returns a p-value. In R it is survdiff(), with the same Surv() ~ group formula.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
library(survival)
lung$died <- ifelse(lung$status == 2, 1, 0)
survdiff(Surv(time, died) ~ sex, data = lung)
The log-rank test adds up, across every death time, the gap between the deaths a group actually had and the deaths expected from its share of those still at risk, and a large running total is evidence the survival curves truly differ.
The log-rank test adds up, across every death time, the gap between the deaths a group actually had and the deaths expected from its share of those still at risk, and a large running total is evidence the survival curves truly differ.

A small p-value says the curves differ more than chance would explain — one group survives longer. But the log-rank test only answers whether they differ, not by how much, and it cannot adjust for other variables the way your logistic model did.

That is where this taste stops. To estimate how much risk differs, adjusted for age and stage, you need Cox proportional-hazards regression and its hazard ratios — the survival cousin of the odds ratio. Cox regression and hazard ratios are deferred to Course 2; survival needs them because the log-rank test compares groups but cannot adjust for

We can compare survival between two groups in the example below

Worked example · A Kaplan-Meier comparison

Work through this example in three stages. You unlock each stage only after the tutor confirms the previous one. Each stage removes more of the scaffolding — by the end you are writing it yourself.

Problem: Using the lung dataset, recode death to 1, fit Kaplan-Meier curves split by sex, and test the difference with the log-rank test.

Stage 1 · Study the solved example
Fully solved solution
library(survival)
lung$died <- ifelse(lung$status == 2, 1, 0)
km_fit <- survfit(Surv(time, died) ~ sex, data = lung)
survdiff(Surv(time, died) ~ sex, data = lung)
Walk-through
  1. recode status so 1 = death
  2. survfit() builds the Kaplan-Meier curves by sex
  3. survdiff() runs the log-rank test for a difference
The log-rank test only confirms that two survival curves differ; it gives no effect size and cannot adjust for other variables, so estimating how much risk differs (adjusted for age and stage) requires Cox regression and its hazard ratio.
The log-rank test only confirms that two survival curves differ; it gives no effect size and cannot adjust for other variables, so estimating how much risk differs (adjusted for age and stage) requires Cox regression and its hazard ratio.
Section 5 of 9

5 Check your understanding

You have reached the end of the module. Try the same questions again — your answers here, paired with your pre-test answers, are how we measure what the module taught you. Answer every question and confidence rating, then click Submit and see results to view your score.

Post-test

You have a fitted logistic model fit and want each patient's estimated probability of the event. Which call returns probabilities rather than log-odds?

Post-test

You run predict(fit) on a logistic model without setting type. What scale are the returned values on?

Post-test

A colleague reports that their prediction model has an AUC of 0.50. How should you read that?

Post-test

Which pair of calls measures a logistic model's discrimination, given the true 0/1 outcome and the predicted probabilities?

Post-test

A patient reaches the end of the study still alive, so their time to death is unknown. In survival analysis this observation is described as

Post-test

The lung dataset codes its status column as 1 = censored and 2 = dead. Before building a Surv() outcome, why must you recode it so 1 = event?

Post-test

Which function estimates the Kaplan-Meier survival curve, split by treatment group?

Post-test

A log-rank test comparing survival in two arms returns p = 0.002. What may you conclude?

Post-confidence

I can generate predicted probabilities from a logistic model with predict(type = "response") and explain why the default returns log-odds instead.

Not at all confident
Fully confident
Post-confidence

I can measure a model's discrimination by building an ROC curve with roc() and reading its AUC, and I know that 0.5 is a coin flip and 1.0 is perfect.

Not at all confident
Fully confident
Post-confidence

I can code a censored time-to-event outcome with Surv(time, event), draw a Kaplan-Meier curve, and compare two groups with the log-rank test via survdiff() while knowing its limits.

Not at all confident
Fully confident
Section 6 of 9

6 Your results

Here is how your post-test answers compare with your pre-test answers. The pre/post pairing is the most reliable way to see what this module actually taught you.

Your score

Submit the post-test to see your results.

Muddiest point

What is the one thing from this module that is still unclear to you?

Rate this module

Overall, how would you rate this module?

How likely are you to recommend this module to a peer? (0 = not at all, 10 = extremely likely)