CoxPH

Cox proportional hazards model.

Usage

Source

CoxPH(
    *,
    ties="efron",
    conf_level=0.95,
)

The Cox proportional hazards model is the most widely used regression method for survival data. It models the hazard (instantaneous risk of an event) as a multiplicative function of covariates: h(t \mid x) = h_0(t) \exp(\beta^\top x). The model is semi-parametric: the baseline hazard h_0(t) is left unspecified (estimated non-parametrically), while covariate effects are estimated parametrically through the log-hazard-ratio coefficients \beta.

To use this model, call fit() with a right-censored or counting-process Surv response and a design matrix of covariates (2-D array or DataFrame). The model automatically handles stratification (via by= in fit), tied event times (via configurable tie-handling methods), and can compute predictions, baseline hazards, and diagnostic residuals. Results include coefficient estimates with confidence intervals, hazard ratios, standard errors, and global significance tests.

The implementation uses maximum partial likelihood to estimate coefficients. Variance estimates use the observed information matrix (Hessian). The model assumes proportional hazards: the ratio of hazards between two subjects remains constant over time. This can be checked using the cox_zph() method for formal tests or diagnostic plots.

Parameters

ties: str = "efron"

Tie-handling method: "efron" (default, as in R) or "breslow".

conf_level: float = 0.95
Confidence level for coefficient and hazard-ratio intervals (default 0.95).

Returns

Fitted estimator
Call fit() to produce a fitted estimator with cached results (coef_, hazard_ratio_, std_error_, z_, p_value_, conf_low_, conf_high_, concordance_, lr_stat_, df_), accessible as arrays or exported to DataFrames.

Details

Call fit(surv, covariates) with a Surv response and a design (a 2-D array or a dataframe of covariates). Rows with missing values are dropped (complete-case, as in R’s default na.omit). Results are exposed as arrays (coef_, std_error_, hazard_ratio_, …) and as tidy frames via to_frame() (optionally format=) and greenwood.tidy.

Examples

Build a Surv response from the bundled lung dataset and fit the model on age and sex. Printing the fitted object reports the coefficient table and global tests in the style of R’s summary.coxph.

import greenwood as gw

# Load data and build a right-censored response
lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))

# Fit a Cox model with age and sex as covariates
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])
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

Hazard ratios (and their confidence limits) come from tidy with exponentiate=True; pass format= to choose the backend (here, Polars):

# Extract hazard ratios as a tidy Polars DataFrame
gw.tidy(cox, exponentiate=True, format="polars")
shape: (2, 7)
termestimatestd_errorstatisticp_valueconf_lowconf_high
strf64f64f64f64f64f64
"age"1.0171910.0092231.8480780.0645910.9989691.035747
"sex"0.5985660.167458-3.064760.0021780.4310940.831099

Methods

Name Description
baseline_hazard() Return the baseline cumulative hazard and survival as a frame, optionally with CIs.
concordance() Harrell’s concordance index (C-statistic) of the fitted risk scores.
cox_zph() Test the proportional-hazards assumption (Grambsch-Therneau).
fit() Fit the model to a Surv response and a covariate design.
frailty_test() Likelihood-ratio test for shared-frailty variance equal to zero.
influence_diagnostics() Return a DataFrame of per-observation influence diagnostics.
predict() Predict from the fitted model.
predict_expectation() Predict the expected (restricted mean) survival time for each subject.
predict_median() Predict the median survival time for each subject.
predict_quantile() Predict survival-time quantiles for each subject.
residuals() Return diagnostic residuals from the fitted Cox model.
smooth_hr() Smooth non-linear hazard ratio curve for a continuous covariate.
to_frame() Return a tidy coefficient table as a DataFrame (one row per term).

baseline_hazard()

Return the baseline cumulative hazard and survival as a frame, optionally with CIs.

Usage

Source

baseline_hazard(
    *,
    ci=False,
    conf_type="log-log",
    format=None,
)

The baseline hazard represents the hazard rate for a reference subject with all covariates at their mean values. It is useful for understanding the underlying time-to-event distribution estimated by the model, and can be combined with individual covariate values to compute predicted survival probabilities for specific subjects.

In Cox proportional hazards models, the hazard for an individual is modeled as: h(t \mid x) = h_0(t) \exp(x^\top \beta), where h_0(t) is the baseline hazard. This method returns the estimated cumulative baseline hazard H_0(t) at each observed event time, evaluated using the Breslow estimator (non-parametric).

Parameters

ci: bool = False

If True, include confidence interval columns for cumulative hazard and survival. The default is False.

conf_type: str = "log-log"

Confidence interval transform (used only if ci=True):

  • "log-log" (default): Log-log transform. Recommended because bounds respect the constraint that survival S(t) \in (0, 1) and cumulative hazard H(t) > 0.
  • "plain": Wald bounds without transform. Simple but may produce invalid bounds (negative cumulative hazard or survival > 1).
