simulate_competing_risks()

Generate synthetic competing-risks data from a latent-Weibull model.

Usage

Source

simulate_competing_risks(
    n=500,
    n_causes=2,
    *,
    shapes=None,
    scales=None,
    censoring_scale=1.5,
    n_covariates=0,
    coef=None,
    states=None,
    seed=None,
)

Each cause has its own Weibull distribution with specified shape and scale parameters. For each subject, a latent event time is drawn independently for every cause, and the cause with the shortest latent time is the one observed. An independent censoring time is drawn from a Weibull(1, scale) distribution (i.e., exponential). If the censoring time is shortest, the subject is censored.

When covariates are requested, cause-specific proportional hazards are applied: the Weibull scale for cause k is adjusted as b_k \cdot \exp(-\mathbf{x}^\top \boldsymbol{\beta}_k / a_k), where a_k and b_k are the shape and scale for cause k. Covariates are drawn from a standard normal distribution.

Parameters

n: int = 500

Number of subjects (default 500).

n_causes: int = 2

Number of competing event types (default 2).

shapes: tuple[float, …] | None = None

Weibull shape parameter for each cause. Length must equal n_causes. Defaults to (1.0, 1.5, ...) cycling through [1.0, 1.5, 0.8]. A shape of 1.0 gives an exponential (constant hazard). Values above 1 give increasing hazard. Values below 1 give decreasing hazard.

scales: tuple[float, …] | None = None

Weibull scale parameter for each cause. Length must equal n_causes. Defaults to 10.0 for every cause.

censoring_scale: float | None = 1.5

Controls censoring intensity. The censoring time is drawn from an exponential distribution with scale equal to censoring_scale times the mean latent event time. Larger values produce less censoring. Set to None or 0 to disable censoring entirely. Default is 1.5, which gives roughly 20-30% censoring depending on the shape/scale configuration.

n_covariates: int = 0

Number of covariates to generate (default 0). When positive, standard-normal covariates are drawn and cause-specific proportional-hazards effects are applied.

coef: Any | None = None

Coefficient matrix of shape (n_covariates, n_causes) for the proportional-hazards effect on each cause. When None (default), coefficients are drawn from Uniform(-0.5, 0.5).Ignored when n_covariates=0.

states: tuple[str, …] | None = None

Names for each cause (e.g., ("relapse", "death")). Length must equal n_causes. Defaults to ("cause1", "cause2", ...).

seed: int | None = None
Random seed for reproducibility.

Returns

SimulatedCompetingRisks
A dataclass with surv (the multi-state Surv response), covariates (the covariate matrix or None), latent_times (per-cause latent times), and censoring_time.

Examples

Generate a simple two-cause competing-risks dataset and fit an Aalen-Johansen estimator:

import greenwood as gw

sim = gw.simulate_competing_risks(n=300, n_causes=2, seed=42)
sim.surv
Surv(type=mright, n=300, events=171, states=('cause1', 'cause2'))
aj = gw.AalenJohansen().fit(sim.surv)
aj
AalenJohansen (Aalen-Johansen cumulative incidence)

states: cause1, cause2
n = 300

        final CIF
cause1     0.5327
cause2     0.4673

Generate data with covariates for a Fine-Gray model:

sim = gw.simulate_competing_risks(
    n=500,
    n_causes=2,
    n_covariates=3,
    states=("relapse", "death"),
    seed=99,
)
fg = gw.FineGray(cause="relapse").fit(sim.surv, covariates=sim.covariates)
gw.tidy(fg, format="polars")
shape: (3, 7)
termestimatestd_errorstatisticp_valueconf_lowconf_high
strf64f64f64f64f64f64
"x0"-0.1829050.073823-2.4776080.013227-0.327596-0.038214
"x1"0.3395350.0796274.2640520.000020.1834680.495601
"x2"0.0201410.0824950.2441520.807113-0.1415460.181829

Control the event-time distribution and censoring rate:

import numpy as np

# Cause 1: increasing hazard (shape > 1), Cause 2: constant hazard
sim = gw.simulate_competing_risks(
    n=1000,
    n_causes=2,
    shapes=(2.0, 1.0),
    scales=(15.0, 10.0),
    censoring_scale=0.8,  # heavier censoring
    seed=7,
)
# Check the censoring fraction
frac_censored = (sim.surv.status == 0).mean()
print(f"Censored: {frac_censored:.1%}")
Censored: 60.1%