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)

我记得有一次处理股票数据,缺失了几个交易日的价格。用线性插值效果还不错,但用多项式插值就离谱了——它给我「预测」出了一个根本不存在的暴涨。所以插值方法选哪个,得看数据本身的特性。

核心原则:缺失值处理没有银弹。我的经验是——先理解业务,再看数据分布,最后选方法。别一上来就 fillna(0),那是偷懒。

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)]
避坑指南:我曾经处理过一组销售数据,用 IQR 法检测出很多「异常值」。结果一查,那些是高端产品的真实高价。如果直接删了,整个分析就偏了。所以——先问业务,再动手

知识体系总览

下面这张图总结了数据清洗的核心逻辑,我画了很久,希望能帮你理清思路:

数据清洗核心流程 原始数据 缺失值处理 重复值处理 异常值处理 删除 填充(均值/中位数) 插值(线性/时间) 按分组填充 完全重复删除 基于列去重 Z-Score 法 IQR 四分位法 可视化检测 干净数据 ✅

这张图把整个流程串起来了。从原始数据出发,经过三个分支的处理,最终得到干净数据。每个分支下都有具体的方法,你可以根据实际情况选。

总结一句话:数据清洗不是技术活,是「业务+技术」的混合活。多问几个为什么,少写几行代码,结果往往更好。

公众号:蓝海资料掘金营,微信deep3321