Skill Tracer
Skills › Scientific Agent Skills › scikit-survival › competing-risks.md

competing-risks.md

328 lines · skills/scikit-survival/references/competing-risks.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Competing risks and cumulative incidence
2
3Verified for scikit-survival 0.28.0 on 2026-10-01.
4

Competing risks and cumulative incidence

Verified for scikit-survival 0.28.0 on 2026-10-01.

Raw lines 1 to 4
5## Estimand
6
7Competing risks are mutually exclusive causes \(J \in \{1,\ldots,K\}\), where the
8first observed cause prevents observing the others as first events.
9
10The cause-\(k\) cumulative incidence function (CIF) is:
12\[
13F_k(t) = P(T \le t, J=k).
14\]
16It is an absolute cause-specific event probability accounting for all competing
17causes. It is not:
19- a cause-specific hazard;
20- `1 - Kaplan-Meier` after censoring other causes;
21- a conditional probability among only those still event-free;
22- a causal effect or clinical-utility measure.
24The total risk is \(\sum_k F_k(t)\). Its complement is estimated all-cause
25event-free survival. Censoring is an observation mechanism, not an additional
26event-free state.

Estimand

Competing risks are mutually exclusive causes \(J \in \{1,\ldots,K\}\), where the first observed cause prevents observing the others as first events.

The cause-\(k\) cumulative incidence function (CIF) is:

\[ F_k(t) = P(T \le t, J=k). \]

It is an absolute cause-specific event probability accounting for all competing causes. It is not:

  • a cause-specific hazard;
  • 1 - Kaplan-Meier after censoring other causes;
  • a conditional probability among only those still event-free;
  • a causal effect or clinical-utility measure.

The total risk is \(\sum_k F_k(t)\). Its complement is estimated all-cause event-free survival. Censoring is an observation mechanism, not an additional event-free state.

Raw lines 5 to 27
28## Event coding
30The nonparametric CIF API takes two separate arrays:
32```python
33# event: 0=censored; 1..K=mutually exclusive causes
34event = frame["status"].to_numpy(dtype=int)
35time = frame["time"].to_numpy(dtype=float)
36```
38Requirements:
40- `event` is integer and non-negative;
41- 0 always denotes right-censoring;
42- positive codes 1..K are contiguous;
43- the data contains observations for every code 1..K;
44- `time` is finite and positive;
45- event/time lengths match.
47Do not pass a boolean `Surv` outcome to
48`cumulative_incidence_competing_risks()`. `Surv` intentionally collapses event
49status to event versus censoring and loses cause identity.

Event coding

The nonparametric CIF API takes two separate arrays:

# event: 0=censored; 1..K=mutually exclusive causes
event = frame["status"].to_numpy(dtype=int)
time = frame["time"].to_numpy(dtype=float)

Requirements:

  • event is integer and non-negative;
  • 0 always denotes right-censoring;
  • positive codes 1..K are contiguous;
  • the data contains observations for every code 1..K;
  • time is finite and positive;
  • event/time lengths match.

Do not pass a boolean Surv outcome to cumulative_incidence_competing_risks(). Surv intentionally collapses event status to event versus censoring and loses cause identity.

Raw lines 28 to 50
51## Nonparametric CIF API
53```python
54from sksurv.nonparametric import cumulative_incidence_competing_risks
56time_points, cumulative_incidence = (
57 cumulative_incidence_competing_risks(event, time)
59```
61Current signature:
63```text
64cumulative_incidence_competing_risks(
65 event,
66 time_exit,
67 time_min=None,
68 conf_level=0.95,
69 conf_type=None,
70 var_type="Aalen",
72```
74Returns:
76- `time_points`: shape `(n_times,)`;
77- `cumulative_incidence`: shape `(K + 1, n_times)`;
78- row 0: total risk of any cause;
79- row `k`: CIF for cause `k`.
81```python
82total_risk = cumulative_incidence[0]
83cause_1 = cumulative_incidence[1]
84cause_2 = cumulative_incidence[2]
86assert np.allclose(
87 total_risk,
88 cumulative_incidence[1:].sum(axis=0),
90```
92`time_min` is documented as conditional survival. In released 0.28.0 it fails
93with a broadcasting error when it removes any unique times; the bundled helper
94rejects that setting. Do not claim a conditional/landmark analysis from this
95helper. A different verified method and explicit target population are needed.

