数据清洗与预处理:缺失值处理、去重、标准化
数据清洗这件事,说白了就是给你的数据“洗澡”。
我从 DrugBank 和 CTD 爬下来的原始数据,那叫一个乱。药物名称有的带括号,有的不带;化学式里氢原子数莫名其妙少了;ATC 编码甚至出现“N/A”和“null”混着写。不清洗?知识图谱建出来也是垃圾堆。
我个人习惯,拿到原始数据第一步绝不是分析,而是先写一套清洗 pipeline。这一章就把我常用的三板斧教给你:缺失值处理、去重、标准化。
缺失值处理:别急着删
你可能觉得缺失值就是空着而已。错了。在药物数据里,缺失值往往藏着信息。
举个例子:某个药物的化学式为空,可能是这个药物是混合物,没有单一化学式。直接删除?你会丢掉重要分类信息。
我个人分三步处理缺失值:
- 统计缺失率——先看看哪些字段“空”得离谱
- 区分缺失类型——是数据没爬下来,还是字段本身就不适用
- 执行策略——删除、填充或标记
缺失值处理策略速查表
| 缺失字段 | 常见原因 | 我推荐的处理方式 |
|---|---|---|
| 药物名称 | 爬取遗漏 / 命名不规范 | 去源数据库补全;补不了就删除该记录 |
| 化学式 | 复杂化合物 / 混合物 | 标记为 "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 |
做完这三步,你的数据基本就“干净”了。接下来就能顺利进入实体抽取和关系构建环节。
嗯,这一章的内容大概就这些。代码你们拿去跑一遍,看看自己的数据能清洗出多少坑来。我保证,你跑完之后会对“数据质量是知识图谱的生命线”这句话有深刻体会。