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: build one RS record per subject and scheduled tumor assessment holding the visit order (AVISITN), the assessment date (ADT), a per-subject record number (RSSEQ), the response test (RSTESTCD/RSTEST), the response (RSSTRESC), and the completion status (RSSTAT).

Input: long-form Operational Data Model (ODM) item data. Each scheduled tumor assessment (IG.VISIT) carries its visit name, visit order, and date. Each lesion measured at an assessment is a repeated item group, IG.TRTARGET for a target lesion and IG.TRNT for a non-target one, carrying the lesion link and the longest diameter. The lesions chosen at study entry are listed in IG.TUTARGET and IG.TUNT.

Variables:

  • AVISITN is the numeric order of the assessment, carried from the scheduled assessment; it orders the assessments of a subject.
  • ADT is the date of the assessment, carried from the scheduled assessment.
  • RSSEQ numbers a subject's response records in visit order, starting at 1.
  • RSTESTCD is TRGRESP and RSTEST is Timepoint Response on every record.
  • RSSTRESC is the response at the assessment: NE (not evaluable) at baseline, when fewer target lesions were measured than were chosen, when the subject has non-target disease only, or when no percent change can be computed (for example a zero or missing baseline sum); CR (complete response) when every target lesion has disappeared; PR (partial response) when the target sum shrank at least 30% from baseline; PD (progressive disease) when it grew at least 20% from baseline; SD (stable disease) otherwise. It stays blank when the assessment was not done.
  • RSSTAT is NOT DONE for a scheduled assessment with no tumor measurement records at all; blank otherwise.

Note: a baseline assessment is never compared against itself, so its response is NE whenever it was done. An assessment with some but not all chosen target lesions measured is still done, and its response is NE rather than a guess from partial data.

Inputs

Under review
1 source file

input/odm.csv

