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 SDTM: 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: draftEditUnder review
What this benchmark means
README.md

Goal: carry each collected vital-signs result with its actual collection datetime, and add the planned time point it was scheduled for: VSTESTCD, VSORRES, VSTPT, VSTPTNUM, VSELTM, VSDTC, and VSDY.

Input: long-form Operational Data Model (ODM) data with one row per collected item, carrying the collection form (FormOID), the item (ItemOID), and the stored value (Value); a small test dictionary (vs_mapping.csv) translating each vital-signs item into its test short name, test name, and unit; plus demographics rows carrying the reference start date. Each planned time point arrives as its own vital-signs form, so the form tells which scheduled reading a result belongs to. The first-dose datetime rides along on the dosing form FO.EX.

Variables:

  • VSSEQ numbers the records per subject in schedule order, then pulse, systolic, and diastolic blood pressure within each time point.
  • VSTPT is the planned time point name for the collection form: PRE-DOSE, 30 MIN POST-DOSE, 1 H POST-DOSE, or 4 H POST-DOSE.
  • VSTPTNUM is the planned time point number for the form: 1 through 4 in schedule order.
  • VSELTM is the planned elapsed time since the first dose for the form, in ISO 8601 duration form: -PT15M before the dose, then PT30M, PT1H, and PT4H after it.
  • VSTESTCD is the test short name, looked up from the test dictionary by item: PULSE, SYSBP, or DIABP. Items with no dictionary entry (the datetime and dosing items) give no record.
  • VSORRES is the result in original units, the value on the test's item row within the form.
  • VSORRESU is the original unit from the test dictionary: beats/min or mmHg.
  • VSSTRESN is the numeric result in standard units; pulse and blood pressure were collected in standard units, so it equals the original result.
  • VSSTRESU is the standard unit, carried from the original unit.
  • VSDTC is the actual collection datetime from the form's datetime item row, shared by every result on that form; a form with no datetime item stops the run.
  • VSDY is the study day of the collection datetime, counted from the subject's reference start date: that date is day 1, there is no day zero, and dates before it count back from -1; missing when the subject has no reference start date.

Note: the planned elapsed time stays in duration form next to the actual collection datetime, so a reading taken later than planned keeps its late actual datetime and the gap shows when the two are compared. Each reading is also checked against the first-dose datetime: a post-dose reading dated before the dose, or a pre-dose reading dated after it, stops the run.

Inputs

Under review
3 source files

input/dm.csv

Edit
2 rows
input/dm.csv
STUDYIDUSUBJIDRFSTDTC
YAMAA-012025-01-08
YAMAA-012025-01-15

input/odm.csv

Edit
34 rows
input/odm.csv
StudyOIDMetaDataVersionOIDSubjectKeyStudyEventOIDStudyEventRepeatKeyFormOIDFormRepeatKeyItemGroupOIDItemGroupRepeatKeyItemOIDValue
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_PREDOSE1IG.VS1IT.VS.VSDTC2025-01-08T08:45:00
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_PREDOSE1IG.VS1IT.VS.PULSE72
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_PREDOSE1IG.VS1IT.VS.SYSBP118
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_PREDOSE1IG.VS1IT.VS.DIABP76
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_30MIN1IG.VS1IT.VS.VSDTC2025-01-08T09:31:00
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_30MIN1IG.VS1IT.VS.PULSE74
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_30MIN1IG.VS1IT.VS.SYSBP120
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_30MIN1IG.VS1IT.VS.DIABP78
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_1H1IG.VS1IT.VS.VSDTC2025-01-08T10:02:00
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_1H1IG.VS1IT.VS.PULSE70
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_1H1IG.VS1IT.VS.SYSBP116
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_1H1IG.VS1IT.VS.DIABP74
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_4H1IG.VS1IT.VS.VSDTC2025-01-08T13:05:00
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_4H1IG.VS1IT.VS.PULSE68
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_4H1IG.VS1IT.VS.SYSBP114
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.VS_4H1IG.VS1IT.VS.DIABP72
YAMAA-01MDV.1YAMAA-01-001SE.D11FO.EX1IG.EX1IT.EX.EXSTDTC2025-01-08T09:00:00
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_PREDOSE1IG.VS1IT.VS.VSDTC2025-01-15T08:44:00
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_PREDOSE1IG.VS1IT.VS.PULSE80
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_PREDOSE1IG.VS1IT.VS.SYSBP124
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_PREDOSE1IG.VS1IT.VS.DIABP82
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_30MIN1IG.VS1IT.VS.VSDTC2025-01-15T09:52:00
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_30MIN1IG.VS1IT.VS.PULSE88
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_30MIN1IG.VS1IT.VS.SYSBP128
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_30MIN1IG.VS1IT.VS.DIABP84
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_1H1IG.VS1IT.VS.VSDTC2025-01-15T10:03:00
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_1H1IG.VS1IT.VS.PULSE78
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_1H1IG.VS1IT.VS.SYSBP122
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_1H1IG.VS1IT.VS.DIABP80
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_4H1IG.VS1IT.VS.VSDTC2025-01-15T13:01:00
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_4H1IG.VS1IT.VS.PULSE76
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_4H1IG.VS1IT.VS.SYSBP120
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.VS_4H1IG.VS1IT.VS.DIABP78
YAMAA-01MDV.1YAMAA-01-002SE.D11FO.EX1IG.EX1IT.EX.EXSTDTC2025-01-15T09:00:00

