Review ask

As a clinical trial statistician or programmer, challenge anything in this benchmark: describe a problem in Comments, or propose a fix with the Edit button on any source file.

  1. Task: Would a real study need this derivation, specified this way? Does it follow CDISC ADaM: variable names, controlled terms, record structure? Flag any rule that is ambiguous or that a sponsor would handle differently.
  2. Inputs: Do they look like data collected in a real trial, and do they include the situations you would expect to meet in one?
  3. Expected output: Would independent QC programming reproduce every value from these inputs and rules?

The other tabs beside Comments are background for assessment, not under review.

Summary

Lifecycle: reviewedEditUnder review
What this benchmark means
README.md

Goal: select each subject's actual treatment (TRT01A), the first and last exposure dates and times (TRTSDT, TRTSDTM, TRTEDT, TRTEDTM), the imputation flags (TRTSTMF, TRTETMF), the treatment duration in days (TRTDURD), and the safety population flag (SAFFL).

Input: one demographics (DM) record per subject carrying the actual arm (ACTARM), plus exposure (EX) records carrying the treatment name (EXTRT), sequence number (EXSEQ), start date (EXSTDTC), and end date (EXENDTC). Records are built from the demographics records, so an exposure record with no matching demographics record adds no record.

Variables:

  • TRT01A: treatment actually received, uppercased: the treatment of the earliest qualifying exposure (a qualifying record names VITAMIN D3 or PLACEBO and carries a start), with ties on the start broken by the lower EXSEQ; then the actual arm (ACTARM) when no exposure qualifies; then the text NOT TREATED when neither source names a treatment.
  • TRTSDT and TRTSDTM: date and date/time of the earliest qualifying exposure start; a start collected without a time uses 00:00:00. Both are blank when the subject has no qualifying exposure record.
  • TRTSTMF: H when the first exposure start was collected without a time, so its time was imputed; otherwise blank.
  • TRTEDT and TRTEDTM: date and date/time of the latest exposure end among VITAMIN D3 and PLACEBO records (a record need not have a start to name an end); an end collected without a time uses 23:59:59. Both are blank when no such record has an end.
  • TRTETMF: H when the last exposure end was collected without a time, so its time was imputed; otherwise blank.
  • TRTDURD: number of days from TRTSDT to TRTEDT, counting both the first and the last day (inclusive), so a single treatment day gives one; blank when either date is blank.
  • SAFFL: Y when the subject has a treatment start date, N otherwise (never blank).

Note: the exposure dates, date/times, and duration are all present or all blank together; the latest end can come from a record whose start was never collected.

Inputs

Under review
2 source files

input/dm.csv

Edit
8 rows
input/dm.csv
STUDYIDUSUBJIDACTARM
YAMAA-01Vitamin D3
YAMAA-01Placebo
YAMAA-01Vitamin D3
YAMAA-01Placebo
YAMAA-01Vitamin D3
YAMAA-01—
YAMAA-01Vitamin D3
YAMAA-01Placebo

input/ex.csv

Edit
13 rows
input/ex.csv
STUDYIDUSUBJIDEXSEQEXTRTEXDOSEEXSTDTCEXENDTC
YAMAA-011VITAMIN D340002025-01-082025-01-14T18:00:00
YAMAA-012VITAMIN D340002025-01-15T08:00:002025-01-28
YAMAA-011PLACEBO02025-02-08T09:00:002025-02-14T17:00:00
YAMAA-012PLACEBO02025-02-15T09:00:002025-02-28T17:00:00
YAMAA-011VITAMIN D340002025-03-052025-03-05
YAMAA-011VITAMIN D340002025-04-03T10:00:002025-04-10
YAMAA-012PLACEBO02025-04-03T10:00:002025-04-16T12:00:00
YAMAA-011RESCUE MED2002025-05-012025-05-07
YAMAA-011VITAMIN D340002025-06-08T09:00:002025-06-14T16:00:00
YAMAA-012VITAMIN D34000—2025-06-20
YAMAA-011PLACEBO02025-07-01T10:00:002025-07-07T10:00:00
YAMAA-012PLACEBO02025-07-01T10:00:002025-07-07T10:00:00
YAMAA-011VITAMIN D340002025-03-08T10:00:002025-03-21T16:00:00

Expected output

Under review
8 expected rows

expected/adsl.csv

