Skip to main content ホーム クリエイター xjtulyc awesome-rosetta-skills survival-analysis
survival-analysis Use this Skill for survival analysis: Kaplan-Meier estimator, log-rank test, Cox PH model (Schoenfeld residuals), AFT models, competing risks (Fine-Gray), and time-varying covariates with lifelines.
インストールへ移動 Skills Marketplace コミュニティが作成したAIスキルを発見・探索
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
直接コマンドでは確認用 Prompt が省略されます。実行前にソースを確認してください。
npx skills add https://github.com/xjtulyc/awesome-rosetta-skills --skill survival-analysisコマンドは1行のまま表示されます。コピー前に横へスクロールして全体を確認してください。
ローカルで確認しますか?SkillsMP が現在取得できるファイルをダウンロードできます。
Zipをダウンロード ダウンロード中... name survival-analysis description Use this Skill for survival analysis: Kaplan-Meier estimator, log-rank test, Cox PH model (Schoenfeld residuals), AFT models, competing risks (Fine-Gray), and time-varying covariates with lifelines.
tags ["mathematics","survival-analysis","Kaplan-Meier","Cox-regression","competing-risks","lifelines"] version 1.0.0 authors [{"name":"awesome-rosetta-skills contributors","github":"@xjtulyc"}] license MIT platforms ["claude-code","codex","gemini-cli","cursor"] dependencies {"python":["lifelines>=0.27","scikit-survival>=0.21","pandas>=1.5","matplotlib>=3.6","numpy>=1.23"]} last_updated 2026-03-17 status stable
Survival Analysis
TL;DR — Estimate time-to-event models: Kaplan-Meier curves with log-rank test,
Cox PH model with proportional-hazards diagnostics (Schoenfeld residuals),
Weibull AFT, and competing risks (Aalen-Johansen / Fine-Gray) using lifelines.
When to Use
Use this Skill when you need to:
Estimate and plot Kaplan-Meier survival curves for one or more groups.
Compare group survival with the log-rank test or Wilcoxon test.
Fit a Cox proportional hazards model and interpret hazard ratios.
Check the PH assumption with Schoenfeld residuals and log-log plots.
Model time to failure with a parametric AFT model (Weibull, Log-Normal, Log-Logistic).
Handle competing events with cause-specific cumulative incidence functions (CIF).
Include time-varying covariates using long-format data.
Task lifelines class Non-parametric survival KaplanMeierFitterCumulative hazard NelsonAalenFitterCox PH CoxPHFitterWeibull AFT WeibullAFTFitterCompeting risks CIF AalenJohansenFitterParametric hazard WeibullFitter, LogNormalFitter
Background & Key Concepts
Censoring
Right censoring (most common): event not observed before study end.
Left censoring : event known to have occurred before observation started.
Interval censoring : event occurred in a known interval but exact time unknown.
Standard lifelines functions handle right censoring; interval censoring requires
the interval_censoring_mode=True argument in some fitters.
Kaplan-Meier Estimator
Non-parametric maximum likelihood estimator of the survival function:
S(t) = prod_{t_i <= t} (1 - d_i / n_i)
where d_i = events at time t_i, n_i = at-risk count just before t_i.
Cox Proportional Hazards Model h(t | X) = h_0(t) * exp(beta^T X)
h_0(t) is an unspecified baseline hazard (semi-parametric).
exp(beta_j) is the hazard ratio for a one-unit increase in X_j.
PH assumption : covariate effect is constant over time (test with Schoenfeld residuals).
Competing Risks When multiple event types can occur, the cause-specific CIF is estimated with
the Aalen-Johansen estimator. Regression is via the Fine-Gray subdistribution hazard
model, available through lifelines or scikit-survival.
Environment Setup conda create -n survival python=3.11 -y
conda activate survival
pip install lifelines>=0.27 scikit-survival>=0.21 \
pandas>=1.5 matplotlib>=3.6 numpy>=1.23
python -c "import lifelines; print('lifelines', lifelines.__version__)"
Core Workflow
Step 1 — Kaplan-Meier Curves and Log-Rank Test import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from lifelines import KaplanMeierFitter, NelsonAalenFitter
from lifelines.statistics import logrank_test, multivariate_logrank_test
np.random.seed(42 )
def simulate_survival_data (
n: int = 200 ,
treatment_effect: float = 0.5 ,
max_follow_up: float = 60.0 ,
censoring_rate: float = 0.3 ,
) -> pd.DataFrame:
"""
Simulate exponential survival times for control and treatment arms.
Args:
n: Total number of subjects (split equally between arms).
treatment_effect: Hazard ratio for treatment vs control.
max_follow_up: Maximum follow-up time in months.
censoring_rate: Probability of random censoring.
Returns:
DataFrame with columns: subject_id, arm, duration, event_observed.
"""
half = n // 2
records = []
for arm_label, hazard in [("Control" , 0.05 ), ("Treatment" , 0.05 * treatment_effect)]:
n_arm = half
true_times = np.random.exponential(1 / hazard, n_arm)
censor_mask = np.random.rand(n_arm) < censoring_rate
censor_times = np.random.uniform(1 , max_follow_up, n_arm)
observed_times = np.where(censor_mask, np.minimum(true_times, censor_times), true_times)
observed_times = np.minimum(observed_times, max_follow_up)
event_observed = (~censor_mask) & (true_times <= max_follow_up)
for i in range (n_arm):
records.append({
"subject_id" : len (records),
"arm" : arm_label,
"duration" : float (observed_times[i]),
"event_observed" : bool (event_observed[i]),
"age" : np.random.randint(30 , 75 ),
"stage" : np.random.choice(["I" , "II" , "III" ], p=[0.3 , 0.4 , 0.3 ]),
})
return pd.DataFrame(records)
df = simulate_survival_data(n=400 , treatment_effect=0.55 )
print (f"Dataset shape : {df.shape} " )
print (df["arm" ].value_counts())
print (f"Event rate : {df['event_observed' ].mean():.2 %} " )
kmf_ctrl = KaplanMeierFitter(label="Control" )
kmf_trt = KaplanMeierFitter(label="Treatment" )
ctrl = df[df["arm" ] == "Control" ]
trt = df[df["arm" ] == "Treatment" ]
kmf_ctrl.fit(ctrl["duration" ], ctrl["event_observed" ])
kmf_trt.fit(trt["duration" ], trt["event_observed" ])
print (f"\nMedian survival (Control) : {kmf_ctrl.median_survival_time_:.1 f} months" )
print (f"Median survival (Treatment): {kmf_trt.median_survival_time_:.1 f} months" )
lr = logrank_test(
ctrl["duration" ], trt["duration" ],
ctrl["event_observed" ], trt["event_observed" ],
)
print (f"\nLog-rank test: chi2={lr.test_statistic:.4 f} p={lr.p_value:.6 f} " )
print (f"Reject H0 (equal survival): {lr.p_value < 0.05 } " )
fig, ax = plt.subplots(figsize=(8 , 5 ))
kmf_ctrl.plot_survival_function(ax=ax, ci_show=True )
kmf_trt.plot_survival_function(ax=ax, ci_show=True )
ax.set_xlabel("Time (months)" )
ax.set_ylabel("Survival probability" )
ax.set_title(f"Kaplan-Meier Curves\n(Log-rank p = {lr.p_value:.4 f} )" )
ax.set_ylim(0 , 1 )
ax.axhline(0.5 , color="grey" , linestyle="--" , alpha=0.5 , label="Median (50%)" )
ax.legend()
fig.tight_layout()
fig.savefig("km_curves.png" , dpi=150 )
print ("Saved km_curves.png" )
naf = NelsonAalenFitter(label="Control" )
naf.fit(ctrl["duration" ], ctrl["event_observed" ])
fig2, ax2 = plt.subplots(figsize=(7 , 4 ))
naf.plot_cumulative_hazard(ax=ax2)
ax2.set_title("Nelson-Aalen Cumulative Hazard — Control Arm" )
fig2.tight_layout()
fig2.savefig("nelson_aalen.png" , dpi=150 )
Step 2 — Cox PH Model and Schoenfeld Residual Test from lifelines import CoxPHFitter
from lifelines.statistics import proportional_hazard_test
df_model = df.copy()
df_model["stage_II" ] = (df_model["stage" ] == "II" ).astype(int )
df_model["stage_III" ] = (df_model["stage" ] == "III" ).astype(int )
df_model["arm_bin" ] = (df_model["arm" ] == "Treatment" ).astype(int )
covariates = ["arm_bin" , "age" , "stage_II" , "stage_III" ]
cph = CoxPHFitter(penalizer=0.01 )
cph.fit(
df_model[covariates + ["duration" , "event_observed" ]],
duration_col="duration" ,
event_col="event_observed" ,
)
cph.print_summary(decimals=4 )
hr_df = pd.DataFrame({
"HR" : np.exp(cph.params_),
"CI_low" : np.exp(cph.confidence_intervals_["95% lower-bound" ]),
"CI_hi" : np.exp(cph.confidence_intervals_["95% upper-bound" ]),
"p" : cph.summary["p" ],
})
print ("\nHazard Ratios:" )
print (hr_df.round (4 ))
ph_test = proportional_hazard_test(cph, df_model[covariates + ["duration" , "event_observed" ]],
time_transform="rank" )
print ("\nSchoenfeld PH test:" )
print (ph_test.summary[["test_statistic" , "p" , "conclusion" ]].to_string())
fig3, ax3 = plt.subplots(figsize=(7 , 4 ))
cph.plot(ax=ax3)
ax3.set_title("Cox PH Hazard Ratios (forest plot)" )
ax3.axvline(0 , color="grey" , linestyle="--" )
fig3.tight_layout()
fig3.savefig("cox_forest_plot.png" , dpi=150 )
print ("Saved cox_forest_plot.png" )
profiles = pd.DataFrame({
"arm_bin" : [0 , 1 ],
"age" : [55 , 55 ],
"stage_II" : [0 , 0 ],
"stage_III" : [0 , 0 ],
})
predicted_survival = cph.predict_survival_function(profiles)
fig4, ax4 = plt.subplots(figsize=(7 , 4 ))
for col in predicted_survival.columns:
label = "Control, age 55, stage I" if col == 0 else "Treatment, age 55, stage I"
ax4.plot(predicted_survival.index, predicted_survival[col], label=label)
ax4.set_xlabel("Time (months)" )
ax4.set_ylabel("Survival probability" )
ax4.set_title("Cox PH Predicted Survival Functions" )
ax4.legend()
fig4.tight_layout()
fig4.savefig("cox_predicted_survival.png" , dpi=150 )
Step 3 — Competing Risks with Aalen-Johansen from lifelines import AalenJohansenFitter
np.random.seed(7 )
n_cr = 500
arm_cr = np.random.choice([0 , 1 ], n_cr)
lambda_cancer = 0.04 - 0.02 * arm_cr
lambda_other = np.full(n_cr, 0.02 )
lambda_censor = np.full(n_cr, 0.03 )
t_cancer = np.random.exponential(1 / lambda_cancer)
t_other = np.random.exponential(1 / lambda_other)
t_censor = np.random.exponential(1 / lambda_censor)
observed_time = np.minimum.reduce([t_cancer, t_other, t_censor])
event_type = np.where(
observed_time == t_cancer, 1 ,
np.where(observed_time == t_other, 2 , 0 )
)
df_cr = pd.DataFrame({
"duration" : observed_time,
"event_type" : event_type,
"arm" : arm_cr,
})
print ("\nCompeting risks event distribution:" )
print (df_cr["event_type" ].value_counts().rename({0 : "Censored" , 1 : "Cancer" , 2 : "Other" }))
fig5, axes5 = plt.subplots(1 , 2 , figsize=(12 , 5 ))
for arm_val, arm_name, ax in zip ([0 , 1 ], ["Control" , "Treatment" ], axes5):
mask = df_cr["arm" ] == arm_val
sub = df_cr[mask]
ajf = AalenJohansenFitter(calculate_variance=True )
ajf.fit(sub["duration" ], sub["event_type" ], event_of_interest=1 )
ajf.plot_cumulative_density(ax=ax, label=f"{arm_name} (n={mask.sum ()} )" )
ax.set_xlabel("Time" )
ax.set_ylabel("Cumulative Incidence" )
ax.set_title(f"Cancer-specific CIF — {arm_name} " )
ax.set_ylim(0 , 0.6 )
fig5.suptitle("Competing Risks: Aalen-Johansen CIF" , y=1.02 )
fig5.tight_layout()
fig5.savefig("competing_risks_cif.png" , dpi=150 )
print ("Saved competing_risks_cif.png" )
from lifelines.statistics import aalen_johansen_multivariate_test
result_gray = aalen_johansen_multivariate_test(
df_cr["duration" ], df_cr["arm" ], df_cr["event_type" ], event_of_interest=1
)
print (f"\nGray's test for CIF equality: "
f"stat={result_gray.test_statistic:.4 f} p={result_gray.p_value:.4 f} " )
Advanced Usage
Weibull AFT Model from lifelines import WeibullAFTFitter
waf = WeibullAFTFitter()
waf.fit(
df_model[covariates + ["duration" , "event_observed" ]],
duration_col="duration" ,
event_col="event_observed" ,
)
waf.print_summary(decimals=4 )
print ("\nTime Ratios (Weibull AFT):" )
print (np.exp(waf.params_[waf.params_.index.get_level_values(0 ) == "lambda_" ]).round (4 ))
Time-Varying Covariates
def to_counting_process (df_wide: pd.DataFrame,
id_col: str = "subject_id" ,
duration_col: str = "duration" ,
event_col: str = "event_observed" ,
baseline_covariate: str = "age" ,
time_covariate_col: str = "biomarker" ) -> pd.DataFrame:
"""
Create a simple two-interval counting process data from wide format.
In practice, visit data drives the interval boundaries.
Returns:
Long-format DataFrame with columns: id, start, stop, event, covariates.
"""
rows = []
for _, row in df_wide.iterrows():
mid = row[duration_col] / 2
rows.append({
id_col : row[id_col],
"start" : 0.0 ,
"stop" : mid,
event_col : False ,
baseline_covariate: row[baseline_covariate],
time_covariate_col: np.random.normal(5 , 1 ),
})
rows.append({
id_col : row[id_col],
"start" : mid,
"stop" : row[duration_col],
event_col : row[event_col],
baseline_covariate: row[baseline_covariate],
time_covariate_col: np.random.normal(6 , 1.2 ),
})
return pd.DataFrame(rows)
df_long = to_counting_process(df[["subject_id" , "duration" , "event_observed" , "age" ]])
cph_tv = CoxPHFitter()
cph_tv.fit(
df_long[["start" , "stop" , "event_observed" , "age" , "biomarker" ]],
duration_col="stop" ,
event_col="event_observed" ,
entry_col="start" ,
)
cph_tv.print_summary(decimals=4 )
Troubleshooting Error / Symptom Cause Fix ConvergenceWarning: Newton-Raphson did not convergeNear-singular information matrix Add penalizer=0.1 to CoxPHFitter; check for collinearity Flat KM curve (S(t) = 1) No events in group Check event column encoding (True/False or 1/0); verify event is not all-censored Schoenfeld test p < 0.05 for a covariate PH assumption violated Stratify on that covariate or include a time-interaction term AalenJohansenFitter returns NaN CIFAll events are of a single type Ensure at least one event of each competing type exists in the subset WeibullAFTFitter log-likelihood = -infNegative duration values Filter out duration <= 0 rows Log-rank test p-value incorrect Tied event times not handled Use Wilcoxon weight by passing weightings='wilcoxon' to logrank_test
External Resources
Examples
Example 1 — KM Forest Plot by Stage from lifelines import KaplanMeierFitter
import matplotlib.pyplot as plt
stage_colors = {"I" : "#2ecc71" , "II" : "#f39c12" , "III" : "#e74c3c" }
fig, ax = plt.subplots(figsize=(9 , 5 ))
for stage_val, color in stage_colors.items():
subset = df[df["stage" ] == stage_val]
kmf_s = KaplanMeierFitter(label=f"Stage {stage_val} (n={len (subset)} )" )
kmf_s.fit(subset["duration" ], subset["event_observed" ])
kmf_s.plot_survival_function(ax=ax, ci_show=True , color=color)
ax.set_xlabel("Time (months)" )
ax.set_ylabel("Survival probability" )
ax.set_title("Kaplan-Meier by Disease Stage" )
ax.set_ylim(0 , 1 )
fig.tight_layout()
fig.savefig("km_by_stage.png" , dpi=150 )
print ("Saved km_by_stage.png" )
mlr = multivariate_logrank_test(df["duration" ], df["stage" ], df["event_observed" ])
print (f"Multi-group log-rank: p = {mlr.p_value:.4 f} " )
Example 2 — Sensitivity Check: Breslow vs Efron Tie Handling from lifelines import CoxPHFitter
import pandas as pd
for tie_method in ["breslow" , "efron" ]:
cph_tie = CoxPHFitter(baseline_estimation_method=tie_method)
cph_tie.fit(
df_model[["arm_bin" , "age" , "duration" , "event_observed" ]],
duration_col="duration" ,
event_col="event_observed" ,
)
print (f"{tie_method} : arm_bin HR = {np.exp(cph_tie.params_['arm_bin' ]):.4 f} , "
f"p = {cph_tie.summary.loc['arm_bin' , 'p' ]:.4 f} " )
Changelog Version Date Change 1.0.0 2026-03-17 Initial release — KM, log-rank, Cox PH, AFT, competing risks, time-varying covariates