input/vs_mapping.csv

Edit
3 rows
input/vs_mapping.csv
ItemOIDVSTESTCDVSTESTUNIT
IT.VS.PULSEPULSEPulsebeats/min
IT.VS.SYSBPSYSBPSystolic Blood PressuremmHg
IT.VS.DIABPDIABPDiastolic Blood PressuremmHg

Expected output

Under review
24 expected rows

expected/vs.csv

Edit
24 rows
expected/vs.csv
DOMAINSTUDYIDUSUBJIDVSSEQVISITVSTPTVSTPTNUMVSELTMVSTESTCDVSORRESVSORRESUVSSTRESNVSSTRESUVSDTCVSDY
VSYAMAA-011DAY 1PRE-DOSE1-PT15MPULSE72beats/min72beats/min2025-01-08T08:45:001
VSYAMAA-012DAY 1PRE-DOSE1-PT15MSYSBP118mmHg118mmHg2025-01-08T08:45:001
VSYAMAA-013DAY 1PRE-DOSE1-PT15MDIABP76mmHg76mmHg2025-01-08T08:45:001
VSYAMAA-014DAY 130 MIN POST-DOSE2PT30MPULSE74beats/min74beats/min2025-01-08T09:31:001
VSYAMAA-015DAY 130 MIN POST-DOSE2PT30MSYSBP120mmHg120mmHg2025-01-08T09:31:001
VSYAMAA-016DAY 130 MIN POST-DOSE2PT30MDIABP78mmHg78mmHg2025-01-08T09:31:001
VSYAMAA-017DAY 11 H POST-DOSE3PT1HPULSE70beats/min70beats/min2025-01-08T10:02:001
VSYAMAA-018DAY 11 H POST-DOSE3PT1HSYSBP116mmHg116mmHg2025-01-08T10:02:001
VSYAMAA-019DAY 11 H POST-DOSE3PT1HDIABP74mmHg74mmHg2025-01-08T10:02:001
VSYAMAA-0110DAY 14 H POST-DOSE4PT4HPULSE68beats/min68beats/min2025-01-08T13:05:001
VSYAMAA-0111DAY 14 H POST-DOSE4PT4HSYSBP114mmHg114mmHg2025-01-08T13:05:001
VSYAMAA-0112DAY 14 H POST-DOSE4PT4HDIABP72mmHg72mmHg2025-01-08T13:05:001
VSYAMAA-011DAY 1PRE-DOSE1-PT15MPULSE80beats/min80beats/min2025-01-15T08:44:001
VSYAMAA-012DAY 1PRE-DOSE1-PT15MSYSBP124mmHg124mmHg2025-01-15T08:44:001
VSYAMAA-013DAY 1PRE-DOSE1-PT15MDIABP82mmHg82mmHg2025-01-15T08:44:001
VSYAMAA-014DAY 130 MIN POST-DOSE2PT30MPULSE88beats/min88beats/min2025-01-15T09:52:001
VSYAMAA-015DAY 130 MIN POST-DOSE2PT30MSYSBP128mmHg128mmHg2025-01-15T09:52:001
VSYAMAA-016DAY 130 MIN POST-DOSE2PT30MDIABP84mmHg84mmHg2025-01-15T09:52:001
VSYAMAA-017DAY 11 H POST-DOSE3PT1HPULSE78beats/min78beats/min2025-01-15T10:03:001
VSYAMAA-018DAY 11 H POST-DOSE3PT1HSYSBP122mmHg122mmHg2025-01-15T10:03:001
VSYAMAA-019DAY 11 H POST-DOSE3PT1HDIABP80mmHg80mmHg2025-01-15T10:03:001
VSYAMAA-0110DAY 14 H POST-DOSE4PT4HPULSE76beats/min76beats/min2025-01-15T13:01:001
VSYAMAA-0111DAY 14 H POST-DOSE4PT4HSYSBP120mmHg120mmHg2025-01-15T13:01:001
VSYAMAA-0112DAY 14 H POST-DOSE4PT4HDIABP78mmHg78mmHg2025-01-15T13:01:001
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

