---------------------------------------------------------------------- This is the API documentation for the polarstate library. ---------------------------------------------------------------------- ## Event table Compute an inspectable Aalen-Johansen event table from time-to-event observations. prepare_event_table(times_and_reals: polars.dataframe.frame.DataFrame) -> polars.dataframe.frame.DataFrame Compute an inspectable Aalen-Johansen event table. Parameters ---------- times_and_reals : pl.DataFrame Subject-level observations containing a numeric ``times`` column and an integer ``reals`` column. Outcome code ``0`` means follow-up ended without an observed event (right-censoring), ``1`` means the event of interest, and optional ``2`` means a competing event. A simple event/censoring analysis uses only ``0`` and ``1``. Returns ------- pl.DataFrame One row per unique observed time. The result includes outcome counts, the risk set, cause-specific event increments, conditional and overall survival, transition increments, and cumulative state-occupancy probabilities for states 1 and 2. Raises ------ TypeError If the input is not a Polars DataFrame, times is not numeric, or reals is not integer-valued. ValueError If required columns are missing, the input is empty, values are null, times are non-finite or negative, or outcome codes are outside {0, 1, 2}. Notes ----- Input rows need not be sorted. Extra columns are ignored. Duplicate times are allowed and are aggregated into a single event-table row. Examples -------- >>> import polars as pl >>> from polarstate import prepare_event_table >>> observations = pl.DataFrame( ... {"times": [2.0, 4.0, 5.0], "reals": [1, 0, 1]} ... ) >>> prepare_event_table(observations).select( ... "times", "at_risk", "count_1", "overall_survival" ... ).shape (3, 4) See Also -------- predict_aj_estimates : Evaluate the event table at fixed horizons. ## Time-horizon estimates Return state-occupancy estimates at requested time horizons. predict_aj_estimates(event_table: polars.dataframe.frame.DataFrame | polarstate.transition_steps.TransitionTableSteps, fixed_time_horizons: polars.series.series.Series | None = None, full_event_table: bool = False) -> polars.dataframe.frame.DataFrame Evaluate state-occupancy probabilities at fixed time horizons or scheduled steps. Parameters ---------- event_table : pl.DataFrame or TransitionTableSteps Event table returned by ``prepare_event_table`` or transition table returned by ``prepare_transition_table_steps``. fixed_time_horizons : pl.Series, optional Numeric horizons to evaluate for time-based event tables, or integer step horizons for step transition tables. If omitted for step transition tables, occupancy for all observed steps is returned. full_event_table : bool, default False If ``True``, append estimates at every observed event time. Use the ``estimate_origin`` column to distinguish those rows from requested horizons. Only supported for continuous event tables. Returns ------- pl.DataFrame For competing risk event tables: columns are ``times``, state-occupancy probabilities for states 0, 1, and 2, and ``estimate_origin``. For discrete step transition tables: columns are ``step``, ``state``, and ``state_occupancy_probability``. Raises ------ TypeError If inputs are not the documented Polars/polarstate types, horizons are not of expected numeric/integer dtypes, or full_event_table is not boolean. ValueError If required event-table columns are missing, either input is empty, or horizons contain null, non-finite, invalid, or unobserved step values. ---------------------------------------------------------------------- This is the User Guide documentation for the package. ---------------------------------------------------------------------- ## User Guide ### How the Estimator Works The Aalen-Johansen estimator generalizes Kaplan-Meier reasoning to one or more absorbing outcomes. In `polarstate`, code `0` means that follow-up ended without an observed event, code `1` is the event of interest, and optional code `2` is a competing event. ![An illustrated introduction to the Aalen-Johansen estimator.](images/aalen-johansen.webp){fig-alt="A watercolor illustration of Aalen and Johansen presenting state-occupancy curves, surrounded by polar bears." width=80%} ```{mermaid} flowchart LR O["Under observation"] -->|"Code 0"| C["Right-censored"] O -->|"Code 1"| E["Event of interest"] O -.->|"Optional code 2"| R["Competing event"] style O fill:#dff3f8,stroke:#1f5f82 style E fill:#e3f4ea,stroke:#287a4b style R fill:#f8e8ef,stroke:#9b4668 ``` Codes `0` and `1` are sufficient for a single-event analysis. Code `2` adds a second absorbing state; it is not required by the API. ## The estimator For event type $j$, the estimated state-occupancy probability is $$ \widehat F_j(t) = \sum_{u \le t} \widehat S(u-) \frac{dN_j(u)}{Y(u)}. $$ {#eq-aalen-johansen} Here, $Y(u)$ is the risk set immediately before time $u$, $dN_j(u)$ is the number of type-$j$ events at that time, and $\widehat S(u-)$ is the probability of remaining in state 0 immediately beforehand. ## From observations to estimates The columns returned by `prepare_event_table()` expose each component of @eq-aalen-johansen: 1. Observations are grouped by time and counted by outcome. 2. `at_risk` supplies $Y(u)$. 3. `csh_1` and `csh_2` supply $dN_j(u)/Y(u)$. 4. `previous_overall_survival` supplies $\widehat S(u-)$. 5. `transition_probabilities_to_*_at_times` contains each weighted increment. 6. `state_occupancy_probability_*_at_times` cumulatively sums those increments. ### Event-table columns | Column | Interpretation | |---|---| | `times` | Unique observed time | | `count_0` | Observations right-censored at that time | | `count_1` | Events of interest at that time | | `count_2` | Competing events at that time; zero in a single-event analysis | | `events_at_times` | Total observations ending follow-up at that time | | `at_risk` | Observations still under observation immediately before that time | | `csh_1`, `csh_2` | Cause-specific event increments: event count divided by risk set | | `conditional_survival` | Probability of avoiding either event at that time, conditional on being at risk | | `overall_survival` | Product of conditional-survival increments through that time | | `previous_overall_survival` | Survival immediately before the current time | | `transition_probabilities_to_*_at_times` | Probability mass entering each absorbing state at that time | | `state_occupancy_probability_*_at_times` | Cumulative probability of occupying each absorbing state | The historical `trainsition_probabilities_to_*_at_times` spelling remains as a backward-compatible alias. The corrected names were introduced in polarstate 0.1.8 and are the canonical names used by the implementation. New code should use the correctly spelled names and select event-table columns by name rather than position. There is currently no scheduled release that removes the historical aliases. If they are removed, polarstate will first announce the removal version and emit a deprecation warning during a documented compatibility window; removal will be released as a breaking schema change. ## From the event table to requested horizons `predict_aj_estimates()` performs a backward as-of join: for each requested horizon, it returns the latest estimate available at or before that time. Horizons before the first observed event receive probability 1 for state 0 and 0 for the absorbing states. | Column | Interpretation | |---|---| | `times` | Requested horizon or observed event time | | `state_occupancy_probability_0` | Probability of remaining event-free | | `state_occupancy_probability_1` | Probability of the event of interest | | `state_occupancy_probability_2` | Probability of the competing event | | `estimate_origin` | Whether the row came from a requested horizon or the full event table | At each horizon, $$ P_0(t) + P_1(t) + P_2(t) = 1. $$ For a single-event analysis, $P_2(t)=0$, so $P_0(t)$ is the Kaplan-Meier survival estimate and $P_1(t)=1-P_0(t)$. ## Interpretation A raw event proportion ignores how long each observation was followed. Aalen-Johansen uses the risk set at each event time, so right-censoring removes an observation from later risk sets without treating it as an event. ::: {.callout-important} The estimates describe the observed cohort under the usual independent censoring assumptions. They do not by themselves adjust for confounding or establish causal effects. ::: Compare the same estimates with other implementations in [Package Comparisons](04-package-comparisons.qmd), continue with [Recipes](05-recipes.qmd) for common data structures, or consult the [API Reference](../reference/index.html) for complete function details. ### Package Comparisons This page uses the same competing-risks data in three implementations. As in the original polarstate documentation, each tab shows the package's **native output**, not only a normalized summary. ## Shared example Each row is one observation. Code `0` means right-censored, `1` is the event of interest, and `2` is the competing event. | `times` | 24.1 | 9.7 | 49.9 | 18.6 | 34.8 | 14.2 | 39.2 | 46.0 | 31.5 | 4.3 | |---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| | `reals` | 1 | 1 | 1 | 1 | 0 | 2 | 1 | 2 | 0 | 1 | ::: {.panel-tabset} ## polarstate (Py 🐍) `polarstate` first returns the complete Polars event table, exposing the risk set, cause-specific hazards, survival, transition increments, and state occupancy probabilities. ```python import polars as pl from polarstate import prepare_event_table, predict_aj_estimates data = pl.DataFrame({ "times": [24.1, 9.7, 49.9, 18.6, 34.8, 14.2, 39.2, 46.0, 31.5, 4.3], "reals": [1, 1, 1, 1, 0, 2, 1, 2, 0, 1], }) event_table = prepare_event_table(data) print(event_table) ``` Native Polars output: ```text shape: (10, 16) β”Œβ”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ times ┆ count_0 ┆ count_1 ┆ count_2 ┆ … ┆ trainsition_pr ┆ state_occupanc ┆ state_occupanc ┆ state_occupanc β”‚ β”‚ --- ┆ --- ┆ --- ┆ --- ┆ ┆ obabilities_to ┆ y_probability_ ┆ y_probability_ ┆ y_probability_ β”‚ β”‚ f64 ┆ u32 ┆ u32 ┆ u32 ┆ ┆ _2_at_times ┆ 0_at_times ┆ 1_at_times ┆ 2_at_times β”‚ β”‚ ┆ ┆ ┆ ┆ ┆ --- ┆ --- ┆ --- ┆ --- β”‚ β”‚ ┆ ┆ ┆ ┆ ┆ f64 ┆ f64 ┆ f64 ┆ f64 β”‚ β•žβ•β•β•β•β•β•β•β•ͺ═════════β•ͺ═════════β•ͺ═════════β•ͺ═══β•ͺ════════════════β•ͺ════════════════β•ͺ════════════════β•ͺ════════════════║ β”‚ 4.3 ┆ 0 ┆ 1 ┆ 0 ┆ … ┆ 0.0 ┆ 0.9 ┆ 0.1 ┆ 0.0 β”‚ β”‚ 9.7 ┆ 0 ┆ 1 ┆ 0 ┆ … ┆ 0.0 ┆ 0.8 ┆ 0.2 ┆ 0.0 β”‚ β”‚ 14.2 ┆ 0 ┆ 0 ┆ 1 ┆ … ┆ 0.1 ┆ 0.7 ┆ 0.2 ┆ 0.1 β”‚ β”‚ 18.6 ┆ 0 ┆ 1 ┆ 0 ┆ … ┆ 0.0 ┆ 0.6 ┆ 0.3 ┆ 0.1 β”‚ β”‚ 24.1 ┆ 0 ┆ 1 ┆ 0 ┆ … ┆ 0.0 ┆ 0.5 ┆ 0.4 ┆ 0.1 β”‚ β”‚ 31.5 ┆ 1 ┆ 0 ┆ 0 ┆ … ┆ 0.0 ┆ 0.5 ┆ 0.4 ┆ 0.1 β”‚ β”‚ 34.8 ┆ 1 ┆ 0 ┆ 0 ┆ … ┆ 0.0 ┆ 0.5 ┆ 0.4 ┆ 0.1 β”‚ β”‚ 39.2 ┆ 0 ┆ 1 ┆ 0 ┆ … ┆ 0.0 ┆ 0.333333 ┆ 0.566667 ┆ 0.1 β”‚ β”‚ 46.0 ┆ 0 ┆ 0 ┆ 1 ┆ … ┆ 0.166667 ┆ 0.166667 ┆ 0.566667 ┆ 0.266667 β”‚ β”‚ 49.9 ┆ 0 ┆ 1 ┆ 0 ┆ … ┆ 0.0 ┆ 0.0 ┆ 0.733333 ┆ 0.266667 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ### Prediction for specific time horizons ```python estimates = predict_aj_estimates( event_table, pl.Series([10.0, 20.0, 30.0, 40.0, 50.0]), ) print(estimates) ``` Native Polars output: ```text shape: (5, 5) β”Œβ”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ times ┆ state_occupancy_prob… ┆ state_occupancy_prob… ┆ state_occupancy_prob… ┆ estimate_origin β”‚ β”‚ --- ┆ --- ┆ --- ┆ --- ┆ --- β”‚ β”‚ f64 ┆ f64 ┆ f64 ┆ f64 ┆ enum β”‚ β•žβ•β•β•β•β•β•β•β•ͺ════════════════════════β•ͺ════════════════════════β•ͺ════════════════════════β•ͺ═════════════════════║ β”‚ 10.0 ┆ 0.8 ┆ 0.2 ┆ 0.0 ┆ fixed_time_horizons β”‚ β”‚ 20.0 ┆ 0.6 ┆ 0.3 ┆ 0.1 ┆ fixed_time_horizons β”‚ β”‚ 30.0 ┆ 0.5 ┆ 0.4 ┆ 0.1 ┆ fixed_time_horizons β”‚ β”‚ 40.0 ┆ 0.333333 ┆ 0.566667 ┆ 0.1 ┆ fixed_time_horizons β”‚ β”‚ 50.0 ┆ 0.0 ┆ 0.733333 ┆ 0.266667 ┆ fixed_time_horizons β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ## lifelines (Py 🐍) `lifelines` fits and returns one cumulative-incidence table for each event type. ```python import pandas as pd from lifelines import AalenJohansenFitter data = pd.DataFrame({ "duration": [24.1, 9.7, 49.9, 18.6, 34.8, 14.2, 39.2, 46.0, 31.5, 4.3], "event": [1, 1, 1, 1, 0, 2, 1, 2, 0, 1], }) primary = AalenJohansenFitter(calculate_variance=False) primary.fit(data["duration"], data["event"], event_of_interest=1) print("Event table for event type 1") print(primary.cumulative_density_) competing = AalenJohansenFitter(calculate_variance=False) competing.fit(data["duration"], data["event"], event_of_interest=2) print("Event table for event type 2") print(competing.cumulative_density_) ``` Native pandas output: ```text Event table for event type 1 CIF_1 event_at 0.0 0.000000 4.3 0.100000 9.7 0.200000 14.2 0.200000 18.6 0.300000 24.1 0.400000 31.5 0.400000 34.8 0.400000 39.2 0.566667 46.0 0.566667 49.9 0.733333 Event table for event type 2 CIF_2 event_at 0.0 0.000000 4.3 0.000000 9.7 0.000000 14.2 0.100000 18.6 0.100000 24.1 0.100000 31.5 0.100000 34.8 0.100000 39.2 0.100000 46.0 0.266667 49.9 0.266667 ``` ### Prediction for specific time horizons ```python print(primary.predict([10.0, 20.0, 30.0, 40.0, 50.0])) print(competing.predict([10.0, 20.0, 30.0, 40.0, 50.0])) ``` Native pandas output: ```text 10.0 0.200000 20.0 0.300000 30.0 0.400000 40.0 0.566667 50.0 0.733333 Name: CIF_1, dtype: float64 10.0 0.000000 20.0 0.100000 30.0 0.100000 40.0 0.100000 50.0 0.266667 Name: CIF_2, dtype: float64 ``` ## tidycmprsk (R πŸ”΅) `tidycmprsk` returns outcome-specific rows in a tibble. ```r library(dplyr) library(tidycmprsk) data <- tibble( times = c(24.1, 9.7, 49.9, 18.6, 34.8, 14.2, 39.2, 46.0, 31.5, 4.3), reals = factor( c(1, 1, 1, 1, 0, 2, 1, 2, 0, 1), levels = c(0, 1, 2), labels = c("censored", "primary_event", "competing_event") ) ) fit <- cuminc(Surv(times, reals) ~ 1, data) tidy(fit) |> select(outcome, time, estimate) |> print(n = 20) ``` Native tibble output: ```text # A tibble: 20 Γ— 3 outcome time estimate 1 primary_event 4.3 0.100 2 primary_event 9.7 0.200 3 primary_event 14.2 0.200 4 primary_event 18.6 0.300 5 primary_event 24.1 0.400 6 primary_event 31.5 0.400 7 primary_event 34.8 0.400 8 primary_event 39.2 0.567 9 primary_event 46.0 0.567 10 primary_event 49.9 0.733 11 competing_event 4.3 0 12 competing_event 9.7 0 13 competing_event 14.2 0.100 14 competing_event 18.6 0.100 15 competing_event 24.1 0.100 16 competing_event 31.5 0.100 17 competing_event 34.8 0.100 18 competing_event 39.2 0.100 19 competing_event 46.0 0.267 20 competing_event 49.9 0.267 ``` ::: ### Recipes Every example on this page is executed when the documentation builds. If an API change breaks a recipe, the documentation check fails with it. ```{python} import pandas as pd import polars as pl from polarstate import prepare_event_table, predict_aj_estimates ``` ## Choose the event structure The workflow stays the same; only the outcome coding changes. ::: {.panel-tabset} ### Single event Use `0` for right-censoring and `1` for the event. ```{python} single_event_data = pl.DataFrame({ "times": [2.0, 4.0, 5.0, 8.0, 10.0], "reals": [1, 0, 1, 0, 1], }) single_table = prepare_event_table(single_event_data) single_result = predict_aj_estimates( single_table, pl.Series([3.0, 6.0, 12.0]) ) single_result ``` State 2 remains zero, while state 0 is the event-free probability. ### Competing risks Encode the event of interest as `1` and the competing event as `2`. ```{python} competing_data = pl.DataFrame({ "times": [2.0, 4.0, 5.0, 8.0, 10.0, 12.0], "reals": [1, 2, 0, 1, 2, 1], }) competing_table = prepare_event_table(competing_data) competing_result = predict_aj_estimates( competing_table, pl.Series([3.0, 6.0, 9.0, 12.0]) ) competing_result ``` Do not censor competing events: coding them as `0` changes the estimand. ::: ## Return horizons and event times together ```{python} combined_result = predict_aj_estimates( competing_table, pl.Series([3.0, 6.0, 9.0, 12.0]), full_event_table=True, ) combined_result.select("times", "estimate_origin") ``` Use `estimate_origin` to distinguish requested horizons from observed times. ## Estimate within groups The public functions operate on one cohort at a time. ```{python} data_with_groups = competing_data.with_columns( pl.Series("group", ["A", "A", "A", "B", "B", "B"]) ) estimates_by_group = { group[0]: predict_aj_estimates( prepare_event_table(group_data.select("times", "reals")), pl.Series([6.0, 12.0]), ) for group, group_data in data_with_groups.group_by("group") } {k: value.shape for k, value in estimates_by_group.items()} ``` These are descriptive within-group estimates, not an adjusted regression model. ## Convert pandas input ```{python} pandas_data = pd.DataFrame({ "time": [2, 4, 6, 8], "event": [1, 0, 2, 1], }) polars_data = pl.from_pandas(pandas_data).select( pl.col("time").cast(pl.Float64).alias("times"), pl.col("event").cast(pl.Int64).alias("reals"), ) prepare_event_table(polars_data).select("times", "at_risk") ``` Recode outcomes before estimation and confirm that only `0`, `1`, and optional `2` are present. ## Next steps - Read [How the estimator works](02-how-it-works.qmd). - Review [Assumptions and limitations](06-assumptions-limitations.qmd). - See [Package comparisons](04-package-comparisons.qmd). - Consult the [API Reference](../reference/index.html). ## What the estimate means `polarstate` computes non-parametric Aalen–Johansen state-occupancy probabilities. With codes `0` and `1`, it describes a standard time-to-event outcome. Adding code `2` represents a competing event. The estimates are descriptive probabilities for the supplied cohort. They are not causal effects and do not adjust for covariates. ## Data assumptions - Each row represents one independent subject with one observed follow-up time. - `times` is numeric, finite, non-negative, and uses a consistent time unit. - `reals` is integer-coded: `0` for right-censoring, `1` for the event of interest, and optionally `2` for a competing event. - Censoring is assumed independent of the event process, conditional on the study design and any stratification performed before estimation. - Events tied at the same time are aggregated. Input rows need not be sorted. The package validates the mechanical requirements above, but it cannot verify study-design assumptions such as independent censoring. ## Scope and limitations The current public API supports one event of interest and one optional competing event. It does not currently provide: - confidence intervals, standard errors, or hypothesis tests; - covariate-adjusted or causal estimates; - left truncation, interval censoring, or time-varying covariates; - survey weights or clustered variance estimates; - automatic missing-data handling. For group-specific estimates, split the data explicitly. Differences between groups remain unadjusted and should not be interpreted as causal effects. ## Boundary behavior Predictions are step functions. A requested horizon between observed times uses the most recent estimate at or before that horizon. A horizon before the first observation returns state probabilities `(1, 0, 0)`. ## Output-name compatibility Correctly spelled transition columns are: - `transition_probabilities_to_1_at_times` - `transition_probabilities_to_2_at_times` The historical misspelled names beginning with `trainsition_` remain in the event table as backward-compatible aliases. New code should use the correctly spelled names. ## Before reporting results Confirm the outcome coding, time origin, time unit, censoring definition, and the number at risk near the horizons you report. Inspect the event table rather than relying only on fixed-horizon output.