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 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

Comments and assessment materials

Comments

Signed in with GitHub; saved in repository Discussions

Mapping spec

Generated from the benchmark YAML; do not edit by hand

Variable NameVariable LabelTypeLengthControlled Terms or FormatOriginCoreConversion DefinitionVariable TypeVariable OrderComments for Define
DOMAINDomain AbbreviationCharAssignedSet constant value "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.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
37 lines

Following CDISC SDTM standards, use the provided ODM, VISITS, NOTDONE, and ITEMS datasets to create a QS dataset with one record per questionnaire item at each visit, plus a total-score record for each fully answered questionnaire.

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

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

QSSEQ numbers each subject's records by visit, then by test short name, so the total-score record follows the nine items and the refused-questionnaire record comes last. QSTESTCD is PHQ901 through PHQ909 for the nine items, PHQ9T for the total-score record, and QSALL for the refused questionnaire. QSTEST is the item text from the item table; the total record reads "Patient Health Questionnaire 9 item total score" and the refused record reads "All Questionnaires". QSCAT is always PHQ-9.

QSORRES is the response text matching the circled score: 0 is "Not at all", 1 is "Several days", 2 is "More than half the days", and 3 is "Nearly every day". It has no value on the total-score record because the score is computed, not collected. QSSTRESC is the collected score as text, or the total as text on the total record, written without a decimal point; QSSTRESN is the same value as a number. The total is the sum of the nine item scores and appears only for a visit where all nine items were answered.

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 have no value for QSORRES, QSSTRESC, and QSSTRESN. QSDRVFL is Y on the total-score record only. QSBLFL is Y on every record from the first visit. VISITNUM and QSDTC are the visit number and collection date from the visit table.

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 ODM, VISITS, NOTDONE, and ITEMS datasets to create a QS dataset with one record per questionnaire item at each visit, plus a total-score record for each fully answered questionnaire.

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

QSTESTCD is PHQ901 through PHQ909 for the nine items, PHQ9T for the total- score record, and QSALL for the refused questionnaire; QSTEST is the item text from the item table, "Patient Health Questionnaire 9 item total score", or "All Questionnaires". QSCAT is always PHQ-9. QSORRES is "Not at all", "Several days", "More than half the days", or "Nearly every day" for a circled score of 0 through 3. QSSTRESC is written without a decimal point. QSSTAT is NOT DONE or has no value, with QSREASND LOGICALLY SKIPPED ITEM or SUBJECT REFUSED. QSDRVFL and QSBLFL 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
11 lines

Following CDISC SDTM standards, use the provided ODM, VISITS, NOTDONE, and ITEMS datasets to create a QS dataset with one record per questionnaire item at each visit, plus a total-score record for each fully answered questionnaire.

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

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

run.R

Edit
4 lines
library(yamaa)qs <- yamaa_domain("spec.yaml")$outputqs

run.py

Edit
4 lines
import yamaaqs = yamaa.yamaa_domain("spec.yaml").outputqs

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
194 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.from pathlib import Pathimport polars as pldef _present(value) -> bool:    return value is not None and str(value) != ""items = pl.read_csv("/app/input/items.csv", infer_schema=False)visits = pl.read_csv("/app/input/visits.csv", infer_schema=False)notdone = pl.read_csv("/app/input/notdone.csv", infer_schema=False)odm = pl.read_csv("/app/input/odm.csv", infer_schema=False)item_by_oid = {r["ItemOID"]: r for r in items.to_dicts()}scores: dict[tuple, str] = {}for row in odm.to_dicts():    key = (str(row["SubjectKey"]), str(row["StudyEventOID"]), str(row["ItemOID"]))    if _present(row["Value"]):        scores[key] = str(row["Value"]).strip()refused: dict[tuple, str] = {}skipped: dict[tuple, str] = {}for row in notdone.to_dicts():    key = (str(row["SubjectKey"]), str(row["StudyEventOID"]))    oid, reason = str(row["ItemOID"] or ""), str(row["Reason"] or "")    if not _present(oid):        refused[key] = reason    else:        skipped[(key[0], key[1], oid)] = reasonlabels = {    "0": "Not at all",    "1": "Several days",    "2": "More than half the days",    "3": "Nearly every day",}total_test = "Patient Health Questionnaire 9 item total score"visit_by_key = {    (str(r["SubjectKey"]), str(r["StudyEventOID"])): r for r in visits.to_dicts()}first_visit = {}for row in visits.to_dicts():    subj = str(row["SubjectKey"])    num = int(row["VISITNUM"])    first_visit[subj] = min(first_visit.get(subj, num), num)rows = []for (subj, event), visit in visit_by_key.items():    studyid = str(visit["StudyOID"])    visitnum = str(visit["VISITNUM"])    vsdtc = str(visit["VisitDate"])    blfl = "Y" if int(visitnum) == first_visit[subj] else None    if (subj, event) in refused:        rows.append(            {                "DOMAIN": "QS",                "STUDYID": studyid,                "USUBJID": subj,                "QSTESTCD": "QSALL",                "QSTEST": "All Questionnaires",                "QSCAT": "PHQ-9",                "QSORRES": None,                "QSSTRESC": None,                "QSSTRESN": None,                "QSSTAT": "NOT DONE",                "QSREASND": refused[(subj, event)],                "QSBLFL": blfl,                "QSDRVFL": None,                "VISITNUM": visitnum,                "QSDTC": vsdtc,            }        )        continue    answered = []    for oid, item in item_by_oid.items():        testcd = str(item["QSTESTCD"])        test = str(item["QSTEST"])        score = scores.get((subj, event, oid))        if _present(score):            answered.append(int(score))            rows.append(                {                    "DOMAIN": "QS",                    "STUDYID": studyid,                    "USUBJID": subj,                    "QSTESTCD": testcd,                    "QSTEST": test,                    "QSCAT": "PHQ-9",                    "QSORRES": labels[score],                    "QSSTRESC": score,                    "QSSTRESN": score,                    "QSSTAT": None,                    "QSREASND": None,                    "QSBLFL": blfl,                    "QSDRVFL": None,                    "VISITNUM": visitnum,                    "QSDTC": vsdtc,                }            )        else:            reason = skipped.get((subj, event, oid))            rows.append(                {                    "DOMAIN": "QS",                    "STUDYID": studyid,                    "USUBJID": subj,                    "QSTESTCD": testcd,                    "QSTEST": test,                    "QSCAT": "PHQ-9",                    "QSORRES": None,                    "QSSTRESC": None,                    "QSSTRESN": None,                    "QSSTAT": "NOT DONE",                    "QSREASND": reason,                    "QSBLFL": blfl,                    "QSDRVFL": None,                    "VISITNUM": visitnum,                    "QSDTC": vsdtc,                }            )    # The total appears only for a visit where all nine items were answered.    if len(answered) == 9:        total = sum(answered)        rows.append(            {                "DOMAIN": "QS",                "STUDYID": studyid,                "USUBJID": subj,                "QSTESTCD": "PHQ9T",                "QSTEST": total_test,                "QSCAT": "PHQ-9",                "QSORRES": None,                "QSSTRESC": str(total),                "QSSTRESN": str(total),                "QSSTAT": None,                "QSREASND": None,                "QSBLFL": blfl,                "QSDRVFL": "Y",                "VISITNUM": visitnum,                "QSDTC": vsdtc,            }        )rows.sort(key=lambda r: (r["USUBJID"], int(r["VISITNUM"]), r["QSTESTCD"]))for by_subj in sorted({r["USUBJID"] for r in rows}):    for seq, row in enumerate(        [r for r in rows if r["USUBJID"] == by_subj], start=1    ):        row["QSSEQ"] = str(seq)qs = (    pl.DataFrame(        rows,        schema={            "DOMAIN": pl.String,            "STUDYID": pl.String,            "USUBJID": pl.String,            "QSSEQ": pl.String,            "QSTESTCD": pl.String,            "QSTEST": pl.String,            "QSCAT": pl.String,            "QSORRES": pl.String,            "QSSTRESC": pl.String,            "QSSTRESN": pl.String,            "QSSTAT": pl.String,            "QSREASND": pl.String,            "QSBLFL": pl.String,            "QSDRVFL": pl.String,            "VISITNUM": pl.String,            "QSDTC": pl.String,        },    )    .with_columns(        pl.col("QSSEQ").cast(pl.Int64).alias("_seq"),        pl.col("VISITNUM").cast(pl.Int64).alias("_visit"),    )    .sort(["USUBJID", "_visit", "QSTESTCD"])    .select(        "DOMAIN", "STUDYID", "USUBJID", "QSSEQ", "QSTESTCD", "QSTEST", "QSCAT",        "QSORRES", "QSSTRESC", "QSSTRESN", "QSSTAT", "QSREASND", "QSBLFL",        "QSDRVFL", "VISITNUM", "QSDTC",    ))Path("/app/output").mkdir(exist_ok=True)qs.write_csv("/app/output/qs.csv")

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;