The Adverse Event Table in R: Summarize TEAEs by SOC and Preferred Term

Build the safety table every Clinical Study Report carries — treatment-emergent adverse events by System Organ Class and Preferred Term, split by treatment arm — with the rtables and tern layout engine on public pharmaverse ADAE data

Pharma & Clinical

A complete, runnable tutorial for the adverse-event summary table — the safety TEAE table by System Organ Class (SOC) and Preferred Term (PT). Learn what the table is (an overall “subjects with at least one treatment-emergent AE” summary, then nested SOC/PT counts as n (%) of subjects, where the denominator is the safety population), then build it with rtables and tern: summarize_num_patients() for the overall and per-SOC patient counts, count_occurrences() for the preferred-term frequencies, and alt_counts_df for the subject-level denominator — on public pharmaverseadam ADAE data, so every line runs.

Published

June 30, 2026

Modified

July 7, 2026

TipKey takeaways
  • The adverse-event (AE) summary table is the safety table every Clinical Study Report carries — treatment-emergent adverse events (TEAEs) summarized by System Organ Class (SOC) and nested Preferred Term (PT), split by treatment arm.
  • Count subjects, not events. The numbers are n (%) of subjects with at least one such event — a subject who reports pruritus three times is counted once. Counting raw rows counts events, which is the classic AE-table error.
  • The denominator is the safety population. Every percentage is out of the column’s arm N (the (N=xx) header), taken from the subject-level ADSL — supplied to rtables through alt_counts_df, never from the AE records themselves.
  • Two tern functions build it. summarize_num_patients() gives the overall and per-SOC “subjects with ≥1 AE” rows; count_occurrences() gives the per-preferred-term frequencies.
  • The published table is sorted by frequency and thresholded. The full table runs to hundreds of preferred terms; a submission table sorts SOCs and PTs by descending frequency and keeps the AEs occurring in ≥ a chosen percent of any arm.

Introduction

After the demographic table, the next output a reviewer turns to is the adverse-event summary table — the heart of the safety section of a Clinical Study Report (CSR). It answers the question every safety read-out exists to answer: what adverse events occurred, how often, and was there more on the active arms than on placebo? Events are grouped by System Organ Class (SOC), listed by Preferred Term (PT) within each class, and counted as the number and percent of subjects affected in each treatment arm.

This is a table in the pharma TLF (tables, listings, and figures) workflow, and it is built from one analysis dataset: ADAE, the adverse-events analysis dataset (one row per adverse-event record). This lesson builds the table the industry way — with the rtables layout engine and its clinical companion tern — on public pharmaverse data, so every line runs as written. It follows tern’s canonical adverse-event table vignette, re-authored into a single guided build.

Here is where we are headed — the most frequent system organ classes by arm, computed straight from the ADAE we summarize below:

Grouped horizontal bar chart of the eight most frequent system organ classes for treatment-emergent adverse events in the CDISC pilot safety population. For each system organ class, three bars give the number of subjects affected on Placebo (azure), Xanomeline Low Dose (orange), and Xanomeline High Dose (green). General disorders and administration-site conditions and skin and subcutaneous tissue disorders dominate, and both active Xanomeline arms have far more subjects affected in those two skin-related classes than placebo, the expected safety signal for a transdermal patch.

The two skin-related classes — general disorders and administration-site conditions and skin and subcutaneous tissue disorders — dominate, and both active arms sit well above placebo there: the expected reaction to a transdermal patch. By the end of this lesson you will have produced that signal as a formal SOC/PT table. If “ADAE”, “treatment-emergent”, or “SOC/PT” are new, build the dataset first in Create ADAE in R with admiral, which derives the ADAE this table reads from.

What an adverse-event summary table is

An AE summary table is a count table with a fixed two-level anatomy: a top block summarizing AEs overall, then one block per System Organ Class (SOC) — a MedDRA (Medical Dictionary for Regulatory Activities) grouping of related body systems — with the Preferred Terms (PTs) inside it. The shape is conventional, so every one you build reads the same way:

