Hazard ratios are used as part of the Kaplan-Meier approach to survivability analysis. For more information, see the example on the Real Statistics site here and here and also check out the Wikipedia page.
If a clinical trial takes place to test the effectiveness of a cancer drug in prolonging survival in 20 cancer patients, the results might look as follows:
import pandas as pd
# Create data frame
dct = {
'timepoint': [0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14],
'died': [0, 2, 1, 0, 0, 2, 1, 2, 1, 0, 3, 1, 1, 1],
'withdrew': [0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0],
}
trial_data = pd.DataFrame(dct)
print(trial_data)
## timepoint died withdrew
## 0 0 0 0
## 1 1 2 0
## 2 2 1 0
## 3 3 0 0
## 4 4 0 0
## 5 5 2 1
## 6 7 1 0
## 7 8 2 0
## 8 9 1 1
## 9 10 0 1
## 10 11 3 1
## 11 12 1 1
## 12 13 1 0
## 13 14 1 0
At each timepoint from 0 years (the start of the trial) to 14 years (note that there was no follow-up done in the 6th year so this was the 13th follow-up) the number of participants who had died and the number that had been lost to follow-up or who had voluntarily withdrawn was counted. The total number of participants sampled at each timepoint can be added to the data frame like so:
# Count participants
trial_data['n'] = 20 - trial_data['died'].cumsum() - trial_data['withdrew'].cumsum()
# Move the values down one row
trial_data['n'] = trial_data['n'].shift(1)
# Add in the starting sample size
trial_data.loc[0, 'n'] = 20
print(trial_data)
## timepoint died withdrew n
## 0 0 0 0 20.0
## 1 1 2 0 20.0
## 2 2 1 0 18.0
## 3 3 0 0 17.0
## 4 4 0 0 17.0
## 5 5 2 1 17.0
## 6 7 1 0 14.0
## 7 8 2 0 13.0
## 8 9 1 1 11.0
## 9 10 0 1 9.0
## 10 11 3 1 8.0
## 11 12 1 1 4.0
## 12 13 1 0 2.0
## 13 14 1 0 1.0
Often, a trial will have two groups (usually a placebo group and an experimental group), so let’s generate the data for a second group and put both data frames into a dictionary:
trial = {}
trial['A'] = trial_data
# Create data frame
dct = {
'timepoint': [0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14],
'died': [0, 0, 1, 1, 0, 1, 3, 1, 0, 2, 0, 2, 0, 1],
'withdrew': [0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 2, 0, 1, 1],
}
trial['B'] = pd.DataFrame(dct)
# Count participants
trial['B']['n'] = 20 - trial['B']['died'].cumsum() - trial['B']['withdrew'].cumsum()
# Move the values down one row
trial['B']['n'] = trial['B']['n'].shift(1)
# Add in the starting sample size
trial['B'].loc[0, 'n'] = 20
print(trial['B'])
## timepoint died withdrew n
## 0 0 0 0 20.0
## 1 1 0 0 20.0
## 2 2 1 0 20.0
## 3 3 1 0 19.0
## 4 4 0 1 18.0
## 5 5 1 1 17.0
## 6 7 3 0 15.0
## 7 8 1 1 12.0
## 8 9 0 1 10.0
## 9 10 2 0 9.0
## 10 11 0 2 7.0
## 11 12 2 0 5.0
## 12 13 0 1 3.0
## 13 14 1 1 2.0
The next step towards working out whether the drug being tested has been effective or not is to work out the number of deaths we would expect to see at each timepoint. We can then compare this to the number of deaths we actually saw, and if this is lower it will go a long way towards establishing that the drug is indeed effective. To calculate the expected number of deaths, first get the total number of deaths by adding the results of trial A to trial B:
# Combined deaths and sample sizes
combined = pd.DataFrame()
combined['died'] = trial['A']['died'] + trial['B']['died']
combined['n'] = trial['A']['n'] + trial['B']['n']
print(combined)
## died n
## 0 0 40.0
## 1 2 40.0
## 2 2 38.0
## 3 1 36.0
## 4 0 35.0
## 5 3 34.0
## 6 4 29.0
## 7 3 25.0
## 8 1 21.0
## 9 2 18.0
## 10 3 15.0
## 11 3 9.0
## 12 1 5.0
## 13 2 3.0
The expected number of deaths at each timepoint in each trial is simply the overall prevalence of death (instances per participant) multiplied by the number of participants:
# Expected deaths for trial A
trial['A']['expected'] = trial['A']['n'] * combined['died'] / combined['n']
print(trial['A'])
## timepoint died withdrew n expected
## 0 0 0 0 20.0 0.000000
## 1 1 2 0 20.0 1.000000
## 2 2 1 0 18.0 0.947368
## 3 3 0 0 17.0 0.472222
## 4 4 0 0 17.0 0.000000
## 5 5 2 1 17.0 1.500000
## 6 7 1 0 14.0 1.931034
## 7 8 2 0 13.0 1.560000
## 8 9 1 1 11.0 0.523810
## 9 10 0 1 9.0 1.000000
## 10 11 3 1 8.0 1.600000
## 11 12 1 1 4.0 1.333333
## 12 13 1 0 2.0 0.400000
## 13 14 1 0 1.0 0.666667
# Expected deaths for trial B
trial['B']['expected'] = trial['B']['n'] * combined['died'] / combined['n']
print(trial['B'])
## timepoint died withdrew n expected
## 0 0 0 0 20.0 0.000000
## 1 1 0 0 20.0 1.000000
## 2 2 1 0 20.0 1.052632
## 3 3 1 0 19.0 0.527778
## 4 4 0 1 18.0 0.000000
## 5 5 1 1 17.0 1.500000
## 6 7 3 0 15.0 2.068966
## 7 8 1 1 12.0 1.440000
## 8 9 0 1 10.0 0.476190
## 9 10 2 0 9.0 1.000000
## 10 11 0 2 7.0 1.400000
## 11 12 2 0 5.0 1.666667
## 12 13 0 1 3.0 0.600000
## 13 14 1 1 2.0 1.333333
The failure rate is the observed number of failures (deaths) relative to the expected number of failures:
# Failure rates
failure_rate = {}
failure_rate['A'] = trial['A']['died'].sum() / trial['A']['expected'].sum()
failure_rate['B'] = trial['B']['died'].sum() / trial['B']['expected'].sum()
print(failure_rate)
## {'A': 1.1596950625269171, 'B': 0.8531473638822527}
The ratio of the failure rates (ie how much more one arm of the trial results in failure relative to the other) is the hazard ratio, also known as the relative risk or risk ratio:
hazard_ratio = failure_rate['B'] / failure_rate['A']
print(hazard_ratio.round(3))
## 0.736
If trial B was the one in which the participants were given the drug in question, it can be reported that those people were 0.736 times as likely to die compared to the people in trial A who weren’t given the drug.