3. 临床数据清洗与预处理

临床数据进入分析环境后,很少能直接用于建模或统计推断。真实世界中的电子病历、检验系统导出表、随访登记表往往同时存在缺失、异常、重复、类型错乱和编码不统一等问题。数据清洗与预处理的目标,是把这些“原始记录”转化为结构一致、语义明确、可被后续统计与机器学习稳定消费的分析数据集。本节围绕缺失值、异常值、重复记录、类型转换、变量重编码与衍生变量六个核心任务展开,并给出可直接复用的处理思路与代码示例。

一、先建立数据质量画像

在动手清洗之前,应先对数据做一次整体体检。常用做法是查看行列规模、每列缺失比例、唯一值数量、数值列的分位数以及分类列的取值分布。这一步决定了后续采用删除、填补还是标记策略。例如,某列缺失率低于 5% 且为随机缺失,可考虑删除缺失行;若缺失率超过 40% 且与结局相关,则更应保留并增加“是否缺失”指示变量,而不是简单丢弃。

import pandas as pd
import numpy as np

df = pd.read_csv("clinical_raw.csv")
print(df.shape)
quality = pd.DataFrame({
    "dtype": df.dtypes.astype(str),
    "missing_rate": df.isna().mean().round(3),
    "n_unique": df.nunique()
})
print(quality.sort_values("missing_rate", ascending=False))

输出中若发现 age 为 object 类型、sex 出现“男/男性/M”多种写法、sbp 存在 0 或 999 等极端值,就说明类型转换、重编码与异常值处理必须同步进行。

二、缺失值处理:先判断机制,再选择策略

缺失机制通常分为完全随机缺失、随机缺失和非随机缺失。临床场景中,检验值缺失常与病情轻重相关,属于非随机缺失,直接删除会引入选择偏倚。因此推荐的处理顺序是:先量化缺失比例,再结合变量临床意义决定策略。

  • 删除:仅适用于缺失比例极低且完全随机的情况,如某次随访体重漏记。
  • 统计量填补:连续变量用中位数、分类变量用众数,适合快速基线分析,但会低估方差。
  • 分组填补:按性别、年龄段分组后分别用组内中位数填补,比全局填补更合理。
  • 模型填补:用随机森林或 KNN 基于其他变量预测缺失值,适合变量间相关性强的场景。
  • 缺失指示:新增 cr_missing 等 0/1 变量,保留“缺失”本身可能携带的信息。
# 分组中位数填补示例
df["age"] = pd.to_numeric(df["age"], errors="coerce")
df["cr"] = pd.to_numeric(df["cr"], errors="coerce")
df["cr_missing"] = df["cr"].isna().astype(int)
df["cr"] = df.groupby("sex")["cr"].transform(
    lambda s: s.fillna(s.median())
)

三、异常值识别与处理

异常值不等于错误值。临床中极端高值可能是真实危重病例,也可能是录入错误。常用识别方法包括:基于临床合理范围(如收缩压 40–300 mmHg)、基于统计分布(均值±3 倍标准差、IQR 1.5 倍法则)以及基于业务规则(如年龄为负、出院日期早于入院日期)。

# IQR 法标记异常
q1, q3 = df["sbp"].quantile([0.25, 0.75])
iqr = q3 - q1
lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr
df["sbp_outlier"] = ((df["sbp"] < lower) | (df["sbp"] > upper)).astype(int)

# 临床范围修正
df.loc[(df["sbp"] < 40) | (df["sbp"] > 300), "sbp"] = np.nan

处理方式应记录在案:可截断到合理边界、替换为缺失后统一填补,或保留但增加异常标记。切忌无差别删除,否则可能丢失重症信号。

四、重复记录与类型转换

重复记录常见于多次导出、多系统合并或同一患者多次登记。判断重复不能只看整行是否相同,而应基于业务主键,如“住院号+检验日期+项目代码”。保留策略通常是保留最新一次或最完整一条。

df = df.sort_values("update_time").drop_duplicates(
    subset=["patient_id", "visit_date", "item_code"],
    keep="last"
)

df["visit_date"] = pd.to_datetime(df["visit_date"], errors="coerce")
df["sex"] = df["sex"].map({"男": "M", "男性": "M", "女": "F", "女性": "F"})
df["stage"] = pd.Categorical(df["stage"], categories=["I", "II", "III", "IV"], ordered=True)

类型转换要显式处理失败值,errors="coerce" 可将无法解析的值转为缺失,避免整列转换报错。分类变量应使用有序类别,便于后续统计和绘图保持正确顺序。

五、变量重编码与衍生变量

重编码是把原始取值映射为分析所需的分组或数值。例如将连续年龄分为 <60 与 ≥60,将 BMI 按中国标准分为偏瘦、正常、超重、肥胖。衍生变量则基于现有字段计算,如体表面积、肾小球滤过率估算值、住院天数、Charlson 合并症指数等。

df["age_group"] = pd.cut(df["age"], bins=[0, 60, 200],
                         labels=["<60", ">=60"], right=False)
df["bmi"] = df["weight"] / (df["height"] / 100) ** 2
df["los"] = (pd.to_datetime(df["discharge_date"])
             - pd.to_datetime(df["admit_date"])).dt.days
df["hypertension"] = (df["sbp"] >= 140).astype(int)

衍生变量必须可解释、可复现,并在数据字典中记录公式、单位和缺失规则。对于分类衍生,应检查各类别样本量,避免出现某一类仅有个位数导致模型不稳定。

临床数据清洗与预处理流程 原始临床数据 EMR / LIS / 随访表 缺失·异常·重复·类型乱 数据质量画像 缺失率 / 唯一值 / 分位数 类型与取值分布检查 缺失值处理 删除 / 中位数 / 分组 / 模型 异常值处理 临床范围 / IQR / 标记保留 重复记录处理 业务主键去重 / 保留最新 类型转换与重编码 日期 / 数值 / 有序分类 衍生变量 BMI / eGFR / 住院天数 年龄分组 / 合并症指数 公式可复现·可解释 分析就绪数据集 结构一致 · 语义明确 · 可追溯 · 可复现 每一步处理均需记录规则与版本,保证清洗过程可追溯、结果可复现

六、处理顺序与可追溯性

推荐的处理顺序是:类型转换 → 重复记录去重 → 异常值识别与修正 → 缺失值填补 → 变量重编码 → 衍生变量创建。顺序并非绝对,但类型转换应尽早进行,否则数值比较和分位数计算会出错;衍生变量应放在缺失处理之后,避免用填补值反复计算导致误差累积。

工程上建议把每一步写成独立函数,并保留处理日志,例如记录删除了多少行、填补了多少个值、异常值修正了多少条。最终输出一份数据字典,说明每个变量的来源、类型、单位、缺失规则和衍生公式。这样,当分析结果出现异常时,可以快速定位是原始数据问题还是清洗规则问题。

对于小样本临床研究,缺失值填补后应做敏感性分析:分别用完整病例分析和填补后数据各跑一次主模型,若结论方向一致,说明结果稳健。
不要用未来信息填补过去值,例如用出院诊断填补入院时的变量;也不要在划分训练集与测试集之前做全局标准化或全局填补,否则会造成数据泄漏。

完成上述步骤后,数据才真正具备进入描述性统计、回归建模或机器学习流程的条件。清洗与预处理不是一次性动作,而应随着数据更新和问题发现持续迭代,并在每次迭代中保持规则透明、结果可复现。