format: str | None = None

Output format: None (default), "pandas", "polars", or "pyarrow".

  • None (default): Auto-detects and tries Polars first, falls back to Pandas, then PyArrow. Raises an error if no DataFrame library is installed.
  • "pandas": returns pandas.DataFrame.
  • "polars": returns polars.DataFrame.
  • "pyarrow": returns pyarrow.Table.

Returns

pandas.DataFrame, polars.DataFrame, or pyarrow.Table

A DataFrame with one row per event time containing:

  • time: Event times at which the baseline hazard is evaluated.
  • cumhaz: Cumulative baseline hazard H_0(t) at each time.
  • survival: Baseline survival probability S_0(t) = \exp(-H_0(t)).
  • cumhaz_lower, cumhaz_upper (if ci=True): Confidence bounds for cumulative hazard.
  • survival_lower, survival_upper (if ci=True): Confidence bounds for survival.
  • strata (if stratified): Stratum label, one baseline hazard per stratum.

Details

The baseline hazard is evaluated only at the event times in the training data. The cumulative hazard is non-decreasing by construction. For stratified models, each stratum has its own baseline hazard while coefficients are shared across strata, allowing different baseline risks for different groups.

The baseline survival S_0(t) is computed from the cumulative hazard using the relationship S_0(t) = \exp(-H_0(t)).

When ci=True, confidence intervals are computed using the log-log transform (the default), which ensures bounds remain valid (cumulative hazard > 0, survival ∈ (0,1)).

Examples

The baseline cumulative hazard (and the implied baseline survival) is reported at every event time. Pass format= to choose the backend (here, Polars):

import greenwood as gw

# Load data and fit a Cox model
lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])

# Export the baseline cumulative hazard as a Polars DataFrame
cox.baseline_hazard(format="polars")
shape: (186, 3)
timecumhazsurvival
f64f64f64
5.00.0029550.997049
11.00.0119060.988164
12.00.0149280.985183
13.00.0210290.97919
15.00.0241080.97618
840.01.8576790.156034
883.02.0130580.13358
965.02.0130580.13358
1010.02.0130580.13358
1022.02.0130580.13358

Add confidence intervals with ci=True:

# Include confidence intervals for the baseline hazard
cox.baseline_hazard(ci=True, format="polars")
shape: (186, 7)
timecumhazsurvivalcumhaz_lowercumhaz_uppersurvival_lowersurvival_upper
f64f64f64f64f64f64f64
5.00.0029550.9970490.0002890.0302550.9701980.999711
11.00.0119060.9881640.0024460.0579590.9436890.997557
12.00.0149280.9851830.0032570.0684180.933870.996748
13.00.0210290.979190.004940.0895260.9143650.995072
15.00.0241080.976180.0057970.1002570.9046050.99422
840.01.8576790.1560340.5272016.5458380.0014360.590255
883.02.0130580.133580.5675597.1400630.0007930.566908
965.02.0130580.133580.5675597.1400630.0007930.566908
1010.02.0130580.133580.5675597.1400630.0007930.566908
1022.02.0130580.133580.5675597.1400630.0007930.566908

The returned DataFrame shows the estimated hazard and survival trajectory for the reference population (covariates at their means). For stratified models, a separate baseline is provided for each stratum:

# Fit a stratified model and get per-stratum baselines
cox_stratified = gw.CoxPH().fit(y, lung[["age", "ph.ecog"]], strata=lung["sex"])
cox_stratified.baseline_hazard(ci=True, format="polars")
shape: (205, 8)
timecumhazsurvivalcumhaz_lowercumhaz_uppersurvival_lowersurvival_upperstrata
f64f64f64f64f64f64f64i64
11.00.0068050.9932180.0020140.0229950.9772670.9979881
12.00.0091120.9909290.0030610.0271220.9732420.9969431
13.00.013810.9862850.0052990.0359920.9646480.9947151
15.00.0161960.9839350.0064620.0405940.9602190.9935591
26.00.0185930.9815790.0076410.0452440.9557640.9923881
735.00.6733030.5100210.1792052.5297120.0796820.8359352
740.00.6733030.5100210.1792052.5297120.0796820.8359352
765.00.8007080.4490110.2491572.5732050.0762910.7794572
821.00.8007080.4490110.2491572.964960.0515630.8055452
965.00.8007080.4490110.2491573.470890.0310890.8313392

The baseline hazard can be combined with individual predictions to compute personalized survival curves (see predict(type="survival")).


concordance()

Harrell’s concordance index (C-statistic) of the fitted risk scores.

Usage

Source

concordance()

The concordance index measures how well the model’s predicted risk scores order subjects by their survival times. It ranges from 0 to 1, where 0.5 indicates predictions are no better than random (coin flip), and 1.0 indicates perfect discrimination (the model always assigns higher risk to subjects who die first).

