Review ask

As a clinical trial statistician or programmer, answer three short questions in Your review: the task, the inputs and outputs, and the AI-assisted review. Agreeing with all three approves this benchmark; disagreeing with any needs a comment. Propose a fix with the Edit button on any source file, or add it to your comments.

We suggest spending no more than 5 minutes on a review. The other tabs beside Your review are background for assessment, not under review.

Summary

Lifecycle: draftEditUnder review
What this benchmark means
README.md

Public entries: Python and R. Passing coverage requires an installed inventory.

Goal: give each infusion its duration in minutes as AVAL, under a parameter PARAM that names the unit, and carry beside it CALCDUR, the duration computed from the start and end times.

Input: Study Data Tabulation Model (SDTM) exposure (EX) records, one per infusion (EXSEQ), with the start (EXSTDTC) and end (EXENDTC) as ISO 8601 date or date-time text and, where the case report form collected it, the infusion's duration as ISO 8601 duration text (EXDUR, for example PT1H30M for one hour 30 minutes).

Variables:

  • AVAL is the collected duration in minutes when EXDUR holds a readable fixed-length ISO 8601 duration; otherwise it is CALCDUR. Text that is not such a duration, for example one without its leading P, counts as not collected. Empty when neither gives a value.
  • CALCDUR is the elapsed time in minutes from the start to the end, with seconds kept as a fraction of a minute and nothing added or rounded. It is empty when the start or the end was collected as a date without a time of day; no time is assumed.

Note: a readable collected duration always wins, even when it disagrees with the start and end times. CALCDUR keeps the computed value beside it so the two can be compared. Every infusion keeps its record, with an empty AVAL when its duration cannot be known.

Inputs

Under review
1 source file

input/ex.csv

Edit
8 rows
input/ex.csv
STUDYIDUSUBJIDEXSEQEXTRTEXROUTEEXSTDTCEXENDTCEXDUR
YAMAA-011STUDY DRUGINTRAVENOUS2025-03-03T09:002025-03-03T10:30PT1H30M
YAMAA-012STUDY DRUGINTRAVENOUS2025-03-242025-03-24PT45M
YAMAA-013STUDY DRUGINTRAVENOUS2025-04-14T09:152025-04-14T11:20PT2H
YAMAA-014STUDY DRUGINTRAVENOUS2025-05-05T08:502025-05-05T10:05—
YAMAA-011STUDY DRUGINTRAVENOUS2025-03-05T13:002025-03-05T14:401H30M
YAMAA-012STUDY DRUGINTRAVENOUS2025-03-26T13:102025-03-26—
YAMAA-013STUDY DRUGINTRAVENOUS2025-04-16T23:30:002025-04-17T00:15:30—
YAMAA-014STUDY DRUGINTRAVENOUS2025-05-072025-05-07T14:00—

Expected output

Under review
8 expected rows

expected/adex.csv

Edit
8 rows
expected/adex.csv
STUDYIDUSUBJIDEXSEQEXSTDTCEXENDTCEXDURPARAMCDPARAMAVALCALCDUR
YAMAA-0112025-03-03T09:002025-03-03T10:30PT1H30MINFDURInfusion Duration (min)9090
YAMAA-0122025-03-242025-03-24PT45MINFDURInfusion Duration (min)45—
YAMAA-0132025-04-14T09:152025-04-14T11:20PT2HINFDURInfusion Duration (min)120125
YAMAA-0142025-05-05T08:502025-05-05T10:05—INFDURInfusion Duration (min)7575
YAMAA-0112025-03-05T13:002025-03-05T14:401H30MINFDURInfusion Duration (min)100100
YAMAA-0122025-03-26T13:102025-03-26—INFDURInfusion Duration (min)——
YAMAA-0132025-04-16T23:30:002025-04-17T00:15:30—INFDURInfusion Duration (min)45.545.5
YAMAA-0142025-05-072025-05-07T14:00—INFDURInfusion Duration (min)——
Shaded columns are derived— = missing value

Human review and assessment

Your review

3 questions and your comments · View benchmark discussions

Mapping spec

Generated from the benchmark YAML; do not edit by hand