Edit
66 rows
input/odm.csv
StudyOIDMetaDataVersionOIDSubjectKeyStudyEventOIDStudyEventRepeatKeyItemGroupOIDItemGroupRepeatKeyItemOIDValue
PILOT7MDV.1P7-921BASELINE1IG.VISIT1IT.VISIT.AVISITBASELINE
PILOT7MDV.1P7-921BASELINE1IG.VISIT1IT.VISIT.AVISITN0
PILOT7MDV.1P7-921BASELINE1IG.VISIT1IT.VISIT.ADT2025-01-05
PILOT7MDV.1P7-921WEEK81IG.VISIT1IT.VISIT.AVISITWEEK 8
PILOT7MDV.1P7-921WEEK81IG.VISIT1IT.VISIT.AVISITN8
PILOT7MDV.1P7-921WEEK81IG.VISIT1IT.VISIT.ADT2025-03-02
PILOT7MDV.1P7-921BASELINE1IG.TRTARGET1IT.TR.TRLNKIDL1
PILOT7MDV.1P7-921BASELINE1IG.TRTARGET1IT.TR.LDIAM40
PILOT7MDV.1P7-921BASELINE1IG.TRTARGET1IT.TR.TRSTAT—
PILOT7MDV.1P7-921BASELINE1IG.TRTARGET2IT.TR.TRLNKIDL2
PILOT7MDV.1P7-921BASELINE1IG.TRTARGET2IT.TR.LDIAM25
PILOT7MDV.1P7-921BASELINE1IG.TRTARGET2IT.TR.TRSTAT—
PILOT7MDV.1P7-921BASELINE1IG.TRTARGET3IT.TR.TRLNKIDL3
PILOT7MDV.1P7-921BASELINE1IG.TRTARGET3IT.TR.LDIAM15
PILOT7MDV.1P7-921BASELINE1IG.TRTARGET3IT.TR.TRSTAT—
PILOT7MDV.1P7-921WEEK81IG.TRTARGET1IT.TR.TRLNKIDL1
PILOT7MDV.1P7-921WEEK81IG.TRTARGET1IT.TR.LDIAM30
PILOT7MDV.1P7-921WEEK81IG.TRTARGET1IT.TR.TRSTAT—
PILOT7MDV.1P7-921WEEK81IG.TRTARGET2IT.TR.TRLNKIDL2
PILOT7MDV.1P7-921WEEK81IG.TRTARGET2IT.TR.LDIAM20
PILOT7MDV.1P7-921WEEK81IG.TRTARGET2IT.TR.TRSTAT—
PILOT7MDV.1P7-921WEEK81IG.TRTARGET3IT.TR.TRLNKIDL3
PILOT7MDV.1P7-921WEEK81IG.TRTARGET3IT.TR.LDIAM—
PILOT7MDV.1P7-921WEEK81IG.TRTARGET3IT.TR.TRSTATNOT DONE
PILOT7MDV.1P7-921BASELINE1IG.TUTARGET1IT.TU.TULNKIDL1
PILOT7MDV.1P7-921BASELINE1IG.TUTARGET2IT.TU.TULNKIDL2
PILOT7MDV.1P7-921BASELINE1IG.TUTARGET3IT.TU.TULNKIDL3
PILOT7MDV.1P7-922BASELINE1IG.VISIT1IT.VISIT.AVISITBASELINE
PILOT7MDV.1P7-922BASELINE1IG.VISIT1IT.VISIT.AVISITN0
PILOT7MDV.1P7-922BASELINE1IG.VISIT1IT.VISIT.ADT2025-01-12
PILOT7MDV.1P7-922WEEK81IG.VISIT1IT.VISIT.AVISITWEEK 8
PILOT7MDV.1P7-922WEEK81IG.VISIT1IT.VISIT.AVISITN8
PILOT7MDV.1P7-922WEEK81IG.VISIT1IT.VISIT.ADT2025-03-09
PILOT7MDV.1P7-922BASELINE1IG.TRTARGET1IT.TR.TRLNKIDL1
PILOT7MDV.1P7-922BASELINE1IG.TRTARGET1IT.TR.LDIAM22
PILOT7MDV.1P7-922BASELINE1IG.TRTARGET1IT.TR.TRSTAT—
PILOT7MDV.1P7-922BASELINE1IG.TRTARGET2IT.TR.TRLNKIDL2
PILOT7MDV.1P7-922BASELINE1IG.TRTARGET2IT.TR.LDIAM18
PILOT7MDV.1P7-922BASELINE1IG.TRTARGET2IT.TR.TRSTAT—
PILOT7MDV.1P7-922WEEK81IG.TRTARGET1IT.TR.TRLNKIDL1
PILOT7MDV.1P7-922WEEK81IG.TRTARGET1IT.TR.LDIAM12
PILOT7MDV.1P7-922WEEK81IG.TRTARGET1IT.TR.TRSTAT—
PILOT7MDV.1P7-922WEEK81IG.TRTARGET2IT.TR.TRLNKIDL2
PILOT7MDV.1P7-922WEEK81IG.TRTARGET2IT.TR.LDIAM10
PILOT7MDV.1P7-922WEEK81IG.TRTARGET2IT.TR.TRSTAT—
PILOT7MDV.1P7-922BASELINE1IG.TUTARGET1IT.TU.TULNKIDL1
PILOT7MDV.1P7-922BASELINE1IG.TUTARGET2IT.TU.TULNKIDL2
PILOT7MDV.1P7-923BASELINE1IG.VISIT1IT.VISIT.AVISITBASELINE
PILOT7MDV.1P7-923BASELINE1IG.VISIT1IT.VISIT.AVISITN0
PILOT7MDV.1P7-923BASELINE1IG.VISIT1IT.VISIT.ADT2025-01-19
PILOT7MDV.1P7-923WEEK81IG.VISIT1IT.VISIT.AVISITWEEK 8
PILOT7MDV.1P7-923WEEK81IG.VISIT1IT.VISIT.AVISITN8
PILOT7MDV.1P7-923WEEK81IG.VISIT1IT.VISIT.ADT2025-03-16
PILOT7MDV.1P7-923BASELINE1IG.TRNT1IT.TR.TRLNKIDL1
PILOT7MDV.1P7-923BASELINE1IG.TRNT1IT.TR.LDIAM20
PILOT7MDV.1P7-923BASELINE1IG.TRNT1IT.TR.TRSTAT—
PILOT7MDV.1P7-923WEEK81IG.TRNT1IT.TR.TRLNKIDL1
PILOT7MDV.1P7-923WEEK81IG.TRNT1IT.TR.LDIAM22
PILOT7MDV.1P7-923WEEK81IG.TRNT1IT.TR.TRSTAT—
PILOT7MDV.1P7-923BASELINE1IG.TUNT1IT.TU.TULNKIDL1
PILOT7MDV.1P7-924BASELINE1IG.VISIT1IT.VISIT.AVISITBASELINE
PILOT7MDV.1P7-924BASELINE1IG.VISIT1IT.VISIT.AVISITN0
PILOT7MDV.1P7-924BASELINE1IG.VISIT1IT.VISIT.ADT2025-02-02
PILOT7MDV.1P7-924WEEK81IG.VISIT1IT.VISIT.AVISITWEEK 8
PILOT7MDV.1P7-924WEEK81IG.VISIT1IT.VISIT.AVISITN8
PILOT7MDV.1P7-924WEEK81IG.VISIT1IT.VISIT.ADT2025-03-30