Edit
8 rows
expected/adsl.csv
STUDYIDUSUBJIDTRT01ATRTSDTTRTSDTMTRTSTMFTRTEDTTRTEDTMTRTETMFTRTDURDSAFFL
YAMAA-01VITAMIN D32025-01-082025-01-08T00:00:00H2025-01-282025-01-28T23:59:59H21Y
YAMAA-01PLACEBO2025-02-082025-02-08T09:00:00—2025-02-282025-02-28T17:00:00—21Y
YAMAA-01VITAMIN D32025-03-052025-03-05T00:00:00H2025-03-052025-03-05T23:59:59H1Y
YAMAA-01VITAMIN D32025-04-032025-04-03T10:00:00—2025-04-162025-04-16T12:00:00—14Y
YAMAA-01VITAMIN D3———————N
YAMAA-01NOT TREATED———————N
YAMAA-01VITAMIN D32025-06-082025-06-08T09:00:00—2025-06-202025-06-20T23:59:59H13Y
YAMAA-01PLACEBO2025-07-012025-07-01T10:00:00—2025-07-072025-07-07T10:00:00—7Y
Shaded columns are derived— = missing value

Comments and assessment materials

Comments

Signed in with GitHub; saved in repository Discussions

Mapping spec

Generated from the benchmark YAML; do not edit by hand

Dataset NameVariable NameVariable LabelTypeCodelist/Controlled TermsCoreComputational MethodOrigin
ADSLSTUDYIDStudy IdentifiercharacterCopy value from DM.STUDYID.Predecessor
ADSLUSUBJIDUnique Subject IdentifiercharacterCopy value from DM.USUBJID.Predecessor
ADSLTRT01AActual Treatment for Period 01characterVITAMIN D3, PLACEBO, NOT TREATEDConvert TRT01SRC to upper case.Derived
ADSLTRTSDTMDatetime of First Exposure to TreatmentnumericISO 8601Impute incomplete datetime from TRTSDTC, defaulting the time to the first instant of the day; a missing source → blank; invalid text → blank.Derived
ADSLTRTSTMFTime of First Exposure Imputation FlagcharacterHIf TRTSTPR = 'D' then "H"; otherwise blank.Derived
ADSLTRTEDTMDatetime of Last Exposure to TreatmentnumericISO 8601Impute incomplete datetime from TRTEDTC, defaulting the time to the last instant of the day; a missing source → blank; invalid text → blank.Derived
ADSLTRTETMFTime of Last Exposure Imputation FlagcharacterHIf TRTETPR = 'D' then "H"; otherwise blank.Derived
ADSLTRTSDTDate of First Exposure to TreatmentnumericISO 8601Take the date part of TRTSDTM.Derived
ADSLTRTEDTDate of Last Exposure to TreatmentnumericISO 8601Take the date part of TRTEDTM.Derived
ADSLTRTDURDTotal Treatment Duration (Days)numericWhole days from TRTSDT to TRTEDT, counting both endpoints.Derived
ADSLSAFFLSafety Population FlagcharacterY, N"Y" when TRTSDT IS NOT NULL; "N" otherwise; "N" when the condition is unknown.Derived
VariablePermitted values
TRT01AVITAMIN D3, PLACEBO, NOT TREATED
TRTSTMFH
TRTETMFH
SAFFLY, N
VersionDateAuthorDescriptionReviewerSign-off
1.32026-09-29yamaa docs buildShow study codelist names, external dictionaries, and ISO 8601 formats in variable rows; list the document's codelist values and metadata
1.22026-09-29yamaa docs buildClassify ADaM copies from a declared input as Predecessor and recodes as Derived, and SDTM recodes of locally derived columns as Derived
1.12026-09-29yamaa docs buildResolve lookups into the donor record they select, and give every derivation shape a plain-language rule instead of its raw YAML
1.0yamaa docs buildInitial generation from spec.yaml

Agent prompt

The request an agent receives with the input files; each tier states no more than the one above

prompts/adam-adsl-treatment/full.md

Edit
33 lines

Following CDISC ADaM standards, use the provided DM and EX datasets to create an ADSL dataset with one record per subject.

The output dataset should contain the following columns in this order: STUDYID, USUBJID, TRT01A, TRTSDT, TRTSDTM, TRTSTMF, TRTEDT, TRTEDTM, TRTETMF, TRTDURD, SAFFL

TRT01A is the treatment actually received, in upper case: the treatment of the earliest qualifying exposure (a record naming VITAMIN D3 or PLACEBO that carries a start), with ties on the start broken by the lower sequence number; then the actual arm when no exposure qualifies; then the text NOT TREATED when neither source names a treatment. An exposure record with no matching demographics record adds no record.

TRTSDT and TRTSDTM are the date and date-time of the earliest qualifying exposure start; both have no value when the subject has no qualifying exposure record. A start collected as a date alone reads midnight, and TRTSTMF holds H then, and has no value otherwise.

