第四章:数据获取与清洗——选股因子的基石
做量化选股,尤其是市场中性策略,我有个深刻的体会:数据质量决定了策略的天花板。你因子挖得再漂亮,回测曲线再完美,只要数据源头有问题,实盘分分钟教你做人。这一章,我们就来聊聊数据获取与清洗的那些事儿。
核心观点: 数据清洗不是苦力活,而是策略的第一道风控。80%的因子失效问题,根源都在数据上。
4.1 数据源选择:Wind、Tushare、聚宽,怎么选?
我个人习惯把数据源分成三类:机构级、个人级、平台级。选哪个,取决于你的资金量和策略复杂度。
| 数据源 | 特点 | 适用场景 | 成本 |
|---|---|---|---|
| Wind | 数据最全,更新最快,API稳定 | 机构、私募、高频策略 | 高(年费数万) |
| Tushare | 开源免费,社区活跃,接口丰富 | 个人研究、小资金实盘 | 低(积分制) |
| 聚宽 | 内置因子库,回测框架一体化 | 快速原型验证、策略开发 | 免费/付费 |
我在项目中遇到过这样的情况:用Tushare做回测,夏普比率做到2.5,换到Wind数据一跑,直接掉到1.2。为什么?因为Tushare的复权数据在某些时间点有偏差。所以我的建议是:研究阶段可以用免费数据,但实盘前一定要用机构数据做交叉验证。
小技巧: 如果你用Tushare,记得设置好token,并且做好本地缓存。我一般每天只拉一次数据,存到本地SQLite里,避免重复请求被限流。
4.2 数据清洗流程——从原始数据到可用因子
数据清洗,说白了就是把脏数据变成干净数据。我总结了一个四步流程,你想想看,是不是这个理儿?
- 格式统一:日期格式、股票代码、字段命名,全部标准化。
- 缺失值处理:该填充的填充,该删除的删除。
- 异常值处理:剔除那些明显不合理的数据点。
- 一致性检查:比如复权因子是否连续,停牌期间数据是否标记。
嗯,这里要注意:清洗的顺序不能乱。我见过有人先做异常值剔除,再处理缺失值,结果把正常数据当异常给删了。正确的做法是:先处理缺失,再处理异常。
4.3 缺失值处理——别让NaN毁了你的因子
缺失值在金融数据里太常见了。停牌、新股上市、数据源漏传,都会导致NaN。处理方式有三种:
- 直接删除:如果缺失比例小于5%,且是随机缺失,直接删行。
- 向前填充:用上一个交易日的数据填充。适用于停牌、节假日。
- 插值法:线性插值或时间加权插值。适用于短期数据缺失。
我曾经犯过一个错:用均值填充缺失的市盈率数据。结果呢?把一堆高估值股票拉低了,因子排序完全失真。后来我改用行业均值填充,效果好了很多。你想想看,不同行业的市盈率天差地别,用全局均值填充,那不是瞎搞吗?
避坑指南: 我曾经在回测中忽略了新股上市前5天的数据缺失,结果因子在次新股上表现异常。后来我强制要求:上市不满60个交易日的股票,直接剔除。这个规则帮我避开了很多坑。
4.4 异常值处理——识别那些「离谱」的数据
异常值,说白了就是那些明显不符合逻辑的数据点。比如某只股票一天涨了1000%,或者市盈率突然变成负数。这些数据如果不处理,会严重扭曲因子计算。
我常用的方法有三种:
- 3σ原则:超过均值±3倍标准差的数据,视为异常。
- 分位数法:剔除上下1%或5%的极端值。
- 业务规则:比如涨跌幅超过20%的,直接标记为异常。
我个人习惯用分位数法,因为金融数据往往不是正态分布,3σ原则容易误杀。举个例子,A股经常有股票连续涨停,用3σ可能会把正常的数据也剔除掉。而分位数法更稳健,你只需要设定一个阈值,比如上下1%,剩下的交给数据说话。
实战经验: 我在做多因子模型时,发现某个因子在极端行情下表现异常。后来一查,原来是异常值处理得太宽松,把股灾期间的极端数据也纳入了计算。从那以后,我改用滚动窗口的异常值检测,每60个交易日重新计算一次阈值,效果好了很多。
4.5 数据清洗的完整代码示例
说了这么多,不如直接上代码。下面是我常用的数据清洗流程,用Python实现:
import pandas as pd
import numpy as np
def clean_data(df):
# 1. 格式统一
df['trade_date'] = pd.to_datetime(df['trade_date'])
df = df.sort_values(['stock_code', 'trade_date'])
# 2. 缺失值处理:向前填充,最多填充5天
df = df.groupby('stock_code').apply(
lambda x: x.fillna(method='ffill', limit=5)
).reset_index(drop=True)
# 3. 删除剩余缺失值
df = df.dropna()
# 4. 异常值处理:分位数法,剔除上下1%
for col in ['pe', 'pb', 'turnover']:
lower = df[col].quantile(0.01)
upper = df[col].quantile(0.99)
df = df[(df[col] >= lower) & (df[col] <= upper)]
return df
# 使用示例
raw_data = pd.read_csv('stock_data.csv')
clean_data = clean_data(raw_data)
print(f'清洗前:{len(raw_data)}行,清洗后:{len(clean_data)}行')
这段代码看起来简单,但我在实际项目中反复迭代过很多次。比如那个limit=5的参数,就是我从多次回测中总结出来的——填充太多天,数据会失真;填充太少,又会丢失太多样本。
4.6 本章知识体系图
为了让你更直观地理解数据获取与清洗的完整流程,我画了一张SVG图:
这张图把整个流程串起来了。从数据源选择开始,到清洗流程的四个步骤,再到具体的处理方法。你保存下来,以后做数据清洗时对照着看,能少走很多弯路。
最后说一句: 数据清洗没有银弹。不同的策略、不同的因子,对数据的要求都不一样。我的建议是:先跑通,再优化。先用最简单的清洗方法让策略跑起来,然后根据回测结果逐步调整。别一开始就追求完美,那样反而容易陷入细节里出不来。