Expected output

Under review
8 expected rows

expected/rs.csv

Edit
8 rows
expected/rs.csv
STUDYIDUSUBJIDAVISITAVISITNADTRSSEQRSTESTCDRSTESTRSSTRESCRSSTAT
PILOT7BASELINE02025-01-051TRGRESPTimepoint ResponseNE—
PILOT7WEEK 882025-03-022TRGRESPTimepoint ResponseNE—
PILOT7BASELINE02025-01-121TRGRESPTimepoint ResponseNE—
PILOT7WEEK 882025-03-092TRGRESPTimepoint ResponsePR—
PILOT7BASELINE02025-01-191TRGRESPTimepoint ResponseNE—
PILOT7WEEK 882025-03-162TRGRESPTimepoint ResponseNE—
PILOT7BASELINE02025-02-021TRGRESPTimepoint Response—NOT DONE
PILOT7WEEK 882025-03-302TRGRESPTimepoint Response—NOT DONE
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
STUDYIDStudy IdentifierCharCollectedCopy value from ODM.StudyOID.SDTM1
USUBJIDUnique Subject IdentifierCharCollectedCopy value from ODM.SubjectKey.SDTM2
AVISITAnalysis VisitCharDerivedSet per constructed row; see the Row construction sheet.SDTM3
AVISITNAnalysis Visit (N)NumDerivedSet per constructed row; see the Row construction sheet.SDTM4
ADTAnalysis DateCharISO 8601DerivedSet per constructed row; see the Row construction sheet.SDTM5
RSSEQSequence NumberNumDerivedRow number within each group of STUDYID, USUBJID, ordered by AVISITN.SDTM6
RSTESTCDResponse Test Short NameCharAssignedSet constant value "TRGRESP".SDTM7
RSTESTResponse Test NameCharAssignedSet constant value "Timepoint Response".SDTM8
RSSTATCompletion StatusCharDerivedIf NTR IS NULL OR NTR = 0 then "NOT DONE"; otherwise blank.SDTM9
RSSTRESCResult or Finding in Standard FormatCharCR, PR, SD, PD, NEDerivedIf RSSTAT = 'NOT DONE' then "None"; If AVISIT = 'BASELINE' then "NE"; If NTARGET IS NULL OR NTARGET = 0 then "NE"; If NMEAS IS NULL OR NMEAS < NTARGET then "NE"; If PCHG IS NULL then "NE"; If SUMV = 0 then "CR"; If PCHG <= -30 then "PR"; If PCHG >= 20 then "PD"; otherwise "SD".SDTM10
TemplateInclude whenAVISITAVISITNADT
visitAVISITN IS NOT NULLAggregate ONLY(ODM.Value) over records matching on the shared output keys, where ODM.ItemOID = 'IT.VISIT.AVISIT'Aggregate ONLY(ODM.Value) over records matching on the shared output keys, where ODM.ItemOID = 'IT.VISIT.AVISITN'Aggregate ONLY(ODM.Value) over records matching on the shared output keys, where ODM.ItemOID = 'IT.VISIT.ADT'
VariablePermitted values
RSSTRESCCR, PR, SD, PD, NE
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-rs-timepoint-response/full.md

