第三章 数据获取与清洗:数据源选择、标准化与异常处理
做量化策略,第一步就是搞数据。这一步要是没做好,后面所有模型都是空中楼阁。我个人习惯把数据获取和清洗看作整个策略开发的「地基工程」——地基歪了,楼盖得再漂亮也得塌。
今天咱们就聊聊数据这块。说白了,就是三件事:去哪拿数据、怎么让数据整齐、怎么处理脏数据。
3.1 数据源选择:Wind、Tushare、AKShare
国内做公募量化,数据源就那么几个。我这些年三个都用过,各有各的脾气。
| 数据源 | 特点 | 适用场景 | 成本 |
|---|---|---|---|
| Wind | 机构标配,数据全,接口稳定 | 公募、券商等专业机构 | 高(年费数万) |
| Tushare | 社区活跃,数据覆盖广,有积分限制 | 个人研究、中小团队 | 低(部分免费) |
| AKShare | 开源免费,更新快,接口丰富 | 快速原型、学习验证 | 免费 |
Wind 是行业标准。我在公募时,公司买的Wind终端,每天开盘前自动拉数据。它的好处是数据质量高,财务数据、估值数据都很全。但有个坑——Wind的Python接口(WindPy)偶尔会断连,尤其是长时间跑批量任务时。我曾经有个回测跑了一整夜,早上发现Wind连接断了,数据只拉到一半...嗯,从那以后我养成了「每拉1000条数据就检查一次连接」的习惯。
Tushare 是我个人比较喜欢的。它的数据结构设计得很规范,文档也清晰。不过要注意积分限制——高频数据、历史全量数据都需要消耗积分。我建议新手先用免费接口跑通流程,再考虑付费。
AKShare 是后起之秀。它最大的优势是「快」——接口更新速度极快,经常今天交易所出新规,明天AKShare就支持了。但缺点也很明显:接口稳定性不如前两者,偶尔会有字段名变动的情况。我一般用它做快速验证,正式策略还是用Wind或Tushare。
3.2 数据格式标准化
数据拿到手,第一件事不是分析,而是「对齐」。不同数据源返回的格式千奇百怪——有的日期是字符串,有的是时间戳;有的股票代码带后缀,有的不带。
我一般会做三件事:
- 统一日期格式:全部转为
YYYY-MM-DD格式 - 统一股票代码:去掉后缀,统一为6位数字
- 统一字段命名:比如「收盘价」统一叫
close,别一会儿close_price一会儿closing_price
下面是我常用的标准化函数:
def standardize_data(df, source='wind'):
"""数据标准化处理"""
# 统一日期
if 'trade_date' in df.columns:
df['date'] = pd.to_datetime(df['trade_date'])
elif 'datetime' in df.columns:
df['date'] = pd.to_datetime(df['datetime'])
# 统一股票代码
if 'code' in df.columns:
df['stock_code'] = df['code'].str[:6]
# 统一字段名
rename_map = {
'close': 'close',
'close_price': 'close',
'closing_price': 'close',
'volume': 'volume',
'vol': 'volume',
'amount': 'amount',
'amt': 'amount'
}
df.rename(columns=rename_map, inplace=True)
return df
你想想看,如果每个数据源都用自己的命名方式,写策略时得写多少if-else?标准化这一步,省下来的时间够你喝好几杯咖啡了。
3.3 缺失值处理
缺失值,说白了就是「该有的数据没有」。原因很多:停牌、新股上市、数据源漏了...处理方式就三种:
- 删除:缺失比例太高(比如超过50%),直接删掉
- 填充:用前值、均值、中位数填充
- 插值:用线性插值或时间序列方法补全
我个人习惯是:先看缺失比例,再决定怎么处理。
- 缺失 < 5%:用前值填充(forward fill)
- 缺失 5%-20%:用线性插值
- 缺失 > 20%:直接删除该特征或股票
我曾经遇到过一个情况:某只股票的市盈率数据,连续三个月都是缺失的。一开始我用了前值填充,结果回测表现特别好——后来发现是因为那只股票停牌了,前值填充相当于「用停牌前的数据预测停牌后的走势」,这明显是未来函数。嗯,从那以后我处理缺失值时,一定会先检查「为什么缺失」。
def handle_missing(df, method='ffill', threshold=0.2):
"""处理缺失值"""
# 计算缺失比例
missing_ratio = df.isnull().mean()
# 删除缺失过多的列
cols_to_drop = missing_ratio[missing_ratio > threshold].index
df.drop(columns=cols_to_drop, inplace=True)
# 处理剩余缺失
if method == 'ffill':
df.fillna(method='ffill', inplace=True)
df.fillna(method='bfill', inplace=True) # 开头缺失用后值
elif method == 'interpolate':
df.interpolate(method='linear', inplace=True)
return df
3.4 异常值检测
异常值,就是那些「明显不对劲」的数据。比如某只股票一天涨了1000%,或者市盈率突然变成负数——这些大概率是数据错误。
检测方法主要有三种:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 3σ原则 | 超过均值±3倍标准差视为异常 | 正态分布的数据 |
| 箱线图法 | 超过Q1-1.5IQR或Q3+1.5IQR视为异常 | 非正态分布的数据 |
| 业务规则 | 根据常识设定上下限 | 所有场景(推荐) |
我个人最推荐的是「业务规则法」。为什么?因为统计方法有时候会误判。举个例子:某只股票因为重大资产重组,一天涨了50%。按3σ原则,这可能是异常值;但从业务角度看,这是合理的。所以我的做法是:先用业务规则过滤明显错误,再用统计方法辅助判断。
def detect_outliers(df, col='pct_chg', method='business'):
"""异常值检测"""
if method == 'business':
# A股涨跌幅限制:±10%(ST股±5%)
lower_bound = -0.105 # 留一点容错
upper_bound = 0.105
outliers = (df[col] < lower_bound) | (df[col] > upper_bound)
elif method == 'iqr':
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = (df[col] < lower_bound) | (df[col] > upper_bound)
return outliers
3.5 本章知识体系
下面这张图,把数据获取与清洗的整个流程串起来了。你可以把它当作操作手册——每次拿到新数据,按这个流程走一遍就行。
数据清洗这件事,说白了就是「把原材料处理成能下锅的食材」。别嫌麻烦,这一步做扎实了,后面的策略开发会顺畅很多。我见过太多人急着跑模型,结果被脏数据坑得怀疑人生——嗯,我自己也踩过这个坑。