4. 临床数据整合与合并

在真实的临床数据工程中,数据几乎从不以“一张干净的大表”形式存在。检验系统(LIS)里是每次检验的明细行,医嘱系统(CPOE)里是每次开单记录,电子病历(EMR)里是病程与诊断,随访系统里是定期访视结果。要把它们变成可分析的队列数据集,核心工作就是三件事:横向的多表连接、纵向的数据拼接,以及贯穿始终的患者主索引(EMPI)构建。本节围绕这三件事展开,并重点处理不同来源数据的时间对齐与冲突消解。

一、先明确“连接键”与“时间语义”

多表连接的第一步不是写 merge,而是确定连接键。临床数据中常见的键有三类:患者标识(住院号、门诊号、病案号、身份证号)、就诊标识(visit_id / encounter_id)、事件标识(检验单号、医嘱号)。连接键选错,结果会静默膨胀或丢失。

时间语义同样关键。同一张表里往往并存多种时间:order_time(开单时间)、specimen_time(采样时间)、report_time(报告时间)、charge_time(计费时间)。做“入院后首次肌酐”这类分析时,应以采样时间为准,而不是报告时间,否则会把入院前采样、入院后才出报告的结果错误地算进来。因此建议在整合前统一约定:临床事件时间 = 采样/执行时间,并保留原始时间列以便追溯。

二、多表连接:从明细到宽表

以“患者基本信息 + 检验结果 + 诊断”为例,典型做法是:先以患者主索引为基准表,左连接检验明细,再左连接诊断。左连接保证不丢失没有检验或没有诊断的患者。需要警惕的是一对多连接导致的行膨胀:一个患者有多条诊断,若直接与检验明细连接,检验行会被复制多份。正确做法是先对诊断按患者聚合(如取主要诊断),再连接。

import pandas as pd

# 诊断表按患者聚合,避免一对多膨胀
dx_main = (dx.sort_values("dx_priority")
             .groupby("patient_id", as_index=False)
             .first()[["patient_id", "main_dx", "dx_code"]])

# 以患者主索引为基准左连接
cohort = (empi.merge(dx_main, on="patient_id", how="left")
              .merge(lab, on="patient_id", how="left"))
print(cohort.shape, cohort["patient_id"].nunique())

连接后必须做行数与唯一患者数校验:若 cohort.shape[0] 远大于检验明细行数,说明发生了膨胀,需回到聚合步骤排查。

三、纵向拼接:同一患者多次访视的堆叠

纵向数据拼接(concat / append)解决的是“同一结构、不同时间点”的合并,例如基线、第1周期、第2周期的随访表。拼接要求列名与含义一致,并新增一列标识来源时点:

visits = []
for tp, df in [("baseline", v0), ("cycle1", v1), ("cycle2", v2)]:
    df = df.copy()
    df["visit"] = tp
    visits.append(df)

long_df = pd.concat(visits, ignore_index=True)
long_df = long_df.sort_values(["patient_id", "visit_date"])

拼接后按 patient_id + visit_date 排序,才能正确计算相邻访视的指标变化。若同一患者同一时点出现重复记录,需要按预设规则去重,例如保留最新录入或标记为冲突待人工核查。

四、患者主索引构建与冲突消解

不同来源系统对同一患者的标识往往不一致:LIS 用住院号,随访系统用手机号,EMR 用病案号。构建 EMPI 的思路是确定性匹配 + 概率匹配结合。确定性匹配用身份证号、住院号等强标识;概率匹配用姓名、性别、出生日期、手机号等组合打分。

冲突处理遵循“强标识优先、时间就近、来源可信度排序”三条原则。例如同一患者两条记录性别不一致,以身份证号解析结果为准;若都无身份证,则以最近一次就诊记录为准,并打上 conflict_flag 供人工复核。

注意:姓名相同不等于同一患者,尤其在常见姓名人群中。任何仅凭姓名+性别的合并都必须保留冲突标记,不能直接覆盖。

五、时间对齐:把事件放到统一时间轴上

时间对齐的目标是回答“某次检验相对于入院/用药是第几天”。常用做法是计算相对时间:relative_day = (event_time - index_time).days,其中 index_time 为入院时间或首次用药时间。参照系一旦确定,全文与图示必须保持一致:本课程约定入院当天为第 0 天,入院前为负值,入院后为正值。

对于需要“窗口内取值”的场景,例如“用药后 7 天内最低血小板”,可用时间窗筛选后再聚合:

win = lab[(lab["relative_day"] >= 0) & (lab["relative_day"] <= 7)]
min_plt = win.groupby("patient_id")["plt"].min()

下面这张图概括了从多源数据到统一分析队列的整合链路,重点标出连接键、时间对齐点与冲突消解环节。

临床多源数据整合链路 LIS 检验明细 住院号 / 采样时间 CPOE 医嘱 就诊号 / 开单时间 EMR 诊断 病案号 / 诊断时间 随访系统 手机号 / 访视日期 患者主索引 EMPI 强标识匹配 + 概率匹配打分 冲突消解 强标识优先 / 时间就近 conflict_flag 标记 多表连接 左连接 + 聚合防膨胀 纵向拼接 concat + visit 标识 时间对齐 relative_day = event − index_time 入院当天 = 第 0 天 分析队列宽表 唯一患者 + 时间轴 + 冲突标记 整合后校验 行数 vs 唯一患者数 · 时间轴单调性 · 冲突标记数量 · 关键字段缺失率 任一异常 → 回溯连接键与聚合步骤

六、整合后的校验清单

整合完成不等于可用。建议固定执行以下检查:

  • 行数与唯一患者数:连接后行数是否等于预期明细行数,唯一患者数是否与 EMPI 一致。
  • 时间轴单调性:同一患者 relative_day 是否随事件顺序递增,是否存在负值异常。
  • 冲突标记数量:conflict_flag 占比过高说明匹配规则过松,需收紧阈值。
  • 关键字段缺失率:如主要诊断、关键检验值缺失比例,决定是否需要补充数据源。
实践建议:把 EMPI 映射表单独落库保存(patient_id ↔ 各系统标识),后续新增数据只需增量匹配,避免每次全量重算,也便于审计与回溯。

至此,多源临床数据被整理为一张以患者为行、以时间轴为序、带冲突标记的分析宽表。后续的特征工程、缺失处理与建模,都建立在这一整合结果之上。