Edit
28 lines

Following CDISC SDTM standards, use the provided ODM dataset to create an RS dataset with one record per subject and scheduled tumor assessment.

The output dataset should contain the following columns in this order: STUDYID, USUBJID, AVISIT, AVISITN, ADT, RSSEQ, RSTESTCD, RSTEST, RSSTRESC, RSSTAT

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

AVISITN is the numeric order of the assessment, ADT its date, and RSSEQ numbers a subject's response records in visit order, starting at 1. RSTESTCD is TRGRESP and RSTEST is Timepoint Response on every record.

RSSTRESC is NE at baseline, when fewer target lesions were measured than were chosen, when the subject has non-target disease only, or when no percent change can be computed (for example a zero or missing baseline sum); CR when every target lesion has disappeared; PR when the target sum shrank at least 30% from baseline; PD when it grew at least 20% from baseline; and SD otherwise. It has no value when the assessment was not done. RSSTAT is NOT DONE for a scheduled assessment with no tumor measurement records at all, and has no value otherwise.

A baseline assessment is never compared against itself, so its response is NE whenever it was done. An assessment with some but not all chosen target lesions measured is still done, and its response is NE.

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

prompts/sdtm-rs-timepoint-response/conventions.md

Edit
14 lines

Following CDISC SDTM standards, use the provided ODM dataset to create an RS dataset with one record per subject and scheduled tumor assessment.

The output dataset should contain the following columns in this order: STUDYID, USUBJID, AVISIT, AVISITN, ADT, RSSEQ, RSTESTCD, RSTEST, RSSTRESC, RSSTAT

RSTESTCD is TRGRESP and RSTEST is Timepoint Response on every record.

RSSTRESC is NE, CR, PR, PD, or SD, or has no value. RSSTAT is NOT DONE or has no value.

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

prompts/sdtm-rs-timepoint-response/brief.md

Edit
9 lines

Following CDISC SDTM standards, use the provided ODM dataset to create an RS dataset with one record per subject and scheduled tumor assessment.

The output dataset should contain the following columns in this order: STUDYID, USUBJID, AVISIT, AVISITN, ADT, RSSEQ, RSTESTCD, RSTEST, RSSTRESC, RSSTAT

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

yamaa code

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

run.R

Edit
4 lines
library(yamaa)rs <- yamaa_domain("spec.yaml")$outputrs

run.py

Edit
4 lines
import yamaars = yamaa.yamaa_domain("spec.yaml").outputrs

Solution without yamaa

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

solutions/sdtm-rs-timepoint-response/result.R

