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)

同时检查三类重复:

  1. 同编码同名称:可能是文件重复行;
  2. 同编码不同名称:可能是版本冲突或节点类型不同;
  3. 不同编码同名称:可能是合理同名,也可能是映射错误。

重复不是一律删除,而是先分类,再决定合并或保留。

四、第三道校验:层级关系

编码系统不是平面字典。丢失父子关系后,统计汇总、导航检索和规则匹配都会受影响。

可以把层级表示为邻接表:

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。不要在生产索引上逐条更新,而是:

  1. 为新版本创建物理索引;
  2. 批量导入并刷新;
  3. 校验文档数量、随机样本和搜索结果;
  4. 使用索引别名从旧版本原子切换到新版本;
  5. 保留旧索引一段时间用于回滚;
  6. 清理与版本绑定的缓存。

关系库负责版本真相和约束,搜索索引负责查询体验。两者之间通过 dataset_id 对齐,并记录同步批次和校验结果。

十四、测试不仅是几条样例

单元测试应覆盖规范化、格式判断、层级构建和状态转换。集成测试使用一份小型固定数据集,验证从原始文件到正式查询的全过程。版本升级时,再运行历史回归用例,确认原有查询在预期范围内变化。

还可以加入属性测试,例如任何非根节点都必须能沿父链到达根节点、任何有效编码的 valid_from 不得晚于 valid_to、任何替代编码必须在目标版本中存在。这类不变量比手工列举样例更容易发现系统性问题。

ICD 数据治理的价值,在于让编码的来源、版本、层级和状态都可以解释。与其写一个“万能清洗脚本”,不如把每条规则变成可测试、可审计、可重复执行的校验步骤。

说明:不同地区与业务场景的编码规则可能不同,生产使用应以相应主管机构发布的正式版本为准。

posted @ 2026-09-20 15:46  楼主好菜啊  阅读(1)  评论(0)    收藏  举报