0%

Simply Understanding Log Rank Test

The logrank test is the most commonly used statistical test in clinical trials to compare the survival distributions in different treatment groups. We usually just use the logrank results to test whether there is a difference between two survival curves. But what does this difference mean?

The main references are the two resources below.

First of all, the difference is a statistical concept, so we need a statistical hypothesis test to get the p-value in order to determine whether the difference is significant.

  • Null hypothesis: The two treatment groups have identical survival distributions.
  • Alternative hypothesis: The two treatment groups have different survival distributions.

Now the question turns to how to get the p-value. Before that we should obtain a test statistic that follows the a chi-squared distribution. Thus the question can be simplified to how to compute the test statistic, with details and equations available in the references above. Let's create an example data used to display the whole calculation process in R.

Suppose we have group 1 and group 2 including six time to event data. The table shows the survival time in t columns and the evt column tells us if an event occurred (evt=1) or the case censored (evt=0) in the corresponding time.

d1 <- data.frame(
  t = c(3.1, 6.8, 9, 9, 11.3, 16.2),
  evt = c(1, 0, 1, 1, 0, 1)
)
d2 <- data.frame(
  t = c(8.7, 9, 10.1, 12.1, 18.7, 23.1),
  evt = c(1, 1, 0, 0, 1, 0)
)

Then we need to convert above datasets to unique survival times, summarize the number of events (m column) and consors (q column), and add one column to represent the risk number (n column) at each time. Then outer join two dataset using t variable and filter the invalid time as the time without events cannot offer any meaningful information for test statistic.

dat1 <- data.frame(
  t = c(3.1, 6.8, 9, 11.3, 16.2),
  m = c(1, 0, 2, 0, 1),
  q = c(0, 1, 0, 1, 0),
  n = c(6, 5, 4, 2, 1)
)
dat2 <- data.frame(
  t = c(8.7, 9, 10.1, 12.1, 18.7, 23.1),
  m = c(1, 1, 0, 0, 1, 0),
  q = c(0, 0, 1, 1, 0, 1),
  n = c(6, 5, 4, 3, 2, 1)
)
dat <- full_join(dat1, dat2, by = "t", suffix = c("1", "2")) %>%
  arrange(t) %>%
  filter(!(m1 %in% c(NA, 0) & m2 %in% c(NA, 0))) %>%
  mutate(
    across(contains(c("n")), \(x) ifelse(is.na(x), lead(x), x)),
    across(contains(c("n1")), \(x) ifelse(row_number() == n(), lag(x) - lag(m1), x)),
    across(contains(c("m", "q")), \(x) replace_na(x, 0))
  )

##      t m1 q1 n1 m2 q2 n2
## 1  3.1  1  0  6  0  0  6
## 2  8.7  0  0  4  1  0  6
## 3  9.0  2  0  4  1  0  5
## 4 16.2  1  0  1  0  0  2
## 5 18.7  0  0  0  1  0  2

To get the statistic, we should firstly compute the so-called expected value (e1 or e2), the difference (me1 or me2) of the observed value (m1 or m2) minus the expected values, and the variance (v).

dat <- dat %>%
  mutate(
    e1 = n1 / (n1 + n2) * (m1 + m2),
    e2 = n2 / (n1 + n2) * (m1 + m2),
    me1 = m1 - e1,
    me2 = m2 - e2,
    v = (n1 * n2 * (m1 + m2) * (n1 + n2 - m1 - m2)) / ((n1 + n2)^2 * (n1 + n2 - 1))
  )

##      t m1 q1 n1 m2 q2 n2        e1        e2        me1        me2         v
## 1  3.1  1  0  6  0  0  6 0.5000000 0.5000000  0.5000000 -0.5000000 0.2500000
## 2  8.7  0  0  4  1  0  6 0.4000000 0.6000000 -0.4000000  0.4000000 0.2400000
## 3  9.0  2  0  4  1  0  5 1.3333333 1.6666667  0.6666667 -0.6666667 0.5555556
## 4 16.2  1  0  1  0  0  2 0.3333333 0.6666667  0.6666667 -0.6666667 0.2222222
## 5 18.7  0  0  0  1  0  2 0.0000000 1.0000000  0.0000000  0.0000000 0.0000000

Base on above computations, now we can simply calculate the test statistic, that is 1.6205 in our example. Then the p-value can be determined using the chi-squared distribution with one degree of freedom (number of groups minus 1).

z <- (sum(dat$me2))^2 / sum(dat$v)
z
## [1] 1.620508

pchisq(z, df = 2 - 1, lower.tail = FALSE)
## [1] 0.2030209

Now that we should have a basic knowledge of logrank, and let us check with those found in the mature R package survival.

data <- bind_rows(
  data.frame(
    t = c(3.1, 6.8, 9, 9, 11.3, 16.2),
    m = c(1, 0, 1, 1, 0, 1)
  ),
  data.frame(
    t = c(8.7, 9, 10.1, 12.1, 18.7, 23.1),
    m = c(1, 1, 0, 0, 1, 0)
  )
  , .id = "grp"
)
survdiff(formula = Surv(t, m==1) ~ grp, data = data)

## Call:
## survdiff(formula = Surv(t, m == 1) ~ grp, data = data)
## 
##       N Observed Expected (O-E)^2/E (O-E)^2/V
## grp=1 6        4     2.57     0.800      1.62
## grp=2 6        3     4.43     0.463      1.62
## 
##  Chisq= 1.6  on 1 degrees of freedom, p= 0.2 

From above we can find the Expected column corresponds to the expected value we calculated, and the Observed column represents the observed value, and the (O-E)^2/V column represents the test statistic where the V is the variance of it. And both of them show the same chisq value and p-value.

For now perhaps you have a bette understanding of logrank test like me after going through the whole computation process.