Edit
136 lines
# Reference solution for the yamaa benchmark sdtm-rs-timepoint-response (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)odm <- read_csv(  "/app/input/odm.csv",  col_types = cols(.default = col_character()))visits <- odm |>  filter(ItemGroupOID == "IG.VISIT") |>  pivot_wider(    id_cols = c(StudyOID, SubjectKey, StudyEventOID),    names_from = ItemOID,    values_from = Value,    values_fn = function(x) x[[1]]  )tr_target <- odm |>  filter(grepl("TARGET", ItemGroupOID, fixed = TRUE),    startsWith(ItemGroupOID, "IG.TR")) |>  pivot_wider(    id_cols = c(StudyOID, SubjectKey, StudyEventOID, ItemGroupRepeatKey),    names_from = ItemOID,    values_from = Value,    values_fn = function(x) x[[1]]  )tr_any <- odm |>  filter(startsWith(ItemGroupOID, "IG.TR")) |>  count(SubjectKey, StudyEventOID, name = "n_tr")tu_target <- odm |>  filter(grepl("TARGET", ItemGroupOID, fixed = TRUE),    startsWith(ItemGroupOID, "IG.TU"),    StudyEventOID == "BASELINE",    ItemOID == "IT.TU.TULNKID",    !is.na(Value) & Value != "") |>  distinct(SubjectKey, Value)response_one <- function(subj, event, avisitn, base_sum, n_chosen, post_diams, has_records) {  if (!has_records) {    return(list(rsstresc = NA_character_, rsstat = "NOT DONE"))  }  if (event == "BASELINE") {    return(list(rsstresc = "NE", rsstat = NA_character_))  }  if (n_chosen == 0) {    return(list(rsstresc = "NE", rsstat = NA_character_))  }  measured <- post_diams[!is.na(post_diams) & post_diams != ""]  if (length(measured) < n_chosen) {    return(list(rsstresc = "NE", rsstat = NA_character_))  }  if (is.na(base_sum) || base_sum == 0) {    return(list(rsstresc = "NE", rsstat = NA_character_))  }  post <- sum(suppressWarnings(as.numeric(measured)))  if (all(suppressWarnings(as.numeric(measured)) == 0)) {    return(list(rsstresc = "CR", rsstat = NA_character_))  }  if ((base_sum - post) / base_sum >= 0.30) {    return(list(rsstresc = "PR", rsstat = NA_character_))  }  if ((post - base_sum) / base_sum >= 0.20) {    return(list(rsstresc = "PD", rsstat = NA_character_))  }  list(rsstresc = "SD", rsstat = NA_character_)}subjects <- unique(visits$SubjectKey)out <- list()for (subj in subjects) {  chosen <- nrow(tu_target |> filter(SubjectKey == subj))  if (chosen == 0) {    chosen <- nrow(tr_target |>      filter(SubjectKey == subj, StudyEventOID == "BASELINE"))  }  base_rows <- tr_target |>    filter(SubjectKey == subj, StudyEventOID == "BASELINE")  base_diams <- base_rows$IT.TR.LDIAM  base_sum <- if (chosen == 0) {    NA_real_  } else if (any(is.na(base_diams) | base_diams == "")) {    NA_real_  } else {    sum(suppressWarnings(as.numeric(base_diams)))  }  subj_visits <- visits |>    filter(SubjectKey == subj) |>    arrange(suppressWarnings(as.integer(IT.VISIT.AVISITN)))  seq <- 0L  for (i in seq_len(nrow(subj_visits))) {    seq <- seq + 1L    event <- subj_visits$StudyEventOID[[i]]    n_rec <- tr_any |>      filter(SubjectKey == subj, StudyEventOID == event) |>      pull(n_tr)    has_records <- length(n_rec) > 0 && n_rec[[1]] > 0    post_diams <- tr_target |>      filter(SubjectKey == subj, StudyEventOID == event) |>      pull(IT.TR.LDIAM)    resp <- response_one(      subj, event, subj_visits$IT.VISIT.AVISITN[[i]], base_sum, chosen,      post_diams, has_records    )    out[[length(out) + 1]] <- tibble(      STUDYID = subj_visits$StudyOID[[i]],      USUBJID = subj,      AVISIT = subj_visits$IT.VISIT.AVISIT[[i]],      AVISITN = subj_visits$IT.VISIT.AVISITN[[i]],      ADT = subj_visits$IT.VISIT.ADT[[i]],      RSSEQ = seq,      RSTESTCD = "TRGRESP",      RSTEST = "Timepoint Response",      RSSTRESC = resp$rsstresc,      RSSTAT = resp$rsstat    )  }}rs <- bind_rows(out) |>  arrange(USUBJID, suppressWarnings(as.integer(AVISITN))) |>  select(    STUDYID, USUBJID, AVISIT, AVISITN, ADT, RSSEQ, RSTESTCD, RSTEST,    RSSTRESC, RSSTAT  )dir.create("/app/output", showWarnings = FALSE)write_csv(rs, "/app/output/rs.csv", na = "")

