Review ask

As a clinical trial statistician or programmer, answer three short questions in Your review: the task, the inputs and outputs, and the AI-assisted review. Agreeing with all three approves this benchmark; disagreeing with any needs a comment. Propose a fix with the Edit button on any source file, or add it to your comments.

We suggest spending no more than 5 minutes on a review. The other tabs beside Your review are background for assessment, not under review.

Summary

Lifecycle: draftEditUnder review
What this benchmark means
README.md

Public entries: Python and R. Passing coverage requires an installed inventory.

Goal: flag each visit-level patient-reported outcome (PRO) score as a deterioration or not, then build one time-to-deterioration record per subject with parameter code TTDGHS (Time to Deterioration in Global Health Status), carrying STARTDT, ADT, AVAL, CNSR, EVNTDESC, CNSDTDSC, SRCDOM, SRCVAR, and SRCSEQ.

Input: subject-level randomization and death dates (RANDDT, DTHDT), visit-level PRO scores on a 0 to 100 scale, each with a sequence number (QSSEQ), an assessment date (ADT), and a score (AVAL), and disposition records naming disease progression, study discontinuation, or withdrawal of consent (DSDECOD, DSDTC).

Variables:

  • DETERFL is Y on an assessment dated after the subject's earliest assessment whose score has fallen at least 10 points below that earliest (baseline) score; blank otherwise, including on the baseline assessment itself. It is computed only to find the earliest flagged assessment per subject and is not an output column.
  • STARTDT is the randomization date, the origin from which the time to deterioration is counted.
  • ADT is the analysis date: the deterioration date when a flagged assessment exists and is not dated after the first censoring reason, else the death date when the subject died and the death is not dated after the first censoring reason. A deterioration and a death on the same date count as deterioration. With neither, ADT is the last assessment dated on or before the first censoring reason, or randomization when no assessment qualifies.
  • AVAL is the number of whole calendar months from STARTDT to ADT.
  • CNSR is 0 when the subject has a deterioration or death event and 1 otherwise.
  • EVNTDESC is PRO DETERIORATION for a deterioration event, DEATH for a death event, and CENSORED otherwise.
  • CNSDTDSC is blank for an event, holds the censoring reason (DISEASE PROGRESSION, STUDY DISCONTINUATION, or WITHDRAWAL OF CONSENT) when one is available, and is STUDY COMPLETION otherwise.
  • SRCDOM is QS for a deterioration or a censored record with a supporting assessment, and ADSL for a death or a record censored at randomization.
  • SRCVAR is ADT for a deterioration or a censored record with a supporting assessment, DTHDT for a death, and RANDDT for a record censored at randomization.
  • SRCSEQ is the sequence number from the deterioration or censoring assessment, and blank when the analysis date comes from a subject-level date.

Note: progression, discontinuation, and withdrawal are censoring reasons, never events: a subject who progresses before any deterioration is censored at the last assessment on or before the progression date, and a deterioration dated the same day as the progression still counts as the event.

Inputs

Under review
3 source files

input/adsl.csv

Edit
6 rows
input/adsl.csv
STUDYIDUSUBJIDRANDDTDTHDT
YAMAA-012026-01-152026-08-20
YAMAA-012026-01-152026-06-10
YAMAA-012026-01-15—
YAMAA-012026-01-15—
YAMAA-012026-01-15—
YAMAA-012026-01-15—

input/ds.csv

Edit
4 rows
input/ds.csv
STUDYIDUSUBJIDDSDECODDSDTCDSSEQ
YAMAA-01DISEASE PROGRESSION2026-04-011
YAMAA-01WITHDRAWAL OF CONSENT2026-06-011
YAMAA-01STUDY DISCONTINUATION2026-01-201
YAMAA-01DISEASE PROGRESSION2026-03-121

input/qs.csv

