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: 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 Target Response on every record: the response of the target lesions alone.
  • 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-051TRGRESPTarget ResponseNE—
PILOT7WEEK 882025-03-022TRGRESPTarget ResponseNE—
PILOT7BASELINE02025-01-121TRGRESPTarget ResponseNE—
PILOT7WEEK 882025-03-092TRGRESPTarget ResponsePR—
PILOT7BASELINE02025-01-191TRGRESPTarget ResponseNE—
PILOT7WEEK 882025-03-162TRGRESPTarget ResponseNE—
PILOT7BASELINE02025-02-021TRGRESPTarget Response—NOT DONE
PILOT7WEEK 882025-03-302TRGRESPTarget Response—NOT DONE
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

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 "Target 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.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/sdtm-rs-timepoint-response/full.md

Edit
29 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 study OID and USUBJID is the ODM subject key, each as given. AVISIT is the name of the scheduled assessment, AVISITN is its numeric order, and ADT is its date. RSSEQ numbers a subject's records in visit order, starting at 1. RSTESTCD is "TRGRESP" and RSTEST is "Target Response" on every record. RSSTAT is "NOT DONE" for a scheduled assessment with no tumor measurement records at all, and has no value otherwise. RSSTRESC has no value when the assessment was not done. A baseline assessment is never compared against itself, so its response is "NE" whenever it was done. RSSTRESC is "NE" when fewer target lesions were measured at the assessment than were chosen at study entry. An assessment at which some but not all of the chosen target lesions were measured is still done, and its response is "NE". RSSTRESC is "NE" when the subject has non-target disease only. RSSTRESC is "NE" when no percent change from baseline can be computed, for example when the baseline target sum is zero or missing. The target sum at an assessment is the sum of the longest diameters of the target lesions measured at that assessment. RSSTRESC is "CR" when every target lesion has disappeared. RSSTRESC is "PR" when the target sum shrank at least 30% from baseline. RSSTRESC is "PD" when the target sum grew at least 20% from baseline. RSSTRESC is "SD" otherwise.

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
13 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 "Target Response" on every record. RSSTRESC is "CR", "PR", "SD", or "NE", 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

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/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 = "Target 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
159 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.import csvimport osfrom collections import defaultdictINPUT = "/app/input"OUTPUT = "/app/output"COLUMNS = [    "STUDYID", "USUBJID", "AVISIT", "AVISITN", "ADT", "RSSEQ",    "RSTESTCD", "RSTEST", "RSSTRESC", "RSSTAT",]def load_items(path):    """Pivot the long ODM extract into per-group item dictionaries."""    groups = {}    order = []    with open(path, newline="") as handle:        for row in csv.DictReader(handle):            key = (                row["StudyOID"], row["SubjectKey"], row["StudyEventOID"],                row["StudyEventRepeatKey"], row["ItemGroupOID"],                row["ItemGroupRepeatKey"],            )            if key not in groups:                groups[key] = {                    "study": row["StudyOID"],                    "subject": row["SubjectKey"],                    "event": row["StudyEventOID"],                    "group": row["ItemGroupOID"],                    "items": {},                }                order.append(key)            groups[key]["items"][row["ItemOID"]] = (row["Value"] or "").strip()    return [groups[key] for key in order]def to_number(text):    if text in (None, ""):        return None    try:        return float(text)    except ValueError:        return Nonedef main():    records = load_items(os.path.join(INPUT, "odm.csv"))    visits = defaultdict(list)          # subject -> [visit dicts]    chosen_targets = defaultdict(set)   # subject -> {lesion ids chosen}    target_meas = defaultdict(dict)     # (subject, event) -> {lesion: ldiam}    any_meas = defaultdict(bool)       # (subject, event) -> any tumor record    for rec in records:        subject = rec["subject"]        items = rec["items"]        if rec["group"] == "IG.VISIT":            visits[subject].append({                "event": rec["event"],                "avisit": items.get("IT.VISIT.AVISIT", ""),                "avisitn": items.get("IT.VISIT.AVISITN", ""),                "adt": items.get("IT.VISIT.ADT", ""),            })        elif rec["group"] == "IG.TUTARGET":            link = items.get("IT.TU.TULNKID", "")            if link:                chosen_targets[subject].add(link)        elif rec["group"] == "IG.TRTARGET":            link = items.get("IT.TR.TRLNKID", "")            if link:                key = (subject, rec["event"])                any_meas[key] = True                target_meas[key][link] = to_number(items.get("IT.TR.LDIAM", ""))        elif rec["group"] in ("IG.TRNT", "IG.TUNT"):            any_meas[(subject, rec["event"])] = True    rows = []    study_of = {}    for rec in records:        study_of.setdefault(rec["subject"], rec["study"])    for subject in sorted(visits):        subject_visits = sorted(            visits[subject],            key=lambda v: (to_number(v["avisitn"]) is None,                           to_number(v["avisitn"]) or 0),        )        if not subject_visits:            continue        baseline_event = subject_visits[0]["event"]        chosen = chosen_targets.get(subject, set())        baseline_sum = None        if chosen:            base = target_meas.get((subject, baseline_event), {})            measured = [base[lesion] for lesion in chosen                        if base.get(lesion) is not None]            if len(measured) == len(chosen):                baseline_sum = sum(measured)        for seq, visit in enumerate(subject_visits, start=1):            event = visit["event"]            done = any_meas.get((subject, event), False)            rsstresc = ""            rsstat = ""            if not done:                rsstat = "NOT DONE"            elif event == baseline_event:                rsstresc = "NE"            elif not chosen:                # Non-target disease only.                rsstresc = "NE"            else:                meas = target_meas.get((subject, event), {})                measured = {lesion: meas[lesion] for lesion in chosen                            if meas.get(lesion) is not None}                if len(measured) < len(chosen):                    rsstresc = "NE"                elif baseline_sum is None or baseline_sum == 0:                    rsstresc = "NE"                else:                    total = sum(measured.values())                    if all(value == 0 for value in measured.values()):                        rsstresc = "CR"                    else:                        change = 100.0 * (total - baseline_sum) / baseline_sum                        if change <= -30.0:                            rsstresc = "PR"                        elif change >= 20.0:                            rsstresc = "PD"                        else:                            rsstresc = "SD"            rows.append({                "STUDYID": study_of.get(subject, ""),                "USUBJID": subject,                "AVISIT": visit["avisit"],                "AVISITN": visit["avisitn"],                "ADT": visit["adt"],                "RSSEQ": seq,                "RSTESTCD": "TRGRESP",                "RSTEST": "Target Response",                "RSSTRESC": rsstresc,                "RSSTAT": rsstat,            })    os.makedirs(OUTPUT, exist_ok=True)    with open(os.path.join(OUTPUT, "rs.csv"), "w", newline="") as handle:        writer = csv.DictWriter(handle, fieldnames=COLUMNS)        writer.writeheader()        writer.writerows(rows)if __name__ == "__main__":    main()

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='Target 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;