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)
衍生变量必须可解释、可复现,并在数据字典中记录公式、单位和缺失规则。对于分类衍生,应检查各类别样本量,避免出现某一类仅有个位数导致模型不稳定。
六、处理顺序与可追溯性
推荐的处理顺序是:类型转换 → 重复记录去重 → 异常值识别与修正 → 缺失值填补 → 变量重编码 → 衍生变量创建。顺序并非绝对,但类型转换应尽早进行,否则数值比较和分位数计算会出错;衍生变量应放在缺失处理之后,避免用填补值反复计算导致误差累积。
工程上建议把每一步写成独立函数,并保留处理日志,例如记录删除了多少行、填补了多少个值、异常值修正了多少条。最终输出一份数据字典,说明每个变量的来源、类型、单位、缺失规则和衍生公式。这样,当分析结果出现异常时,可以快速定位是原始数据问题还是清洗规则问题。
完成上述步骤后,数据才真正具备进入描述性统计、回归建模或机器学习流程的条件。清洗与预处理不是一次性动作,而应随着数据更新和问题发现持续迭代,并在每次迭代中保持规则透明、结果可复现。