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 QSSEQ, QSTESTCD, QSTEST, QSCAT, QSORRES, QSSTRESC, QSSTRESN, QSSTAT, QSREASND, QSBLFL, QSDRVFL, VISITNUM, and QSDTC for the nine PHQ-9 items at two visits for two subjects, plus a total-score record for each fully answered questionnaire, in Questionnaires (QS).

Input: input/odm.csv is long-form ODM item data: one row per collected item, identified by study (StudyOID), subject (SubjectKey), visit (StudyEventOID), item group, and item (ItemOID), with the circled score (0 through 3) in Value. input/visits.csv gives the visit number and collection date for each subject visit. input/notdone.csv declares what was not collected, with the reason: one logically skipped item (its own item row) and one whole questionnaire the subject refused (a single row with no item, standing for the questionnaire). input/items.csv lists the nine instrument items with their short names and full text.

Variables:

  • QSTESTCD is PHQ901 through PHQ909 for the nine items, PHQ9T for the total-score record, and QSALL for the refused questionnaire. These short names are fixture-local labels for this benchmark, not verified CDISC controlled terminology.
  • QSTEST is the item text; the total record reads "Patient Health Questionnaire 9 item total score"; the refused-questionnaire record reads "All Questionnaires".
  • QSCAT is always PHQ-9.
  • QSORRES is the response text matching the circled score ("Not at all", "Several days", "More than half the days", "Nearly every day"). The total-score record has no original result because the score is computed, not collected, so QSORRES stays missing on it.
  • QSSTRESC is the collected score as text, or the total as text on the total record.
  • QSSTRESN is the collected score as a number, or the total as a number.
  • QSSTAT is NOT DONE on the skipped-item record and on the refused-questionnaire record; QSREASND says why (LOGICALLY SKIPPED ITEM or SUBJECT REFUSED).
  • Records with QSSTAT = 'NOT DONE' carry no result: QSORRES, QSSTRESC, and QSSTRESN stay empty.
  • The total-score record appears only for a visit where all nine items were answered; a visit with a skipped or refused item gets no total.
  • QSDRVFL is Y on the total-score record only.
  • QSBLFL is Y on every record from the first visit.
  • QSDTC is the date the questionnaire was collected.
  • QSSEQ numbers the subject's records by visit, then by test short name, so the total-score record (PHQ9T) sorts after the nine items, and the refused-questionnaire record (QSALL) sorts last, alphabetically.

Note: the logically skipped item keeps its record with QSSTAT = 'NOT DONE' instead of disappearing, following the FDA convention for items the instrument instructions skip; the refused questionnaire keeps one record instead of nine, with QSTESTCD = 'QSALL', QSSTAT = 'NOT DONE', and QSREASND = 'SUBJECT REFUSED', per issue #374's design.

Inputs

Under review
4 source files

input/items.csv

Edit
9 rows
input/items.csv
ItemOIDItemOrderQSTESTCDQSTEST
IT.QS.PHQ9.011PHQ901Little interest or pleasure in doing things
IT.QS.PHQ9.022PHQ902Feeling down, depressed, or hopeless
IT.QS.PHQ9.033PHQ903Trouble falling or staying asleep, or sleeping too much
IT.QS.PHQ9.044PHQ904Feeling tired or having little energy
IT.QS.PHQ9.055PHQ905Poor appetite or overeating
IT.QS.PHQ9.066PHQ906Feeling bad about yourself - or that you are a failure or have let yourself or your family down
IT.QS.PHQ9.077PHQ907Trouble concentrating on things, such as reading the newspaper or watching television
IT.QS.PHQ9.088PHQ908Moving or speaking so slowly that other people could have noticed. Or the opposite - being so fidgety or restless that you have been moving around a lot more than usual
IT.QS.PHQ9.099PHQ909Thoughts that you would be better off dead, or of hurting yourself in some way

input/notdone.csv

Edit
2 rows
input/notdone.csv
StudyOIDSubjectKeyStudyEventOIDItemOIDReason
MDD01MDD01-1001VISIT2IT.QS.PHQ9.09LOGICALLY SKIPPED ITEM
MDD01MDD01-1002VISIT2—SUBJECT REFUSED

input/odm.csv

