Notebook 1: Clinical Trial Data Generation¶
RX-7281 Phase III Trial — Synthetic Data Pipeline¶
This notebook generates a realistic clinical trial dataset for RX-7281, a novel anti-inflammatory compound being evaluated against placebo in patients with moderate-to-severe inflammatory disease.
Study Design¶
- Phase: III, randomized, double-blind, placebo-controlled
- Population: 1,200 patients across 20 sites
- Primary Endpoint: ACR20-like composite response at Week 24
- Secondary Endpoints: Progression-free survival, biomarker changes, safety
- Randomization: 1:1 (RX-7281 vs Placebo)
Data Dictionary¶
| Variable | Description | Type |
|---|---|---|
age |
Patient age in years (18-85) | Continuous |
sex |
Biological sex (M/F) | Categorical |
race |
Self-reported race | Categorical |
bmi |
Body mass index (kg/m²) | Continuous |
crp_baseline |
C-reactive protein (mg/L) | Continuous |
il6_baseline |
Interleukin-6 (pg/mL) | Continuous |
tnf_baseline |
TNF-alpha (pg/mL) | Continuous |
esr_baseline |
Erythrocyte sedimentation rate (mm/hr) | Continuous |
responder |
Met ACR20-like threshold (0/1) | Binary |
pfs_weeks |
Progression-free survival (weeks) | Time-to-event |
inflammation_index |
Composite inflammation score | Continuous |
In [ ]:
import sys
sys.path.insert(0, "../src")
import warnings
warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from clinical_eda.data_generator import generate_trial_dataset, BIOMARKER_COLS
sns.set_theme(style="whitegrid", font_scale=1.1)
print("Libraries loaded successfully")
1. Generate the Trial Dataset¶
The data generator simulates realistic clinical trial data with:
- Demographics drawn from epidemiologically plausible distributions
- Biomarkers using log-normal distributions (common for inflammatory markers)
- Treatment effect modulated by baseline inflammation — patients with high CRP/IL-6 respond better to RX-7281, creating a discoverable biomarker signature
- ~3% missing data injected into PFS to simulate real-world missingness
In [ ]:
# Generate 1,200 patients with seed=42 for reproducibility
df = generate_trial_dataset(n_patients=1200, seed=42)
print(f"Dataset shape: {df.shape}")
print(f"Columns: {list(df.columns)}")
df.head(10)
2. Data Quality Overview¶
In [ ]:
# Data types and missing values
print("=" * 60)
print("DATA TYPES & MISSING VALUES")
print("=" * 60)
info_df = pd.DataFrame({
"dtype": df.dtypes,
"non_null": df.count(),
"null_count": df.isnull().sum(),
"null_pct": (df.isnull().sum() / len(df) * 100).round(2),
})
print(info_df.to_string())
print(f"\nTotal rows: {len(df)}")
print(f"Total columns: {len(df.columns)}")
print(f"Memory usage: {df.memory_usage(deep=True).sum() / 1024:.1f} KB")
In [ ]:
# Descriptive statistics for numeric columns
df.describe().round(3)
3. Treatment Arm Balance¶
A well-randomized trial should show balanced demographics and baseline characteristics across arms.
In [ ]:
# Treatment arm distribution
arm_counts = df["treatment_arm"].value_counts()
print("Treatment Arm Distribution:")
print(arm_counts)
print(f"\nBalance ratio: {arm_counts.min() / arm_counts.max():.3f} (ideal = 1.0)")
# Quick demographic comparison
print("\n" + "=" * 60)
print("DEMOGRAPHIC SUMMARY BY ARM")
print("=" * 60)
for col in ["age", "bmi", "disease_duration_years"]:
print(f"\n{col}:")
print(df.groupby("treatment_arm")[col].describe()[["mean", "std", "min", "max"]].round(2))
4. Quick Sanity Visualizations¶
In [ ]:
fig, axes = plt.subplots(1, 3, figsize=(16, 4))
# Enrollment timeline
df["enrollment_date"].dt.date.value_counts().sort_index().cumsum().plot(
ax=axes[0], color="#2196F3", linewidth=2
)
axes[0].set_title("Cumulative Enrollment")
axes[0].set_xlabel("Date")
axes[0].set_ylabel("Patients Enrolled")
# Site distribution
df["site_id"].value_counts().sort_index().plot(
kind="bar", ax=axes[1], color="#4CAF50", alpha=0.8
)
axes[1].set_title("Patients per Site")
axes[1].set_xlabel("Site")
axes[1].tick_params(axis="x", rotation=45, labelsize=8)
# Response score distribution by arm
for arm, color in [("RX-7281", "#2196F3"), ("Placebo", "#9E9E9E")]:
subset = df[df["treatment_arm"] == arm]
axes[2].hist(subset["response_score"], bins=40, alpha=0.6, label=arm, color=color)
axes[2].set_title("Response Score Distribution")
axes[2].set_xlabel("Response Score (lower = better)")
axes[2].legend()
plt.tight_layout()
plt.show()
Summary¶
Dataset generated successfully with 1,200 patients across 20 sites. Key observations:
- Treatment arms are approximately balanced (1:1 randomization)
- Enrollment follows a steady pattern across sites
- Response score shows a clear leftward shift for RX-7281 vs Placebo, suggesting treatment efficacy
Next: Proceed to 02_exploratory_analysis.ipynb for detailed EDA with demographic breakdowns, biomarker distributions, and correlation analysis.