Pairs of subjects are compared: a subject who experiences an event at time t is considered to have “failed before” another subject still under observation at t (including one censored exactly at t). If the model assigns higher risk to the subject who failed first, the pair is concordant. Ties in predicted risk are treated as half-concordant.

For stratified models, only within-stratum pairs are compared.

Returns

float

The concordance index, a value between 0 and 1. Typical interpretation:

  • 0.5: Random predictions.
  • 0.6-0.7: Acceptable discrimination.
  • 0.7-0.8: Excellent discrimination.
  • 0.8+: Outstanding discrimination.

Details

The concordance index is equivalent to the Area Under the Receiver Operating Characteristic curve (AUC) for binary classification problems. It is computed as the fraction of concordant pairs out of all comparable pairs.

Comparable pairs are those where:

  • One subject has an event (event=True) and exits at time t.
  • The other subject exits at time > t, OR exits at time = t with event=False (censored).

Tied event times within the same outcome (both events or both censored at the same time) are excluded from comparison.

Examples

Harrell’s C is returned as a single number between 0 and 1. A value of 0.5 means the model is not better than random guessing. A value of 1.0 means perfect discrimination:

import greenwood as gw

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])
cox.concordance()
0.6028530028979714

cox_zph()

Test the proportional-hazards assumption (Grambsch-Therneau).

Usage

Source

cox_zph(
    *,
    transform="identity",
    breaks=None,
)

The Cox model assumes that the hazard ratio between any two subjects is constant over time (proportional hazards). If this assumption is violated (for example, if a treatment effect diminishes over time) the Cox estimates may be biased. This test checks for violations by regressing scaled Schoenfeld residuals on time.

Large test statistics or small p-values (typically p < 0.05) suggest the proportional-hazards assumption is violated for that covariate. When violated, consider stratified analysis (separate baseline hazards per stratum), time-dependent covariates, or time-varying coefficients.

Parameters

transform: str = "identity"

Transformation to apply to time when computing the test. Options are:

  • "identity" (default): Use time as-is. Regression on raw time.
  • "log": Use log(time). Regression on log-transformed time.
  • "km": Use 1 - KM(t-), where KM is the marginal Kaplan-Meier estimate (ignoring covariates) evaluated just before each event time. This maps the time axis onto the probability scale.
  • "rank": Use the rank of each event time (with average ranks for ties). Equivalent to a rank-based correlation test.
breaks: list[float] | tuple[float, …] | None = None
Time points at which to split the follow-up into windows. When provided, the test is run within each resulting time interval in addition to the overall test, revealing where in time the proportional-hazards assumption breaks down. For example, breaks=[180, 365] produces three windows: (0, 180], (180, 365], and (365, inf]. Windows with fewer than 2 events are skipped.

Returns

ZPHResult
An object containing per-term test results (per_term dict) and a global test (global_test dict) across all covariates. Each includes chi-squared statistic, degrees of freedom, and p-value. When breaks is provided, the result also contains windows, a list of ZPHWindowResult objects with per-window test statistics. Access results via .to_frame() or .to_frame(detail="windows").

Details

The test uses scaled Schoenfeld residuals, which under the null hypothesis (proportional hazards) have a known asymptotic distribution. The test statistic is approximately chi-squared with 1 df for each term, and chi-squared with degrees of freedom equal to the number of terms for the global test.

Schoenfeld residuals are weighted by the variance-covariance matrix of the risk set at each event time. The regression accounts for the constraint that Schoenfeld residuals sum to zero.

Examples

The test returns a ZPHResult summarizing per-term and global p-values:

import greenwood as gw

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])

zph = cox.cox_zph()
zph
ZPHResult(transform='identity', age: p=0.7065, sex: p=0.0992, GLOBAL p=0.2425)

The full statistics are available as a tidy frame, one row per term plus a GLOBAL row. Pass format= to choose the backend (here, Polars):

zph.to_frame(format="polars")
shape: (3, 4)
termchisqdfp_value
strf64i64f64
"age"0.14174810.70655
"sex"2.71840110.099197
"GLOBAL"2.83339120.242514

Split the follow-up at day 180 and day 365 to test proportional hazards within each time window. This reveals whether PH violations are concentrated early or late:

zph_w = cox.cox_zph(breaks=[180, 365])
zph_w.to_frame(detail="windows", format="polars")
shape: (9, 6)
windown_eventstermchisqdfp_value
stri64strf64i64f64
"(0.0, 180]"63"age"6.89146910.008661
"(0.0, 180]"63"sex"0.00006510.99355
"(0.0, 180]"63"GLOBAL"6.96432120.030741
"(180, 365]"58"age"1.96656710.160813
"(180, 365]"58"sex"0.10242510.748938
"(180, 365]"58"GLOBAL"2.1225920.346007
"(365, inf]"44"age"0.07263710.787535
"(365, inf]"44"sex"1.1070610.292722
"(365, inf]"44"GLOBAL"1.20170420.548344