Variable NameVariable LabelTypeLengthControlled Terms or FormatOriginCoreConversion DefinitionVariable TypeVariable OrderComments for Define
DOMAINDomain AbbreviationCharAssignedSet constant value "VS".SDTM1
STUDYIDStudy IdentifierCharCollectedCopy value from ODM.StudyOID.SDTM2
USUBJIDUnique Subject IdentifierCharCollectedCopy value from ODM.SubjectKey.SDTM3
VISITVisit NameCharCollectedRecode ODM.StudyEventOID: "SE.D1" → "DAY 1"; missing or unlisted values are errors.SDTM4
VSTPTPlanned Time Point NameCharCollectedRecode ODM.FormOID: "FO.VS_PREDOSE" → "PRE-DOSE", "FO.VS_30MIN" → "30 MIN POST-DOSE", "FO.VS_1H" → "1 H POST-DOSE", "FO.VS_4H" → "4 H POST-DOSE"; missing or unlisted values are errors.SDTM5
VSTPTNUMPlanned Time Point NumberNumCollectedRecode ODM.FormOID: "FO.VS_PREDOSE" → "1", "FO.VS_30MIN" → "2", "FO.VS_1H" → "3", "FO.VS_4H" → "4"; missing or unlisted values are errors.SDTM6
VSELTMPlanned Elapsed TimeCharCollectedRecode ODM.FormOID: "FO.VS_PREDOSE" → "-PT15M", "FO.VS_30MIN" → "PT30M", "FO.VS_1H" → "PT1H", "FO.VS_4H" → "PT4H"; missing or unlisted values are errors.SDTM7
VSTESTCDVital Signs Test Short NameCharPULSE, SYSBP, DIABPDerivedSet per constructed row; see the Row construction sheet.SDTM8
VSSEQSequence NumberNumDerivedRow number within each group of STUDYID, USUBJID, ordered by VSTPTNUM then ORD_TEST.SDTM9
VSORRESResult or Finding in Original UnitsCharDerivedSet per constructed row; see the Row construction sheet.SDTM10
VSORRESUOriginal UnitsCharbeats/min, mmHgDerivedSet per constructed row; see the Row construction sheet.SDTM11
VSSTRESNNumeric Result/Finding in Standard UnitsNumDerivedCopy value from VSORRES.SDTM12
VSSTRESUStandard UnitsCharDerivedCopy value from VSORRESU.SDTM13
VSDTCDate/Time of MeasurementsCharISO 8601DerivedAggregate ONLY(ODM.Value) over records whose StudyOID equals ODM.StudyOID, SubjectKey equals ODM.SubjectKey, StudyEventOID equals ODM.StudyEventOID and FormOID equals ODM.FormOID, where ODM.ItemOID = 'IT.VS.VSDTC'.SDTM14
VSDYStudy Day of Vital SignsNumDerivedCDISC study day of VSDT against RFSTDTC as day 1; there is no day zero.SDTM15
TemplateInclude whenVSTESTCDVSORRESVSORRESU
vs_readingVSTESTCD IS NOT NULLCopy VSTESTCD from the VS_MAPPING record whose ItemOID equals ODM.ItemOID, and no match yields blankAggregate ONLY(ODM.Value) over records matching on the shared output keysCopy UNIT from the VS_MAPPING record whose ItemOID equals ODM.ItemOID, and no match yields blank
VariablePermitted values
VSTESTCDPULSE, SYSBP, DIABP
VSORRESUbeats/min, mmHg
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/sdtm-vs-planned-time-points/full.md

