4. 选股池构建(下):数据清洗与预处理
上一节我们把股票池拉出来了,但别急着跑策略。你想想看,从交易所拿到的原始数据,就像刚从菜市场买回来的菜——带着泥、带着根,甚至还有烂叶子。直接下锅?那味道肯定不对。
数据清洗与预处理,说白了就是给数据“摘菜洗菜”的过程。我个人习惯把这步分成三个核心动作:处理缺失值、复权处理、对齐时间序列。这三件事做不好,后面的回测全是自欺欺人。
核心原则:宁可少数据,不要脏数据。缺失值处理不当,会导致协整检验出现假阳性,这是配对交易的大忌。
4.1 处理缺失值:别让“空”坑了你
缺失值在金融数据里太常见了。停牌、节假日、数据源抽风,都可能导致某只股票某天没有价格。我刚开始做配对交易时,就吃过这个亏——没处理缺失值直接跑协整检验,结果发现一对“完美”的股票,其实是停牌期硬凑出来的假相关。
处理缺失值,我一般分两步走:
- 先看缺失率:如果某只股票缺失率超过5%,我建议直接剔除。别心疼,市场上几千只股票,不缺这一只。
- 再选填充方法:缺失率低的,用前向填充(ffill)或线性插值。
我的习惯:配对交易里,我倾向于用前向填充。因为股票停牌后复牌,价格通常会跳空,用前一天的价格填充反而更贴近实际交易逻辑。
import pandas as pd
import numpy as np
# 假设 df 是包含多只股票收盘价的 DataFrame,索引为日期
def clean_missing(df, max_missing_rate=0.05):
"""
清洗缺失值
- 删除缺失率超过 max_missing_rate 的股票
- 剩余股票用前向填充
"""
# 计算每只股票的缺失率
missing_rate = df.isnull().mean()
# 删除高缺失率的股票
cols_to_keep = missing_rate[missing_rate <= max_missing_rate].index
df_clean = df[cols_to_keep].copy()
print(f"删除了 {len(df.columns) - len(cols_to_keep)} 只缺失率过高的股票")
# 前向填充剩余缺失值
df_clean = df_clean.ffill()
# 如果开头还有缺失(比如前几行),用后向填充
df_clean = df_clean.bfill()
return df_clean
# 使用示例
df_clean = clean_missing(df_price)
注意:千万不要用均值填充金融时间序列!股票价格是非平稳的,用均值填充会引入严重的偏差。我曾经见过有人这么干,结果回测曲线漂亮得不像话,实盘直接崩了。
4.2 复权处理:还原真实收益率
复权是个老生常谈的问题,但很多人就是不当回事。股票分红、送股、配股之后,价格会跳空。如果不复权,你的收益率计算就是错的。
我个人只用后复权。为什么?因为后复权能保持历史价格走势的连续性,而且当前价格就是实际交易价格,方便计算。
前复权会把历史价格调低,导致早期价格变成负数——这在配对交易里很麻烦,因为价差计算会出问题。
import akshare as ak
def get_adj_close(stock_code, start_date, end_date):
"""
获取后复权收盘价
akshare 的复权因子接口返回的是后复权因子
"""
# 获取不复权数据
df = ak.stock_zh_a_hist(
symbol=stock_code,
period="daily",
start_date=start_date,
end_date=end_date,
adjust="qfq" # 前复权
)
# 实际上,我更推荐用后复权
df_hfq = ak.stock_zh_a_hist(
symbol=stock_code,
period="daily",
start_date=start_date,
end_date=end_date,
adjust="hfq" # 后复权
)
return df_hfq.set_index("日期")["收盘"]
避坑指南:我曾经用前复权数据跑配对交易,发现价差序列在分红日附近出现异常波动。排查了两天才发现是复权方式的问题。换成后复权后,信号干净多了。
4.3 对齐时间序列:让所有股票“步调一致”
不同股票的交易日可能不一样。A股虽然统一交易日,但停牌会导致某只股票在某天没有数据。如果不对齐,你的价差计算就会错位。
对齐的核心思想很简单:只保留所有股票都有数据的交易日。
def align_time_series(df):
"""
对齐时间序列
只保留所有股票都有数据的日期
"""
# 删除任何包含 NaN 的行
df_aligned = df.dropna()
print(f"对齐前:{df.shape[0]} 个交易日")
print(f"对齐后:{df_aligned.shape[0]} 个交易日")
print(f"删除了 {df.shape[0] - df_aligned.shape[0]} 个不一致的交易日")
return df_aligned
# 完整的数据清洗流程
def data_pipeline(df_raw):
"""
数据清洗完整流程
"""
# 1. 处理缺失值
df_clean = clean_missing(df_raw)
# 2. 对齐时间序列
df_aligned = align_time_series(df_clean)
return df_aligned
# 使用
df_final = data_pipeline(df_price)
一个小细节:对齐之后,我习惯检查一下日期是否连续。如果中间有跳空(比如非交易日),用 reindex 补全,然后用前向填充。这样能保证时间序列的完整性。
4.4 实战中的坑与经验
说了这么多理论,我分享几个实战中踩过的坑:
- 坑一:复权因子不一致——不同数据源的复权因子算法可能不同。我建议固定用一个数据源,不要混用。
- 坑二:停牌处理——长期停牌的股票,复牌后往往有补涨或补跌。如果你用前向填充,这个跳空会被忽略。我的做法是:停牌超过20天的,直接剔除。
- 坑三:IPO和退市——新股上市前几个月波动大,退市股就不用说了。我一般会过滤掉上市不满6个月的股票。
重要提醒:数据清洗的每一步都要记录日志。我见过太多人清洗完数据就忘了自己删了什么、填充了什么。等到回测结果异常时,想回溯都找不到原因。
嗯,数据清洗这部分就讲到这里。你可能会觉得这些步骤有点繁琐,但相信我——在数据上偷的懒,都会在回测结果里加倍还回来。把基础打牢,后面的协整检验和策略构建才能站得住脚。