数据清洗与预处理:缺失值处理、去重、标准化

数据清洗这件事,说白了就是给你的数据“洗澡”。

我从 DrugBank 和 CTD 爬下来的原始数据,那叫一个乱。药物名称有的带括号,有的不带;化学式里氢原子数莫名其妙少了;ATC 编码甚至出现“N/A”和“null”混着写。不清洗?知识图谱建出来也是垃圾堆。

我个人习惯,拿到原始数据第一步绝不是分析,而是先写一套清洗 pipeline。这一章就把我常用的三板斧教给你:缺失值处理、去重、标准化。

数据清洗与预处理流程 原始数据入库 ① 缺失值处理 删除 / 填充 / 标记 ② 去重 精确去重 / 模糊匹配 ③ 标准化 名称 / 化学式 / ATC 清洗后数据 导入图数据库 提示:三个步骤可以按任意顺序执行,通常建议先处理缺失值,再做去重和标准化

缺失值处理:别急着删

你可能觉得缺失值就是空着而已。错了。在药物数据里,缺失值往往藏着信息。

举个例子:某个药物的化学式为空,可能是这个药物是混合物,没有单一化学式。直接删除?你会丢掉重要分类信息。

我个人分三步处理缺失值:

  1. 统计缺失率——先看看哪些字段“空”得离谱
  2. 区分缺失类型——是数据没爬下来,还是字段本身就不适用
  3. 执行策略——删除、填充或标记

缺失值处理策略速查表

缺失字段 常见原因 我推荐的处理方式
药物名称 爬取遗漏 / 命名不规范 去源数据库补全;补不了就删除该记录
化学式 复杂化合物 / 混合物 标记为 "MIXTURE" 或保留空值
ATC 编码 老药未分类 / 编码失效 先用 "UNASSIGNED" 填充,后续再人工核查
分子量 化学式缺失导致无法计算 用 NaN 占位,计算时跳过

具体实现上,我习惯用 pandas 先扫一遍:

import pandas as pd

df = pd.read_csv('drugbank_raw.csv')
# 查看每列缺失值数量和比例
missing_counts = df.isnull().sum()
missing_ratio = df.isnull().mean()

# 只保留缺失率低于 30% 的字段
threshold = 0.3
cols_to_keep = missing_ratio[missing_ratio < threshold].index
df = df[cols_to_keep]

# 对 ATC 编码单独处理
df['atc_code'].fillna('UNASSIGNED', inplace=True)

# 对化学式字段做标记
df['chemical_formula'].fillna('MIXTURE', inplace=True)

小技巧:不要一口气对整个 DataFrame 执行 fillna()。我曾因统一填充 'Unknown' 把 ATC 编码和药物名称全串了,最后清洗了三天才恢复。每个字段单独写逻辑,后面查 bug 也快。

去重:你以为重复的就删掉?

药物数据的去重比普通文本去重要复杂得多。

你想想看:“阿司匹林”和“Aspirin”是不是同一个药?从名称上看不一样,但从药理上完全是同一款药物。如果直接做字符串精确匹配,你会保留两倍的数据。

我的做法是分两层:

  • 第一层:精确去重——基于药物名称、化学式、ATC 编码三字段联合判断
  • 第二层:模糊去重——基于药物名称的文本相似度,配合领域词表

精确去重用 pandas 一行搞定:

df = df.drop_duplicates(subset=['drug_name', 'chemical_formula', 'atc_code'])

但只靠这步远远不够。我遇到过一个真实案例:“Paracetamol”和“Acetaminophen”是同一种药,但上面三个字段都不同。光靠精确去重完全抓不住。

模糊去重我常用的工具是 fuzzywuzzy:

from fuzzywuzzy import fuzz

# 对名称列做两两相似度计算
duplicates = []
names = df['drug_name'].unique()

for i in range(len(names)):
    for j in range(i+1, len(names)):
        score = fuzz.ratio(names[i], names[j])
        if score > 85:  # 相似度超过 85% 怀疑为重复
            duplicates.append((names[i], names[j], score))

# 输出可疑重复对
for a, b, s in duplicates[:10]:
    print(f"{a} <-> {b} : {s}%")

注意:模糊匹配不能自动合并。你需要人工复核这些“可疑对”,确认后再干掉一条。我曾经图省事,设了个 90% 阈值自动合并,结果把“Dopamine”和“Dobutamine”两个不同药给合并了,差点把整个图谱搞崩。

标准化:统一语言的最后一公里

标准化分三个维度:药物名称、化学式、ATC 编码。

药物名称标准化

不同数据源对同一个药物的命名千奇百怪。我整理过一个线上问题清单:

  • 大小写不统一:aspirin vs ASPIRIN vs Aspirin
  • 多余空格或特殊字符:Ibuprofen (Advil) 里的括号
  • 中英文混合:阿司匹林 (Aspirin)

我的清洗套路:

import re

def standardize_name(name):
    # 统一小写
    name = name.lower().strip()
    # 去掉括号及其内容(保留通用名)
    name = re.sub(r'\(.*?\)', '', name)
    # 去掉多余空格
    name = re.sub(r'\s+', ' ', name)
    return name.strip()

df['drug_name_std'] = df['drug_name'].apply(standardize_name)

化学式标准化

化学式这坑更大。有的数据源写 “C9H8O4”,有的写 “C9H804”(字母 O 和数字 0 搞混),甚至有的化学式里元素顺序是乱的。

我搞了个标准化函数:

from collections import Counter
import re

def standardize_formula(formula):
    if pd.isna(formula) or formula == 'MIXTURE':
        return formula
    # 提取元素和数量,按元素名称排序
    pattern = re.findall(r'([A-Z][a-z]*)(\d*)', formula)
    elements = {}
    for elem, count in pattern:
        count = int(count) if count else 1
        elements[elem] = elements.get(elem, 0) + count
    # 按 Hill 系统排序(先 C, H, 然后其他按字母序)
    hill_order = ['C', 'H']
    others = sorted([e for e in elements if e not in hill_order])
    sorted_elems = hill_order + others
    return ''.join(f"{e}{elements[e] if elements[e] > 1 else ''}" for e in sorted_elems)

df['formula_std'] = df['chemical_formula'].apply(standardize_formula)

ATC 编码标准化

ATC 编码是个七位字符,有固定层级结构:首字母(解剖学主类)+ 两位数字(治疗组)+ 一位字母(药理组)+ 两位数字(化学组)。

常见问题:

  • 多余连字符:A01AA01 vs A01AA-01
  • 大小写混乱:a01aa01 vs A01AA01
  • 无效编码混入:比如 “N/A”、“unknown”、“-“

标准化代码很简单:

def standardize_atc(code):
    if pd.isna(code):
        return 'UNASSIGNED'
    code = str(code).upper().replace('-', '').strip()
    if len(code) == 7 and code.isalnum():
        return code
    else:
        return 'UNASSIGNED'

df['atc_std'] = df['atc_code'].apply(standardize_atc)

标准化前后对比示例

字段 原始数据 标准化后
药物名称 Ibuprofen (Advil) ibuprofen
化学式 C13H18O2 C13H18O2
ATC 编码 M01AE-01 M01AE01
化学式(乱序) O2H18C13 C13H18O2
ATC 编码(无效) N/A UNASSIGNED

做完这三步,你的数据基本就“干净”了。接下来就能顺利进入实体抽取和关系构建环节。

嗯,这一章的内容大概就这些。代码你们拿去跑一遍,看看自己的数据能清洗出多少坑来。我保证,你跑完之后会对“数据质量是知识图谱的生命线”这句话有深刻体会。