4. 数据抽取与清洗实战

药物重定位知识图谱的质量,八成取决于数据进入图谱之前的那一段处理。本节聚焦一个真实工程问题:如何把 DrugBank、TTD、CTD 这类结构化数据库,以及 PubMed 摘要、临床试验描述这类半结构化文本,统一抽取成“药物—靶点—疾病”三元组,并在入库前处理缺失值、冗余与冲突。我们以一个可复现的小型流水线为例,说明每一步的输入、处理逻辑和判断依据。

一、两类数据源的抽取路径不同

结构化数据库的抽取本质是“字段映射”。以 DrugBank 的 drug target 记录为例,一条记录通常包含 DrugBank ID、药物名称、靶点 UniProt ID、作用方式(如 inhibitor、agonist)等字段。抽取时我们建立一张映射表,把源字段映射到图谱本体中的实体类型和关系类型:药物名称映射到 Drug.name,UniProt ID 映射到 Target.uniprot_id,作用方式映射到关系属性 action_type。这类抽取的难点不在解析,而在标识符归一化——同一个靶点在不同库中可能用基因名、UniProt ID 或 Ensembl ID 表示,必须先统一到主键。

半结构化文本的抽取则是“识别 + 归一化”。以 PubMed 摘要中的句子 “Metformin activates AMPK and alleviates hepatic steatosis” 为例,先用命名实体识别模型标出药物实体(Metformin)、靶点实体(AMPK)、疾病实体(hepatic steatosis),再用关系分类模型判断实体对之间是否存在“药物—靶点”或“药物—疾病”关系。文本抽取的召回率通常低于结构化库,但能补充大量数据库未收录的间接关系,因此两条路径需要合并而非二选一。

药物-靶点-疾病关系抽取与清洗流水线 结构化数据库 DrugBank / TTD / CTD 字段:DrugID, UniProt, action 半结构化文本 PubMed 摘要 / 试验描述 句子:Metformin activates AMPK… 字段映射 源字段 → 本体属性 标识符归一化 NER + 关系分类 实体识别 → 关系判定 置信度阈值过滤 三元组合并 (Drug, Target, Disease) 来源标记 + 置信度 缺失值处理 丢弃 / 补全 / 标记 unknown 冗余去重 同三元组多来源合并 冲突消解 来源优先级 + 投票 清洗后三元组集合 Metformin — AMPK — hepatic steatosis | confidence 0.86 | source: DrugBank+PubMed 示例:缺失值处理 action_type 缺失 → 标记 unknown UniProt 缺失 → 丢弃该条 冲突示例:DrugBank 标注 Metformin–AMPK 为 activator,PubMed 抽取为 indirect → 按来源优先级保留 activator,并记录冲突日志 冗余示例:同一 (Drug, Target, Disease) 来自 3 个来源 → 合并为一条,source 字段记录全部来源,confidence 取加权平均

二、缺失值:先判断“缺的是什么”

缺失值不能一律删除。我们按字段类型分三类处理。第一类是关系必需字段,例如靶点的 UniProt ID 缺失,这条“药物—靶点”关系就无法与其它库对齐,应直接丢弃。第二类是关系属性字段,例如 action_type 缺失,但药物和靶点都明确,此时保留关系并把属性标为 unknown,避免因属性缺失损失整条边。第三类是可选注释字段,例如文献 PMID 缺失,保留关系即可,不影响图谱结构。

在代码层面,可以用一个简单的规则表驱动:

required_fields = ["drug_id", "target_id", "disease_id"]
optional_attr = ["action_type", "pmid", "confidence"]

def handle_missing(record):
    for f in required_fields:
        if not record.get(f):
            return None  # 丢弃
    for a in optional_attr:
        if not record.get(a):
            record[a] = "unknown"
    return record

这样处理后,我们得到的是“结构完整、属性可缺”的三元组集合,而不是被缺失值大量削减的稀疏图。

三、冗余:同一关系被多个来源重复描述

冗余在药物重定位数据中非常普遍。同一个“二甲双胍—AMPK—脂肪肝”关系,可能同时出现在 DrugBank、CTD 和两篇 PubMed 摘要中。如果直接入库,图谱中会出现四条平行边,既浪费存储,也会在后续图算法中放大该关系的权重。处理方式是按 (drug_id, target_id, disease_id) 三元组做聚合:合并来源列表,置信度取加权平均,作用方式若多个来源一致则保留,不一致则进入冲突处理。

聚合后的记录应保留 sources 数组和 confidence 字段,便于后续追溯。例如:

{
  "drug": "DB00331",
  "target": "P54646",
  "disease": "DOID:9352",
  "action_type": "activator",
  "sources": ["DrugBank", "CTD", "PMID:12345678"],
  "confidence": 0.86
}

四、冲突:同一关系出现矛盾标注

冲突比冗余更难处理,因为两个来源对同一关系给出了不同判断。常见冲突有两类:作用方式冲突(一个说 activator,一个说 inhibitor)和关系方向冲突(一个说 A 调控 B,一个说 B 调控 A)。我们的处理策略是“来源优先级 + 投票 + 日志”。来源优先级按数据库可信度排序,例如 DrugBank 人工审编记录优先于文本抽取结果;若同优先级来源出现矛盾,则按多数投票决定,票数相同时保留高置信度一方,并把冲突写入日志表,供人工复核。

注意:冲突消解不能简单“取第一条”。在药物重定位场景中,作用方式直接决定后续推理方向——activator 和 inhibitor 可能导向完全相反的适应症假设。因此冲突记录必须可追溯,不能静默覆盖。

五、一个可复用的清洗检查清单

把上述逻辑固化成检查清单,可以在每次数据更新时快速核对:

检查项判断依据处理动作
必需 ID 是否完整drug_id / target_id / disease_id 任一为空丢弃该条记录
标识符是否归一化靶点是否统一为 UniProt ID映射到主键,无法映射则丢弃
是否存在重复三元组相同 (drug, target, disease) 出现多次合并来源,置信度加权平均
作用方式是否冲突同三元组出现 activator 与 inhibitor按来源优先级 + 投票,记录冲突日志
置信度是否低于阈值文本抽取 confidence < 0.6降级为候选边,不进入主图

完成这一轮抽取与清洗后,我们得到的不是“最全”的数据,而是“结构一致、来源可查、冲突可解释”的三元组集合。这正是后续实体对齐、关系推理和知识图谱入库能够稳定运行的前提。实际工程中,建议把清洗规则写成可配置的 YAML 或 JSON,每次数据源更新时只调整规则而不改代码,从而让整条流水线具备可维护性。