TRTEDT and TRTEDTM are the date and date-time of the latest exposure end among VITAMIN D3 and PLACEBO records; a record need not have a start to name an end. An end collected as a date alone reads the last moment of the day, and TRTETMF holds H then, and has no value otherwise. Both have no value when no such record has an end.

TRTDURD is the number of days from TRTSDT to TRTEDT, counting both the first and the last day, so a single treatment day gives one; it has no value when either date has none. SAFFL is Y when the subject has a treatment start date and N otherwise.

Read the source datasets from /app/input and save the completed dataset as /app/output/adsl.csv.

prompts/adam-adsl-treatment/conventions.md

Edit
15 lines

Following CDISC ADaM standards, use the provided DM and EX datasets to create an ADSL dataset with one record per subject.

The output dataset should contain the following columns in this order: STUDYID, USUBJID, TRT01A, TRTSDT, TRTSDTM, TRTSTMF, TRTEDT, TRTEDTM, TRTETMF, TRTDURD, SAFFL

TRT01A is the treatment in upper case, or the text NOT TREATED.

TRTSTMF and TRTETMF each hold H or have no value.

TRTDURD counts both the first and the last day. SAFFL is Y or N.

Read the source datasets from /app/input and save the completed dataset as /app/output/adsl.csv.

prompts/adam-adsl-treatment/brief.md

Edit
9 lines

Following CDISC ADaM standards, use the provided DM and EX datasets to create an ADSL dataset with one record per subject.

The output dataset should contain the following columns in this order: STUDYID, USUBJID, TRT01A, TRTSDT, TRTSDTM, TRTSTMF, TRTEDT, TRTEDTM, TRTETMF, TRTDURD, SAFFL

Read the source datasets from /app/input and save the completed dataset as /app/output/adsl.csv.

yamaa code

Runs the yamaa specification; its result must match the expected output

run.R

Edit
4 lines
library(yamaa)adsl <- yamaa_domain("spec.yaml")$outputadsl

run.py

Edit
4 lines
import yamaaadsl = yamaa.yamaa_domain("spec.yaml").outputadsl

Solution without yamaa

Written from the full prompt and the inputs alone; CI checks it reproduces the expected output

solutions/adam-adsl-treatment/result.R

Edit
80 lines
# Reference solution for the yamaa benchmark adam-adsl-treatment (R track).## Written from the benchmark's full prompt and its inputs alone, as an agent# would write it. Harbor's oracle agent runs it to check that the prompt can# be solved and that the grader scores a correct answer 1.library(dplyr, warn.conflicts = FALSE)library(readr)library(stringr)library(lubridate)dm <- read_csv(  "/app/input/dm.csv",  col_types = cols(.default = col_character()))ex <- read_csv(  "/app/input/ex.csv",  col_types = cols(EXSEQ = col_integer(), .default = col_character()))qual <- c("VITAMIN D3", "PLACEBO")parse_start <- function(x) {  x[!is.na(x) & x == ""] <- NA_character_  dt <- suppressWarnings(ymd_hms(x, quiet = TRUE, tz = "UTC"))  d <- suppressWarnings(ymd(x, quiet = TRUE, tz = "UTC"))  coalesce(dt, as.POSIXct(d, tz = "UTC"))}parse_end <- function(x) {  base <- parse_start(x)  is_date <- !is.na(x) & x != "" & !str_detect(x, "T")  base[is_date] <- base[is_date] + 23 * 3600 + 59 * 60 + 59  base}ex_dt <- ex |>  mutate(    `__start_dt` = parse_start(EXSTDTC),    `__end_dt` = parse_end(EXENDTC)  )first_start <- ex_dt |>  filter(EXTRT %in% qual, !is.na(`__start_dt`)) |>  arrange(`__start_dt`, EXSEQ) |>  distinct(STUDYID, USUBJID, .keep_all = TRUE) |>  select(STUDYID, USUBJID, TRT01RAW = EXTRT, `__stdtm` = `__start_dt`, `__stdtc` = EXSTDTC)last_end <- ex_dt |>  filter(EXTRT %in% qual, !is.na(`__end_dt`)) |>  arrange(desc(`__end_dt`)) |>  distinct(STUDYID, USUBJID, .keep_all = TRUE) |>  select(STUDYID, USUBJID, `__endtm` = `__end_dt`, `__endtc` = EXENDTC)adsl <- dm |>  left_join(first_start, by = c("STUDYID", "USUBJID")) |>  left_join(last_end, by = c("STUDYID", "USUBJID")) |>  mutate(    `__src` = coalesce(TRT01RAW, ACTARM),    TRT01A = case_when(      is.na(`__src`) | `__src` == "" ~ "NOT TREATED",      .default = str_to_upper(`__src`)    ),    TRTSDTM = format(`__stdtm`, "%Y-%m-%dT%H:%M:%S", tz = "UTC"),    TRTSDT = as.Date(`__stdtm`, tz = "UTC"),    TRTSTMF = if_else(!is.na(`__stdtc`) & !str_detect(`__stdtc`, "T"), "H", NA_character_),    TRTEDTM = format(`__endtm`, "%Y-%m-%dT%H:%M:%S", tz = "UTC"),    TRTEDT = as.Date(`__endtm`, tz = "UTC"),    TRTETMF = if_else(!is.na(`__endtc`) & !str_detect(`__endtc`, "T"), "H", NA_character_),    TRTDURD = as.integer(TRTEDT - TRTSDT) + 1L,    SAFFL = if_else(is.na(TRTSDT), "N", "Y")  ) |>  select(    STUDYID, USUBJID, TRT01A, TRTSDT, TRTSDTM, TRTSTMF,    TRTEDT, TRTEDTM, TRTETMF, TRTDURD, SAFFL  ) |>  arrange(USUBJID)dir.create("/app/output", showWarnings = FALSE)write_csv(adsl, "/app/output/adsl.csv", na = "")

