Deriving the ADAE Adverse-Event Analysis Dataset
Every treatment-emergent adverse event (TEAE) table in a clinical study report rests on a single derived flag, TRTEMFL, and the most common way that table comes out wrong is a partial adverse-event start date that quietly gets treated as pre-treatment — dropping a real safety signal from the count. ADAE is the Occurrence Data Structure (OCCDS) analysis dataset for adverse events: one row per reported event, merged to the subject spine, with the treatment-emergent and occurrence flags that safety analyses run on. This page is the step-by-step companion to ADaM Dataset Derivation for Statistical Analysis and sits within the wider Clinical Data Architecture & EDC Standards reference where the EDC is an immutable, read-only source of truth. It is written for the Python ETL engineers and safety programmers who have to turn the SDTM AE domain and the ADSL spine into a defensible adverse-event analysis dataset.
The Treatment-Emergent Decision Flow
ADAE differs from a measurement dataset: there is no baseline or change-from-baseline, only occurrences classified against the treatment window. The derivation merges each AE to its subject’s treatment dates, imputes partial start dates, and then decides emergence.
Root Cause: Why TEAE Determination Is Error-Prone
The treatment-emergent flag is a date comparison, and clinical dates are the least reliable data in the study. An AE is treatment-emergent when it starts on or after the first dose (and, by many protocols, within a defined window after the last dose); everything before first dose is pre-treatment and excluded from the TEAE table. The comparison needs three dates aligned to the same subject — AESTDTC from AE, and TRTSDT/TRTEDT carried from ADSL — which is why the merge key and what it carries matter as much as the comparison itself. The failure surface is the partial date: AE frequently records a month or year only (2026-03), and a partial that is compared naively against a full TRTSDT either raises or, worse, silently sorts as pre-treatment. Because a dropped TEAE understates the safety profile, the imputation policy has to be conservative and visible, not incidental. The upstream mapping that produces a clean AE domain is covered in Mapping Adverse Events to the SDTM AE Domain.
Step-by-Step Implementation
Step 1: Merge AE to the ADSL Spine
ADAE is one row per AE occurrence, so unlike ADSL the grain is many-per-subject — but the merge must still be many-to-one against ADSL, carrying treatment dates and population context. USUBJID is the only safe key; merging on anything else risks attaching the wrong subject’s treatment window.
# ALCOA+ requirement: Attributable — each AE inherits its own subject's treatment
# dates via USUBJID; validate many_to_one so a broken spine fails at the merge.
import pandas as pd
def merge_ae_adsl(ae: pd.DataFrame, adsl: pd.DataFrame) -> pd.DataFrame:
carry = ["USUBJID", "TRT01P", "TRT01A", "TRTSDT", "TRTEDT", "SAFFL"]
adae = ae.merge(adsl[carry], on="USUBJID", how="inner", validate="many_to_one")
adae["TRTP"] = adae["TRT01P"] # planned treatment for the AE
adae["TRTA"] = adae["TRT01A"] # actual treatment — safety analyses use this
return adae
Step 2: Impute Partial AE Start Dates
Before any comparison, resolve partial AESTDTC values with a conservative, documented rule and record that the imputation happened. For a treatment-emergent determination the conservative direction is to not push a genuinely on-treatment event into the pre-treatment bucket, so impute missing day/month to the earliest plausible value and flag it.
# ALCOA+ requirement: Original + Accurate — partial dates are imputed to a documented
# rule and stamped with AESTDTCF, never silently coerced to a full date.
import pandas as pd
def impute_ae_start(adae: pd.DataFrame) -> pd.DataFrame:
raw = adae["AESTDTC"].astype("string")
parts = raw.str.len()
adae["ASTDT"] = pd.to_datetime(
raw.mask(parts == 7, raw + "-01") # YYYY-MM -> first of month
.mask(parts == 4, raw + "-01-01"), # YYYY -> first of year
errors="coerce",
)
adae["ASTDTF"] = raw.where(parts >= 10).isna().map( # imputation flag: D, M, or none
{True: "M", False: pd.NA}
)
return adae
Step 3: Derive the Treatment-Emergent Flag
With aligned dates, TRTEMFL is the window test: emergent when the imputed AE start is on or after TRTSDT. Protocols that count events for a period after last dose extend the upper bound with a defined window; anything failing the test is pre-treatment.
# ALCOA+ requirement: Accurate — TRTEMFL is derived from the treatment window,
# not collected; a null treatment start cannot be emergent by definition.
import pandas as pd
def derive_trtemfl(adae: pd.DataFrame, post_window_days: int = 30) -> pd.DataFrame:
trtsdt = pd.to_datetime(adae["TRTSDT"], errors="coerce")
trtedt = pd.to_datetime(adae["TRTEDT"], errors="coerce")
upper = trtedt + pd.to_timedelta(post_window_days, unit="D")
emergent = (
trtsdt.notna()
& adae["ASTDT"].notna()
& (adae["ASTDT"] >= trtsdt)
& (adae["ASTDT"] <= upper.fillna(pd.Timestamp.max))
)
adae["TRTEMFL"] = emergent.map({True: "Y", False: pd.NA}) # blank, not N, per convention
return adae
Step 4: Derive Analysis Relative Day and Toxicity Grade
ADY expresses the AE onset relative to treatment start as an integer study day, with the CDISC convention that has no day zero: days on or after TRTSDT are positive and start at 1, days before are negative. Toxicity grade carries the severity used in grade-by-grade summaries.
# ALCOA+ requirement: Consistent — ADY follows the CDISC no-day-zero convention so
# relative-day math is comparable across every ADaM dataset in the submission.
import pandas as pd
def derive_ady(adae: pd.DataFrame) -> pd.DataFrame:
trtsdt = pd.to_datetime(adae["TRTSDT"], errors="coerce")
delta = (adae["ASTDT"] - trtsdt).dt.days
# No day zero: >= start day is delta+1, before start day is delta (negative).
adae["ADY"] = delta.where(delta < 0, delta + 1).astype("Int64")
adae["ATOXGR"] = adae["AETOXGR"] # analysis toxicity grade, carried from AE
return adae
Step 5: Derive Occurrence Flags
Occurrence flags mark the first record of an event per subject so a subject who reports the same event three times is counted once in an incidence table. AOCCFL flags the first occurrence overall; AOCCPFL flags the first within a preferred term. Sort deterministically before flagging so the “first” is reproducible.
# ALCOA+ requirement: Complete — occurrence flags de-duplicate for incidence counts
# without deleting rows; a stable sort makes "first occurrence" reproducible.
import numpy as np
import pandas as pd
def derive_occurrence_flags(adae: pd.DataFrame) -> pd.DataFrame:
teae = adae[adae["TRTEMFL"].eq("Y")].sort_values(
["USUBJID", "ASTDT", "AESEQ"], kind="mergesort"
)
first_any = teae.groupby("USUBJID").head(1).index
first_pt = teae.groupby(["USUBJID", "AEDECOD"]).head(1).index
adae["AOCCFL"] = np.where(adae.index.isin(first_any), "Y", pd.NA) # first TEAE overall
adae["AOCCPFL"] = np.where(adae.index.isin(first_pt), "Y", pd.NA) # first per pref. term
return adae
Verification and Audit Trail
Because ADAE feeds the safety tables that regulators read first, its checks are archived as OQ evidence. Capture the following on every run.
| Evidence field | Purpose |
|---|---|
| TEAE count reconciliation | Confirms TRTEMFL="Y" rows match the TEAE table denominator |
Imputation register (ASTDTF) |
Lists every AE whose start date was imputed, for medical review |
| Pre-treatment exclusion log | Shows events with ASTDT < TRTSDT correctly excluded from TEAE counts |
| Merge-key integrity check | Proves every ADAE row carried its own subject’s TRTSDT from ADSL |
| First-occurrence audit | Verifies AOCCFL/AOCCPFL flag exactly one row per subject/term |
Run the reconciliation as a test, because a TEAE that flips to pre-treatment on a date-parsing edge case is precisely the defect that a manual review misses.
# GxP test artifact: TEAE integrity is asserted, not inspected; the report is
# archived as OQ evidence proving no on-treatment event was wrongly excluded.
def test_adae_integrity(adae):
emergent = adae[adae["TRTEMFL"].eq("Y")]
# No emergent AE may predate its own treatment start — the core TEAE invariant.
bad = emergent[emergent["ASTDT"] < pd.to_datetime(emergent["TRTSDT"])]
assert bad.empty, "TEAE flagged before treatment start"
assert (adae.groupby("USUBJID")["AOCCFL"].apply(
lambda s: (s == "Y").sum()) <= 1).all(), "AOCCFL not unique per subject"
Edge Cases and Vendor-Specific Gotchas
Partial AE start dates flipping TEAE. A 2026-03 start compared against a 2026-03-15 TRTSDT is ambiguous: impute to the first of the month and the event reads as pre-treatment; impute to the last and it reads emergent. Choose the protocol-defined convention, set ASTDTF, and surface every imputed TEAE for medical review rather than letting the parser decide silently.
Pre-treatment adverse events. Events that start before first dose are real and stay in ADAE — they are simply not treatment-emergent. Never filter them out of the dataset; set TRTEMFL blank and let the TEAE table subset on the flag. Deleting them corrupts the all-events listing and breaks traceability back to AE.
Carrying the wrong TRTSDT. Merging AE to ADSL on any key other than USUBJID, or forgetting to carry TRTSDT/TRTEDT at all, attaches the wrong window and mislabels emergence wholesale. Merge on USUBJID with validate="many_to_one", and assert every ADAE row has a non-null TRTSDT before deriving TRTEMFL.
Frequently Asked Questions
What makes an adverse event treatment-emergent?
An adverse event is treatment-emergent when its start date falls on or after the first dose of study treatment, and — where the protocol defines a post-treatment window — no later than that window past the last dose. Events starting before first dose are pre-treatment. The derivation encodes this as TRTEMFL, comparing the imputed AE start date against TRTSDT and TRTEDT carried from ADSL.
Should pre-treatment AEs be removed from ADAE?
No. Pre-treatment adverse events stay in ADAE with TRTEMFL blank; only the TEAE tables subset on the flag. Keeping them preserves the complete all-events listing and the traceability back to the SDTM AE domain. Deleting them would understate the event record and break the lineage a reviewer expects.
How do I handle a partial AE start date for TEAE determination?
Impute the missing day or month with the protocol-defined convention, set an imputation flag such as ASTDTF, and surface every imputed treatment-emergent event for medical review. Because the imputation direction can flip an event between pre-treatment and emergent, it must be documented and visible, never left to whatever a date parser happens to do.
Why merge ADAE to ADSL on USUBJID specifically?
USUBJID is the unique subject identifier that ADSL is keyed on, so it is the only key that attaches each adverse event to its own subject’s treatment dates and population flags. Merging on any other field risks pairing an event with the wrong treatment window, which mislabels emergence. Validating the merge as many-to-one catches a duplicated ADSL subject at the point of use.
Related
- ADaM Dataset Derivation for Statistical Analysis — the parent guide covering ADSL, BDS, and OCCDS structures together.
- Deriving the ADSL Subject-Level Analysis Dataset — the subject spine ADAE merges to for treatment dates.
- Mapping Adverse Events to the SDTM AE Domain — the upstream tabulation that produces the AE input.
- Clinical Data Architecture & EDC Standards — the reference architecture this analysis layer sits within.