Part What it is In this lesson
Overall summary Subjects with ≥1 TEAE, and the total event count, per arm The top two rows
Row groups One block per System Organ Class Skin disorders, GI disorders, …
Rows within a group One row per Preferred Term Application-site pruritus, diarrhoea, …
Columns The treatment arms, plus a Total Placebo · Xanomeline Low · Xanomeline High · All Patients
Cell statistic n (%) of subjects affected in that arm 21 (29.2%)
Column header (N=) The arm’s safety population size The denominator of every percentage

Two conventions decide whether the table is right:

  • You count subjects, not events. A “treatment-emergent adverse event (TEAE)” is one that started on or after first dose — the events safety tables count. But the table reports how many subjects had at least one such event, not how many events occurred. A subject who reports application-site pruritus on three separate days is one subject in that preferred term’s count. Getting this wrong — counting AE records — inflates every number and is the single most common AE-table mistake.
  • The percentage denominator is the safety population, not the number of AE records. A 29.2% for “application-site pruritus” on the high-dose arm means 29.2% of the high-dose safety population, taken from the subject-level dataset — never a percent of the AE rows. This is why the denominator has to be supplied separately, as you will see.

The data

We use the ADAE (adverse-events analysis dataset) and ADSL (subject-level analysis dataset) shipped in pharmaverseadam — the synthetic, license-free CDISC (Clinical Data Interchange Standards Consortium) pilot study comparing placebo against two doses of Xanomeline, delivered as a transdermal patch. ADAE already carries the coded AEBODSYS (system organ class) and AEDECOD (preferred term), the treatment arm TRT01A, and the treatment-emergent flag TRTEMFL, so no derivation is needed here — that work is done in the ADAE lesson.

Two filters set up the analysis. We keep the treatment-emergent events (TRTEMFL == "Y") for the AE records, and we summarize the safety population (SAFFL == "Y", the safety-population flag) — the standard population for a safety table, which drops the screen-failure subjects who were never dosed.

library(dplyr, warn.conflicts = FALSE)
library(pharmaverseadam)

arm_levels <- c("Placebo", "Xanomeline Low Dose", "Xanomeline High Dose")

# ADSL — one row per subject; the safety population is the denominator source
adsl <- pharmaverseadam::adsl %>%
  filter(SAFFL == "Y") %>%
  mutate(TRT01A = factor(TRT01A, levels = arm_levels))

# ADAE — one row per AE record; keep the treatment-emergent events for the safety population
adae <- pharmaverseadam::adae %>%
  filter(TRTEMFL == "Y", SAFFL == "Y") %>%
  mutate(TRT01A = factor(TRT01A, levels = arm_levels))

adae %>%
  select(USUBJID, TRT01A, AEBODSYS, AEDECOD, TRTEMFL) %>%
  head(5)
# A tibble: 5 × 5
  USUBJID     TRT01A  AEBODSYS                                   AEDECOD TRTEMFL
  <chr>       <fct>   <chr>                                      <chr>   <chr>  
1 01-701-1015 Placebo GENERAL DISORDERS AND ADMINISTRATION SITE… APPLIC… Y      
2 01-701-1015 Placebo GENERAL DISORDERS AND ADMINISTRATION SITE… APPLIC… Y      
3 01-701-1015 Placebo GASTROINTESTINAL DISORDERS                 DIARRH… Y      
4 01-701-1023 Placebo SKIN AND SUBCUTANEOUS TISSUE DISORDERS     ERYTHE… Y      
5 01-701-1023 Placebo SKIN AND SUBCUTANEOUS TISSUE DISORDERS     ERYTHE… Y      

Making TRT01A a factor with placebo first fixes the column order of the table (placebo, then ascending dose) instead of letting it fall alphabetical. After the filters, adsl holds the 254 dosed subjects (86 placebo, 96 low dose, 72 high dose) and adae holds their 1,122 treatment-emergent AE records. Those two objects — the events and the subject-level denominator — are everything the table needs.

