数据清洗实战:缺失值、异常值与重复数据
数据清洗这件事,说白了就是给模型喂饭前先帮它把鱼刺挑出来。我见过太多项目,算法选得再好,特征工程做得再花哨,结果数据里一堆坑,模型直接翻车。今天咱们就把第二章节的内容掰开揉碎,聊聊缺失值、异常值和重复数据这三个最常见的坑。
核心观点:数据清洗不是体力活,而是对数据分布理解的体现。你清洗的方式,直接决定了模型的上限。
2.1 缺失值处理:别急着删,先看看为什么缺失
遇到缺失值,很多新手第一反应就是删掉。我刚开始做项目时也这么干过,结果模型效果差得离谱。后来才明白,缺失本身可能就带着信息。
2.1.1 缺失机制:先搞清楚来龙去脉
我个人习惯,拿到数据先问三个问题:
- 完全随机缺失(MCAR):缺失跟数据本身没关系,比如传感器偶尔断电。这种情况,删掉或简单填充影响不大。
- 随机缺失(MAR):缺失跟其他变量有关。比如收入数据缺失,可能跟职业有关。这时候用均值填充就会引入偏差。
- 非随机缺失(MNAR):缺失跟变量本身有关。比如高收入人群不愿意填收入。这是最麻烦的情况,需要谨慎处理。
我的经验:在金融风控项目中,我发现逾期用户的收入字段缺失率明显更高。这就是典型的非随机缺失,直接用均值填充会严重低估风险。
2.1.2 简单填充方法:均值/中位数/众数
这些方法虽然简单,但用对场景效果不错。我一般这样选:
| 方法 | 适用场景 | 注意事项 |
|---|---|---|
| 均值填充 | 数据近似正态分布,无异常值 | 会降低方差,影响模型对极端值的敏感度 |
| 中位数填充 | 数据有偏分布,或存在异常值 | 鲁棒性好,但会丢失分布形状信息 |
| 众数填充 | 分类变量,或离散型数值 | 可能引入偏差,尤其是类别不平衡时 |
import pandas as pd
import numpy as np
# 示例数据
df = pd.DataFrame({
'age': [25, 30, np.nan, 35, 40, np.nan, 28],
'income': [5000, 8000, 6000, np.nan, 12000, 7000, np.nan],
'gender': ['M', 'F', 'M', np.nan, 'F', 'M', 'F']
})
# 均值填充
df['age_fill_mean'] = df['age'].fillna(df['age'].mean())
# 中位数填充
df['income_fill_median'] = df['income'].fillna(df['income'].median())
# 众数填充
df['gender_fill_mode'] = df['gender'].fillna(df['gender'].mode()[0])
避坑指南:我曾经在一个电商项目中,用均值填充了用户购买金额的缺失值。结果模型预测的购买力全部集中在均值附近,完全失去了区分度。后来改用中位数填充,效果好了很多。
2.1.3 模型预测填充:更高级的做法
当缺失值比例较高(比如超过20%),或者变量之间关系复杂时,简单填充就不够用了。这时候我会用模型来预测缺失值。
具体做法:把缺失的变量当作目标变量,用其他完整变量做特征,训练一个模型来预测。我常用的是KNN或随机森林。
from sklearn.impute import KNNImputer
# KNN填充
imputer = KNNImputer(n_neighbors=3)
df_imputed = pd.DataFrame(
imputer.fit_transform(df[['age', 'income']]),
columns=['age', 'income']
)
# 随机森林填充(更灵活)
from sklearn.ensemble import RandomForestRegressor
# 假设income有缺失,用age和其他特征预测
train = df[df['income'].notna()]
test = df[df['income'].isna()]
rf = RandomForestRegressor()
rf.fit(train[['age']], train['income'])
df.loc[df['income'].isna(), 'income_pred'] = rf.predict(test[['age']])
我的建议:模型预测填充不是万能的。如果缺失变量与其他变量相关性很弱,预测结果可能还不如均值填充。先做相关性分析,再决定是否用这个方法。
2.2 异常值检测:别把宝藏当垃圾扔了
异常值这件事,我吃过不少亏。有一次在工业传感器数据中,我把所有偏离正常范围的值都删了,结果模型在故障预测任务上完全失效。后来才发现,那些异常值恰恰是故障前兆。
2.2.1 IQR方法:简单直观
IQR(四分位距)方法是我最常用的快速检测手段。它不假设数据分布,对偏态数据也适用。
def detect_outliers_iqr(data, factor=1.5):
Q1 = data.quantile(0.25)
Q3 = data.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - factor * IQR
upper_bound = Q3 + factor * IQR
return (data < lower_bound) | (data > upper_bound)
# 使用
df['is_outlier_iqr'] = detect_outliers_iqr(df['income'])
factor参数很关键。1.5是标准值,但我在不同项目中会调整:
- factor=1.5:常规检测,适合大多数场景
- factor=3:宽松检测,适合数据波动大的场景
- factor=1:严格检测,适合对异常敏感的场景(如金融欺诈)
2.2.2 Z-Score方法:适合正态分布
如果数据近似正态分布,Z-Score是个好选择。它衡量的是数据点偏离均值的标准差倍数。
from scipy import stats
def detect_outliers_zscore(data, threshold=3):
z_scores = np.abs(stats.zscore(data))
return z_scores > threshold
# 使用
df['is_outlier_zscore'] = detect_outliers_zscore(df['age'])
避坑指南:我曾经在一个用户行为分析项目中,直接用Z-Score检测异常值。结果发现所有活跃用户都被标记为异常,因为他们的行为数据严重偏离均值。后来改用IQR方法,才正确识别出真正的异常行为。
2.2.3 异常值处理策略
检测到异常值后,怎么处理?我一般按这个优先级来:
- 确认是否真的是异常:先跟业务方确认,别自己瞎判断
- 如果是数据错误:直接删除或修正
- 如果是真实但罕见的情况:考虑截尾处理(winsorize)
- 如果异常值有信息量:保留,或者单独作为一个特征
# 截尾处理示例
def winsorize_series(data, limits=[0.01, 0.01]):
lower = data.quantile(limits[0])
upper = data.quantile(1 - limits[1])
return data.clip(lower, upper)
df['income_winsorized'] = winsorize_series(df['income'])
2.3 重复数据处理:看似简单,实则暗藏玄机
重复数据这个问题,看起来最简单,但处理不好也会出大问题。我记得有一次做用户画像,发现数据里有很多重复用户,一查才知道是不同渠道导入的数据有重叠。
2.3.1 完全重复 vs 部分重复
| 类型 | 定义 | 处理方法 |
|---|---|---|
| 完全重复 | 所有字段都相同 | 直接删除,保留一条 |
| 部分重复 | 关键字段相同,其他字段不同 | 需要合并或去重策略 |
# 完全重复检测
df_duplicates = df[df.duplicated(keep=False)]
# 删除完全重复
df_clean = df.drop_duplicates()
# 部分重复检测(基于指定列)
df_partial_dups = df[df.duplicated(subset=['user_id'], keep=False)]
# 部分重复处理:保留最新记录
df_deduplicated = df.sort_values('timestamp').drop_duplicates(subset=['user_id'], keep='last')
我的经验:处理部分重复时,我通常会先跟业务方确认去重规则。比如用户数据,是保留最新注册的,还是保留信息最完整的?这个决策直接影响后续分析结果。
2.3.2 模糊匹配去重
有时候重复数据不是完全一致的,比如“张三”和“张 三”这种。这时候需要用模糊匹配。
from fuzzywuzzy import fuzz
def find_similar_duplicates(df, column, threshold=90):
duplicates = []
for i in range(len(df)):
for j in range(i+1, len(df)):
similarity = fuzz.ratio(df.iloc[i][column], df.iloc[j][column])
if similarity >= threshold:
duplicates.append((i, j, similarity))
return duplicates
避坑指南:模糊匹配很耗性能,数据量大时别直接用。我一般先做精确匹配过滤,再对剩余数据做模糊匹配。另外,阈值设置要谨慎,设低了会误判,设高了会漏判。
2.4 本章知识体系总览
下面这张图把本章的核心内容串起来了。你可以把它当作数据清洗的检查清单。