fit()

Fit the model to a Surv response and a covariate design.

Usage

Source

fit(
    surv,
    covariates,
    *,
    data=None,
    strata=None,
    robust=False,
    cluster=None,
    frailty=None,
    frailty_cluster=None,
    frailty_theta=0.5,
    frailty_max_iter=30,
    max_iter=30,
    tol=1e-09,
)

covariates is a dataframe or 2-D array, or a right-hand-side formula string (for example "age + sex + C(ph.ecog)") evaluated against data. strata gives per-stratum baseline hazards with shared coefficients. robust=True (or providing cluster ids) reports the Lin-Wei sandwich variance. The cluster option sums the score residuals within groups before forming the sandwich.

Parameters

surv: Surv

A Surv object representing the response (censoring type must be right-censored or counting-process).

covariates: Any

Covariate design, either a 2-D array, dataframe, or formula string.

data: Any = None

DataFrame to evaluate formula strings against (required if covariates= is a formula string).

strata: Any = None

Optional stratification variable, giving each stratum its own baseline hazard while sharing coefficients. Can be a 1-D array or series.

robust: bool = False

If True, report Lin-Wei sandwich variance (robust standard errors). Default is False.

cluster: Any = None

Optional cluster labels for grouped robust variance estimation. Sums score residuals within groups before forming the sandwich.

frailty: str | None = None

Optional shared frailty distribution. Supports "gamma" (multiplicative gamma random effect, closed-form EM) and "lognormal" (additive normal random effect on the log-hazard, fitted via penalized partial likelihood with Laplace approximation, analogous to coxme in R). Both require frailty_cluster= and ties="breslow".

frailty_cluster: Any = None

Cluster labels for shared frailty random effects (one frailty per cluster).

frailty_theta: float = 0.5

Initial value for the frailty variance parameter (must be > 0). For gamma, this is the variance of the multiplicative frailty. For log-normal, this is the initial sigma^2 (variance of the log-hazard random effect).

frailty_max_iter: int = 30

Maximum number of outer iterations for shared frailty fitting (EM steps for gamma, REML loops for log-normal).

max_iter: int = 30

Maximum number of iterations for the Newton-Raphson solver. The default is 30.

tol: float = 1e-09
Convergence tolerance for the optimization. The default is 1e-9.

Returns

CoxPH
Returns self with fitted attributes including coef_, std_error_, hazard_ratio_, z_, p_value_, and other model diagnostics.

Examples

Passing strata= gives each stratum its own baseline hazard while sharing the coefficients. Here we fit age and ph.ecog stratified by sex:

import greenwood as gw

# Load data and build a right-censored response
lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))

# Fit a stratified Cox model and export the coefficients
gw.CoxPH().fit(y, lung[["age", "ph.ecog"]], strata=lung["sex"]).to_frame(
    format="polars"
)
shape: (2, 7)
termestimatestd_errorstatisticp_valueconf_lowconf_high
strf64f64f64f64f64f64
"age"0.0105660.0092411.1433640.252887-0.0075470.028679
"ph.ecog"0.4624240.1147614.0294530.0000560.2374970.687352

The covariates argument also accepts a right-hand-side formula string (for example "age + sex + C(ph.ecog)"), and robust=True reports the Lin-Wei sandwich variance.


frailty_test()

Likelihood-ratio test for shared-frailty variance equal to zero.

Usage

Source

frailty_test()

Tests whether the frailty (random-effect) variance is significantly different from zero. A significant result indicates that there is unexplained heterogeneity across clusters (e.g., centres, families) beyond what the fixed-effect covariates capture.

For gamma frailty the LRT uses the profile marginal likelihood (integrating out the random effects analytically). For log-normal frailty it uses the Laplace approximation to the marginal likelihood compared against the regular Cox partial likelihood.

The model must have been fitted with frailty="gamma" or frailty="lognormal" for this test to be available.

Returns

dict
A dictionary with keys "theta" (estimated frailty variance), "lr_statistic" (likelihood-ratio test statistic), "df" (degrees of freedom, always 1), and "p_value".

Raises

ValueError
If the model was not fitted with a frailty term.

Examples

Fit a Cox model with a gamma frailty term grouped by institution, then test whether the frailty variance is significant:

import greenwood as gw

# Load data and build a right-censored response
lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))

# Fit a Cox model with shared gamma frailty by institution
cox = gw.CoxPH(ties="breslow").fit(
    y, covariates=lung[["age", "sex"]],
    frailty="gamma", frailty_cluster=lung["inst"],
)

