How well do you “really” know survival analysis?
By: Lathan Liou
Tags: Survival Analysis, Editors Pick, Getting Started, Data Science, R

Key Survival Concepts

This article will assume some familiarity with some more common topics in survival analysis and epidemiology, so I won’t go into too much technical detail. In this section, I’ll describe some key concepts that will be used throughout.

Survival analysis is a statistical field that considers a random variable, X, that represents some amount of time until an event. This event can be good, like a tumor remission, or bad, like death. There are many ways to characterize X, but I’ll only go over one briefly: the survival function.

The survival function represents the probability of an individual surviving beyond time x. This quantity should be strictly decreasing. When we know the outcomes for all of our observations, we can estimate the survival function empirically — at any time x, the survival function is the proportion of people or things that haven’t experienced the event yet. However, in real data, this is often not the case. Either the study ends before we can observe events for all of the study participants or participants drop out and leave the study. Thus, we introduce a concept called censoring. It’s a binary variable, and conventionally, participants who are censored are those that either haven’t experienced the event yet or have dropped out so we can no longer observe them. If you ignore censoring, you overestimate the survival probability.

As a quick note, censoring should be random. As an example, let’s say we are conducting a clinical trial for drug X, but it seems to induce really painful adverse effects in Asian males. So when all the Asian males drop out, the study sample becomes biased, and we lose information on what the actual effects of drug X could’ve been on the entire group.

Back to estimating the survival function. When we don’t have information on outcomes for everyone, we use the Kaplan-Meier estimator.

where n_i represents the number of observations who still haven’t experienced the event and d_i represents the number of observations that experienced the event at time t_i

If we were to plot time on the x-axis and the Kaplan-Meier estimator on the y-axis, we get a Kaplan-Meier curve.

There is one more key term I have to introduce: truncation. This is commonly confused with censoring — it took me a while to confidently distinguish between the two. This may be more easily understood via a diagram.

Censoring vs. Truncation Diagram

Left truncation occurs when data is only recorded for individuals who survive past a certain time as a result of how the study is designed. We generally call individuals who experienced the event prior to when the study began (and thus, not included in the study), left-truncated. The simplest rule of thumb I can come up with (for quite a tricky topic!) to differentiate between truncation and censoring is that censored individuals are still part of the study whereas truncated individuals are not. (Note that this rule of thumb starts failing to apply when we are dealing with multiple events, see this article for a more in-depth explanation). Ignoring truncation produces a biased estimate because we are trying to make a conclusion about the general population from our sample that didn’t even include truncated individuals. One simple self-test question I find helpful to determine whether your data is truncated is Could certain people not be included in my study?

Hopefully, you’ve made it through up until this point — that’s all the math I’m willing to include in this article! As I mentioned in the beginning, this article assumes some baseline understanding of survival analysis, so ideally, the previous section served as a refresher more so than an introduction.

Length Bias

Background

Length bias is the first bias we’re going to talk about (I alluded it to it 2 paragraphs ago!). It’s also known as length-time bias, length-biased sampling, survival bias, survivorship bias, delayed entry bias just to name a few. It occurs when we ignore left truncation. Accounting for left truncation means adjusting the risk set, or the number of people who still haven’t experienced the event, to only include the people who have entered the study at each event time. Let me demonstrate with a toy example:

X indicates an event

If we wanted to estimate our survival function at time 30 correctly adjusting for left truncation, we would get 2/3, or a 66% survival rate. Without correctly accounting for left truncation, we would include the top individual who has not yet entered the study at time 30, and erroneously calculate 3/4, or an inflated 75% survival rate. Do you see how our estimate biased upwards because our sample falsely included everyone, assuming that everyone was part of the study?

R demonstration with real-world data

install.packages("pacman")

pacman::p_load(tidyverse, broom, survival, survminer, KMsurv) # KMsurv provides datasets from Klein and Moeschberger (1997) data(channing)

Let’s first see the data we’re working with:

head(channing, 5) obs death ageentry age time gender

1 1 1 1042 1172 130 2

2 2 1 921 1040 119 2

3 3 1 885 1003 118 2

4 4 1 901 1018 117 2

5 5 1 808 932 124 2

death is the censoring variable, ageentry denotes the age (in months) when an individual entered the study (a.k.a observed truncation time) and age denotes the age (in months) when they experienced the event/became censored. In this case, the time of origin is birth (since we’re dealing with age), but in other studies, the time of origin can be year of disease diagnosis, for example, and then the time scale will be calendar years. For this example, we’re asking the question “How long do people survive following entry into a retirement home?”