Edit
35 lines

Following CDISC SDTM standards, use the provided ODM, DM, and VS_MAPPING datasets to create a VS dataset with one record per collected vital-signs result.

The output dataset should contain the following columns in this order: DOMAIN, STUDYID, USUBJID, VSSEQ, VISIT, VSTPT, VSTPTNUM, VSELTM, VSTESTCD, VSORRES, VSORRESU, VSSTRESN, VSSTRESU, VSDTC, VSDY

STUDYID is the ODM StudyOID and USUBJID is the SubjectKey, each as given.

VSSEQ numbers the records per subject from 1 in schedule order, then pulse, systolic, and diastolic blood pressure within each time point. VISIT is DAY 1 for study event SE.D1. VSTPT is the planned time point name for the collection form: PRE-DOSE, 30 MIN POST-DOSE, 1 H POST-DOSE, or 4 H POST-DOSE. VSTPTNUM is the planned time point number for the form: 1 through 4 in schedule order. VSELTM is the planned elapsed time since the first dose for the form: -PT15M before the dose, then PT30M, PT1H, and PT4H after it.

VSTESTCD is the test short name from the test dictionary: PULSE, SYSBP, or DIABP. Items with no dictionary entry give no record. VSORRES is the result in original units, and VSORRESU the original unit from the test dictionary (beats/min or mmHg). VSSTRESN is the numeric result in standard units; pulse and blood pressure were collected in standard units, so it equals the original result. VSSTRESU is the standard unit, carried from the original unit. VSDTC is the actual collection datetime from the form's datetime item row, shared by every result on that form. VSDY is the study day of the collection datetime relative to the subject's reference start date.

The planned elapsed time stays next to the actual collection datetime, so a reading taken later than planned keeps its late actual datetime.

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

prompts/sdtm-vs-planned-time-points/conventions.md

Edit
16 lines

Following CDISC SDTM standards, use the provided ODM, DM, and VS_MAPPING datasets to create a VS dataset with one record per collected vital-signs result.

The output dataset should contain the following columns in this order: DOMAIN, STUDYID, USUBJID, VSSEQ, VISIT, VSTPT, VSTPTNUM, VSELTM, VSTESTCD, VSORRES, VSORRESU, VSSTRESN, VSSTRESU, VSDTC, VSDY

VISIT is DAY 1 for study event SE.D1. VSTPT is PRE-DOSE, 30 MIN POST-DOSE, 1 H POST-DOSE, or 4 H POST-DOSE. VSTPTNUM is 1 through 4 in schedule order. VSELTM is -PT15M before the dose, then PT30M, PT1H, and PT4H after it.

VSTESTCD is PULSE, SYSBP, or DIABP.

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

prompts/sdtm-vs-planned-time-points/brief.md

Edit
10 lines

Following CDISC SDTM standards, use the provided ODM, DM, and VS_MAPPING datasets to create a VS dataset with one record per collected vital-signs result.

The output dataset should contain the following columns in this order: DOMAIN, STUDYID, USUBJID, VSSEQ, VISIT, VSTPT, VSTPTNUM, VSELTM, VSTESTCD, VSORRES, VSORRESU, VSSTRESN, VSSTRESU, VSDTC, VSDY

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

yamaa code

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

run.R

Edit
4 lines
library(yamaa)vs <- yamaa_domain("spec.yaml")$outputvs

run.py

Edit
4 lines
import yamaavs = yamaa.yamaa_domain("spec.yaml").outputvs

Solution without yamaa

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

solutions/sdtm-vs-planned-time-points/result.R

