Synthetic EHR Data Generation using GANs
Introduction
Sharing Electronic Health Records (EHR) for research is severely restricted due to patient privacy laws. One emerging solution is to generate high-fidelity synthetic EHR data that mirrors the statistical properties of real patient cohorts without exposing any actual Protected Health Information (PHI). In this tutorial, we will train a Generative Adversarial Network (GAN) to synthesize structured tabular patient data (demographics, labs, and diagnoses).
Architecture Overview
graph LR
Real(Real EHR Data) --> Pre(Preprocessing / One-Hot)
Pre --> Disc(Discriminator)
Noise(Latent Noise Z) --> Gen(Generator)
Gen --> Fake(Fake EHR Data)
Fake --> Disc
Disc --> Loss(Minimax Loss)
Loss --> Gen
Fake --> Eval(ML Efficacy / TSNE Eval)
Prerequisites
- Python 3.10+
- PyTorch, Pandas, Scikit-learn, UMAP-learn
- A sample de-identified EHR dataset (e.g., Synthea or MIMIC-III Demo)
- A GPU is highly recommended
Step 1: Data Preprocessing
Tabular EHR data contains a mix of continuous variables (e.g., lab values, age) and categorical variables (e.g., ICD-10 codes, gender). We must normalize continuous variables and one-hot encode categorical variables before feeding them to a neural network.
import pandas as pd
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
# Load real patient data
df = pd.read_csv('real_patient_cohort.csv')
continuous_cols = ['age', 'heart_rate', 'systolic_bp', 'creatinine']
categorical_cols = ['gender', 'primary_diagnosis_code', 'discharge_status']
preprocessor = ColumnTransformer(
transformers=[
('num', StandardScaler(), continuous_cols),
('cat', OneHotEncoder(sparse_output=False), categorical_cols)
]
)
real_data_encoded = preprocessor.fit_transform(df)
print(f"Encoded shape: {real_data_encoded.shape}")
Step 2: Defining the GAN Architecture
A standard tabular GAN consists of a Generator that takes random noise and outputs synthetic patient vectors, and a Discriminator that tries to distinguish between real and synthetic vectors.
import torch
import torch.nn as nn
class Generator(nn.Module):
def __init__(self, latent_dim, output_dim):
super().__init__()
self.model = nn.Sequential(
nn.Linear(latent_dim, 128),
nn.ReLU(),
nn.BatchNorm1d(128),
nn.Linear(128, 256),
nn.ReLU(),
nn.BatchNorm1d(256),
nn.Linear(256, output_dim),
nn.Tanh() # Assuming inputs are scaled appropriately
)
def forward(self, z):
return self.model(z)
class Discriminator(nn.Module):
def __init__(self, input_dim):
super().__init__()
self.model = nn.Sequential(
nn.Linear(input_dim, 256),
nn.LeakyReLU(0.2),
nn.Dropout(0.3),
nn.Linear(256, 128),
nn.LeakyReLU(0.2),
nn.Dropout(0.3),
nn.Linear(128, 1),
nn.Sigmoid()
)
def forward(self, x):
return self.model(x)
Step 3: Training the GAN
The Generator and Discriminator play a minimax game. The Generator tries to fool the Discriminator, while the Discriminator tries to catch the Generator.
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
latent_dim = 64
output_dim = real_data_encoded.shape[1]
netG = Generator(latent_dim, output_dim).to(device)
netD = Discriminator(output_dim).to(device)
criterion = nn.BCELoss()
optimizerD = torch.optim.Adam(netD.parameters(), lr=0.0002, betas=(0.5, 0.999))
optimizerG = torch.optim.Adam(netG.parameters(), lr=0.0002, betas=(0.5, 0.999))
real_tensor = torch.tensor(real_data_encoded, dtype=torch.float32)
dataloader = torch.utils.data.DataLoader(real_tensor, batch_size=128, shuffle=True)
for epoch in range(100):
for i, real_batch in enumerate(dataloader):
real_batch = real_batch.to(device)
batch_size = real_batch.size(0)
# 1. Train Discriminator
netD.zero_grad()
label_real = torch.ones(batch_size, 1).to(device)
output_real = netD(real_batch)
lossD_real = criterion(output_real, label_real)
noise = torch.randn(batch_size, latent_dim).to(device)
fake_batch = netG(noise)
label_fake = torch.zeros(batch_size, 1).to(device)
output_fake = netD(fake_batch.detach())
lossD_fake = criterion(output_fake, label_fake)
lossD = lossD_real + lossD_fake
lossD.backward()
optimizerD.step()
# 2. Train Generator
netG.zero_grad()
label_target = torch.ones(batch_size, 1).to(device) # G wants D to think fake is real
output_G = netD(fake_batch)
lossG = criterion(output_G, label_target)
lossG.backward()
optimizerG.step()
Step 4: Evaluating Synthetic Data Quality (UMAP & ML Efficacy)
After training, we generate synthetic patients. We evaluate quality visually using UMAP, and quantitatively using "Machine Learning Efficacy" (training a classifier on fake data, testing on real data).
import umap
import matplotlib.pyplot as plt
# Generate 1000 synthetic patients
noise = torch.randn(1000, latent_dim).to(device)
synthetic_encoded = netG(noise).detach().cpu().numpy()
# 1. Visual Evaluation (UMAP)
reducer = umap.UMAP()
real_umap = reducer.fit_transform(real_data_encoded[:1000])
fake_umap = reducer.transform(synthetic_encoded)
plt.scatter(real_umap[:, 0], real_umap[:, 1], label='Real', alpha=0.5)
plt.scatter(fake_umap[:, 0], fake_umap[:, 1], label='Synthetic', alpha=0.5)
plt.legend()
plt.title("UMAP Projection of Real vs Synthetic EHR")
plt.show()
# 2. Machine Learning Efficacy
# Train XGBoost on synthetic_encoded, evaluate on real_data_encoded...
Conclusion
Generating high-quality synthetic EHR data allows researchers and developers to iterate on healthcare algorithms openly without risking patient privacy. Advanced models like CTGAN handle categorical variables even better than standard GANs and are worth exploring for production use cases.