
接手医院数据清洗项目的第一周我就被现实教育了一顿。原以为“清洗”就是把空值补一补、去重、改格式结果摆在我面前的是同一个患者在不同科室系统里存了三种姓名格式、诊断编码混着ICD-9和ICD-10、时间字段有的是字符串、有的是Excel序列号、还有科室自己造的“备注说明”里夹着关键信息。那段时间我几乎把市面上常见的数据清洗工具都试了一遍最后把所有处理逻辑都收敛到本地脚本方案里。这篇文章就把我的对比过程和最终抉择完整写出来希望能帮到正在处理医疗数据、尤其是被困在数据质量泥潭里的同行。医疗数据清洗这个事跟普通业务数据清洗最大的区别在于“错不起”。少一条患者记录、弄错一个诊断编码、把“未检测”误当成缺失值删掉轻则影响统计报表重则可能影响后续的临床决策和费用核算。所以从一开始我的原则就是清洗逻辑必须可解释、可复现、可审计——这条原则直接决定了后面工具选型的方向。1. 医疗数据真正难清洗的点不在“脏”而在“语义”先说一个很多人没意识到的问题医疗数据清洗的难度80%不是来自技术而是来自“你到底知不知道这条数据应该是什么样”。工具只能执行规则但规则得由懂医疗业务的人定出来。1.1 数据来源五花八门交接格式千奇百怪医院里的数据源至少包括这几类HIS系统导出的门诊记录、LIS系统的检验结果、RIS/PACS系统的检查报告、病案室手工补录的病案首页、护士站手工录入的生命体征记录以及各类Excel表格。这些系统的导出口径完全不同同一家医院里光“患者姓名”这个字段就存在“姓名性别”“姓名首字母全名”“姓名中间带空格”等七八种变体。更让人崩溃的是数据格式。有些老系统导出来的日期是“2024-01-15 10:23:45”有些是“20240115”有些是“2024/1/15”还有些直接在Excel里以“41594”这种序列号形式存在。如果你用代码直接读不做兼容处理后面所有统计分析都会跑偏。1.2 编码体系不统一是最大隐患诊断编码、手术编码、药品编码这些是医疗数据的骨架。但实际情况是病案室录入时默认用ICD-10但老系统里存量数据有大量ICD-9编码检验项目名称在不同科室有不同缩写——同一个“白细胞计数”检验科叫“WBC”血液科叫“白细胞”有的界面里叫“白血球”。这些如果不做编码映射后续做疾病谱分析、费用结构分析时结果完全没有可信度。1.3 “空值”在医疗数据里有真实含义普通业务数据里空值一般是“没填”处理方式大多是删除或填充默认值。医疗数据完全不是这样。举个例子一个患者没有做某项检查对应字段就是空这个“空”代表“未检查”是有临床意义的但如果删掉这条记录统计分析就会以为“所有患者都做了检查”结论彻底失真。所以我在清洗规则里单独定义了空值处理策略区分“未采集”“无记录”“数值为0但被写成空”三种情况分别打不同标签处理。你会发现如果没有这个前提后面所有步骤都是错的基础。2. 三款清洗方案的横向对比DataX、Kettle、Python脚本我实际对比的对象不是三个“小插件”而是三条技术路线阿里开源的DataX、开源ETL工具Kettle也叫PDI、以及基于Python Pandas的本地清洗脚本。选这三者是因为它们分别代表了“纯同步工具”“可视化ETL工具”“编程式脚本”三种典型思路。2.1 三款方案的定位差异对比维度DataXKettlePDIPython Pandas脚本定位数据同步/迁移工具可视化ETL平台编程式数据清洗框架使用门槛中需写JSON配置低拖拽式控件中高需写代码对复杂业务规则的支持弱主要做字段映射中可通过脚本节点扩展强任意逻辑可实现运行环境本地或集群均可本地或服务器本地可审计性一般配置文件可追溯弱流程文件难读强代码即文档性能表现高流式读取中大文件易内存溢出中上配合分块读取维护成本低中高版本迁移有坑中需管理依赖环境2.2 DataX同步不错清洗功能太薄DataX是阿里开源的异构数据源离线同步工具性能好、插件丰富从MySQL到HDFS、从Oracle到SQL Server都能跑而且支持断点续传、限流控制这些特性。我用它做过一个病历表的历史数据迁移几千万行数据跑下来非常稳。但它在“清洗”这件事上能力有限。DataX的核心是“取数→转换→写入”转换主要靠JSON配置文件里的transformer插件实现。做字段级映射、简单的类型转换、脱敏处理没问题但遇到“根据诊断描述判断疾病大类”“根据患者年龄和检查结果生成分层标签”这类需要业务上下文的逻辑配置文件根本写不进去。另外一个尴尬是DataX解决的是“结构清洗”几乎不解决“语义清洗”。它能帮你把日期格式统一了但没法判断“2024-02-30”这种不合法日期该不该修正、怎么修正。2.3 Kettle可视化方便但复杂规则力不从心KettlePDI是很多传统数仓团队在用的图形化ETL工具。它的优点是上手快把几百个转换控件拖到画布上连起来就行不懂代码的人也能整理出一条清洗流程。我也确实用Kettle搭过一条简单的清洗作业去除空行、字段拆分、字典映射。但用久了问题就暴露了。第一维护性差。一条清洗作业流程文件动辄上百个节点谁画的图谁自己看得懂三个月后回头维护一脸懵。团队协作时代码可以diff但流程文件几乎没法做代码评审。第二大文件处理吃力。我拿一张2000万行的医嘱表测过Kettle默认堆内存下跑得极慢调JVM参数后勉强能跑但中途一个节点报错就得从头排查。相比之下同样数据量我用Pandas分块读取逻辑清晰很多。第三版本兼容性是真坑。我从Kettle 8换到9好几个自作转换控件直接不可用社区资料也良莠不齐。说白了Kettle适合快速出一版清洗演示不适合作为长期维护的生产级清洗方案。2.4 Python Pandas脚本灵活、可复现、可控最后是主角——本地Python脚本。我用Pandas NumPy openpyxl这一套组合自己维护了一套清洗代码库。核心优势就是三个词灵活、可复现、可控。清洗逻辑本质上是“一批带业务含义的规则”代码可以把规则写得清清楚楚比如“诊断编码若以字母开头保留否则加ICD-10前缀”“体温字段超过43度标记为异常但保留原值”。这些逻辑用配置项或可视化节点表达都很痛苦但在代码里就是几行if-else的事。可复现体现在每次清洗都会生成一份完整的清洗日志和前后对照表哪些数据被改过、为什么改全部留痕。这个能力在医疗场景里极其重要——统计出报告之后如果被质疑可以回溯每一步处理过程。3. 为什么最后选了本地合规、定制、长期维护三维度很多同行问我你提这些需求市面上那些商业数据质量平台难道满足不了有些能但我在医疗这个特殊场景里反复权衡后还是决定把清洗链路完全放在本地。原因有三条。3.1 合规红线数据不能随便出域医疗数据属于敏感个人信息医院信息科对外发数据通常有严格审计流程。如果清洗工具是云平台哪怕只上传脱敏后的数据也会带来不小的合规风险。本地清洗从物理层面保证数据全程不出域——读取、处理、存储都在医院内网机器上完成这是最“笨”但最稳妥的方案。有人会说我可以用开源工具在自建服务器上部署。这也是本地的一种完全可行。但考虑到很多医院信息科的运维力量有限简单依赖一个内部Python环境比维护一套分布式集群要现实得多。3.2 清洗规则高度碎片化通用工具覆盖不了医疗数据清洗规则跟业务强绑定而且变动频繁。今年卫生统计口径改了、明年DRG分组细化、不同科室有自己的填报规范这些都会直接改变清洗规则。通用工具把规则固定在配置界面里每次改动要进配置、测试、发布流程冗长。脚本方式下改规则就是改函数、跑测试、更新文档效率高一个量级。3.3 可审计性是硬要求医院数据的清洗过程通常要求留痕。脚本天然的文本形态可以直接进Git做版本管理每次清洗之前固定好数据处理脚本的commit号跑出来的结果就是这个commit对应的产物。这种可追溯性是可视化ETL工具和商业平台很难做到的。这也是我后来在本地方案里坚持“每一步都写日志”的原因——不仅仅是技术洁癖而是当数据出现争议时清洗流程本身要能拿出来“自证清白”。4. 本地清洗流程的落地一套可复用的四阶段实践选型定了之后具体怎么落地才是重点。我把整套本地清洗流程拆成四个阶段探查、定规则、清洗执行、结果验证。四个阶段串成一条流水线任何一批新数据来都能套用。4.1 阶段一数据探查先摸清家底再做规则拿到一张新表第一步不急着清洗先做描述性统计和样本抽查。import pandas as pd # 读取原始数据注意医院系统导出的编码问题后面会细说 df pd.read_csv(raw_patient_records.csv, encodinggbk, dtypestr) # 字段概况总览 print(f总行数: {len(df)}, 总列数: {len(df.columns)}) print(f字段名列表: {list(df.columns)}) # 每列缺失情况 missing_summary df.isnull().sum() missing_ratio missing_summary / len(df) print(pd.DataFrame({缺失数量: missing_summary, 缺失比例: missing_ratio})) # 关键字段的取值分布配合业务判断是否合理 for col in [性别, 诊断编码, 入院科室]: if col in df.columns: print(f\n字段 {col} 的取值分布:) print(df[col].value_counts(dropnaFalse).head(20)) else: print(f提醒: 字段 {col} 不存在请核对导出口径)这段代码输出之后你会立刻看到很多问题比如“性别”字段里有“男”“M”“1”“男性”四种写法“诊断编码”里有大量NaN或非标准格式。这一阶段的目标不是“修”而是“列出问题清单”后面每一步清洗规则都由问题清单驱动。我在实际项目中会把探查结果整理成一页纸的“数据质量报告”内容包括字段总数、关键字段缺失率、疑似异常值数量、编码不合法数量。信息科或临床科室看完这份报告再决定清洗优先级比直接闷头写代码高效得多。4.2 阶段二规则制定把“业务语义”翻译成代码探查结束之后就要把业务要求转换成可执行的清洗规则表。我的做法是维护一个清洗规则台账每条规则包含规则编号、规则名称、触发条件、处理方式、负责人、生效日期。举几个真实的规则例子日期标准化规则所有日期字段统一为YYYY-MM-DD HH:MM:SS格式。Excel序列号先转换字符串格式先按正则识别再统一。诊断编码规则若编码以字母开头且长度为3-5位视为合法否则加入“待人工审核”表同时保留原值并打标签“NONSTANDARD_DX”。不直接删改原始编码——保留证据是基本原则。空值分类规则建立三分类标签NOT_COLLECTED未做该项检查、NOT_RECORDED录入缺失、ZERO_AS_BLANK数值0被写成空后续统计时按标签分别处理。数值合理性规则例如体温有效范围30℃-45℃超出则打异常标签“ABNORMAL_VITALS”但保留原始值。血压的收缩压/舒张压逻辑关系异常同理。每个规则除了写明“怎么做”最重要的是写清楚“为什么这么做”。这样过一段时间回头看不会出现“这个字段为什么都变成0了”的困惑。4.3 阶段三清洗执行模块化函数全链路日志清洗代码我强烈建议模块化而不是写一个巨长的Jupyter Notebook从上往下堆代码。我通常把清洗函数拆分成几个模块时间字段处理模块、编码映射模块、空值分类模块、异常值打标模块。# 清洗执行主流程示例 from clean_modules.time_processor import normalize_datetime from clean_modules.code_mapper import map_diagnosis_code from clean_modules.missing_classifier import classify_missing # 1. 日期字段标准化 for col in date_columns: df[col] df[col].apply(normalize_datetime) # 2. 诊断编码映射 df[诊断编码_清洗后] df[诊断编码].apply(map_diagnosis_code) # 3. 空值分类打标 df[体温_缺失标签] df[体温].apply(classify_missing) # 4. 记录清洗操作日志 clean_log.append({ timestamp: datetime.now().isoformat(), table_name: patient_records, rows_before: rows_before, rows_after: len(df), action: 日期标准化编码映射空值分类, })每一个步骤都往日志表里写一笔内容包括操作时间、影响行数、处理类型。这是整个清洗流程中最容易被忽略但价值最高的部分。有了日志你在写清洗报告时直接能复制出一整段可读性极强的处理说明不必靠回忆。同时我坚持一个原则不在原始DataFrame上直接改值而是新增“清洗后”列或在清洗副本上操作保证任何时候原始数据都还在可以随时回溯。4.4 阶段四结果验证清洗不是做完就结束清洗完成后必须做两步验证否则你根本不知道清洗搞坏了什么。第一步是规则覆盖率验证统计有多少行数据命中了预设的清洗规则。如果规则覆盖率低于期望值要回去补规则而不是硬着头皮往下走。第二步是清洗前后关键指标对比清洗前后的记录总数、唯一患者数、诊断编码合法率、缺失值比例。这些指标能让数据质量提升直观可见。# 清洗前后效果对比 def summarize_quality(df, label): total len(df) valid_dx df[诊断编码_清洗后].str.match(r^[A-Z][0-9]{2,4}).sum() return { 数据批次: label, 总记录数: total, 诊断编码合法数: valid_dx, 编码合法率: round(valid_dx / total, 4), 缺失率: round(df.isnull().mean().mean(), 4), } before_report summarize_quality(df_raw, 清洗前) after_report summarize_quality(df_clean, 清洗后) quality_compare pd.DataFrame([before_report, after_report]) print(quality_compare)结果验证通过后再生成最终清洗数据和清洗报告。清洗报告我习惯用Markdown或者Excel输出主要内容包括数据来源、清洗工具及版本、清洗规则清单、每步处理行数、前后对比指标、待人工审核异常数据清单。这份报告随清洗后的数据一起交付后续任何人接手都能快速了解这批数据的完整情况。5. 本地清洗路上踩过的坑五条真实教训5.1 编码问题医院老系统导出的CSV不一定是UTF-8我拿到过一批病案首页导出文件用Pandas默认参数读中文全变乱码。排查半天才发现源头是医院老系统以GBK编码导出。解决方法很简单读文件时显式指定编码。df pd.read_csv(raw_data.csv, encodinggbk, dtypestr, keep_default_naFalse)但这里有个更深层次的提醒不要用Excel打开CSV后再另存为因为Excel会把日期格式搞乱、会把长ID变成科学计数法。所有数据文件都直接处理原始导出文件不要经过中间人“好心转换”。5.2 空字符串和NaN要分开处理pd.read_csv()默认会把空单元格解析为NaN但有些导出系统在空值位置写入的是空字符串。如果不区分这两种情况后续去重、合并时会出现同一个患者出现两行“看起来一样但一个NaN一个空串”的数据。我在读入时统一用keep_default_naFalse然后自己统一空值标记避免歧义。5.3 身份证号、病案号这类字段千万别直接当数值处理这算是低级错误但我确实见过有人踩。病案号如果是纯数字且达到一定长度Excel或某些导入工具可能自动转成科学计数法。解决办法读取时所有ID类字段一律指定为字符串不做隐式类型转换避免精度丢失。5.4 清洗规则的“继承”和“回滚”问题医疗数据清洗是一年一年持续做的事情。今年清洗规则改了往年的规则要不要保留我的处理方法是清洗代码严格按照日期打tag比如clean_rules_v2024.py、clean_rules_v2025.py。跑历史数据回看时用对应年份的规则版本跑绝不拿最新规则去改历史数据。否则跨年对比时你会发现统计口径漂移了那才是大灾难。5.5 大数据量本地处理要主动管理内存本地机器处理几千万行数据时别一个df从头用到尾。用分块读取、用完就删中间变量、配合gc.collect()主动回收内存能有效避免中途崩溃。chunk_size 500000 result_chunks [] for chunk in pd.read_csv(huge_data.csv, chunksizechunk_size, encodinggbk, dtypestr): cleaned process(chunk) result_chunks.append(cleaned) final_df pd.concat(result_chunks, ignore_indexTrue)这套做法在普通办公电脑上处理千万级数据没问题如果数据量更大建议直接换用DuckDB或ClickHouse Local这类本地分析引擎不引入集群复杂度但性能高很多。6. 还有一点想说的数据清洗不只是技术问题我见过不少团队买工具、搭平台、上代码最后发现数据质量还是提不上去。问题往往出在源头系统导出口径不统一、录入界面没有必填与格式校验、临床科室觉得信息化是负担。清洗做得再好也只是在给脏数据“擦屁股”真正的长期方案一定是推动源头规范。所以我的建议是前期花70%精力建立清洗流程和规则库让机器能按规范跑批后面每隔一段时间就跟信息系统管理员、数据录入人员对一遍导出口径和字段含义。数据清洗真正跑顺之后你会发现自己做的已经不是“清洁工”而是在搭建一套数据质量管理体系。如果你的项目也是医疗数据方向希望这篇对比和实操经验能帮你少走点弯路。尤其是“先探查、再定规则、后执行、终验证”这个四步流程换个业务场景同样适用——毕竟数据清洗的本质永远是让数据变得可解释、可信任。