Edit
106 lines
# Reference solution for the yamaa benchmark sdtm-vs-planned-time-points (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)library(readr)library(tidyr)study_day <- function(ref, coll) {  if (is.na(ref) || is.na(coll)) {    return(NA_integer_)  }  delta <- as.integer(coll - ref)  if (delta >= 0L) delta + 1L else delta}time_point <- function(form) {  name <- toupper(form)  if (grepl("PREDOSE", name, fixed = TRUE)) {    list(vstpt = "PRE-DOSE", vstptnum = 1L, vseltm = "-PT15M")  } else if (grepl("30MIN", name, fixed = TRUE)) {    list(vstpt = "30 MIN POST-DOSE", vstptnum = 2L, vseltm = "PT30M")  } else if (grepl("1H", name, fixed = TRUE)) {    list(vstpt = "1 H POST-DOSE", vstptnum = 3L, vseltm = "PT1H")  } else if (grepl("4H", name, fixed = TRUE)) {    list(vstpt = "4 H POST-DOSE", vstptnum = 4L, vseltm = "PT4H")  } else {    NULL  }}dm <- read_csv(  "/app/input/dm.csv",  col_types = cols(.default = col_character()))odm <- read_csv(  "/app/input/odm.csv",  col_types = cols(.default = col_character()))mapping <- read_csv(  "/app/input/vs_mapping.csv",  col_types = cols(.default = col_character()))ref_map <- setNames(suppressWarnings(ymd(dm$RFSTDTC)), dm$USUBJID)test_map <- setNames(mapping$VSTESTCD, mapping$ItemOID)unit_map <- setNames(mapping$UNIT, mapping$ItemOID)forms <- odm |>  filter(StudyEventOID == "SE.D1", grepl("^FO\\.VS_", FormOID)) |>  group_by(SubjectKey, FormOID, StudyOID) |>  summarise(    vsdtc = Value[ItemOID == "IT.VS.VSDTC"][1],    .groups = "drop"  )results <- odm |>  filter(    StudyEventOID == "SE.D1",    grepl("^FO\\.VS_", FormOID),    ItemOID %in% names(test_map),    !is.na(Value) & Value != ""  ) |>  left_join(forms, by = c("SubjectKey", "FormOID", "StudyOID")) |>  rowwise() |>  mutate(    point = list(time_point(FormOID)),    VSTPT = point$vstpt,    VSTPTNUM = point$vstptnum,    VSELTM = point$vseltm,    VSTESTCD = test_map[[ItemOID]],    VSORRES = Value,    VSORRESU = unit_map[[ItemOID]],    VSSTRESN = suppressWarnings(as.numeric(Value)),    VSSTRESU = VSORRESU,    VSDTC = vsdtc,    ref = ref_map[[SubjectKey]],    VSDY = study_day(ref, suppressWarnings(as_date(ymd_hms(VSDTC)))),    test_ord = case_when(      VSTESTCD == "PULSE" ~ 1L,      VSTESTCD == "SYSBP" ~ 2L,      .default = 3L    )  ) |>  ungroup() |>  filter(!is.na(VSTPT), !is.na(VSDTC) & VSDTC != "") |>  arrange(SubjectKey, VSTPTNUM, test_ord) |>  group_by(SubjectKey) |>  mutate(VSSEQ = row_number()) |>  ungroup() |>  arrange(SubjectKey, VSSEQ) |>  transmute(    DOMAIN = "VS",    STUDYID = StudyOID,    USUBJID = SubjectKey,    VSSEQ,    VISIT = "DAY 1",    VSTPT, VSTPTNUM, VSELTM, VSTESTCD, VSORRES, VSORRESU, VSSTRESN, VSSTRESU,    VSDTC, VSDY  )dir.create("/app/output", showWarnings = FALSE)write_csv(results, "/app/output/vs.csv", na = "")

solutions/sdtm-vs-planned-time-points/result.py