solutions/sdtm-rs-timepoint-response/result.py

Edit
182 lines
# Reference solution for the yamaa benchmark sdtm-rs-timepoint-response (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 pldef _present(value) -> bool:    return value is not None and str(value).strip() != ""odm = pl.read_csv("/app/input/odm.csv", infer_schema=False)visits: dict[tuple, dict] = {}tr_target: dict[tuple, list] = {}tr_any: dict[tuple, int] = {}tu_target_ids: dict[str, set] = {}baseline_target_diams: dict[str, list] = {}for row in odm.to_dicts():    subj, event = str(row["SubjectKey"]), str(row["StudyEventOID"])    group, oid = str(row["ItemGroupOID"]), str(row["ItemOID"])    value = row["Value"]    key = (subj, event)    if group == "IG.VISIT":        visits.setdefault(            key, {"studyid": str(row["StudyOID"]), "usubjid": subj}        )[oid] = str(value) if _present(value) else None    elif "TARGET" in group and group.startswith("IG.TR"):        # Target tumor measurements at this visit.        rep = str(row["ItemGroupRepeatKey"])        tr_target.setdefault((subj, event, rep), {})[oid] = (            str(value) if _present(value) else None        )        tr_any[key] = tr_any.get(key, 0) + 0    elif group.startswith("IG.TR"):        # Non-target measurements still count as done.        tr_any[key] = tr_any.get(key, 0) + 1    elif (        "TARGET" in group        and group.startswith("IG.TU")        and event == "BASELINE"        and oid == "IT.TU.TULNKID"        and _present(value)    ):        tu_target_ids.setdefault(subj, set()).add(str(value).strip())# Any TR row (target or not) means the assessment has measurement records.for (subj, event, _rep) in list(tr_target.keys()):    tr_any[(subj, event)] = tr_any.get((subj, event), 0) + 1# Chosen target lesions from the baseline tumor identification, else the# baseline target measurements; none means non-target disease only.chosen: dict[str, int] = {}for subj in {s for s, _e in visits}:    if subj in tu_target_ids:        chosen[subj] = len(tu_target_ids[subj])    else:        baseline_reps = {rep for (s, e, rep) in tr_target if s == subj and e == "BASELINE"}        chosen[subj] = len(baseline_reps)baseline_sum: dict[str, float | None] = {}for subj in {s for s, _e in visits}:    diams = []    missing = False    for (s, e, _rep), vals in tr_target.items():        if s != subj or e != "BASELINE":            continue        diam = vals.get("IT.TR.LDIAM")        if not _present(diam):            missing = True        else:            try:                diams.append(float(str(diam)))            except ValueError:                missing = True    if missing or not diams:        # No percent change can be computed from a missing baseline sum.        # An empty baseline (non-target only) is handled as NE elsewhere.        baseline_sum[subj] = None if (missing or not diams) and chosen.get(subj, 0) else 0.0        if chosen.get(subj, 0) == 0:            baseline_sum[subj] = None        elif not missing and diams:            baseline_sum[subj] = sum(diams)    else:        baseline_sum[subj] = sum(diams)rows = []by_subject: dict[str, list] = {}for (subj, event), visit in visits.items():    by_subject.setdefault(subj, []).append((event, visit))for subj, items in by_subject.items():    items.sort(key=lambda kv: int(kv[1].get("IT.VISIT.AVISITN") or 0))    for seq, (event, visit) in enumerate(items, start=1):        avisit = visit.get("IT.VISIT.AVISIT")        avisitn = visit.get("IT.VISIT.AVISITN")        adt = visit.get("IT.VISIT.ADT")        has_records = tr_any.get((subj, event), 0) > 0        if not has_records:            # A scheduled assessment with no tumor measurement records.            rsstresc, rsstat = None, "NOT DONE"        elif event == "BASELINE":            # A baseline assessment is never compared against itself.            rsstresc, rsstat = "NE", None        elif chosen.get(subj, 0) == 0:            rsstresc, rsstat = "NE", None        else:            post_vals = [                vals.get("IT.TR.LDIAM")                for (s, e, _r), vals in tr_target.items()                if s == subj and e == event            ]            measured = [v for v in post_vals if _present(v)]            if len(measured) < chosen[subj]:                rsstresc, rsstat = "NE", None            else:                base = baseline_sum.get(subj)                if base is None or base == 0:                    rsstresc, rsstat = "NE", None                else:                    try:                        post = sum(float(str(v)) for v in measured)                    except ValueError:                        rsstresc, rsstat = "NE", None                    else:                        if all(float(str(v)) == 0 for v in measured):                            rsstresc = "CR"                        elif (base - post) / base >= 0.30:                            rsstresc = "PR"                        elif (post - base) / base >= 0.20:                            rsstresc = "PD"                        else:                            rsstresc = "SD"                        rsstat = None        rows.append(            {                "STUDYID": visit["studyid"],                "USUBJID": visit["usubjid"],                "AVISIT": avisit,                "AVISITN": avisitn,                "ADT": adt,                "RSSEQ": str(seq),                "RSTESTCD": "TRGRESP",                "RSTEST": "Timepoint Response",                "RSSTRESC": rsstresc,                "RSSTAT": rsstat,            }        )rs = (    pl.DataFrame(        rows,        schema={            "STUDYID": pl.String,            "USUBJID": pl.String,            "AVISIT": pl.String,            "AVISITN": pl.String,            "ADT": pl.String,            "RSSEQ": pl.String,            "RSTESTCD": pl.String,            "RSTEST": pl.String,            "RSSTRESC": pl.String,            "RSSTAT": pl.String,        },    )    .with_columns(        pl.col("RSSEQ").cast(pl.Int64).alias("_seq"),        pl.col("AVISITN").cast(pl.Int64).alias("_visit"),    )    .sort(["USUBJID", "_visit"])    .select(        "STUDYID", "USUBJID", "AVISIT", "AVISITN", "ADT", "RSSEQ",        "RSTESTCD", "RSTEST", "RSSTRESC", "RSSTAT",    ))Path("/app/output").mkdir(exist_ok=True)rs.write_csv("/app/output/rs.csv")