Dataset NameVariable NameVariable LabelTypeCodelist/Controlled TermsCoreComputational MethodOrigin
ADEXSTUDYIDStudy IdentifiercharacterCopy value from EX.STUDYID.Predecessor
ADEXUSUBJIDUnique Subject IdentifiercharacterCopy value from EX.USUBJID.Predecessor
ADEXEXSEQSequence NumbernumericCopy value from EX.EXSEQ.Predecessor
ADEXEXSTDTCStart Date/Time of TreatmentcharacterCopy value from EX.EXSTDTC.Predecessor
ADEXEXENDTCEnd Date/Time of TreatmentcharacterCopy value from EX.EXENDTC.Predecessor
ADEXEXDURDuration of TreatmentcharacterCopy value from EX.EXDUR.Predecessor
ADEXPARAMCDParameter CodecharacterSet constant value "INFDUR".Assigned
ADEXPARAMParametercharacterSet constant value "Infusion Duration (min)".Assigned
ADEXCALCDURDuration from Start and End (min)numericElapsed minutes from ASTDTM to AENDTM, negative when AENDTM is earlier, not rounded.Derived
ADEXAVALAnalysis ValuenumericFirst non-missing of COLLDUR and CALCDUR.Derived
VersionDateAuthorDescriptionReviewerSign-off
1.72026-10-20yamaa docs buildRead a specification in the 1.0 surface as its 0.5 counterpart; state that missing values stay missing where a mapping declares no missing handler, and leave out a declared missing answer that restates a handler's default
1.62026-10-17yamaa docs buildDescribe collected date parts written as ISO 8601 date text and times of day counted in seconds since midnight
1.52026-10-17yamaa docs buildName collected precision in words instead of letter codes; state imputation fallbacks, minimum source precision, bounds ignored outside the collected period, and invalid text with no handler as an error; describe date and datetime shifts, imputation flag codes, and partial text giving no date part
1.42026-10-15yamaa docs buildState datetime precision codes through the hour and minute, and complete only the time parts a collected datetime lacks
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-adex-infusion-duration/full.md

Edit
20 lines

Following CDISC ADaM standards, use the provided EX dataset to create an ADEX dataset with one record per infusion.

The output dataset should contain the following columns in this order: STUDYID, USUBJID, EXSEQ, EXSTDTC, EXENDTC, EXDUR, PARAMCD, PARAM, AVAL, CALCDUR

PARAMCD is "INFDUR" and PARAM is "Infusion Duration (min)" on every record. CALCDUR is the elapsed time in minutes from EXSTDTC to EXENDTC, with seconds kept as a fraction of a minute and nothing added or rounded. CALCDUR has no value when EXSTDTC or EXENDTC was collected as a date without a time of day; no time is assumed. AVAL is the collected duration in minutes when EXDUR holds a readable fixed-length ISO 8601 duration; otherwise AVAL is CALCDUR. Text that is not such a duration, for example one without its leading P, counts as not collected. A readable collected duration always wins, even when it disagrees with the start and end times. AVAL has no value when neither gives a value, and every infusion keeps its record.

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

prompts/adam-adex-infusion-duration/conventions.md

Edit
13 lines

Following CDISC ADaM standards, use the provided EX dataset to create an ADEX dataset with one record per infusion.

The output dataset should contain the following columns in this order: STUDYID, USUBJID, EXSEQ, EXSTDTC, EXENDTC, EXDUR, PARAMCD, PARAM, AVAL, CALCDUR

PARAMCD is "INFDUR" and PARAM is "Infusion Duration (min)" on every record. CALCDUR is the elapsed time in minutes from EXSTDTC to EXENDTC, with seconds kept as a fraction of a minute and nothing added or rounded.

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

prompts/adam-adex-infusion-duration/brief.md

Edit
9 lines

Following CDISC ADaM standards, use the provided EX dataset to create an ADEX dataset with one record per infusion.

The output dataset should contain the following columns in this order: STUDYID, USUBJID, EXSEQ, EXSTDTC, EXENDTC, EXDUR, PARAMCD, PARAM, AVAL, CALCDUR

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

yamaa code

Public calls generated from benchmark.yaml; results must match the expected output

R

Edit
3 lines
library(yamaa)yamaa_spec("spec.yaml")$run()$output

Python

Edit
3 lines
import yamaayamaa.spec("spec.yaml").run().output

Solution without yamaa

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

solutions/adam-adex-infusion-duration/result.R

