---
title: "Building an Adverse Events Table"
output:
  rmarkdown::html_vignette:
    toc: true
vignette: >
  %\VignetteIndexEntry{Building an Adverse Events Table}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
library(tplyr2)
library(knitr)
tplyr2_options(IBMRounding = FALSE)
```

## Introduction

The adverse events (AE) table is the most common safety display in a clinical study report, and it exercises more of tplyr2 than almost any other single table: hierarchical (system-organ-class by preferred-term) counts, subject-level incidence, population-based denominators, an overall summary row, sorting, and often a between-arm comparison. Those pieces are documented individually across several vignettes; this one puts them together and builds a complete AE table step by step.

We use the bundled `tplyr_adae` (one row per adverse event) and `tplyr_adsl` (one row per subject) datasets, which follow the CDISC ADaM structure.

## The Population Principle

The single most important thing to get right in an AE table is the **denominator**. `tplyr_adae` contains only subjects who experienced at least one event; if you count it on its own, every percentage is computed against "subjects who had an AE" instead of the number of subjects at risk, and the incidences come out far too high.

The denominator must come from the population dataset -- here `ADSL`, the safety population. You wire it up in two places: a `pop_data()` mapping in the spec, and the population data frame supplied at build time. Throughout this vignette the spec carries

```r
pop_data = pop_data(cols = c("TRTA" = "TRT01A"))
```

which maps the treatment variable in the analysis data (`TRTA`, actual treatment) to its counterpart in the population data (`TRT01A`). We also restrict the analysis data to treatment-emergent events with a table-level `where`, while the denominators continue to come from the full population. (See `vignette("count")` and `vignette("denom")` for the underlying mechanics.)

## Step 1: Incidence by System Organ Class and Preferred Term

An AE table is a nested count: system organ class (`AEBODSYS`) as the outer level, preferred term (`AEDECOD`) as the inner level. Passing a two-element vector to `group_count()` produces the hierarchy, and `distinct_by = "USUBJID"` counts each subject once (a subject with three headaches counts once for HEADACHE).

```{r step1}
ae_settings <- layer_settings(
  distinct_by = "USUBJID",
  format_strings = list(
    n_counts = f_str("xxx (xx.x%)", "distinct_n", "distinct_pct")
  )
)

spec <- tplyr_spec(
  cols = "TRTA",
  where = TRTEMFL == "Y",
  pop_data = pop_data(cols = c("TRTA" = "TRT01A")),
  layers = tplyr_layers(
    group_count(c("AEBODSYS", "AEDECOD"), settings = ae_settings)
  )
)

result <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl)
kable(head(result[, c("rowlabel1", "rowlabel2", "res1", "res2", "res3")], 12))
```

Outer (system organ class) rows carry an empty `rowlabel2` and count the distinct subjects with *any* event in that class; inner (preferred term) rows carry the term in `rowlabel2`. Because the denominators come from `ADSL`, the column N is the full arm size -- confirm it with `tplyr_header_n()`:

```{r step1-headern}
kable(tplyr_header_n(result))
```

## Step 2: "Any Adverse Event" and Subjects With No Events

Two summary rows round out the table. `total_row = TRUE` adds an overall row -- the number of subjects with any adverse event -- and `missing_subjects = TRUE` adds a row for subjects who are in the population but have no records in the analysis data (a large share of the safety population for most studies). Both depend on population data being present.

```{r step2}
ae_settings <- layer_settings(
  distinct_by = "USUBJID",
  total_row = TRUE,
  total_row_label = "Any adverse event",
  missing_subjects = TRUE,
  missing_subjects_label = "No adverse events",
  format_strings = list(
    n_counts = f_str("xxx (xx.x%)", "distinct_n", "distinct_pct")
  )
)

spec <- tplyr_spec(
  cols = "TRTA",
  where = TRTEMFL == "Y",
  pop_data = pop_data(cols = c("TRTA" = "TRT01A")),
  layers = tplyr_layers(
    group_count(c("AEBODSYS", "AEDECOD"), settings = ae_settings)
  )
)

result <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl)

# The "Any adverse event" and "No adverse events" summary rows
summary_rows <- result[result$rowlabel1 %in% c("Any adverse event", "No adverse events"), ]
kable(summary_rows[, c("rowlabel1", "res1", "res2", "res3")])
```

## Step 3: A Between-Arm Comparison

Reviewers frequently want a comparison against the control arm on every row. `assoc_test()` in its pairwise mode does this: supply a `reference` arm and the `comparisons` to make, and a function that takes a 2x2 incidence matrix and returns a p-value. It emits one `pval` column per comparison, with a value on **every** row of the table -- each preferred term, each system-organ-class subtotal, and (unless you turn it off) the "Any adverse event" total row. The 2x2 uses each row's distinct counts and the population denominators, so it is consistent with the incidences on display.

```{r step3}
ae_settings <- layer_settings(
  distinct_by = "USUBJID",
  total_row = TRUE,
  total_row_label = "Any adverse event",
  format_strings = list(
    n_counts = f_str("xxx (xx.x%)", "distinct_n", "distinct_pct")
  ),
  assoc_test = assoc_test(
    fn = function(m) suppressWarnings(fisher.test(m)$p.value),
    reference   = "Placebo",
    comparisons = c("Xanomeline High Dose", "Xanomeline Low Dose"),
    format      = f_str("x.xxx", "p")
  )
)

