数据清洗基础:缺失值处理、异常值检测与重复数据处理
数据清洗这事儿,说白了就是给原始数据「洗澡」。我做了这么多年量化,见过太多人一上来就搞复杂模型,结果数据里全是坑,跑出来的结果根本没法看。今天咱们就把数据清洗最核心的三个问题讲透:缺失值、异常值、重复数据。
核心观点:数据清洗不是体力活,是决定模型上限的关键一步。你想想看,垃圾数据进,垃圾模型出,这是铁律。
一、缺失值处理
缺失值在金融数据里太常见了。停牌、数据源中断、节假日,都会导致数据缺失。我个人习惯,拿到数据第一件事就是看缺失率。
1. 删除缺失值
最简单粗暴的方法。但要注意,不是所有情况都适合删。
- 行删除:当某行数据缺失太多,比如超过50%的字段都是空值,直接删掉这行
- 列删除:当某列缺失率极高,比如超过80%,这列基本没啥用了
- 条件删除:只删除关键字段缺失的行,比如股票代码、交易日期这些
注意:我曾经在回测时直接删除了所有含缺失值的行,结果发现样本量少了30%,而且被删掉的都是小市值股票,导致回测结果严重偏乐观。嗯,这里要注意,删除前一定要分析缺失模式。
import pandas as pd
import numpy as np
# 查看缺失率
df.isnull().sum() / len(df)
# 删除缺失率超过50%的列
df = df.dropna(thresh=len(df) * 0.5, axis=1)
# 删除关键字段缺失的行
df = df.dropna(subset=['stock_code', 'trade_date'])
2. 均值/中位数/众数填充
这是最常用的方法。但选哪个统计量,得看数据分布。
| 填充方法 | 适用场景 | 我的经验 |
|---|---|---|
| 均值填充 | 数据近似正态分布,无极端值 | 市盈率这类指标可以用,但要注意行业差异 |
| 中位数填充 | 数据有偏分布,存在异常值 | 我习惯用中位数,因为金融数据大多有偏 |
| 众数填充 | 分类变量,或离散型数据 | 行业分类、交易所代码这些用众数 |
# 均值填充
df['pe_ratio'] = df['pe_ratio'].fillna(df['pe_ratio'].mean())
# 中位数填充(我推荐这个)
df['pe_ratio'] = df['pe_ratio'].fillna(df['pe_ratio'].median())
# 按行业分组填充中位数
df['pe_ratio'] = df.groupby('industry')['pe_ratio'].transform(
lambda x: x.fillna(x.median())
)
小技巧:按行业或板块分组填充,效果比全局填充好很多。不同行业的估值水平差异很大,混在一起填会出问题。
3. 前向/后向填充
时间序列数据里,前向填充特别实用。比如某天停牌,用前一天的数据填充,逻辑上说得通。
# 前向填充:用上一个有效值填充
df['close_price'] = df['close_price'].ffill()
# 后向填充:用下一个有效值填充
df['close_price'] = df['close_price'].bfill()
# 限制填充步数,最多向前填充3天
df['close_price'] = df['close_price'].ffill(limit=3)
为什么会限制步数?因为停牌太久,前向填充的数据就失真了。我一般限制在5个交易日以内。
二、异常值检测与处理
异常值在量化里是个双刃剑。它可能是数据错误,也可能是真正的市场异动。我见过有人把2015年股灾的数据当异常值删掉,结果模型在极端行情下完全失效。
1. 3σ原则
假设数据服从正态分布,超出均值±3倍标准差的值视为异常。这个方法简单,但金融数据很少是正态分布。
def detect_outliers_3sigma(data, col, n=3):
mean = data[col].mean()
std = data[col].std()
lower = mean - n * std
upper = mean + n * std
return data[(data[col] < lower) | (data[col] > upper)]
# 找出异常值
outliers = detect_outliers_3sigma(df, 'daily_return')
# 处理:用上下限截断
df['daily_return'] = df['daily_return'].clip(lower, upper)
避坑指南:我曾经直接用3σ处理收益率数据,结果发现很多正常的涨停板都被标记为异常。因为A股有涨跌停限制,收益率分布是截断的,不是正态分布。后来我改用分位数方法。
2. IQR方法
基于四分位距的方法,对数据分布没有严格要求。我个人更推荐这个。
def detect_outliers_iqr(data, col, multiplier=1.5):
Q1 = data[col].quantile(0.25)
Q3 = data[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - multiplier * IQR
upper = Q3 + multiplier * IQR
return data[(data[col] < lower) | (data[col] > upper)]
# 用1.5倍IQR检测
outliers = detect_outliers_iqr(df, 'volume')
# 用3倍IQR检测(更宽松)
outliers_strict = detect_outliers_iqr(df, 'volume', multiplier=3)
你想想看,IQR方法的好处是什么?它不受极端值影响。均值会被极端值拉偏,但中位数和四分位数很稳健。
3. 异常值处理策略
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 删除 | 直接删除异常值所在行 | 明显的数据录入错误 |
| 截断 | 用上下限替换异常值 | 保留样本量,但限制极端影响 |
| 填充 | 用中位数或均值替换 | 异常值较少时 |
| 保留 | 不做处理,单独标记 | 异常值可能是有效信号 |
我的习惯:先标记异常值,而不是直接处理。在因子分析时,我会对比包含和不包含异常值的版本,看看因子表现是否稳健。
三、重复数据处理
重复数据看起来简单,但坑不少。我遇到过因为数据源重复推送,导致某只股票的成交量翻倍,因子计算全错了。
1. 完全重复
所有字段都一样的行,直接删除。
# 检查重复行
df.duplicated().sum()
# 删除完全重复的行
df = df.drop_duplicates()
2. 部分重复
关键字段重复,但其他字段不同。比如同一只股票在同一天有两条记录。
# 基于关键字段去重,保留第一条
df = df.drop_duplicates(subset=['stock_code', 'trade_date'], keep='first')
# 保留最后一条
df = df.drop_duplicates(subset=['stock_code', 'trade_date'], keep='last')
# 标记重复,看看差异
df['is_duplicate'] = df.duplicated(subset=['stock_code', 'trade_date'], keep=False)
注意:不要盲目去重。我曾经在合并多个数据源时,发现某只股票有两条记录,一条是复权价,一条是未复权价。直接去重会丢失重要信息。要先搞清楚重复的原因。
四、知识体系总览
下面这张图,把数据清洗的核心逻辑串起来了。我建议你保存下来,每次做数据清洗前看一眼。
数据清洗没有标准答案,关键是根据业务场景选择合适的方法。我做了这么多年,最大的体会就是:理解数据比掌握方法更重要。你花时间搞清楚数据是怎么来的、缺失的原因是什么、异常值代表什么,比盲目套用各种方法要有效得多。
最后说一句:数据清洗不是一次性工作。随着新数据不断进来,你需要持续监控数据质量。我每周都会跑一次数据质量报告,看看缺失率、异常率有没有异常变化。养成这个习惯,能帮你省掉很多后期排查问题的麻烦。