Nonparametric CIF API

from sksurv.nonparametric import cumulative_incidence_competing_risks

time_points, cumulative_incidence = (
    cumulative_incidence_competing_risks(event, time)
)

Current signature:

cumulative_incidence_competing_risks(
    event,
    time_exit,
    time_min=None,
    conf_level=0.95,
    conf_type=None,
    var_type="Aalen",
)

Returns:

  • time_points: shape (n_times,);
  • cumulative_incidence: shape (K + 1, n_times);
  • row 0: total risk of any cause;
  • row k: CIF for cause k.
total_risk = cumulative_incidence[0]
cause_1 = cumulative_incidence[1]
cause_2 = cumulative_incidence[2]

assert np.allclose(
    total_risk,
    cumulative_incidence[1:].sum(axis=0),
)

time_min is documented as conditional survival. In released 0.28.0 it fails with a broadcasting error when it removes any unique times; the bundled helper rejects that setting. Do not claim a conditional/landmark analysis from this helper. A different verified method and explicit target population are needed.

Raw lines 51 to 96
97### Confidence intervals
99```python
100time_points, cumulative_incidence, confidence_interval = (
101 cumulative_incidence_competing_risks(
102 event,
103 time,
104 conf_type="log-log",
105 conf_level=0.95,
106 var_type="Aalen",
107 )
109```
111`confidence_interval` has shape `(K + 1, 2, n_times)`. Native 0.28.0 returns
112cause-specific rows in lower/upper order, but **total-risk row 0 is reversed and
113ignores `conf_level`**. Repair that row with the public all-event KM estimator:
115```python
116from sksurv.nonparametric import kaplan_meier_estimator
118km_time, _, km_ci = kaplan_meier_estimator(
119 event > 0, time, conf_type="log-log", conf_level=0.95,
121assert np.array_equal(km_time, time_points)
122confidence_interval[0] = 1.0 - km_ci[::-1]
123assert np.all(confidence_interval[:, 0] <= confidence_interval[:, 1])
124```
126The bundled helper applies this repair using the requested level and rejects
127non-finite or reversed intervals. This repairs the total-risk row, not general
128small-sample coverage.
129Current variance choices are:
131- `"Aalen"`
132- `"Dinse"`
133- `"Dinse_Approx"`
135Pointwise confidence intervals are not simultaneous confidence bands. Sparse
136causes and late follow-up can make estimates unstable even when the function
137returns a result.

Confidence intervals

time_points, cumulative_incidence, confidence_interval = (
    cumulative_incidence_competing_risks(
        event,
        time,
        conf_type="log-log",
        conf_level=0.95,
        var_type="Aalen",
    )
)

confidence_interval has shape (K + 1, 2, n_times). Native 0.28.0 returns cause-specific rows in lower/upper order, but **total-risk row 0 is reversed and ignores conf_level**. Repair that row with the public all-event KM estimator:

from sksurv.nonparametric import kaplan_meier_estimator

km_time, _, km_ci = kaplan_meier_estimator(
    event > 0, time, conf_type="log-log", conf_level=0.95,
)
assert np.array_equal(km_time, time_points)
confidence_interval[0] = 1.0 - km_ci[::-1]
assert np.all(confidence_interval[:, 0] <= confidence_interval[:, 1])

The bundled helper applies this repair using the requested level and rejects non-finite or reversed intervals. This repairs the total-risk row, not general small-sample coverage. Current variance choices are:

  • "Aalen"
  • "Dinse"
  • "Dinse_Approx"

Pointwise confidence intervals are not simultaneous confidence bands. Sparse causes and late follow-up can make estimates unstable even when the function returns a result.

Raw lines 97 to 138
139## Built-in competing-risk datasets
141```python
142from sksurv.datasets import load_bmt, load_cgvhd
144X_bmt, y_bmt = load_bmt() # status codes 0, 1, 2
145X_cgvhd, y_cgvhd = load_cgvhd() # status codes 0, 1, 2, 3
146```
148The first structured field is integer cause status, not boolean. These are real
149study datasets distributed for examples. The bundled tests do not use them; they
150use synthetic non-clinical outcomes only.

Built-in competing-risk datasets

from sksurv.datasets import load_bmt, load_cgvhd