Edit
44 lines
# Reference solution for the yamaa benchmark adam-adex-infusion-duration (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(lubridate, warn.conflicts = FALSE)library(readr)library(stringr)ex <- read_csv(  "/app/input/ex.csv",  col_types = cols(EXSEQ = col_integer(), .default = col_character()))# A date alone has no time of day, and none is assumed; a time without# seconds counts as second zero.moment <- function(x) {  full <- if_else(nchar(x) == 16, paste0(x, ":00"), x)  if_else(!is.na(x) & nchar(x) > 10, ymd_hms(full, tz = "UTC", quiet = TRUE), as.POSIXct(NA, tz = "UTC"))}# An ISO 8601 duration of days, hours, minutes and seconds, with at least one# part; anything else counts as not collected.collected_minutes <- function(x) {  pattern <- "^P(?:(\\d+)D)?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+(?:\\.\\d+)?)S)?)?$"  parts <- str_match(x, pattern)  number <- function(i) coalesce(as.numeric(parts[, i]), 0)  found <- !is.na(parts[, 1]) & rowSums(!is.na(parts[, 2:5, drop = FALSE])) > 0 & !str_ends(coalesce(x, ""), "T")  if_else(found, number(2) * 1440 + number(3) * 60 + number(4) + number(5) / 60, NA_real_)}adex <- ex |>  mutate(    CALCDUR = as.numeric(difftime(moment(EXENDTC), moment(EXSTDTC), units = "mins")),    AVAL = coalesce(collected_minutes(EXDUR), CALCDUR),    PARAMCD = "INFDUR",    PARAM = "Infusion Duration (min)"  ) |>  select(STUDYID, USUBJID, EXSEQ, EXSTDTC, EXENDTC, EXDUR, PARAMCD, PARAM, AVAL, CALCDUR)dir.create("/app/output", showWarnings = FALSE)write_csv(adex, "/app/output/adex.csv", na = "")

solutions/adam-adex-infusion-duration/result.py

Edit
140 lines
# Reference solution for the yamaa benchmark adam-adex-infusion-duration (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."""Create ADEX (one record per infusion) from EX.PARAMCD is "INFDUR" and PARAM is "Infusion Duration (min)" on everyrecord.CALCDUR is the elapsed time in minutes from EXSTDTC to EXENDTC, withseconds kept as a fraction of a minute and nothing added or rounded.It has no value when EXSTDTC or EXENDTC was collected as a datewithout a time of day.AVAL is the collected duration in minutes when EXDUR holds a readablefixed-length ISO 8601 duration; otherwise AVAL is CALCDUR. Text thatis not such a duration (for example one without its leading P) countsas not collected, and a readable collected duration always wins evenwhen it disagrees with the start and end times. AVAL has no value whenneither gives a value; every infusion keeps its record."""import csvimport refrom datetime import datetimeINPUT_EX = "/app/input/ex.csv"OUTPUT_ADEX = "/app/output/adex.csv"COLUMNS = [    "STUDYID",    "USUBJID",    "EXSEQ",    "EXSTDTC",    "EXENDTC",    "EXDUR",    "PARAMCD",    "PARAM",    "AVAL",    "CALCDUR",]# A fixed-length ISO 8601 duration: P[nD][T[nH][nM][nS]] with at least# one component. Years, months and weeks are not fixed length, so a# duration using them is not readable here.DURATION_RE = re.compile(    r"^P(?:(?P<days>\d+(?:\.\d+)?)D)?"    r"(?:T(?:(?P<hours>\d+(?:\.\d+)?)H)?"    r"(?:(?P<minutes>\d+(?:\.\d+)?)M)?"    r"(?:(?P<seconds>\d+(?:\.\d+)?)S)?)?$")def parse_duration_minutes(text):    """Minutes in a fixed-length ISO 8601 duration, or None."""    if not text:        return None    match = DURATION_RE.match(text.strip())    if match is None:        return None    parts = match.groupdict()    if all(value is None for value in parts.values()):        return None    total = 0.0    if parts["days"] is not None:        total += float(parts["days"]) * 1440.0    if parts["hours"] is not None:        total += float(parts["hours"]) * 60.0    if parts["minutes"] is not None:        total += float(parts["minutes"])    if parts["seconds"] is not None:        total += float(parts["seconds"]) / 60.0    return totaldef parse_datetime(text):    """A datetime for an ISO 8601 value that carries a time of day,    or None when the value is a date without a time (or missing)."""    if not text:        return None    text = text.strip()    if "T" not in text:        return None    return datetime.fromisoformat(text)def calc_duration_minutes(start_text, end_text):    """Elapsed minutes from start to end, or None when either value    was collected as a date without a time of day."""    start = parse_datetime(start_text)    end = parse_datetime(end_text)    if start is None or end is None:        return None    return (end - start).total_seconds() / 60.0def format_number(value):    """A number as text: whole minutes without a decimal point,    otherwise the exact value (no rounding beyond float repr)."""    if value is None:        return ""    if float(value).is_integer():        return str(int(value))    return repr(float(value))def main():    with open(INPUT_EX, newline="", encoding="utf-8") as handle:        ex_rows = list(csv.DictReader(handle))    out_rows = []    for row in ex_rows:        calcdur = calc_duration_minutes(row["EXSTDTC"], row["EXENDTC"])        collected = parse_duration_minutes(row["EXDUR"])        aval = collected if collected is not None else calcdur        out_rows.append(            {                "STUDYID": row["STUDYID"],                "USUBJID": row["USUBJID"],                "EXSEQ": row["EXSEQ"],                "EXSTDTC": row["EXSTDTC"],                "EXENDTC": row["EXENDTC"],                "EXDUR": row["EXDUR"],                "PARAMCD": "INFDUR",                "PARAM": "Infusion Duration (min)",                "AVAL": format_number(aval),                "CALCDUR": format_number(calcdur),            }        )    with open(OUTPUT_ADEX, "w", newline="", encoding="utf-8") as handle:        writer = csv.DictWriter(handle, fieldnames=COLUMNS)        writer.writeheader()        writer.writerows(out_rows)if __name__ == "__main__":    main()