Build the overall AE summary

Start at the top of the table: how many subjects on each arm had at least one treatment-emergent AE, and how many events were there in total? The tern layout-creating function for this is summarize_num_patients(), which counts unique subjects (each subject once) and non-unique records (every event). You read the layout top-to-bottom as a recipe; nothing is computed until build_table() applies it to the data.

library(rtables)
library(tern)

basic_table() %>%
  split_cols_by("TRT01A") %>%                       # one column per arm
  add_overall_col("All Patients") %>%               # the Total column
  add_colcounts() %>%                               # the (N=xx) header row
  summarize_num_patients(
    var    = "USUBJID",
    .stats = c("unique", "nonunique"),
    .labels = c(
      unique    = "Patients with at least one AE",
      nonunique = "Total number of events"
    )
  ) %>%
  build_table(df = adae, alt_counts_df = adsl)
                                 Placebo     Xanomeline Low Dose   Xanomeline High Dose   All Patients
                                  (N=86)           (N=96)                 (N=72)            (N=254)   
——————————————————————————————————————————————————————————————————————————————————————————————————————
Patients with at least one AE   65 (75.6%)       84 (87.5%)             68 (94.4%)        217 (85.4%) 
Total number of events             281               427                   414                1122    

The key argument is alt_counts_df = adsl. The table body is built from adae (the AE records), but the column N in the header — the percentage denominator — must come from the subjects, so we hand build_table() the subject-level ADSL as the alternative counts dataset. Read the result: 65 of 86 placebo subjects (75.6%) had a treatment-emergent AE, rising to 87.5% on low dose and 94.4% on high dose — more subjects affected on the active arms, the expected pattern. The second row counts events (1,122 in total), which is why it is larger than the subject counts above it. Without alt_counts_df, the percentages would be computed out of the AE-record count and be meaningless.

Build the table by system organ class and preferred term

The full table repeats that summary inside each system organ class, then lists the preferred term frequencies under it. Two more steps build it: split_rows_by("AEBODSYS") opens a row group per SOC, and count_occurrences() counts the subjects per preferred term (once per subject, even with repeat events).

lyt <- basic_table() %>%
  split_cols_by("TRT01A") %>%
  add_overall_col("All Patients") %>%
  add_colcounts() %>%
  # top block: overall subjects-with-an-AE summary
  summarize_num_patients(
    var = "USUBJID", .stats = c("unique", "nonunique"),
    .labels = c(unique = "Patients with at least one AE",
                nonunique = "Total number of events")
  ) %>%
  # one row group per system organ class
  split_rows_by(
    "AEBODSYS", child_labels = "visible", nested = FALSE,
    indent_mod = -1L, split_fun = drop_split_levels
  ) %>%
  # per-SOC subjects-with-an-AE-in-this-class summary
  summarize_num_patients(
    var = "USUBJID", .stats = "unique",
    .labels = c(unique = "Patients with at least one AE in this class")
  ) %>%
  # preferred-term frequencies within each class
  count_occurrences(vars = "AEDECOD")

tbl <- build_table(lyt, df = adae, alt_counts_df = adsl)

nrow(tbl)
[1] 278

This is a complete, correct AE table — but it has 278 rows, because the pilot study recorded a preferred term for nearly every kind of event. A submission never prints all of them: it sorts by frequency and applies a reporting threshold. We handle that next. The point here is that lyt is a reusable layout — a description of the table, not a result — so the same object can be re-applied to any dataset (a subgroup, a different study) without re-specifying anything.

Make it the published table: sort by frequency and threshold

A real safety table is sorted by descending frequency (most common SOCs and PTs first, not alphabetical) and limited to the AEs that occur often enough to matter — conventionally those in at least a chosen percent of subjects in any arm (5% is a common cut). First select the preferred terms that clear the threshold:

# subjects per arm (the denominators)
arm_n <- table(adsl$TRT01A)

