Fine-Gray subdistribution hazard model for a competing-risks endpoint.
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)| term | estimate | std_error | statistic | p_value | conf_low | conf_high |
|---|
| str | f64 | f64 | f64 | f64 | f64 | f64 |
| "age" | 0.982848 | 0.005701 | -3.034706 | 0.002408 | 0.971927 | 0.993892 |
| "sexM" | 0.771282 | 0.185579 | -1.399407 | 0.161691 | 0.536103 | 1.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.
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)| term | estimate | std_error | statistic | p_value | conf_low | conf_high |
|---|
| str | f64 | f64 | f64 | f64 | f64 | f64 |
| "age" | 0.982848 | 0.005701 | -3.034706 | 0.002408 | 0.971927 | 0.993892 |
| "sexM" | 0.771282 | 0.185579 | -1.399407 | 0.161691 | 0.536103 | 1.109631 |
to_frame()
Return the Fine-Gray coefficient table as a DataFrame.
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)| term | estimate | std_error | statistic | p_value | conf_low | conf_high |
|---|
| str | f64 | f64 | f64 | f64 | f64 | f64 |
| "age" | -0.017301 | 0.005701 | -3.034706 | 0.002408 | -0.028474 | -0.006127 |
| "sexM" | -0.259701 | 0.185579 | -1.399407 | 0.161691 | -0.62343 | 0.104028 |
To report subdistribution hazard ratios instead of coefficients:
fg.to_frame(format="polars", exponentiate=True)
shape: (2, 7)| term | estimate | std_error | statistic | p_value | conf_low | conf_high |
|---|
| str | f64 | f64 | f64 | f64 | f64 | f64 |
| "age" | 0.982848 | 0.005701 | -3.034706 | 0.002408 | 0.971927 | 0.993892 |
| "sexM" | 0.771282 | 0.185579 | -1.399407 | 0.161691 | 0.536103 | 1.109631 |