3、数据清洗基础:缺失值处理、异常值检测、重复数据处理

数据清洗,说白了就是给原始数据「洗澡」。我做了这么多年量化,见过太多人一上来就建模,结果模型跑出来一塌糊涂。为什么?因为数据本身就有问题。你想想看,汇率数据里夹杂着缺失值、异常点、重复记录,模型再厉害也白搭。

今天咱们就聊聊数据清洗的三个基本功:缺失值怎么补、异常值怎么揪、重复数据怎么去。这些都是我每天在用的东西,希望能给你一些实在的启发。

核心观点:数据清洗不是「可有可无」的步骤,而是决定模型成败的关键。我见过太多项目,80%的时间花在清洗上,20%的时间建模,结果反而最好。

数据清洗核心流程 原始汇率数据 缺失值处理 异常值检测 重复数据处理 向前填充 插值法 3-sigma IQR法 去重 + 保留策略 清洗后的高质量数据

3.1 缺失值处理:别让「空」毁了你的模型

汇率数据里,缺失值太常见了。节假日、数据源中断、系统故障,随便一个原因就能让数据出现空洞。我刚开始做量化的时候,遇到缺失值就直接删掉,结果模型在节假日附近的表现惨不忍睹。

后来我学乖了——缺失值不能随便删,得根据场景来补。常用的方法有两种:向前填充和插值法。

3.1.1 向前填充(Forward Fill)

这个方法很简单:用上一个有效值填充当前缺失值。说白了就是「假设数据不变」。在汇率预测里,这个方法特别适合处理非交易时段的缺失。

举个例子,周末外汇市场休市,周一开盘时我们通常用周五的收盘价作为起点。这就是向前填充的思路。

import pandas as pd
import numpy as np

# 模拟汇率数据
dates = pd.date_range('2024-01-01', periods=10, freq='D')
rates = [7.12, 7.13, np.nan, np.nan, 7.15, 7.14, np.nan, 7.16, 7.17, 7.18]
df = pd.DataFrame({'date': dates, 'rate': rates})

# 向前填充
df['rate_ffill'] = df['rate'].ffill()
print(df[['date', 'rate', 'rate_ffill']])

我的经验:向前填充适合短期缺失(1-2个时间点)。如果缺失超过5个点,我建议用插值法。我曾经在欧元兑美元的数据上试过,向前填充超过3天,误差就开始明显了。

3.1.2 插值法(Interpolation)

插值法更聪明一些。它利用缺失值前后的数据,通过数学方法「猜」出中间的值。常用的有线性插值、多项式插值、时间插值等。

我个人最常用的是线性插值。为什么?因为它简单、稳定,在汇率这种相对平滑的时间序列上效果不错。

# 线性插值
df['rate_interp'] = df['rate'].interpolate(method='linear')
print(df[['date', 'rate', 'rate_interp']])

# 时间插值(考虑时间间隔)
df['rate_time'] = df['rate'].interpolate(method='time')
print(df[['date', 'rate', 'rate_time']])
方法 适用场景 优点 缺点
向前填充 短期缺失、非交易时段 简单、快速 忽略趋势变化
线性插值 数据平滑、趋势明显 考虑前后趋势 对突变不敏感
时间插值 时间间隔不均匀 考虑时间权重 计算稍复杂

避坑指南:我曾经在英镑/日元的数据上用过多项式插值,结果插出了负值——汇率怎么可能为负?所以,高阶插值要慎用,尤其是在数据波动大的时候。线性插值虽然「笨」,但至少不会出离谱的结果。

3.2 异常值检测:揪出那些「不听话」的数据点

异常值,就是那些明显偏离正常范围的数据点。在汇率数据里,异常值可能是数据录入错误、系统故障,也可能是真实的「黑天鹅事件」。怎么区分?得靠方法。

常用的方法有两种:3-sigma 和 IQR。我一般两个都用,互相验证。

3.2.1 3-sigma 法

3-sigma 基于正态分布假设。数据落在均值±3倍标准差之外的概率只有0.3%,所以这些点被认为是异常值。

嗯,这里要注意:汇率数据并不完全服从正态分布,但3-sigma仍然是一个很好的参考。

# 3-sigma 异常值检测
mean = df['rate'].mean()
std = df['rate'].std()
threshold = 3

df['is_outlier_3sigma'] = (np.abs(df['rate'] - mean) > threshold * std)
print(f"均值: {mean:.4f}, 标准差: {std:.4f}")
print(f"异常值数量: {df['is_outlier_3sigma'].sum()}")

3.2.2 IQR 法

