R
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Data Structures
-
Section 4: Data Manipulation
-
Section 5: Visualization and Statistics
-
Section 6: Advanced R
-
Section 7: Practical Projects
-
Section 8: Interview Practice
-
Section 9: More Practice Exercises
-
Section 10: Shiny Apps in Depth
-
Section 11: More Data Wrangling
-
Section 12: Tidyverse Deep Dive
-
Section 13: Statistical Modeling Deep Dive
-
Section 14: Machine Learning in R
-
Section 15: R Visualization Deep Dive
-
Section 16: R Package Development Deep Dive
-
Section 17: R for Reproducible Research
-
Section 18: R and Databases
-
Section 19: R Performance Optimization
-
Section 20: Bioinformatics and Specialized R
-
Section 21: More Shiny Practice
-
Section 22: More Practice Exercises
-
Section 23: R Data Structures Deep Dive
-
Section 24: More Interview and Review
-
Section 25: R for Business Analytics
-
Section 26: R Text Mining and NLP
-
Section 27: R Spatial Data Analysis
-
Section 28: R Deep Learning
-
Section 29: Advanced Statistical Techniques
-
Section 30: R Object Systems Deep Dive
-
Section 31: R Environments and Metaprogramming
-
Section 32: R for Finance
-
Section 33: R for Clinical and Health Data
-
Section 34: More Shiny Advanced Practice
-
Section 35: R Data Cleaning Deep Dive
-
Section 36: R Reporting Automation
-
Section 37: More Practical Projects Round 2
-
Section 38: R Ecosystem and Career
-
Section 39: More Visualization Practice
-
Section 40: R for Experimentation
-
Section 41: R for Genomics and Bioinformatics Deep Dive
-
Section 42: R for Marketing Analytics
-
Section 43: R Data Import/Export Deep Dive
-
Section 44: R String Processing Deep Dive
-
Section 45: R for Actuarial and Insurance Analytics
-
Section 46: R Testing and Quality Assurance Deep Dive
-
Section 47: R Version Control and Collaboration
-
Section 48: R Advanced Functional Programming
-
Section 49: R for Supply Chain and Operations
-
Section 50: More Practice Exercises Round 3
-
Section 51: R Dashboards and BI Integration
-
Section 52: R Data Governance and Ethics
-
Section 53: More Modeling Practice
-
Section 54: R Final Capstone Projects
-
Section 55: R for Sports Analytics
-
Section 56: More Interview Practice Round 2
-
Section 57: R Networking and APIs
-
Section 58: R for Environmental Science
-
Section 59: R Notebook and Documentation Practices
-
Section 60: More Data Wrangling Mastery
-
Section 61: R for A/B Testing at Scale
-
Section 62: R Package Ecosystem Deep Dive
134: Survival Curves and Kaplan-Meier Estimates Revisited
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_occurredcolumn into a numeric format where "Yes" (the event) is 1 and "No" (censored) is 0. - Create a
survfitobject comparing thearmgroups. - Generate a
ggsurvplotthat includes confidence intervals and a risk table. - Calculate the p-value using
survdiffand thepchisqfunction. - Print the p-value to the console.
There are no comments for now.