Edit
139 lines
# Reference solution for the yamaa benchmark sdtm-vs-planned-time-points (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 datetime import date, datetimefrom pathlib import Pathimport polars as pldef parse_date(text: str | None) -> date | None:    if text is None or text == "":        return None    try:        return datetime.fromisoformat(text).date()    except ValueError:        try:            return date.fromisoformat(text[:10])        except ValueError:            return Nonedef study_day(ref: date | None, coll: date | None) -> int | None:    if ref is None or coll is None:        return None    delta = (coll - ref).days    return delta + 1 if delta >= 0 else deltadef time_point(form: str) -> tuple[str, int, str] | None:    # The planned time point for each collection form, in schedule order.    name = form.upper()    if "PREDOSE" in name:        return ("PRE-DOSE", 1, "-PT15M")    if "30MIN" in name:        return ("30 MIN POST-DOSE", 2, "PT30M")    if "1H" in name:        return ("1 H POST-DOSE", 3, "PT1H")    if "4H" in name:        return ("4 H POST-DOSE", 4, "PT4H")    return Nonedm = pl.read_csv("/app/input/dm.csv", infer_schema=False)odm = pl.read_csv("/app/input/odm.csv", infer_schema=False)mapping = pl.read_csv("/app/input/vs_mapping.csv", infer_schema=False)ref_map = {row["USUBJID"]: parse_date(row["RFSTDTC"]) for row in dm.to_dicts()}test_map = {    row["ItemOID"]: (row["VSTESTCD"], row["UNIT"]) for row in mapping.to_dicts()}# Within each time point: pulse, systolic, then diastolic.test_order = {"PULSE": 1, "SYSBP": 2, "DIABP": 3}# One form per subject per planned time point: its datetime and results.forms: dict[tuple[str, str], dict] = {}for row in odm.to_dicts():    if row["StudyEventOID"] != "SE.D1":        continue    if not row["FormOID"].startswith("FO.VS_"):        continue    key = (row["SubjectKey"], row["FormOID"])    entry = forms.setdefault(        key,        {            "subject": row["SubjectKey"],            "study": row["StudyOID"],            "form": row["FormOID"],            "dt": None,            "results": {},        },    )    if row["ItemOID"] == "IT.VS.VSDTC":        entry["dt"] = row["Value"]    elif row["ItemOID"] in test_map:        entry["results"][row["ItemOID"]] = row["Value"]records = []for entry in forms.values():    point = time_point(entry["form"])    if point is None or entry["dt"] in (None, ""):        continue    vstpt, vstptnum, vseltm = point    coll = parse_date(entry["dt"])    vsdy = study_day(ref_map.get(entry["subject"]), coll)    for item, value in entry["results"].items():        if value is None or value == "":            continue        testcd, unit = test_map[item]        records.append(            {                "DOMAIN": "VS",                "STUDYID": entry["study"],                "USUBJID": entry["subject"],                "VISIT": "DAY 1",                "VSTPT": vstpt,                "VSTPTNUM": vstptnum,                "VSELTM": vseltm,                "VSTESTCD": testcd,                "_tord": test_order.get(testcd, 9),                "VSORRES": value,                "VSORRESU": unit,                "VSSTRESN": float(value),                "VSSTRESU": unit,                "VSDTC": entry["dt"],                "VSDY": vsdy,            }        )vs = (    pl.DataFrame(records)    .sort(["USUBJID", "VSTPTNUM", "_tord"])    .with_columns(        VSSEQ=pl.int_range(1, pl.len() + 1).over("USUBJID"),    )    .sort(["USUBJID", "VSSEQ"])    .select(        "DOMAIN",        "STUDYID",        "USUBJID",        "VSSEQ",        "VISIT",        "VSTPT",        "VSTPTNUM",        "VSELTM",        "VSTESTCD",        "VSORRES",        "VSORRESU",        "VSSTRESN",        "VSSTRESU",        "VSDTC",        "VSDY",    ))Path("/app/output").mkdir(exist_ok=True)vs.write_csv("/app/output/vs.csv")

solutions/sdtm-vs-planned-time-points/result.sas