# Test whether the frailty variance is significant
cox.frailty_test()
{'theta': 1.3779723598335543e-08,
 'lr_statistic': 0.00044152233749628067,
 'df': 1.0,
 'p_value': 0.49161786891235687}

influence_diagnostics()

Return a DataFrame of per-observation influence diagnostics.

Usage

Source

influence_diagnostics(
    *,
    format=None,
)

Combines leverage, martingale residuals, deviance residuals, dfbeta, dfbetas, and an approximate likelihood-displacement statistic into a single table for identifying influential observations.

The likelihood displacement for observation i is \text{ld}_i = \Delta\hat{\beta}_i^\top \, V^{-1} \, \Delta\hat{\beta}_i where \Delta\hat{\beta}_i is the dfbeta vector and V is the naive variance-covariance matrix.

Parameters

format: str | None = None
Output format: None (auto-detect), "pandas", "polars", or "pyarrow".

Returns

DataFrame
One row per observation with columns: leverage, martingale, deviance, dfbeta_<term>, dfbetas_<term>, and ld (likelihood displacement).

predict()

Predict from the fitted model.

Usage

Source

predict(
    newdata=None,
    *,
    type="lp",
    times=None,
    strata=None,
    conditional_after=None,
    ci=False,
    trajectory=None,
    format=None,
)

type is one of "lp" (centered linear predictor), "risk" (exp(lp)), or "survival". For "survival", returns a frame of survival probabilities at times (defaulting to the union of all event times), one column per row of newdata.

For stratified models, each subject must be assigned to a stratum. When newdata is provided, pass strata= with one label per row matching a stratum seen at fit time. When newdata is None, stratum assignments are taken from the fitted data.

conditional_after (a scalar or one value per subject) predicts survival conditional on having already survived to that time: the returned value at time t is P(T > t \mid T > c) = S(t) / S(c), and is 1 for t \le c.

With ci=True (survival only), the frame also carries _lower and _upper columns per subject: a pointwise confidence band from the cumulative-hazard standard error (the log transform used by R’s survfit), at the model’s conf_level.

trajectory predicts survival for a single subject whose covariates change over time. It is a DataFrame (any backend) with columns tstart, tstop, and one column per covariate in the fitted model. Each row defines a constant-covariate interval; the covariate values in interval k apply to (t_k^{\text{start}}, t_k^{\text{stop}}]. The survival at time t is accumulated as

S(t) = \exp\!\Bigl(-\sum_k r_k \cdot \bigl[H_0(\min(t,\, t_k^{\text{stop}})) - H_0(t_k^{\text{start}})\bigr]\Bigr)

where r_k = \exp(\hat{\beta}^\top x_k) and the sum runs over all intervals where t_k^{\text{start}} < t. For times beyond the last interval the last covariate value is carried forward (the baseline hazard keeps accumulating). Mutually exclusive with newdata; requires type="survival".

Parameters

newdata: Any = None

Covariate design for prediction. If None, predictions are made on the fitted data. Can be a 2-D array or dataframe. Mutually exclusive with trajectory.

type: str = "lp"

Type of prediction: "lp" (centered linear predictor, default), "risk" (exp of linear predictor), or "survival" (survival probability).

times: Any = None

Time points at which to compute survival probabilities (for type="survival"). Defaults to the union of all event times from the fitted model.

strata: Any = None

Stratum labels for new subjects (required when newdata= is provided and the model was fitted with strata=). Must have one label per row of newdata=, matching a stratum label seen at fit time. For trajectory=, a single stratum label for the subject. Ignored for non-stratified models.

conditional_after: Any = None

Optional scalar or per-subject time for conditional survival prediction. Computes P(T > t \mid T > c) where c is the conditional_after time.

ci: bool = False

If True (survival only), include confidence intervals (_lower and _upper columns). The default is False. Supported with conditional_after= and trajectory=.

trajectory: Any = None

DataFrame with columns tstart, tstop, and covariates, describing one subject’s time-varying covariate path. See the description above. Mutually exclusive with newdata; requires type="survival".

format: str | None = None

Output format (for type="survival" only): None (default), "pandas", "polars", or "pyarrow".

  • None (default): Auto-detects and tries Polars first, falls back to Pandas, then Pyarrow. Raises an error if no DataFrame library is installed.
  • "pandas": returns pandas.DataFrame.
  • "polars": returns polars.DataFrame.
  • "pyarrow": returns pyarrow.Table.

Returns

ndarray or DataFrame
For type="lp" or "risk", returns a 1-D array with one prediction per row. For type="survival", returns a DataFrame with rows for each time point and columns for each subject (named subject_1, subject_2, etc.), optionally with _lower and _upper columns for confidence intervals. With trajectory, returns a DataFrame with a single subject_1 column.

Examples

The default type="lp" returns the centered linear predictor as a NumPy array, one value per fitted subject:

import greenwood as gw

