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. Since the data lives in a frame, we describe the response by its column names and let Greenwood read them from the frame.

# 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")

# Describe a right-censored response by column names, where a status of 2 marks a death
death = gw.Outcome.surv(time="time", event="status")

# Evaluate it against the frame to see the Surv it produces
death.bind(lung)
Surv(type=right, n=228, events=165)

The summary reports 228 patients, of whom 165 died during the study (the rest were censored). gw.Outcome.surv() takes the same arguments as R’s survival::Surv(), written as column names. One detail to note: this dataset codes status as 1 for censored and 2 for dead (R’s convention). As in R, a 1/2 coding is recognized and passes straight through. The frame can be Polars, Pandas, PyArrow, or DuckDB. When the values are already in hand as lists or arrays, gw.Surv() builds the response from them directly.

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.

# Fit the Kaplan-Meier estimator, reading the columns from the frame
km = gw.KaplanMeier().fit(death, data=lung)

# 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.

# Fit the Kaplan-Meier estimator
km = gw.KaplanMeier().fit(death, data=lung)

# 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, writing the model as an R-style formula. The response and the covariates are read from the same frame, and any row with a missing value in a column the model uses is dropped (the printout says how many). Printing the model shows a coefficient table.

# Fit a Cox proportional hazards model with age and sex as covariates
cox = gw.CoxPH().fit("Surv(time, status) ~ age + sex", data=lung)

# 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. As in R, a frailty() term in the formula names the cluster column.

# Shared frailty Cox model (clustered by institution)
cox_frailty = gw.CoxPH(ties="breslow").fit(
    "Surv(time, status) ~ age + sex + frailty(inst)", data=lung
)

# 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.