Edit
14 rows
input/qs.csv
STUDYIDUSUBJIDQSSEQAVISITADTAVAL
YAMAA-011BASELINE2026-01-1580
YAMAA-012WEEK 82026-03-1274
YAMAA-013WEEK 162026-05-0760
YAMAA-011BASELINE2026-01-1570
YAMAA-012WEEK 82026-03-1268
YAMAA-013WEEK 162026-05-0765
YAMAA-011BASELINE2026-01-1575
YAMAA-012WEEK 82026-03-1272
YAMAA-011BASELINE2026-01-1580
YAMAA-012WEEK 82026-03-1278
YAMAA-013WEEK 162026-05-0776
YAMAA-011BASELINE2026-01-1570
YAMAA-011BASELINE2026-01-1585
YAMAA-012WEEK 82026-03-1270

Expected output

Under review
6 expected rows

expected/adtte.csv

Edit
6 rows
expected/adtte.csv
STUDYIDUSUBJIDPARAMCDPARAMSTARTDTADTAVALCNSREVNTDESCCNSDTDSCSRCDOMSRCVARSRCSEQ
YAMAA-01TTDGHSTime to Deterioration in Global Health Status2026-01-152026-05-0730PRO DETERIORATION—QSADT3
YAMAA-01TTDGHSTime to Deterioration in Global Health Status2026-01-152026-06-1040DEATH—ADSLDTHDT—
YAMAA-01TTDGHSTime to Deterioration in Global Health Status2026-01-152026-03-1211CENSOREDDISEASE PROGRESSIONQSADT2
YAMAA-01TTDGHSTime to Deterioration in Global Health Status2026-01-152026-05-0731CENSOREDWITHDRAWAL OF CONSENTQSADT3
YAMAA-01TTDGHSTime to Deterioration in Global Health Status2026-01-152026-01-1501CENSOREDSTUDY DISCONTINUATIONQSADT1
YAMAA-01TTDGHSTime to Deterioration in Global Health Status2026-01-152026-03-1210PRO DETERIORATION—QSADT2
Shaded columns are derived— = missing value

Human review and assessment

Your review

3 questions and your comments · View benchmark discussions

Mapping spec

Generated from the benchmark YAML; do not edit by hand