# Load data and fit a Cox model
lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])

# Predict the centered linear predictor for the first five subjects
cox.predict(type="lp")[:5]
array([0.3995047 , 0.2972327 , 0.09268872, 0.10973405, 0.16087005])

With type="survival" and newdata, the result is a frame of survival probabilities at the requested times, one column per new subject. Pass format= to choose the backend (here, Polars):

# Predict survival probabilities for three subjects at 180 and 365 days
cox.predict(
    lung[["age", "sex"]][:3], type="survival", times=[180, 365], format="polars"
)
shape: (2, 4)
timesubject_1subject_2subject_3
f64f64f64f64
180.00.622340.6517050.705421
365.00.2687570.3053760.380304

Passing ci=True adds pointwise confidence bands, and conditional_after= gives survival conditional on having already survived to a landmark time.

With trajectory, survival is computed along a covariate path for a single subject:

import pandas as pd

pbcseq = gw.load_dataset("pbcseq", backend="pandas")
base = (pbcseq.drop_duplicates("id")[["id", "futime", "status"]]
              .rename(columns={"futime": "time"}))
long = gw.split_episodes(
    base, pbcseq[["id", "day", "bili", "albumin", "protime"]],
    id="id", time="time", event="status", visit_time="day", format="pandas",
)
long = long.dropna(subset=["bili", "albumin", "protime"])
long["event_bin"] = (long["status"] == 2).astype(int)

y = gw.Surv.counting(long["tstart"], long["tstop"], long["event_bin"])
cox = gw.CoxPH().fit(y, long[["bili", "albumin", "protime"]])

# Subject 1's covariate path (two visits)
tvc_path = pd.DataFrame({
    "tstart":  [0.0, 192.0],
    "tstop":   [192.0, 400.0],
    "bili":    [14.5, 21.3],
    "albumin": [2.60, 2.94],
    "protime": [12.2, 11.2],
})
cox.predict(trajectory=tvc_path, times=[100, 200, 300, 400], format="pandas")
/tmp/ipykernel_3164/1120825321.py:14: UserWarning: Subjects in counting-process data have different start times, some much larger than 0. This may indicate that start/stop times are calendar time rather than subject-relative time. Each subject's timeline should begin at 0. If you have calendar dates, subtract each subject's entry date from their start/stop times before fitting.
  cox = gw.CoxPH().fit(y, long[["bili", "albumin", "protime"]])
time subject_1
0 100.0 0.851568
1 200.0 0.654596
2 300.0 0.562411
3 400.0 0.395678

predict_expectation()

Predict the expected (restricted mean) survival time for each subject.

Usage

Source

predict_expectation(
    newdata=None,
    *,
    tau,
    strata=None,
    ci=False,
    format=None,
)

The restricted mean survival time (RMST) up to horizon \tau is the area under the predicted survival curve: \mathrm{RMST}_i(\tau) = \int_0^{\tau} \hat{S}(t \mid x_i)\, dt. For the Cox model the survival curve is a step function, so the integral is a sum of rectangles.

When ci=True, confidence intervals are obtained by integrating the upper and lower pointwise survival bands (the log-transform method used by R’s survfit). The upper survival band yields the upper RMST bound and vice versa.

Parameters

newdata: Any = None

Covariate design for prediction. If None, predictions are made on the fitted data. Can be a 2-D array or DataFrame.

tau: float

The restriction time (time horizon). Must be positive.

strata: Any = None

Stratum labels for new subjects (required when newdata is provided and the model was fitted with strata=). One label per row of newdata, matching a stratum label seen at fit time.

ci: bool = False

If True, include confidence intervals in the result. Default is False.

format: str | None = None
Output format: None (auto-detect), "pandas", "polars", or "pyarrow".

Returns

DataFrame
A DataFrame with a tau column and columns subject_1, subject_2, etc. containing the RMST for each subject. With ci=True, additional subject_N_lower and subject_N_upper columns are included.

Examples

import greenwood as gw

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])

# Expected survival time up to one year for three subjects
cox.predict_expectation(lung[["age", "sex"]][:3], tau=365, format="polars")
shape: (1, 4)
tausubject_1subject_2subject_3
f64f64f64f64
365.0231.056509240.26317257.601844

With confidence intervals:

cox.predict_expectation(lung[["age", "sex"]][:3], tau=365, ci=True, format="polars")
shape: (1, 10)
tausubject_1subject_1_lowersubject_1_uppersubject_2subject_2_lowersubject_2_uppersubject_3subject_3_lowersubject_3_upper
f64f64f64f64f64f64f64f64f64f64
365.0231.056509203.429381264.560433240.26317217.079898267.05118257.601844234.60702283.921012

predict_median()

Predict the median survival time for each subject.

Usage

Source

predict_median(
    newdata=None,
    *,
    strata=None,
    ci=False,
    format=None,
)

