6  AE Listing

Objectives

This guide demonstrates AE listing showing detailed individual adverse event records with key information like severity, relationship, and outcomes. For design philosophy and architecture details, see ae_summary.qmd.

6.1 Setup

import os
import sys
from pathlib import Path
import polars as pl

sys.path.insert(0, 'src')

from rtflite import LibreOfficeConverter
try:
    converter = LibreOfficeConverter()
except Exception:
    converter = None
    print("WARNING: LibreOffice not found. PDF conversion will be skipped.")
from csrlite import load_plan, study_plan_to_ae_listing
from csrlite.ae.ae_listing import ae_listing_ard, ae_listing_rtf, ae_listing

6.2 What’s Different?

AE Summary shows high-level aggregated counts (e.g., “12 (14.3%)” subjects with any AE). Uses a three-step pipeline with data pivoting.

AE Specific shows aggregated counts by individual event terms (e.g., “5 (6.0%)” subjects with “Diarrhea”). Uses a three-step pipeline with data pivoting.

AE Listing shows detailed individual event records with information like subject ID, study day, severity, relationship, action taken, and outcome. Uses a simpler two-step pipeline (no pivoting needed). Can access both population (demographics like SEX, RACE, AGE) and observation (event details) columns, with optional grouping by subject or treatment for section headers.

6.3 StudyPlan-Driven Workflow

study_plan = load_plan("studies/xyz123/yaml/plan_xyz123.yaml")
study_plan.get_plan_df().filter(pl.col("analysis") == "ae_listing")
2026-02-03 15:26:20,068 - csrlite.common.plan - INFO - Successfully loaded dataset 'adsl' from 'studies/xyz123/yaml/../../../data/adsl.parquet'
2026-02-03 15:26:20,072 - csrlite.common.plan - INFO - Successfully loaded dataset 'adae' from 'studies/xyz123/yaml/../../../data/adae.parquet'
2026-02-03 15:26:20,073 - csrlite.common.plan - INFO - Successfully loaded dataset 'adie' from 'studies/xyz123/yaml/../../../data/adie.parquet'
2026-02-03 15:26:20,075 - csrlite.common.plan - INFO - Successfully loaded dataset 'adpd' from 'studies/xyz123/yaml/../../../data/adpd.parquet'
shape: (4, 5)
analysis population observation parameter group
str str str str str
"ae_listing" "apat" "week12" "ser" "trt01a"
"ae_listing" "apat" "week24" "ser" "trt01a"
"ae_listing" "apat" "week12" "ser" "trt01a"
"ae_listing" "apat" "week24" "ser" "trt01a"
output_files = study_plan_to_ae_listing(study_plan)
studies/xyz123/rtf/ae_listing_apat_week12_ser.rtf
studies/xyz123/rtf/ae_listing_apat_week24_ser.rtf
studies/xyz123/rtf/ae_listing_apat_week12_ser.rtf
studies/xyz123/rtf/ae_listing_apat_week24_ser.rtf

6.3.1 week12_ser

6.3.2 week24_ser

6.4 Complete Pipeline

adsl = pl.read_parquet("data/adsl.parquet")
adae = pl.read_parquet("data/adae.parquet")

ae_listing(
    population=adsl,
    observation=adae,
    population_filter="SAFFL = 'Y'",
    observation_filter="TRTEMFL = 'Y'",
    parameter_filter=None,
    id=("USUBJID", "Subject ID"),
    title=[
        "Listing of Participants With Adverse Events",
        "(Treatment-Emergent, Safety Population)"
    ],
    footnote=["Adverse events are sorted by subject ID and study day."],
    source=["Source: ADSL and ADAE datasets"],
    output_file="studies/xyz123/rtf/ae_listing.rtf",
    population_columns=[
        ("TRT01A", "Treatment")
    ],
    observation_columns=[
        ("AEDECOD", "Adverse Event"),
        ("ASTDY", "Study Day"),   
    ],
    sort_columns=["TRT01A", "USUBJID", "ASTDY"],
    group_by=["USUBJID"],
    page_by=["TRT01A"],
)
studies/xyz123/rtf/ae_listing.rtf
'studies/xyz123/rtf/ae_listing.rtf'

6.5 Step-by-Step Pipeline

6.5.1 Step 1: Generate ARD

Key Parameters: - population_columns: List of tuples (column_name, label) from population (ADSL) to include (e.g., [("SEX", "Sex"), ("RACE", "Race")]). - observation_columns: List of tuples (column_name, label) from observation (ADAE) to include (e.g., [("AEDECOD", "Adverse Event")]). - sort_columns: List of column names to sort by. If None (default), sorts by id column. - parameter_filter: Optional SQL WHERE clause for filtering AEs (e.g., “AESER = ‘Y’” for serious AEs) - page_by: Optional list of columns to create an __index__ column for pagination.

Example with default (ID only):

_ard_minimal = ae_listing_ard(
    population=adsl,
    observation=adae,
    population_filter="SAFFL = 'Y'",
    observation_filter="TRTEMFL = 'Y'",
    parameter_filter=None,
    id=("USUBJID", "Subject ID"),
)