Dataset NameVariable NameVariable LabelTypeCodelist/Controlled TermsCoreComputational MethodOrigin
ADTTESTUDYIDStudy IdentifiercharacterCopy value from ADSL.STUDYID.Predecessor
ADTTEUSUBJIDUnique Subject IdentifiercharacterCopy value from ADSL.USUBJID.Predecessor
ADTTEPARAMCDParameter CodecharacterSet constant value "TTDGHS".Assigned
ADTTEPARAMParametercharacterSet constant value "Time to Deterioration in Global Health Status".Assigned
ADTTESTARTDTTime-to-Event Origin DatenumericISO 8601Copy value from ADSL.RANDDT.Predecessor
ADTTEADTAnalysis DatenumericISO 8601First non-missing of EVENTDT and CENSORDT.Derived
ADTTEAVALAnalysis ValuenumericWhole months from STARTDT to ADT, counting the end date but not the start.Derived
ADTTECNSRCensornumericIf EVENTDT IS NOT NULL then "0"; otherwise "1".Derived
ADTTEEVNTDESCEvent or Censoring DescriptioncharacterIf DETERDT IS NOT NULL AND (DEATHDT IS NULL OR DETERDT <= DEATHDT) AND (CENSORRSNDT IS NULL OR DETERDT <= CENSORRSNDT) then "PRO DETERIORATION"; If DEATHDT IS NOT NULL AND (CENSORRSNDT IS NULL OR DEATHDT <= CENSORRSNDT) then "DEATH"; otherwise "CENSORED".Derived
ADTTECNSDTDSCCensoring Date DescriptioncharacterIf EVENTDT IS NOT NULL then "None"; If CENSORRSN IS NOT NULL then "CENSORRSN"; otherwise "STUDY COMPLETION".Derived
ADTTESRCDOMSource DatacharacterIf DETERDT IS NOT NULL AND (DEATHDT IS NULL OR DETERDT <= DEATHDT) AND (CENSORRSNDT IS NULL OR DETERDT <= CENSORRSNDT) then "QS"; If DEATHDT IS NOT NULL AND (CENSORRSNDT IS NULL OR DEATHDT <= CENSORRSNDT) then "ADSL"; If LASTQSLE IS NULL then "ADSL"; otherwise "QS".Derived
ADTTESRCVARSource VariablecharacterIf DETERDT IS NOT NULL AND (DEATHDT IS NULL OR DETERDT <= DEATHDT) AND (CENSORRSNDT IS NULL OR DETERDT <= CENSORRSNDT) then "ADT"; If DEATHDT IS NOT NULL AND (CENSORRSNDT IS NULL OR DEATHDT <= CENSORRSNDT) then "DTHDT"; If LASTQSLE IS NULL then "RANDDT"; otherwise "ADT".Derived
ADTTESRCSEQSource Sequence NumbernumericIf DETERDT IS NOT NULL AND (DEATHDT IS NULL OR DETERDT <= DEATHDT) AND (CENSORRSNDT IS NULL OR DETERDT <= CENSORRSNDT) then "FIRSTDETER.QSSEQ"; If EVNTDESC = 'CENSORED' AND LASTQSLE IS NOT NULL then "LASTQSSEQ"; otherwise blank.Derived
VersionDateAuthorDescriptionReviewerSign-off
1.72026-10-20yamaa docs buildRead a specification in the 1.0 surface as its 0.5 counterpart; state that missing values stay missing where a mapping declares no missing handler, and leave out a declared missing answer that restates a handler's default
1.62026-10-17yamaa docs buildDescribe collected date parts written as ISO 8601 date text and times of day counted in seconds since midnight
1.52026-10-17yamaa docs buildName collected precision in words instead of letter codes; state imputation fallbacks, minimum source precision, bounds ignored outside the collected period, and invalid text with no handler as an error; describe date and datetime shifts, imputation flag codes, and partial text giving no date part
1.42026-10-15yamaa docs buildState datetime precision codes through the hour and minute, and complete only the time parts a collected datetime lacks
1.32026-09-29yamaa docs buildShow study codelist names, external dictionaries, and ISO 8601 formats in variable rows; list the document's codelist values and metadata
1.22026-09-29yamaa docs buildClassify ADaM copies from a declared input as Predecessor and recodes as Derived, and SDTM recodes of locally derived columns as Derived
1.12026-09-29yamaa docs buildResolve lookups into the donor record they select, and give every derivation shape a plain-language rule instead of its raw YAML
1.0yamaa docs buildInitial generation from spec.yaml

Agent prompt

The request an agent receives with the input files; each tier states no more than the one above

prompts/adam-adtte-pro-deterioration/full.md

Edit
37 lines

Following CDISC ADaM standards, use the provided ADSL, DS, and QS datasets to create an ADTTE dataset with one record per subject.

The output dataset should contain the following columns in this order: STUDYID, USUBJID, PARAMCD, PARAM, STARTDT, ADT, AVAL, CNSR, EVNTDESC, CNSDTDSC, SRCDOM, SRCVAR, SRCSEQ

PARAMCD is "TTDGHS" and PARAM is "Time to Deterioration in Global Health Status". STARTDT is the randomization date. A deterioration is an assessment dated after the subject's earliest assessment whose score has fallen at least 10 points below that earliest assessment's score. The deterioration date is the date of the subject's earliest deterioration. The first censoring reason is the subject's earliest disposition record naming DISEASE PROGRESSION, STUDY DISCONTINUATION, or WITHDRAWAL OF CONSENT, and progression, discontinuation, and withdrawal are censoring reasons, never events. ADT is the deterioration date when a deterioration exists and is not dated after the first censoring reason. A deterioration and a death on the same date count as deterioration. Otherwise ADT is the death date when the subject died and the death is not dated after the first censoring reason. With neither, ADT is the last assessment dated on or before the first censoring reason, or the randomization date when no assessment qualifies. AVAL is the number of whole calendar months from STARTDT to ADT. CNSR is 0 when the subject has a deterioration or death event and 1 otherwise. EVNTDESC is "PRO DETERIORATION" for a deterioration event, "DEATH" for a death event, and "CENSORED" otherwise. CNSDTDSC has no value for an event, holds the censoring reason when one is available, and is "STUDY COMPLETION" otherwise. SRCDOM is "QS" for a deterioration or a censored record with a supporting assessment, and "ADSL" for a death or a record censored at randomization. SRCVAR is "ADT" for a deterioration or a censored record with a supporting assessment, "DTHDT" for a death, and "RANDDT" for a record censored at randomization. SRCSEQ is the QSSEQ of the deterioration or censoring assessment, and has no value when the analysis date comes from a subject-level date.

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

