第4章:数据清洗实战——缺失值处理、重复值检测、异常值识别
数据清洗这活儿,说白了就是给数据「洗澡」。我做了这么多年量化,见过太多脏数据了——有的缺胳膊少腿,有的重复得离谱,还有的数值大得吓人。你想想看,如果拿这些数据直接建模,结果能靠谱吗?
今天咱们就聊聊数据清洗的三个核心环节:缺失值、重复值、异常值。每个环节我都会结合实战经验来讲,希望能帮你少踩几个坑。
4.1 缺失值处理:别急着删,先看看情况
缺失值是最常见的问题。我刚开始做量化时,遇到缺失值就习惯性删掉,后来发现这其实挺危险的。为什么?因为缺失可能不是随机的,它背后可能有规律。
4.1.1 先检测缺失情况
拿到数据第一步,先看看缺失值长什么样。我个人习惯用 isnull() 和 info() 快速扫一眼:
import pandas as pd
import numpy as np
# 模拟一份金融数据
df = pd.DataFrame({
'日期': ['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04'],
'收盘价': [100.5, np.nan, 102.3, 101.8],
'成交量': [10000, 12000, np.nan, 11000],
'市盈率': [15.2, 15.5, np.nan, np.nan]
})
# 快速查看缺失情况
print(df.isnull().sum())
print(df.info())
嗯,这里要注意:info() 能告诉你每列的非空数量,如果某列非空数远小于总行数,那就要警惕了。
4.1.2 删除法:什么时候用?
删除法是最简单粗暴的。但我的建议是:只在缺失比例很小(比如低于5%)时用。我在项目中遇到过一只股票,某天因为停牌导致数据缺失,如果直接删掉那行,整个时间序列就断了,后面做回测会出问题。
# 删除含有缺失值的行
df_dropna = df.dropna()
# 删除某列缺失的行(比如只删'收盘价'缺失的)
df_drop_subset = df.dropna(subset=['收盘价'])
4.1.3 填充法:更聪明的做法
我个人更推荐填充法。常用的填充方式有几种:
- 均值/中位数填充:适合数值型数据,比如用过去30天的平均收盘价填充缺失值
- 前向填充/后向填充:时间序列数据特别适合,比如用前一天的数据填充
- 插值法:线性插值、多项式插值,更平滑
# 均值填充
df['收盘价'].fillna(df['收盘价'].mean(), inplace=True)
# 前向填充(金融时间序列常用)
df['收盘价'].fillna(method='ffill', inplace=True)
# 线性插值
df['收盘价'].interpolate(method='linear', inplace=True)
4.1.4 插值法:更精细的处理
插值法适合数据有趋势的情况。比如股价在上涨趋势中缺失了一天,线性插值能给出一个合理的估计值。
# 时间序列插值(按时间索引)
df['日期'] = pd.to_datetime(df['日期'])
df.set_index('日期', inplace=True)
df['收盘价'].interpolate(method='time', inplace=True)
你想想看,如果缺失值出现在趋势转折点,插值法可能会低估或高估。所以我的习惯是:先可视化看看趋势,再决定用哪种插值。
4.2 重复值检测:别让数据「复制粘贴」
重复值在金融数据里也挺常见的。比如数据源重复推送、同一笔交易被记录两次。我见过最夸张的一次,某只股票的数据有30%是重复的,那回测结果简直没法看。
4.2.1 检测重复行
# 检测完全重复的行
duplicates = df.duplicated()
print(f"重复行数: {duplicates.sum()}")
# 查看重复的具体行
df[df.duplicated(keep=False)]
keep 参数很关键:keep='first' 保留第一个,keep='last' 保留最后一个,keep=False 标记所有重复。
4.2.2 处理重复值
# 删除重复行,保留第一个
df_clean = df.drop_duplicates()
# 按指定列判断重复(比如按'日期'去重)
df_clean = df.drop_duplicates(subset=['日期'])
4.3 异常值识别与处理:别让「黑天鹅」毁了模型
异常值在金融数据里特别棘手。一方面,它可能是数据录入错误;另一方面,它也可能是真实的极端行情(比如2020年原油期货跌到负值)。
4.3.1 常用识别方法
我常用的方法有三种:
| 方法 | 适用场景 | 原理 |
|---|---|---|
| Z-score | 正态分布数据 | 超过均值±3倍标准差 |
| IQR(四分位距) | 偏态分布数据 | 低于Q1-1.5IQR或高于Q3+1.5IQR |
| 可视化法 | 所有数据 | 箱线图、散点图 |
# Z-score 方法
from scipy import stats
z_scores = np.abs(stats.zscore(df['收盘价'].dropna()))
outliers = df[z_scores > 3]
# IQR 方法
Q1 = df['收盘价'].quantile(0.25)
Q3 = df['收盘价'].quantile(0.75)
IQR = Q3 - Q1
outliers_iqr = df[(df['收盘价'] < Q1 - 1.5*IQR) | (df['收盘价'] > Q3 + 1.5*IQR)]
4.3.2 处理策略
识别出异常值后,怎么处理?我的经验是分情况讨论:
- 明显错误(比如股价为负数):直接删除或修正
- 极端但真实(比如金融危机时的暴跌):保留,但做缩尾处理
- 不确定:用中位数或前后值替换
# 缩尾处理(将异常值替换为边界值)
def winsorize_series(s, limits=[0.01, 0.01]):
lower = s.quantile(limits[0])
upper = s.quantile(1 - limits[1])
return s.clip(lower, upper)
df['收盘价_缩尾'] = winsorize_series(df['收盘价'])
4.4 知识体系总览
下面这张图总结了数据清洗的核心逻辑,你可以对照着梳理自己的流程:
4.5 实战建议
最后,我想分享几点实战中的体会:
- 先备份原始数据:我每次清洗前都会复制一份原始数据,万一处理错了还能恢复
- 记录处理日志:删了多少行、填充了什么值,都记下来。这在做报告时特别有用
- 不要过度清洗:有时候「脏数据」反而包含信息。比如异常值可能预示着市场异动
好了,数据清洗这部分就聊到这儿。记住一句话:垃圾进,垃圾出。花在数据清洗上的时间,永远值得。