These are the everyday programming loops a clinical programmer runs — an ADaM analysis dataset, an SDTM domain, a codelist decode, the temporal shapes — each rendered live below from the bundled demo data, so what you see is exactly what the code produces. Every recipe ends on a conformed result; the closing line shows how to ship it to any deliverable, just by changing the extension.
An ADaM build: ADSL
A real derivation program accumulates working columns the spec never declares — here an age-group cut. Those are “extras”; extra = "drop" trims them to exactly the spec’s columns, recorded by the extra_variable finding so the drop is never silent. apply_spec() then coerces, orders, sorts, and stamps:
adsl_raw <- cdisc_adsl
adsl_raw$AGEGR1_TMP <- cut(
adsl_raw$AGE,
breaks = c(-Inf, 64, 74, Inf),
labels = c("<65", "65-74", ">=75")
)
adsl_raw$AGEGR1 <- as.character(adsl_raw$AGEGR1_TMP)
adsl <- apply_spec(adsl_raw, adam_spec, "ADSL", extra = "drop")
Warning: 1 conformance error for "ADSL".
ℹ Run `conformance(x)` on the returned frame to see every finding.
<artoo_columns> ADSL -- 43 variables, 60 obs
# Variable Type Len Format Label Key
1 STUDYID Char 12 Study Identifier 1
2 USUBJID Char 11 Unique Subject Identifier 2
3 SUBJID Char 4 Subject Identifier for the Study
4 SITEID Char 3 Study Site Identifier
5 SITEGR1 Char 3 Pooled Site Group 1
6 ARM Char 20 Description of Planned Arm
7 TRT01P Char 20 Planned Treatment for Period 01
8 TRT01PN Num Planned Treatment for Period 01 (N)
9 TRT01A Char 20 Actual Treatment for Period 01
10 TRT01AN Num Actual Treatment for Period 01 (N)
11 TRTSDT Num DATE9. Date of First Exposure to Treatment
12 TRTEDT Num DATE9. Date of Last Exposure to Treatment
13 AVGDD Num 5.1 Avg Daily Dose (as planned)
14 CUMDOSE Num 8.1 Cumulative Dose (as planned)
15 AGE Num Age
16 AGEGR1 Char 5 Pooled Age Group 1
17 AGEGR1N Num Pooled Age Group 1 (N)
18 AGEU Char 5 Age Units
19 RACE Char 32 Race
20 RACEN Num Race (N)
21 SEX Char 1 Sex
22 ETHNIC Char 22 Ethnicity
23 SAFFL Char 1 Safety Population Flag
24 ITTFL Char 1 Intent-To-Treat Population Flag
25 EFFFL Char 1 Efficacy Population Flag
26 COMP8FL Char 1 Completers of Week 8 Population Flag
27 COMP16FL Char 1 Completers of Week 16 Population Flag
28 COMP24FL Char 1 Completers of Week 24 Population Flag
29 DISCONFL Char 1 Subject Discontinued Study Flag
30 DSRAEFL Char 1 Subject Discontinued due to AE Flag
31 DTHFL Char 1 Subject Death Flag
32 BMIBL Num 5.1 Baseline BMI (kg/m^2)
33 BMIBLGR1 Char 6 Pooled Baseline BMI Group 1
34 HEIGHTBL Num 6.1 Baseline Height (cm)
35 WEIGHTBL Num 6.1 Baseline Weight (kg)
36 EDUCLVL Num Years of Education
37 DURDIS Num 6.1 Duration of Disease (Months)
38 DURDSGR1 Char 4 Pooled Disease Duration Group 1
39 VISIT1DT Num DATE9. Date of Visit 1
40 RFSTDTC Char 10 Subject Reference Start Date/Time
41 RFENDTC Char 10 Subject Reference End Date/Time
42 VISNUMEN Num End of Trt Visit (Vis 12 or Early Term.)
43 RFENDT Num Date of Discontinuation/Completion
write_xpt(adsl, "adsl.xpt") # ship: or .json / .parquet / .rds
An SDTM build: DM
The pipeline is identical — only the spec and the data change. Assemble the domain (with a QC temporary), conform, then read the result back as a one-line inventory with members():
dm_raw <- cdisc_dm
dm_raw$AGE_CHECK <- dm_raw$AGE >= 18
dm <- apply_spec(dm_raw, sdtm_spec, "DM", extra = "drop", conformance = "off")
json <- tempfile(fileext = ".json")
write_json(dm, json)
members(json)
<artoo_members> 1 dataset
file member label records variables format
file1eda5c7e1111.json DM Demographics 60 15 json
write_xpt(dm, "dm.xpt") # ship: or .json / .parquet / .rds
Codelists: decode in either direction
decode_column() reads the codelist a variable is bound to in the spec and maps in either direction — here, deriving the numeric RACEN code from the RACE decode (direction = "to_code"):
adsl <- apply_spec(cdisc_adsl, adam_spec, "ADSL", conformance = "off")
coded <- decode_column(
adsl, adam_spec, "ADSL",
from = "RACE", to = "RACEN", direction = "to_code"
)
unique(coded[, c("RACE", "RACEN")])
RACE RACEN
1 WHITE 1
20 BLACK OR AFRICAN AMERICAN 2
24 AMERICAN INDIAN OR ALASKA NATIVE 6
A value outside the codelist’s terms aborts (artoo_error_codelist) unless you pass no_match = "keep" / "na".
Dates, times, and --DTC
Clinical dates travel in two shapes, and artoo keeps the distinction explicit through dataType, targetDataType, and displayFormat. Scope note: this is about carriage — partial-date imputation is an analysis decision your SAP owns; artoo never imputes.
An SDTM --DTC variable typed date is ISO 8601 text by the CDISC storage rule, so it stays character through the pipeline, partial values included ("2024-03" is a legal ISO date, and padding it would be imputation by stealth):
dm <- apply_spec(cdisc_dm, sdtm_spec, "DM", conformance = "off")
class(dm$RFSTDTC)
A variable typed date with no targetDataType realizes to an R Date in memory:
adsl <- apply_spec(cdisc_adsl, adam_spec, "ADSL", conformance = "off")
class(adsl$DISONSDT)
The ADaM numeric-date convention is the other shape: a variable carried as an integer SAS-epoch day count, with a displayFormat (date9.) telling SAS how to render it. TRTSDT is one, and the format rides in the metadata:
$dataType
[1] "integer"
$displayFormat
[1] "date9."
SAS TIME values import as hms (seconds since midnight), so values past 24 hours, negative values, and fractional seconds all survive:
Either date shape round-trips byte-faithfully — the SAS-epoch (1960-01-01) versus R-epoch conversion happens inside the codecs, never in your code: