Survival analysis for public health¶
Time-to-event is the native outcome of cohort epidemiology and clinical research. Mortality, relapse, time-to-diagnosis, time-to-discharge — all are right-censored durations. StatsPAI ships the standard estimators (Kaplan–Meier, Cox proportional hazards, parametric AFT, frailty models) plus the causal-survival tools (inverse-probability-of-censoring weights, causal survival forests) that connect survival outcomes to the g-methods in the g-methods guide.
Scope note: sp.cox is parity-tested against R's survival::coxph and
Stata's stcox (see tests/r_parity/, tests/stata_parity/). The
causal-survival surface (sp.ipcw, sp.causal_survival_forest,
sp.synth_survival) is API-stable but not yet cross-language certified
— validate before publication and read
sp.describe_function(name)["limitations"].
1. Kaplan–Meier survival curves¶
import statspai as sp
km = sp.kaplan_meier(data=df, duration="time", event="event", group="arm", conf_type="log-log")
print(km.median_survival) # median survival per group
print(km.survival_table) # risk table: n.risk, n.event, S(t), CI
km.plot() # step survival curves with at-risk counts
event is the standard 1 = event observed, 0 = right-censored coding.
Pass group= to stratify; omit it for a single pooled curve.
weights= gives weighted curves (with inverse-probability-of-treatment
weights, the confounder-adjusted curve of each arm), start= delayed
entry, and cluster= a subject id for repeated rows. The variance
follows R survfit: the infinitesimal jackknife (se_method="ij") with
a cluster or non-integer weights, Greenwood otherwise;
se_method="xie_liu" is the variance adjustedCurves reports for IPTW
curves.
2. Cox proportional hazards¶
import statspai as sp
# Formula or explicit columns — both work
cox = sp.cox(data=df, duration="time", event="event", x=["arm", "age", "sex"],
ties="efron")
print(cox.summary())
The reported coefficients are log hazard ratios; exponentiate for
the hazard ratio (pass hazard_ratio=True to print HRs directly). You
can add:
strata=for a stratified baseline hazard,robust="hc0"orcluster=for robust / clustered standard errors, withssc="r"(no small-sample factor, ascoxph) orssc="stata"(N/(N-1)robust,G/(G-1)clustered, asstcox),ties="efron"(default) /"breslow"for tie handling,start=for counting-process rows(start, stop]: delayed entry and time-varying covariates split into intervals (RSurv(start, stop, event), Statastset, time0()); passcluster=<subject id>with several rows per subject,weights=for case weights; with IPT or survey weights alsoweights_as="sampling", which gives the robust variance R reports for such weights and Statastcox [pw=]always uses.
msm_cox = sp.cox(data=df, duration="time", event="event", x=["arm"],
weights="iptw", weights_as="sampling", ssc="r")
tv_cox = sp.cox(data=long, duration="tstop", start="tstart", event="event",
x=["cd4", "arm"], cluster="id", ssc="r")
Always sanity-check the proportional-hazards assumption (Schoenfeld residuals) before trusting a single HR over long follow-up.
3. Parametric survival (AFT) and frailty¶
When you need a fully parametric model — for extrapolation, or because
the accelerated-failure-time interpretation is more natural — use
sp.survreg / sp.aft:
import statspai as sp
# Weibull / lognormal / loglogistic / exponential AFT
m = sp.survreg(data=df, duration="time", event="event", x=["arm", "age"],
dist="weibull")
print(m.summary())
aft = sp.aft("time ~ arm + age", data=df, family="lognormal")
For clustered / shared-frailty survival (recurrent events, multi-centre
cohorts) the sp.survival module also exposes a frailty model.
4. Informative censoring — IPCW¶
When loss-to-follow-up depends on measured covariates, censoring is informative and naïve KM/Cox are biased. Inverse-probability-of- censoring weights restore the population that would have been observed under complete follow-up [@robins2000marginal]:
import statspai as sp
ipcw = sp.ipcw(
data=df, time="time", event="event",
censor_covariates=["age", "ldl", "on_treatment"],
stabilize=True,
method="pooled_logistic",
)
print(ipcw.summary_stats) # weight distribution — watch for extremes
weights = ipcw.weights # feed into a weighted outcome analysis
These weights compose directly with sp.clone_censor_weight for
per-protocol target-trial analyses (see
g-methods).
5. Causal survival effects¶
For heterogeneous treatment effects on a survival outcome under
unconfoundedness, sp.causal_survival_forest estimates a
restricted-mean-survival-time or survival-probability contrast that
varies with covariates; sp.synth_survival brings the synthetic-control
idea to time-to-event panel settings.
import statspai as sp
csf = sp.causal_survival_forest(
data=df, time="time", event="event",
treat="arm", covariates=["age", "sex", "ldl"],
horizon=36, target="RMST", # or target="survival_probability"
)
csf.ate, csf.se # doubly-robust average effect
Choose the horizon from the study design; the estimator weights by the
probability of staying uncensored up to it. See
the GRF family guide for the method, its evidence against
grf, and sp.survival_forest for conditional survival curves.
6. Average effects on survival time¶
Restricted mean survival time. sp.rmst integrates the
Kaplan–Meier curve of each group up to tau and reports the difference
and the RMST / RMTL ratios (R survRM2::rmst2). With IPT weights it is
the confounder-adjusted RMST difference, with the infinitesimal-jackknife
standard error (which holds the weights fixed):
r = sp.rmst(df, duration="time", event="event", group="arm", tau=36,
weights="iptw")
r.contrasts # difference, RMST ratio, RMTL ratio
Effects on mean survival time from parametric models.
sp.stteffects is Stata's stteffects: regression adjustment
(method="ra"), weighted RA ("wra"), inverse-probability weighting
("ipw") and IPW regression adjustment ("ipwra"), for the ATE or the
ATET, with Weibull / exponential / log-normal outcome and censoring
models and the stacked-estimating-equation standard errors.
te = sp.stteffects(df, duration="time", event="event", treat="arm",
covariates=["age", "sex"], method="ipwra")
te.detail # ATE, POmean0, POmean1
For ipwra with estimand="ATET" StatsPAI averages over the treated;
Stata averages the same models over the whole sample (see
docs/dev/modules/survival.md).
Dropout in a marginal structural model. sp.msm(censor=...) adds
stabilized inverse-probability-of-censoring weights for person-period
data (R ipw::ipwtm(type = "first")).
7. What is not here yet¶
- Competing risks are covered by
sp.cuminc(Aalen–Johansen cumulative incidence) andsp.finegray; weighted versions are not. CoxResult.ph_test()does not yet handle weighted or counting-process fits (it refuses them); use Rcox.zphfor those.- Time-dependent ROC / net-benefit / decision-curve analysis.
If your analysis depends on these, validate carefully and consider
cross-checking with R's cmprsk / survival in the interim.