X_bmt, y_bmt = load_bmt()       # status codes 0, 1, 2
X_cgvhd, y_cgvhd = load_cgvhd() # status codes 0, 1, 2, 3

The first structured field is integer cause status, not boolean. These are real study datasets distributed for examples. The bundled tests do not use them; they use synthetic non-clinical outcomes only.

Raw lines 139 to 151
152## Why `1 - Kaplan-Meier` is wrong for one cause
154If cause 2 prevents cause 1, censoring cause 2 in a Kaplan-Meier curve treats those
155subjects as if they could still experience cause 1 later under non-informative
156censoring. That counterfactual risk set does not estimate the observed-world
157probability \(F_1(t)\) and typically overstates cause-1 probability.
159Use CIF for cause-specific absolute probability:
161```python
162time_points, cif = cumulative_incidence_competing_risks(event, time)
163probability_cause_1_by_t = cif[1]
164```
166Kaplan-Meier remains appropriate for all-cause event-free survival after collapsing
167all causes to event, if that is the estimand and censoring assumptions hold.

Why 1 - Kaplan-Meier is wrong for one cause

If cause 2 prevents cause 1, censoring cause 2 in a Kaplan-Meier curve treats those subjects as if they could still experience cause 1 later under non-informative censoring. That counterfactual risk set does not estimate the observed-world probability \(F_1(t)\) and typically overstates cause-1 probability.

Use CIF for cause-specific absolute probability:

time_points, cif = cumulative_incidence_competing_risks(event, time)
probability_cause_1_by_t = cif[1]

Kaplan-Meier remains appropriate for all-cause event-free survival after collapsing all causes to event, if that is the estimand and censoring assumptions hold.

Raw lines 152 to 168
169## Comparing groups
171For prespecified groups where every original cause occurs (the native function
172requires contiguous observed causes), estimate point curves:
174```python
175curves = {}
176for label in prespecified_groups:
177 mask = group == label
178 curves[label] = cumulative_incidence_competing_risks(
179 event[mask],
180 time[mask],
181 )
182```
184If a subgroup has no event for a cause, do not silently relabel its rows or invent
185an event to satisfy the API. Preserve a global cause mapping and use a method
186that supports absent causes; an all-censored group needs separate handling.
188Plotting pointwise intervals does not test equality. scikit-survival 0.28 does not
189provide Gray's test in this API. Do not substitute an ordinary log-rank test:
190survival and CIF group hypotheses differ.
192Group labels and comparison times should be prespecified. Report at-risk/event
193support; late visual separation with few rows can be misleading.

Comparing groups

For prespecified groups where every original cause occurs (the native function requires contiguous observed causes), estimate point curves:

curves = {}
for label in prespecified_groups:
    mask = group == label
    curves[label] = cumulative_incidence_competing_risks(
        event[mask],
        time[mask],
    )

If a subgroup has no event for a cause, do not silently relabel its rows or invent an event to satisfy the API. Preserve a global cause mapping and use a method that supports absent causes; an all-censored group needs separate handling.

Plotting pointwise intervals does not test equality. scikit-survival 0.28 does not provide Gray's test in this API. Do not substitute an ordinary log-rank test: survival and CIF group hypotheses differ.

Group labels and comparison times should be prespecified. Report at-risk/event support; late visual separation with few rows can be misleading.

Raw lines 169 to 194
195## Cause-specific Cox hazards
197For cause \(k\), a cause-specific hazard model encodes that cause as an event and
198other causes as censored at their occurrence time:
200```python
201from sksurv.linear_model import CoxPHSurvivalAnalysis
202from sksurv.util import Surv
204y_cause_1 = Surv.from_arrays(
205 event=(event == 1),
206 time=time,
208cause_1_hazard_model = CoxPHSurvivalAnalysis(alpha=0.1)
209cause_1_hazard_model.fit(X_train, y_cause_1_train)
210```
212This estimates association with the instantaneous cause-specific hazard under a
213PH model. Other causes are censored for this hazard likelihood, which is different
214from pretending they are independent censoring when estimating absolute CIF.
216To derive cause-specific CIF predictions from cause-specific hazards, all modeled
217causes must be combined:
219\[
220F_k(t \mid x) =
221\int_0^t S(u^- \mid x)\,dH_k(u \mid x),
222\quad
223S(t \mid x)=\exp\left[-\sum_j H_j(t \mid x)\right].
224\]
226Therefore, `1 - cause_1_model.predict_survival_function(...)` is not the cause-1
227CIF. A set of separately fitted cause-specific models requires careful joint
228integration, common time grids, and external validation.

