SurvivalTree

A survival decision tree grown with the log-rank splitting rule.

Usage

Source

SurvivalTree(
    *,
    max_depth=None,
    min_samples_split=6,
    min_samples_leaf=3,
    max_features=None,
    splitter="best",
    engine="numpy",
    random_state=None,
)

The tree recursively partitions subjects by covariate thresholds, at each step choosing the split that maximizes the two-sample log-rank statistic between the resulting child groups. Each leaf holds a Nelson-Aalen cumulative hazard and a Kaplan-Meier survival curve estimated from its training subjects, evaluated on the full training set’s event-time grid. Prediction routes a subject to a leaf and reads off that leaf’s curves; the scalar risk score is the summed cumulative hazard (ensemble mortality).

A single tree is a high-variance estimator and is most useful as an interpretable building block or the base learner of a RandomSurvivalForest.

Parameters

max_depth: int | None = None

Maximum depth of the tree. None grows until other stopping rules apply.

min_samples_split: int = 6

Minimum number of subjects a node must have to be eligible for splitting.

min_samples_leaf: int = 3

Minimum number of subjects required in each child of a split.

max_features: Any = None

Number of covariates considered at each split: "sqrt", "log2", an int, a float fraction, or None (all features). A random subset is drawn per node.

splitter: str = "best"

How thresholds are chosen: "best" (default) scans every candidate cut-point for the optimal log-rank split; "random" draws a single random threshold per candidate feature (the extremely-randomized-trees rule used by ExtraSurvivalTrees).

engine: str = "numpy"

Compute backend for the best-split search: "numpy" (default) uses the deterministic vectorized path; "numba" uses a compiled kernel (requires the fast extra) that is faster on large datasets; "auto" uses Numba when available. The Numba path produces a statistically equivalent fit but, because it accumulates the log-rank statistic in a different order, may differ from the NumPy path in tie-breaking.

random_state: Any = None
Seed or numpy.random.Generator controlling the per-node feature subsampling.

Examples

Grow a survival tree on the bundled lung dataset and predict a risk score:

import greenwood as gw

lung = gw.load_dataset("lung", backend="pandas").dropna(subset=["ph.ecog", "ph.karno"])

# Fit a survival tree and score the first five subjects
tree = gw.SurvivalTree(max_depth=3, random_state=0).fit(
    "Surv(time, status) ~ age + sex + ph.ecog + ph.karno + wt.loss", data=lung
)
tree.predict(lung)[:5]
array([34.84166667, 95.15345288, 95.15345288, 95.15345288, 95.15345288])

Methods

Name Description
fit() Fit the survival tree to a right-censored response and covariate design.
predict() Predict a risk score, survival curves, or cumulative-hazard curves.

fit()

Fit the survival tree to a right-censored response and covariate design.

Usage

Source

fit(
    surv,
    covariates=None,
    *,
    data=None,
    _rng=None,
    _event_times=None,
    _time=None,
    _event=None,
)

Parameters

surv: Surv | Outcome | str | None

A right-censored Surv response, built with gw.Surv(time=..., event=...). An Outcome or a formula string such as 'Surv(time, status == 2) ~ age + sex' is also accepted, with its columns read from data. The right-hand side sets covariates.

covariates: Any = None

A dataframe, a 2-D array, or a right-hand-side formula string evaluated against data. A list of column names in data also works.

data: Any = None
A data frame (pandas, Polars, PyArrow, DuckDB, a lazy frame, …) holding the columns named by the response and covariates. When surv is an Outcome or a formula, rows with a missing value in any column used are dropped before fitting. Covariate formula strings and lists of column names are also resolved here.

Returns

SurvivalTree
The fitted estimator (for method chaining), with cached attributes such as event_times_, n_features_in_, and feature_names_in_.

Examples

Grow a survival tree on the lung dataset with a formula interface. The formula specifies both the response and the covariates in a single string:

import greenwood as gw

lung = gw.load_dataset("lung", backend="pandas").dropna(
    subset=["ph.ecog", "ph.karno"]
)

tree = gw.SurvivalTree(max_depth=3, random_state=0).fit(
    "Surv(time, status == 2) ~ age + sex + ph.ecog + ph.karno",
    data=lung,
)
tree
SurvivalTree (log-rank splits, 15 nodes, 8 leaves)
n = 226, events = 163, features = 4

The same fit can be done by passing an Outcome and the covariate columns separately:

death = gw.Outcome.surv(time="time", event="status == 2")
tree2 = gw.SurvivalTree(max_depth=3, random_state=0).fit(
    death, covariates=["age", "sex", "ph.ecog", "ph.karno"], data=lung
)
tree2
SurvivalTree (log-rank splits, 15 nodes, 8 leaves)
n = 226, events = 163, features = 4

predict()

Predict a risk score, survival curves, or cumulative-hazard curves.