Edit
26 rows
input/odm.csv
StudyOIDMetaDataVersionOIDSubjectKeyStudyEventOIDStudyEventRepeatKeyItemGroupOIDItemGroupRepeatKeyItemOIDValue
MDD01MDV.1MDD01-1001VISIT11IG.QS1IT.QS.PHQ9.012
MDD01MDV.1MDD01-1001VISIT11IG.QS1IT.QS.PHQ9.021
MDD01MDV.1MDD01-1001VISIT11IG.QS1IT.QS.PHQ9.032
MDD01MDV.1MDD01-1001VISIT11IG.QS1IT.QS.PHQ9.040
MDD01MDV.1MDD01-1001VISIT11IG.QS1IT.QS.PHQ9.051
MDD01MDV.1MDD01-1001VISIT11IG.QS1IT.QS.PHQ9.062
MDD01MDV.1MDD01-1001VISIT11IG.QS1IT.QS.PHQ9.071
MDD01MDV.1MDD01-1001VISIT11IG.QS1IT.QS.PHQ9.082
MDD01MDV.1MDD01-1001VISIT11IG.QS1IT.QS.PHQ9.091
MDD01MDV.1MDD01-1001VISIT21IG.QS1IT.QS.PHQ9.011
MDD01MDV.1MDD01-1001VISIT21IG.QS1IT.QS.PHQ9.020
MDD01MDV.1MDD01-1001VISIT21IG.QS1IT.QS.PHQ9.031
MDD01MDV.1MDD01-1001VISIT21IG.QS1IT.QS.PHQ9.041
MDD01MDV.1MDD01-1001VISIT21IG.QS1IT.QS.PHQ9.050
MDD01MDV.1MDD01-1001VISIT21IG.QS1IT.QS.PHQ9.062
MDD01MDV.1MDD01-1001VISIT21IG.QS1IT.QS.PHQ9.071
MDD01MDV.1MDD01-1001VISIT21IG.QS1IT.QS.PHQ9.080
MDD01MDV.1MDD01-1002VISIT11IG.QS1IT.QS.PHQ9.011
MDD01MDV.1MDD01-1002VISIT11IG.QS1IT.QS.PHQ9.021
MDD01MDV.1MDD01-1002VISIT11IG.QS1IT.QS.PHQ9.030
MDD01MDV.1MDD01-1002VISIT11IG.QS1IT.QS.PHQ9.042
MDD01MDV.1MDD01-1002VISIT11IG.QS1IT.QS.PHQ9.051
MDD01MDV.1MDD01-1002VISIT11IG.QS1IT.QS.PHQ9.060
MDD01MDV.1MDD01-1002VISIT11IG.QS1IT.QS.PHQ9.071
MDD01MDV.1MDD01-1002VISIT11IG.QS1IT.QS.PHQ9.082
MDD01MDV.1MDD01-1002VISIT11IG.QS1IT.QS.PHQ9.090

input/visits.csv

Edit
4 rows
input/visits.csv
StudyOIDSubjectKeyStudyEventOIDVISITNUMVisitDate
MDD01MDD01-1001VISIT112026-01-05
MDD01MDD01-1001VISIT222026-03-02
MDD01MDD01-1002VISIT112026-01-06
MDD01MDD01-1002VISIT222026-03-03

Expected output

Under review
30 expected rows

expected/qs.csv