solutions/adam-adsl-treatment/result.py

Edit
93 lines
# Reference solution for the yamaa benchmark adam-adsl-treatment (Python track).## Written from the benchmark's full prompt and its inputs alone, as an agent# would write it. Harbor's oracle agent runs it to check that the prompt can# be solved and that the grader scores a correct answer 1.from pathlib import Pathimport polars as pldm = pl.read_csv("/app/input/dm.csv", infer_schema=False, null_values="")ex = pl.read_csv("/app/input/ex.csv", infer_schema=False, null_values="").with_columns(    pl.col("EXSEQ").cast(pl.Int64, strict=False),)qual = ["VITAMIN D3", "PLACEBO"]# A start collected as a date alone reads midnight; an end collected as a# date alone reads the last moment of the day.ex_dt = ex.with_columns(    __start_dt=pl.col("EXSTDTC").str.to_datetime(strict=False),    __end_base=pl.col("EXENDTC").str.to_datetime(strict=False),).with_columns(    __end_dt=pl.when(pl.col("EXENDTC").str.contains("T"))    .then(pl.col("__end_base"))    .when(pl.col("EXENDTC").str.len_chars() == 10)    .then(pl.col("__end_base") + pl.duration(hours=23, minutes=59, seconds=59))    .otherwise(pl.col("__end_base")),)start_qual = ex_dt.filter(    pl.col("EXTRT").is_in(qual) & pl.col("__start_dt").is_not_null()).sort(["__start_dt", "EXSEQ"])first_start = start_qual.unique(["STUDYID", "USUBJID"], keep="first", maintain_order=True).select(    "STUDYID", "USUBJID", TRT01RAW="EXTRT", TRTSDTM_DT="__start_dt", __first_std="EXSTDTC")end_qual = ex_dt.filter(    pl.col("EXTRT").is_in(qual) & pl.col("__end_dt").is_not_null()).sort(["__end_dt"], descending=True)last_end = end_qual.unique(["STUDYID", "USUBJID"], keep="first", maintain_order=True).select(    "STUDYID", "USUBJID", TRTEDTM_DT="__end_dt", __last_endtc="EXENDTC")adsl = (    dm.join(first_start, on=["STUDYID", "USUBJID"], how="left", maintain_order="left")    .join(last_end, on=["STUDYID", "USUBJID"], how="left")    .with_columns(        __src=pl.coalesce("TRT01RAW", "ACTARM"),    )    .with_columns(        TRT01A=pl.when(pl.col("__src").is_null())        .then(pl.lit("NOT TREATED"))        .otherwise(pl.col("__src").str.to_uppercase()),        TRTSDTM=pl.col("TRTSDTM_DT").dt.strftime("%Y-%m-%dT%H:%M:%S"),        TRTSDT=pl.col("TRTSDTM_DT").cast(pl.Date),        TRTSTMF=pl.when(            pl.col("__first_std").is_not_null() & ~pl.col("__first_std").str.contains("T")        )        .then(pl.lit("H"))        .otherwise(None),        TRTEDTM=pl.col("TRTEDTM_DT").dt.strftime("%Y-%m-%dT%H:%M:%S"),        TRTEDT=pl.col("TRTEDTM_DT").cast(pl.Date),        TRTETMF=pl.when(            pl.col("__last_endtc").is_not_null() & ~pl.col("__last_endtc").str.contains("T")        )        .then(pl.lit("H"))        .otherwise(None),    )    .with_columns(        TRTDURD=(pl.col("TRTEDT") - pl.col("TRTSDT")).dt.total_days() + 1,        SAFFL=pl.when(pl.col("TRTSDT").is_not_null())        .then(pl.lit("Y"))        .otherwise(pl.lit("N")),    )    .select(        "STUDYID",        "USUBJID",        "TRT01A",        "TRTSDT",        "TRTSDTM",        "TRTSTMF",        "TRTEDT",        "TRTEDTM",        "TRTETMF",        "TRTDURD",        "SAFFL",    )    .sort("USUBJID"))Path("/app/output").mkdir(parents=True, exist_ok=True)adsl.write_csv("/app/output/adsl.csv", null_value="")

