## event_table()


Tabulate the event history: risk sets and events at each observed time.


Usage

``` python
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](Surv.md#greenwood.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.


``` python
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:


``` python
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\\:


``` python
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()`.