Edit
30 rows
expected/qs.csv
DOMAINSTUDYIDUSUBJIDQSSEQQSTESTCDQSTESTQSCATQSORRESQSSTRESCQSSTRESNQSSTATQSREASNDQSBLFLQSDRVFLVISITNUMQSDTC
QSMDD011PHQ901Little interest or pleasure in doing thingsPHQ-9More than half the days22——Y—12026-01-05
QSMDD012PHQ902Feeling down, depressed, or hopelessPHQ-9Several days11——Y—12026-01-05
QSMDD013PHQ903Trouble falling or staying asleep, or sleeping too muchPHQ-9More than half the days22——Y—12026-01-05
QSMDD014PHQ904Feeling tired or having little energyPHQ-9Not at all00——Y—12026-01-05
QSMDD015PHQ905Poor appetite or overeatingPHQ-9Several days11——Y—12026-01-05
QSMDD016PHQ906Feeling bad about yourself - or that you are a failure or have let yourself or your family downPHQ-9More than half the days22——Y—12026-01-05
QSMDD017PHQ907Trouble concentrating on things, such as reading the newspaper or watching televisionPHQ-9Several days11——Y—12026-01-05
QSMDD018PHQ908Moving or speaking so slowly that other people could have noticed. Or the opposite - being so fidgety or restless that you have been moving around a lot more than usualPHQ-9More than half the days22——Y—12026-01-05
QSMDD019PHQ909Thoughts that you would be better off dead, or of hurting yourself in some wayPHQ-9Several days11——Y—12026-01-05
QSMDD0110PHQ9TPatient Health Questionnaire 9 item total scorePHQ-9—1212——YY12026-01-05
QSMDD0111PHQ901Little interest or pleasure in doing thingsPHQ-9Several days11————22026-03-02
QSMDD0112PHQ902Feeling down, depressed, or hopelessPHQ-9Not at all00————22026-03-02
QSMDD0113PHQ903Trouble falling or staying asleep, or sleeping too muchPHQ-9Several days11————22026-03-02
QSMDD0114PHQ904Feeling tired or having little energyPHQ-9Several days11————22026-03-02
QSMDD0115PHQ905Poor appetite or overeatingPHQ-9Not at all00————22026-03-02
QSMDD0116PHQ906Feeling bad about yourself - or that you are a failure or have let yourself or your family downPHQ-9More than half the days22————22026-03-02
QSMDD0117PHQ907Trouble concentrating on things, such as reading the newspaper or watching televisionPHQ-9Several days11————22026-03-02
QSMDD0118PHQ908Moving or speaking so slowly that other people could have noticed. Or the opposite - being so fidgety or restless that you have been moving around a lot more than usualPHQ-9Not at all00————22026-03-02
QSMDD0119PHQ909Thoughts that you would be better off dead, or of hurting yourself in some wayPHQ-9———NOT DONELOGICALLY SKIPPED ITEM——22026-03-02
QSMDD011PHQ901Little interest or pleasure in doing thingsPHQ-9Several days11——Y—12026-01-06
QSMDD012PHQ902Feeling down, depressed, or hopelessPHQ-9Several days11——Y—12026-01-06
QSMDD013PHQ903Trouble falling or staying asleep, or sleeping too muchPHQ-9Not at all00——Y—12026-01-06
QSMDD014PHQ904Feeling tired or having little energyPHQ-9More than half the days22——Y—12026-01-06
QSMDD015PHQ905Poor appetite or overeatingPHQ-9Several days11——Y—12026-01-06
QSMDD016PHQ906Feeling bad about yourself - or that you are a failure or have let yourself or your family downPHQ-9Not at all00——Y—12026-01-06
QSMDD017PHQ907Trouble concentrating on things, such as reading the newspaper or watching televisionPHQ-9Several days11——Y—12026-01-06
QSMDD018PHQ908Moving or speaking so slowly that other people could have noticed. Or the opposite - being so fidgety or restless that you have been moving around a lot more than usualPHQ-9More than half the days22——Y—12026-01-06
QSMDD019PHQ909Thoughts that you would be better off dead, or of hurting yourself in some wayPHQ-9Not at all00——Y—12026-01-06
QSMDD0110PHQ9TPatient Health Questionnaire 9 item total scorePHQ-9—88——YY12026-01-06
QSMDD0111QSALLAll QuestionnairesPHQ-9———NOT DONESUBJECT REFUSED——22026-03-03
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
DOMAINDomain AbbreviationCharAssignedSet constant value "QS".SDTM1
STUDYIDStudy IdentifierCharDerivedSet per constructed row; see the Row construction sheet.SDTM2
USUBJIDUnique Subject IdentifierCharDerivedSet per constructed row; see the Row construction sheet.SDTM3
VISITNUMVisit NumberNumDerivedSet per constructed row; see the Row construction sheet.SDTM4
QSDTCDate/Time of CollectionCharISO 8601DerivedSet per constructed row; see the Row construction sheet.SDTM5
QSTESTCDQuestionnaire Test Short NameCharDerivedSet per constructed row; see the Row construction sheet.SDTM6
QSTESTQuestionnaire Test NameCharDerivedSet per constructed row; see the Row construction sheet.SDTM7
QSCATCategory for QuestionnairesCharAssignedSet constant value "PHQ-9".SDTM8
QSORRESResult or Finding in Original UnitsCharDerivedSet per constructed row; see the Row construction sheet.SDTM9
QSSTRESCCharacter Result/Finding in Std FormatCharDerivedSet per constructed row; see the Row construction sheet.SDTM10
QSSTRESNNumeric Result/Finding in Standard UnitsNumDerivedSet per constructed row; see the Row construction sheet.SDTM11
QSSTATCompletion StatusCharDerivedSet per constructed row; see the Row construction sheet.SDTM12
QSREASNDReason Not DoneCharDerivedSet per constructed row; see the Row construction sheet.SDTM13
QSBLFLBaseline FlagCharDerivedIf VISITNUM = 1 then "Y"; otherwise blank.SDTM14
QSDRVFLDerived FlagCharDerivedIf QSTESTCD = 'PHQ9T' then "Y"; otherwise blank.SDTM15
QSSEQSequence NumberNumDerivedRow number within each group of STUDYID, USUBJID, ordered by VISITNUM then QSTESTCD.SDTM16
TemplateInclude whenSTUDYIDUSUBJIDVISIT_SUBJECTVISIT_EVENTITEM_OIDVISITNUMQSDTCQSTESTCDQSTESTQSORRESQSSTRESCQSSTRESNQSSTATQSREASNDITEMCNTTOTALSCORE
itemCopy ODM.StudyOIDCopy ODM.SubjectKeyCopy ODM.SubjectKeyCopy ODM.StudyEventOIDCopy ODM.ItemOIDCopy VISITNUM from the VISITS record whose SubjectKey equals VISIT_SUBJECT and StudyEventOID equals VISIT_EVENT, and a row with no match is an errorCopy VisitDate from the VISITS record whose SubjectKey equals VISIT_SUBJECT and StudyEventOID equals VISIT_EVENT, and a row with no match is an errorCopy QSTESTCD from the ITEMS record whose ItemOID equals ITEM_OID, and a row with no match is an errorCopy QSTEST from the ITEMS record whose ItemOID equals ITEM_OID, and a row with no match is an errorIf ODM.Value = 0 then "Not at all"; If ODM.Value = 1 then "Several days"; If ODM.Value = 2 then "More than half the days"; If ODM.Value = 3 then "Nearly every day"; otherwise blankCopy ODM.ValueCopy ODM.ValueConstant "None"Constant "None"Constant "None"Constant "None"
notdoneCopy NOTDONE.StudyOIDCopy NOTDONE.SubjectKeyCopy NOTDONE.SubjectKeyCopy NOTDONE.StudyEventOIDCopy NOTDONE.ItemOIDCopy VISITNUM from the VISITS record whose SubjectKey equals VISIT_SUBJECT and StudyEventOID equals VISIT_EVENT, and a row with no match is an errorCopy VisitDate from the VISITS record whose SubjectKey equals VISIT_SUBJECT and StudyEventOID equals VISIT_EVENT, and a row with no match is an errorIf NOTDONE.ItemOID IS NULL then "QSALL"; If NOTDONE.ItemOID IS NOT NULL then "ITEM.QSTESTCD"; otherwise blankIf NOTDONE.ItemOID IS NULL then "All Questionnaires"; If NOTDONE.ItemOID IS NOT NULL then "ITEM.QSTEST"; otherwise blankConstant "None"Constant "None"Constant "None"Constant "NOT DONE"Copy NOTDONE.ReasonConstant "None"Constant "None"
totalITEMCNT = 9Copy ODM.StudyOIDCopy ODM.SubjectKeyCopy ODM.SubjectKeyCopy ODM.StudyEventOIDConstant "None"Copy VISITNUM from the VISITS record whose SubjectKey equals VISIT_SUBJECT and StudyEventOID equals VISIT_EVENT, and a row with no match is an errorCopy VisitDate from the VISITS record whose SubjectKey equals VISIT_SUBJECT and StudyEventOID equals VISIT_EVENT, and a row with no match is an errorConstant "PHQ9T"Constant "Patient Health Questionnaire 9 item total score"Constant "None"Copy TOTALSCORECopy TOTALSCOREConstant "None"Constant "None"Aggregate COUNT(ODM.Value) over records matching on the shared output keysAggregate SUM(ODM.Value) over records matching on the shared output keys
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-qs-questionnaire-items/full.md

