Survival Analysis
Overview
Survival analysis studies time until an event occurs, handling censored data where events haven't happened for some subjects, enabling prediction of lifetimes and risk assessment.
Key Concepts
- Survival Time: Time until event
- Censoring: Event not observed (subject dropped out)
- Hazard: Instantaneous risk at time t
- Survival Curve: Probability of surviving past time t
- Hazard Ratio: Relative risk between groups
Common Models
- Kaplan-Meier: Non-parametric survival curves
- Cox Proportional Hazards: Semi-parametric regression
- Weibull/Exponential: Parametric models
- Log-rank Test: Comparing survival curves
- Competing Risks: Multiple event types
Implementation with Python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from lifelines import KaplanMeierFitter, CoxPHFitter, WeibullAFTFitter
from lifelines.statistics import logrank_test
import warnings
warnings.filterwarnings('ignore')
# Generate sample survival data
np.random.seed(42)
n_patients = 200
# Time to event (in months)
event_times = np.random.exponential(scale=24, size=n_patients)
# Censoring indicator (1 = event occurred, 0 = censored)
event_observed = np.random.binomial(1, 0.7, n_patients)
# Group assignment (0 = control, 1 = treatment)
group = np.random.binomial(1, 0.5, n_patients)
# Age at baseline
age = np.random.uniform(30, 80, n_patients)
# Risk score
risk_score = np.random.uniform(0, 100, n_patients)
# Adjust event times based on group (simulate treatment effect)
event_times = event_times * (1 + group * 0.3)
df = pd.DataFrame({
'time': event_times,
'event': event_observed,
'group': group,
'age': age,
'risk_score': risk_score,
})
print("Survival Data Summary:")
print(df.head(10))
print(f"\nTotal subjects: {len(df)}")
print(f"Events: {df['event'].sum()} ({df['event'].sum()/len(df)*100:.1f}%)")
print(f"Censored: {(1-df['event']).sum()} ({(1-df['event']).sum()/len(df)*100:.1f}%)")
# 1. Kaplan-Meier Estimation
kmf = KaplanMeierFitter()
kmf.fit(df['time'], df['event'], label='Overall')
print("\n1. Kaplan-Meier Survival Estimates:")
print(f"Median survival time: {kmf.median_survival_time_:.1f} months")
print(f"6-month survival: {kmf.predict(6):.1%}")
print(f"12-month survival: {kmf.predict(12):.1%}")
print(f"24-month survival: {kmf.predict(24):.1%}")
# 2. Group Comparison
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Overall survival curve
ax = axes[0, 0]
kmf.plot_survival_function(ax=ax, linewidth=2)
ax.set_xlabel('Time (months)')
ax.set_ylabel('Survival Probability')
ax.set_title('Kaplan-Meier Survival Curve (Overall)')
ax.grid(True, alpha=0.3)
# Survival curves by group
ax = axes[0, 1]
for group_val in [0, 1]:
mask = df['group'] == group_val
kmf.fit(df[mask]['time'], df[mask]['event'],
label=f'{"Control" if group_val == 0 else "Treatment"}')
kmf.plot_survival_function(ax=ax, linewidth=2)
ax.set_xlabel('Time (months)')
ax.set_ylabel('Survival Probability')
ax.set_title('Kaplan-Meier Curves by Group')
ax.grid(True, alpha=0.3)
# 3. Log-Rank Test
mask_control = df['group'] == 0
mask_treatment = df['group'] == 1
results = logrank_test(
df[mask_control]['time'],
df[mask_treatment]['time'],
df[mask_control]['event'],
df[mask_treatment]['event']
)
print(f"\n3. Log-Rank Test:")
print(f"Test statistic: {results.test_statistic:.4f}")
print(f"P-value: {results.p_value:.4f}")
print(f"Significant: {'Yes' if results.p_value < 0.05 else 'No'}")
# 4. Risk Groups (by quartiles)
df['risk_quartile'] = pd.qcut(df['risk_score'], q=4, labels=['Low', 'Medium-Low', 'Medium-High', 'High'])
ax = axes[1, 0]
for risk_group in ['Low', 'Medium-Low', 'Medium-High', 'High']:
mask = df['risk_quartile'] == risk_group
kmf.fit(df[mask]['time'], df[mask]['event'], label=risk_group)
kmf.plot_survival_function(ax=ax, linewidth=2)
ax.set_xlabel('Time (months)')
ax.set_ylabel('Survival Probability')
ax.set_title('Kaplan-Meier Curves by Risk Quartile')
ax.legend()
ax.grid(True, alpha=0.3)
# 5. Cumulative Hazard
ax = axes[1, 1]
kmf.fit(df['time'], df['event'])
kmf.plot_cumulative_density(ax=ax, linewidth=2)
ax.set_xlabel('Time (months)')
ax.set_ylabel('Cumulative Event Probability')
ax.set_title('Cumulative Event Probability')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 6. Cox Proportional Hazards Model
cph = CoxPHFitter()
cph.fit(df[['time', 'event', 'group', 'age', 'risk_score']], duration_col='time', event_col='event')
print(f"\n6. Cox Proportional Hazards Model:")
print(cph.summary)
# Hazard ratios
print(f"\nHazard Ratios:")
for var in ['group', 'age', 'risk_score']:
hr = np.exp(cph.params_[var])
print(f" {var}: {hr:.3f}")
# 7. Model Diagnostics
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Partial effects plot
ax = axes[0, 0]
df_partial = df.copy()
df_partial['partial_hazard'] = cph.predict_partial_hazard(df_partial)
for group_val in [0, 1]:
mask = df_partial['group'] == group_val
ax.scatter(df_partial[mask]['risk_score'], df_partial[mask]['partial_hazard'],
alpha=0.6, label=f'{"Control" if group_val == 0 else "Treatment"}')
ax.set_xlabel('Risk Score')
ax.set_ylabel('Partial Hazard')
ax.set_title('Partial Hazard by Risk Score and Group')
ax.legend()
ax.grid(True, alpha=0.3)
# Concordance index over time
ax = axes[0, 1]
concordance_index = cph.concordance_index_
ax.text(0.5, 0.5, f'Concordance Index: {concordance_index:.3f}',
ha='center', va='center', fontsize=14,
bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.7))
ax.axis('off')
ax.set_title('Model Performance')
# Survival curves by predicted risk
ax = axes[1, 0]
df['predicted_hazard'] = cph.predict_partial_hazard(df)
df['hazard_quartile'] = pd.qcut(df['predicted_hazard'], q=4, labels=['Low', 'Medium-Low', 'Medium-High', 'High'])
for hazard_group in ['Low', 'Medium-Low', 'Medium-High', 'High']:
mask = df['hazard_quartile'] == hazard_group
kmf.fit(df[mask]['time'], df[mask]['event'], label=hazard_group)
kmf.plot_survival_function(ax=ax, linewidth=2)
ax.set_xlabel('Time (months)')
ax.set_ylabel('Survival Probability')
ax.set_title('Survival by Predicted Risk Quartile')
ax.grid(True, alpha=0.3)
# Variable importance
ax = axes[1, 1]
coef_df = cph.summary[['coef', 'exp(coef)']].copy()
coef_df = coef_df.sort_values('coef')
colors = ['red' if x < 0 else 'green' for x in coef_df['coef']]
ax.barh(coef_df.index, coef_df['coef'], color=colors, alpha=0.7, edgecolor='black')
ax.set_xlabel('Coefficient')
ax.set_title('Variable Coefficients')
ax.axvline(x=0, color='black', linestyle='-', linewidth=0.8)
ax.grid(True, alpha=0.3, axis='x')
plt.tight_layout()
plt.show()
# 8. Survival Prediction
new_patient = pd.DataFrame({
'group': [1],
'age': [65],
'risk_score': [75],
})
survival_prob = cph.predict_survival_function(new_patient, times=[6, 12, 24])
print(f"\n8. Survival Prediction for New Patient (age 65, treatment, risk 75):")
print(f"6-month survival: {survival_prob.iloc[0, 0]:.1%}")
print(f"12-month survival: {survival_prob.iloc[1, 0]:.1%}")
print(f"24-month survival: {survival_prob.iloc[2, 0]:.1%}")
# 9. Proportional Hazards Assumption
print(f"\n9. Proportional Hazards Test:")
from lifelines.statistics import proportional_hazard_assumption
ph_test = proportional_hazard_assumption(cph, df[['time', 'event', 'group', 'age', 'risk_score']],
time_transform='rank')
print(ph_test)
# 10. Summary Statistics
print(f"\n" + "="*50)
print("SURVIVAL ANALYSIS SUMMARY")
print("="*50)
print(f"Control median survival: {df[df['group']==0]['time'].median():.1f} months")
print(f"Treatment median survival: {df[df['group']==1]['time'].median():.1f} months")
print(f"Log-rank p-value: {results.p_value:.4f}")
print(f"Concordance index: {concordance_index:.3f}")
print("="*50)
Censoring Types
- Right censoring: Event hasn't occurred (most common)
- Left censoring: Event occurred before observation
- Interval censoring: Event in unknown time interval
Model Comparison
- Kaplan-Meier: Describes, doesn't explain
- Cox Model: Adjusts for covariates, proportional hazards
- Parametric: Assumes distribution
- Competing Risks: Multiple event types
Applications
- Clinical trials
- Equipment reliability
- Customer churn
- Employee retention
- Product lifetime
Deliverables
- Kaplan-Meier survival curves
- Survival probability estimates
- Log-rank test results
- Cox model coefficients
- Hazard ratios
- Risk stratification groups
- Survival predictions
- Model diagnostics
1---2name: survival-analysis3description: Analyze time-to-event data, calculate survival probabilities, and compare groups using Kaplan-Meier and Cox proportional hazards models4---5
6# Survival Analysis
7
8## Overview
9
10Survival analysis studies time until an event occurs, handling censored data where events haven't happened for some subjects, enabling prediction of lifetimes and risk assessment.
11
12## Key Concepts
13
14- **Survival Time**: Time until event
15- **Censoring**: Event not observed (subject dropped out)
16- **Hazard**: Instantaneous risk at time t
17- **Survival Curve**: Probability of surviving past time t
18- **Hazard Ratio**: Relative risk between groups
19
20## Common Models
21
22- **Kaplan-Meier**: Non-parametric survival curves
23- **Cox Proportional Hazards**: Semi-parametric regression
24- **Weibull/Exponential**: Parametric models
25- **Log-rank Test**: Comparing survival curves
26- **Competing Risks**: Multiple event types
27
28## Implementation with Python
29
30```python
31import pandas as pd
32import numpy as np
33import matplotlib.pyplot as plt
34import seaborn as sns
35from lifelines import KaplanMeierFitter, CoxPHFitter, WeibullAFTFitter
36from lifelines.statistics import logrank_test
37import warnings
38warnings.filterwarnings('ignore')
39
40# Generate sample survival data
41np.random.seed(42)
42n_patients = 200
43
44# Time to event (in months)
45event_times = np.random.exponential(scale=24, size=n_patients)
46# Censoring indicator (1 = event occurred, 0 = censored)
47event_observed = np.random.binomial(1, 0.7, n_patients)
48# Group assignment (0 = control, 1 = treatment)
49group = np.random.binomial(1, 0.5, n_patients)
50# Age at baseline
51age = np.random.uniform(30, 80, n_patients)
52# Risk score
53risk_score = np.random.uniform(0, 100, n_patients)
54
55# Adjust event times based on group (simulate treatment effect)
56event_times = event_times * (1 + group * 0.3)
57
58df = pd.DataFrame({
59 'time': event_times,
60 'event': event_observed,
61 'group': group,
62 'age': age,
63 'risk_score': risk_score,
64})
65
66print("Survival Data Summary:")
67print(df.head(10))
68print(f"\nTotal subjects: {len(df)}")
69print(f"Events: {df['event'].sum()} ({df['event'].sum()/len(df)*100:.1f}%)")
70print(f"Censored: {(1-df['event']).sum()} ({(1-df['event']).sum()/len(df)*100:.1f}%)")
71
72# 1. Kaplan-Meier Estimation
73kmf = KaplanMeierFitter()
74kmf.fit(df['time'], df['event'], label='Overall')
75
76print("\n1. Kaplan-Meier Survival Estimates:")
77print(f"Median survival time: {kmf.median_survival_time_:.1f} months")
78print(f"6-month survival: {kmf.predict(6):.1%}")
79print(f"12-month survival: {kmf.predict(12):.1%}")
80print(f"24-month survival: {kmf.predict(24):.1%}")
81
82# 2. Group Comparison
83fig, axes = plt.subplots(2, 2, figsize=(14, 10))
84
85# Overall survival curve
86ax = axes[0, 0]
87kmf.plot_survival_function(ax=ax, linewidth=2)
88ax.set_xlabel('Time (months)')
89ax.set_ylabel('Survival Probability')
90ax.set_title('Kaplan-Meier Survival Curve (Overall)')
91ax.grid(True, alpha=0.3)
92
93# Survival curves by group
94ax = axes[0, 1]
95for group_val in [0, 1]:
96 mask = df['group'] == group_val
97 kmf.fit(df[mask]['time'], df[mask]['event'],
98 label=f'{"Control" if group_val == 0 else "Treatment"}')
99 kmf.plot_survival_function(ax=ax, linewidth=2)
100
101ax.set_xlabel('Time (months)')
102ax.set_ylabel('Survival Probability')
103ax.set_title('Kaplan-Meier Curves by Group')
104ax.grid(True, alpha=0.3)
105
106# 3. Log-Rank Test
107mask_control = df['group'] == 0
108mask_treatment = df['group'] == 1
109
110results = logrank_test(
111 df[mask_control]['time'],
112 df[mask_treatment]['time'],
113 df[mask_control]['event'],
114 df[mask_treatment]['event']
115)
116
117print(f"\n3. Log-Rank Test:")
118print(f"Test statistic: {results.test_statistic:.4f}")
119print(f"P-value: {results.p_value:.4f}")
120print(f"Significant: {'Yes' if results.p_value < 0.05 else 'No'}")
121
122# 4. Risk Groups (by quartiles)
123df['risk_quartile'] = pd.qcut(df['risk_score'], q=4, labels=['Low', 'Medium-Low', 'Medium-High', 'High'])
124
125ax = axes[1, 0]
126for risk_group in ['Low', 'Medium-Low', 'Medium-High', 'High']:
127 mask = df['risk_quartile'] == risk_group
128 kmf.fit(df[mask]['time'], df[mask]['event'], label=risk_group)
129 kmf.plot_survival_function(ax=ax, linewidth=2)
130
131ax.set_xlabel('Time (months)')
132ax.set_ylabel('Survival Probability')
133ax.set_title('Kaplan-Meier Curves by Risk Quartile')
134ax.legend()
135ax.grid(True, alpha=0.3)
136
137# 5. Cumulative Hazard
138ax = axes[1, 1]
139kmf.fit(df['time'], df['event'])
140kmf.plot_cumulative_density(ax=ax, linewidth=2)
141ax.set_xlabel('Time (months)')
142ax.set_ylabel('Cumulative Event Probability')
143ax.set_title('Cumulative Event Probability')
144ax.grid(True, alpha=0.3)
145
146plt.tight_layout()
147plt.show()
148
149# 6. Cox Proportional Hazards Model
150cph = CoxPHFitter()
151cph.fit(df[['time', 'event', 'group', 'age', 'risk_score']], duration_col='time', event_col='event')
152
153print(f"\n6. Cox Proportional Hazards Model:")
154print(cph.summary)
155
156# Hazard ratios
157print(f"\nHazard Ratios:")
158for var in ['group', 'age', 'risk_score']:
159 hr = np.exp(cph.params_[var])
160 print(f" {var}: {hr:.3f}")
161
162# 7. Model Diagnostics
163fig, axes = plt.subplots(2, 2, figsize=(14, 10))
164
165# Partial effects plot
166ax = axes[0, 0]
167df_partial = df.copy()
168df_partial['partial_hazard'] = cph.predict_partial_hazard(df_partial)
169
170for group_val in [0, 1]:
171 mask = df_partial['group'] == group_val
172 ax.scatter(df_partial[mask]['risk_score'], df_partial[mask]['partial_hazard'],
173 alpha=0.6, label=f'{"Control" if group_val == 0 else "Treatment"}')
174
175ax.set_xlabel('Risk Score')
176ax.set_ylabel('Partial Hazard')
177ax.set_title('Partial Hazard by Risk Score and Group')
178ax.legend()
179ax.grid(True, alpha=0.3)
180
181# Concordance index over time
182ax = axes[0, 1]
183concordance_index = cph.concordance_index_
184ax.text(0.5, 0.5, f'Concordance Index: {concordance_index:.3f}',
185 ha='center', va='center', fontsize=14,
186 bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.7))
187ax.axis('off')
188ax.set_title('Model Performance')
189
190# Survival curves by predicted risk
191ax = axes[1, 0]
192df['predicted_hazard'] = cph.predict_partial_hazard(df)
193df['hazard_quartile'] = pd.qcut(df['predicted_hazard'], q=4, labels=['Low', 'Medium-Low', 'Medium-High', 'High'])
194
195for hazard_group in ['Low', 'Medium-Low', 'Medium-High', 'High']:
196 mask = df['hazard_quartile'] == hazard_group
197 kmf.fit(df[mask]['time'], df[mask]['event'], label=hazard_group)
198 kmf.plot_survival_function(ax=ax, linewidth=2)
199
200ax.set_xlabel('Time (months)')
201ax.set_ylabel('Survival Probability')
202ax.set_title('Survival by Predicted Risk Quartile')
203ax.grid(True, alpha=0.3)
204
205# Variable importance
206ax = axes[1, 1]
207coef_df = cph.summary[['coef', 'exp(coef)']].copy()
208coef_df = coef_df.sort_values('coef')
209
210colors = ['red' if x < 0 else 'green' for x in coef_df['coef']]
211ax.barh(coef_df.index, coef_df['coef'], color=colors, alpha=0.7, edgecolor='black')
212ax.set_xlabel('Coefficient')
213ax.set_title('Variable Coefficients')
214ax.axvline(x=0, color='black', linestyle='-', linewidth=0.8)
215ax.grid(True, alpha=0.3, axis='x')
216
217plt.tight_layout()
218plt.show()
219
220# 8. Survival Prediction
221new_patient = pd.DataFrame({
222 'group': [1],
223 'age': [65],
224 'risk_score': [75],
225})
226
227survival_prob = cph.predict_survival_function(new_patient, times=[6, 12, 24])
228print(f"\n8. Survival Prediction for New Patient (age 65, treatment, risk 75):")
229print(f"6-month survival: {survival_prob.iloc[0, 0]:.1%}")
230print(f"12-month survival: {survival_prob.iloc[1, 0]:.1%}")
231print(f"24-month survival: {survival_prob.iloc[2, 0]:.1%}")
232
233# 9. Proportional Hazards Assumption
234print(f"\n9. Proportional Hazards Test:")
235from lifelines.statistics import proportional_hazard_assumption
236
237ph_test = proportional_hazard_assumption(cph, df[['time', 'event', 'group', 'age', 'risk_score']],
238 time_transform='rank')
239print(ph_test)
240
241# 10. Summary Statistics
242print(f"\n" + "="*50)
243print("SURVIVAL ANALYSIS SUMMARY")
244print("="*50)
245print(f"Control median survival: {df[df['group']==0]['time'].median():.1f} months")
246print(f"Treatment median survival: {df[df['group']==1]['time'].median():.1f} months")
247print(f"Log-rank p-value: {results.p_value:.4f}")
248print(f"Concordance index: {concordance_index:.3f}")
249print("="*50)
250```
251
252## Censoring Types
253
254- **Right censoring**: Event hasn't occurred (most common)
255- **Left censoring**: Event occurred before observation
256- **Interval censoring**: Event in unknown time interval
257
258## Model Comparison
259
260- **Kaplan-Meier**: Describes, doesn't explain
261- **Cox Model**: Adjusts for covariates, proportional hazards
262- **Parametric**: Assumes distribution
263- **Competing Risks**: Multiple event types
264
265## Applications
266
267- Clinical trials
268- Equipment reliability
269- Customer churn
270- Employee retention
271- Product lifetime
272
273## Deliverables
274
275- Kaplan-Meier survival curves
276- Survival probability estimates
277- Log-rank test results
278- Cox model coefficients
279- Hazard ratios
280- Risk stratification groups
281- Survival predictions
282- Model diagnostics