_ard_minimal.head(5)
shape: (5, 2)
__index__ USUBJID
str str
"Subject ID = 01-701-1015" "01-701-1015"
"Subject ID = 01-701-1015" "01-701-1015"
"Subject ID = 01-701-1015" "01-701-1015"
"Subject ID = 01-701-1023" "01-701-1023"
"Subject ID = 01-701-1023" "01-701-1023"

Example with explicit columns and pagination:

population_columns = [
    ("SEX", "Sex"),
    ("RACE", "Race"),
    ("AGE", "Age"),
    ("TRT01A", "Treatment")
]
observation_columns = [
    ("AEDECOD", "Adverse Event"),
    ("ASTDY", "Study Day"),
]

_ard = ae_listing_ard(
    population=adsl,
    observation=adae,
    population_filter="SAFFL = 'Y'",
    observation_filter="TRTEMFL = 'Y'",
    parameter_filter=None,
    id=("USUBJID", "Subject ID"),
    population_columns=population_columns,
    observation_columns=observation_columns,
    sort_columns=["TRT01A", "USUBJID", "ASTDY"],
    page_by=["USUBJID", "AGE", "SEX", "RACE", "TRT01A"],
)

_ard.head(10)
shape: (10, 4)
__index__ USUBJID AEDECOD ASTDY
str str str f64
"Subject ID = 01-701-1015, Age … "01-701-1015" "APPLICATION SITE ERYTHEMA" 2.0
"Subject ID = 01-701-1015, Age … "01-701-1015" "APPLICATION SITE PRURITUS" 2.0
"Subject ID = 01-701-1015, Age … "01-701-1015" "DIARRHOEA" 8.0
"Subject ID = 01-701-1023, Age … "01-701-1023" "ERYTHEMA" 3.0
"Subject ID = 01-701-1023, Age … "01-701-1023" "ERYTHEMA" 3.0
"Subject ID = 01-701-1023, Age … "01-701-1023" "ERYTHEMA" 3.0
"Subject ID = 01-701-1023, Age … "01-701-1023" "ATRIOVENTRICULAR BLOCK SECOND … 22.0
"Subject ID = 01-701-1028, Age … "01-701-1028" "APPLICATION SITE ERYTHEMA" 3.0
"Subject ID = 01-701-1028, Age … "01-701-1028" "APPLICATION SITE PRURITUS" 21.0
"Subject ID = 01-701-1034, Age … "01-701-1034" "APPLICATION SITE PRURITUS" 58.0

Output Structure: Filtered observation records joined with population data, containing selected columns from both datasets, sorted as specified.

6.5.2 Step 2: Generate RTF Output

The ARD from step 1 is already display-ready (filtered and sorted), so we can generate RTF directly.

Key Parameters: - column_labels: A dictionary mapping column names to their display labels. - group_by: Optional list of column names to group by for section headers.

First, let’s create the column_labels dictionary:

id_col = ("USUBJID", "Subject ID")
column_labels = dict([id_col] + population_columns + observation_columns)

ae_listing_rtf(
    _ard.head(10),
    column_labels=column_labels,
    title=[
        "Adverse Event Listing",
        "(Treatment-Emergent, Safety Population)"
    ],
    footnote=["Adverse events are sorted by treatment, subject ID, and study day."],
    source=["Source: ADSL and ADAE datasets"],
    group_by=["USUBJID"],
    page_by=["__index__"],
).write_rtf("studies/xyz123/rtf/ae_listing_step.rtf")
studies/xyz123/rtf/ae_listing_step.rtf

6.6 Customizing Columns, Grouping, and Pagination

You can customize which columns to display, how to group sections, and how to paginate the output.

ae_listing(
    population=adsl,
    observation=adae,
    population_filter="SAFFL = 'Y'",
    observation_filter="TRTEMFL = 'Y'",
    parameter_filter="AESER = 'Y'",  # Only serious events
    id=("USUBJID", "Subject ID"),
    title=[
        "Serious Adverse Event Listing",
        "(Treatment-Emergent, Safety Population)"
    ],
    footnote=["Listing includes only serious adverse events."],
    source=["Source: ADSL and ADAE datasets"],
    output_file="studies/xyz123/rtf/ae_listing_custom.rtf",
    population_columns=[("SEX", "Sex"), ("AGE", "Age"), ("TRT01A", "Treatment")],  # Minimal demographics
    observation_columns=[
        ("AEDECOD", "Adverse Event"),
        ("AESEV", "Severity"),
        ("AEREL", "Related"),
        ("AEOUT", "Outcome")
    ],  # Key event details
    sort_columns=["TRT01A", "USUBJID"],  # Sort by treatment and subject
    group_by=["USUBJID"],  # Group by subject for section headers
    page_by=["TRT01A", "USUBJID", "AGE", "SEX"],  # Create a new page for each treatment group
)
studies/xyz123/rtf/ae_listing_custom.rtf
'studies/xyz123/rtf/ae_listing_custom.rtf'