数据清洗实战:缺失值、异常值与重复数据

数据清洗这件事,说白了就是给模型喂饭前先帮它把鱼刺挑出来。我见过太多项目,算法选得再好,特征工程做得再花哨,结果数据里一堆坑,模型直接翻车。今天咱们就把第二章节的内容掰开揉碎,聊聊缺失值、异常值和重复数据这三个最常见的坑。

核心观点:数据清洗不是体力活,而是对数据分布理解的体现。你清洗的方式,直接决定了模型的上限。

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 异常值处理策略

检测到异常值后,怎么处理?我一般按这个优先级来:

  1. 确认是否真的是异常:先跟业务方确认,别自己瞎判断
  2. 如果是数据错误:直接删除或修正
  3. 如果是真实但罕见的情况:考虑截尾处理(winsorize)
  4. 如果异常值有信息量:保留,或者单独作为一个特征
# 截尾处理示例
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 本章知识体系总览

下面这张图把本章的核心内容串起来了。你可以把它当作数据清洗的检查清单。

数据清洗知识体系 数据清洗实战知识体系 缺失值处理 异常值检测与处理 重复数据处理 缺失机制:MCAR / MAR / MNAR 简单填充:均值 / 中位数 / 众数 模型预测填充:KNN / 随机森林 IQR方法:四分位距检测 Z-Score方法:标准差检测 处理策略:删除 / 截尾 / 保留 完全重复:直接删除 部分重复:合并 / 去重策略 模糊匹配:fuzzywuzzy 核心原则:理解数据分布,结合业务场景,选择合适策略 💡 数据清洗不是一次性工作 在特征工程和模型迭代过程中,可能需要反复清洗 每次清洗后都要验证对模型效果的影响