Edit
52 lines

Following CDISC SDTM standards, use the provided ITEMS, NOTDONE, ODM, and VISITS datasets to create a QS dataset with one record per questionnaire item per subject per visit.

The output dataset should contain the following columns in this order: DOMAIN, STUDYID, USUBJID, QSSEQ, QSTESTCD, QSTEST, QSCAT, QSORRES, QSSTRESC, QSSTRESN, QSSTAT, QSREASND, QSBLFL, QSDRVFL, VISITNUM, QSDTC

DOMAIN is "QS". STUDYID is the study OID in ODM and USUBJID is the subject key in ODM, each as given. Each collected item gives one record; QSTESTCD is the item's short name in ITEMS, "PHQ901" through "PHQ909", and QSTEST is the item text in ITEMS. QSCAT is "PHQ-9" on every record. QSORRES is the response text matching the circled score: "Not at all" for 0, "Several days" for 1, "More than half the days" for 2, and "Nearly every day" for 3. QSSTRESC is the collected score as text, and QSSTRESN is the collected score as a number. VISITNUM is the visit number VISITS gives for the subject's visit, and QSDTC is the collection date VISITS gives for it. A visit where all nine items were answered also gives a total-score record, with QSTESTCD "PHQ9T" and QSTEST "Patient Health Questionnaire 9 item total score". On the total-score record, QSSTRESC is the sum of the nine item scores as text and QSSTRESN is that sum as a number. The total-score record's QSORRES has no value, because the total is computed, not collected. A visit with a skipped or a refused item gives no total-score record. QSDRVFL is "Y" on the total-score record only, and has no value on every other record. NOTDONE declares what was not collected: a row naming an item is that item's own record, and a row naming no item stands for the whole questionnaire. The skipped item keeps its record, with its item's QSTESTCD and QSTEST, QSSTAT "NOT DONE", and QSREASND "LOGICALLY SKIPPED ITEM". A refused questionnaire keeps one record instead of nine, with QSTESTCD "QSALL", QSTEST "All Questionnaires", QSSTAT "NOT DONE", and QSREASND "SUBJECT REFUSED". A record with QSSTAT "NOT DONE" carries no result: QSORRES, QSSTRESC, and QSSTRESN have no value. QSSTAT and QSREASND have no value on every other record. QSBLFL is "Y" on every record whose VISITNUM is 1, and has no value on every other record. QSSEQ numbers a subject's records from 1 by visit number, then by QSTESTCD. The total-score record follows the nine item records of its visit, and the refused-questionnaire record is the last record of its visit.

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