# keep preferred terms reported by >=5% of subjects in ANY arm
pt_keep <- adae %>%
  distinct(USUBJID, TRT01A, AEDECOD) %>%               # subject-level: one row per subject x PT
  count(TRT01A, AEDECOD, name = "n_subj") %>%
  mutate(pct = 100 * n_subj / as.numeric(arm_n[as.character(TRT01A)])) %>%
  group_by(AEDECOD) %>%
  summarise(max_pct = max(pct), .groups = "drop") %>%
  filter(max_pct >= 5) %>%
  pull(AEDECOD)

length(pt_keep)
[1] 24

Then rebuild the table on those preferred terms and sort it. prune_table() drops any empty rows, sort_at_path() orders the system organ classes by total subject count (cont_n_allcols) and the preferred terms within each class by frequency (score_occurrences):

adae_common <- adae %>%
  filter(AEDECOD %in% pt_keep) %>%
  mutate(AEBODSYS = as.factor(AEBODSYS), AEDECOD = as.factor(AEDECOD))

basic_table(
    title    = "Table 14-3.01",
    subtitles = "Treatment-Emergent Adverse Events in >=5% of Any Arm (Safety Population)"
  ) %>%
  split_cols_by("TRT01A") %>%
  add_overall_col("All Patients") %>%
  add_colcounts() %>%
  split_rows_by(
    "AEBODSYS", child_labels = "visible", nested = FALSE,
    indent_mod = -1L, split_fun = drop_split_levels
  ) %>%
  summarize_num_patients(
    var = "USUBJID", .stats = "unique",
    .labels = c(unique = "Patients with at least one AE in this class")
  ) %>%
  count_occurrences(vars = "AEDECOD") %>%
  build_table(df = adae_common, alt_counts_df = adsl) %>%
  prune_table() %>%
  sort_at_path(path = "AEBODSYS", scorefun = cont_n_allcols) %>%
  sort_at_path(path = c("AEBODSYS", "*", "AEDECOD"), scorefun = score_occurrences)
Table 14-3.01
Treatment-Emergent Adverse Events in >=5% of Any Arm (Safety Population)

—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
                                                        Placebo     Xanomeline Low Dose   Xanomeline High Dose   All Patients
                                                         (N=86)           (N=96)                 (N=72)            (N=254)   
—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
SKIN AND SUBCUTANEOUS TISSUE DISORDERS                                                                                       
Patients with at least one AE in this class            19 (22.1%)       36 (37.5%)             35 (48.6%)         90 (35.4%) 
  PRURITUS                                              8 (9.3%)        21 (21.9%)             25 (34.7%)         54 (21.3%) 
  ERYTHEMA                                              8 (9.3%)        14 (14.6%)             14 (19.4%)         36 (14.2%) 
  RASH                                                  5 (5.8%)        13 (13.5%)             8 (11.1%)          26 (10.2%) 
  HYPERHIDROSIS                                         2 (2.3%)         4 (4.2%)              8 (11.1%)          14 (5.5%)  
  SKIN IRRITATION                                       3 (3.5%)         6 (6.2%)               5 (6.9%)          14 (5.5%)  
  BLISTER                                                  0             5 (5.2%)               1 (1.4%)           6 (2.4%)  
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS                                                                         
Patients with at least one AE in this class            16 (18.6%)       39 (40.6%)             31 (43.1%)         86 (33.9%) 
  APPLICATION SITE PRURITUS                             6 (7.0%)        23 (24.0%)             21 (29.2%)         50 (19.7%) 
  APPLICATION SITE ERYTHEMA                             3 (3.5%)        13 (13.5%)             14 (19.4%)         30 (11.8%) 
  APPLICATION SITE DERMATITIS                           5 (5.8%)         9 (9.4%)               7 (9.7%)          21 (8.3%)  
  APPLICATION SITE IRRITATION                           3 (3.5%)         9 (9.4%)              9 (12.5%)          21 (8.3%)  
  APPLICATION SITE VESICLES                             1 (1.2%)         5 (5.2%)               5 (6.9%)          11 (4.3%)  
  FATIGUE                                               1 (1.2%)         5 (5.2%)               5 (6.9%)          11 (4.3%)  
