2. 关联分析工程目标定义

在多表临床数据关联分析中,最容易出问题的地方往往不是 SQL 写不出来,而是目标没有定义清楚。临床医生提出的是“我想看看用了某药的患者后来肾功能怎么样”,而数据工程需要的是“以谁为一行、取哪段时间、关联哪些表、输出哪些字段、用什么口径判断异常”。这两者之间隔着一层必须显式完成的翻译工作。本节要解决的就是这层翻译:把模糊的临床问题,拆解为可执行、可验证、可复现的数据关联任务。

一、目标定义要回答的四个核心问题

一个可执行的关联任务,必须能明确回答以下四件事,缺一件都会在实现阶段返工:

  • 分析粒度(Grain):结果表的“一行”代表什么?是一次就诊、一位患者、一次用药、还是一天?粒度决定了主键、去重逻辑和聚合方式。
  • 时间窗口(Time Window):围绕哪个时间锚点取数?锚点前多久、后多久?窗口是闭区间还是开区间?
  • 关联路径(Join Path):从主表出发,通过哪些键、以什么方向关联到哪些表?是一对一、一对多还是多对多?
  • 输出指标(Output Metrics):最终要产出哪些字段?每个字段的取值口径、单位、缺失处理规则是什么?

这四者不是并列关系,而是有依赖顺序的:粒度决定主键,主键决定关联路径,时间窗口决定取数范围,输出指标决定聚合与筛选。下面这张图展示了从临床问题到可执行任务的完整翻译链路,以及四个核心问题在链路中的位置。

临床问题(模糊表述) “用了某药的患者肾功能变化如何?” 目标翻译层:把临床语义映射为数据语义 明确“谁、何时、关联什么、输出什么” ① 分析粒度 一行 = 一次用药记录? 一行 = 一位患者? ② 时间窗口 锚点:首次用药日 窗口:用药后 7~90 天 ③ 关联路径 用药表 → 检验表 键:患者ID + 时间 ④ 输出指标 肌酐基线值、峰值、 变化幅度、是否异常 可执行的数据关联任务 主键明确 · 窗口明确 · 关联键明确 · 输出字段明确 可验证性检查 行数是否符合预期?时间范围是否落在窗口内?缺失率是否可接受?

二、分析粒度:先定“一行是什么”

粒度是关联分析的基石。以“某药用药后肾功能变化”为例,至少有三种常见粒度选择:

  • 患者级:一位患者一行,取该患者首次用药后的肾功能指标。适合做组间比较、生存分析。
  • 用药事件级:一次用药记录一行。适合分析不同用药方案、剂量与肾功能的关系。
  • 时间点级:一次检验结果一行。适合做纵向趋势分析,但关联时容易产生笛卡尔积。

粒度一旦确定,主键就确定了。例如选择“用药事件级”,主键可能是 (patient_id, drug_code, start_date)。后续所有关联都必须保证不破坏这个粒度——如果检验表一个用药事件对应多条检验记录,就需要先聚合再关联,而不是直接 JOIN 导致行数膨胀。

判断粒度是否合理的实用方法:先写出目标结果表的主键,再问“这个主键在业务上是否唯一标识一行”。如果同一主键出现多行,说明粒度定义有歧义。

三、时间窗口:锚点、方向与边界

时间窗口的定义必须包含三个要素:锚点、方向、长度。以用药后肾功能监测为例:

  • 锚点:首次用药日期 first_drug_date。
  • 方向:向后(用药后),即 lab_date >= first_drug_date。
  • 长度:7 天到 90 天,即 lab_date BETWEEN first_drug_date + 7 AND first_drug_date + 90。

这里有两个容易踩的坑。第一,边界开闭:BETWEEN 是闭区间,如果业务上要求“用药后第 7 天开始算”,就要确认第 7 天当天是否纳入。第二,基线窗口:很多指标需要“用药前基线值”,这就要定义另一个反向窗口,例如用药前 30 天内最近一次检验值。两个窗口的锚点相同、方向相反,必须在任务定义中分别写清楚。

下面给出一个时间窗口定义的示例表格,用于在任务文档中固化口径:

窗口名称 锚点 方向 范围 用途
基线窗口 首次用药日 向前 [-30天, 0天) 取用药前最近一次肌酐值
随访窗口 首次用药日 向后 [7天, 90天] 取用药后肌酐峰值
排除窗口 首次用药日 向前 [-365天, 0天) 排除既往肾功能异常患者

四、关联路径:从主表到目标表的“路线图”

关联路径要明确三件事:起点表、终点表、中间经过哪些表以及用什么键连接。以“用药事件 → 患者 → 检验结果”为例,典型路径是:

drug_event (patient_id, drug_code, start_date)
  → patient (patient_id)          -- 补充人口学信息
  → lab_result (patient_id, lab_date, lab_item, value)
       ON lab_result.patient_id = drug_event.patient_id
      AND lab_result.lab_date BETWEEN drug_event.start_date + 7
                                   AND drug_event.start_date + 90
      AND lab_result.lab_item = 'CREA'

这条路径中,patient_id 是连接键,lab_date 是时间约束键。注意关联条件里同时出现了等值连接和时间范围连接,这是临床数据关联的典型特征。如果检验表在窗口内有多条记录,就需要在关联前或关联后做聚合(取最大值、最小值或最近值),具体取决于输出指标的定义。

多对多关联是行数膨胀的主要来源。在写 JOIN 之前,先用 SELECT patient_id, COUNT(*) FROM lab_result GROUP BY patient_id HAVING COUNT(*) > 1 检查一对多关系,确认聚合策略后再关联。

五、输出指标:口径、单位与缺失处理

输出指标是任务的最终交付物,必须逐字段定义。以肌酐变化分析为例,输出字段可以包括:

  • baseline_crea:基线肌酐值,单位 μmol/L,取基线窗口内最近一次检验结果。
  • peak_crea:随访窗口内肌酐峰值,单位 μmol/L,取最大值。
  • crea_change:变化幅度,peak_crea - baseline_crea,单位 μmol/L。
  • aki_flag:是否满足急性肾损伤判断,依据 KDIGO 标准,crea_change >= 26.5 或 peak_crea / baseline_crea >= 1.5。

每个字段都要明确缺失处理规则:如果基线窗口内没有检验记录,baseline_crea 是置空、排除该患者,还是用其他方式填补?这些规则必须在任务定义阶段写清楚,而不是留到实现时临时决定。输出指标的定义越具体,后续的 SQL 实现和结果验证就越顺畅。

六、从定义到可执行任务的检查清单

在把任务交给实现之前,可以用下面这份清单做一次自检:

  1. 结果表的主键是否明确?一行代表什么业务对象?
  2. 时间锚点、方向、窗口长度、边界开闭是否全部写清楚?
  3. 关联路径中每一跳的连接键和时间约束是否明确?
  4. 是否存在一对多或多对多关系?聚合策略是什么?
  5. 每个输出字段的口径、单位、缺失处理规则是否定义?
  6. 预期结果行数量级是否可估算?用于验证关联是否膨胀或丢失。