prompts/sdtm-qs-questionnaire-items/conventions.md

Edit
20 lines

Following CDISC SDTM standards, use the provided ITEMS, NOTDONE, ODM, and VISITS datasets to create a QS dataset with one record per questionnaire item per subject per visit.

The output dataset should contain the following columns in this order: DOMAIN, STUDYID, USUBJID, QSSEQ, QSTESTCD, QSTEST, QSCAT, QSORRES, QSSTRESC, QSSTRESN, QSSTAT, QSREASND, QSBLFL, QSDRVFL, VISITNUM, QSDTC

DOMAIN is "QS". QSCAT is "PHQ-9" on every record. QSTESTCD is "PHQ901" through "PHQ909", "PHQ9T" on the total-score record, or "QSALL" on the refused record; QSTEST is "Patient Health Questionnaire 9 item total score" or "All Questionnaires". QSORRES is "Not at all" for 0, "Several days" for 1, "More than half the days" for 2, and "Nearly every day" for 3. QSSTAT is "NOT DONE" or has no value; QSREASND is "LOGICALLY SKIPPED ITEM" or "SUBJECT REFUSED", or has no value; QSBLFL and QSDRVFL are "Y" or have no value.

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

prompts/sdtm-qs-questionnaire-items/brief.md

Edit
10 lines

Following CDISC SDTM standards, use the provided ITEMS, NOTDONE, ODM, and VISITS datasets to create a QS dataset with one record per questionnaire item per subject per visit.

The output dataset should contain the following columns in this order: DOMAIN, STUDYID, USUBJID, QSSEQ, QSTESTCD, QSTEST, QSCAT, QSORRES, QSSTRESC, QSSTRESN, QSSTAT, QSREASND, QSBLFL, QSDRVFL, VISITNUM, QSDTC

Read the source datasets from /app/input and save the completed dataset as /app/output/qs.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-qs-questionnaire-items/result.R

