4. 数据清洗基础:缺失值处理、异常值检测、重复数据去重
数据清洗,说白了就是给知识图谱“洗澡”。
你从各大数据库、公开文献、甚至药品说明书里扒拉出来的数据,哪能那么干净?错别字、空字段、重复记录、异常数值……什么妖魔鬼怪都有。我刚开始做药物知识图谱那会儿,就因为一个“缺失值”没处理好,导致整个推理链路出了bug。
嗯,今天我们就来聊聊这三个最基础的坎儿:缺失值、异常值和重复数据。
核心观点:数据清洗不是一次性的,而是贯穿整个知识图谱构建流程的迭代过程。清洗得越早,后面融合、推理的坑就越少。
4.1 缺失值处理
缺失值,就是数据中某些字段是空的。比如一个药品的“副作用”字段没填,或者一个药物的“分子式”缺失了。
怎么处理?我一般分三步走:诊断 → 决策 → 执行。
4.1.1 诊断:缺失值长什么样?
先看看数据里哪些字段是空的。在Python里,我习惯用pandas来探路。
import pandas as pd
# 假设你有一个关于药物的DataFrame
df = pd.read_csv('drug_data.csv')
# 诊断缺失情况
print(df.isnull().sum())
输出结果会告诉你:
drug_name 0
target 12
indication 3
side_effect 45
molecular_weight 8
dtype: int64
你看,side_effect字段缺了45个。为什么会这样?可能是数据源本身就不完整,也可能是录入时漏了。
个人经验:我遇到过最离谱的缺失,是某个药物交互数据库里,有一半的“相互作用”字段是空的。追问之后才发现,数据源用了不同的占位符(比如“NA”、“-”、“空白”),但没统一处理。所以,第一步永远先搞清楚“缺失”到底长什么样。
4.1.2 三大处理策略
诊断完,就得决定怎么动手。我总结了三种常用策略:
| 策略 | 适用场景 | 缺点 |
|---|---|---|
| 删除 | 缺失比例很小(<5%),或缺失行非关键节点 | 信息损失,可能破坏数据分布 |
| 填充 | 缺失比例中等,或字段有统计意义 | 填充值选择不当会引入偏差 |
| 标记 | 缺失本身携带信息(如“该药物无已知副作用”) | 需额外特殊处理,增加复杂度 |
删除操作:
# 删除所有包含缺失值的行
df_cleaned = df.dropna()
# 只删除指定字段缺失的行(比如只保留有分子式的)
df_cleaned = df.dropna(subset=['molecular_weight'])
填充操作:
# 用均值填充连续值字段
df['molecular_weight'].fillna(df['molecular_weight'].mean(), inplace=True)
# 用众数填充分类型字段
df['indication'].fillna(df['indication'].mode()[0], inplace=True)
# 前向填充(适用于时间序列)
df['daily_dosage'].fillna(method='ffill', inplace=True)
避坑指南:我曾经用均值填充一个“药物相互作用强度”字段,结果发现填充后,模型把几个弱相互作用误判成了强相互作用。原因很简单——那个字段的分布是偏态分布的,均值并不能代表真实情况。所以,填充前一定要先看分布。
4.1.3 特殊场景:知识图谱中的三元组缺失
知识图谱不是表格,是三元组(头实体、关系、尾实体)。缺失往往意味着某个关系不存在,或者信息未录入。
我的处理原则:
- 如果是“关系”缺失(比如一个药物应该有“适应症”,但没找到),我倾向于标记为
UNKNOWN,而不是删除。 - 如果是“属性”缺失(比如分子量),可以尝试从其他数据源补齐(比如去PubChem查)。
4.2 异常值检测
异常值,就是那些“不对劲”的数据点。
比方说,一个药物的分子量是-200(负数),或者一个药物的剂量是10000mg(正常人早挂了)。这些值,要么是录入错误,要么是单位搞错了。
4.2.1 怎么找异常值?
我常用的方法有两种:统计法和业务法。
统计法:用箱线图或Z-score来判断。
# Z-score方法
from scipy import stats
import numpy as np
z_scores = np.abs(stats.zscore(df['molecular_weight']))
threshold = 3
outliers = df[(z_scores > threshold)]
print(f"发现 {len(outliers)} 个异常值")
# IQR方法(四分位距)
Q1 = df['molecular_weight'].quantile(0.25)
Q3 = df['molecular_weight'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df[(df['molecular_weight'] < lower_bound) | (df['molecular_weight'] > upper_bound)]
业务法:这招更狠。你想想看,一个药物的“每日最大剂量”如果超过FDA规定的上限,那肯定是异常。所以,我会建立一套业务规则库,直接过滤。
# 业务规则示例:阿司匹林的每日最大剂量不应超过4g
max_aspirin = 4.0 # 单位:克
df_filtered = df[(df['drug_name'] != 'Aspirin') | (df['daily_dose_g'] <= max_aspirin)]
个人建议:统计法和业务法要结合着用。统计法能找到“数学上的异常”,但可能误伤正常值(比如某些天然药物的分子量本来就大)。业务法能保证领域正确性,但可能漏掉一些未知异常。我的习惯是,先用统计法筛选一轮,再人工复核那些离群值。
4.2.2 异常值处理:删?改?还是留着?
处理方式取决于异常值的“成因”:
| 成因 | 处理方式 | 示例 |
|---|---|---|
| 录入错误 | 修正(如果能追溯)或删除 | 分子量 -200 → 实际应为 200 |
| 真实极端值 | 保留,但标记为潜在强影响因素 | 某种毒素的半致死剂量极低 |
| 单位不一致 | 转换单位后统一 | 毫克 vs 克 |
| 超出业务范围 | 删除或归为“不可信”类别 | 剂量超过FDA上限 |
踩过的坑:我曾经犯过一个低级错误——把所有Z-score大于3的异常值全删了。结果发现,那些所谓的“异常值”其实是某种罕见病药物的特有属性。所以,删除之前,一定得问一句:这个异常值,真的没意义吗?
4.3 重复数据去重
重复数据,说白了就是同一个实体在数据里出现了多次。
比如“布洛芬”在大数据里出现了两次,一次叫“Ibuprofen”,一次叫“布洛芬片”。如果不处理,知识图谱里就会有两个节点,明明是同一种药。
4.3.1 精准去重 vs 模糊去重
精准去重:字段完全相同。
# 找出所有完全重复的行
duplicates = df[df.duplicated(subset=['drug_name', 'CAS_number'], keep=False)]
# 删除重复,保留第一个
df_unique = df.drop_duplicates(subset=['drug_name', 'CAS_number'], keep='first')
模糊去重:字段相似但不同。比如“阿司匹林”和“Aspirin”其实是同一种药。
这种情况,就得用字符串匹配算法了。我常用的是Levenshtein距离或Jaccard相似度。
# 示例:使用模糊匹配(简化版)
from fuzzywuzzy import fuzz
threshold = 80 # 相似度阈值
potential_duplicates = []
for i, row_i in df.iterrows():
for j, row_j in df.iterrows():
if i >= j:
continue
similarity = fuzz.ratio(row_i['drug_name'], row_j['drug_name'])
if similarity >= threshold:
potential_duplicates.append((i, j, similarity))
print(f"发现 {len(potential_duplicates)} 对潜在重复")
4.3.2 去重中的合并策略
找到重复了,但怎么合并?总不能直接丢弃一个吧?
我一般会定一个优先级规则:
- 优先保留信息更完整的那条。比如一条有分子式,另一条没有,那保留有分子式的。
- 优先保留更新日期更近的那条。知识库数据经常有版本更新。
- 如果两者互补,那就合并。把两个来源的字段拼起来。
# 合并示例:合并两条重复记录
# 假设df_1和df_2是重复的,但字段互为补充
merged_drug = {}
for key in df_1.keys():
if pd.notna(df_1[key]):
merged_drug[key] = df_1[key]
elif pd.notna(df_2[key]):
merged_drug[key] = df_2[key]
else:
merged_drug[key] = None
核心理念:去重不是简单地删,而是“去冗余、保信息”。一个好的去重策略,能让知识图谱更紧凑、更准确,而不会损失关键数据。
4.4 本章知识体系:一个完整的清洗流程
我把上面三个环节串起来,画了一张流程图。你看,数据清洗不是割裂的步骤,而是一个闭环流程:
流程图里,每一步都会反馈到下一步,而且每个步骤都有可能回退到上一步(比如发现异常值后,需要重新审视缺失值策略)。这就是我常说的“迭代清洗”。
实操建议:建议把清洗过程写成可复用的Python脚本。这样每次跑新数据时,直接调用同一套流程,能避免很多手工操作的遗漏。我个人习惯是把清洗脚本放在Git仓库里,版本管理,方便回溯。
嗯,数据清洗这块儿,就聊到这儿。记住三点:缺的别乱补,异常的别乱删,重复的别乱丢。用心对待每一行数据,知识图谱的质量才能有保证。