Building an SDTM DM Domain with dplyr
Mapping raw demographics to the SDTM DM structure.
Learning objectives
- 1Map raw demographics to SDTM DM variables using controlled terminology.
- 2Apply required identifiers (STUDYID, USUBJID, DOMAIN) correctly.
- 3Derive RFSTDTC, RFENDTC and age in the standard SDTM form.
- 4Order variables and attach labels exactly to the SDTM IG specification.
Key SAS® → R comparison
| Task | SAS® | R |
|---|---|---|
| Build domain | DATA step + keep/retain order | dplyr::transmute() in target order |
| USUBJID construction | CATX(studyid, site, subj) | str_c() or paste with sep |
| Date variables | ISO or numeric dates + formats | character ISO 8601 strings (no formats) |
| Controlled terms | PROC FORMAT + put() | case_when() or metadata join |
Code examples
data dm (keep=studyid domain usubjid siteid ...);
set raw_dm;
studyid = 'ABC';
domain = 'DM';
usubjid = catx('-', studyid, siteid, subjid);
...
run;dm <- raw_dm |>
transmute(
STUDYID = "ABC",
DOMAIN = "DM",
USUBJID = str_c("ABC", siteid, subjid, sep = "-"),
SITEID = siteid,
SUBJID = subjid
)Note: transmute() is the R equivalent of a keep list in the DATA step — it guarantees the exact variable set and order you need for the domain.
rfstdtc = put(min(exstdt), yymmdd10.);
age = floor((rfstdt - birthdt)/365.25);dm <- dm |>
derive_vars_dt(
new_vars_prefix = "RF",
dtc = RFSTDTC # from first exposure or enrollment
) |>
mutate(
AGE = as.integer(floor(as.numeric(difftime(RFSTDTC, BRTHDTC, units="days")) / 365.25)),
AGEU = "YEARS"
)Note: admiral has dedicated functions for the common RFSTDTC/RFENDTC derivations used across almost every SDTM domain.
sex = put(sex, $sex.);
label age = 'Age';
label ageu = 'Age Units';dm <- dm |>
mutate(
SEX = case_when(SEX == "M" ~ "M", SEX == "F" ~ "F", TRUE ~ "U")
) |>
xportr_label(spec) # or manual labelled::var_label()Note: xportr (or the labelled package) makes attaching the exact SDTM labels and formats mechanical and auditable.
Explanation
The Demographics (DM) domain is usually the first SDTM domain a programmer builds because almost every other domain references its identifiers and timing variables. The work is mostly deterministic mapping plus careful handling of dates and controlled terminology.
In R the combination of `transmute()` for structure + admiral helpers for dates + xportr for metadata produces very readable, self-documenting code. Every derivation is explicit, which makes double programming and regulatory review easier than deeply nested SAS macros.
The most common pitfalls are incorrect USUBJID construction, wrong variable order (which breaks define.xml), and non-ISO date formats. The R approach forces you to be explicit about each of these.