Edit
158 lines
# Reference solution for the yamaa benchmark sdtm-qs-questionnaire-items (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)items <- read_csv(  "/app/input/items.csv",  col_types = cols(ItemOrder = col_integer(), .default = col_character()))visits <- read_csv(  "/app/input/visits.csv",  col_types = cols(VISITNUM = col_integer(), .default = col_character()))notdone <- read_csv(  "/app/input/notdone.csv",  col_types = cols(.default = col_character()))odm <- read_csv(  "/app/input/odm.csv",  col_types = cols(.default = col_character()))labels <- c(  "0" = "Not at all",  "1" = "Several days",  "2" = "More than half the days",  "3" = "Nearly every day")scores <- odm |>  filter(!is.na(Value) & Value != "") |>  select(SubjectKey, StudyEventOID, ItemOID, Value)skipped <- notdone |>  filter(!is.na(ItemOID) & ItemOID != "") |>  select(SubjectKey, StudyEventOID, ItemOID, Reason)refused <- notdone |>  filter(is.na(ItemOID) | ItemOID == "") |>  select(SubjectKey, StudyEventOID, Reason)first_visit <- visits |>  group_by(SubjectKey) |>  summarise(first_num = min(VISITNUM), .groups = "drop")# Every item at each visit, with the collected score or a NOT DONE record.item_rows <- visits |>  left_join(first_visit, by = "SubjectKey") |>  cross_join(items) |>  left_join(    scores,    by = c("SubjectKey", "StudyEventOID", "ItemOID")  ) |>  left_join(    skipped,    by = c("SubjectKey", "StudyEventOID", "ItemOID")  ) |>  left_join(    refused |> select(SubjectKey, StudyEventOID) |> mutate(is_refused = TRUE),    by = c("SubjectKey", "StudyEventOID")  ) |>  # A refused questionnaire contributes only its QSALL record below.  filter(is.na(is_refused)) |>  mutate(    DOMAIN = "QS",    STUDYID = StudyOID,    USUBJID = SubjectKey,    QSTESTCD = QSTESTCD,    QSTEST = QSTEST,    QSCAT = "PHQ-9",    answered = !is.na(Value) & Value != "",    QSORRES = if_else(answered, unname(labels[Value]), NA_character_),    QSSTRESC = if_else(answered, Value, NA_character_),    QSSTRESN = suppressWarnings(as.numeric(QSSTRESC)),    QSSTAT = if_else(answered, NA_character_, "NOT DONE"),    QSREASND = if_else(answered, NA_character_, Reason),    QSBLFL = if_else(VISITNUM == first_num, "Y", NA_character_),    QSDRVFL = NA_character_,    QSDTC = VisitDate  ) |>  select(    DOMAIN, STUDYID, USUBJID, QSTESTCD, QSTEST, QSCAT, QSORRES, QSSTRESC,    QSSTRESN, QSSTAT, QSREASND, QSBLFL, QSDRVFL, VISITNUM, QSDTC, Value  )totals <- item_rows |>  group_by(STUDYID, USUBJID, VISITNUM, QSDTC, QSBLFL) |>  summarise(    n_answered = sum(!is.na(Value)),    total = sum(suppressWarnings(as.numeric(Value)), na.rm = TRUE),    .groups = "drop"  ) |>  filter(n_answered == 9) |>  mutate(    DOMAIN = "QS",    QSTESTCD = "PHQ9T",    QSTEST = "Patient Health Questionnaire 9 item total score",    QSCAT = "PHQ-9",    QSORRES = NA_character_,    QSSTRESC = as.character(total),    QSSTRESN = total,    QSSTAT = NA_character_,    QSREASND = NA_character_,    QSDRVFL = "Y"  ) |>  select(    DOMAIN, STUDYID, USUBJID, QSTESTCD, QSTEST, QSCAT, QSORRES, QSSTRESC,    QSSTRESN, QSSTAT, QSREASND, QSBLFL, QSDRVFL, VISITNUM, QSDTC  )refused_rows <- visits |>  inner_join(refused, by = c("SubjectKey", "StudyEventOID")) |>  left_join(first_visit, by = "SubjectKey") |>  mutate(    DOMAIN = "QS",    STUDYID = StudyOID,    USUBJID = SubjectKey,    QSTESTCD = "QSALL",    QSTEST = "All Questionnaires",    QSCAT = "PHQ-9",    QSORRES = NA_character_,    QSSTRESC = NA_character_,    QSSTRESN = NA_real_,    QSSTAT = "NOT DONE",    QSREASND = Reason,    QSBLFL = if_else(VISITNUM == first_num, "Y", NA_character_),    QSDRVFL = NA_character_,    QSDTC = VisitDate  ) |>  select(    DOMAIN, STUDYID, USUBJID, QSTESTCD, QSTEST, QSCAT, QSORRES, QSSTRESC,    QSSTRESN, QSSTAT, QSREASND, QSBLFL, QSDRVFL, VISITNUM, QSDTC  )qs <- bind_rows(  item_rows |>    select(-Value) |>    mutate(QSSTRESN = suppressWarnings(as.numeric(QSSTRESC))),  totals,  refused_rows) |>  arrange(USUBJID, VISITNUM, QSTESTCD) |>  group_by(USUBJID) |>  mutate(QSSEQ = row_number()) |>  ungroup() |>  arrange(USUBJID, QSSEQ) |>  select(    DOMAIN, STUDYID, USUBJID, QSSEQ, QSTESTCD, QSTEST, QSCAT, QSORRES,    QSSTRESC, QSSTRESN, QSSTAT, QSREASND, QSBLFL, QSDRVFL, VISITNUM, QSDTC  )dir.create("/app/output", showWarnings = FALSE)write_csv(qs, "/app/output/qs.csv", na = "")

solutions/sdtm-qs-questionnaire-items/result.py

