Quick start

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.