GASTROINTESTINAL DISORDERS                                                                                                   
Patients with at least one AE in this class            12 (14.0%)       10 (10.4%)             16 (22.2%)         38 (15.0%) 
  DIARRHOEA                                            9 (10.5%)         5 (5.2%)               3 (4.2%)          17 (6.7%)  
  VOMITING                                              3 (3.5%)         4 (4.2%)               6 (8.3%)          13 (5.1%)  
  NAUSEA                                                3 (3.5%)         3 (3.1%)               6 (8.3%)          12 (4.7%)  
  SALIVARY HYPERSECRETION                                  0                 0                  4 (5.6%)           4 (1.6%)  
NERVOUS SYSTEM DISORDERS                                                                                                     
Patients with at least one AE in this class             4 (4.7%)        14 (14.6%)             15 (20.8%)         33 (13.0%) 
  DIZZINESS                                             2 (2.3%)         9 (9.4%)              10 (13.9%)         21 (8.3%)  
  HEADACHE                                              3 (3.5%)         3 (3.1%)               5 (6.9%)          11 (4.3%)  
  SYNCOPE                                                  0             5 (5.2%)               2 (2.8%)           7 (2.8%)  
CARDIAC DISORDERS                                                                                                            
Patients with at least one AE in this class             6 (7.0%)         8 (8.3%)              10 (13.9%)         24 (9.4%)  
  SINUS BRADYCARDIA                                     2 (2.3%)         7 (7.3%)              8 (11.1%)          17 (6.7%)  
  MYOCARDIAL INFARCTION                                 4 (4.7%)         2 (2.1%)               4 (5.6%)          10 (3.9%)  
INFECTIONS AND INFESTATIONS                                                                                                  
Patients with at least one AE in this class             8 (9.3%)         5 (5.2%)              9 (12.5%)          22 (8.7%)  
  NASOPHARYNGITIS                                       2 (2.3%)         4 (4.2%)               6 (8.3%)          12 (4.7%)  
  UPPER RESPIRATORY TRACT INFECTION                     6 (7.0%)         1 (1.0%)               3 (4.2%)          10 (3.9%)  
RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS                                                                              
Patients with at least one AE in this class             1 (1.2%)         5 (5.2%)               5 (6.9%)          11 (4.3%)  
  COUGH                                                 1 (1.2%)         5 (5.2%)               5 (6.9%)          11 (4.3%)  

Now the table reads like a published safety output. The two skin-related classes lead, exactly as the hero figure promised. Application-site pruritus is the standout preferred term — 7.0% on placebo against 24.0% and 29.2% on the two Xanomeline arms — followed by application-site erythema and dermatitis. The pattern is coherent and clinically obvious: Xanomeline here is a transdermal patch, so skin and application-site reactions are the dominant signal, far above placebo, while systemic classes (gastrointestinal, nervous-system) are more evenly spread. That contrast — active arms well above placebo on the skin classes — is precisely what a safety reviewer reads this table to find.

A quick gtsummary alternative — and when to use each

For fast exploration or a manuscript, gtsummary gives a polished HTML version of the overall summary in a couple of calls. It is excellent for the high-level “subjects with an AE per arm” view:

library(gtsummary)

adsl %>%
  mutate(any_teae = USUBJID %in% adae$USUBJID) %>%    # did the subject have a TEAE?
  select(TRT01A, any_teae) %>%
  tbl_summary(
    by = TRT01A,
    label = any_teae ~ "Patients with at least one TEAE",
    statistic = any_teae ~ "{n} ({p}%)"
  ) %>%
  add_overall(last = TRUE) %>%
  modify_header(label ~ "**Adverse events**") %>%
  bold_labels()
Adverse events Placebo
N = 861
Xanomeline Low Dose
N = 961
Xanomeline High Dose
N = 721
Overall
N = 2541
Patients with at least one TEAE 65 (76%) 84 (88%) 68 (94%) 217 (85%)
1 n (%)

