4. 数据清洗:处理缺失值、重复值与异常值
数据清洗这步,说白了就是给数据「洗澡」。我见过太多人拿到数据直接开跑模型,结果跑出来的结果自己都不敢信。嗯,数据清洗占整个数据准备工作的60%以上,这话一点不夸张。
今天咱们就聊三个最头疼的问题:缺失值、重复值、异常值。每个我都踩过坑,咱们一个一个说。
4.1 缺失值处理
缺失值是最常见的。你想想看,现实中的数据采集哪有那么完美?传感器坏了、用户没填、系统漏了……原因多了去了。
处理缺失值,我一般分三步走:先查有多少缺失,再看缺失模式,最后决定怎么处理。
4.1.1 检测缺失值
先看看数据长什么样:
import pandas as pd
import numpy as np
# 加载数据
df = pd.read_csv('sales_data.csv')
# 查看每列缺失数量
print(df.isnull().sum())
# 缺失比例
print(df.isnull().mean().round(4) * 100)
我个人习惯用 isnull().sum() 快速扫一眼。如果某列缺失超过50%,我基本会直接考虑删掉这列——留着的意义不大。
missingno 库可以可视化缺失模式,一眼看出缺失是不是随机的。
4.1.2 删除缺失值
删除是最简单粗暴的方法。但要注意,不是所有情况都适合删。
什么时候删?
- 缺失比例极小(<5%),且样本量足够大
- 缺失完全是随机的(MCAR)
- 该列对分析不重要
# 删除含有缺失值的行
df_clean = df.dropna()
# 删除指定列有缺失的行
df_clean = df.dropna(subset=['age', 'income'])
# 删除缺失超过3个的列
df_clean = df.dropna(thresh=len(df) * 0.7, axis=1)
4.1.3 填充缺失值
填充比删除更常用。我常用的填充方法有这几种:
| 方法 | 适用场景 | 代码示例 |
|---|---|---|
| 固定值填充 | 分类变量、已知默认值 | df.fillna(0) |
| 均值/中位数填充 | 数值型、分布较对称 | df.fillna(df.mean()) |
| 众数填充 | 分类变量 | df.fillna(df.mode()[0]) |
| 前向/后向填充 | 时间序列数据 | df.fillna(method='ffill') |
# 均值填充
df['age'].fillna(df['age'].mean(), inplace=True)
# 按分组填充(更精细)
df['income'] = df.groupby('city')['income'].transform(lambda x: x.fillna(x.median()))
# 前向填充(时间序列常用)
df['temperature'].fillna(method='ffill', inplace=True)
为什么我推荐按分组填充?举个例子,北京和鹤岗的收入中位数差好几倍,你用全局均值去填,那数据就失真了。按城市分组填充,更合理。
4.1.4 插值法
插值比简单填充更「聪明」一些。它利用已知数据点之间的关系来估算缺失值。
# 线性插值
df['value'] = df['value'].interpolate(method='linear')
# 时间序列插值
df['value'] = df['value'].interpolate(method='time')
# 多项式插值(慎用,容易过拟合)
df['value'] = df['value'].interpolate(method='polynomial', order=2)
我记得有一次处理股票数据,缺失了几个交易日的价格。用线性插值效果还不错,但用多项式插值就离谱了——它给我「预测」出了一个根本不存在的暴涨。所以插值方法选哪个,得看数据本身的特性。
4.2 处理重复值
重复值比缺失值隐蔽。很多时候你以为数据没问题,结果一查,好几行一模一样的数据。
4.2.1 检测重复值
# 查看完全重复的行
print(df.duplicated().sum())
# 查看基于某些列的重复
print(df.duplicated(subset=['user_id', 'date']).sum())
4.2.2 删除重复值
# 删除完全重复的行
df_unique = df.drop_duplicates()
# 基于指定列去重,保留第一条
df_unique = df.drop_duplicates(subset=['user_id'], keep='first')
# 保留最后一条
df_unique = df.drop_duplicates(subset=['user_id'], keep='last')
这里有个坑:不是所有重复都要删。比如电商订单数据,同一个用户同一天买了两件不同商品,这不叫重复。但如果是系统故障导致同一订单录入了两次,那就得删。
4.3 处理异常值
异常值是最让人头疼的。它可能是真实的数据点(比如双十一的销量暴增),也可能是数据采集错误(比如年龄填了200岁)。
4.3.1 检测异常值
我常用的方法有三种:
方法一:Z-Score 法
from scipy import stats
import numpy as np
z_scores = np.abs(stats.zscore(df['price']))
# 一般认为 Z-Score > 3 是异常
outliers = df[z_scores > 3]
方法二:IQR 四分位距法
Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df[(df['price'] < lower_bound) | (df['price'] > upper_bound)]
方法三:可视化检测
import matplotlib.pyplot as plt
# 箱线图一眼看出异常值
df.boxplot(column='price')
plt.show()
# 散点图看分布
plt.scatter(df.index, df['price'])
plt.show()
我个人更偏爱 IQR 法。为什么?因为 Z-Score 对数据分布有假设(正态分布),但实际数据往往不是正态的。IQR 法更稳健,不受极端值影响。
4.3.2 处理异常值
发现异常值后怎么处理?我的经验是:
- 确认是错误数据:直接删除或修正(比如年龄200改成20)
- 确认是真实极端值:保留,或者做截尾处理(Winsorize)
- 不确定:先标记,跟业务方确认后再处理
# 截尾处理:将异常值替换为边界值
df['price'] = df['price'].clip(lower=lower_bound, upper=upper_bound)
# 或者直接删除异常行
df_clean = df[(df['price'] >= lower_bound) & (df['price'] <= upper_bound)]
知识体系总览
下面这张图总结了数据清洗的核心逻辑,我画了很久,希望能帮你理清思路:
这张图把整个流程串起来了。从原始数据出发,经过三个分支的处理,最终得到干净数据。每个分支下都有具体的方法,你可以根据实际情况选。
公众号:蓝海资料掘金营,微信deep3321