4、数据清洗基础:缺失值处理(删除、填充、插值),重复值检测与去重
数据清洗,说白了就是给数据「洗澡」。
我做了这么多年量化,见过最离谱的数据集,缺失率高达 40%,还有一堆重复的行情记录。你想想看,拿这种数据去跑因子,结果能信吗?
今天我们就来聊聊数据清洗里最基础、也最核心的两个动作:缺失值处理和重复值处理。
核心观点:清洗不是越狠越好,而是「对症下药」。缺失值怎么补、重复值怎么删,取决于你的数据场景和业务逻辑。
一、缺失值处理:三种策略
缺失值在金融数据里太常见了。停牌、节假日、数据源故障,都会导致空值。我个人习惯,拿到数据第一件事就是 df.isnull().sum(),先摸清底细。
1. 删除缺失值
最粗暴,但也最有效——前提是你丢得起。
import pandas as pd
# 删除包含任何缺失值的行
df_clean = df.dropna()
# 删除某列缺失的行
df_clean = df.dropna(subset=['close', 'volume'])
# 删除缺失比例超过 50% 的列
df_clean = df.dropna(thresh=len(df) * 0.5, axis=1)
⚠️ 注意:删除操作会丢失信息。如果缺失率超过 30%,我建议你慎重考虑。我曾经在回测中删掉了 40% 的数据,结果因子表现虚高——因为删掉的都是熊市数据。
2. 填充缺失值
填充比删除更「温柔」。常用的填充方式有几种:
| 填充方式 | 适用场景 | 代码示例 |
|---|---|---|
| 固定值填充 | 缺失值有明确业务含义(如 0) | df.fillna(0) |
| 均值/中位数填充 | 数值型特征,分布较稳定 | df.fillna(df.mean()) |
| 前向/后向填充 | 时间序列数据(如股票价格) | df.fillna(method='ffill') |
| 分组填充 | 不同组别差异大(如不同股票) | df.groupby('code').fillna(method='ffill') |
举个例子。处理股票数据时,停牌日的价格是缺失的。用前向填充(ffill)最合理——停牌期间价格不变嘛。
# 按股票代码分组,前向填充停牌日价格
df['close'] = df.groupby('code')['close'].fillna(method='ffill')
💡 我的经验:千万别一股脑全用均值填充。金融数据有很强的时序特性,均值填充会破坏序列的连续性。我见过有人把收益率缺失值填 0,结果夏普比率直接翻倍——这完全是假象。
3. 插值法
插值比简单填充更「聪明」。它利用已知数据点之间的趋势来估算缺失值。
# 线性插值
df['close'] = df['close'].interpolate(method='linear')
# 时间插值(适用于时间序列)
df['close'] = df['close'].interpolate(method='time')
# 多项式插值(高阶,慎用)
df['close'] = df['close'].interpolate(method='polynomial', order=2)
为什么会用插值?说白了,金融数据很多时候是连续的。比如 5 分钟 K 线,中间缺了一根,用前后两根线性插值,比用均值填充合理得多。
关键区别:填充是「猜一个值放进去」,插值是「根据趋势算一个值放进去」。插值更尊重数据本身的形态。
二、重复值检测与去重
重复值比缺失值更隐蔽。缺失值一眼就能看到 NaN,重复值可能藏在数据里,悄无声息地放大你的统计量。
1. 检测重复值
# 检查完全重复的行
df.duplicated().sum()
# 检查某几列是否重复
df.duplicated(subset=['code', 'date', 'close']).sum()
# 查看具体的重复行
df[df.duplicated(keep=False)]
我遇到过最离谱的一次,某只股票一天内被记录了 5 次相同的行情数据。原因是数据源重复推送了。如果不处理,那天的收益率计算就会出问题。
2. 去重策略
| 策略 | 说明 | 代码 |
|---|---|---|
| 保留第一个 | 默认行为,保留首次出现的行 | df.drop_duplicates() |
| 保留最后一个 | 适合时间序列,保留最新记录 | df.drop_duplicates(keep='last') |
| 全部删除 | 只要重复就全删,一个不留 | df.drop_duplicates(keep=False) |
| 按列去重 | 只根据关键列判断重复 | df.drop_duplicates(subset=['code', 'date']) |
# 按股票代码和日期去重,保留最新一条
df_clean = df.sort_values('date').drop_duplicates(
subset=['code', 'date'],
keep='last'
)
⚠️ 避坑指南:我曾经在合并多个数据源时,忘了去重就直接算因子。结果因子值被重复计算了两次,相关性分析完全跑偏。后来我养成了一个习惯:任何数据合并操作之后,第一件事就是检查重复值。
三、实战中的注意事项
- 先处理缺失值还是重复值?我个人习惯先处理重复值。因为重复行会影响缺失值的统计量(比如均值),先去掉重复,缺失值处理更准确。
- 不要盲目删除。金融数据里,缺失本身可能包含信息。比如某只股票长期缺失交易数据,说明它流动性差。直接删掉,反而会高估组合的流动性。
- 记录清洗日志。我每次清洗都会记录删了多少行、填了多少值。这样回测出问题时,能快速定位是数据问题还是策略问题。
# 一个完整的清洗流程示例
def clean_data(df):
# 1. 去重
before = len(df)
df = df.drop_duplicates(subset=['code', 'date'], keep='last')
print(f"去重删除 {before - len(df)} 行")
# 2. 处理缺失值
df['close'] = df.groupby('code')['close'].fillna(method='ffill')
df['volume'] = df.groupby('code')['volume'].fillna(0)
# 3. 删除仍然缺失的行(比如开头几行)
df = df.dropna(subset=['close'])
return df
💡 最后说一句:数据清洗没有银弹。你花 80% 的时间在清洗上,一点都不夸张。但正是这 80% 的时间,决定了你后面 20% 的建模工作能不能站得住脚。