solutions/adam-adsl-treatment/result.sas

Edit
50 lines
/* opensas reference solution for the yamaa benchmark adam-adsl-treatment.   Reads the benchmark inputs and derives the requested output.   Execute with the existing opensas runtime: opensas /app/output/result.sas. */data dm;  length STUDYID USUBJID ACTARM $1024;  infile "/app/input/dm.csv" dsd dlm="," firstobs=2 truncover lrecl=32767;  input STUDYID : $char1024. USUBJID : $char1024. ACTARM : $char1024.;run;data ex;  length STUDYID USUBJID EXTRT EXSTDTC EXENDTC $1024;  infile "/app/input/ex.csv" dsd dlm="," firstobs=2 truncover lrecl=32767;  input STUDYID : $char1024. USUBJID : $char1024. EXSEQ EXTRT : $char1024. EXDOSE EXSTDTC : $char1024. EXENDTC : $char1024.;run;data exposure;  length startmoment endmoment $19;  set ex;  if upcase(EXTRT) in ('VITAMIN D3','PLACEBO');  startmoment=EXSTDTC; endmoment=EXENDTC;  if lengthn(EXSTDTC)=10 then startmoment=cats(EXSTDTC,'T00:00:00');  else if lengthn(EXSTDTC)=16 then startmoment=cats(EXSTDTC,':00');  if lengthn(EXENDTC)=10 then endmoment=cats(EXENDTC,'T23:59:59');  else if lengthn(EXENDTC)=16 then endmoment=cats(EXENDTC,':00');run;proc sort data=exposure(where=(EXSTDTC ne '')) out=starts; by STUDYID USUBJID startmoment EXSEQ; run;data firstex; set starts; by STUDYID USUBJID; if first.USUBJID; run;proc sort data=exposure(where=(EXENDTC ne '')) out=ends; by STUDYID USUBJID descending endmoment; run;data lastex; set ends; by STUDYID USUBJID; if first.USUBJID; run;proc sql; create table joined as select a.*,b.EXTRT,b.EXSTDTC,b.startmoment,c.EXENDTC,c.endmoment from dm as a  left join firstex as b on a.STUDYID=b.STUDYID and a.USUBJID=b.USUBJID left join lastex as c on a.STUDYID=c.STUDYID and a.USUBJID=c.USUBJID; quit;data result;  length TRT01A $1024 TRTSDT TRTEDT $10 TRTSDTM TRTEDTM $19 TRTSTMF TRTETMF SAFFL $1;  set joined;  TRT01A=upcase(coalescec(EXTRT,ACTARM,'NOT TREATED'));  TRTSDT=''; TRTEDT=''; TRTSTMF=''; TRTETMF=''; TRTDURD=.;  if lengthn(EXSTDTC)>=10 then TRTSDT=substr(EXSTDTC,1,10);  if lengthn(EXENDTC)>=10 then TRTEDT=substr(EXENDTC,1,10);  TRTSDTM=tranwrd(startmoment,'T',' '); TRTEDTM=tranwrd(endmoment,'T',' ');  if lengthn(EXSTDTC)=10 then TRTSTMF='H';  if lengthn(EXENDTC)=10 then TRTETMF='H';  if not missing(TRTSDT) and not missing(TRTEDT) then TRTDURD=input(TRTEDT,yymmdd10.)-input(TRTSDT,yymmdd10.)+1;  SAFFL='N'; if not missing(TRTSDT) then SAFFL='Y';run;proc sql;  create table final as select STUDYID, USUBJID, TRT01A, TRTSDT, TRTSDTM, TRTSTMF, TRTEDT, TRTEDTM, TRTETMF, TRTDURD, SAFFL from result;quit;proc export data=final outfile="/app/output/adsl.csv" dbms=csv replace; run;