prompts/adam-adtte-pro-deterioration/conventions.md

Edit
16 lines

Following CDISC ADaM standards, use the provided ADSL, DS, and QS datasets to create an ADTTE dataset with one record per subject.

The output dataset should contain the following columns in this order: STUDYID, USUBJID, PARAMCD, PARAM, STARTDT, ADT, AVAL, CNSR, EVNTDESC, CNSDTDSC, SRCDOM, SRCVAR, SRCSEQ

PARAMCD is "TTDGHS" and PARAM is "Time to Deterioration in Global Health Status". AVAL is the number of whole calendar months from STARTDT to ADT. CNSR is 0 or 1. EVNTDESC is "PRO DETERIORATION", "DEATH", or "CENSORED". CNSDTDSC holds the censoring reason, or is "STUDY COMPLETION", or has no value. SRCDOM is "QS" or "ADSL". SRCVAR is "ADT", "DTHDT", or "RANDDT".

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

prompts/adam-adtte-pro-deterioration/brief.md

Edit
9 lines

Following CDISC ADaM standards, use the provided ADSL, DS, and QS datasets to create an ADTTE dataset with one record per subject.

The output dataset should contain the following columns in this order: STUDYID, USUBJID, PARAMCD, PARAM, STARTDT, ADT, AVAL, CNSR, EVNTDESC, CNSDTDSC, SRCDOM, SRCVAR, SRCSEQ

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

yamaa code

Public calls generated from benchmark.yaml; results must match the expected output

R

Edit
3 lines
library(yamaa)yamaa_spec("spec.yaml")$run()$output

Python

Edit
3 lines
import yamaayamaa.spec("spec.yaml").run().output

Solution without yamaa

Written from the full prompt and the inputs alone; CI checks it reproduces the expected output

solutions/adam-adtte-pro-deterioration/result.R

