3、数据获取与预处理:使用Tushare/Akshare获取A股行业指数数据、数据清洗、缺失值处理、数据标准化

好,咱们进入实操环节。数据获取与预处理,说白了就是给策略模型「喂饭」。饭要是馊了,再牛的厨师也白搭。我见过太多人把精力全花在模型调参上,结果数据源就有问题,最后跑出来的曲线漂亮得不像话,一上实盘就崩。嗯,咱们先把地基打牢。

3.1 数据源的选择:Tushare vs Akshare

国内做量化,绕不开这两个库。我个人习惯是:研究阶段用Akshare,生产环境用Tushare。为什么?

  • Akshare:免费、无需Token、接口丰富。适合快速验证想法。缺点是偶尔接口会变,稳定性稍弱。
  • Tushare:需要注册获取Token,有积分门槛。但数据质量高,接口稳定,有专业版支持。

我在项目中遇到过一个问题:用Akshare拉取历史数据时,某天突然返回空值,排查了半天发现是对方网站改版了。所以如果你做生产环境,建议用Tushare,或者两个库做交叉验证。

我的小建议: 初期学习阶段,先用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(涨跌幅)这种直接可用的列。

注意: 申万行业指数代码在Tushare里是6位数字加.SI后缀,比如801010.SI。Akshare里是'sw'加6位数字。别搞混了。

3.3 数据清洗:别让脏数据毁了你的策略

数据拿到手,第一件事不是分析,是清洗。我见过有人直接拿原始数据跑回测,结果某天出现了一个涨停板,仔细一看是除权除息日没处理。嗯,这种坑我踩过。

清洗步骤一般包括:

  1. 去重:检查是否有重复日期
  2. 排序:按日期升序排列
  3. 异常值处理:比如涨跌幅超过±20%的,大概率是数据错误
  4. 复权处理:行业指数一般不需要复权,但个股需要

代码示例:

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 知识体系总览

说了这么多,咱们用一张图把整个流程串起来:

数据获取与预处理流程 数据获取 Tushare / Akshare 数据清洗 去重 / 排序 / 异常值 缺失值处理 ffill / interpolate / drop 数据标准化 Z-score / Min-Max 聚类输入数据 行业×日期 收益率矩阵 行业轮动策略 聚类 → 轮动信号 每个阶段都有对应的Python代码实现 数据质量决定了策略的上限 数据获取 → 清洗 → 缺失值处理 → 标准化 → 聚类输入

3.7 避坑指南

最后,分享几个我踩过的坑:

  • 时间对齐问题:不同行业的交易日可能不完全一致,比如某个行业指数在2015年股灾期间停牌了。合并数据时要用pd.mergeinner模式,只保留共同交易日。
  • 未来数据泄露:标准化时如果用到了未来的统计量(比如整个时间序列的均值和标准差),那就相当于偷看了未来数据。一定要用滚动窗口扩展窗口做标准化。
  • 行业分类变更:申万行业分类在2021年做过一次大调整,有些行业被合并或拆分。如果你拉取2019年的数据,要注意代码是否对应。
一个小技巧: 在数据预处理阶段,每做一步都保存一个副本。比如df_rawdf_cleandf_scaled。这样出了问题可以回溯,不用从头再来。

好了,数据获取与预处理就讲到这里。记住一句话:数据质量决定了策略的上限,模型只是逼近这个上限。下一节咱们会基于这些干净数据,开始做聚类分析。

专注资料整理