The numbers match the tern overview exactly — 65 (76%) on placebo, and so on — because both read the same subjects. Use this as the decision cue between the two engines:

gtsummary rtables + tern
Best for Fast exploration, manuscripts, the high-level AE summary The full SOC/PT safety table for a submission
Nested SOC → PT table Possible, but tern is purpose-built for it The standard, with sorting/pruning/thresholds
Denominator control tbl_summary on the subject data alt_counts_df (subjects) vs df (events)
Output Polished HTML / gt Monospaced, paginated, RTF-ready
Industry role Academia + early analysis The pharma standard for validated TLFs

In practice teams use gtsummary to explore and draft, then rtables/tern for the validated SOC/PT table that goes into the submission. They are complementary, not competing.

🟢 With an AI agent

Ask Prova “how do I add a column showing the risk difference versus placebo to my tern adverse event table?” — it answers grounded in this pillar’s lessons, with runnable code you can try on the example pharmaverse data. The runtime is the judge. Ask Prova →

Common issues

Your counts are far too high — you counted events, not subjects. An AE table reports subjects with at least one event, but adae has many rows per subject. summarize_num_patients() and count_occurrences() both count unique subjects for you, which is exactly why you use them instead of count() on the raw rows. If a “subjects with an AE” number exceeds the arm’s N, you are counting AE records.

Every percentage is out of the wrong total. If your percentages look too small, you forgot alt_counts_df = adsl. The table body comes from the AE records, but the denominator must be the subject-level safety population — alt_counts_df is what supplies it. Without it, tern divides by the number of AE rows in each column and the (N=xx) header is wrong.

The table is alphabetical instead of frequency-ordered. By default rtables lists system organ classes and preferred terms in factor/alphabetical order. A published safety table is sorted by descending frequency. Use sort_at_path() with cont_n_allcols (to order the SOCs) and score_occurrences (to order the PTs within each SOC), as shown above.

Pre-treatment events inflate the table. If you summarize the raw ADAE without filter(TRTEMFL == "Y"), events that started before first dose (often medical history captured as AEs) leak into the counts. A TEAE table counts treatment-emergent events only — filter on TRTEMFL == "Y" first, every time.

Frequently asked questions

It is the safety table that summarizes treatment-emergent adverse events — those that started on or after first dose — by System Organ Class (SOC) and Preferred Term (PT), split by treatment arm. Each cell is the number and percent of subjects affected in that arm (not the number of events), with the percentage taken out of the arm’s safety-population size. It is the core table of the safety section of a Clinical Study Report, defined in the ICH E3 guideline on CSR structure.

Use rtables + tern: split the columns by treatment arm, add the column counts, then combine summarize_num_patients() for the “subjects with ≥1 AE” rows with count_occurrences(vars = "AEDECOD") for the preferred-term frequencies, inside a split_rows_by("AEBODSYS") for the system organ classes. Apply the layout with build_table(df = adae, alt_counts_df = adsl), where alt_counts_df supplies the subject-level denominator. The full build is worked through above on public pharmaverse data.

Out of subjects. The cell 21 (29.2%) means 21 subjects — 29.2% of that arm’s safety population — had at least one such event, no matter how many times each reported it. The denominator is the column N in the header, taken from the subject-level ADSL via alt_counts_df. Counting AE records instead of subjects (and dividing by the record count) is the most common AE-table error.

Compute, per preferred term, the percent of subjects affected in each arm, keep the terms whose maximum across arms is ≥ 5%, filter the ADAE to those terms, and rebuild the table. The threshold is a study choice (5% in any arm is common); the worked example above derives the eligible preferred terms with a distinct() + count() over subjects, then rebuilds and sorts the table.

Sort the built table with rtables::sort_at_path(). Order the system organ classes by total subject count with scorefun = cont_n_allcols at path = "AEBODSYS", and order the preferred terms within each class with scorefun = score_occurrences at path = c("AEBODSYS", "*", "AEDECOD"). Run prune_table() first to drop empty rows. Without this the table stays in factor/alphabetical order, which is not how a safety table is presented.