spec <- tplyr_spec(
  cols = "TRTA",
  where = TRTEMFL == "Y",
  pop_data = pop_data(cols = c("TRTA" = "TRT01A")),
  layers = tplyr_layers(
    group_count(c("AEBODSYS", "AEDECOD"), settings = ae_settings)
  )
)

result <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl)
kable(head(result[, c("rowlabel1", "rowlabel2", "res1", "res2", "res3",
                      "pval1", "pval2")], 12))
```

The two `pval` columns compare each Xanomeline arm to Placebo. `assoc_test()` is fully general -- any test that takes a 2x2 matrix plugs in, and the function can even return a finished display string (a significance flag, a `>.99` ceiling) instead of a number. `vignette("binding-statistics")` covers the full contract, including nested layers, the total-row toggle, and character returns.

> **Risk difference vs. association test on a nested layer.** `risk_diff` (see `vignette("riskdiff")`) is the tool for a *single-level* count layer -- it emits a risk difference with a confidence interval. For the *nested* SOC/PT layout above, use pairwise `assoc_test()` as shown here.

## Step 4: A "Most Frequent Events" Variant, With Risk Difference

A common companion display drops the body-system hierarchy and lists preferred terms overall, sorted by descending frequency, often with a risk difference. This is a *single-level* count layer, which is where `risk_diff` applies (on a nested layer, use the pairwise `assoc_test()` from Step 3).

```{r step4}
spec <- tplyr_spec(
  cols = "TRTA",
  where = TRTEMFL == "Y",
  pop_data = pop_data(cols = c("TRTA" = "TRT01A")),
  layers = tplyr_layers(
    group_count("AEDECOD",
      settings = layer_settings(
        distinct_by = "USUBJID",
        order_count_method = "bycount",
        result_order_var = "distinct_n",
        format_strings = list(
          n_counts = f_str("xxx (xx.x%)", "distinct_n", "distinct_pct")
        ),
        risk_diff = list(
          comparisons = list(c("Xanomeline High Dose", "Placebo")),
          format = f_str("xx.x (xx.x, xx.x)", "rdiff", "lower", "upper")
        )
      )
    )
  )
)

result <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl)
result <- result[order(result$ord_layer_1), ]

kable(head(result[, c("rowlabel1", "res1", "res2", "res3", "rdiff1")], 10))
```

`order_count_method = "bycount"` with `result_order_var = "distinct_n"` sorts the preferred terms by the number of distinct subjects affected, descending, so the most frequent events lead. The `rdiff1` column is the High Dose minus Placebo incidence difference with its 95% confidence interval. (`risk_diff` applies to single-level count layers; on the nested layer of Step 3 use the pairwise `assoc_test()` shown there.)

## Step 5: A Display-Ready Table

The final step turns the machine-friendly build into something a renderer can consume. `as_display()` trims the frame to just the display columns (`rowlabel*`, `res*`, `pval*`), dropping the internal `ord_*` columns and, with `labels = TRUE`, renaming the result columns to their `(N=)` header labels. `collapse_row_labels()` then merges the two row-label columns into a single indented SOC/PT stub. Applying `as_display()` first keeps the header labels while leaving the `rowlabel*` columns for `collapse_row_labels()` to fold together.

```{r step5}
# Rebuild the Step 3 table (nested + Fisher p-values)
result <- tplyr_build(tplyr_spec(
  cols = "TRTA",
  where = TRTEMFL == "Y",
  pop_data = pop_data(cols = c("TRTA" = "TRT01A")),
  layers = tplyr_layers(
    group_count(c("AEBODSYS", "AEDECOD"), settings = ae_settings)
  )
), tplyr_adae, pop_data = tplyr_adsl)

display <- result |>
  as_display(labels = TRUE) |>
  collapse_row_labels("rowlabel1", "rowlabel2", indent = "   ")

kable(head(display, 14))
```

The result reads like the final table shell: an indented SOC/PT stub column, one incidence column per arm carrying its `(N=)` header, and the two comparison p-value columns -- built entirely within tplyr2, with the safety-population denominators applied throughout.

## See Also

- `vignette("count")` -- count-layer fundamentals, population data, nested counts, and stat columns.
- `vignette("denom")` -- denominator control in depth, including single-proportion confidence intervals and the "no events reported" row.
- `vignette("riskdiff")` -- risk differences on single-level count layers.
- `vignette("binding-statistics")` -- the full `assoc_test()` contract (omnibus, pairwise, nested, character returns) and binding externally computed model results.
- `vignette("sort")` -- ordering rows by frequency, factor levels, or a VARN companion.
- `vignette("post_processing")` -- `collapse_row_labels()`, `as_display()`, and other display helpers.