Cause-specific Cox hazards

For cause \(k\), a cause-specific hazard model encodes that cause as an event and other causes as censored at their occurrence time:

from sksurv.linear_model import CoxPHSurvivalAnalysis
from sksurv.util import Surv

y_cause_1 = Surv.from_arrays(
    event=(event == 1),
    time=time,
)
cause_1_hazard_model = CoxPHSurvivalAnalysis(alpha=0.1)
cause_1_hazard_model.fit(X_train, y_cause_1_train)

This estimates association with the instantaneous cause-specific hazard under a PH model. Other causes are censored for this hazard likelihood, which is different from pretending they are independent censoring when estimating absolute CIF.

To derive cause-specific CIF predictions from cause-specific hazards, all modeled causes must be combined:

\[ F_k(t \mid x) = \int_0^t S(u^- \mid x)\,dH_k(u \mid x), \quad S(t \mid x)=\exp\left[-\sum_j H_j(t \mid x)\right]. \]

Therefore, 1 - cause_1_model.predict_survival_function(...) is not the cause-1 CIF. A set of separately fitted cause-specific models requires careful joint integration, common time grids, and external validation.

Raw lines 195 to 229
230## Fine-Gray regression
232scikit-survival 0.28 does not implement Fine-Gray subdistribution-hazard
233regression. Do not invent an import or describe `cumulative_incidence_competing_risks`
234as Fine-Gray; it is a nonparametric CIF estimator.
236If using another implementation:
238- verify it is actively maintained and supports the required censoring/truncation;
239- use its official API documentation;
240- distinguish subdistribution from cause-specific hazard coefficients;
241- keep preprocessing and tuning leakage-safe;
242- validate cause-specific absolute probabilities, not only coefficients.
244Neither hazard parameterization is universally "better." The estimand determines
245the method.

Fine-Gray regression

scikit-survival 0.28 does not implement Fine-Gray subdistribution-hazard regression. Do not invent an import or describe cumulative_incidence_competing_risks as Fine-Gray; it is a nonparametric CIF estimator.

If using another implementation:

  • verify it is actively maintained and supports the required censoring/truncation;
  • use its official API documentation;
  • distinguish subdistribution from cause-specific hazard coefficients;
  • keep preprocessing and tuning leakage-safe;
  • validate cause-specific absolute probabilities, not only coefficients.

Neither hazard parameterization is universally "better." The estimand determines the method.

Raw lines 230 to 246
247## Prediction evaluation
249Standard `concordance_index_ipcw`, `cumulative_dynamic_auc`, and Brier APIs in
250scikit-survival are documented for right-censored single-event outcomes. A
251competing-risk prediction question needs:
253- a named cause;
254- a case/control definition at each horizon;
255- handling of other causes consistent with that definition;
256- cause-specific probability predictions for calibration/Brier evaluation;
257- censoring weights fitted on training data;
258- evaluation times supported by training follow-up;
259- nested tuning or an untouched holdout.
261Do not label an all-event C-index as cause-specific discrimination, and do not use
262an all-event survival probability as a cause-specific CIF.

Prediction evaluation

Standard concordance_index_ipcw, cumulative_dynamic_auc, and Brier APIs in scikit-survival are documented for right-censored single-event outcomes. A competing-risk prediction question needs:

  • a named cause;
  • a case/control definition at each horizon;
  • handling of other causes consistent with that definition;
  • cause-specific probability predictions for calibration/Brier evaluation;
  • censoring weights fitted on training data;
  • evaluation times supported by training follow-up;
  • nested tuning or an untouched holdout.

Do not label an all-event C-index as cause-specific discrimination, and do not use an all-event survival probability as a cause-specific CIF.