Use gtsummary for fast exploration, manuscripts, and the high-level “subjects with an AE per arm” summary — it renders polished HTML in a couple of calls. Use rtables/tern for the full nested SOC/PT safety table that goes into a regulatory submission: it is purpose-built for the layout, the subject-vs-event denominators (alt_counts_df), the frequency sorting, and the monospaced, RTF-exportable output a CSR needs. Many teams use both.

Test your understanding

Using the adsl and adae objects from this lesson, build an overall summary of serious treatment-emergent adverse events by treatment arm: the number and percent of subjects with at least one serious TEAE, plus the total number of serious events. Serious AEs are flagged AESER == "Y" in the data. Keep the safety-population denominator.

Serious AEs are a subset of the AE records, so filter adae to AESER == "Y" and pass that as df to build_table() — but keep alt_counts_df = adsl so the denominator stays the full safety population (the percent is “of all dosed subjects”, not “of those with a serious AE”). The layout is the same summarize_num_patients() overall summary you built first.

library(dplyr)
library(rtables)
library(tern)

adae_serious <- adae %>% filter(AESER == "Y")

basic_table() %>%
  split_cols_by("TRT01A") %>%
  add_overall_col("All Patients") %>%
  add_colcounts() %>%
  summarize_num_patients(
    var = "USUBJID", .stats = c("unique", "nonunique"),
    .labels = c(unique = "Patients with at least one serious AE",
                nonunique = "Total number of serious events")
  ) %>%
  build_table(df = adae_serious, alt_counts_df = adsl)

Serious AEs are far rarer than AEs overall, so the counts are small in every arm. The crucial detail is alt_counts_df = adsl: it keeps the denominator the full safety population, so a cell reads “of all dosed subjects on this arm, this many had a serious event” — the meaning a safety table needs. To extend it to a SOC/PT breakdown, add the same split_rows_by("AEBODSYS") + count_occurrences("AEDECOD") steps on adae_serious.

A. The number of adverse-event records (every occurrence) in that arm B. The number of subjects with at least one such event in that arm, out of the arm’s safety-population N C. The number of subjects with that event, out of everyone in the study

B. Each cell counts subjects (a subject with three episodes of the same preferred term is counted once), and the percentage denominator is that column’s arm N — the safety population, supplied through alt_counts_df. A is the event count (the “Total number of events” row, and the classic error if you count raw rows); C would use the grand total as the denominator instead of the arm’s N, which is what splitting by TRT01A avoids.

Conclusion

The adverse-event summary table is the safety read-out of a Clinical Study Report, and it comes down to two rules: count subjects, not events, and divide by the safety population. With rtables and tern those rules are built into the layout — summarize_num_patients() for the overall and per-class subject counts, count_occurrences() for the preferred-term frequencies, and alt_counts_df for the subject-level denominator. Add a frequency sort and a reporting threshold and you have the published table: system organ classes and preferred terms ordered by how often they occur, with the active-versus-placebo signal — here, the transdermal patch’s skin reactions — reading straight off the page. The layout is reusable, so the same object produces a subgroup table, a serious-AE table, or the next study’s table with no re-specification.

Note

This lesson is reproducible: every result on this page was produced by the code shown — copy any block and run it to reproduce them. The runtime is the judge.

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {The {Adverse} {Event} {Table} in {R:} {Summarize} {TEAEs} by
    {SOC} and {Preferred} {Term}},
  date = {2026-06-30},
  url = {https://www.datanovia.com/learn/pharma-clinical/04-tlf-generation/adverse-event-table-rtables},
  langid = {en}
}
For attribution, please cite this work as:
“The Adverse Event Table in R: Summarize TEAEs by SOC and Preferred Term.” 2026. June 30. https://www.datanovia.com/learn/pharma-clinical/04-tlf-generation/adverse-event-table-rtables.