Tabulate the event history: risk sets and events at each observed time.
event_table(
surv,
*,
group=None,
weights=None,
)
Creates a structured summary of the survival data at each unique event time. The table shows how many subjects were at risk, how many experienced events, and how many were censored at each time point. This is the foundational data structure used by non-parametric estimators (Kaplan-Meier, Nelson-Aalen) and the log-rank test.
Uses:
- Verification: Inspect risk sets to understand data structure and check for censoring patterns.
- Manual calculations: Compute survival estimates, cumulative event rates, or other summaries directly from the risk-set counts.
- Understanding censoring: See censoring patterns by time and stratification.
- Reporting: Present summary tables in publications (common in clinical trials).
Parameters
surv: Surv
-
A Surv response (time-to-event data). Supports right-censored or counting-process format. Weighted responses are supported; weights are incorporated into risk-set counts.
group: Any = None
-
Optional grouping variable for stratification, one value per subject. Can be a Pandas/Polars series, 1-D array, or Python sequence. When provided, the table is split into blocks with a strata column, one group per block. Groups appear in order of first appearance in the data.
weights: Any = None
-
Optional case weights. Can be a 1-D array or series. If
None (default), uses weights from the surv response if present, otherwise treats all subjects as weight 1.
Returns
EventTable
-
A structured result with the following attributes:
time: Unique event times (ascending, per stratum if grouped).
n_risk: Weighted number of subjects at risk (alive/uncensored and under follow-up) at each time.
n_event: Weighted number of events at each time.
n_censor: Weighted number of censored subjects at each time.
strata (if grouped): Stratum label for each row.
Access columns via .to_frame() (optionally format=), or iterate directly.
Details
Risk-set definition: At time \(t\), subjects “at risk” are those with:
- Entry time \(\le t\) (for counting-process data)
- Exit time \(> t\) (not yet having an event or censoring)
For right-censored data, entry is always 0, so the condition simplifies.
Censoring and events at the same time: Subjects censored at time t are handled carefully. A subject censored at exactly time t is at risk for any event at t (following convention in survival analysis). The table counts them in n_risk at time t, but removes them from n_risk at times > t.
Stratification order: If grouped, strata appear in the order of first appearance in the input data, not alphabetically. This allows meaningful orderings (e.g., control, then treatment).
Weights: If weights are provided, all counts (n_risk, n_event, n_censor) are sums of weights, not subject counts. This handles case weights or frequency weights.
Examples
View the basic event table for the lung dataset: how many subjects are at risk, events, and censoring at each time.
import greenwood as gw
lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
et = gw.event_table(y)
et.to_frame(format="polars").head(10)
shape: (10, 4)| time | n_risk | n_event | n_censor |
|---|
| f64 | f64 | f64 | f64 |
| 5.0 | 228.0 | 1.0 | 0.0 |
| 11.0 | 227.0 | 3.0 | 0.0 |
| 12.0 | 224.0 | 1.0 | 0.0 |
| 13.0 | 223.0 | 2.0 | 0.0 |
| 15.0 | 221.0 | 1.0 | 0.0 |
| 26.0 | 220.0 | 1.0 | 0.0 |
| 30.0 | 219.0 | 1.0 | 0.0 |
| 31.0 | 218.0 | 1.0 | 0.0 |
| 53.0 | 217.0 | 2.0 | 0.0 |
| 54.0 | 215.0 | 1.0 | 0.0 |
The first row shows the first event time: how many subjects were at risk, how many experienced an event, and how many were censored. Note that n_risk decreases over time as subjects leave the risk set (events or censoring).
Stratify by sex to see event patterns for each group separately:
et_sex = gw.event_table(y, group=lung["sex"])
et_sex.to_frame(format="polars").head(15)
shape: (15, 5)| strata | time | n_risk | n_event | n_censor |
|---|
| object | f64 | f64 | f64 | f64 |
| 1 | 11.0 | 138.0 | 3.0 | 0.0 |
| 1 | 12.0 | 135.0 | 1.0 | 0.0 |
| 1 | 13.0 | 134.0 | 2.0 | 0.0 |
| 1 | 15.0 | 132.0 | 1.0 | 0.0 |
| 1 | 26.0 | 131.0 | 1.0 | 0.0 |
| … | … | … | … | … |
| 1 | 60.0 | 124.0 | 1.0 | 0.0 |
| 1 | 65.0 | 123.0 | 2.0 | 0.0 |
| 1 | 71.0 | 121.0 | 1.0 | 0.0 |
| 1 | 81.0 | 120.0 | 1.0 | 0.0 |
| 1 | 88.0 | 119.0 | 2.0 | 0.0 |
Now each unique time appears twice (once per stratum) with a strata column indicating the group. This is useful for inspecting whether event rates and censoring patterns differ by group.
Compute a manual survival estimate from risk-set counts. The survival probability at time \(t\) is the product of (1 - n_event / n_risk) over all times \(\le t\):
import numpy as np
et = gw.event_table(y)
df = et.to_frame(format="pandas")
# Kaplan-Meier survival at each time
df["surv"] = np.cumprod(1 - df["n_event"] / df["n_risk"])
df[["time", "n_risk", "n_event", "surv"]].head(10)
|
time |
n_risk |
n_event |
surv |
| 0 |
5.0 |
228.0 |
1.0 |
0.995614 |
| 1 |
11.0 |
227.0 |
3.0 |
0.982456 |
| 2 |
12.0 |
224.0 |
1.0 |
0.978070 |
| 3 |
13.0 |
223.0 |
2.0 |
0.969298 |
| 4 |
15.0 |
221.0 |
1.0 |
0.964912 |
| 5 |
26.0 |
220.0 |
1.0 |
0.960526 |
| 6 |
30.0 |
219.0 |
1.0 |
0.956140 |
| 7 |
31.0 |
218.0 |
1.0 |
0.951754 |
| 8 |
53.0 |
217.0 |
2.0 |
0.942982 |
| 9 |
54.0 |
215.0 |
1.0 |
0.938596 |
This manual calculation matches the Kaplan-Meier estimate from KaplanMeier().fit().