前言
最近在复现一篇用eICU-CRD做跨医院死亡预测的论文,把整个流程从头到尾跑了一遍。网上关于MIMIC的教程不少,但eICU的完整实战教程真的不多。这篇记录一下从数据申请到模型训练的全流程,代码可以直接跑,希望对同样在复现论文的朋友有帮助。
eICU-CRD是MIT计算生理学实验室与Philips Healthcare于2018年联合发布的多中心重症监护数据集,覆盖美国208家医院、335个ICU单元,包含200,859次ICU住院记录。它和MIMIC最大的区别是多中心——可以按医院划分训练集和测试集,做真正的跨机构泛化评估。
一、数据申请与下载
eICU托管在PhysioNet上,需要完成以下步骤才能下载:
- 注册CITI培训:去citiprogram.org注册,完成"Data or Specimens Only Research"课程(免费),拿到完成报告PDF
- 注册PhysioNet账号:physionet.org注册
- 提交凭证申请:上传CITI完成报告,填写申请表
- 签署DUA:审批通过后(通常1-3个工作日)签署数据使用协议
- 下载数据:用wget下载压缩包
# 审批通过后,PhysioNet会给你一个下载命令,大概长这样
wget -r -N -c -np --user <你的用户名> --ask-password \
https://physionet.org/files/eicu-crd/2.0/
下载下来是一个约3.6GB的压缩包,解压后是31个CSV文件。
二、数据加载与表结构概览
2.1 31张表一览
eicu-crd-2.0/
├── patient.csv # ← 核心:ICU住院主记录,所有查询的入口
├── vitalPeriodic.csv # ← 核心:5分钟间隔生命体征
├── vitalAperiodic.csv # 非周期性生命体征
├── nurseCharting.csv # 护士记录的体征
├── lab.csv # 实验室检验
├── medication.csv # 药物管理
├── diagnosis.csv # 诊断
├── admissionDx.csv # 入院诊断
├── admissionDrug.csv # 入院用药
├── allergy.csv # 过敏
├── apacheApsVar.csv # APACHE评分变量
├── apachePatientResult.csv # APACHE评分结果
├── apachePredVar.csv # APACHE预测变量
├── carePlan*.csv (5张) # 护理计划系列
├── customLab.csv # 非标准检验
├── infusionDrug.csv # 输注药物
├── intakeOutput.csv # 出入量
├── microLab.csv # 微生物培养
├── note.csv # 临床笔记
├── nurseAssessment.csv # 护理评估
├── nurseCare.csv # 护理记录
├── pastHistory.csv # 既往史
├── physicalExam.csv # 体格检查
├── respiratoryCare.csv # 呼吸管理
├── respiratoryCharting.csv # 呼吸记录
├── treatment.csv # 治疗操作
└── hospital.csv # 医院信息
实际做死亡预测,最常用的是patient表(基本信息+标签)和vitalPeriodic表(生命体征特征)。如果要做更复杂的模型,还会用到lab、medication、diagnosis等。
2.2 加载数据
import pandas as pd
import numpy as np
# 加载核心表
patient = pd.read_csv('eicu-crd-2.0/patient.csv')
vitalPeriodic = pd.read_csv('eicu-crd-2.0/vitalPeriodic.csv')
hospital = pd.read_csv('eicu-crd-2.0/hospital.csv')
print(f"patient表: {patient.shape}") # (200859, ~25)
print(f"vitalPeriodic表: {vitalPeriodic.shape}") # (~1.46亿行, ~9)
print(f"hospital表: {hospital.shape}") # (208, ~5)
⚠️ vitalPeriodic表有约1.46亿行,加载时注意内存。建议先用
nrows预览结构,再决定是否全量加载或分块处理。
三、核心表字段详解
3.1 patient表
patient表是全库的入口,所有其他表通过patientUnitStayID与它关联。
| 字段名 | 类型 | 说明 | 用途 |
|---|---|---|---|
| patientUnitStayID | int | ICU住院唯一标识 | 全库主外键 |
| uniquepid | int | 患者唯一标识 | 患者级去重、再入院追踪 |
| gender | varchar | 性别 | 公平性分析 |
| age | varchar | 年龄(如"68",“>89”) | 年龄调整建模 |
| ethnicity | varchar | 种族 | 公平性分组 |
| hospitalID | int | 去标识医院代码(1-208) | 按医院划分训练/测试集 |
| unitType | varchar | ICU类型 | 亚组分析 |
| apacheAdmissionDx | varchar | APACHE入院诊断 | 队列筛选 |
| hospitalDischargeStatus | varchar | 出院状态(Alive/Expired) | 死亡预测标签 |
| unitDischargeOffset | int | ICU出院时间偏移(分钟) | LOS计算 |
| admissionHeight | decimal | 入院身高(cm) | BMI计算 |
| admissionWeight | decimal | 入院体重(kg) | 药物剂量归一化 |
3.2 vitalPeriodic表
每5分钟一条,存监护仪自动采集的生命体征:
| 字段名 | 说明 | AI用途 |
|---|---|---|
| patientUnitStayID | 外键 | 连接键 |
| observationOffset | 观测时间(分钟偏移) | 时序建模 |
| heartrate | 心率(bpm) | 循环状态特征 |
| respRate | 呼吸频率(/min) | 呼吸状态特征 |
| sao2 | SpO₂(%) | 氧合状态特征 |
| systemicsystolic | 无创收缩压(mmHg) | 循环状态特征 |
| systemicdiastolic | 无创舒张压(mmHg) | 循环状态特征 |
| temperature | 体温(°C) | 感染/炎症特征 |
⚠️ 关键坑点:
-1表示"设备未连接/信号中断",不是零也不是正常值,处理前必须替换为NaN。
四、特征工程实战
4.1 提取死亡预测标签
# 从patient表提取标签
labels = patient[['patientUnitStayID', 'hospitalDischargeStatus', 'hospitalID']].copy()
labels['mortality'] = (labels['hospitalDischargeStatus'] == 'Expired').astype(int)
print(f"总样本数: {len(labels)}")
print(f"死亡人数: {labels['mortality'].sum()} ({labels['mortality'].mean()*100:.1f}%)")
# 死亡率约6.5%,属于不平衡分类
labels = labels.drop(columns=['hospitalDischargeStatus'])
4.2 提取前24小时生命体征特征
只取ICU入住后前24小时的数据做特征,模拟"早期预测"场景:
# ⚠️ 第一步:处理-1特殊编码,必须在做任何聚合之前
vital_cols = ['heartrate', 'systemicsystolic', 'systemicdiastolic',
'sao2', 'respRate', 'temperature']
for col in vital_cols:
vitalPeriodic[col] = vitalPeriodic[col].replace(-1, np.nan)
# 只取前24小时(1440分钟)的数据
vitals_24h = vitalPeriodic[vitalPeriodic['observationOffset'] <= 1440].copy()
# 按patientUnitStayID聚合:均值、标准差、最小值、最大值
agg_dict = {}
for col in vital_cols:
agg_dict[col] = ['mean', 'std', 'min', 'max']
vital_features = vitals_24h.groupby('patientUnitStayID').agg(agg_dict)
# 扁平化列名
vital_features.columns = [f'{col}_{stat}' for col, stat in vital_features.columns]
vital_features = vital_features.reset_index()
print(f"生命体征特征表: {vital_features.shape}")
# 大约 (200000, 25) —— 6个指标 × 4个统计量 + ID
4.3 合并特征与标签
# 合并特征和标签
data = labels.merge(vital_features, on='patientUnitStayID', how='left')
# 检查缺失率
missing_rate = data.isnull().mean()
print("各列缺失率:")
print(missing_rate[missing_rate > 0].sort_values(ascending=False).head(10))
# 用中位数填充缺失值
feature_cols = [c for c in data.columns if c not in
['patientUnitStayID', 'hospitalID', 'mortality']]
for col in feature_cols:
median_val = data[col].median()
data[col] = data[col].fillna(median_val)
print(f"\n最终数据表: {data.shape}")
五、按医院划分训练/测试集
这是eICU最重要的特性——按医院划分而非随机划分。
5.1 为什么不能随机划分
# ❌ 错误做法:随机划分(会严重高估泛化性能)
from sklearn.model_selection import train_test_split
X_train_wrong, X_test_wrong, y_train_wrong, y_test_wrong = train_test_split(
data[feature_cols], data['mortality'], test_size=0.2, random_state=42
)
# 这样划分,同一家医院的数据会同时出现在训练集和测试集中
# 模型"见过"这家医院的数据分布,测试结果偏乐观
5.2 正确做法:按医院划分
# ✅ 正确做法:按hospitalID划分
all_hospitals = data['hospitalID'].unique()
print(f"总医院数: {len(all_hospitals)}") # 208
# 按7:1:2划分医院
np.random.seed(42)
np.random.shuffle(all_hospitals)
n_train = int(len(all_hospitals) * 0.7) # ~145家
n_val = int(len(all_hospitals) * 0.15) # ~31家
# 剩余~32家做测试集
train_hospitals = all_hospitals[:n_train]
val_hospitals = all_hospitals[n_train:n_train+n_val]
test_hospitals = all_hospitals[n_train+n_val:]
train_data = data[data['hospitalID'].isin(train_hospitals)]
val_data = data[data['hospitalID'].isin(val_hospitals)]
test_data = data[data['hospitalID'].isin(test_hospitals)]
print(f"训练集: {len(train_data)} 样本, {len(train_hospitals)} 医院, 死亡率{train_data['mortality'].mean()*100:.1f}%")
print(f"验证集: {len(val_data)} 样本, {len(val_hospitals)} 医院, 死亡率{val_data['mortality'].mean()*100:.1f}%")
print(f"测试集: {len(test_data)} 样本, {len(test_hospitals)} 医院, 死亡率{test_data['mortality'].mean()*100:.1f}%")
⚠️ 关键坑点:划分后一定要检查各表在不同医院的数据覆盖率。比如
microLab表只有10.58%的医院有数据,如果你的特征用到了微生物培养结果,测试集中该特征可能全部缺失。
六、基线模型训练(PyTorch)
6.1 数据准备
import torch
from torch.utils.data import Dataset, DataLoader
from sklearn.preprocessing import StandardScaler
# 标准化(用训练集的统计量)
scaler = StandardScaler()
scaler.fit(train_data[feature_cols].values)
X_train = scaler.transform(train_data[feature_cols].values)
X_val = scaler.transform(val_data[feature_cols].values)
X_test = scaler.transform(test_data[feature_cols].values)
y_train = train_data['mortality'].values
y_val = val_data['mortality'].values
y_test = test_data['mortality'].values
class ICUDataset(Dataset):
def __init__(self, X, y):
self.X = torch.FloatTensor(X)
self.y = torch.FloatTensor(y)
def __len__(self):
return len(self.X)
def __getitem__(self, idx):
return self.X[idx], self.y[idx]
train_ds = ICUDataset(X_train, y_train)
val_ds = ICUDataset(X_val, y_val)
test_ds = ICUDataset(X_test, y_test)
train_loader = DataLoader(train_ds, batch_size=512, shuffle=True)
val_loader = DataLoader(val_ds, batch_size=512)
test_loader = DataLoader(test_ds, batch_size=512)
6.2 模型定义
import torch.nn as nn
class MortalityPredictor(nn.Module):
def __init__(self, input_dim, hidden_dim=128, dropout=0.3):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.BatchNorm1d(hidden_dim),
nn.Dropout(dropout),
nn.Linear(hidden_dim, hidden_dim // 2),
nn.ReLU(),
nn.BatchNorm1d(hidden_dim // 2),
nn.Dropout(dropout),
nn.Linear(hidden_dim // 2, 1),
nn.Sigmoid()
)
def forward(self, x):
return self.net(x).squeeze(-1)
model = MortalityPredictor(input_dim=len(feature_cols))
print(f"模型参数量: {sum(p.numel() for p in model.parameters()):,}")
6.3 训练循环
from sklearn.metrics import roc_auc_score, average_precision_score
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
# 不平衡数据:用加权BCE Loss
pos_weight = torch.tensor([(len(y_train) - y_train.sum()) / y_train.sum()]).to(device)
criterion = nn.BCELoss(weight=pos_weight.expand(512)) # 按batch扩展
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5)
best_val_auc = 0
best_state = None
for epoch in range(30):
# 训练
model.train()
train_loss = 0
for X_batch, y_batch in train_loader:
X_batch, y_batch = X_batch.to(device), y_batch.to(device)
optimizer.zero_grad()
pred = model(X_batch)
loss = criterion(pred, y_batch)
loss.backward()
optimizer.step()
train_loss += loss.item() * len(X_batch)
train_loss /= len(train_ds)
# 验证
model.eval()
val_preds, val_true = [], []
with torch.no_grad():
for X_batch, y_batch in val_loader:
pred = model(X_batch.to(device)).cpu().numpy()
val_preds.extend(pred)
val_true.extend(y_batch.numpy())
val_auc = roc_auc_score(val_true, val_preds)
val_ap = average_precision_score(val_true, val_preds)
if val_auc > best_val_auc:
best_val_auc = val_auc
best_state = model.state_dict().copy()
print(f"Epoch {epoch+1:2d} | Loss: {train_loss:.4f} | Val AUC: {val_auc:.4f} | Val AP: {val_ap:.4f}")
print(f"\n最佳验证AUC: {best_val_auc:.4f}")
6.4 测试集评估
# 加载最佳模型
model.load_state_dict(best_state)
model.eval()
test_preds, test_true = [], []
with torch.no_grad():
for X_batch, y_batch in test_loader:
pred = model(X_batch.to(device)).cpu().numpy()
test_preds.extend(pred)
test_true.extend(y_batch.numpy())
test_auc = roc_auc_score(test_true, test_preds)
test_ap = average_precision_score(test_true, test_preds)
print(f"测试集 AUROC: {test_auc:.4f}")
print(f"测试集 AUPRC: {test_ap:.4f}")
参考输出(前24小时聚合特征 + 3层MLP):
| 指标 | 数值 | 说明 |
|---|---|---|
| AUROC | 0.82-0.86 | 随机划分会到0.88+,按医院划分会降 |
| AUPRC | 0.25-0.32 | 死亡率6.5%,AUPRC比AUROC更有区分力 |
| 召回率(阈值0.5) | 0.55-0.65 | 加权Loss提高了召回但牺牲了精度 |
如果换用XGBoost/CatBoost,AUROC通常能到0.85-0.90。用LSTM处理原始时序数据(而非聚合特征),可以到0.88-0.93。
七、踩坑总结
| 坑点 | 影响 | 解决方案 |
|---|---|---|
-1未替换为NaN | 特征出现负数均值 | 聚合前replace(-1, np.nan) |
| 随机划分代替按医院划分 | 泛化性能虚高3-5% | 按hospitalID划分 |
| labname未标准化 | 检验指标大量漏数据 | 手动维护名称映射表 |
| 忽略医院级覆盖率差异 | 测试集特征全空 | 划分后检查各表覆盖率 |
| age字段含字符串">89" | 数值计算报错 | replace('>89', 90) |
扩展阅读
在写这篇教程的过程中,我参考了不少资料。其中千方病案的AI Ready数据集百科对eICU做了一个很系统的整理,页面长这样:

资源页面:https://www.qianfanghub.com/ai-ready-dataset/eicu-collaborative/9
里面包含31个表的完整字段字典、缺失值编码规则、8个常见坑点详解、DAIMS质量评分,以及基准性能排行榜。如果你在做eICU相关的实验,这个页面值得收藏作为参考手册。
其他资源:
- eICU官方文档:https://eicu-crd.mit.edu/
- PhysioNet数据页面:https://physionet.org/content/eicu-crd/2.0/
- 官方代码仓库:https://github.com/MIT-LCP/eicu-code
参考
- Pollard TJ, Johnson AEW, Raffa JD, et al. “The eICU Collaborative Research Database, a freely available multi-center database for critical care research.” Scientific Data 5, 180178 (2018). DOI: 10.1038/sdata.2018.178
- Sheikhalishahi S et al. “Benchmarking machine learning models on multi-centre eICU critical care dataset.” arXiv (2019).
- Safaei N et al. “E-CatBoost: An efficient machine learning framework for predicting ICU mortality using a large eICU database.” PLOS ONE (2022).
&spm=1001.2101.3001.5002&articleId=163268308&d=1&t=3&u=389fc51b23f64ef68ebf964f8cd7afee)
354

被折叠的 条评论
为什么被折叠?



