Skip to content

feat(competing-risks): add FineGrayFitter -- Fine & Gray (1999) subdistribution hazard - #1689

Open
hass-nation wants to merge 2 commits into
CamDavidsonPilon:masterfrom
hass-nation:feat/fine-gray-competing-risks
Open

feat(competing-risks): add FineGrayFitter -- Fine & Gray (1999) subdistribution hazard#1689
hass-nation wants to merge 2 commits into
CamDavidsonPilon:masterfrom
hass-nation:feat/fine-gray-competing-risks

Conversation

@hass-nation

Copy link
Copy Markdown

Summary

Adds FineGrayFitter — the Fine & Gray (1999) proportional subdistribution hazard model for competing risks. This is the most widely-used regression approach for competing risks data, implemented in R as cmprsk::crr, but not yet available in lifelines.

Closes the long-standing feature request for competing-risks regression (issue #619, issue #539).

Why Fine-Gray?

The existing AalenJohansenFitter gives the non-parametric CIF estimate, but offers no way to model covariate effects. Using CoxPHFitter on competing-risks data (treating competing events as censored) over-estimates the CIF and gives coefficients on the wrong quantity. Fine-Gray directly models the subdistribution hazard, which maps one-to-one to the CIF.

Model

The subdistribution hazard for cause k:

h_k(t | x) = h_k0(t) * exp(x' beta)

Related to the CIF by:

F_k(t | x) = 1 - exp(-integral_0^t h_k0(s) ds * exp(x' beta))

Algorithm (Fine & Gray 1999):

  1. Estimate censoring survival G(t) = P(C > t) via Kaplan-Meier (inverting the event indicator)
  2. Build the modified risk set R_tilde(t) at each event time t:
    • All subjects with T_i >= t: weight = 1 (standard risk set)
    • Subjects with a competing event at T_i < t: re-enter with IPCW weight G(T_i)/G(t)
    • Censored subjects and subjects with event of interest at T_i < t: excluded
  3. Maximize the weighted partial log-likelihood via Newton-Raphson
  4. Breslow estimator for the baseline cumulative subdistribution hazard Lambda_0(t)
  5. Standard errors from the observed Fisher information (inverse Hessian)

New Public API

import pandas as pd
from lifelines import FineGrayFitter

df = pd.DataFrame({
    'T':  [1, 5, 3, 9, 7, 4, 6, 2, 8, 10],
    'E':  [1, 2, 1, 0, 1, 2, 0, 1, 2,  1],   # 0=censored, 1=event, 2=competing
    'age': [45, 60, 52, 38, 70, 55, 48, 62, 41, 58],
    'trt': [0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
})

fgf = FineGrayFitter()
fgf.fit(df, duration_col='T', event_col='E', event_of_interest=1)
fgf.print_summary()

Output:

         FineGrayFitter
event_of_interest = 1
duration_col = 'T'
event_col = 'E'
number_of_subjects = 10
number_of_events_of_interest = 5
log-likelihood = -8.43
partial_AIC = 20.86

         coef  exp(coef)  se(coef)       z        p  0.05 lower-bound  0.05 upper-bound
covariate
age      0.05       1.05      0.06    0.83    0.406           -0.07              0.17
trt      0.38       1.46      0.98    0.39    0.698           -1.54              2.30
# Predict CIF at specific times
fgf.predict_cumulative_incidence(df, times=[2.0, 5.0, 8.0])
# DataFrame of shape (3, 10): row = time, col = subject

# Plot CIF by treatment group
fgf.plot_partial_effects_on_outcome('trt', values=[0, 1])

# AIC / BIC
print(fgf.AIC_partial_, fgf.BIC_partial_)

# Baseline subdistribution hazard
fgf.baseline_cumulative_subdistribution_hazard_
fgf.baseline_cumulative_incidence_

Files added / changed

File Description
lifelines/fitters/fine_gray_fitter.py New FineGrayFitter class (~480 lines)
lifelines/tests/test_fine_gray.py 55 tests across 8 test classes
lifelines/__init__.py Added import and __all__ entry

Tests

55 tests, 0 failures

Test coverage:

  • Instantiation and repr
  • Fit validation (missing columns, negative durations, non-numeric covariates, unknown event code)
  • Attribute shapes (params_, variance_matrix_, confidence_intervals_, etc.)
  • Prediction shapes and bounds (CIF in [0,1], non-decreasing over time)
  • Statistical correctness: positive beta → positive coefficient, high-risk group has higher CIF
  • 95% CI coverage: 17/20 trials with n=300 (expected ~19, acceptable for small n)
  • Baseline CIF agreement with Aalen-Johansen (max deviation < 5.4% with no covariates)
  • Weights column acceptance
  • Multi-cause competing events (3+ event types)
  • Top-level import and __all__ membership

No new dependencies

Only NumPy, SciPy, and pandas — already required by lifelines. Uses the existing KaplanMeierFitter for censoring estimation.

References

  1. Fine, J. P. and Gray, R. J. (1999). A proportional hazards model for the subdistribution of a competing risk. Journal of the American Statistical Association, 94(446):496-509.

Generated with Claude Code

hass-nation and others added 2 commits June 27, 2026 22:38
…ion hazard model

Implements Fine-Gray (1999) competing-risks regression, the standard
model for estimating covariate effects on the cumulative incidence
function (CIF) when competing events are present.

New class FineGrayFitter:
- Two-step IPCW partial likelihood (Fine & Gray 1999)
- Modified risk set: competing-event subjects re-enter with
  IPCW weight G(Ti)/G(t), where G is the KM censoring survival
- Newton-Raphson optimisation with Hessian-based convergence check
- Breslow baseline subdistribution hazard estimator
- Predicted CIF: F(t|x) = 1 - exp(-exp(x'beta) * Lambda0(t))
- Standard errors from observed Fisher information
- predict_cumulative_incidence(), predict_partial_hazard()
- plot_partial_effects_on_outcome(), print_summary()
- AIC_partial_, BIC_partial_, log_likelihood_

Added to lifelines.__init__ and __all__.
55 tests across 8 test classes; 0 regressions on existing suite.

References
----------
Fine, J. P. and Gray, R. J. (1999). A proportional hazards model for
the subdistribution of a competing risk. Journal of the American
Statistical Association, 94(446):496-509.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs found during test run on current lifelines main:

1. Printer API changed: now takes positional List[Tuple] args, not kwargs.
   Updated print_summary() to use the current Printer(model, headers, footers,
   justify, header_kwargs, decimals, columns) signature.

2. Printer internally calls self.model.summary — added summary @Property
   returning the full coefficient table (coef, exp(coef), se(coef), z, p,
   CI bounds, cmp to, -log2(p)) matching the columns Printer expects.

3. test_equal_weights_same_as_no_weights was adding 'w' to both DataFrames
   before the unweighted fit, causing 'w' to appear as a covariate. Fixed
   by using separate df / df_w variables.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant