Skip to content

Repository files navigation

DSX Data Scientist Coding Assessment

A six-question coding assessment covering R package development, CDISC data standards (SDTM/ADaM), clinical data visualisation, REST API development, and LLM-powered data querying. Questions 1–4 are implemented in R; questions 5–6 are implemented in Python.


Table of Contents

  1. Repository Structure
  2. Setup & Installation
  3. Question 1 – R Package: Descriptive Statistics
  4. Question 2 – SDTM DS Domain Creation
  5. Question 3 – ADaM ADSL Dataset Creation
  6. Question 4 – Tables, Listings & Graphs (TLG)
  7. Question 5 – Clinical Data REST API (FastAPI)
  8. Question 6 – GenAI Clinical Data Assistant
  9. Testing
  10. Dependencies

Repository Structure

online_assessment/
├── README.md                              # This file
├── pyproject.toml                         # Python project config (UV package manager)
├── uv.lock                                # Locked Python dependency versions
├── install.cmd                            # Windows one-step install script
├── .env                                   # API keys for question 6 (not committed)
├── .gitignore
│
├── question_1/descriptiveStats/           # R package
├── question_2_sdtm/                       # SDTM DS domain script
├── question_3_adam/                       # ADaM ADSL script
├── question_4_tlg/                        # TLG output scripts
├── question_5_python/                     # FastAPI clinical data service
└── question_6_genai/                      # LangChain-powered NLP agent

Setup & Installation

Python (Questions 5 & 6)

The project uses UV as the package manager.

# Windows – run the provided install script
install.cmd

# Or manually with UV
pip install uv
uv sync

Optional – LLM API keys for Question 6 (add to .env):

ANTHROPIC_API_KEY=...   # Uses Claude Haiku
OPENAI_API_KEY=...      # Uses GPT-4o-mini
GROQ_API_KEY=...        # Uses Llama-3.1-8b (free tier)

If no key is provided, Question 6 falls back to a rule-based mock implementation.

R (Questions 1–4)

All required packages are loaded inline. Install them from an R session:

install.packages(c("devtools", "testthat", "dplyr", "lubridate", "stringr",
                   "sdtm.oak", "admiral", "pharmaverseraw", "pharmaversesdtm",
                   "pharmaverseadam", "gtsummary", "ggplot2"))

Question 1 – R Package: Descriptive Statistics

Folder: question_1/descriptiveStats/

A fully-structured R package providing core descriptive statistics functions built from scratch (no stats package shortcuts).

Package Structure

descriptiveStats/
├── DESCRIPTION              # Package metadata, version 0.1.0, MIT licence
├── NAMESPACE                # Auto-generated by Roxygen2 (6 exported functions)
├── LICENSE
├── README.md                # Usage guide with examples
├── R/
│   ├── utils.R              # Internal validate_numeric_vector() helper
│   ├── calc_mean.R          # Arithmetic mean
│   ├── calc_median.R        # Median
│   ├── calc_mode.R          # Mode (handles ties and no-repeat cases)
│   └── calc_quartiles.R     # Q1, Q3, IQR using Type 7 interpolation
├── man/                     # Roxygen2-generated .Rd documentation
└── tests/testthat/
    ├── test-calc_mean.R
    ├── test-calc_median.R
    ├── test-calc_mode.R
    └── test-calc_quartiles.R

Exported Functions

Function Description
calc_mean(x, na.rm) Arithmetic mean
calc_median(x, na.rm) Median
calc_mode(x, na.rm) Mode; returns all tied values, or all values if no repeats
calc_q1(x, na.rm) First quartile (Type 7 interpolation)
calc_q3(x, na.rm) Third quartile (Type 7 interpolation)
calc_iqr(x, na.rm) Interquartile range (Q3 − Q1)

All functions share a common validate_numeric_vector() guard that rejects non-numeric input and empty vectors early with descriptive error messages.

Install & Run

devtools::install("question_1/descriptiveStats")
library(descriptiveStats)
calc_mean(c(1, 2, 3, NA), na.rm = TRUE)  # 2
calc_mode(c(1, 1, 2, 3))                 # 1

Question 2 – SDTM DS Domain Creation

Folder: question_2_sdtm/

Creates a CDISC-compliant SDTM Disposition (DS) domain from raw source data.

Contents

question_2_sdtm/
├── 02_create_ds_domain.R    # Main transformation script (313 lines)
└── sdtm_ct.csv              # Controlled terminology codelist

What It Does

  • Input: pharmaverseraw::ds_raw (850 rows × 13 columns of raw disposition data)
  • Output: SDTM DS domain with the following variables:
Variable Description
STUDYID Study identifier
DOMAIN Domain abbreviation ("DS")
USUBJID Unique subject identifier
DSSEQ Sequence number
DSTERM Verbatim disposition term
DSDECOD Controlled terminology decoded value
DSCAT Category of disposition event
VISITNUM Visit number
VISIT Visit name
DSDTC Date/time of disposition event (ISO 8601)
DSSTDTC Start date/time
DSSDY Study day of disposition event
  • Uses the sdtm.oak package mapping functions to apply controlled terminology from sdtm_ct.csv.
  • The codelist CSV contains columns: codelist_code, term_code, term_value, collected_value, term_preferred_term, term_synonyms.

Run

source("question_2_sdtm/02_create_ds_domain.R")

Known Controlled Terminology Mapping Gaps

When the script runs, sdtm.oak's assign_ct() emits informational messages for raw values that could not be matched to a collected_value entry in sdtm_ct.csv. These are not errors — unmatched records receive NA for the target variable, which is acceptable SDTM behaviour. The gaps fall into two categories:

1. DSDECOD — codelist C66727

Raw value in IT.DSDECOD Root cause
"Randomized" Not a C66727 disposition term. "Randomized" is a protocol milestone; it is handled by DSCAT = "PROTOCOL MILESTONE" via hardcode_no_ct() rather than a CT lookup.
"Completed" Case/text mismatch: CT collected_value is "Complete", not "Completed".
"Screen Failure" Text mismatch: CT collected_value is "Trial Screen Failure".
"Study Terminated by Sponsor" Case mismatch: CT collected_value is "Study Terminated By Sponsor" (capital B).
"Lost to Follow-Up" Case mismatch: CT collected_value is "Lost To Follow-Up" (capital T).

The mismatches arise because the raw eCRF values in pharmaverseraw::ds_raw were not aligned with the collected_value column in the study's sdtm_ct.csv. In a real study, the CT file would be updated to match the exact strings collected on the eCRF, or the raw values would be pre-normalised before mapping.

2. VISIT / VISITNUM — study codelists

Raw value in INSTANCE Root cause
"Ambul Ecg Removal" Case mismatch: CT collected_value is "Ambul ECG Removal" (uppercase ECG).
"Unscheduled 1.1", "4.1", "5.1", "6.1", "8.2", "13.1" These unscheduled visit labels are absent from the VISIT and VISITNUM codelists entirely. Only "Unscheduled 3.1" was added to the CT.

Unscheduled visits routinely receive NA for VISITNUM and VISIT in SDTM — this is common practice since unscheduled visits are not pre-defined in the protocol schedule. They are still retained in the dataset via DSTERM and DSDTC.


Question 3 – ADaM ADSL Dataset Creation

Folder: question_3_adam/

Derives a CDISC-compliant ADaM Subject-Level Analysis Dataset (ADSL).

Contents

question_3_adam/
└── create_adsl.R    # Main derivation script (307 lines)

What It Does

  • Input: SDTM source domains — DM, VS, EX, DS, AE (from pharmaversesdtm)
  • Output: ADSL dataset with standard and custom derived variables

Key derivations:

Variable Description
AGEGR9 Age group: "<18", "18-50", ">50"
AGEGR9N Numeric age group code
TRTSDTM Treatment start datetime
TRTSTMF Treatment start time imputation flag
TRTEDTM Treatment end datetime
ITTFL Intent-to-treat population flag
ABNSBPFL Abnormal systolic blood pressure flag
LSTALVDT Last known alive date (derived from VS, AE, DS, and treatment dates)
CARPOPFL Carpool population flag

Uses the admiral package for standardised ADaM derivation functions.

Run

source("question_3_adam/create_adsl.R")

Question 4 – Tables, Listings & Graphs (TLG)

Folder: question_4_tlg/

