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.
- 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.
- Inputs: Do they look like data collected in a real trial, and do they include the situations you would expect to meet in one?
- 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
README.mdGoal: 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 |
Comments and assessment materials
| 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.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
Edit37 linesFollowing 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
Edit20 linesFollowing 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
Edit11 linesFollowing 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.
run.R
Edit4 lineslibrary(yamaa)qs <- yamaa_domain("spec.yaml")$outputqsrun.py
Edit4 linesimport yamaaqs = yamaa.yamaa_domain("spec.yaml").outputqssolutions/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
Edit194 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
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
Comments
Signed in with GitHub; saved in repository Discussions