Convenience wrapper around predict_quantile(p=0.5). See predict_quantile() for full documentation.

Parameters

newdata: Any = None

Covariate design for prediction. If None, predictions are made on the fitted data. Can be a 2-D array or DataFrame.

strata: Any = None

Stratum labels for new subjects (required when newdata is provided and the model was fitted with strata=). One label per row of newdata.

ci: bool = False

If True, include confidence intervals. Default is False.

format: str | None = None
Output format: None (auto-detect), "pandas", "polars", or "pyarrow".

Returns

DataFrame
A single-row DataFrame with columns subject_1, subject_2, etc. containing the median survival time for each subject. With ci=True, additional subject_N_lower and subject_N_upper columns are included.

Examples

import greenwood as gw

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])

cox.predict_median(lung[["age", "sex"]][:3], format="polars")
shape: (1, 4)
psubject_1subject_2subject_3
f64f64f64f64
0.5230.0268.0301.0

With confidence intervals:

cox.predict_median(lung[["age", "sex"]][:3], ci=True, format="polars")
shape: (1, 10)
psubject_1subject_1_lowersubject_1_uppersubject_2subject_2_lowersubject_2_uppersubject_3subject_3_lowersubject_3_upper
f64f64f64f64f64f64f64f64f64f64
0.5230.0189.0305.0268.0208.0310.0301.0245.0363.0

predict_quantile()

Predict survival-time quantiles for each subject.

Usage

Source

predict_quantile(
    newdata=None,
    *,
    p=0.5,
    strata=None,
    ci=False,
    format=None,
)

The quantile at failure probability p is the earliest time t at which the predicted survival curve drops to or below 1 - p: \hat{t}_p = \inf\{t : \hat{S}(t \mid x) \le 1 - p\}.

When ci=True, confidence intervals for each quantile are obtained by inverting the pointwise confidence bands of the survival curve (the log-transform method used by R’s survfit). The lower bound of the quantile is the time at which the lower survival band first crosses 1 - p, and the upper bound is the time at which the upper survival band first crosses 1 - p.

If the survival curve for a subject never reaches 1 - p, the quantile (or the corresponding CI bound) is NaN.

Parameters

newdata: Any = None

Covariate design for prediction. If None, predictions are made on the fitted data. Can be a 2-D array or DataFrame.

p: Any = 0.5

Failure probability or probabilities at which to compute quantiles. Can be a scalar (e.g., 0.5 for median) or array-like (e.g., [0.25, 0.5, 0.75] for quartiles). Must be in (0, 1). Default is 0.5.

strata: Any = None

Stratum labels for new subjects (required when newdata is provided and the model was fitted with strata=). One label per row of newdata, matching a stratum label seen at fit time.

ci: bool = False

If True, include confidence intervals in the result. Default is False.

format: str | None = None
Output format: None (auto-detect), "pandas", "polars", or "pyarrow".

Returns

DataFrame
A DataFrame with a p column listing the failure probabilities, and columns subject_1, subject_2, etc. containing the corresponding quantile for each subject. With ci=True, additional subject_N_lower and subject_N_upper columns are included.

Examples

import greenwood as gw

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])

# Predicted survival-time quartiles for three subjects
cox.predict_quantile(lung[["age", "sex"]][:3], p=[0.25, 0.5, 0.75], format="polars")
shape: (3, 4)
psubject_1subject_2subject_3
f64f64f64f64
0.25132.0145.0166.0
0.5230.0268.0301.0
0.75390.0433.0520.0

With confidence intervals:

cox.predict_quantile(
    lung[["age", "sex"]][:3], p=0.5, ci=True, format="polars"
)
shape: (1, 10)
psubject_1subject_1_lowersubject_1_uppersubject_2subject_2_lowersubject_2_uppersubject_3subject_3_lowersubject_3_upper
f64f64f64f64f64f64f64f64f64f64
0.5230.0189.0305.0268.0208.0310.0301.0245.0363.0

residuals()

Return diagnostic residuals from the fitted Cox model.

Usage

Source

residuals(
    type="martingale",
    *,
    format=None,
)

Parameters

type: str = "martingale"

Type of residuals to return:

  • "martingale" (default): One per observation, range (-\infty, 1]. Positive values indicate underestimated risk.
  • "deviance": Normalized martingale residuals, more symmetrically distributed. Useful for detecting outliers.
  • "score": Efficient score residuals, one row per observation and one column per covariate. Used to construct the robust (sandwich) variance estimator.
  • "schoenfeld": One row per event, one column per covariate. Useful for checking the proportional-hazards assumption.
  • "scaledsch": Scaled Schoenfeld residuals (Grambsch-Therneau), one row per event. Under the PH assumption, the expected value at each event time equals the true coefficient.
  • "dfbeta": Approximate change in each coefficient if observation i were deleted. One row per observation, one column per covariate.
  • "dfbetas": Standardized dfbeta (dfbeta divided by the coefficient SE). Comparable across covariates on different scales.
  • "leverage": Hat-matrix diagonal. Measures each observation’s influence on the fitted values. Values near 0 have little leverage; values close to 1 dominate the fit. One value per observation.
