FineGray

Fine-Gray subdistribution hazard model for a competing-risks endpoint.

Usage

Source

FineGray(
    cause,
    *,
    conf_level=0.95,
)

The Fine-Gray model extends Cox regression to competing-risks settings where multiple event types (e.g., disease progression vs. death) can occur and only the first is observed. Rather than modeling the cause-specific hazard (as in standard Cox), it models the subdistribution hazard: the rate at which subjects experience the target cause as if competing events did not occur. This allows inference directly on the cumulative incidence function, making it ideal for policy-relevant questions like “what is the effect of treatment on my probability of experiencing event A?”

Technically, the Fine-Gray model uses a weighted Cox-like approach: subjects who experience a competing event remain in the risk set but with decreasing inverse-probability-of-censoring weights, reflecting their reduced ability to contribute information about the target cause. Call fit() with a multi-state Surv response (built with Surv.multistate()) and specify the target cause of interest. Coefficients, hazard ratios, and standard errors are computed via weighted partial likelihood, with robust (clustered) standard errors accounting for the weighting scheme.

The implementation automatically computes event weights and handles censoring. Standard errors use the Lin-Wei robust (sandwich) estimator, validated against R’s survival package. Unlike the Aalen-Johansen non-parametric approach, Fine-Gray provides covariate-adjusted estimates and supports prediction of cumulative incidence at specified times.

Parameters

cause: Any

The target cause-of-interest label from the multi-state Surv response.

conf_level: float = 0.95
Confidence level for coefficient 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_), accessible as arrays or exported to DataFrames.

Details

Call fit(surv, covariates) with a multi-state Surv response (built with Surv.multistate()) and specify the target cause. The model uses weighted Cox-like optimization with robust standard errors. Results can be tidy frames via to_frame() (optionally format=).

Examples

Using the same competing-risks setup as AalenJohansen, model the cumulative incidence of plasma-cell malignancy ("pcm") as a function of age and sex. The age and sex columns of the mgus2 frame form the covariate design. Printing the fitted object reports the subdistribution-hazard coefficient table.

import greenwood as gw

mg = gw.load_dataset("mgus2", backend="pandas")
etime = mg["ptime"].where(mg["pstat"] == 1, mg["futime"])
cause = mg["pstat"].where(mg["pstat"] == 1, 2 * mg["death"])
cr = gw.Surv.multistate(etime, event=cause, states=("pcm", "death"))
fg = gw.FineGray("pcm").fit(cr, mg[["age", "sex"]])
fg
FineGray (Fine-Gray subdistribution hazard model, cause='pcm')

         coef  exp(coef)  se(coef)       z         p
age   -0.0173     0.9828  0.005701  -3.035  0.002408
sexM  -0.2597     0.7713    0.1856  -1.399    0.1617

n = 1384, events = 115
Standard errors: robust (clustered)

Passing exponentiate=True to tidy reports the subdistribution hazard ratios (with their confidence limits) instead of the log-scale coefficients; pass format= to choose the backend (here, Polars):

gw.tidy(fg, exponentiate=True, format="polars")
shape: (2, 7)
termestimatestd_errorstatisticp_valueconf_lowconf_high
strf64f64f64f64f64f64
"age"0.9828480.005701-3.0347060.0024080.9719270.993892
"sexM"0.7712820.185579-1.3994070.1616910.5361031.109631

Methods

Name Description
fit() Fit the Fine-Gray subdistribution hazard model to competing-risks data.
to_frame() Return the Fine-Gray coefficient table as a DataFrame.

fit()

Fit the Fine-Gray subdistribution hazard model to competing-risks data.

Usage

Source

fit(surv, covariates, *, max_iter=30, tol=1e-09)

Fits a Cox-like regression model to the subdistribution hazard of a target cause in a competing-risks setting. Subjects who experience a competing event remain in the risk set with inverse-probability-of-censoring weights, allowing direct inference on cumulative incidence of the target cause. The model reports coefficients, hazard ratios, and clustered robust (Lin-Wei) standard errors.

Parameters
surv: Surv

A multi-state Surv response built with Surv.multistate(). Must have multiple causes-of-interest. Raises ValueError if a single-event response is passed.

covariates: Any

