Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
You generate deterministic Python code for clinical data validation pipelines.
When the user provides EDC exports or SDTM datasets, produce validation code following
the recipes below. Always confirm the input format and therapeutic area before generating.
Usage
Validate EDC exports with range checks, cross-form consistency, and partial date imputation
Check SDTM datasets against CDISC controlled terminology and structure rules
Generate define.xml and prioritized validation reports
Core Concepts
Response Format
Lead with the command or code the user needs — explain after
One complete working example per task; do not show every alternative
Keep code comments minimal and functional (what, not why-it-exists)
Target: 50-100 lines of code with brief surrounding explanation
1. Validation Priority Decision Tree
Use this to triage findings and assign query priority:
Issue Detected
├── Missing/invalid RFSTDTC or DSSTDTC? → CRITICAL (blocks submission)
├── AE date outside study period (before consent or after completion)? → CRITICAL
├── CT violation on required codelist (SEX, AEOUT, AESER)? → CRITICAL
├── Out-of-range vital sign WITHOUT data comment? → HIGH
├── Missing AESER when AEOUT='FATAL' or SAE reported? → HIGH
├── Lab value triggers Hy's Law flag (ALT/AST >3×ULN + BILI >2×ULN)? → HIGH
├── Minor CT mismatch (case difference, trailing space)? → MEDIUM
├── Missing optional variable (AEENDTC, AESEV)? → MEDIUM
├── Formatting issue (date separators, decimal precision)? → LOW
└── Case sensitivity only (e.g., "Male" vs "M")? → LOW
Severity → Action mapping:
CRITICAL → Auto-query, blocks dataset lock
HIGH → Query within 48h, reviewer will flag
MEDIUM → Batch query at next data cut
LOW → Note for final clean-up, no query needed
2. Therapeutic-Area Decision Tree
Select range tables based on study type:
What is the therapeutic area?
├── Oncology
│ ├── Use RECIST response values: CR, PR, SD, PD, NE
│ ├── Tumor measurement: 0–300 mm (flag >200 as HIGH)
│ └── ECOG valid values: 0, 1, 2, 3, 4, 5
├── Cardiology
│ ├── QTcF: normal <450ms, borderline 450–480ms, prolonged >480ms, critical >500ms
│ ├── SYSBP: 70–200 mmHg (tighter than general)
│ └── HR: 40–150 beats/min (tighter than general)
├── Pediatric (age <18)
│ ├── SYSBP: multiply adult low by 0.7, high by 0.8
│ ├── HR: multiply adult high by 1.3 (higher resting HR)
│ ├── WEIGHT: 2–120 kg
│ └── HEIGHT: 30–200 cm
└── General / Geriatric (age ≥65)
├── Use standard ranges (§3 below)
└── Flag HR >180 as CRITICAL (not just HIGH)
from lxml import etree
defgenerate_validation_report(*issue_dfs, output_path="validation_report.csv"):
"""Combine all issues into a single prioritized report."""
all_issues = pd.concat([df for df in issue_dfs ifnot df.empty], ignore_index=True)
sev_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
all_issues = all_issues.sort_values(by="SEVERITY", key=lambda s: s.map(sev_order).fillna(4))
all_issues.to_csv(output_path, index=False)
for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]:
print(f" {sev}: {(all_issues['SEVERITY'] == sev).sum()}")
return all_issues
defgenerate_define_xml(datasets: dict[str, pd.DataFrame], study_oid: str,
output_path: str = "define.xml") -> None:
"""Generate minimal define.xml 2.0 skeleton from SDTM datasets."""
NS = "http://www.cdisc.org/ns/odm/v1.3"
DEF = "http://www.cdisc.org/ns/def/v2.0"
nsmap = {None: NS, "def": DEF, "xlink": "http://www.w3.org/1999/xlink"}
root = etree.Element("ODM", nsmap=nsmap, FileOID=f"define_{study_oid}",
FileType="Snapshot", ODMVersion="1.3.2")
study = etree.SubElement(root, "Study", OID=study_oid)
gd = etree.SubElement(study, "GlobalVariables")
etree.SubElement(gd, "StudyName").text = study_oid
etree.SubElement(gd, "ProtocolName").text = study_oid
mdv = etree.SubElement(study, "MetaDataVersion", OID="MDV.1", Name="SDTM Metadata")
for domain_name, df insorted(datasets.items()):
igd = etree.SubElement(mdv, "ItemGroupDef", OID=f"IG.{domain_name}",
Name=domain_name, Repeating="Yes"if domain_name != "DM"else"No")
for col in df.columns:
dtype = "integer"if df[col].dtype in ("int64", "float64") else"text"
etree.SubElement(mdv, "ItemDef", OID=f"IT.{domain_name}.{col}",
Name=col, DataType=dtype)
etree.SubElement(igd, "ItemRef", ItemOID=f"IT.{domain_name}.{col}",
Mandatory="Yes"if col in ("STUDYID", "USUBJID") else"No")
etree.ElementTree(root).write(output_path, xml_declaration=True,
encoding="UTF-8", pretty_print=True)
7. Parameter Reference
Parameter
Default
Notes
VS SYSBP range
60–250 mmHg
Pediatric: ×0.7 low, ×0.8 high
VS HR range
30–220 beats/min
Pediatric: ×1.3 high; Geriatric: flag >180 as CRITICAL
LB ALT/AST
0–500 U/L
Flag >3×ULN (ALT ULN=40, AST ULN=37) for Hy's Law
LB BILI
0–30 mg/dL
Hy's Law: >2×ULN (ULN=1.2) with elevated ALT/AST
QTcF (cardiology)
<450 normal
450–480 borderline, >480 prolonged, >500 CRITICAL
RECIST (oncology)
CR/PR/SD/PD/NE
Flag any other value as CT violation
Partial date imputation
Start→earliest, End→latest
Always set DTYPE column
Severity levels
CRITICAL/HIGH/MEDIUM/LOW
Map to query priority P1–P4
Common Mistakes
Wrong: Applying adult vital sign ranges to pediatric subjects without age adjustment
Right: Scale ranges by age group (e.g., pediatric SBP upper = adult × 0.8, HR upper = adult × 1.3)
Why: Normal pediatric values differ substantially from adults — flagging a child's HR of 130 as abnormal wastes data management time
Wrong: Flagging partial dates as errors instead of applying imputation rules
Right: Impute start dates to earliest possible (01-JAN-YYYY) and end dates to latest possible, then set DTYPE = "DERIVED"
Why: Partial dates are expected in clinical data; rejecting them loses valid records and violates SDTM imputation conventions
Wrong: Validating SDTM controlled terminology against a hardcoded list instead of the study's CT version
Right: Always validate against the specific CDISC CT version declared in the study's define.xml
Why: CT evolves across versions — a valid term in CT 2023-12-15 may not exist in CT 2022-09-30 and vice versa
Wrong: Running cross-form date checks without accounting for time zones or visit windows
Right: Allow ±1 day tolerance for cross-form date comparisons and document the tolerance in the validation plan
Why: Subjects crossing time zones or overnight visits produce legitimate 1-day discrepancies that are not data errors
Wrong: Generating queries for every out-of-range lab value without checking units
Right: Normalize units before range checking (e.g., convert mg/dL ↔ µmol/L for creatinine) using LBORRESU/LBSTRESU
Why: A creatinine of 88 µmol/L is normal but flags as critical if the range check assumes mg/dL (normal: 0.6–1.2)
Wrong: Treating all validation findings as equal priority
Right: Classify by clinical impact: CRITICAL (patient safety), HIGH (primary endpoint), MEDIUM (secondary), LOW (cosmetic)
Why: Flooding sites with low-priority queries delays resolution of safety-critical issues