Do you remember the graph I showed you at the beginning? If I took this graph at face value, I would conclude that gender is associated with differing survival rates in this retirement home where males are have a lower survival rate than females on average over time!

The same graph as earlier in the article

But guess what, it’s because I ignored left truncation. In the interview, this is the type of thing that they wanted me to think about, but I didn’t know.

To really exaggerate the point of ignoring the effects of ignoring left truncation, I modified the data slightly. I randomly picked 50 males in this dataset and set their time to event age equal to their time to entry ageentry . These 50 males are now left-truncated.

set.seed(47)

male_indices <- which(channing$gender == 1)

truncated_sample <- sample(male_indices, 50)

xchanning <- channing %>%

mutate(id = row_number(),

age = case_when(id %in% truncated_sample ~ ageentry,

TRUE ~ age),

time = age - ageentry)

Then I defined a function that fits KM curves without adjusting for left truncation.

fit_naively <- function(data) {

naive_fit <- survfit(Surv(age, death) ~ gender, data = data)

plot <- survminer::ggsurvplot(naive_fit,

data = data,

conf.int = T,

risk.table = T,

pval = T,

fun = "pct",

legend = "bottom",

legend.title = "sex",

legend.labs = c("Male", "Female"),

tables.theme = theme_cleantable())

return(list(fit = naive_fit,

plot = plot))

} fit_naively(xchanning)$plot

Just as in our toy example, the survival estimator did not account for left truncation, where individuals haven’t technically entered the risk set at certain times, and overestimated the survival probability. Length bias stems from the fact that in order to be included in this study (i.e. enter into the retirement home), one has to survive long enough!

It’s too late to account for left truncation at the design stage, but we can still account for it at the analysis stage by using the R survival package. We simply specify the Surv object to be of the form: Surv(truncation_time, time_to_event, status, type = "counting") and filter out those who are left-truncated at the beginning of the study period since they should not be included in the risk set (usually, in real settings, we don’t even have data on those who experienced an event prior to the beginning of the study because we would never be aware of these people!).

fit_with_lt <- function(data) {

adj_fit <- survfit(Surv(ageentry, age, death, type = "counting") ~ gender, data = data %>% filter(age > ageentry))

plot <- survminer::ggsurvplot(adj_fit,

data = data,

conf.int = T,

risk.table = T,

fun = "pct",

legend = "bottom",

legend.title = "sex",

legend.labs = c("Male", "Female"),

tables.theme = theme_cleantable())

return(list(fit = adj_fit,

plot = plot))

}

The easiest way to see how the gender effect is biased is to run a Cox PH model with coxph . I won’t explain that in this article for fear that this article will run longer that it already has, but you can check out a simple explanation here.

naive_cox_x <- coxph(Surv(age, death) ~ gender, data = xchanning)

broom::tidy(naive_cox_x, exponentiate = T) # A tibble: 1 x 5

term estimate std.error statistic p.value

<chr> <dbl> <dbl> <dbl> <dbl>

1 gender 0.561 0.173 -3.34 0.000823 adj_cox_x <- coxph(Surv(ageentry, age, death) ~ gender, data = xchanning)

broom::tidy(adj_cox_x) # A tibble: 1 x 5

term estimate std.error statistic p.value

<chr> <dbl> <dbl> <dbl> <dbl>

1 gender 0.791 0.232 -1.01 0.311

Do you see how accounting for left truncation (accounting for the people who weren’t included in the study in our modified dataset) shows that gender doesn’t really have an association with survival rates over time? Briefly, the coxph function accounts for left truncation by computing a survival (technically hazard but this article doesn’t cover that!) function that is conditional on people surviving long enough to enter the study.

Always account for truncation; otherwise, you can get some incorrect, biased results.

Time-dependent Bias

Background

I want to go over one more common type of bias in survival analysis. This bias can manifest in different forms throughout your data, but it’s essentially when a time-dependent exposure (an exposure you expect to change over time) is incorrectly analyzed as an unchanging baseline covariate. This bias is also known as immortal time bias or survivor treatment selection bias. Let me demonstrate with a toy example. In this example, let’s say we’re interested in an exposure called treatment A.

X indicates an event

In this hypothetical study, treatment A is only dispensed at time 30. It goes without saying that a patient who doesn’t live up until time 30 won’t be able to receive treatment A. However, for those who do, it would be a mistake to classify a patient as having received treatment A before time 30 because it necessarily biases the results in favor of the exposure (e.g. treatment) under study by conferring a spurious survival advantage to the exposed group. Conceptually, we should only classify person-time as exposed when a person has actually received the exposure.