Usage

Source

predict(
    newdata=None,
    *,
    type="risk",
    times=None,
    format=None,
)

Three prediction types are available:

  1. type="risk" (default): Ishwaran’s ensemble mortality, the sum of the leaf cumulative hazard over the training event times. Higher values indicate higher risk. Returned as a 1-D NumPy array.

  2. type="survival": Kaplan-Meier survival probabilities S(t \mid x) at times, one column per subject. Returned as a DataFrame.

  3. type="cumulative_hazard": Nelson-Aalen cumulative hazard H(t \mid x) at times. Returned as a DataFrame.

Parameters

newdata: Any = None

Covariates to predict for. None predicts for the training subjects. A data frame may also hold other columns. The covariates are picked out by name and coded as they were at fit time, so the frame the model was fit on can be passed as is.

type: str = "risk"

One of "risk", "survival", or "cumulative_hazard".

times: Any = None

Times at which to evaluate survival / cumulative hazard. Defaults to the training event times. Ignored for type="risk".

format: str | None = None
DataFrame backend for curve output: None, "pandas", "polars", or "pyarrow".

Returns

numpy.ndarray or DataFrame
A risk vector, or a curve frame with a time column and one column per subject.

Examples

Compute the ensemble-mortality risk score for each subject (a 1-D array where higher values indicate higher risk):

import greenwood as gw

lung = gw.load_dataset("lung", backend="pandas").dropna(
    subset=["ph.ecog", "ph.karno"]
)

tree = gw.SurvivalTree(max_depth=3, random_state=0).fit(
    "Surv(time, status) ~ age + sex + ph.ecog + ph.karno + wt.loss", data=lung
)
tree.predict(lung)[:5]
array([34.84166667, 95.15345288, 95.15345288, 95.15345288, 95.15345288])

Predict survival curves at specific time points. The result is a DataFrame with a time column and one column per subject:

tree.predict(lung, type="survival", times=[180, 365, 730])
shape: (3, 227)
timesubject_1subject_2subject_3subject_4subject_5subject_6subject_7subject_8subject_9subject_10subject_11subject_12subject_13subject_14subject_15subject_16subject_17subject_18subject_19subject_20subject_21subject_22subject_23subject_24subject_25subject_26subject_27subject_28subject_29subject_30subject_31subject_32subject_33subject_34subject_35subject_36…subject_190subject_191subject_192subject_193subject_194subject_195subject_196subject_197subject_198subject_199subject_200subject_201subject_202subject_203subject_204subject_205subject_206subject_207subject_208subject_209subject_210subject_211subject_212subject_213subject_214subject_215subject_216subject_217subject_218subject_219subject_220subject_221subject_222subject_223subject_224subject_225subject_226
f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64…f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64f64
180.00.8888890.7655260.7655260.7655260.7655260.7655260.7222220.7222220.7655260.3333330.7655260.8888890.8918920.7655260.7655260.7655260.8888890.7222220.8888890.7655260.8666670.7655260.7655260.7655260.8918920.7655260.2777780.7655260.2777780.8666670.2777780.2777780.7222220.2777780.8888890.888889…0.7655260.8888890.8888890.7655260.7655260.7655260.8666670.7655260.8666670.7655260.8918920.7655260.8666670.8666670.8666670.8918920.7222220.8888890.8918920.8666670.7655260.7655260.8888890.7655260.7655260.8666670.2777780.7222220.8918920.7655260.8666670.50.50.7655260.7222220.7655260.866667
365.00.7777780.4257390.4257390.4257390.4257390.4257390.0842590.0842590.4257390.00.4257390.7777780.6803750.4257390.4257390.4257390.7777780.0842590.7777780.4257390.5360220.4257390.4257390.4257390.6803750.4257390.0555560.4257390.0555560.5360220.0555560.0555560.0842590.0555560.7777780.777778…0.4257390.7777780.7777780.4257390.4257390.4257390.5360220.4257390.5360220.4257390.6803750.4257390.5360220.5360220.5360220.6803750.0842590.7777780.6803750.5360220.4257390.4257390.7777780.4257390.4257390.5360220.0555560.0842590.6803750.4257390.5360220.00.00.4257390.0842590.4257390.536022
730.00.2074070.1016910.1016910.1016910.1016910.1016910.00.00.1016910.00.1016910.2074070.3555810.1016910.1016910.1016910.2074070.00.2074070.1016910.00.1016910.1016910.1016910.3555810.1016910.00.1016910.00.00.00.00.00.00.2074070.207407…0.1016910.2074070.2074070.1016910.1016910.1016910.00.1016910.00.1016910.3555810.1016910.00.00.00.3555810.00.2074070.3555810.00.1016910.1016910.2074070.1016910.1016910.00.00.00.3555810.1016910.00.00.00.1016910.00.1016910.0