Edit
153 lines
# Reference solution for the yamaa benchmark adam-adtte-pro-deterioration# (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(lubridate, warn.conflicts = FALSE)whole_months <- function(d1, d2) {  m <- (year(d2) - year(d1)) * 12 + (month(d2) - month(d1))  last <- days_in_month(d2)  m - as.integer(day(d2) < day(d1) & day(d2) != last)}adsl <- read_csv(  "/app/input/adsl.csv",  col_types = cols(    RANDDT = col_date(), DTHDT = col_date(), .default = col_character()  ))qs <- read_csv(  "/app/input/qs.csv",  col_types = cols(    QSSEQ = col_integer(), ADT = col_date(), AVAL = col_integer(),    .default = col_character()  ))ds <- read_csv(  "/app/input/ds.csv",  col_types = cols(    DSSEQ = col_integer(), DSDTC = col_date(), .default = col_character()  ))# Flag deteriorations as an internal step: the baseline is the earliest# dated assessment with a score (a scoreless row cannot be the baseline:# its null score would leave no later row able to meet the cutoff), and# an assessment deteriorates when it is dated after the baseline and its# score is at least 10 points below the baseline score.baseline <- qs |>  filter(!is.na(ADT), !is.na(AVAL)) |>  arrange(STUDYID, USUBJID, ADT, QSSEQ) |>  distinct(STUDYID, USUBJID, .keep_all = TRUE) |>  select(STUDYID, USUBJID, BASEDT = ADT, BASEVAL = AVAL)qs_flagged <- qs |>  left_join(baseline, by = c("STUDYID", "USUBJID")) |>  mutate(    DETERFL = if_else(      !is.na(ADT) & !is.na(BASEDT) & ADT > BASEDT & !is.na(AVAL) &        AVAL <= BASEVAL - 10L,      "Y",      NA_character_    )  ) |>  select(STUDYID, USUBJID, QSSEQ, AVISIT, ADT, AVAL, DETERFL) |>  arrange(USUBJID, QSSEQ)# The first deterioration per subject, earliest date then lowest sequence.deter <- qs_flagged |>  filter(DETERFL == "Y") |>  arrange(STUDYID, USUBJID, ADT, QSSEQ) |>  distinct(STUDYID, USUBJID, .keep_all = TRUE) |>  select(STUDYID, USUBJID, DETERDT = ADT, DETERSEQ = QSSEQ)# The first censoring reason per subject. Only progression,# discontinuation, and withdrawal count; any other DS record is ignored.reason <- ds |>  filter(    DSDECOD %in% c(      "DISEASE PROGRESSION", "STUDY DISCONTINUATION", "WITHDRAWAL OF CONSENT"    )  ) |>  arrange(STUDYID, USUBJID, DSDTC, DSSEQ) |>  distinct(STUDYID, USUBJID, .keep_all = TRUE) |>  select(STUDYID, USUBJID, CENSORRSNDT = DSDTC, CENSORRSN = DSDECOD)# The latest assessment with a score dated on or before the censoring# reason, with the latest sequence breaking a tied date; a scoreless row# is not an evaluable assessment and cannot set the censoring date.censor_qs <- qs_flagged |>  inner_join(reason, by = c("STUDYID", "USUBJID")) |>  filter(!is.na(ADT), !is.na(AVAL), ADT <= CENSORRSNDT) |>  group_by(STUDYID, USUBJID) |>  summarize(    LASTQSLE = max(ADT),    LASTQSSEQ = max(QSSEQ[ADT == max(ADT)]),    .groups = "drop"  )adtte <- adsl |>  left_join(deter, by = c("STUDYID", "USUBJID")) |>  left_join(reason, by = c("STUDYID", "USUBJID")) |>  left_join(censor_qs, by = c("STUDYID", "USUBJID")) |>  mutate(    STARTDT = RANDDT,    # Deterioration wins over death on the same date. Neither counts when    # it falls after the first censoring reason: progression,    # discontinuation, and withdrawal are censoring reasons, never events,    # though a same-day deterioration or death still counts.    DETER_EVENT = !is.na(DETERDT) & (is.na(DTHDT) | DETERDT <= DTHDT) &      (is.na(CENSORRSNDT) | DETERDT <= CENSORRSNDT),    DEATH_EVENT = !is.na(DTHDT) &      (is.na(CENSORRSNDT) | DTHDT <= CENSORRSNDT),    EVENTDT = case_when(      DETER_EVENT ~ DETERDT,      DEATH_EVENT ~ DTHDT,      .default = as.Date(NA)    ),    CENSORDT = coalesce(LASTQSLE, STARTDT),    ADT = coalesce(EVENTDT, CENSORDT),    AVAL = whole_months(STARTDT, ADT),    CNSR = if_else(!is.na(EVENTDT), 0L, 1L),    EVNTDESC = case_when(      DETER_EVENT ~ "PRO DETERIORATION",      DEATH_EVENT ~ "DEATH",      .default = "CENSORED"    ),    CNSDTDSC = case_when(      !is.na(EVENTDT) ~ NA_character_,      .default = coalesce(CENSORRSN, "STUDY COMPLETION")    ),    SRCDOM = case_when(      DETER_EVENT ~ "QS",      DEATH_EVENT ~ "ADSL",      is.na(LASTQSLE) ~ "ADSL",      .default = "QS"    ),    SRCVAR = case_when(      DETER_EVENT ~ "ADT",      DEATH_EVENT ~ "DTHDT",      is.na(LASTQSLE) ~ "RANDDT",      .default = "ADT"    ),    SRCSEQ = case_when(      DETER_EVENT ~ DETERSEQ,      is.na(EVENTDT) & !is.na(LASTQSLE) ~ LASTQSSEQ,      .default = NA_integer_    ),    PARAMCD = "TTDGHS",    PARAM = "Time to Deterioration in Global Health Status"  ) |>  select(    STUDYID, USUBJID, PARAMCD, PARAM, STARTDT, ADT, AVAL, CNSR,    EVNTDESC, CNSDTDSC, SRCDOM, SRCVAR, SRCSEQ  ) |>  arrange(USUBJID)dir.create("/app/output", showWarnings = FALSE)write_csv(adtte, "/app/output/adtte.csv", na = "")