Edit
181 lines
# Reference solution for the yamaa benchmark sdtm-qs-questionnaire-items (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."""Build the SDTM QS dataset (PHQ-9) from ITEMS, NOTDONE, ODM and VISITS."""import csvimport osINPUT_DIR = "/app/input"OUTPUT_DIR = "/app/output"RESPONSE_TEXT = {    0: "Not at all",    1: "Several days",    2: "More than half the days",    3: "Nearly every day",}COLUMNS = [    "DOMAIN", "STUDYID", "USUBJID", "QSSEQ", "QSTESTCD", "QSTEST",    "QSCAT", "QSORRES", "QSSTRESC", "QSSTRESN", "QSSTAT", "QSREASND",    "QSBLFL", "QSDRVFL", "VISITNUM", "QSDTC",]def read_csv(name):    with open(os.path.join(INPUT_DIR, name), newline="", encoding="utf-8") as fh:        return list(csv.DictReader(fh))def main():    items = read_csv("items.csv")    notdone = read_csv("notdone.csv")    odm = read_csv("odm.csv")    visits = read_csv("visits.csv")    # Item metadata keyed by ItemOID, kept in ITEMS order.    item_by_oid = {row["ItemOID"]: row for row in items}    # Visit number and collection date per (subject, study event).    visit_info = {}    for row in visits:        visit_info[(row["SubjectKey"], row["StudyEventOID"])] = (            int(row["VISITNUM"]),            row["VisitDate"],        )    # Collected scores: (subject, event, item oid) -> int score.    collected = {}    study_by_subject = {}    for row in odm:        study_by_subject[row["SubjectKey"]] = row["StudyOID"]        if row["ItemOID"] in item_by_oid:            collected[(row["SubjectKey"], row["StudyEventOID"], row["ItemOID"])] = int(row["Value"])    # Not-done declarations: whole questionnaire (item oid empty) or per item.    refused = {}       # (subject, event) -> reason    skipped = {}       # (subject, event, item oid) -> reason    for row in notdone:        key = (row["SubjectKey"], row["StudyEventOID"])        if row["ItemOID"]:            skipped[key + (row["ItemOID"],)] = row["Reason"]        else:            refused[key] = row["Reason"]    # Every subject/visit seen in ODM, NOTDONE or VISITS.    subject_events = set()    for subject, event in visit_info:        subject_events.add((subject, event))    for subject, event, _oid in collected:        subject_events.add((subject, event))    for subject, event in refused:        subject_events.add((subject, event))    for subject, event, _oid in skipped:        subject_events.add((subject, event))    def sort_key(se):        subject, event = se        visitnum = visit_info.get((subject, event), (0, ""))[0]        return (subject, visitnum, event)    records = []    for subject, event in sorted(subject_events, key=sort_key):        visitnum, visit_date = visit_info.get((subject, event), ("", ""))        studyid = study_by_subject.get(subject, "")        blfl = "Y" if visitnum == 1 else ""        base = {            "DOMAIN": "QS",            "STUDYID": studyid,            "USUBJID": subject,            "QSCAT": "PHQ-9",            "VISITNUM": visitnum,            "QSDTC": visit_date,            "QSBLFL": blfl,        }        visit_records = []        if (subject, event) in refused:            rec = dict(base)            rec.update({                "QSTESTCD": "QSALL",                "QSTEST": "All Questionnaires",                "QSORRES": "",                "QSSTRESC": "",                "QSSTRESN": "",                "QSSTAT": "NOT DONE",                "QSREASND": refused[(subject, event)],                "QSDRVFL": "",            })            visit_records.append(rec)        else:            answered = 0            total = 0            for item in items:                oid = item["ItemOID"]                key = (subject, event, oid)                rec = dict(base)                rec.update({                    "QSTESTCD": item["QSTESTCD"],                    "QSTEST": item["QSTEST"],                    "QSDRVFL": "",                })                if key in skipped:                    rec.update({                        "QSORRES": "",                        "QSSTRESC": "",                        "QSSTRESN": "",                        "QSSTAT": "NOT DONE",                        "QSREASND": skipped[key],                    })                elif key in collected:                    score = collected[key]                    answered += 1                    total += score                    rec.update({                        "QSORRES": RESPONSE_TEXT[score],                        "QSSTRESC": str(score),                        "QSSTRESN": score,                        "QSSTAT": "",                        "QSREASND": "",                    })                else:                    continue                visit_records.append(rec)            if answered == len(items):                rec = dict(base)                rec.update({                    "QSTESTCD": "PHQ9T",                    "QSTEST": "Patient Health Questionnaire 9 item total score",                    "QSORRES": "",                    "QSSTRESC": str(total),                    "QSSTRESN": total,                    "QSSTAT": "",                    "QSREASND": "",                    "QSDRVFL": "Y",                })                visit_records.append(rec)        # QSSEQ within the visit follows QSTESTCD order; the total-score        # record (PHQ9T) and the refused record (QSALL) sort last anyway.        visit_records.sort(key=lambda r: r["QSTESTCD"])        records.extend(visit_records)    # QSSEQ numbers each subject's records from 1.    seq_by_subject = {}    for rec in records:        subject = rec["USUBJID"]        seq_by_subject[subject] = seq_by_subject.get(subject, 0) + 1        rec["QSSEQ"] = seq_by_subject[subject]    os.makedirs(OUTPUT_DIR, exist_ok=True)    with open(os.path.join(OUTPUT_DIR, "qs.csv"), "w", newline="", encoding="utf-8") as fh:        writer = csv.DictWriter(fh, fieldnames=COLUMNS)        writer.writeheader()        for rec in records:            writer.writerow({col: rec.get(col, "") for col in COLUMNS})if __name__ == "__main__":    main()

solutions/sdtm-qs-questionnaire-items/result.sas

