ICD编码数据清洗与校验
ICD 编码数据清洗:从“能导入”到“可长期维护”的五道校验
摘要:ICD 数据看起来只是编码和名称两列,实际落库后常见版本混用、层级丢失、扩展码误判和名称重复。本文给出一套从文件接收到上线使用的清洗与校验方法。
标签:ICD 数据治理 医疗信息化 Python 数据质量
一、先回答:这份编码属于哪个版本
处理 ICD 数据时,最危险的不是空值,而是“看起来正确”。同一编码在不同版本、不同地区扩展和不同年度维护表中可能存在差异。若导入时只保存编码和名称,后续几乎无法判断来源。
建议每次导入都绑定一个版本实体:
dataset_id 数据集唯一标识
standard ICD-10 / ICD-9-CM-3 等
locale 国家或地区扩展
release_version 发布版本或年度
source_org 发布机构
published_at 发布日期
imported_at 导入时间
file_hash 原始文件哈希
业务表中的每条编码都引用 dataset_id。这样即使将来升级版本,也能复现历史结果。
二、第一道校验:格式规范化
常见问题包括全角字符、不可见空格、大小写不一致、句点缺失和 Excel 自动转换。规范化时应谨慎,不能为了“看起来统一”而改变官方编码。
可以建立两列:
raw_code:原文件内容,永久保留;normalized_code:用于检索和关联的规范形式。
规范化操作应有日志,例如去除首尾空格、转为大写、统一句点形式。任何无法解释的修改都不应静默执行。
三、第二道校验:唯一性不是只看编码
有些文件包含章、节、类目、亚目和扩展码,多层节点可能出现相似名称。不能简单地给 code 建全局唯一索引,更合理的唯一键通常是:
(dataset_id, normalized_code, node_type)
同时检查三类重复:
- 同编码同名称:可能是文件重复行;
- 同编码不同名称:可能是版本冲突或节点类型不同;
- 不同编码同名称:可能是合理同名,也可能是映射错误。
重复不是一律删除,而是先分类,再决定合并或保留。
四、第三道校验:层级关系
编码系统不是平面字典。丢失父子关系后,统计汇总、导航检索和规则匹配都会受影响。
可以把层级表示为邻接表:
code | parent_code | level | is_leaf
导入后至少验证:
- 非根节点的父节点是否存在;
- 是否出现环;
- 层级是否连续;
- 叶子节点是否仍有子节点;
- 父子编码的格式关系是否符合该版本规则。
不要仅根据字符串长度推断父节点。不同扩展规则下,这种做法很容易出错,应优先使用官方层级字段。
五、第四道校验:有效性与状态
编码表中可能包含停用、替换、仅用于统计或不可作为最终编码的节点。建议显式保存:
status active / deprecated / replaced
valid_from 生效时间
valid_to 失效时间
replacement_code 替代码
billable 是否可作为最终使用项
查询接口默认只返回当前有效项,但历史数据回溯必须能够查到旧编码。删除停用编码会破坏历史可解释性,标记状态通常比物理删除更稳妥。
六、第五道校验:名称与搜索体验
官方名称适合规范展示,却不一定符合用户搜索习惯。可以在不修改标准名称的前提下,建立独立的别名表:
alias | code | alias_type | source | reviewed
别名来源可包括常见简称、旧称、拼音首字母和业务系统历史名称。机器自动生成的别名必须与人工审核状态分开,避免错误别名污染检索结果。
七、用数据质量报告代替“导入成功”
一次导入结束后,输出一份可比较的质量报告:总行数、有效编码数、重复数、孤儿节点数、状态缺失数、名称为空数、与上一版本新增/删除/变更数量。只有文件写入数据库并不代表导入完成。
版本升级时还应生成差异清单,明确哪些编码新增、停用、改名或更换父节点。下游系统可以据此评估规则、报表和历史映射是否需要同步调整。
八、设计可重复执行的导入流程
编码导入经常需要反复执行:修复解析规则、补充字段或验证新版本。如果脚本每次运行都会产生重复记录,后续排查会非常困难。因此流程需要满足幂等性。
一种常见设计是先导入临时表:
原始文件
→ staging_raw(逐行原样保存)
→ staging_normalized(规范化与解析)
→ validation_result(质量问题)
→ official_code(正式版本)
只有校验通过的数据集才能从临时区发布到正式表。发布使用事务或版本指针切换,避免用户在导入一半时查到不完整数据。
每条临时记录保存 source_row_number,错误报告就能准确定位到原始文件行。对于 Excel 合并单元格、隐藏行和公式值,还应明确读取的是显示值还是底层值。
简单的校验代码可以写成独立规则:
from dataclasses import dataclass
@dataclass
class Issue:
row: int
rule: str
level: str
message: str
def validate_code(row_no: int, code: str) -> list[Issue]:
issues = []
if not code:
issues.append(Issue(row_no, "CODE_REQUIRED", "error", "编码为空"))
if code != code.strip():
issues.append(Issue(row_no, "CODE_WHITESPACE", "warning", "编码包含首尾空格"))
return issues
规则返回问题列表,而不是直接抛异常,可以一次性生成完整质量报告。
九、版本差异不只看新增和删除
两版编码表对比时,至少要识别以下变化:
- 编码新增或停用;
- 标准名称变化;
- 父节点变化;
- 节点类型或可用状态变化;
- 替代编码变化;
- 生效和失效日期变化;
- 仅标点、空格变化的非语义修改。
对比结果需要区分“真实业务变化”和“源文件排版变化”。如果新文件只是把名称中的全角括号改成半角括号,不应触发大量下游告警。
可以对规范化后的关键字段计算记录哈希,快速发现变化;但最终差异报告仍应展示字段级别的旧值与新值。例如:
{
"code": "示例编码",
"change_type": "name_changed",
"before": "旧标准名称",
"after": "新标准名称",
"review_status": "pending"
}
重要变化应由业务人员确认后再发布,尤其是停用、替代和层级变化。
十、面向查询接口的设计
编码库不应只有一个“模糊搜索”接口。根据使用场景,可以提供:
- 按精确编码查询;
- 按名称、别名和拼音搜索;
- 查询父节点、子节点和完整路径;
- 查询指定日期有效的编码;
- 查询某编码在新版本中的替代项;
- 比较两个版本中的同一编码;
- 批量验证一组编码及其有效状态。
接口返回时带上数据集版本和命中方式。对于模糊搜索结果,说明是标准名称命中、别名命中还是拼音命中,便于调用方判断可靠性。
批量接口需要设置条数限制和逐项错误结果,不能因为一个无效编码导致整批请求失败。缓存键中也应包含版本号,否则升级后可能继续返回旧结果。
十一、关系数据库表结构示例
下面以 PostgreSQL 为例,将数据集、编码节点和别名拆开:
create table code_dataset (
id uuid primary key,
standard varchar(32) not null,
locale varchar(32) not null,
release_version varchar(64) not null,
source_org varchar(128) not null,
file_sha256 char(64) not null,
published_at date,
status varchar(16) not null,
unique (standard, locale, release_version)
);
create table code_node (
dataset_id uuid not null references code_dataset(id),
code varchar(32) not null,
raw_code varchar(64) not null,
name text not null,
parent_code varchar(32),
node_type varchar(32) not null,
status varchar(16) not null,
valid_from date,
valid_to date,
replacement_code varchar(32),
source_row integer not null,
primary key (dataset_id, code, node_type)
);
create index idx_code_node_parent
on code_node(dataset_id, parent_code);
create index idx_code_node_validity
on code_node(dataset_id, status, valid_from, valid_to);
父节点外键不能简单写成只引用 code,因为同一编码可能存在于多个数据集。若数据库要做复合外键,需要让被引用列具有对应唯一约束;也可以在发布前通过校验任务保证层级完整。
别名表建议增加来源和审核状态,避免自动产生的别名直接影响正式搜索:
create table code_alias (
dataset_id uuid not null,
code varchar(32) not null,
alias text not null,
alias_type varchar(32) not null,
source varchar(64) not null,
reviewed boolean not null default false
);
十二、用递归查询验证层级路径
PostgreSQL 的递归 CTE 可以查询某个节点到根节点的路径,同时发现异常深度:
with recursive ancestors as (
select code, parent_code, 0 as depth,
array[code] as visited
from code_node
where dataset_id = :dataset_id and code = :code
union all
select p.code, p.parent_code, a.depth + 1,
a.visited || p.code
from code_node p
join ancestors a on p.code = a.parent_code
where p.dataset_id = :dataset_id
and not p.code = any(a.visited)
and a.depth < 32
)
select * from ancestors order by depth desc;
visited 用于阻止循环,depth < 32 是防御性上限。真正的环检测应作为全量发布校验执行,而不是只在查询某个编码时发现。
十三、增量发布与索引切换
若查询量较大,可以把正式版本同步到 Elasticsearch。不要在生产索引上逐条更新,而是:
- 为新版本创建物理索引;
- 批量导入并刷新;
- 校验文档数量、随机样本和搜索结果;
- 使用索引别名从旧版本原子切换到新版本;
- 保留旧索引一段时间用于回滚;
- 清理与版本绑定的缓存。
关系库负责版本真相和约束,搜索索引负责查询体验。两者之间通过 dataset_id 对齐,并记录同步批次和校验结果。
十四、测试不仅是几条样例
单元测试应覆盖规范化、格式判断、层级构建和状态转换。集成测试使用一份小型固定数据集,验证从原始文件到正式查询的全过程。版本升级时,再运行历史回归用例,确认原有查询在预期范围内变化。
还可以加入属性测试,例如任何非根节点都必须能沿父链到达根节点、任何有效编码的 valid_from 不得晚于 valid_to、任何替代编码必须在目标版本中存在。这类不变量比手工列举样例更容易发现系统性问题。
ICD 数据治理的价值,在于让编码的来源、版本、层级和状态都可以解释。与其写一个“万能清洗脚本”,不如把每条规则变成可测试、可审计、可重复执行的校验步骤。
说明:不同地区与业务场景的编码规则可能不同,生产使用应以相应主管机构发布的正式版本为准。

浙公网安备 33010602011771号