solutions/adam-adex-infusion-duration/result.sas

Edit
42 lines
/* opensas reference solution for the yamaa benchmark adam-adex-infusion-duration.   Reads the benchmark inputs and derives the requested output.   Execute with the existing opensas runtime: opensas /app/output/result.sas. */data ex;  length STUDYID USUBJID EXTRT EXROUTE EXSTDTC EXENDTC EXDUR $1024;  infile "/app/input/ex.csv" dsd dlm="," firstobs=2 truncover lrecl=32767;  input STUDYID : $char1024. USUBJID : $char1024. EXSEQ EXTRT : $char1024. EXROUTE : $char1024.        EXSTDTC : $char1024. EXENDTC : $char1024. EXDUR : $char1024.;run;/* A date alone has no time of day, and none is assumed; a time without   seconds counts as second zero. An ISO 8601 duration of days, hours,   minutes and seconds wins; anything else counts as not collected. */data final;  length PARAMCD $6 PARAM $23;  set ex;  PARAMCD='INFDUR'; PARAM='Infusion Duration (min)';  st=.; en=.;  if lengthn(EXSTDTC)>10 then st=input(cats(EXSTDTC, ifc(lengthn(EXSTDTC)=16, ':00', '')), e8601dt.);  if lengthn(EXENDTC)>10 then en=input(cats(EXENDTC, ifc(lengthn(EXENDTC)=16, ':00', '')), e8601dt.);  CALCDUR=.;  if not missing(st) and not missing(en) then CALCDUR=(en-st)/60;  COLL=.;  rx=prxparse('/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/');  if not missing(EXDUR) and prxmatch(rx, strip(EXDUR)) and lengthn(EXDUR)>1 and substr(EXDUR, lengthn(EXDUR), 1) ne 'T' then do;    d=input(prxposn(rx, 1, strip(EXDUR)), ?? best.);    h=input(prxposn(rx, 2, strip(EXDUR)), ?? best.);    m=input(prxposn(rx, 3, strip(EXDUR)), ?? best.);    s=input(prxposn(rx, 4, strip(EXDUR)), ?? best.);    COLL=sum(0, d*1440, h*60, m, s/60);  end;  AVAL=coalesce(COLL, CALCDUR);  format AVAL CALCDUR best32.;  keep STUDYID USUBJID EXSEQ EXSTDTC EXENDTC EXDUR PARAMCD PARAM AVAL CALCDUR;run;proc sql;  create table out as select STUDYID, USUBJID, EXSEQ, EXSTDTC, EXENDTC, EXDUR, PARAMCD, PARAM, AVAL, CALCDUR    from final;quit;proc export data=out outfile="/app/output/adex.csv" dbms=csv replace; run;