Edit
49 lines
/* opensas reference solution for the yamaa benchmark sdtm-qs-questionnaire-items.   Reads the benchmark inputs and derives the requested output.   Execute with the existing opensas runtime: opensas /app/output/result.sas. */data items;  length ItemOID ItemOrder QSTESTCD QSTEST $1024;  infile "/app/input/items.csv" dsd dlm="," firstobs=2 truncover lrecl=32767;  input ItemOID : $char1024. ItemOrder : $char1024. QSTESTCD : $char1024. QSTEST : $char1024.;run;data notdone;  length StudyOID SubjectKey StudyEventOID ItemOID Reason $1024;  infile "/app/input/notdone.csv" dsd dlm="," firstobs=2 truncover lrecl=32767;  input StudyOID : $char1024. SubjectKey : $char1024. StudyEventOID : $char1024. ItemOID : $char1024. Reason : $char1024.;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;data visits;  length StudyOID SubjectKey StudyEventOID VisitDate $1024;  infile "/app/input/visits.csv" dsd dlm="," firstobs=2 truncover lrecl=32767;  input StudyOID : $char1024. SubjectKey : $char1024. StudyEventOID : $char1024. VISITNUM VisitDate : $char1024.;run;proc sql;create table answered as select a.*,b.QSTESTCD,b.QSTEST,c.VISITNUM,c.VisitDate from odm a inner join items b on a.ItemOID=b.ItemOID inner join visits c on a.StudyOID=c.StudyOID and a.SubjectKey=c.SubjectKey and a.StudyEventOID=c.StudyEventOID;create table skipped as select a.*,b.QSTESTCD,b.QSTEST,c.VISITNUM,c.VisitDate from notdone a inner join items b on a.ItemOID=b.ItemOID inner join visits c on a.StudyOID=c.StudyOID and a.SubjectKey=c.SubjectKey and a.StudyEventOID=c.StudyEventOID;create table refused as select a.*,c.VISITNUM,c.VisitDate from notdone a inner join visits c on a.StudyOID=c.StudyOID and a.SubjectKey=c.SubjectKey and a.StudyEventOID=c.StudyEventOID where missing(a.ItemOID);quit;data answers; set answered; length QSORRES QSSTRESC QSSTAT QSREASND QSDRVFL $1024; QSSTRESN=input(Value,best32.); QSSTRESC=strip(put(QSSTRESN,best32.)); QSORRES=''; if QSSTRESN=0 then QSORRES='Not at all'; if QSSTRESN=1 then QSORRES='Several days'; if QSSTRESN=2 then QSORRES='More than half the days'; if QSSTRESN=3 then QSORRES='Nearly every day'; QSSTAT=''; QSREASND=''; QSDRVFL=''; run;proc sql; create table totals as select StudyOID,SubjectKey,StudyEventOID,VISITNUM,VisitDate,sum(QSSTRESN) as QSSTRESN from answers where not missing(QSSTRESN) group by StudyOID,SubjectKey,StudyEventOID,VISITNUM,VisitDate having count(*)=9; quit;data totalout; set totals; length QSTESTCD QSTEST QSORRES QSSTRESC QSSTAT QSREASND QSDRVFL $1024; QSTESTCD='PHQ9T'; QSTEST='Patient Health Questionnaire 9 item total score'; QSORRES=''; QSSTRESC=strip(put(QSSTRESN,best32.)); QSSTAT=''; QSREASND=''; QSDRVFL='Y'; run;data skippedout; set skipped; length QSORRES QSSTRESC QSSTAT QSREASND QSDRVFL $1024; QSORRES=''; QSSTRESC=''; QSSTRESN=.; QSSTAT='NOT DONE'; QSREASND=Reason; QSDRVFL=''; run;data refusedout; set refused; length QSTESTCD QSTEST QSORRES QSSTRESC QSSTAT QSREASND QSDRVFL $1024; QSTESTCD='QSALL'; QSTEST='All Questionnaires'; QSORRES=''; QSSTRESC=''; QSSTRESN=.; QSSTAT='NOT DONE'; QSREASND=Reason; QSDRVFL=''; run;data prepared; set answers totalout skippedout refusedout; length DOMAIN $2 STUDYID USUBJID QSCAT QSBLFL QSDTC $1024; DOMAIN='QS'; STUDYID=StudyOID; USUBJID=SubjectKey; QSCAT='PHQ-9'; QSDTC=VisitDate; QSBLFL=''; if VISITNUM=1 then QSBLFL='Y'; run;proc sort data=prepared; by STUDYID USUBJID VISITNUM QSTESTCD; run;data result; set prepared; by STUDYID USUBJID; retain QSSEQ;  if first.USUBJID then QSSEQ=0;  QSSEQ+1;run;proc sql;  create table final as select DOMAIN, STUDYID, USUBJID, QSSEQ, QSTESTCD, QSTEST, QSCAT, QSORRES, QSSTRESC, QSSTRESN, QSSTAT, QSREASND, QSBLFL, QSDRVFL, VISITNUM, QSDTC from result;quit;proc export data=final outfile="/app/output/qs.csv" dbms=csv replace; run;