
SDTM AE Domain in R with sdtm.oak: Map Adverse Events to CDISC SDTM
Map a raw adverse-event EDC extract into a CDISC-conformant SDTM AE domain in R with sdtm.oak — the Events observation class, controlled-terminology qualifiers, ISO 8601 dates, and the sequence variable
A complete, runnable tutorial for the SDTM AE (adverse events) domain in R. Learn what the CDISC SDTM Events observation class is (one record per event, keyed by the topic variable AETERM), how it differs from the Findings class, then map a raw adverse-event extract into a conformant AE domain with sdtm.oak: generate_oak_id_vars() for record lineage, assign_no_ct() for the verbatim term and the MedDRA-coded AEDECOD/AEBODSYS, assign_ct() for the controlled-terminology qualifiers (AESEV/AESER/AEREL/AEOUT), assign_datetime() for ISO 8601 AESTDTC/AEENDTC, and derive_seq() for AESEQ — every line runnable, feeding the downstream ADAE dataset.
- AE is an Events domain, not a Findings one. The Findings class (vital signs, labs) records measurements keyed by a test code; the Events class records things that happened — one row per adverse event, keyed by the reported term
AETERM. - The topic variable is
AETERM(free text), and it carries no controlled terminology.assign_no_ct()copies the verbatim reported term as collected; the codedAEDECODandAEBODSYScome from MedDRA coding done upstream, so they too are copied, not recoded. - The qualifiers are controlled.
assign_ct()recodes the collected severity, seriousness, relationship, and outcome to their CDISC standard terms (AESEV,AESER,AEREL,AEOUT). - Timing is two ISO 8601 dates.
assign_datetime()builds the start (AESTDTC) and end (AEENDTC) from the raw date/time fields — and safely returns a partial date when the raw day is unknown, rather than inventing one. derive_seq()numbers the events.AESEQis unique per subject, and the whole domain becomes the standard input the downstream ADAE analysis dataset reads from.
Introduction
Adverse events are the safety backbone of every clinical trial. Every study collects them, every regulator scrutinizes them, and the reviewer who decides whether a drug is safe reads them through one standardized dataset: the SDTM (Study Data Tabulation Model) AE (adverse events) domain. What the site actually records is messier — a CRF (case report form) page with a free-text reported term, a severity ticked as “Mild”, a start date typed however the coordinator typed it. The job here is to turn that raw extract into a conformant AE domain a regulator can open and read the same way for any study.
This lesson does that mapping end to end in R with sdtm.oak, the pharmaverse package for raw-to-SDTM transformation. It is the Events counterpart to mapping a Findings domain (vital signs) with sdtm.oak: the same oak algorithms, a different observation class. SDTM is the upstream of the clinical data flow — raw → SDTM → ADaM → tables, listings, and figures — so a clean AE domain here is what makes the next step, building the ADAE adverse-events analysis dataset with admiral, straightforward.
Here is where we are headed — the conformant AE domain we will build, summarized by body system and severity straight off the mapped result:
Every segment there is one SDTM record — a reported event, coded to a body system, graded for severity. By the end of this lesson you will have produced all of them from a raw extract. New to the wider pipeline? Start with the clinical-trial data flow, which maps where SDTM sits, and the CDISC standards, which places SDTM next to CDASH and ADaM.
What the SDTM Events class is
SDTM — the Study Data Tabulation Model — is the CDISC (Clinical Data Interchange Standards Consortium) standard for organizing collected trial data into fixed domains. It is published as the SDTM foundational standard and the SDTM Implementation Guide (SDTMIG). SDTM sorts every observation into one of three general observation classes, and the class decides the shape of a record:
| Observation class | What it records | Example domains | Topic variable |
|---|---|---|---|
| Interventions | Things given to or done to the subject | CM (con meds), EX (exposure) | --TRT |
| Events | Things that happened to the subject | AE (adverse events), MH (medical history), DS (disposition) | --TERM |
| Findings | Measurements and assessments | VS (vital signs), LB (labs), EG (ECG) | --TESTCD |
The companion vital-signs lesson mapped a Findings domain, where the topic variable is a test code (VSTESTCD = “what was measured”) and each record answers “what was measured and what was the result.” Events are different. An adverse event is not a measurement — it is an occurrence, so the topic variable is the reported term AETERM (“what happened”), and there is one record per event, not per assessment. That single difference — term instead of test code, event instead of measurement — is the whole distinction between the two classes.
An AE record is that topic term plus its qualifiers, identifiers, and timing:
| Variable role | AE example | What it carries |
|---|---|---|
| Identifiers | STUDYID, USUBJID, DOMAIN, AESEQ |
Study, subject, domain code (AE), and a unique sequence number per record. |
| Topic | AETERM |
The verbatim reported term exactly as collected on the CRF. |
| Synonym / dictionary | AEDECOD, AEBODSYS |
The MedDRA preferred term (PT) and system organ class (SOC) — the coded event. |
| Result qualifiers | AESEV, AESER, AEREL, AEOUT |
Severity, seriousness, relationship to study drug, and outcome. |
| Timing | AESTDTC, AEENDTC |
Start and end date/time in ISO 8601 (--DTC variables). |
Two conventions carry the whole mapping. First, the topic variable (AETERM) names the observation and everything else qualifies it. Second, most qualifier columns are restricted to controlled terminology (CT) — CDISC-published codelists that fix the allowed values (AESEV must be MILD, not “mild” or “1”). CDISC maintains them as SDTM Controlled Terminology. Mapping raw to SDTM is, in essence, filling the right qualifier columns with the right CT-valid values — the same job as for Findings, done for a different record shape.
The raw adverse-event extract and its controlled terminology
Two inputs drive a mapping: the raw extract and the study controlled terminology. sdtm.oak ships raw examples for concomitant medications and vital signs but not adverse events, so we build a small raw AE extract in the shape an EDC (electronic data capture) system exports one — one wide row per event, sponsor-chosen column names, dates in the format the form used.
library(sdtm.oak)
library(dplyr, warn.conflicts = FALSE)
# A raw adverse-event EDC extract: one row per event, as collected
ae_raw <- data.frame(
PATNUM = c("375","375","375","376","376","377","377","377","378","378","379","379"),
AESPID = c("AE01","AE02","AE03","AE01","AE02","AE01","AE02","AE03","AE01","AE02","AE01","AE02"),
AETERM_R = c("HEADACHE","Nausea","Insomnia","Fatigue","MILD RASH","Dizziness",
"Headache","Vomiting","Diarrhoea","Headache","Nausea","Pyrexia"),
AEPT_R = c("Headache","Nausea","Insomnia","Fatigue","Rash","Dizziness",
"Headache","Vomiting","Diarrhoea","Headache","Nausea","Pyrexia"),
AESOC_R = c("Nervous system disorders","Gastrointestinal disorders","Psychiatric disorders",
"General disorders","Skin and subcutaneous tissue disorders","Nervous system disorders",
"Nervous system disorders","Gastrointestinal disorders","Gastrointestinal disorders",
"Nervous system disorders","Gastrointestinal disorders","General disorders"),
AESTDAT = c("05 Jan 2023","07 Jan 2023","10 Jan 2023","12 Feb 2023","13 Feb 2023","02 Mar 2023",
"04 Mar 2023","06 Mar 2023","09 Jan 2023","15 Jan 2023","21 Feb 2023","25 Feb 2023"),
AESTTIM = c("08:30","14:10","22:00","09:00","20:45","07:15","11:00","16:20","13:30","08:00","19:45","06:30"),
AEENDAT = c("06 Jan 2023","07 Jan 2023","12 Jan 2023","UN UNK 2023","20 Feb 2023","02 Mar 2023",
"05 Mar 2023","06 Mar 2023","11 Jan 2023","16 Jan 2023","22 Feb 2023","27 Feb 2023"),
AEENTIM = c("10:00","18:30","07:00","","12:00","23:00","09:00","20:00","10:00","12:00","08:00","14:00"),
SEV = c("Mild","Moderate","Mild","Moderate","Mild","Severe","Moderate","Severe","Moderate","Mild","Moderate","Mild"),
SER = c("No","No","No","No","No","Yes","No","Yes","No","No","No","No"),
REL = c("Not related","Related","Not related","Not related","Related","Related",
"Not related","Related","Related","Not related","Related","Not related"),
OUT = c("Recovered","Recovered","Recovered","Not recovered","Recovered","Recovering",
"Recovered","Recovered","Recovered","Recovered","Recovered","Recovered"),
stringsAsFactors = FALSE
)
ae_raw |>
select(PATNUM, AETERM_R, AESOC_R, AESTDAT, SEV, SER, OUT) |>
head() PATNUM AETERM_R AESOC_R AESTDAT SEV
1 375 HEADACHE Nervous system disorders 05 Jan 2023 Mild
2 375 Nausea Gastrointestinal disorders 07 Jan 2023 Moderate
3 375 Insomnia Psychiatric disorders 10 Jan 2023 Mild
4 376 Fatigue General disorders 12 Feb 2023 Moderate
5 376 MILD RASH Skin and subcutaneous tissue disorders 13 Feb 2023 Mild
6 377 Dizziness Nervous system disorders 02 Mar 2023 Severe
SER OUT
1 No Recovered
2 No Recovered
3 No Recovered
4 No Not recovered
5 No Recovered
6 Yes Recovering
Each raw row is one reported event: a verbatim term (AETERM_R), a pre-coded preferred term and body system (AEPT_R, AESOC_R), a start and end date/time, and the collected severity, seriousness, and outcome. The controlled terminology maps each collected value to its standard CDISC term within a codelist. A real study CT ships as part of the SDTM specification; here we define the small set of AE codelists we need:
# Study controlled terminology: collected value -> CDISC standard term, by codelist
ae_ct <- data.frame(
codelist_code = c("C66769","C66769","C66769", "C66742","C66742",
"C66768","C66768","C66768", "STUDYREL","STUDYREL"),
term_code = c("C41338","C41339","C41340", "C49488","C49487",
"C49498","C49496","C49494", "R01","R02"),
term_value = c("MILD","MODERATE","SEVERE", # C66769 AESEV severity
"Y","N", # C66742 no/yes
"RECOVERED/RESOLVED","RECOVERING/RESOLVING","NOT RECOVERED/NOT RESOLVED", # C66768 AEOUT
"NOT RELATED","RELATED"), # STUDYREL study relationship
collected_value = c("Mild","Moderate","Severe", "Yes","No",
"Recovered","Recovering","Not recovered", "Not related","Related"),
term_preferred_term = NA_character_, term_synonyms = NA_character_,
stringsAsFactors = FALSE
)
ae_ct |> select(codelist_code, term_value, collected_value) codelist_code term_value collected_value
1 C66769 MILD Mild
2 C66769 MODERATE Moderate
3 C66769 SEVERE Severe
4 C66742 Y Yes
5 C66742 N No
6 C66768 RECOVERED/RESOLVED Recovered
7 C66768 RECOVERING/RESOLVING Recovering
8 C66768 NOT RECOVERED/NOT RESOLVED Not recovered
9 STUDYREL NOT RELATED Not related
10 STUDYREL RELATED Related
collected_value is what the CRF recorded; term_value is the CDISC-standard term sdtm.oak will substitute. The severity, seriousness, and outcome codelists (C66769, C66742, C66768) are CDISC standard; relationship to study drug has no single CDISC codelist, so studies define their own — here STUDYREL. The ct_clst argument of every CT-aware function points at one of these codelist_code values.
Stamp record lineage with oak_id_vars
Because we map one raw field at a time and stitch the pieces together, sdtm.oak needs a stable key on every raw row. generate_oak_id_vars() adds three: oak_id (the row number), raw_source (which extract it came from), and patient_number.
ae_raw <- ae_raw |>
generate_oak_id_vars(pat_var = "PATNUM", raw_src = "ae_raw")
ae_raw |>
select(oak_id, raw_source, patient_number, AETERM_R) |>
head() oak_id raw_source patient_number AETERM_R
1 1 ae_raw 375 HEADACHE
2 2 ae_raw 375 Nausea
3 3 ae_raw 375 Insomnia
4 4 ae_raw 376 Fatigue
5 5 ae_raw 376 MILD RASH
6 6 ae_raw 377 Dizziness
These three columns are the oak_id_vars. Every qualifier map below passes id_vars = oak_id_vars(), which tells sdtm.oak to join the new column back onto the right record by this key — the mechanism that lets partial maps combine without duplicating rows.
Map the topic variable
An Events record begins with its topic — the reported term. Unlike a Findings test code, AETERM is free text under no controlled terminology, so we copy the collected value verbatim with assign_no_ct() (“assign”, because it copies a collected value; “no_ct”, because there is no codelist to recode through).
ae <- assign_no_ct(
raw_dat = ae_raw,
raw_var = "AETERM_R",
tgt_var = "AETERM"
)
ae |>
select(oak_id, patient_number, AETERM) |>
head()# A tibble: 6 × 3
oak_id patient_number AETERM
<int> <chr> <chr>
1 1 375 HEADACHE
2 2 375 Nausea
3 3 375 Insomnia
4 4 376 Fatigue
5 5 376 MILD RASH
6 6 377 Dizziness
One SDTM column exists so far, one record per raw event. Notice AETERM keeps the verbatim casing (HEADACHE, MILD RASH) exactly as the site typed it — SDTM preserves the reported term unchanged; standardization happens in the coded columns, next.
Map the MedDRA-coded terms
AEDECOD (the preferred term) and AEBODSYS (the system organ class) are the coded view of the event. They come from MedDRA (the Medical Dictionary for Regulatory Activities, the global standard for coding adverse events — meddra.org): a medical coder maps each verbatim AETERM to a MedDRA PT (preferred term) and its SOC (system organ class). That coding happens upstream of this mapping, so in the raw extract the coded values already sit in AEPT_R and AESOC_R — we copy them as-is with assign_no_ct(); sdtm.oak maps the structure, it does not do the dictionary coding.
ae <- ae |>
assign_no_ct(raw_dat = ae_raw, raw_var = "AEPT_R",
tgt_var = "AEDECOD", id_vars = oak_id_vars()) |>
assign_no_ct(raw_dat = ae_raw, raw_var = "AESOC_R",
tgt_var = "AEBODSYS", id_vars = oak_id_vars())
ae |>
select(patient_number, AETERM, AEDECOD, AEBODSYS) |>
head()# A tibble: 6 × 4
patient_number AETERM AEDECOD AEBODSYS
<chr> <chr> <chr> <chr>
1 375 HEADACHE Headache Nervous system disorders
2 375 Nausea Nausea Gastrointestinal disorders
3 375 Insomnia Insomnia Psychiatric disorders
4 376 Fatigue Fatigue General disorders
5 376 MILD RASH Rash Skin and subcutaneous tissue disorders
6 377 Dizziness Dizziness Nervous system disorders
MedDRA coding is a separate step, not part of the mapping. Deriving AEDECOD/AEBODSYS requires a licensed MedDRA dictionary and medical judgment, and is done in dedicated coding software before SDTM programming. This lesson assumes the coding is complete (the raw extract carries the coded columns) and maps them into the SDTM structure — which is exactly how a real AE domain is built.
Map the controlled-terminology qualifiers
Now the columns that qualify each event: how bad it was, whether it was serious, whether it was related to the study drug, and how it resolved. Each is a collected value recoded through controlled terminology, so each uses assign_ct() with the codelist that governs it:
AESEV(severity) — codelistC66769:Mild/Moderate/Severe→MILD/MODERATE/SEVERE.AESER(serious) — the no/yes codelistC66742:No/Yes→N/Y.AEREL(relationship to study drug) — the study codelistSTUDYREL.AEOUT(outcome) — codelistC66768:Recovered→RECOVERED/RESOLVED, and so on.
ae <- ae |>
assign_ct(raw_dat = ae_raw, raw_var = "SEV", tgt_var = "AESEV",
ct_spec = ae_ct, ct_clst = "C66769", id_vars = oak_id_vars()) |>
assign_ct(raw_dat = ae_raw, raw_var = "SER", tgt_var = "AESER",
ct_spec = ae_ct, ct_clst = "C66742", id_vars = oak_id_vars()) |>
assign_ct(raw_dat = ae_raw, raw_var = "REL", tgt_var = "AEREL",
ct_spec = ae_ct, ct_clst = "STUDYREL", id_vars = oak_id_vars()) |>
assign_ct(raw_dat = ae_raw, raw_var = "OUT", tgt_var = "AEOUT",
ct_spec = ae_ct, ct_clst = "C66768", id_vars = oak_id_vars())
ae |>
select(patient_number, AETERM, AESEV, AESER, AEREL, AEOUT) |>
head()# A tibble: 6 × 6
patient_number AETERM AESEV AESER AEREL AEOUT
<chr> <chr> <chr> <chr> <chr> <chr>
1 375 HEADACHE MILD N NOT RELATED RECOVERED/RESOLVED
2 375 Nausea MODERATE N RELATED RECOVERED/RESOLVED
3 375 Insomnia MILD N NOT RELATED RECOVERED/RESOLVED
4 376 Fatigue MODERATE N NOT RELATED NOT RECOVERED/NOT RESOLVED
5 376 MILD RASH MILD N RELATED RECOVERED/RESOLVED
6 377 Dizziness SEVERE Y RELATED RECOVERING/RESOLVING
Every collected value is now a standard CDISC term: AESEV is MILD/MODERATE/SEVERE, AESER is N/Y. That is the point of controlled terminology — a reviewer, or an FDA validation tool, reads the same coded values across every study, regardless of how each site phrased them on the CRF.
Map the timing variables
An event has a start and an end, each an ISO 8601 date/time (--DTC variables). ISO 8601 is the international date standard (2023-01-05T08:30) that makes dates sortable and comparable across studies. The raw date and time sit in two fields, in a non-standard format; assign_datetime() parses them and writes the compliant column.
ae <- ae |>
assign_datetime(
raw_dat = ae_raw,
raw_var = c("AESTDAT", "AESTTIM"),
tgt_var = "AESTDTC",
raw_fmt = c(list(c("d-m-y", "dd mmm yyyy")), "H:M")
) |>
assign_datetime(
raw_dat = ae_raw,
raw_var = c("AEENDAT", "AEENTIM"),
tgt_var = "AEENDTC",
raw_fmt = c(list(c("d-m-y", "dd mmm yyyy")), "H:M")
)
ae |>
select(patient_number, AETERM, AESTDTC, AEENDTC) |>
head()# A tibble: 6 × 4
patient_number AETERM AESTDTC AEENDTC
<chr> <chr> <iso8601> <iso8601>
1 375 HEADACHE 2023-01-05T08:30 2023-01-06T10:00
2 375 Nausea 2023-01-07T14:10 2023-01-07T18:30
3 375 Insomnia 2023-01-10T22:00 2023-01-12T07:00
4 376 Fatigue 2023-02-12T09:00 2023
5 376 MILD RASH 2023-02-13T20:45 2023-02-20T12:00
6 377 Dizziness 2023-03-02T07:15 2023-03-02T23:00
raw_fmt tells sdtm.oak how the raw date and time were written (05 Jan 2023, 08:30); it returns the standard 2023-01-05T08:30 form. Look at the fourth row: its raw end date was UN UNK 2023 (day and month unknown), and AEENDTC becomes just 2023 — a valid partial ISO 8601 date. This is the correct behaviour: SDTM records the precision that was actually collected and never invents a day. The date-handling vignette documents the full set of supported formats and partial-date rules.
Add the identifiers and the sequence number
A conformant record needs its identifiers and a sequence number. STUDYID, DOMAIN, and USUBJID are simple assignments; derive_seq() assigns AESEQ — a unique sequence number per record within a subject, an SDTM requirement.
ae <- ae |>
mutate(
STUDYID = "CDISC01",
DOMAIN = "AE",
USUBJID = paste0(STUDYID, "-", patient_number)
) |>
derive_seq(
tgt_var = "AESEQ",
rec_vars = c("USUBJID", "AETERM", "AESTDTC")
) |>
select(STUDYID, DOMAIN, USUBJID, AESEQ, AETERM, AEDECOD, AEBODSYS,
AESEV, AESER, AEREL, AEOUT, AESTDTC, AEENDTC)
head(ae)# A tibble: 6 × 13
STUDYID DOMAIN USUBJID AESEQ AETERM AEDECOD AEBODSYS AESEV AESER AEREL AEOUT
<chr> <chr> <chr> <int> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
1 CDISC01 AE CDISC01-… 1 HEADA… Headac… Nervous… MILD N NOT … RECO…
2 CDISC01 AE CDISC01-… 2 Insom… Insomn… Psychia… MILD N NOT … RECO…
3 CDISC01 AE CDISC01-… 3 Nausea Nausea Gastroi… MODE… N RELA… RECO…
4 CDISC01 AE CDISC01-… 1 Fatig… Fatigue General… MODE… N NOT … NOT …
5 CDISC01 AE CDISC01-… 2 MILD … Rash Skin an… MILD N RELA… RECO…
6 CDISC01 AE CDISC01-… 1 Dizzi… Dizzin… Nervous… SEVE… Y RELA… RECO…
# ℹ 2 more variables: AESTDTC <iso8601>, AEENDTC <iso8601>
That is a conformant AE domain: the SDTM identifier columns first, then the topic (AETERM), the coded terms, the qualifiers, and the timing. derive_seq() orders the records by rec_vars and numbers them 1, 2, 3… within each subject, so AESEQ is unique per USUBJID — the key every downstream join relies on. A quick structural check confirms the SDTM contract:
required <- c("STUDYID", "DOMAIN", "USUBJID", "AESEQ", "AETERM", "AEDECOD")
all(required %in% names(ae)) # required identifiers + topic present?[1] TRUE
length(unique(ae$DOMAIN)) == 1 # one domain code?[1] TRUE
!any(duplicated(ae[c("USUBJID", "AESEQ")])) # AESEQ unique within subject?[1] TRUE
Three TRUEs: the domain is structurally sound. In production the full check is a conformance tool (Pinnacle 21, or the pharmaverse sdtmchecks package, shown in the vital-signs lesson) that runs the same integrity rules a regulator would. With a conformant AE domain in hand, the next stage reads straight from it: building ADAE, the adverse-events analysis dataset, with admiral adds the treatment-emergent flag and occurrence flags on top of exactly these columns.
Ask Prova “how do I add the AEACN action-taken and AEACNOTH qualifiers to this SDTM AE domain, and which controlled-terminology codelist governs AEACN?” — it answers grounded in this pillar’s lessons, with runnable sdtm.oak code you can try on the example data. The runtime is the judge. Ask Prova →
Common issues
You reach for a test code and there is none — the AE has no --TESTCD. That is the Events-class trap: you are thinking in Findings terms. An adverse event is an occurrence, not a measurement, so its topic variable is the reported term AETERM, mapped with assign_no_ct() (free text, no CT) — there is no AETESTCD. If you find yourself looking for a test-code codelist for AE, you have the wrong observation class in mind; map the term, not a test.
A partial or missing date makes assign_datetime() return a short string like 2023 — and you assume it failed. It did not. When the raw day or month is unknown (UN UNK 2023), the correct ISO 8601 result is the truncated 2023 (or 2023-01) — SDTM records only the precision collected and never fabricates a day. Do not “fix” it by padding with 01; a partial --DTC is conformant, and the downstream analysis dataset imputes analysis dates from it deliberately.
assign_ct() warns that a value “could not be mapped,” and the SDTM column keeps the raw text. The collected value is not in the codelist you pointed at — a real finding, not a bug. sdtm.oak passes the raw value through unchanged and tells you, so you fix the terminology, not the data. Check the codelist with subset(ae_ct, codelist_code == "<code>") and confirm its collected_value column actually contains the raw value you are mapping (a common cause is a case or spelling mismatch, e.g. "YES" vs "Yes").
Frequently asked questions
The SDTM AE domain is the CDISC SDTM dataset that holds a study’s adverse events — one standardized record per event, in the Events observation class. Each record carries the reported term AETERM, the MedDRA-coded AEDECOD and AEBODSYS, controlled-terminology qualifiers (AESEV, AESER, AEREL, AEOUT), ISO 8601 start/end dates (AESTDTC, AEENDTC), and identifiers (USUBJID, AESEQ). It is a required domain in a regulatory submission and the source for the ADAE analysis dataset.
Findings records a measurement (a lab value, a blood pressure) — its topic variable is a test code (--TESTCD), and each record answers “what was measured and what was the result.” Events records something that happened (an adverse event, a medical-history condition) — its topic variable is a term (--TERM, e.g. AETERM), with one record per event. Same sdtm.oak algorithms, different record shape: for AE you map a reported term, not a test-code-plus-result.
AETERM is the verbatim reported term — the adverse event exactly as the investigator wrote it on the CRF (“MILD RASH”). AEDECOD is the MedDRA preferred term (PT) it was coded to (“Rash”) — the standardized dictionary term that lets you group and count events consistently across sites and studies. AETERM is copied as-is with assign_no_ct(); AEDECOD (and the system organ class AEBODSYS) come from MedDRA coding done before SDTM programming.
No. sdtm.oak maps the structure — it moves the already-coded AEDECOD/AEBODSYS from the raw extract into the SDTM columns. MedDRA coding itself (assigning a preferred term and system organ class to each verbatim term) needs a licensed MedDRA dictionary and medical judgment, and is done in dedicated coding software upstream. By the time you map to SDTM, the coded columns already exist in the raw data.
Use assign_no_ct() for values copied as-is (the free-text AETERM, and the pre-coded AEDECOD/ AEBODSYS); assign_ct() for collected values recoded through a codelist (AESEV, AESER, AEREL, AEOUT); assign_datetime() for the ISO 8601 dates (AESTDTC, AEENDTC); and derive_seq() for AESEQ. generate_oak_id_vars() first stamps the lineage key every map joins back on. The algorithms vignette lists the full set.
Test your understanding
The raw extract does not yet carry action taken with study drug, but suppose it did, in a field ACN collected as Dose Not Changed / Drug Withdrawn / Dose Reduced. Sketch the mapping for the SDTM AEACN qualifier: which sdtm.oak function would you use, and what would the controlled-terminology step look like?
AEACN is a collected value that must be recoded to CDISC standard terms — so it is the same pattern as AESEV/AEOUT. You need the function that recodes through a codelist, plus a codelist entry mapping each collected value to its term_value.
Use assign_ct(), with an entry in the study CT (CDISC codelist C66767 governs AEACN) that maps each collected value to its standard term:
# add C66767 rows to ae_ct: collected -> standard
# "Dose Not Changed" -> "DOSE NOT CHANGED"
# "Drug Withdrawn" -> "DRUG WITHDRAWN"
# "Dose Reduced" -> "DOSE REDUCED"
ae <- ae |>
assign_ct(raw_dat = ae_raw, raw_var = "ACN", tgt_var = "AEACN",
ct_spec = ae_ct, ct_clst = "C66767", id_vars = oak_id_vars())It is the identical algorithm chain as the other qualifiers, with the raw field, target variable, and codelist swapped — which is exactly why sdtm.oak’s algorithms are reusable: every CT qualifier maps the same way.
A. Because AE is a Findings domain and AETERM is its test code B. Because AE is an Events domain — it records occurrences, keyed by the reported term C. Because sdtm.oak requires every domain to use a --TERM topic variable
B. AE is in the Events observation class, which records things that happened. Its topic variable is therefore the reported term AETERM (“what happened”), with one record per event — unlike the Findings class (vital signs, labs), whose topic is a test code --TESTCD (“what was measured”). A has the class wrong (AE is Events, not Findings), and C is false — the topic variable depends on the observation class, not a package rule.
Conclusion
Mapping the AE domain is the same one-field-at-a-time loop as any SDTM domain, applied to the Events class: fill the right SDTM column from the right raw field with the right algorithm. The topic variable AETERM names each event (copied verbatim with assign_no_ct()); the MedDRA-coded AEDECOD/ AEBODSYS are copied from upstream coding; assign_ct() recodes the severity, seriousness, relationship, and outcome qualifiers through controlled terminology; assign_datetime() builds the ISO 8601 start and end — honouring partial dates; and derive_seq() numbers the events. Master the Events loop here and every event-type domain — medical history, disposition — is the same loop with different fields. With a conformant SDTM AE in hand, the next step is the analysis layer: building the ADAE adverse-events dataset with admiral.
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
@online{2026,
author = {},
title = {SDTM {AE} {Domain} in {R} with Sdtm.oak: {Map} {Adverse}
{Events} to {CDISC} {SDTM}},
date = {2026-07-01},
url = {https://www.datanovia.com/learn/pharma-clinical/02-sdtm-programming/sdtm-ae-adverse-events-domain},
langid = {en}
}