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.
- 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.
- Inputs: Do they look like data collected in a real trial, and do they include the situations you would expect to meet in one?
- 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
README.mdGoal: pick the reference range the way a multi-lab study does -- by
lab, test, sex and age band, and by the period the range was in effect --
and flag each result: LBSTNRLO, LBSTNRHI, and LBNRIND.
Input: lab results as Operational Data Model (ODM) item data -- one row
per collected item, with the test code, the collecting lab, the collection
date and the numeric result as IT.LB.* items of a repeating IG.LB panel,
and recorded sex and birth date as IT.DM.* items -- plus a reference
dictionary keyed by lab, test code, sex and age band, each entry carrying the
dates it was in effect, the unit and the lower and upper limits.
Variables:
LBTESTCDis the test code from theIT.LB.LBTESTCDitem.SEXis recorded sex from theIT.DM.SEXitem.LBNAMis the collecting lab from theIT.LB.LBNAMitem; different labs may carry different ranges for the same test.LBSTRESNis the numeric result in standard units from theIT.LB.LBSTRESNitem; missing when the result was not collected.LBORRESUis the unit of the reference entry for the result's lab, test code and sex whose age band holds the subject's age at collection and which was in effect on the collection date; blank, like both limits, when no entry matches.LBSTNRLOis the lower reference limit in standard units from that entry.LBSTNRHIis the upper reference limit in standard units from that entry.LBNRINDisLOWwhen the result is below the lower limit,HIGHwhen it is above the upper limit, andNORMALotherwise; blank when the result or either reference limit is missing.
Note: when a lab revises a range, the entry in effect on the collection
date wins -- a result collected on the first day of the new range is judged
against the new limits -- and a subject whose age falls on an age-band
boundary takes the band that starts at that age. Each output row is built
from one IT.LB.LBDTC item, with the other items of its panel read
alongside; LBSEQ is the panel's repeat key.
Inputs
Under review2 source filesinput/lbrange.csv
Edit10 rows| LBNAM | LBTESTCD | SEX | AGELO | AGEHI | EFFSTDT | EFFENDT | UNIT | NRLO | NRHI |
|---|---|---|---|---|---|---|---|---|---|
| Central Lab | ALT | M | 18 | 99 | 2024-01-01 | 2025-06-30 | U/L | 0 | 40 |
| Central Lab | ALT | M | 18 | 99 | 2025-07-01 | — | U/L | 0 | 45 |
| Central Lab | ALT | F | 18 | 99 | 2024-01-01 | 2025-06-30 | U/L | 0 | 40 |
| Central Lab | ALT | F | 18 | 99 | 2025-07-01 | — | U/L | 0 | 38 |
| Central Lab | CREAT | M | 18 | 99 | 2024-01-01 | — | mg/dL | 0.7 | 1.3 |
| Local Lab A | HGB | F | 0 | 17 | 2024-01-01 | — | g/dL | 11.5 | 15.5 |
| Local Lab A | HGB | F | 18 | 99 | 2024-01-01 | — | g/dL | 12 | 16 |
| Local Lab A | HGB | M | 0 | 17 | 2024-01-01 | — | g/dL | 12 | 16.5 |
| Local Lab A | HGB | M | 18 | 99 | 2024-01-01 | — | g/dL | 13.5 | 17.5 |
| Local Lab A | POTASSIUM | F | 18 | 99 | 2024-01-01 | — | mmol/L | 3.5 | 5.1 |
input/odm.csv
Edit48 rows| StudyOID | MetaDataVersionOID | SubjectKey | StudyEventOID | StudyEventRepeatKey | ItemGroupOID | ItemGroupRepeatKey | ItemOID | Value |
|---|---|---|---|---|---|---|---|---|
| PILOT7 | MDV.1 | P7-501 | SCREENING | 1 | IG.DM | 1 | IT.DM.SEX | M |
| PILOT7 | MDV.1 | P7-501 | SCREENING | 1 | IG.DM | 1 | IT.DM.BRTHDTC | 1980-05-01 |
| PILOT7 | MDV.1 | P7-501 | LAB | 1 | IG.LB | 1 | IT.LB.LBTESTCD | ALT |
| PILOT7 | MDV.1 | P7-501 | LAB | 1 | IG.LB | 1 | IT.LB.LBNAM | Central Lab |
| PILOT7 | MDV.1 | P7-501 | LAB | 1 | IG.LB | 1 | IT.LB.LBDTC | 2025-03-15 |
| PILOT7 | MDV.1 | P7-501 | LAB | 1 | IG.LB | 1 | IT.LB.LBSTRESN | 15 |
| PILOT7 | MDV.1 | P7-502 | SCREENING | 1 | IG.DM | 1 | IT.DM.SEX | F |
| PILOT7 | MDV.1 | P7-502 | SCREENING | 1 | IG.DM | 1 | IT.DM.BRTHDTC | 1990-02-10 |
| PILOT7 | MDV.1 | P7-502 | LAB | 1 | IG.LB | 1 | IT.LB.LBTESTCD | ALT |
| PILOT7 | MDV.1 | P7-502 | LAB | 1 | IG.LB | 1 | IT.LB.LBNAM | Central Lab |
| PILOT7 | MDV.1 | P7-502 | LAB | 1 | IG.LB | 1 | IT.LB.LBDTC | 2025-07-01 |
| PILOT7 | MDV.1 | P7-502 | LAB | 1 | IG.LB | 1 | IT.LB.LBSTRESN | 39 |
| PILOT7 | MDV.1 | P7-503 | SCREENING | 1 | IG.DM | 1 | IT.DM.SEX | F |
| PILOT7 | MDV.1 | P7-503 | SCREENING | 1 | IG.DM | 1 | IT.DM.BRTHDTC | 2017-09-20 |
| PILOT7 | MDV.1 | P7-503 | LAB | 1 | IG.LB | 1 | IT.LB.LBTESTCD | HGB |
| PILOT7 | MDV.1 | P7-503 | LAB | 1 | IG.LB | 1 | IT.LB.LBNAM | Local Lab A |
| PILOT7 | MDV.1 | P7-503 | LAB | 1 | IG.LB | 1 | IT.LB.LBDTC | 2025-02-01 |
| PILOT7 | MDV.1 | P7-503 | LAB | 1 | IG.LB | 1 | IT.LB.LBSTRESN | 12.5 |
| PILOT7 | MDV.1 | P7-504 | SCREENING | 1 | IG.DM | 1 | IT.DM.SEX | M |
| PILOT7 | MDV.1 | P7-504 | SCREENING | 1 | IG.DM | 1 | IT.DM.BRTHDTC | 2007-06-01 |
| PILOT7 | MDV.1 | P7-504 | LAB | 1 | IG.LB | 1 | IT.LB.LBTESTCD | HGB |
| PILOT7 | MDV.1 | P7-504 | LAB | 1 | IG.LB | 1 | IT.LB.LBNAM | Local Lab A |
| PILOT7 | MDV.1 | P7-504 | LAB | 1 | IG.LB | 1 | IT.LB.LBDTC | 2025-06-01 |
| PILOT7 | MDV.1 | P7-504 | LAB | 1 | IG.LB | 1 | IT.LB.LBSTRESN | 12.5 |
| PILOT7 | MDV.1 | P7-505 | SCREENING | 1 | IG.DM | 1 | IT.DM.SEX | M |
| PILOT7 | MDV.1 | P7-505 | SCREENING | 1 | IG.DM | 1 | IT.DM.BRTHDTC | 1975-11-11 |
| PILOT7 | MDV.1 | P7-505 | LAB | 1 | IG.LB | 1 | IT.LB.LBTESTCD | CREAT |
| PILOT7 | MDV.1 | P7-505 | LAB | 1 | IG.LB | 1 | IT.LB.LBNAM | Central Lab |
| PILOT7 | MDV.1 | P7-505 | LAB | 1 | IG.LB | 1 | IT.LB.LBDTC | 2025-04-01 |
| PILOT7 | MDV.1 | P7-505 | LAB | 1 | IG.LB | 1 | IT.LB.LBSTRESN | 0.9 |
| PILOT7 | MDV.1 | P7-506 | SCREENING | 1 | IG.DM | 1 | IT.DM.SEX | M |
| PILOT7 | MDV.1 | P7-506 | SCREENING | 1 | IG.DM | 1 | IT.DM.BRTHDTC | 1988-03-03 |
| PILOT7 | MDV.1 | P7-506 | LAB | 1 | IG.LB | 1 | IT.LB.LBTESTCD | ALT |
| PILOT7 | MDV.1 | P7-506 | LAB | 1 | IG.LB | 1 | IT.LB.LBNAM | Central Lab |
| PILOT7 | MDV.1 | P7-506 | LAB | 1 | IG.LB | 1 | IT.LB.LBDTC | 2025-08-01 |
| PILOT7 | MDV.1 | P7-506 | LAB | 1 | IG.LB | 1 | IT.LB.LBSTRESN | — |
| PILOT7 | MDV.1 | P7-507 | SCREENING | 1 | IG.DM | 1 | IT.DM.SEX | F |
| PILOT7 | MDV.1 | P7-507 | SCREENING | 1 | IG.DM | 1 | IT.DM.BRTHDTC | 1995-12-12 |
| PILOT7 | MDV.1 | P7-507 | LAB | 1 | IG.LB | 1 | IT.LB.LBTESTCD | POTASSIUM |
| PILOT7 | MDV.1 | P7-507 | LAB | 1 | IG.LB | 1 | IT.LB.LBNAM | Local Lab A |
| PILOT7 | MDV.1 | P7-507 | LAB | 1 | IG.LB | 1 | IT.LB.LBDTC | 2025-05-05 |
| PILOT7 | MDV.1 | P7-507 | LAB | 1 | IG.LB | 1 | IT.LB.LBSTRESN | 5.5 |
| PILOT7 | MDV.1 | P7-508 | SCREENING | 1 | IG.DM | 1 | IT.DM.SEX | M |
| PILOT7 | MDV.1 | P7-508 | SCREENING | 1 | IG.DM | 1 | IT.DM.BRTHDTC | 1980-01-01 |
| PILOT7 | MDV.1 | P7-508 | LAB | 1 | IG.LB | 1 | IT.LB.LBTESTCD | GLUCOSE |
| PILOT7 | MDV.1 | P7-508 | LAB | 1 | IG.LB | 1 | IT.LB.LBNAM | Central Lab |
| PILOT7 | MDV.1 | P7-508 | LAB | 1 | IG.LB | 1 | IT.LB.LBDTC | 2025-05-05 |
| PILOT7 | MDV.1 | P7-508 | LAB | 1 | IG.LB | 1 | IT.LB.LBSTRESN | 5 |
Expected output
Under review8 expected rowsexpected/lb.csv
Edit8 rows| DOMAIN | STUDYID | USUBJID | LBSEQ | LBTESTCD | SEX | LBNAM | LBSTRESN | LBORRESU | LBSTNRLO | LBSTNRHI | LBNRIND |
|---|---|---|---|---|---|---|---|---|---|---|---|
| LB | PILOT7 | 1 | ALT | M | Central Lab | 15 | U/L | 0 | 40 | NORMAL | |
| LB | PILOT7 | 1 | ALT | F | Central Lab | 39 | U/L | 0 | 38 | HIGH | |
| LB | PILOT7 | 1 | HGB | F | Local Lab A | 12.5 | g/dL | 11.5 | 15.5 | NORMAL | |
| LB | PILOT7 | 1 | HGB | M | Local Lab A | 12.5 | g/dL | 13.5 | 17.5 | LOW | |
| LB | PILOT7 | 1 | CREAT | M | Central Lab | 0.9 | mg/dL | 0.7 | 1.3 | NORMAL | |
| LB | PILOT7 | 1 | ALT | M | Central Lab | — | U/L | 0 | 45 | — | |
| LB | PILOT7 | 1 | POTASSIUM | F | Local Lab A | 5.5 | mmol/L | 3.5 | 5.1 | HIGH | |
| LB | PILOT7 | 1 | GLUCOSE | M | Central Lab | 5 | — | — | — | — |
Comments and assessment materials
| Variable Name | Variable Label | Type | Length | Controlled Terms or Format | Origin | Core | Conversion Definition | Variable Type | Variable Order | Comments for Define |
|---|---|---|---|---|---|---|---|---|---|---|
| DOMAIN | Domain Abbreviation | Char | Assigned | Set constant value "LB". | SDTM | 1 | ||||
| STUDYID | Study Identifier | Char | Collected | Copy value from ODM.StudyOID. | SDTM | 2 | ||||
| USUBJID | Unique Subject Identifier | Char | Collected | Copy value from ODM.SubjectKey. | SDTM | 3 | ||||
| LBSEQ | Sequence Number | Num | Collected | Copy value from ODM.ItemGroupRepeatKey. | SDTM | 4 | ||||
| LBTESTCD | Lab Test or Examination Short Name | Char | Derived | Take Value from the ODM record whose StudyOID equals ODM.StudyOID, SubjectKey equals ODM.SubjectKey, StudyEventOID equals ODM.StudyEventOID, ItemGroupOID equals ODM.ItemGroupOID and ItemGroupRepeatKey equals ODM.ItemGroupRepeatKey, where ODM.ItemOID = 'IT.LB.LBTESTCD', and a row with no match is an error. | SDTM | 5 | ||||
| SEX | Sex | Char | Derived | Take Value from the ODM record whose StudyOID equals ODM.StudyOID and SubjectKey equals ODM.SubjectKey, where ODM.ItemOID = 'IT.DM.SEX', and a row with no match is an error. | SDTM | 6 | ||||
| LBNAM | Lab Name | Char | Derived | Take Value from the ODM record whose StudyOID equals ODM.StudyOID, SubjectKey equals ODM.SubjectKey, StudyEventOID equals ODM.StudyEventOID, ItemGroupOID equals ODM.ItemGroupOID and ItemGroupRepeatKey equals ODM.ItemGroupRepeatKey, where ODM.ItemOID = 'IT.LB.LBNAM', and a row with no match is an error. | SDTM | 7 | ||||
| LBSTRESN | Numeric Result/Finding in Standard Units | Num | Derived | Take Value from the ODM record whose StudyOID equals ODM.StudyOID, SubjectKey equals ODM.SubjectKey, StudyEventOID equals ODM.StudyEventOID, ItemGroupOID equals ODM.ItemGroupOID and ItemGroupRepeatKey equals ODM.ItemGroupRepeatKey, where ODM.ItemOID = 'IT.LB.LBSTRESN', and a row with no match is an error. | SDTM | 8 | ||||
| LBORRESU | Original Units | Char | Derived | Take UNIT from the LBRANGE record matching on LBNAM, LBTESTCD and SEX, where AGE falls within AGELO to AGEHI, where LBRANGE.EFFSTDT <= ODM.Value AND (LBRANGE.EFFENDT IS NULL OR ODM.Value <= LBRANGE.EFFENDT), and no match yields blank. | SDTM | 9 | ||||
| LBSTNRLO | Reference Range Lower Limit-Std Units | Num | Derived | Take NRLO from the LBRANGE record matching on LBNAM, LBTESTCD and SEX, where AGE falls within AGELO to AGEHI, where LBRANGE.EFFSTDT <= ODM.Value AND (LBRANGE.EFFENDT IS NULL OR ODM.Value <= LBRANGE.EFFENDT), and no match yields blank. | SDTM | 10 | ||||
| LBSTNRHI | Reference Range Upper Limit-Std Units | Num | Derived | Take NRHI from the LBRANGE record matching on LBNAM, LBTESTCD and SEX, where AGE falls within AGELO to AGEHI, where LBRANGE.EFFSTDT <= ODM.Value AND (LBRANGE.EFFENDT IS NULL OR ODM.Value <= LBRANGE.EFFENDT), and no match yields blank. | SDTM | 11 | ||||
| LBNRIND | Reference Range Indicator | Char | Derived | If LBSTRESN IS NULL OR LBSTNRLO IS NULL OR LBSTNRHI IS NULL then "None"; If LBSTRESN < LBSTNRLO then "LOW"; If LBSTRESN > LBSTNRHI then "HIGH"; otherwise "NORMAL". | SDTM | 12 |
| Template | Include when |
|---|---|
| result | ODM.ItemOID = 'IT.LB.LBDTC' |
| Version | Date | Author | Description | Reviewer | Sign-off |
|---|---|---|---|---|---|
| 1.3 | 2026-09-29 | yamaa docs build | Show study codelist names, external dictionaries, and ISO 8601 formats in variable rows; list the document's codelist values and metadata | ||
| 1.2 | 2026-09-29 | yamaa docs build | Classify ADaM copies from a declared input as Predecessor and recodes as Derived, and SDTM recodes of locally derived columns as Derived | ||
| 1.1 | 2026-09-29 | yamaa docs build | Resolve lookups into the donor record they select, and give every derivation shape a plain-language rule instead of its raw YAML | ||
| 1.0 | yamaa docs build | Initial generation from spec.yaml |
prompts/sdtm-lb-lab-specific-ranges/full.md
Edit29 linesFollowing CDISC SDTM standards, use the provided ODM and LBRANGE datasets to create an LB dataset with one record per collected laboratory result.
The output dataset should contain the following columns in this order: DOMAIN, STUDYID, USUBJID, LBSEQ, LBTESTCD, SEX, LBNAM, LBSTRESN, LBORRESU, LBSTNRLO, LBSTNRHI, LBNRIND
STUDYID is the ODM StudyOID and USUBJID is the SubjectKey, each as given.
LBTESTCD is the collected test code, SEX the recorded sex, LBNAM the collecting lab, and LBSTRESN the numeric result in standard units, which has no value when the result was not collected. LBSEQ is the panel repeat key.
Each result is judged against the reference entry for its lab, test code, and sex whose age band holds the subject's age at collection and which was in effect on the collection date: a result collected on the first day of a new range uses the new limits, and an age on a band boundary takes the band that starts at that age. LBORRESU, LBSTNRLO, and LBSTNRHI are the unit and the lower and upper limits from that entry; all three have no value when no entry matches.
LBNRIND is LOW when the result is below the lower limit, HIGH when it is above the upper limit, and NORMAL otherwise; it has no value when the result or either limit is missing. A record whose result is missing still carries its unit and limits.
Read the source datasets from /app/input and save the completed dataset as /app/output/lb.csv.
prompts/sdtm-lb-lab-specific-ranges/conventions.md
Edit11 linesFollowing CDISC SDTM standards, use the provided ODM and LBRANGE datasets to create an LB dataset with one record per collected laboratory result.
The output dataset should contain the following columns in this order: DOMAIN, STUDYID, USUBJID, LBSEQ, LBTESTCD, SEX, LBNAM, LBSTRESN, LBORRESU, LBSTNRLO, LBSTNRHI, LBNRIND
LBNRIND is LOW, HIGH, or NORMAL, or has no value.
Read the source datasets from /app/input and save the completed dataset as /app/output/lb.csv.
prompts/sdtm-lb-lab-specific-ranges/brief.md
Edit9 linesFollowing CDISC SDTM standards, use the provided ODM and LBRANGE datasets to create an LB dataset with one record per collected laboratory result.
The output dataset should contain the following columns in this order: DOMAIN, STUDYID, USUBJID, LBSEQ, LBTESTCD, SEX, LBNAM, LBSTRESN, LBORRESU, LBSTNRLO, LBSTNRHI, LBNRIND
Read the source datasets from /app/input and save the completed dataset as /app/output/lb.csv.
run.R
Edit4 lineslibrary(yamaa)lb <- yamaa_domain("spec.yaml")$outputlbrun.py
Edit4 linesimport yamaalb = yamaa.yamaa_domain("spec.yaml").outputlbsolutions/sdtm-lb-lab-specific-ranges/result.R
Edit117 lines# Reference solution for the yamaa benchmark sdtm-lb-lab-specific-ranges (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(tidyr)library(purrr)library(lubridate)odm <- read_csv( "/app/input/odm.csv", col_types = cols(.default = col_character())) |> mutate(Value = na_if(Value, ""))lbrange <- read_csv( "/app/input/lbrange.csv", col_types = cols( AGELO = col_integer(), AGEHI = col_integer(), NRLO = col_double(), NRHI = col_double(), .default = col_character() ))age_years <- function(birth, collected) { b <- ymd(birth) c <- ymd(collected) years <- year(c) - year(b) before_birthday <- (month(c) < month(b)) | (month(c) == month(b) & day(c) < day(b)) years - if_else(before_birthday, 1L, 0L)}dm <- odm |> filter(ItemGroupOID == "IG.DM") |> select(SubjectKey, ItemOID, Value) |> pivot_wider(names_from = ItemOID, values_from = Value)labs <- odm |> filter(ItemGroupOID == "IG.LB") |> distinct(StudyOID, SubjectKey, ItemGroupRepeatKey)one_lab <- function(study, subj, igrep) { sub <- odm |> filter( SubjectKey == subj, ItemGroupOID == "IG.LB", ItemGroupRepeatKey == igrep ) get <- function(oid) { hit <- sub |> filter(ItemOID == oid) if (nrow(hit) == 0) return(NA_character_) hit$Value[[1]] } dsub <- dm |> filter(SubjectKey == subj) sex <- if (nrow(dsub) == 0) NA_character_ else dsub$`IT.DM.SEX`[[1]] birth <- if (nrow(dsub) == 0) NA_character_ else dsub$`IT.DM.BRTHDTC`[[1]] code <- get("IT.LB.LBTESTCD") lbnam <- get("IT.LB.LBNAM") lbdtc <- get("IT.LB.LBDTC") raw <- get("IT.LB.LBSTRESN") stresn <- suppressWarnings(as.numeric(raw)) unit <- NA_character_ lo <- NA_real_ hi <- NA_real_ if (!is.na(birth) && !is.na(lbdtc) && !is.na(code) && !is.na(lbnam) && !is.na(sex)) { age <- age_years(birth, lbdtc) hit <- lbrange |> filter( LBNAM == lbnam, LBTESTCD == code, SEX == sex, AGELO <= age, age < AGEHI, EFFSTDT <= lbdtc, is.na(EFFENDT) | lbdtc <= EFFENDT ) |> arrange(desc(EFFSTDT), desc(AGELO)) |> slice_head(n = 1) if (nrow(hit) == 1) { unit <- hit$UNIT[[1]] lo <- hit$NRLO[[1]] hi <- hit$NRHI[[1]] } } nrind <- if (is.na(stresn) || is.na(lo) || is.na(hi)) { NA_character_ } else if (stresn < lo) { "LOW" } else if (stresn > hi) { "HIGH" } else { "NORMAL" } tibble( DOMAIN = "LB", STUDYID = study, USUBJID = subj, LBSEQ = as.integer(igrep), LBTESTCD = code, SEX = sex, LBNAM = lbnam, LBSTRESN = stresn, LBORRESU = unit, LBSTNRLO = lo, LBSTNRHI = hi, LBNRIND = nrind )}lb <- pmap_dfr( list(labs$StudyOID, labs$SubjectKey, labs$ItemGroupRepeatKey), one_lab) |> arrange(STUDYID, USUBJID, LBSEQ)dir.create("/app/output", showWarnings = FALSE)write_csv(lb, "/app/output/lb.csv", na = "")solutions/sdtm-lb-lab-specific-ranges/result.py
Edit123 lines# Reference solution for the yamaa benchmark sdtm-lb-lab-specific-ranges (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 datefrom pathlib import Pathimport polars as plodm = pl.read_csv("/app/input/odm.csv", infer_schema=False)lbrange = pl.read_csv("/app/input/lbrange.csv", infer_schema=False).with_columns( pl.col("AGELO").cast(pl.Int64), pl.col("AGEHI").cast(pl.Int64), pl.col("NRLO").cast(pl.Float64, strict=False), pl.col("NRHI").cast(pl.Float64, strict=False),)subjects = {}labs = {}study_of = {}for r in odm.to_dicts(): study_of[r["SubjectKey"]] = r["StudyOID"] if r["ItemGroupOID"] == "IG.DM": subjects.setdefault(r["SubjectKey"], {})[r["ItemOID"]] = ( r["Value"] if r["Value"] not in (None, "") else None ) elif r["ItemGroupOID"] == "IG.LB": key = (r["SubjectKey"], r["ItemGroupRepeatKey"]) labs.setdefault(key, {})[r["ItemOID"]] = ( r["Value"] if r["Value"] not in (None, "") else None )def age_years(birth, collected): b = date.fromisoformat(birth) c = date.fromisoformat(collected) years = c.year - b.year if (c.month, c.day) < (b.month, b.day): years -= 1 return yearsranges = sorted(lbrange.to_dicts(), key=lambda r: (r["EFFSTDT"], r["AGELO"]), reverse=True)rows = []for (subj, repkey), lab in labs.items(): dm = subjects.get(subj, {}) sex = dm.get("IT.DM.SEX") birth = dm.get("IT.DM.BRTHDTC") code = lab.get("IT.LB.LBTESTCD") lbnam = lab.get("IT.LB.LBNAM") lbdtc = lab.get("IT.LB.LBDTC") raw = lab.get("IT.LB.LBSTRESN") try: stresn = float(raw) if raw not in (None, "") else None except ValueError: stresn = None unit = lo = hi = None if birth and lbdtc and code and lbnam and sex: age = age_years(birth, lbdtc) best = None for rg in ranges: if rg["LBNAM"] != lbnam or rg["LBTESTCD"] != code or rg["SEX"] != sex: continue if not (rg["AGELO"] <= age < rg["AGEHI"]): continue eff_start = rg["EFFSTDT"] eff_end = rg["EFFENDT"] if rg["EFFENDT"] not in (None, "") else None if lbdtc < eff_start: continue if eff_end is not None and lbdtc > eff_end: continue best = rg break if best is not None: unit, lo, hi = best["UNIT"], best["NRLO"], best["NRHI"] if stresn is None or lo is None or hi is None: nrind = None elif stresn < lo: nrind = "LOW" elif stresn > hi: nrind = "HIGH" else: nrind = "NORMAL" rows.append( { "DOMAIN": "LB", "STUDYID": study_of[subj], "USUBJID": subj, "LBSEQ": int(repkey), "LBTESTCD": code, "SEX": sex, "LBNAM": lbnam, "LBSTRESN": stresn, "LBORRESU": unit, "LBSTNRLO": lo, "LBSTNRHI": hi, "LBNRIND": nrind, } )lb = pl.DataFrame( rows, schema={ "DOMAIN": pl.String, "STUDYID": pl.String, "USUBJID": pl.String, "LBSEQ": pl.Int64, "LBTESTCD": pl.String, "SEX": pl.String, "LBNAM": pl.String, "LBSTRESN": pl.Float64, "LBORRESU": pl.String, "LBSTNRLO": pl.Float64, "LBSTNRHI": pl.Float64, "LBNRIND": pl.String, },).sort(["STUDYID", "USUBJID", "LBSEQ"])Path("/app/output").mkdir(parents=True, exist_ok=True)lb.write_csv("/app/output/lb.csv")solutions/sdtm-lb-lab-specific-ranges/result.sas
Edit57 lines/* opensas reference solution for the yamaa benchmark sdtm-lb-lab-specific-ranges. Reads the benchmark inputs and derives the requested output. Execute with the existing opensas runtime: opensas /app/output/result.sas. */data lbrange; length LBNAM LBTESTCD SEX EFFSTDT EFFENDT UNIT $1024; infile "/app/input/lbrange.csv" dsd dlm="," firstobs=2 truncover lrecl=32767; input LBNAM : $char1024. LBTESTCD : $char1024. SEX : $char1024. AGELO AGEHI EFFSTDT : $char1024. EFFENDT : $char1024. UNIT : $char1024. NRLO NRHI; RANGE_ORDER=_n_;run;data odm; length StudyOID MetaDataVersionOID SubjectKey StudyEventOID StudyEventRepeatKey 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. ItemGroupOID : $char1024. ItemGroupRepeatKey : $char1024. ItemOID : $char1024. Value : $char1024.;run;proc sort data=odm out=ordered; by StudyOID SubjectKey; run;data subjects; length SEX BRTHDTC $1024; retain SEX BRTHDTC; set ordered; where ItemGroupOID='IG.DM'; by StudyOID SubjectKey; if first.SubjectKey then do; SEX=''; BRTHDTC=''; end; if ItemOID='IT.DM.SEX' then SEX=Value; if ItemOID='IT.DM.BRTHDTC' then BRTHDTC=Value; if last.SubjectKey then output;run;proc sort data=odm out=ordered; by StudyOID SubjectKey ItemGroupRepeatKey; run;data panels; length LBDTC LBNAM LBTESTCD RESULTTEXT $1024; retain LBDTC LBNAM LBTESTCD RESULTTEXT; set ordered; where ItemGroupOID='IG.LB'; by StudyOID SubjectKey ItemGroupRepeatKey; if first.ItemGroupRepeatKey then do; LBDTC=''; LBNAM=''; LBTESTCD=''; RESULTTEXT=''; end; if ItemOID='IT.LB.LBDTC' then LBDTC=Value; if ItemOID='IT.LB.LBNAM' then LBNAM=Value; if ItemOID='IT.LB.LBTESTCD' then LBTESTCD=Value; if ItemOID='IT.LB.LBSTRESN' then RESULTTEXT=Value; if last.ItemGroupRepeatKey then output;run;proc sql; create table withbirth as select a.*,b.SEX,b.BRTHDTC from panels a left join subjects b on a.StudyOID=b.StudyOID and a.SubjectKey=b.SubjectKey; quit;data collected; set withbirth; AGE=.; if lengthn(BRTHDTC)=10 and lengthn(LBDTC)=10 then AGE=intck('year',input(BRTHDTC,yymmdd10.),input(LBDTC,yymmdd10.),'continuous'); LBSTRESN=input(RESULTTEXT,?? best32.); LBSEQ=input(ItemGroupRepeatKey,best32.); run;proc sql; create table joined as select a.*,b.UNIT as LBORRESU,b.NRLO as LBSTNRLO,b.NRHI as LBSTNRHI,b.EFFSTDT as RANGE_START,b.AGELO as RANGE_AGE,b.RANGE_ORDER from collected a left join lbrange b on a.LBNAM=b.LBNAM and a.LBTESTCD=b.LBTESTCD and a.SEX=b.SEX and a.AGE>=b.AGELO and a.AGE<b.AGEHI and a.LBDTC>=b.EFFSTDT and (missing(b.EFFENDT) or a.LBDTC<=b.EFFENDT); quit;proc sort data=joined; by StudyOID SubjectKey LBSEQ descending RANGE_START descending RANGE_AGE RANGE_ORDER; run;data selected; set joined; by StudyOID SubjectKey LBSEQ; if first.LBSEQ; run;data result; set selected; length DOMAIN $2 STUDYID USUBJID $1024 LBNRIND $8; DOMAIN='LB'; STUDYID=StudyOID; USUBJID=SubjectKey; LBNRIND=''; if not missing(LBSTRESN) and not missing(LBSTNRLO) and not missing(LBSTNRHI) then do; if LBSTRESN<LBSTNRLO then LBNRIND='LOW'; else if LBSTRESN>LBSTNRHI then LBNRIND='HIGH'; else LBNRIND='NORMAL'; end; run;proc sql; create table final as select DOMAIN, STUDYID, USUBJID, LBSEQ, LBTESTCD, SEX, LBNAM, LBSTRESN, LBORRESU, LBSTNRLO, LBSTNRHI, LBNRIND from result;quit;proc export data=final outfile="/app/output/lb.csv" dbms=csv replace; run;
yamaa
Comments
Signed in with GitHub; saved in repository Discussions