Skip to Content
Course content

134: Survival Curves and Kaplan-Meier Estimates Revisited

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I’ve seen this happen more times than I can count: a researcher comes to me panicked because their survival curve is a flat line at 100%, or it drops to 0% instantly, and they're convinced their data is corrupted. Usually, the data is fine—the problem is that the survival package is very particular about how it perceives "events" versus "censoring."

library(survival)
library(survminer)

# Hypothetical patient data
# time: days until event or end of study
# status: 1 = survived, 0 = died (Wait, is this right?)
df <- data.frame(
  time = c(10, 12, 15, 20, 25, 30, 35, 40),
  status = c(1, 1, 0, 1, 0, 1, 0, 1), 
  group = c("A", "A", "A", "A", "B", "B", "B", "B")
)

# Attempting to create the KM estimate
km_fit <- survfit(Surv(time, status) ~ group, data = df)
ggsurvplot(km_fit)

The "Impossible Survival" Plot

If you run the code above, you'll notice something strange. The survival probability stays incredibly high, or the "drops" in the curve don't make sense relative to the clinical reality of the study. Why? Because in R's survival package, the status argument in Surv() expects the event of interest to be coded as 1 and the censored observations to be coded as 0.

In my example above, I coded 1 as "survived" and 0 as "died." To the survfit function, this means "surviving" is the event we are tracking. The curve isn't showing the probability of staying alive; it's showing the probability of not yet having survived. It's a complete inversion of logic that leads to misleading plots and incorrect p-values.

Flipping the Status Logic

To fix this, we need to ensure the event (death, failure, or relapse) is 1. If your dataset is already exported with the opposite logic, don't manually edit the CSV—do it in R so your pipeline remains reproducible. I usually prefer a simple logical flip or a mutate call.

# The fix: Recode status so that 1 = Event (Death) and 0 = Censored
df$status_fixed <- ifelse(df$status == 0, 1, 0)

# Now we run the KM estimate again
km_fit_fixed <- survfit(Surv(time, status_fixed) ~ group, data = df)
ggsurvplot(km_fit_fixed, conf.int = TRUE, risk.table = TRUE)

Now the curve makes sense. It drops when a death occurs and stays flat when a patient is censored. I added conf.int = TRUE and risk.table = TRUE because, in my experience, a survival curve without a risk table is almost useless for a peer review; you need to know how many people were actually left at risk at each time point to trust the tail of the curve.

Fine-Tuning the Kaplan-Meier Interpretation

Once the code is running, we need to revisit the survdiff function. The survfit object tells us how the groups differ visually, but it doesn't tell us if the difference is statistically significant. That's where the Log-Rank test comes in.

# Statistical test to compare groups
diff_test <- survdiff(Surv(time, status_fixed) ~ group, data = df)
print(diff_test)

When you look at the output of survdiff, don't look for a "p-value" field—it isn't explicitly labeled. You'll see a Chi-squared value and degrees of freedom. To get the actual p-value, you have to calculate it using the distribution: 1 - pchisq(diff_test$chisq, length(diff_test$n) - 1). It's a bit of a clunky API, but that's R for you.

Handling Multiple Covariates

One final reminder: the Kaplan-Meier estimate is univariate. If you find yourself trying to add three or four different grouping variables into the ~ formula, you're going to end up with a "spaghetti plot" that is impossible to read. If you need to adjust for age, weight, and dosage simultaneously, stop using survfit and move to a Cox Proportional Hazards model (coxph). KM is for visualization and simple group comparisons; Cox is for multivariate analysis.




📋 Practical Task

Analyzing Treatment Efficacy in a Lung Cancer Dataset

You are provided with a small sample of clinical data. Your task is to identify a logic error in the survival status, generate a clean Kaplan-Meier plot, and determine if there is a statistically significant difference between the two treatment arms.

Dataset:

trial_data <- data.frame(
  days = c(120, 450, 600, 100, 200, 500, 300, 700, 800, 150),
  event_occurred = c("No", "Yes", "Yes", "No", "Yes", "No", "Yes", "No", "Yes", "No"),
  arm = c("Placebo", "Placebo", "Placebo", "Placebo", "Placebo", "Drug", "Drug", "Drug", "Drug", "Drug")
)

Requirements:

  • Convert the event_occurred column into a numeric format where "Yes" (the event) is 1 and "No" (censored) is 0.
  • Create a survfit object comparing the arm groups.
  • Generate a ggsurvplot that includes confidence intervals and a risk table.
  • Calculate the p-value using survdiff and the pchisq function.
  • Print the p-value to the console.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.