solutions/sdtm-rs-timepoint-response/result.sas

Edit
59 lines
/* opensas reference solution for the yamaa benchmark sdtm-rs-timepoint-response.   Reads the benchmark inputs and derives the requested output.   Execute with the existing opensas runtime: opensas /app/output/result.sas. */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 StudyEventOID; run;data visits;  length AVISIT VISITORDER ADT $1024;  retain AVISIT VISITORDER ADT;  set ordered;  where ItemGroupOID='IG.VISIT';  by StudyOID SubjectKey StudyEventOID;  if first.StudyEventOID then do;    AVISIT=''; VISITORDER=''; ADT='';  end;    if ItemOID='IT.VISIT.AVISIT' then AVISIT=Value;  if ItemOID='IT.VISIT.AVISITN' then VISITORDER=Value;  if ItemOID='IT.VISIT.ADT' then ADT=Value;  if last.StudyEventOID then output;run;proc sort data=odm out=ordered; by StudyOID SubjectKey StudyEventOID ItemGroupRepeatKey; run;data targetforms;  length DIAM $1024;  retain DIAM;  set ordered;  where ItemGroupOID='IG.TRTARGET';  by StudyOID SubjectKey StudyEventOID ItemGroupRepeatKey;  if first.ItemGroupRepeatKey then do;    DIAM='';  end;    if ItemOID='IT.TR.LDIAM' then DIAM=Value;  if last.ItemGroupRepeatKey then output;run;data targets; set targetforms; DIAMETER=input(DIAM,?? best32.); run;proc sql;create table chosen as select StudyOID,SubjectKey,count(distinct Value) as NCHOSEN from odm where ItemGroupOID='IG.TUTARGET' and StudyEventOID='BASELINE' and ItemOID='IT.TU.TULNKID' and not missing(Value) group by StudyOID,SubjectKey;create table baseline as select StudyOID,SubjectKey,count(*) as NBASE,count(DIAMETER) as NBASEDONE,sum(DIAMETER) as BASE from targets where StudyEventOID='BASELINE' group by StudyOID,SubjectKey;create table measures as select StudyOID,SubjectKey,StudyEventOID,count(DIAMETER) as NMEAS,sum(DIAMETER) as TOTAL,max(DIAMETER) as MAXDIAM from targets group by StudyOID,SubjectKey,StudyEventOID;create table done as select distinct StudyOID,SubjectKey,StudyEventOID from odm where ItemGroupOID in ('IG.TRTARGET','IG.TRNT');create table joined as select a.*,b.NCHOSEN,c.NBASE,c.NBASEDONE,c.BASE,d.NMEAS,d.TOTAL,d.MAXDIAM,e.StudyEventOID as DONEEVENT from visits a left join chosen b on a.StudyOID=b.StudyOID and a.SubjectKey=b.SubjectKey left join baseline c on a.StudyOID=c.StudyOID and a.SubjectKey=c.SubjectKey left join measures d on a.StudyOID=d.StudyOID and a.SubjectKey=d.SubjectKey and a.StudyEventOID=d.StudyEventOID left join done e on a.StudyOID=e.StudyOID and a.SubjectKey=e.SubjectKey and a.StudyEventOID=e.StudyEventOID; quit;data prepared; set joined; length STUDYID USUBJID RSTESTCD RSTEST RSSTRESC RSSTAT $1024; STUDYID=StudyOID; USUBJID=SubjectKey; AVISITN=input(VISITORDER,best32.); RSTESTCD='TRGRESP'; RSTEST='Timepoint Response'; RSSTRESC='NE'; RSSTAT=''; NSELECTED=coalesce(NCHOSEN,NBASE,0);if missing(DONEEVENT) then do; RSSTRESC=''; RSSTAT='NOT DONE'; end;else if StudyEventOID ne 'BASELINE' and NSELECTED>0 and NMEAS>=NSELECTED and NBASEDONE=NBASE and not missing(BASE) and BASE ne 0 then do;if MAXDIAM=0 then RSSTRESC='CR'; else if (BASE-TOTAL)/BASE>=0.30 then RSSTRESC='PR'; else if (TOTAL-BASE)/BASE>=0.20 then RSSTRESC='PD'; else RSSTRESC='SD'; end; run;proc sort data=prepared; by STUDYID USUBJID AVISITN; run;data result; set prepared; by STUDYID USUBJID; retain RSSEQ;  if first.USUBJID then RSSEQ=0;  RSSEQ+1;run;proc sql;  create table final as select STUDYID, USUBJID, AVISIT, AVISITN, ADT, RSSEQ, RSTESTCD, RSTEST, RSSTRESC, RSSTAT from result;quit;proc export data=final outfile="/app/output/rs.csv" dbms=csv replace; run;