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.