This is a short tour that goes from raw data to a survival curve, a plot, and a fitted model. Each step is only a few lines. The rest of the guide explains every piece in depth, and links are collected at the end.
Represent the data
Survival data pairs a follow-up time with an indicator of whether the event was actually observed or the subject was censored (still event-free when we last saw them). Greenwood keeps the two together in a Surv object. We use the bundled lung dataset, the survival times of patients with advanced lung cancer.
# Import the Greenwood library
import greenwood as gw
# Load the bundled lung cancer dataset as a Polars DataFrame
lung = gw.load_dataset("lung", backend="polars")
# Build a right-censored response; status == 2 marks a death
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
y
Surv(type=right, n=228, events=165)
The summary reports 228 patients, of whom 165 died during the study (the rest were censored). One detail to note: this dataset codes status as 1 for censored and 2 for dead, so we turn it into a plain event indicator with status == 2.
Estimate the survival curve
The Kaplan-Meier estimator turns those times into a survival curve, the probability of surviving past each point in time. Printing the fitted estimator reports the median survival.
# Load data and build the right-censored response
lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
# Fit the Kaplan-Meier estimator
km = gw.KaplanMeier().fit(y)
# Print a summary: median survival with its confidence interval
km
KaplanMeier (Kaplan-Meier survival estimate)
n events median 0.95LCL 0.95UCL
228 165 310 285 363
The median is 310 days: half of the patients are still alive at 310 days. The two columns after it give a 95% confidence interval for that median, 285 to 363 days.
Plot the curve
The plot_survival() function draws the curve as an interactive chart, with a shaded confidence band and a notch wherever a patient was censored.
# Load data and build the right-censored response
lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
# Fit the Kaplan-Meier estimator
km = gw.KaplanMeier().fit(y)
# Draw an interactive survival curve with confidence band and censoring marks
gw.plot_survival(km)
The curve starts at 1.0 and steps down at each death. It does not reach 0 because some patients were still alive at the end of follow-up, so their survival time is only known to be at least that long.
Model a covariate effect
To ask how a characteristic changes the risk of death, fit a Cox proportional hazards model. Here we include age and sex. Printing the model shows a coefficient table.
# Load data and build the right-censored response
lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
# Fit a Cox proportional hazards model with age and sex as covariates
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])
# Print the coefficient table and global test statistics
cox
CoxPH (Cox proportional hazards model, ties='efron')
coef exp(coef) se(coef) z p
age 0.01705 1.017 0.009223 1.848 0.06459
sex -0.5132 0.5986 0.1675 -3.065 0.002178
n = 228, events = 165
Likelihood ratio test = 14.12 on 2 df, p = 0.0008574
The exp(coef) column holds hazard ratios. For sex (coded 1 for male, 2 for female) the hazard ratio is about 0.60, which means women had roughly 40% lower risk of death than men over the study, holding age fixed. A ratio below 1 is lower risk, above 1 is higher risk.
Shared frailty for clustered survival data
If subjects are clustered (for example by center, family, or recurrent-event unit), you can make use of the shared gamma frailty Cox model.
# Shared frailty Cox model (clustered by institution)
cox_frailty = gw.CoxPH(ties="breslow").fit(
y,
lung[["age", "sex"]],
frailty="gamma",
frailty_cluster=lung["inst"],
)
# Frailty variance inference (tests theta = 0)
cox_frailty.frailty_test()
{'theta': 1.3779723598335543e-08,
'lr_statistic': 0.00043770764023065567,
'df': 1.0,
'p_value': 0.49165415243657906}
See Cox regression for full guidance and interpretation.
Next steps
That is the core loop: represent the data, estimate a curve, visualize it, and model an effect. Each part of the guide takes one of these steps further.