Produces analysis-ready tables, listings, and visualisations for adverse event (AE) data.

Contents

question_4_tlg/
├── 01_create_ae_summary_table.R    # Hierarchical AE summary table
├── 02_create_visualizations.R      # AE bar chart & forest plot
├── 03_create_listings.R            # Detailed AE patient listing
│
├── ae_summary_table.html           # Output: summary table (78 KB)
├── ae_listings.html                # Output: patient listings (530 KB)
├── ae_severity_bar.png             # Output: severity bar chart (69 KB)
└── ae_top10_forest.png             # Output: top-10 AE forest plot (101 KB)

What Each Script Produces

01_create_ae_summary_table.R

  • Hierarchical AE count table using gtsummary::tbl_hierarchical()
  • Structured as: System Organ Class (SOC) > Preferred Term (PT)
  • Split by treatment arm (ACTARM); denominator = Safety population (SAFFL == "Y")
  • Filter: Treatment-emergent AEs only (TRTEMFL == "Y")

02_create_visualizations.R

  • AE Severity Bar Chart: Stacked bar of MILD / MODERATE / SEVERE counts per treatment arm
  • Top-10 AE Forest Plot: Displays the 10 most frequent AEs with incidence rates and confidence intervals across arms

03_create_listings.R

  • Patient-level AE listing with subject ID, preferred term, SOC, severity, seriousness, and dates
  • Formatted as a scrollable HTML table

Data source: pharmaverseadam package (adae, adsl datasets)

Run

source("question_4_tlg/01_create_ae_summary_table.R")
source("question_4_tlg/02_create_visualizations.R")
source("question_4_tlg/03_create_listings.R")

Question 5 – Clinical Data REST API (FastAPI)

Folder: question_5_python/

A RESTful API for querying and summarising clinical trial adverse event data.

Contents

question_5_python/
├── main.py              # FastAPI application (160 lines)
├── README.md            # Endpoint reference
├── adae.csv             # Adverse event dataset (1,191 rows, 1.05 MB)
└── tests/
    ├── conftest.py
    └── test_api.py      # API endpoint tests (6.6 KB)

Endpoints

Method Path Description
GET / Health check — returns service status
POST /ae-query Filter AEs by severity and/or treatment arm
GET /subject-risk/{subject_id} Compute a weighted safety risk score for a subject

Key Design Decisions

  • Pydantic models: AEQueryFilter, AEQueryResponse, RiskScoreResponse provide strict request/response validation.
  • Risk score weights: MILD = 1, MODERATE = 3, SEVERE = 5 — summed across all of a subject's AEs.
  • Optional filters: Any null field in /ae-query is ignored (partial filtering supported).
  • Data loading: adae.csv is loaded once at startup into a pandas DataFrame; all columns stored as strings for safe string-based filtering.
  • Interactive docs: available at /docs (Swagger UI) and /redoc (ReDoc) when the server is running.

Run

uv run uvicorn question_5_python.main:app --reload

Running Locally & Interactive UI

After starting the server, the following URLs are available on your local machine:

UI URL Description
Swagger UI http://127.0.0.1:8000/docs Try every endpoint directly in the browser — send requests and inspect responses without any extra tooling
ReDoc http://127.0.0.1:8000/redoc Clean, readable API reference documentation
Health check http://127.0.0.1:8000/ Confirm the server is up

These URLs are only accessible on the machine running the server. To review the API without running it locally, the endpoint behaviour and example responses are documented below.

Example Requests & Responses

GET /

curl http://127.0.0.1:8000/
{"message": "Clinical Trial Data API is running"}

POST /ae-query

curl -X POST http://127.0.0.1:8000/ae-query \
  -H "Content-Type: application/json" \
  -d '{"severity": ["SEVERE"], "treatment_arm": "Placebo"}'
{
  "record_count": 8,
  "subject_count": 7,
  "subjects": ["01-703-1175","01-704-1445","01-709-1259","01-710-1077","01-710-1083","01-710-1271","01-710-1368"]
}

GET /subject-risk/{subject_id}

curl http://127.0.0.1:8000/subject-risk/01-701-1015
{
  "subject_id": "01-701-1015",
  "risk_score": 3,
  "risk_category": "Low"
}