IQR(四分位距法)不依赖正态分布假设,更稳健。它用Q1(25%分位数)和Q3(75%分位数)来定义正常范围:
正常范围 = [Q1 - 1.5*IQR, Q3 + 1.5*IQR]

我个人更喜欢IQR法。为什么?因为它对极端值不敏感。你想想看,如果数据里有一个巨大的异常值,3-sigma的均值和标准差都会被带偏,但IQR不受影响。

# IQR 异常值检测
Q1 = df['rate'].quantile(0.25)
Q3 = df['rate'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

df['is_outlier_iqr'] = (df['rate'] < lower_bound) | (df['rate'] > upper_bound)
print(f"Q1: {Q1:.4f}, Q3: {Q3:.4f}, IQR: {IQR:.4f}")
print(f"正常范围: [{lower_bound:.4f}, {upper_bound:.4f}]")
print(f"异常值数量: {df['is_outlier_iqr'].sum()}")
方法 假设 稳健性 适用场景
3-sigma 正态分布 低(受极端值影响) 数据分布对称、无极端异常
IQR 无分布假设 高(不受极端值影响) 数据偏态、存在极端异常

我的建议:两个方法都用,取交集。如果某个点同时被两种方法标记为异常,那它大概率真的有问题。如果只有一种方法标记,我会再人工检查一下。毕竟,有些「异常值」其实是重要的市场信号。

3.3 重复数据处理:别让「复制粘贴」骗了你

重复数据,说白了就是一模一样的记录出现了多次。在汇率数据里,重复数据通常来自数据源合并、API重试、或者人为操作失误。

我记得有一次,一个同事把同一个数据源导入了两次,结果模型训练出来的参数全偏了。排查了半天才发现是重复数据的问题。从那以后,我每次做数据清洗都会先检查重复值。

# 检查重复数据
print(f"重复行数: {df.duplicated().sum()}")

# 查看重复的具体行
print(df[df.duplicated(keep=False)])

# 删除重复数据(保留第一个)
df_clean = df.drop_duplicates(keep='first')
print(f"去重后行数: {len(df_clean)}")

小技巧:有时候数据不是完全重复,而是「几乎重复」——比如时间戳差了几毫秒,但价格完全一样。这种情况可以用 df[df.duplicated(subset=['rate'], keep=False)] 来检查特定列的重复。

3.3.1 重复数据的处理策略

处理重复数据,不是简单删掉就完事了。你得想清楚:

  • 保留第一个:适合时间序列数据,默认保留最早出现的记录
  • 保留最后一个:适合数据更新场景,保留最新版本
  • 聚合处理:对重复记录取均值或中位数,适合数据有微小差异的情况
# 聚合处理重复数据
df_grouped = df.groupby('date').agg({'rate': 'mean'}).reset_index()
print(df_grouped)

避坑指南:我曾经在美元/人民币的数据上,因为没处理好重复数据,导致模型在某个时间点出现了「跳变」。后来发现是同一个时间点有两条记录,一条是询价价,一条是成交价。所以,处理重复数据前,先搞清楚为什么会有重复。

3.4 实战:完整的数据清洗流程

好了,理论说完了,咱们来一个完整的实战。假设你拿到了一份美元/人民币的日线数据,包含缺失值、异常值和重复数据。你会怎么做?

# 完整的数据清洗流程
def clean_exchange_rate_data(df):
    """
    汇率数据清洗函数
    """
    # 1. 删除完全重复的行
    df = df.drop_duplicates(keep='first')
    
    # 2. 处理缺失值:先用向前填充,再用线性插值补剩余
    df['rate'] = df['rate'].ffill(limit=2)  # 最多向前填充2个
    df['rate'] = df['rate'].interpolate(method='linear')
    
    # 3. 异常值检测(IQR法)
    Q1 = df['rate'].quantile(0.25)
    Q3 = df['rate'].quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - 1.5 * IQR
    upper = Q3 + 1.5 * IQR
    
    # 将异常值替换为边界值(而不是删除)
    df['rate'] = df['rate'].clip(lower=lower, upper=upper)
    
    return df

# 使用示例
df_cleaned = clean_exchange_rate_data(df)
print(f"清洗前: {len(df)} 行, 清洗后: {len(df_cleaned)} 行")

核心原则:数据清洗的目标不是「完美」,而是「可用」。你不需要把数据洗到一尘不染,只要保证它不会误导模型就行。我见过太多人花了两周洗数据,结果模型效果还不如简单清洗的版本——过犹不及啊。

好了,数据清洗的基础就聊到这里。记住,清洗数据就像做饭前的洗菜切菜——虽然麻烦,但省不了。你想想看,用脏数据建模,就像用发霉的米煮饭,能好吃吗?


专注资料整理