A dataframe (pandas or polars) or 2-D array of covariates to adjust for in the subdistribution hazard. An intercept is added automatically. Must have the same number of rows as surv.

max_iter: int = 30

Maximum number of Newton-Raphson iterations (default 30).

tol: float = 1e-09
Convergence tolerance for coefficient changes (default 1e-9).
Returns
FineGray
The fitted estimator object itself (for method chaining) with cached coefficient arrays (coef_, std_error_, hazard_ratio_, z_, p_value_), event counts, and model fit statistics.
Details

The Fine-Gray model estimates the subdistribution hazard \(h_j(t)\) using weighted partial likelihood. Weights are the inverse of the Kaplan-Meier estimate of the censoring distribution, computed just before each target event time. Subjects with competing events receive time-decreasing weights reflecting reduced ability to contribute information.

Coefficients are interpreted as log-hazard ratios on the subdistribution hazard scale, not the cause-specific hazard. The model directly targets cumulative incidence (\(F_j\)), making it ideal for policy-relevant questions about the probability of experiencing a specific cause.

Examples

Fit a Fine-Gray model on the competing-risks mgus2 dataset, modeling the cumulative incidence of plasma-cell malignancy (pcm) as a function of age and sex:

import greenwood as gw

mg = gw.load_dataset("mgus2", backend="pandas")
etime = mg["ptime"].where(mg["pstat"] == 1, mg["futime"])
cause = mg["pstat"].where(mg["pstat"] == 1, 2 * mg["death"])
cr = gw.Surv.multistate(etime, event=cause, states=("pcm", "death"))
fg = gw.FineGray("pcm").fit(cr, mg[["age", "sex"]])
fg
FineGray (Fine-Gray subdistribution hazard model, cause='pcm')

         coef  exp(coef)  se(coef)       z         p
age   -0.0173     0.9828  0.005701  -3.035  0.002408
sexM  -0.2597     0.7713    0.1856  -1.399    0.1617

n = 1384, events = 115
Standard errors: robust (clustered)

Extract hazard ratios with exponentiation for interpretation; pass format= to choose the backend (here, Polars):

gw.tidy(fg, exponentiate=True, format="polars")
shape: (2, 7)
termestimatestd_errorstatisticp_valueconf_lowconf_high
strf64f64f64f64f64f64
"age"0.9828480.005701-3.0347060.0024080.9719270.993892
"sexM"0.7712820.185579-1.3994070.1616910.5361031.109631

to_frame()

Return the Fine-Gray coefficient table as a DataFrame.

Usage

Source

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

Exports one row per term with coefficient estimates, robust standard errors, test statistics, p-values, and confidence limits. Set exponentiate=True to return subdistribution hazard ratios and exponentiated confidence limits.

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
Whether to return subdistribution hazard ratios instead of log-scale coefficients. Default False.
Returns
pandas.DataFrame, polars.DataFrame, or pyarrow.Table
A tidy table with columns term, estimate, std_error, statistic, p_value, conf_low, and conf_high.
Raises
ImportError
If the requested (or, when auto-detecting, any) DataFrame library is not installed.
Examples

Fit a Fine-Gray model on the competing-risks mgus2 data and export the coefficient table as a Polars frame:

import greenwood as gw

mg = gw.load_dataset("mgus2", backend="pandas")
etime = mg["ptime"].where(mg["pstat"] == 1, mg["futime"])
cause = mg["pstat"].where(mg["pstat"] == 1, 2 * mg["death"])
cr = gw.Surv.multistate(etime, event=cause, states=("pcm", "death"))
fg = gw.FineGray("pcm").fit(cr, mg[["age", "sex"]])
fg.to_frame(format="polars")
shape: (2, 7)
termestimatestd_errorstatisticp_valueconf_lowconf_high
strf64f64f64f64f64f64
"age"-0.0173010.005701-3.0347060.002408-0.028474-0.006127
"sexM"-0.2597010.185579-1.3994070.161691-0.623430.104028

To report subdistribution hazard ratios instead of coefficients:

fg.to_frame(format="polars", exponentiate=True)
shape: (2, 7)
termestimatestd_errorstatisticp_valueconf_lowconf_high
strf64f64f64f64f64f64
"age"0.9828480.005701-3.0347060.0024080.9719270.993892
"sexM"0.7712820.185579-1.3994070.1616910.5361031.109631