Raw lines 247 to 263
264## Bundled helper
266The helper defaults to deterministic synthetic data:
268```bash
269python skills/scikit-survival/scripts/competing_risk_cif.py
270```
272For local CSV:
274```bash
275python skills/scikit-survival/scripts/competing_risk_cif.py \
276 --input competing.csv \
277 --event-column status \
278 --time-column time \
279 --horizons 2,5,10 \
280 --confidence \
281 --curve-output cif-curves.npz \
282 --output cif-summary.json
283```
285It:
287- rejects URLs, symlinks, missing/non-contiguous causes, and invalid times;
288- bounds file size and row count;
289- verifies that cause-specific rows sum to total CIF;
290- writes numeric arrays without pickle;
291- reports point estimates at requested horizons within observed follow-up;
292- repairs total-risk intervals and guards the upstream conditional-time defect;
293- makes no network calls.
295Use only authorized, de-identified local data. Do not include row-level data or PHI
296in reports.

Bundled helper

The helper defaults to deterministic synthetic data:

python skills/scikit-survival/scripts/competing_risk_cif.py

For local CSV:

python skills/scikit-survival/scripts/competing_risk_cif.py \
  --input competing.csv \
  --event-column status \
  --time-column time \
  --horizons 2,5,10 \
  --confidence \
  --curve-output cif-curves.npz \
  --output cif-summary.json

It:

  • rejects URLs, symlinks, missing/non-contiguous causes, and invalid times;
  • bounds file size and row count;
  • verifies that cause-specific rows sum to total CIF;
  • writes numeric arrays without pickle;
  • reports point estimates at requested horizons within observed follow-up;
  • repairs total-risk intervals and guards the upstream conditional-time defect;
  • makes no network calls.

Use only authorized, de-identified local data. Do not include row-level data or PHI in reports.

Raw lines 264 to 297
Runs code python skills/scikit-survival/scripts/competing_risk_cif.py :269
298## Reporting checklist
300- cause definitions and code mapping;
301- censoring definition and follow-up window;
302- CIF versus cause-specific or subdistribution hazard estimand;
303- number of rows/events for every cause;
304- horizon-specific CIF with uncertainty and support;
305- whether intervals are pointwise;
306- handling of `time_min`, if any;
307- competing-risk-specific prediction evaluation;
308- no causal or clinical-utility claim from association/probability alone.

Reporting checklist

  • cause definitions and code mapping;
  • censoring definition and follow-up window;
  • CIF versus cause-specific or subdistribution hazard estimand;
  • number of rows/events for every cause;
  • horizon-specific CIF with uncertainty and support;
  • whether intervals are pointwise;
  • handling of time_min, if any;
  • competing-risk-specific prediction evaluation;
  • no causal or clinical-utility claim from association/probability alone.
Raw lines 298 to 309
310## Sources
312Official scikit-survival sources checked 2026-10-01:
314- [Released CIF implementation](https://github.com/sebp/scikit-survival/blob/v0.28.0/sksurv/nonparametric.py) — native defects above reproduced on a six-row synthetic fixture.
316- [Competing-risks user guide](https://scikit-survival.readthedocs.io/en/stable/user_guide/competing-risks.html)
317- [CIF API](https://scikit-survival.readthedocs.io/en/stable/api/generated/sksurv.nonparametric.cumulative_incidence_competing_risks.html)
318- [Dataset API](https://scikit-survival.readthedocs.io/en/stable/api/datasets.html)
319- [0.24 release notes introducing CIF](https://scikit-survival.readthedocs.io/en/stable/release_notes/v0.24.html)
321Primary methods:
323- Aalen O. "Nonparametric estimation of partial transition probabilities in
324 multiple decrement models." *Annals of Statistics* 6 (1978), 534-545.
325 [Project Euclid record](https://projecteuclid.org/journals/annals-of-statistics/volume-6/issue-3/Nonparametric-Estimation-of-Partial-Transition-Probabilities-in-Multiple-Decrement-Models/10.1214/aos/1176344198.full)
326- Gray RJ. "A class of K-sample tests for comparing the cumulative incidence of
327 a competing risk." *Annals of Statistics* 16 (1988), 1141-1154.
328 [doi:10.1214/aos/1176350951](https://doi.org/10.1214/aos/1176350951)

Sources

Official scikit-survival sources checked 2026-10-01:

Primary methods:

  • Aalen O. "Nonparametric estimation of partial transition probabilities in multiple decrement models." Annals of Statistics 6 (1978), 534-545. Project Euclid record
  • Gray RJ. "A class of K-sample tests for comparing the cumulative incidence of a competing risk." Annals of Statistics 16 (1988), 1141-1154. doi:10.1214/aos/1176350951
Raw lines 310 to 328