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仓库里,版本管理,方便回溯。

嗯,数据清洗这块儿,就聊到这儿。记住三点:缺的别乱补,异常的别乱删,重复的别乱丢。用心对待每一行数据,知识图谱的质量才能有保证。