Edit
53 lines
/* opensas reference solution for the yamaa benchmark sdtm-vs-planned-time-points.   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 RFSTDTC $1024;  infile "/app/input/dm.csv" dsd dlm="," firstobs=2 truncover lrecl=32767;  input STUDYID : $char1024. USUBJID : $char1024. RFSTDTC : $char1024.;run;data odm;  length StudyOID MetaDataVersionOID SubjectKey StudyEventOID StudyEventRepeatKey FormOID FormRepeatKey ItemGroupOID ItemGroupRepeatKey ItemOID Value $1024;  infile "/app/input/odm.csv" dsd dlm="," firstobs=2 truncover lrecl=32767;  input StudyOID : $char1024. MetaDataVersionOID : $char1024. SubjectKey : $char1024. StudyEventOID : $char1024. StudyEventRepeatKey : $char1024. FormOID : $char1024. FormRepeatKey : $char1024. ItemGroupOID : $char1024. ItemGroupRepeatKey : $char1024. ItemOID : $char1024. Value : $char1024.;run;data vs_mapping;  length ItemOID VSTESTCD VSTEST UNIT $1024;  infile "/app/input/vs_mapping.csv" dsd dlm="," firstobs=2 truncover lrecl=32767;  input ItemOID : $char1024. VSTESTCD : $char1024. VSTEST : $char1024. UNIT : $char1024.;run;proc sort data=odm out=ordered; by StudyOID SubjectKey StudyEventOID StudyEventRepeatKey FormOID FormRepeatKey; run;data dates;  length VSDTC $1024;  retain VSDTC;  set ordered;  where ItemGroupOID='IG.VS';  by StudyOID SubjectKey StudyEventOID StudyEventRepeatKey FormOID FormRepeatKey;  if first.FormRepeatKey then do;    VSDTC='';  end;    if ItemOID='IT.VS.VSDTC' then VSDTC=Value;  if last.FormRepeatKey then output;run;proc sql; create table joined as select a.*,b.VSDTC,c.VSTESTCD,c.UNIT,d.RFSTDTC from odm a inner join dates b on a.StudyOID=b.StudyOID and a.SubjectKey=b.SubjectKey and a.StudyEventOID=b.StudyEventOID and a.StudyEventRepeatKey=b.StudyEventRepeatKey and a.FormOID=b.FormOID and a.FormRepeatKey=b.FormRepeatKey inner join vs_mapping c on a.ItemOID=c.ItemOID left join dm d on a.StudyOID=d.STUDYID and a.SubjectKey=d.USUBJID; quit;data prepared; set joined; length DOMAIN $2 STUDYID USUBJID VISIT VSTPT VSELTM VSORRES VSORRESU VSSTRESU $1024; DOMAIN='VS'; STUDYID=StudyOID; USUBJID=SubjectKey; VISIT='DAY 1'; VSTPT=''; VSELTM=''; VSTPTNUM=.;if FormOID='FO.VS_PREDOSE' then do; VSTPT='PRE-DOSE'; VSTPTNUM=1; VSELTM='-PT15M'; end;if FormOID='FO.VS_30MIN' then do; VSTPT='30 MIN POST-DOSE'; VSTPTNUM=2; VSELTM='PT30M'; end;if FormOID='FO.VS_1H' then do; VSTPT='1 H POST-DOSE'; VSTPTNUM=3; VSELTM='PT1H'; end;if FormOID='FO.VS_4H' then do; VSTPT='4 H POST-DOSE'; VSTPTNUM=4; VSELTM='PT4H'; end;VSORRES=Value; VSORRESU=UNIT; VSSTRESU=UNIT; VSSTRESN=input(Value,best32.); VSDY=.; if lengthn(VSDTC)>=10 and lengthn(RFSTDTC)>=10 then do; VSDY=input(substr(VSDTC,1,10),yymmdd10.)-input(substr(RFSTDTC,1,10),yymmdd10.); if VSDY>=0 then VSDY+1; end;TESTORD=3; if VSTESTCD='PULSE' then TESTORD=1; if VSTESTCD='SYSBP' then TESTORD=2; run;proc sort data=prepared; by STUDYID USUBJID VSTPTNUM TESTORD; run;data result; set prepared; by STUDYID USUBJID; retain VSSEQ;  if first.USUBJID then VSSEQ=0;  VSSEQ+1;run;proc sql;  create table final as select DOMAIN, STUDYID, USUBJID, VSSEQ, VISIT, VSTPT, VSTPTNUM, VSELTM, VSTESTCD, VSORRES, VSORRESU, VSSTRESN, VSSTRESU, VSDTC, VSDY from result;quit;proc export data=final outfile="/app/output/vs.csv" dbms=csv replace; run;