R demonstration with real-world data

The analytical approach for dealing with time-dependent bias is fairly similar to that for dealing with length bias, but the two types of biases are distinct. Time-dependent bias is fundamentally an information misclassification bias, while length bias is fundamentally a selection bias.

Let’s first see the data we’re working with:

cgd <- survival::cgd0

head(cgd %>% select(id, treat, futime, etime1:etime7)) id treat futime etime1 etime2 etime3 etime4 etime5 etime6 etime7

1 1 414 219 373 NA NA NA NA NA

2 0 439 8 26 152 241 249 322 350

3 1 382 NA NA NA NA NA NA NA

4 1 388 NA NA NA NA NA NA NA

5 0 383 246 253 NA NA NA NA NA

6 1 364 NA NA NA NA NA NA NA

Chronic granulomatous disease (CGD) is a group of uncommon inherited disorders characterized by recurrent infections that usually begin early in life and may lead to death in childhood. In 1986, Genentech, Inc. conducted a randomized, double-blind, placebo-controlled trial in which 128 CGD patients received either an experimental drug or a placebo three times daily for a year ( treatment ). Data were collected on all serious infections until the end of followup futime , which occurred before day 400 for most patients. etime represents on which day a patient had an infection(s). For example, patient #1 had 2 infections on days 219 and 373. The research questions is whether the treatment has any effect on the survival rates related to infection.

We need to change this dataset so that we have time intervals between each infection per row. The following code isn’t the prettiest, but it gets the job done with survival::tmerge() . For more information about what tmerge is doing, see page 8 of this article.

newcgd <- tmerge(cgd %>% select(-starts_with("etime")), cgd, id=id, tstop=futime,

infect = event(etime1), infect= event(etime2),

infect = event(etime3), infect= event(etime4),

infect = event(etime5), infect= event(etime6),

infect = event(etime7))

newcgd <- tmerge(newcgd, newcgd, id=id, enum=cumtdc(tstart)) head(newcgd %>% select(id, treat, infect, futime, tstart, tstop, enum)) id treat infect futime tstart tstop enum

1 1 1 414 0 219 1

1 1 1 414 219 373 2

1 1 0 414 373 414 3

2 0 1 439 0 8 1

2 0 1 439 8 26 2

2 0 1 439 26 152 3

If we ignored time-dependent bias, which means considering the treatment as non time-varying, we would get an erroneous result like this:

coxph(Surv(futime, infect) ~ treat + cluster(id), newcgd) %>%

broom::tidy(exponentiate = T) # A tibble: 1 x 6

term estimate std.error robust.se statistic p.value

<chr> <dbl> <dbl> <dbl> <dbl> <dbl>

1 treat 0.578 0.266 0.349 -1.57 0.117

To account for this bias, we use a similar Surv approach as we did for left truncation bias by specifying two time points.

coxph(Surv(tstart, tstop, infect) ~ treat + cluster(id), newcgd) %>%

broom::tidy(exponentiate = T) # A tibble: 1 x 6

term estimate std.error robust.se statistic p.value

<chr> <dbl> <dbl> <dbl> <dbl> <dbl>

1 treat 0.334 0.261 0.312 -3.51 0.000446

We can see that not accounting for time-dependent bias underestimated the hazard ratio, in this case, that treatment had no significant effect on infection rates. However, in actuality, treatment did have a significant effect on infection rate, where those who received treatment were 0.3 times as likely as those who didn’t to get infected.

For a more in-depth explanation of this, see this article.

Always account for time-dependent bias; otherwise, you can underestimate the risk of exposed patients and overestimate the risk of unexposed patients.

Concluding Thoughts

Survival analysis in observational studies is common enough that these biases need to be well-understood in order to mitigate erroneous conclusions.

I’ll end by taking a step back from the technical. Understanding is a spectrum. I took the interview result hard because I thought for a long time that it meant I didn’t understand survival analysis at all. That’s simply not true. There’s always something deeper and more complex to understand. In the case of survival analysis, I’m sure many people will be able to tell you what it is, but a deeper understanding is ultimately what separates your research from others. Practice makes perfect, and studying hard and putting in the time will yield a more rigorous grasp of the subject. I guess what I’m trying to say is: if you want to truly advance a field, a topic or whatever in life, don’t give up and keep learning!