Question 6 – GenAI Clinical Data Assistant

Folder: question_6_genai/

A natural-language query agent that translates plain-English questions about adverse events into structured pandas filters using a LangChain LCEL chain (ChatPromptTemplate | LLM | JsonOutputParser). The LLM infers the correct dataset column and filter value from a schema embedded in the system prompt — no hard-coded field rules.

Contents

question_6_genai/
├── question_6.py    # ClinicalTrialDataAgent class + mock LLM fallback
├── chat.py          # Interactive REPL — type questions, get results live
├── test_script.py   # Runs 3 pre-defined queries and prints output
├── README.md        # Usage guide
├── requirements.txt
└── adae.csv         # AE dataset (1,191 rows — shared with Question 5)

Architecture

User question (natural language)
        │
        ▼
ChatPromptTemplate  ←── AE dataset schema + column mapping rules + few-shot examples
        │
        ▼
LLM  (auto-selected — see backend table below)
        │
        ▼
JsonOutputParser  →  AEIntent { "target_column": "AESEV", "filter_value": "MODERATE" }
        │
        ▼
pandas DataFrame filter on adae.csv  →  unique USUBJID list returned

LLM Backend Selection (automatic)

The agent reads .env at startup and picks the first available backend:

Priority .env variable Model
1 GROQ_API_KEY Llama-3.1-8b via Groq (free tier — recommended)
2 ANTHROPIC_API_KEY Claude Haiku
3 OPENAI_API_KEY GPT-4o-mini
4 (none set) Rule-based mock — no API key needed

Interactive Chat

Launch the REPL and type questions in plain English:

uv run python question_6_genai/chat.py

The session banner shows the active backend and example questions to try:

============================================================
  GenAI Clinical Data Assistant
  Dataset : adae.csv  (1,191 rows)
  Backend : groq (Llama-3.1-8b — free tier)
============================================================
  Example questions:
  - Give me subjects with moderate severity AEs
  - Which patients had cardiac disorders?
  - Show subjects in the Placebo group
  - Find serious adverse events
  Type 'quit' to exit.
============================================================

Batch Test Script

Runs 3 pre-defined queries non-interactively and prints the results:

uv run python question_6_genai/test_script.py

Programmatic Usage

from question_6_genai.question_6 import ClinicalTrialDataAgent

agent = ClinicalTrialDataAgent(
    data_path="question_5_python/adae.csv",
    use_mock=False,   # False = auto-detect LLM; True = force mock
)

result = agent.ask("Give me subjects with severe adverse events")

print(result["subject_count"])   # 12
print(result["subjects"])        # ["01-701-1015", ...]
print(result["parsed_intent"])   # {"target_column": "AESEV", "filter_value": "SEVERE"}

The returned dict always contains:

Key Type Description
question str Original question
parsed_intent dict LLM output: target_column + filter_value
subject_count int Number of unique matching subjects
subjects list[str] Sorted list of USUBJIDs

Dependencies

Python (pyproject.toml)

Package Version Used In
fastapi ≥ 0.115.0 Question 5
uvicorn ≥ 0.30.0 Question 5
pydantic ≥ 2.0.0 Questions 5 & 6
pandas ≥ 2.0.0 Questions 5 & 6
langchain-core ≥ 0.3.0 Question 6
langchain-anthropic ≥ 0.3.0 Question 6
langchain-openai ≥ 0.3.0 Question 6
langchain-groq ≥ 1.1.2 Question 6
python-dotenv ≥ 1.0.0 Question 6
pytest ≥ 8.0.0 Dev
pytest-asyncio ≥ 0.23.0 Dev

R Key Packages

Package Used In
devtools, testthat Question 1
sdtm.oak Question 2
admiral Question 3
pharmaverseraw, pharmaversesdtm, pharmaverseadam Questions 2–4
gtsummary Question 4
ggplot2, dplyr, lubridate, stringr Questions 2–4

Standards & Terminology

  • CDISC SDTM – Study Data Tabulation Model (Question 2)
  • CDISC ADaM – Analysis Data Model (Question 3)
  • MedDRA – Medical Dictionary for Regulatory Activities (Questions 2–4)
  • Pharmaverse conventions for clinical trial data pipelines

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages