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
README.mdPublic 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:
QSTESTCDisPHQ901throughPHQ909for the nine items,PHQ9Tfor the total-score record, andQSALLfor the refused questionnaire. These short names are fixture-local labels for this benchmark, not verified CDISC controlled terminology.QSTESTis the item text; the total record reads "Patient Health Questionnaire 9 item total score"; the refused-questionnaire record reads "All Questionnaires".QSCATis alwaysPHQ-9.QSORRESis 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, soQSORRESstays missing on it.QSSTRESCis the collected score as text, or the total as text on the total record.QSSTRESNis the collected score as a number, or the total as a number.QSSTATisNOT DONEon the skipped-item record and on the refused-questionnaire record;QSREASNDsays why (LOGICALLY SKIPPED ITEMorSUBJECT REFUSED).- Records with
QSSTAT = 'NOT DONE'carry no result:QSORRES,QSSTRESC, andQSSTRESNstay 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.
QSDRVFLisYon the total-score record only.QSBLFLisYon every record from the first visit.QSDTCis the date the questionnaire was collected.QSSEQnumbers 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 review4 source filesinput/items.csv
Edit9 rows| ItemOID | ItemOrder | QSTESTCD | QSTEST |
|---|---|---|---|
| IT.QS.PHQ9.01 | 1 | PHQ901 | Little interest or pleasure in doing things |
| IT.QS.PHQ9.02 | 2 | PHQ902 | Feeling down, depressed, or hopeless |
| IT.QS.PHQ9.03 | 3 | PHQ903 | Trouble falling or staying asleep, or sleeping too much |
| IT.QS.PHQ9.04 | 4 | PHQ904 | Feeling tired or having little energy |
| IT.QS.PHQ9.05 | 5 | PHQ905 | Poor appetite or overeating |
| IT.QS.PHQ9.06 | 6 | PHQ906 | Feeling bad about yourself - or that you are a failure or have let yourself or your family down |
| IT.QS.PHQ9.07 | 7 | PHQ907 | Trouble concentrating on things, such as reading the newspaper or watching television |
| IT.QS.PHQ9.08 | 8 | PHQ908 | Moving 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.09 | 9 | PHQ909 | Thoughts that you would be better off dead, or of hurting yourself in some way |
input/notdone.csv
Edit2 rows| StudyOID | SubjectKey | StudyEventOID | ItemOID | Reason |
|---|---|---|---|---|
| MDD01 | MDD01-1001 | VISIT2 | IT.QS.PHQ9.09 | LOGICALLY SKIPPED ITEM |
| MDD01 | MDD01-1002 | VISIT2 | — | SUBJECT REFUSED |
input/odm.csv
Edit26 rows| StudyOID | MetaDataVersionOID | SubjectKey | StudyEventOID | StudyEventRepeatKey | ItemGroupOID | ItemGroupRepeatKey | ItemOID | Value |
|---|---|---|---|---|---|---|---|---|
| MDD01 | MDV.1 | MDD01-1001 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.01 | 2 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.02 | 1 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.03 | 2 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.04 | 0 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.05 | 1 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.06 | 2 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.07 | 1 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.08 | 2 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.09 | 1 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT2 | 1 | IG.QS | 1 | IT.QS.PHQ9.01 | 1 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT2 | 1 | IG.QS | 1 | IT.QS.PHQ9.02 | 0 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT2 | 1 | IG.QS | 1 | IT.QS.PHQ9.03 | 1 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT2 | 1 | IG.QS | 1 | IT.QS.PHQ9.04 | 1 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT2 | 1 | IG.QS | 1 | IT.QS.PHQ9.05 | 0 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT2 | 1 | IG.QS | 1 | IT.QS.PHQ9.06 | 2 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT2 | 1 | IG.QS | 1 | IT.QS.PHQ9.07 | 1 |
| MDD01 | MDV.1 | MDD01-1001 | VISIT2 | 1 | IG.QS | 1 | IT.QS.PHQ9.08 | 0 |
| MDD01 | MDV.1 | MDD01-1002 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.01 | 1 |
| MDD01 | MDV.1 | MDD01-1002 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.02 | 1 |
| MDD01 | MDV.1 | MDD01-1002 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.03 | 0 |
| MDD01 | MDV.1 | MDD01-1002 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.04 | 2 |
| MDD01 | MDV.1 | MDD01-1002 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.05 | 1 |
| MDD01 | MDV.1 | MDD01-1002 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.06 | 0 |
| MDD01 | MDV.1 | MDD01-1002 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.07 | 1 |
| MDD01 | MDV.1 | MDD01-1002 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.08 | 2 |
| MDD01 | MDV.1 | MDD01-1002 | VISIT1 | 1 | IG.QS | 1 | IT.QS.PHQ9.09 | 0 |
input/visits.csv
Edit4 rows| StudyOID | SubjectKey | StudyEventOID | VISITNUM | VisitDate |
|---|---|---|---|---|
| MDD01 | MDD01-1001 | VISIT1 | 1 | 2026-01-05 |
| MDD01 | MDD01-1001 | VISIT2 | 2 | 2026-03-02 |
| MDD01 | MDD01-1002 | VISIT1 | 1 | 2026-01-06 |
| MDD01 | MDD01-1002 | VISIT2 | 2 | 2026-03-03 |
Expected output
Under review30 expected rowsexpected/qs.csv
Edit30 rows| DOMAIN | STUDYID | USUBJID | QSSEQ | QSTESTCD | QSTEST | QSCAT | QSORRES | QSSTRESC | QSSTRESN | QSSTAT | QSREASND | QSBLFL | QSDRVFL | VISITNUM | QSDTC |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| QS | MDD01 | 1 | PHQ901 | Little interest or pleasure in doing things | PHQ-9 | More than half the days | 2 | 2 | — | — | Y | — | 1 | 2026-01-05 | |
| QS | MDD01 | 2 | PHQ902 | Feeling down, depressed, or hopeless | PHQ-9 | Several days | 1 | 1 | — | — | Y | — | 1 | 2026-01-05 | |
| QS | MDD01 | 3 | PHQ903 | Trouble falling or staying asleep, or sleeping too much | PHQ-9 | More than half the days | 2 | 2 | — | — | Y | — | 1 | 2026-01-05 | |
| QS | MDD01 | 4 | PHQ904 | Feeling tired or having little energy | PHQ-9 | Not at all | 0 | 0 | — | — | Y | — | 1 | 2026-01-05 | |
| QS | MDD01 | 5 | PHQ905 | Poor appetite or overeating | PHQ-9 | Several days | 1 | 1 | — | — | Y | — | 1 | 2026-01-05 | |
| QS | MDD01 | 6 | PHQ906 | Feeling bad about yourself - or that you are a failure or have let yourself or your family down | PHQ-9 | More than half the days | 2 | 2 | — | — | Y | — | 1 | 2026-01-05 | |
| QS | MDD01 | 7 | PHQ907 | Trouble concentrating on things, such as reading the newspaper or watching television | PHQ-9 | Several days | 1 | 1 | — | — | Y | — | 1 | 2026-01-05 | |
| QS | MDD01 | 8 | PHQ908 | Moving 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 | PHQ-9 | More than half the days | 2 | 2 | — | — | Y | — | 1 | 2026-01-05 | |
| QS | MDD01 | 9 | PHQ909 | Thoughts that you would be better off dead, or of hurting yourself in some way | PHQ-9 | Several days | 1 | 1 | — | — | Y | — | 1 | 2026-01-05 | |
| QS | MDD01 | 10 | PHQ9T | Patient Health Questionnaire 9 item total score | PHQ-9 | — | 12 | 12 | — | — | Y | Y | 1 | 2026-01-05 | |
| QS | MDD01 | 11 | PHQ901 | Little interest or pleasure in doing things | PHQ-9 | Several days | 1 | 1 | — | — | — | — | 2 | 2026-03-02 | |
| QS | MDD01 | 12 | PHQ902 | Feeling down, depressed, or hopeless | PHQ-9 | Not at all | 0 | 0 | — | — | — | — | 2 | 2026-03-02 | |
| QS | MDD01 | 13 | PHQ903 | Trouble falling or staying asleep, or sleeping too much | PHQ-9 | Several days | 1 | 1 | — | — | — | — | 2 | 2026-03-02 | |
| QS | MDD01 | 14 | PHQ904 | Feeling tired or having little energy | PHQ-9 | Several days | 1 | 1 | — | — | — | — | 2 | 2026-03-02 | |
| QS | MDD01 | 15 | PHQ905 | Poor appetite or overeating | PHQ-9 | Not at all | 0 | 0 | — | — | — | — | 2 | 2026-03-02 | |
| QS | MDD01 | 16 | PHQ906 | Feeling bad about yourself - or that you are a failure or have let yourself or your family down | PHQ-9 | More than half the days | 2 | 2 | — | — | — | — | 2 | 2026-03-02 | |
| QS | MDD01 | 17 | PHQ907 | Trouble concentrating on things, such as reading the newspaper or watching television | PHQ-9 | Several days | 1 | 1 | — | — | — | — | 2 | 2026-03-02 | |
| QS | MDD01 | 18 | PHQ908 | Moving 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 | PHQ-9 | Not at all | 0 | 0 | — | — | — | — | 2 | 2026-03-02 | |
| QS | MDD01 | 19 | PHQ909 | Thoughts that you would be better off dead, or of hurting yourself in some way | PHQ-9 | — | — | — | NOT DONE | LOGICALLY SKIPPED ITEM | — | — | 2 | 2026-03-02 | |
| QS | MDD01 | 1 | PHQ901 | Little interest or pleasure in doing things | PHQ-9 | Several days | 1 | 1 | — | — | Y | — | 1 | 2026-01-06 | |
| QS | MDD01 | 2 | PHQ902 | Feeling down, depressed, or hopeless | PHQ-9 | Several days | 1 | 1 | — | — | Y | — | 1 | 2026-01-06 | |
| QS | MDD01 | 3 | PHQ903 | Trouble falling or staying asleep, or sleeping too much | PHQ-9 | Not at all | 0 | 0 | — | — | Y | — | 1 | 2026-01-06 | |
| QS | MDD01 | 4 | PHQ904 | Feeling tired or having little energy | PHQ-9 | More than half the days | 2 | 2 | — | — | Y | — | 1 | 2026-01-06 | |
| QS | MDD01 | 5 | PHQ905 | Poor appetite or overeating | PHQ-9 | Several days | 1 | 1 | — | — | Y | — | 1 | 2026-01-06 | |
| QS | MDD01 | 6 | PHQ906 | Feeling bad about yourself - or that you are a failure or have let yourself or your family down | PHQ-9 | Not at all | 0 | 0 | — | — | Y | — | 1 | 2026-01-06 | |
| QS | MDD01 | 7 | PHQ907 | Trouble concentrating on things, such as reading the newspaper or watching television | PHQ-9 | Several days | 1 | 1 | — | — | Y | — | 1 | 2026-01-06 | |
| QS | MDD01 | 8 | PHQ908 | Moving 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 | PHQ-9 | More than half the days | 2 | 2 | — | — | Y | — | 1 | 2026-01-06 | |
| QS | MDD01 | 9 | PHQ909 | Thoughts that you would be better off dead, or of hurting yourself in some way | PHQ-9 | Not at all | 0 | 0 | — | — | Y | — | 1 | 2026-01-06 | |
| QS | MDD01 | 10 | PHQ9T | Patient Health Questionnaire 9 item total score | PHQ-9 | — | 8 | 8 | — | — | Y | Y | 1 | 2026-01-06 | |
| QS | MDD01 | 11 | QSALL | All Questionnaires | PHQ-9 | — | — | — | NOT DONE | SUBJECT REFUSED | — | — | 2 | 2026-03-03 |
Human review and assessment
| Variable Name | Variable Label | Type | Length | Controlled Terms or Format | Origin | Core | Conversion Definition | Variable Type | Variable Order | Comments for Define |
|---|---|---|---|---|---|---|---|---|---|---|
| DOMAIN | Domain Abbreviation | Char | Assigned | Set constant value "QS". | SDTM | 1 | ||||
| STUDYID | Study Identifier | Char | Derived | Set per constructed row; see the Row construction sheet. | SDTM | 2 | ||||
| USUBJID | Unique Subject Identifier | Char | Derived | Set per constructed row; see the Row construction sheet. | SDTM | 3 | ||||
| VISITNUM | Visit Number | Num | Derived | Set per constructed row; see the Row construction sheet. | SDTM | 4 | ||||
| QSDTC | Date/Time of Collection | Char | ISO 8601 | Derived | Set per constructed row; see the Row construction sheet. | SDTM | 5 | |||
| QSTESTCD | Questionnaire Test Short Name | Char | Derived | Set per constructed row; see the Row construction sheet. | SDTM | 6 | ||||
| QSTEST | Questionnaire Test Name | Char | Derived | Set per constructed row; see the Row construction sheet. | SDTM | 7 | ||||
| QSCAT | Category for Questionnaires | Char | Assigned | Set constant value "PHQ-9". | SDTM | 8 | ||||
| QSORRES | Result or Finding in Original Units | Char | Derived | Set per constructed row; see the Row construction sheet. | SDTM | 9 | ||||
| QSSTRESC | Character Result/Finding in Std Format | Char | Derived | Set per constructed row; see the Row construction sheet. | SDTM | 10 | ||||
| QSSTRESN | Numeric Result/Finding in Standard Units | Num | Derived | Set per constructed row; see the Row construction sheet. | SDTM | 11 | ||||
| QSSTAT | Completion Status | Char | Derived | Set per constructed row; see the Row construction sheet. | SDTM | 12 | ||||
| QSREASND | Reason Not Done | Char | Derived | Set per constructed row; see the Row construction sheet. | SDTM | 13 | ||||
| QSBLFL | Baseline Flag | Char | Derived | If VISITNUM = 1 then "Y"; otherwise blank. | SDTM | 14 | ||||
| QSDRVFL | Derived Flag | Char | Derived | If QSTESTCD = 'PHQ9T' then "Y"; otherwise blank. | SDTM | 15 | ||||
| QSSEQ | Sequence Number | Num | Derived | Row number within each group of STUDYID, USUBJID, ordered by VISITNUM then QSTESTCD. | SDTM | 16 |
| Template | Include when | STUDYID | USUBJID | VISIT_SUBJECT | VISIT_EVENT | ITEM_OID | VISITNUM | QSDTC | QSTESTCD | QSTEST | QSORRES | QSSTRESC | QSSTRESN | QSSTAT | QSREASND | ITEMCNT | TOTALSCORE |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| item | Copy ODM.StudyOID | Copy ODM.SubjectKey | Copy ODM.SubjectKey | Copy ODM.StudyEventOID | Copy ODM.ItemOID | Copy VISITNUM from the VISITS record whose SubjectKey equals VISIT_SUBJECT and StudyEventOID equals VISIT_EVENT, and a row with no match is an error | Copy VisitDate from the VISITS record whose SubjectKey equals VISIT_SUBJECT and StudyEventOID equals VISIT_EVENT, and a row with no match is an error | Copy QSTESTCD from the ITEMS record whose ItemOID equals ITEM_OID, and a row with no match is an error | Copy QSTEST from the ITEMS record whose ItemOID equals ITEM_OID, and a row with no match is an error | If 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 blank | Copy ODM.Value | Copy ODM.Value | Constant "None" | Constant "None" | Constant "None" | Constant "None" | |
| notdone | Copy NOTDONE.StudyOID | Copy NOTDONE.SubjectKey | Copy NOTDONE.SubjectKey | Copy NOTDONE.StudyEventOID | Copy NOTDONE.ItemOID | Copy VISITNUM from the VISITS record whose SubjectKey equals VISIT_SUBJECT and StudyEventOID equals VISIT_EVENT, and a row with no match is an error | Copy VisitDate from the VISITS record whose SubjectKey equals VISIT_SUBJECT and StudyEventOID equals VISIT_EVENT, and a row with no match is an error | If NOTDONE.ItemOID IS NULL then "QSALL"; If NOTDONE.ItemOID IS NOT NULL then "ITEM.QSTESTCD"; otherwise blank | If NOTDONE.ItemOID IS NULL then "All Questionnaires"; If NOTDONE.ItemOID IS NOT NULL then "ITEM.QSTEST"; otherwise blank | Constant "None" | Constant "None" | Constant "None" | Constant "NOT DONE" | Copy NOTDONE.Reason | Constant "None" | Constant "None" | |
| total | ITEMCNT = 9 | Copy ODM.StudyOID | Copy ODM.SubjectKey | Copy ODM.SubjectKey | Copy ODM.StudyEventOID | Constant "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 error | Copy VisitDate from the VISITS record whose SubjectKey equals VISIT_SUBJECT and StudyEventOID equals VISIT_EVENT, and a row with no match is an error | Constant "PHQ9T" | Constant "Patient Health Questionnaire 9 item total score" | Constant "None" | Copy TOTALSCORE | Copy TOTALSCORE | Constant "None" | Constant "None" | Aggregate COUNT(ODM.Value) over records matching on the shared output keys | Aggregate SUM(ODM.Value) over records matching on the shared output keys |
| Version | Date | Author | Description | Reviewer | Sign-off |
|---|---|---|---|---|---|
| 1.7 | 2026-10-20 | yamaa docs build | Read 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.6 | 2026-10-17 | yamaa docs build | Describe collected date parts written as ISO 8601 date text and times of day counted in seconds since midnight | ||
| 1.5 | 2026-10-17 | yamaa docs build | Name 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.4 | 2026-10-15 | yamaa docs build | State datetime precision codes through the hour and minute, and complete only the time parts a collected datetime lacks | ||
| 1.3 | 2026-09-29 | yamaa docs build | Show study codelist names, external dictionaries, and ISO 8601 formats in variable rows; list the document's codelist values and metadata | ||
| 1.2 | 2026-09-29 | yamaa docs build | Classify ADaM copies from a declared input as Predecessor and recodes as Derived, and SDTM recodes of locally derived columns as Derived | ||
| 1.1 | 2026-09-29 | yamaa docs build | Resolve lookups into the donor record they select, and give every derivation shape a plain-language rule instead of its raw YAML | ||
| 1.0 | yamaa docs build | Initial generation from spec.yaml |
prompts/sdtm-qs-questionnaire-items/full.md
Edit52 linesFollowing 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
Edit20 linesFollowing 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
Edit10 linesFollowing 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.
R
Edit3 lineslibrary(yamaa)yamaa_spec("spec.yaml")$run()$outputPython
Edit3 linesimport yamaayamaa.spec("spec.yaml").run().outputsolutions/sdtm-qs-questionnaire-items/result.R
Edit158 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
Edit181 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
Edit49 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;
yamaa