solutions/adam-adtte-pro-deterioration/result.py

Edit
218 lines
# Reference solution for the yamaa benchmark adam-adtte-pro-deterioration (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."""Create the ADTTE dataset (time to deterioration in global health status).Reads ADSL, DS and QS from /app/input and writes /app/output/adtte.csvwith one record per subject."""import csvimport osfrom datetime import dateINPUT_DIR = "/app/input"OUTPUT_DIR = "/app/output"CENSOR_REASONS = {    "DISEASE PROGRESSION",    "STUDY DISCONTINUATION",    "WITHDRAWAL OF CONSENT",}def parse_date(text):    text = (text or "").strip()    if not text:        return None    return date.fromisoformat(text[:10])def fmt(d):    return d.isoformat() if d is not None else ""def whole_months(start, end):    """Whole calendar months from start to end."""    months = (end.year - start.year) * 12 + (end.month - start.month)    if end.day < start.day:        months -= 1    return monthsdef read_csv(name):    path = os.path.join(INPUT_DIR, name)    with open(path, newline="", encoding="utf-8") as handle:        return list(csv.DictReader(handle))def to_int(text, default=0):    try:        return int(float(text))    except (TypeError, ValueError):        return defaultdef main():    adsl = read_csv("adsl.csv")    ds = read_csv("ds.csv")    qs = read_csv("qs.csv")    ds_by_subject = {}    for row in ds:        ds_by_subject.setdefault(row["USUBJID"], []).append(row)    qs_by_subject = {}    for row in qs:        qs_by_subject.setdefault(row["USUBJID"], []).append(row)    out_rows = []    for subj in adsl:        usubjid = subj["USUBJID"]        randdt = parse_date(subj.get("RANDDT"))        dthdt = parse_date(subj.get("DTHDT"))        # Assessments with both a date and a score, in date/sequence order.        assessments = []        for row in qs_by_subject.get(usubjid, []):            adt = parse_date(row.get("ADT"))            score_text = (row.get("AVAL") or "").strip()            if adt is None or not score_text:                continue            assessments.append(                {                    "date": adt,                    "score": float(score_text),                    "seq": to_int(row.get("QSSEQ")),                }            )        assessments.sort(key=lambda a: (a["date"], a["seq"]))        # Deterioration: an assessment dated after the earliest assessment        # whose score has fallen at least 10 points below that earliest        # assessment's score; the earliest such assessment gives the date.        deterioration = None        if assessments:            baseline = assessments[0]            for a in assessments[1:]:                if a["date"] > baseline["date"] and (                    baseline["score"] - a["score"] >= 10                ):                    deterioration = a                    break        # First censoring reason: earliest disposition record naming one of        # the censoring reasons (never an event).        censor_date = None        censor_reason = ""        records = []        for row in ds_by_subject.get(usubjid, []):            if (row.get("DSDECOD") or "").strip() in CENSOR_REASONS:                records.append(                    {                        "date": parse_date(row.get("DSDTC")),                        "seq": to_int(row.get("DSSEQ")),                        "reason": (row.get("DSDECOD") or "").strip(),                    }                )        records.sort(            key=lambda r: (r["date"] is None, r["date"] or date.min, r["seq"])        )        if records:            censor_date = records[0]["date"]            censor_reason = records[0]["reason"]        def not_after_censor(d):            return d is not None and (censor_date is None or d <= censor_date)        # Event selection: deterioration and death compete on date, with a        # deterioration winning a same-date tie; each must not be dated        # after the first censoring reason.        event = None  # (kind, date, seq)        candidates = []        if deterioration is not None and not_after_censor(deterioration["date"]):            candidates.append(("deterioration", deterioration["date"], 0))        if dthdt is not None and not_after_censor(dthdt):            candidates.append(("death", dthdt, 1))        if candidates:            candidates.sort(key=lambda c: (c[1], c[2]))            kind, event_date, _ = candidates[0]            if kind == "deterioration":                event = ("deterioration", event_date, deterioration["seq"])            else:                event = ("death", event_date, None)        if event is not None:            kind, adt, seq = event            cnsr = 0            cnsdtdsc = ""            if kind == "deterioration":                evntdesc = "PRO DETERIORATION"                srcdom, srcvar, srcseq = "QS", "ADT", str(seq)            else:                evntdesc = "DEATH"                srcdom, srcvar, srcseq = "ADSL", "DTHDT", ""        else:            cnsr = 1            evntdesc = "CENSORED"            cnsdtdsc = censor_reason if censor_reason else "STUDY COMPLETION"            qualifying = [                a                for a in assessments                if censor_date is None or a["date"] <= censor_date            ]            if qualifying:                last = max(qualifying, key=lambda a: (a["date"], a["seq"]))                adt = last["date"]                srcdom, srcvar, srcseq = "QS", "ADT", str(last["seq"])            else:                adt = randdt                srcdom, srcvar, srcseq = "ADSL", "RANDDT", ""        out_rows.append(            {                "STUDYID": subj["STUDYID"],                "USUBJID": usubjid,                "PARAMCD": "TTDGHS",                "PARAM": "Time to Deterioration in Global Health Status",                "STARTDT": fmt(randdt),                "ADT": fmt(adt),                "AVAL": str(whole_months(randdt, adt)),                "CNSR": str(cnsr),                "EVNTDESC": evntdesc,                "CNSDTDSC": cnsdtdsc,                "SRCDOM": srcdom,                "SRCVAR": srcvar,                "SRCSEQ": srcseq,            }        )    fieldnames = [        "STUDYID",        "USUBJID",        "PARAMCD",        "PARAM",        "STARTDT",        "ADT",        "AVAL",        "CNSR",        "EVNTDESC",        "CNSDTDSC",        "SRCDOM",        "SRCVAR",        "SRCSEQ",    ]    os.makedirs(OUTPUT_DIR, exist_ok=True)    with open(        os.path.join(OUTPUT_DIR, "adtte.csv"), "w", newline="", encoding="utf-8"    ) as handle:        writer = csv.DictWriter(handle, fieldnames=fieldnames)        writer.writeheader()        writer.writerows(out_rows)if __name__ == "__main__":    main()

