Simulate survival data with a known truth.
gen_surv generates synthetic time-to-event datasets from twelve models —
proportional hazards, accelerated failure time, competing risks, cure
fractions, piecewise hazards, recurrent events and two illness-death
processes — so you can test an estimator against parameters you chose
yourself.
It is a Python port of the R package genSurv, extended well past the original's four models.
📖 Documentation · 🚀 Quickstart · 🧪 Choosing a model
pip install gen-survPython 3.11, 3.12 and 3.13. Everything is included except scikit-survival, which is optional and needed only for the two conversion helpers:
pip install scikit-survivalThe package ships py.typed, so mypy and pyright check your calls into it
rather than treating it as untyped.
from gen_surv import generate
df = generate(
model="cphm", # Cox proportional hazards
n=6,
beta=0.5, # log hazard ratio
covariate_range=2.0, # X0 ~ Uniform(0, 2)
model_cens="uniform",
cens_par=1.0,
seed=42,
)
print(df) time status X0
0 0.438878 0.0 1.547912
1 0.094177 0.0 1.394736
2 0.037041 1.0 1.522279
3 0.370798 0.0 0.900772
4 0.646901 1.0 1.287730
5 0.251113 1.0 0.454477
You picked beta = 0.5, so you know what a correct estimator should recover:
from lifelines import CoxPHFitter
df = generate(model="cphm", n=5000, beta=0.5, covariate_range=2.0,
model_cens="uniform", cens_par=1.0, seed=7)
CoxPHFitter().fit(df, duration_col="time", event_col="status").params_
# X0 0.501That is the whole idea. Every column was produced by a mechanism you specified, so anything an estimator gets wrong is the estimator's fault.
model= |
Family | Rows per subject |
|---|---|---|
cphm |
Cox proportional hazards | 1 |
aft_ln |
Log-normal AFT | 1 |
aft_weibull |
Weibull AFT | 1 |
aft_log_logistic |
Log-logistic AFT | 1 |
piecewise_exponential |
Piecewise constant hazard | 1 |
competing_risks |
Cause-specific constant hazards | 1 |
competing_risks_weibull |
Cause-specific Weibull hazards | 1 |
mixture_cure |
Logistic cure + exponential failure | 1 |
cmm |
Illness-death, counting-process intervals | 2 or 3 |
thmm |
Illness-death, observed state panel | 2 or 3 |
tdcm |
Cox with a time-dependent covariate | 1 |
recurrent_events |
Repeated events: Andersen-Gill, PWP | 1 per at-risk interval |
Every model has a page covering its parameters, the mathematics, a worked example, and a check that the parameters can be recovered from the data it generates — start at Choosing a model.
The output shape is not the same for every model. Multi-state generators return several rows per subject, and column names differ between families. See Output schemas before writing code that consumes a generated frame.
The twelve above each fix a state structure. When yours is not among them,
gen_multistate takes the graph itself:
from gen_surv import ExponentialBaseline, Transition, WeibullBaseline, gen_multistate
multistate = gen_multistate(
n=500,
transitions=[
Transition(1, 2, WeibullBaseline(shape=1.2, scale=3.0), [0.4]), # fall ill
Transition(2, 1, ExponentialBaseline(rate=0.6), [0.0]), # recover
Transition(2, 3, ExponentialBaseline(rate=0.2), [0.6]), # die while ill
],
clock="reset",
seed=1,
)Every edge carries its own baseline hazard and coefficients. A state with no
outgoing edge is absorbing, and cycles are allowed - recovery above is a
transition like any other. clock="forward" measures the hazard from entry to
the study, giving a Markov process; clock="reset" restarts it at each state,
giving a semi-Markov one.
cmm and thmm are configurations of this engine rather than separate
implementations. It takes a list of objects rather than scalars, so it has no
model= string and no command-line form - import it directly. See
The multistate engine.
A generated frame looks like a real one — which means it hides the same things.
simulate() hands back what a real dataset never could:
from gen_surv import simulate
result = simulate("cphm", n=1000, beta=0.5, covariate_range=2.0,
model_cens="uniform", cens_par=1.0, seed=42)
result.data # the frame generate() would return
result.config # model, parameters, seed, gen_surv version
result.truth["event_time"] # when each subject would have failed
result.truth["censoring_time"] # what censoring hidSeveral models draw their coefficients for you when you leave them out.
result.truth["betas"] is the only way to learn what they were — without it
those datasets cannot validate anything.
Every generator that draws a time inverts a cumulative hazard, so the shape is a parameter rather than a fork in the code:
from gen_surv import generate, LogLogisticBaseline
recurrent = generate(model="recurrent_events", n=500,
baseline=LogLogisticBaseline(shape=2.0, scale=1.5),
betas=[0.4, -0.2], followup_time=6.0, seed=1)Exponential, Weibull, Gompertz, log-logistic and piecewise-constant are
built in, and anything implementing hazard, cumulative_hazard and its
inverse works too.
from gen_surv import describe_survival, plot_survival_curve, export_dataset, to_sksurv
describe_survival(df) # events, censoring, median follow-up
plot_survival_curve(df) # Kaplan-Meier, optionally stratified
export_dataset(df, "data.rds") # csv, json, feather or rds
to_sksurv(df) # structured array for scikit-survival- Ground truth — configurations, latent times, the coefficients actually used
- Baseline hazards — the five families, and writing your own
- Censoring — the built-in mechanisms, hitting a target event rate, applying your own distribution
- Covariates — the three schemes across model families
- Summaries — event counts, quality checks, dataset comparison
- Plotting — survival curves, hazard comparisons, covariate effects
- Fitting models — lifelines, scikit-survival, scikit-learn, R
gen_surv dataset cphm --n 1000 --beta 0.5 --seed 42 -o survival.csv
gen_surv visualize survival.csv --group-col X0 --output km.pngRepeat a flag for list arguments — --beta 0.5 --beta -0.3. Every one of the
twelve models is reachable from the command line. Full option reference in the
CLI guide.
Every generator takes a seed, accepting an int or a
numpy.random.Generator. The same seed on the same version always gives the
same frame, on any platform.
A bug fix in a sampler changes the draws a seed produces, so pin the version alongside the seed for anything that must reproduce:
gen-surv==3.1.2
See Reproducibility.
https://diogoribeiro7.github.io/genSurvPy/
| Section | Contents |
|---|---|
| Getting started | Installation, quickstart, output schemas, reproducibility |
| Models | Per-model parameters, mathematics, examples, recovery checks |
| Guides | Censoring, covariates, summaries, plotting, export, interoperability, CLI |
| Theory | The mathematics behind every generator, plus the bibliography |
| API | Full reference, generated from the source |
Built with Material for MkDocs and mkdocstrings, and rebuilt from the release tag by the Pages workflow — so it documents the version on PyPI, not unreleased work.
git clone https://github.com/DiogoRibeiro7/genSurvPy.git
cd genSurvPy
poetry install --with dev
pre-commit install
pre-commit run --all-files # black, isort, flake8, mypy
pytest # tests needing optional packages skip themselvesDocs:
poetry install --with docs
poetry run mkdocs serve # live reload on http://127.0.0.1:8000On Debian and Ubuntu, building scikit-survival may need
build-essential gfortran libopenblas-dev.
Work happens on develop; main carries releases. See
CONTRIBUTING.md and ROADMAP.md.
@software{ribeiro_gensurv,
title = {gen_surv: Survival Data Simulation in Python},
author = {Diogo Ribeiro},
url = {https://github.com/DiogoRibeiro7/genSurvPy},
version = {3.1.2}
}Machine-readable metadata: CITATION.cff and .zenodo.json.
MIT — see LICENSE.
Diogo Ribeiro — ESMAD, Instituto Politécnico do Porto
- ORCID: https://orcid.org/0009-0001-2022-7072
- Email: dfr@esmad.ipp.pt · diogo.debastos.ribeiro@gmail.com
- GitHub: @DiogoRibeiro7