3、数据获取与预处理:使用Tushare/Akshare获取A股行业指数数据、数据清洗、缺失值处理、数据标准化
好,咱们进入实操环节。数据获取与预处理,说白了就是给策略模型「喂饭」。饭要是馊了,再牛的厨师也白搭。我见过太多人把精力全花在模型调参上,结果数据源就有问题,最后跑出来的曲线漂亮得不像话,一上实盘就崩。嗯,咱们先把地基打牢。
3.1 数据源的选择:Tushare vs Akshare
国内做量化,绕不开这两个库。我个人习惯是:研究阶段用Akshare,生产环境用Tushare。为什么?
- Akshare:免费、无需Token、接口丰富。适合快速验证想法。缺点是偶尔接口会变,稳定性稍弱。
- Tushare:需要注册获取Token,有积分门槛。但数据质量高,接口稳定,有专业版支持。
我在项目中遇到过一个问题:用Akshare拉取历史数据时,某天突然返回空值,排查了半天发现是对方网站改版了。所以如果你做生产环境,建议用Tushare,或者两个库做交叉验证。
3.2 获取行业指数数据
咱们做行业轮动,核心数据是各行业指数的日频行情。A股常见的行业分类有申万一级、中信一级等。我一般用申万一级,28个行业,不多不少,刚好够用。
先看Akshare怎么拿数据:
import akshare as ak
import pandas as pd
from datetime import datetime, timedelta
# 获取申万一级行业指数历史行情
# 以 'sw' 开头,后面跟行业代码
# 比如 'sw801010' 是农林牧渔
industry_code = 'sw801010'
start_date = '20200101'
end_date = '20231231'
df = ak.stock_zh_index_daily(symbol=industry_code)
df['date'] = pd.to_datetime(df['date'])
df = df[(df['date'] >= start_date) & (df['date'] <= end_date)]
print(df.head())
再看Tushare的写法:
import tushare as ts
# 初始化pro接口
pro = ts.pro_api('你的token')
# 获取申万一级行业指数
# 接口名:index_daily
df = pro.index_daily(ts_code='801010.SI',
start_date='20200101',
end_date='20231231')
print(df.head())
你想想看,两个库的返回格式其实差不多,都是DataFrame。但Tushare返回的字段更规范,比如有pct_chg(涨跌幅)这种直接可用的列。
3.3 数据清洗:别让脏数据毁了你的策略
数据拿到手,第一件事不是分析,是清洗。我见过有人直接拿原始数据跑回测,结果某天出现了一个涨停板,仔细一看是除权除息日没处理。嗯,这种坑我踩过。
清洗步骤一般包括:
- 去重:检查是否有重复日期
- 排序:按日期升序排列
- 异常值处理:比如涨跌幅超过±20%的,大概率是数据错误
- 复权处理:行业指数一般不需要复权,但个股需要
代码示例:
def clean_data(df):
# 去重
df = df.drop_duplicates(subset=['date'])
# 排序
df = df.sort_values('date').reset_index(drop=True)
# 异常值处理
# 涨跌幅超过±20%视为异常
df = df[(df['pct_chg'] > -20) & (df['pct_chg'] < 20)]
return df
df_clean = clean_data(df)
3.4 缺失值处理:填还是不填?
这是个经典问题。行业指数数据一般比较完整,但偶尔也会因为停牌、节假日等原因出现缺失。我的处理原则是:
- 连续缺失不超过3天:用前向填充(ffill)
- 连续缺失超过3天:用插值法(interpolate)
- 缺失超过10天:直接删除该时间段
我曾经在回测中发现某个行业连续缺失了半个月数据,用ffill填充后,策略在那段时间表现异常好。后来一查,原来是那个行业停牌了,数据根本不应该存在。所以,缺失值处理要结合业务逻辑,不能无脑填充。
# 前向填充
df['close'] = df['close'].ffill()
# 线性插值
df['close'] = df['close'].interpolate(method='linear')
# 删除缺失过多的行
df = df.dropna(thresh=len(df.columns) - 2)
3.5 数据标准化:让不同行业站在同一起跑线
不同行业的指数点位差异很大,比如银行股可能只有几千点,而科技股可能上万点。直接拿原始点位做聚类,结果会被高点数行业主导。所以需要标准化。
常用的标准化方法有两种:
| 方法 | 公式 | 适用场景 |
|---|---|---|
| Z-score标准化 | (x - μ) / σ | 数据近似正态分布 |
| Min-Max标准化 | (x - min) / (max - min) | 数据有明确边界 |
我个人习惯用Z-score。为什么?因为行业轮动策略关注的是相对强弱,Z-score能保留数据的分布特征,而Min-Max会把极端值压缩得太厉害。
from sklearn.preprocessing import StandardScaler
# 假设df_pivot是行业×日期的收益率矩阵
# 行是日期,列是行业
scaler = StandardScaler()
df_scaled = pd.DataFrame(
scaler.fit_transform(df_pivot),
index=df_pivot.index,
columns=df_pivot.columns
)
这里要注意:标准化应该在每个时间截面上做,而不是在整个时间序列上做。也就是说,对于每一天,把所有行业的收益率标准化。这样才能捕捉到当天的行业相对强弱关系。
3.6 知识体系总览
说了这么多,咱们用一张图把整个流程串起来:
3.7 避坑指南
最后,分享几个我踩过的坑:
- 时间对齐问题:不同行业的交易日可能不完全一致,比如某个行业指数在2015年股灾期间停牌了。合并数据时要用
pd.merge的inner模式,只保留共同交易日。 - 未来数据泄露:标准化时如果用到了未来的统计量(比如整个时间序列的均值和标准差),那就相当于偷看了未来数据。一定要用滚动窗口或扩展窗口做标准化。
- 行业分类变更:申万行业分类在2021年做过一次大调整,有些行业被合并或拆分。如果你拉取2019年的数据,要注意代码是否对应。
df_raw、df_clean、df_scaled。这样出了问题可以回溯,不用从头再来。
好了,数据获取与预处理就讲到这里。记住一句话:数据质量决定了策略的上限,模型只是逼近这个上限。下一节咱们会基于这些干净数据,开始做聚类分析。