format: str | None = None
Output format for multi-column residual types: None (auto-detect), "pandas", "polars", or "pyarrow". Returns a numpy array for "martingale", "deviance", and "leverage".

Returns

ndarray or DataFrame
For "martingale", "deviance", and "leverage": a 1-D array (one value per observation). For all other types: a DataFrame with one column per covariate.

Examples

import greenwood as gw

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])

cox.residuals("martingale")[:5]
array([ 0.00438999, -0.50576203, -3.12981924,  0.53275397, -2.35065745])
cox.residuals("dfbeta", format="polars")
shape: (228, 2)
agesex
f64f64
-0.000007-0.001313
-0.0001380.004039
0.0019160.028972
-0.00032-0.005218
0.0005010.024671
-0.0006180.003966
0.0006140.003063
-0.000138-0.002783
-0.0000710.002948
0.000082-0.003918

smooth_hr()

Smooth non-linear hazard ratio curve for a continuous covariate.

Usage

Source

smooth_hr(
    term,
    *,
    df=4,
    n_grid=200,
    reference=None,
)

Refits the Cox model replacing the linear term for term with a B-spline basis expansion, then computes the log-hazard ratio (and confidence band) across the covariate’s range relative to a reference value. The result reveals non-linear covariate effects that a single coefficient cannot capture.

Parameters

term: str

Name of the covariate to expand. Must be one of the fitted model’s term_names_.

df: int = 4

Degrees of freedom for the spline (number of basis functions). Defaults to 4, which gives a cubic spline with one interior knot. Higher values allow more flexible curves but risk overfitting.

n_grid: int = 200

Number of equally spaced points at which to evaluate the curve (default 200).

reference: float | None = None
Reference value for the covariate. The log-HR is zero at this point. Defaults to the weighted mean of the covariate in the training data.

Returns

SmoothHRResult
Contains the evaluation grid, log-HR, HR, and pointwise confidence bands. Use to_frame() for a tidy DataFrame or pass the result to plot_smooth_hr().

Examples

import greenwood as gw

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])

shr = cox.smooth_hr("age")
shr.to_frame(format="polars")
shape: (200, 4)
agelog_hrlog_hr_lowerlog_hr_upper
f64f64f64f64
39.0-1.250662-3.0872270.585903
39.21608-1.199427-2.9660870.567234
39.432161-1.149428-2.8433410.544484
39.648241-1.100653-2.720690.519383
39.864322-1.053087-2.6070680.500893
81.1356780.970678-0.1780322.119388
81.3517590.993666-0.2085252.195857
81.5678391.016902-0.2497472.283551
81.783921.040385-0.2915682.372339
82.01.064115-0.3340542.462285

to_frame()

Return a tidy coefficient table as a DataFrame (one row per term).

Usage

Source

to_frame(
    *,
    format=None,
    exponentiate=False,
)

The table contains coefficient estimates, standard errors, test statistics, p-values, and confidence limits. If exponentiate=True, returns hazard ratios instead of log-hazards.

Parameters

format: str | None = None

Output format: None (default), "pandas", "polars", or "pyarrow". When None, a backend is auto-detected (Polars, then Pandas, then PyArrow).

exponentiate: bool = False
If True, return hazard ratios (exp of coefficients). Default is False.

Returns

pandas.DataFrame, polars.DataFrame, or pyarrow.Table
One row per term with columns: term, estimate, std_error, statistic, p_value, conf_low, conf_high.

Raises

ImportError
If the requested (or, when auto-detecting, any) DataFrame library is not installed.

Examples

Fit a Cox model and export the coefficient table as a Polars frame:

import greenwood as gw

# Load data and fit a Cox model
lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])

# Export the coefficient table as a Polars DataFrame
cox.to_frame(format="polars")
shape: (2, 7)
termestimatestd_errorstatisticp_valueconf_lowconf_high
strf64f64f64f64f64f64
"age"0.0170450.0092231.8480780.064591-0.0010320.035123
"sex"-0.5132190.167458-3.064760.002178-0.84143-0.185007

With exponentiate=True, estimates become hazard ratios:

# Export hazard ratios instead of log-hazard coefficients
cox.to_frame(format="polars", exponentiate=True)
shape: (2, 7)
termestimatestd_errorstatisticp_valueconf_lowconf_high
strf64f64f64f64f64f64
"age"1.0171910.0092231.8480780.0645910.9989691.035747
"sex"0.5985660.167458-3.064760.0021780.4310940.831099