3、数据清洗基础:缺失值处理、异常值检测、重复数据处理
数据清洗,说白了就是给原始数据「洗澡」。我做了这么多年量化,见过太多人一上来就建模,结果模型跑出来一塌糊涂。为什么?因为数据本身就有问题。你想想看,汇率数据里夹杂着缺失值、异常点、重复记录,模型再厉害也白搭。
今天咱们就聊聊数据清洗的三个基本功:缺失值怎么补、异常值怎么揪、重复数据怎么去。这些都是我每天在用的东西,希望能给你一些实在的启发。
核心观点:数据清洗不是「可有可无」的步骤,而是决定模型成败的关键。我见过太多项目,80%的时间花在清洗上,20%的时间建模,结果反而最好。
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)} 行")
核心原则:数据清洗的目标不是「完美」,而是「可用」。你不需要把数据洗到一尘不染,只要保证它不会误导模型就行。我见过太多人花了两周洗数据,结果模型效果还不如简单清洗的版本——过犹不及啊。
好了,数据清洗的基础就聊到这里。记住,清洗数据就像做饭前的洗菜切菜——虽然麻烦,但省不了。你想想看,用脏数据建模,就像用发霉的米煮饭,能好吃吗?