第1章:数据清洗与预处理——异常值检测、缺失值处理与去重策略

各位同学好,我是老张。在交易审计这个行当摸爬滚打了十几年,我最大的体会就是:数据清洗不是脏活累活,而是整个分析流程的基石。你想想看,如果原始数据里藏着各种“地雷”,后面再漂亮的模型也是白搭。

今天咱们就聊聊数据清洗与预处理中最核心的三个环节:异常值检测、缺失值处理、重复数据去重。嗯,这三个问题,我几乎在每一个项目里都遇到过。

核心观点:数据清洗不是一次性的工作,而是需要反复迭代的过程。我个人的习惯是,每做一步清洗,都会保留一份原始数据的快照,方便回溯。

数据清洗与预处理知识体系 数据清洗与预处理 异常值检测 缺失值处理 重复数据去重 3σ原则 IQR箱线图 均值填充 业务规则推导 多字段联合去重 目标:干净、一致、可信的数据

一、异常值检测——别让“野点”毁了你的分析

异常值,说白了就是那些“格格不入”的数据点。我在审计一家电商平台时,发现某笔交易金额高达 999999 元,而同类交易平均只有 200 元。这就是典型的异常值——要么是测试数据,要么是系统 bug。

常用的检测方法有两种:

1. 3σ原则(拉依达准则)

这个方法假设数据服从正态分布。如果某个数据点与均值的距离超过 3 倍标准差,就判定为异常。公式很简单:

# 3σ原则示例代码
import numpy as np

def detect_outliers_3sigma(data):
    mean = np.mean(data)
    std = np.std(data)
    threshold = 3 * std
    outliers = [x for x in data if abs(x - mean) > threshold]
    return outliers

# 实际应用
transaction_amounts = [100, 120, 95, 110, 999999, 105, 98]
outliers = detect_outliers_3sigma(transaction_amounts)
print(f"检测到的异常值: {outliers}")
# 输出: [999999]

我的经验:3σ原则对极端值敏感,但要求数据近似正态分布。在金融交易数据中,金额分布往往呈长尾分布,这时候我会先做对数变换再应用 3σ。

2. IQR箱线图法

这个方法不依赖正态分布假设,更稳健。它基于四分位数:

  • 计算 Q1(25%分位数)和 Q3(75%分位数)
  • IQR = Q3 - Q1
  • 异常值定义为:小于 Q1 - 1.5*IQR 或大于 Q3 + 1.5*IQR
# IQR箱线图法示例
def detect_outliers_iqr(data):
    Q1 = np.percentile(data, 25)
    Q3 = np.percentile(data, 75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    outliers = [x for x in data if x < lower_bound or x > upper_bound]
    return outliers

# 同样的数据
outliers_iqr = detect_outliers_iqr(transaction_amounts)
print(f"IQR方法检测到的异常值: {outliers_iqr}")
# 输出: [999999]

避坑指南:我曾经在一个项目中,直接用 IQR 方法处理了所有字段,结果把正常的节假日促销数据也标记为异常了。记住:业务上下文比统计规则更重要。比如双十一的销售额,用平时的 IQR 阈值去判断,肯定全是异常。

二、缺失值处理——别让“空洞”误导你的判断

交易数据中,缺失值太常见了。比如用户没有填写手机号、交易对手信息不完整等。处理缺失值,我一般遵循两个原则:

  1. 能不删就不删——删除数据会损失信息
  2. 填充要有依据——不能为了填充而填充

1. 均值填充

最简单的方法,用该字段的均值或中位数填充缺失值。适用于数值型字段,且缺失率不高(一般低于 20%)。

# 均值填充示例
import pandas as pd

df = pd.DataFrame({
    '交易金额': [100, 200, None, 150, 300, None, 250],
    '交易时间': ['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04', 
                '2024-01-05', '2024-01-06', '2024-01-07']
})

# 用均值填充缺失的交易金额
mean_amount = df['交易金额'].mean()
df['交易金额_填充'] = df['交易金额'].fillna(mean_amount)
print(df)

我的习惯:均值填充前,我会先看看数据分布。如果分布偏态严重,用中位数比均值更靠谱。比如用户收入数据,用中位数填充能避免被高收入人群拉高。

2. 业务规则推导

这是我最喜欢的方法——利用业务逻辑来推算缺失值。举个例子:

  • 如果交易金额缺失,但手续费和费率已知,可以反推交易金额
  • 如果用户年龄缺失,但身份证号已知,可以提取出生年份
  • 如果交易对手信息缺失,但交易流水号关联了其他表,可以关联补全
# 业务规则推导示例
def derive_missing_amount(row):
    """根据手续费和费率推导缺失的交易金额"""
    if pd.isna(row['交易金额']) and not pd.isna(row['手续费']) and not pd.isna(row['费率']):
        return row['手续费'] / row['费率']
    return row['交易金额']

df['交易金额_推导'] = df.apply(derive_missing_amount, axis=1)

核心原则:业务规则推导的优先级永远高于统计填充。因为业务规则反映了真实的交易逻辑,而统计填充只是数学上的近似。

三、重复数据去重——别让“复制粘贴”污染你的数据

重复数据是交易审计中的“隐形杀手”。我见过一个案例,因为系统重试机制导致同一笔交易被记录了 3 次,结果审计报告里的交易总额虚增了 200%。

去重策略的核心是:确定唯一标识。常见的做法有:

去重策略 适用场景 注意事项
单字段去重 有唯一交易流水号 确保流水号本身不重复
多字段联合去重 没有唯一ID,但多个字段组合可唯一标识 字段选择要谨慎,避免过度去重
时间窗口去重 短时间内重复提交的交易 需要定义合理的时间窗口
# 多字段联合去重示例
df_clean = df.drop_duplicates(subset=['交易时间', '交易金额', '付款方账号', '收款方账号'])

# 时间窗口去重(5分钟内相同金额的交易视为重复)
df['交易时间'] = pd.to_datetime(df['交易时间'])
df_sorted = df.sort_values('交易时间')
df_sorted['时间差'] = df_sorted.groupby(['交易金额', '付款方账号'])['交易时间'].diff()
df_unique = df_sorted[df_sorted['时间差'].isna() | (df_sorted['时间差'] > pd.Timedelta(minutes=5))]

避坑指南:我曾经在去重时,只用了交易金额和交易时间两个字段,结果把两笔金额相同但交易对手不同的正常交易也删掉了。记住:去重字段要能唯一标识一笔交易,宁可少去重,不可过度去重。

四、实战建议——我的数据清洗工作流

说了这么多,最后分享一个我常用的数据清洗工作流:

  1. 先备份原始数据——这是铁律,任何时候都不要在原数据上直接操作
  2. 先做去重——重复数据会影响后续的统计和异常检测
  3. 再做缺失值处理——先尝试业务规则推导,再考虑统计填充
  4. 最后做异常值检测——异常值检测要结合业务上下文,不能只看统计指标
  5. 记录每一步的清洗日志——包括删除了多少条、填充了多少条、修改了哪些字段

最后说一句:数据清洗没有标准答案,每个项目都有自己的“脾气”。但只要你掌握了 3σ、IQR、均值填充、业务规则推导、去重策略这些基本功,再结合对业务的理解,就能把数据清洗做得八九不离十。


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