Piecewise exponential¶
model="piecewise_exponential" — a baseline hazard that is constant inside
intervals you choose and jumps between them. The cheapest way to get an
arbitrary hazard shape.
The model¶
Given breakpoints \(0 < \tau_1 < \dots < \tau_k\) and rates \(\lambda_0, \dots, \lambda_k\), the baseline hazard is
with \(\tau_0 = 0\) and \(\tau_{k+1} = \infty\). Covariates act proportionally:
There is always one more rate than there are breakpoints — two breakpoints cut the timeline into three pieces.
Parameters¶
gen_piecewise_exponential(n, breakpoints, hazard_rates, betas=None,
n_covariates=2, covariate_dist="normal",
covariate_params=None, model_cens="uniform",
cens_par=5.0, seed=None)
| Parameter | Type | Default | Meaning |
|---|---|---|---|
n |
int |
— | number of subjects |
breakpoints |
list[float] |
— | increasing times where the hazard changes |
hazard_rates |
list[float] |
— | one rate per interval — len(breakpoints) + 1 of them |
betas |
list[float] | None |
None |
coefficients; random values are drawn when omitted |
n_covariates |
int |
2 |
number of covariates when betas is not given |
covariate_dist |
"normal" | "uniform" | "binary" |
"normal" |
covariate distribution — see Covariates |
covariate_params |
dict | None |
None |
distribution parameters |
model_cens |
str |
"uniform" |
censoring mechanism |
cens_par |
float |
5.0 |
censoring parameter |
seed |
int | Generator | None |
None |
reproducibility |
Pin betas when you are validating
With betas=None the coefficients are drawn at random, so you no
longer know the truth you are testing against. Pass them explicitly —
betas=[0.0, 0.0] isolates the baseline hazard entirely.
Example¶
from gen_surv import generate
df = generate(model="piecewise_exponential", n=6,
breakpoints=[1.0, 3.0], hazard_rates=[0.5, 1.0, 0.2],
seed=42)
print(df)
id time status X0 X1
0 0 1.682976 1 0.750451 0.940565
1 1 1.513450 1 -1.951035 -1.302180
2 2 0.980989 1 0.127840 -0.316243
3 3 2.270415 1 -0.016801 -0.853044
4 4 0.495887 1 0.879398 0.777792
5 5 0.973194 0 0.066031 1.127241
Check: does each interval have the hazard you asked for?¶
The occurrence/exposure estimate — events in an interval divided by the time subjects spent at risk in it — should return each rate:
import numpy as np
from gen_surv import generate
breakpoints, rates = [1.0, 3.0], [0.5, 2.0, 0.2]
df = generate(model="piecewise_exponential", n=40000,
breakpoints=breakpoints, hazard_rates=rates,
betas=[0.0, 0.0], model_cens="uniform", cens_par=100.0, seed=7)
edges = [0.0] + breakpoints + [float(df["time"].max())]
for lo, hi, true_h in zip(edges[:-1], edges[1:], rates):
at_risk = df[df["time"] > lo]
exposure = float(np.minimum(at_risk["time"], hi).sub(lo).clip(lower=0).sum())
events = int(((df["time"] > lo) & (df["time"] <= hi) & (df["status"] == 1)).sum())
print(f"[{lo}, {hi:.2f}) declared={true_h} empirical={events / exposure:.3f}")
[0.0, 1.00) declared=0.5 empirical=0.501
[1.0, 3.00) declared=2.0 empirical=2.011
[3.0, 37.31) declared=0.2 empirical=0.202
Each interval carries the rate it was given. The open-ended last interval is
populated only by the survivors of the earlier ones, so it is the noisiest of
the three — widen n before reading much into it.
Approximating a smooth hazard¶
Piecewise-constant hazards approximate any shape if you use enough pieces — this is the standard trick behind piecewise-exponential survival models:
import numpy as np
# Approximate a Weibull hazard, h(t) = 1.5 * t^0.5, on [0, 5]
grid = np.linspace(0, 5, 11)
breakpoints = list(grid[1:-1])
midpoints = (grid[:-1] + grid[1:]) / 2
hazard_rates = list(1.5 * midpoints ** 0.5)
Ten pieces reproduce a Weibull hazard closely enough for most purposes; the approximation error is bounded by how much the true hazard moves within a piece, so refine the grid where it is steepest rather than everywhere.
Related¶
- Cox proportional hazards — constant baseline, no breakpoints
- AFT models — monotone or unimodal hazards from a parametric family
- Covariates —
covariate_distandcovariate_params - API:
gen_piecewise_exponential