solutions/adam-adtte-pro-deterioration/result.sas

Edit
60 lines
/* opensas reference solution for the yamaa benchmark adam-adtte-pro-deterioration.   Reads the benchmark inputs and derives the requested output.   Execute with the existing opensas runtime: opensas /app/output/result.sas. */data adsl;  length STUDYID USUBJID RANDDT DTHDT $1024;  infile "/app/input/adsl.csv" dsd dlm="," firstobs=2 truncover lrecl=32767;  input STUDYID : $char1024. USUBJID : $char1024. RANDDT : $char1024. DTHDT : $char1024.;run;data ds;  length STUDYID USUBJID DSDECOD DSDTC $1024;  infile "/app/input/ds.csv" dsd dlm="," firstobs=2 truncover lrecl=32767;  input STUDYID : $char1024. USUBJID : $char1024. DSDECOD : $char1024. DSDTC : $char1024. DSSEQ;run;data qs;  length STUDYID USUBJID AVISIT ADT $1024;  infile "/app/input/qs.csv" dsd dlm="," firstobs=2 truncover lrecl=32767;  input STUDYID : $char1024. USUBJID : $char1024. QSSEQ AVISIT : $char1024. ADT : $char1024. AVAL;run;proc sort data=qs(where=(ADT ne '')) out=ordered; by STUDYID USUBJID ADT QSSEQ; run;data deteriorations;  length BASEDT $10;  retain BASEDT BASEVAL;  set ordered; by STUDYID USUBJID;  /* The baseline is the earliest dated assessment with a score; a scoreless     row cannot be the baseline. */  if first.USUBJID then do; BASEDT=''; BASEVAL=.; end;  if missing(BASEDT) and not missing(AVAL) then do; BASEDT=ADT; BASEVAL=AVAL; end;  if not missing(BASEDT) and ADT>BASEDT and not missing(AVAL) and AVAL<=BASEVAL-10;run;data deterioration; set deteriorations; by STUDYID USUBJID; if first.USUBJID; keep STUDYID USUBJID ADT QSSEQ; rename ADT=DETERDT QSSEQ=DETERSEQ; run;data ds_order; set ds; NODATE=missing(DSDTC); run;proc sort data=ds_order(where=(DSDECOD in ('DISEASE PROGRESSION','STUDY DISCONTINUATION','WITHDRAWAL OF CONSENT'))) out=reasons; by STUDYID USUBJID NODATE DSDTC DSSEQ; run;data reason; set reasons; by STUDYID USUBJID; if first.USUBJID; keep STUDYID USUBJID DSDTC DSDECOD; rename DSDTC=REASONDT DSDECOD=REASON; run;proc sql; create table assessment_reasons as select a.*,b.REASONDT from ordered as a left join reason as b on a.STUDYID=b.STUDYID and a.USUBJID=b.USUBJID; quit;data candidates; set assessment_reasons; if not missing(AVAL) and (missing(REASONDT) or ADT<=REASONDT); run;proc sort data=candidates; by STUDYID USUBJID descending ADT descending QSSEQ; run;data lastassessment; set candidates; by STUDYID USUBJID; if first.USUBJID; keep STUDYID USUBJID ADT QSSEQ; rename ADT=LASTDT QSSEQ=LASTSEQ; run;proc sql; create table joined as select a.*,b.DETERDT,b.DETERSEQ,c.REASONDT,c.REASON,d.LASTDT,d.LASTSEQ from adsl as a  left join deterioration as b on a.STUDYID=b.STUDYID and a.USUBJID=b.USUBJID left join reason as c on a.STUDYID=c.STUDYID and a.USUBJID=c.USUBJID left join lastassessment as d on a.STUDYID=d.STUDYID and a.USUBJID=d.USUBJID; quit;data result;  length PARAMCD $8 PARAM $60 STARTDT ADT $10 EVNTDESC CNSDTDSC $40 SRCDOM $4 SRCVAR $16;  set joined; PARAMCD='TTDGHS'; PARAM='Time to Deterioration in Global Health Status'; STARTDT=RANDDT;  if not missing(DETERDT) and (missing(DTHDT) or DETERDT<=DTHDT) and (missing(REASONDT) or DETERDT<=REASONDT) then do; ADT=DETERDT; CNSR=0; EVNTDESC='PRO DETERIORATION'; SRCDOM='QS'; SRCVAR='ADT'; SRCSEQ=DETERSEQ; end;  else if not missing(DTHDT) and (missing(REASONDT) or DTHDT<=REASONDT) then do; ADT=DTHDT; CNSR=0; EVNTDESC='DEATH'; SRCDOM='ADSL'; SRCVAR='DTHDT'; end;  else do;    CNSR=1; EVNTDESC='CENSORED'; CNSDTDSC=coalescec(REASON,'STUDY COMPLETION');    if not missing(LASTDT) then do; ADT=LASTDT; SRCDOM='QS'; SRCVAR='ADT'; SRCSEQ=LASTSEQ; end;    else do; ADT=RANDDT; SRCDOM='ADSL'; SRCVAR='RANDDT'; end;  end;  if not missing(STARTDT) and not missing(ADT) then AVAL=intck('month',input(STARTDT,yymmdd10.),input(ADT,yymmdd10.),'continuous');run;proc sql;  create table final as select STUDYID, USUBJID, PARAMCD, PARAM, STARTDT, ADT, AVAL, CNSR, EVNTDESC, CNSDTDSC, SRCDOM, SRCVAR, SRCSEQ from result;quit;proc export data=final outfile="/app/output/adtte.csv" dbms=csv replace; run;