第二章:数据获取与清洗——用Tushare/Akshare搞定财报数据
做量化投资,数据就是你的弹药。没有干净的数据,再牛的模型也是白搭。
这一章,我带你走一遍数据获取和清洗的全流程。说白了,就是教你怎么从Tushare和Akshare这两个工具里拿到财报数据,然后把它收拾得服服帖帖。
2.1 数据源的选择:Tushare vs Akshare
先说说这两个工具。我个人习惯是两者搭配着用。
| 特性 | Tushare | Akshare |
|---|---|---|
| 数据质量 | 高,经过校验 | 中等,偶尔有异常 |
| 获取方式 | 需要token,有积分限制 | 免费,无需注册 |
| 更新频率 | 实时更新 | 日频更新 |
| 适用场景 | 正式策略、回测 | 快速验证、研究 |
嗯,这里要注意:Tushare的免费额度有限,我建议你先把Akshare用熟了,再上Tushare。我在项目中遇到过,有人一上来就买Tushare的付费套餐,结果发现数据用不上,白白浪费钱。
2.2 用Akshare获取财报数据
Akshare的接口很直观。你想想看,它就是把网页上的数据直接扒下来给你。
import akshare as ak
# 获取利润表
profit_df = ak.stock_profit_sheet_by_report_em(symbol="600519")
print(profit_df.head())
# 获取资产负债表
balance_df = ak.stock_balance_sheet_by_report_em(symbol="600519")
print(balance_df.head())
# 获取现金流量表
cashflow_df = ak.stock_cash_flow_sheet_by_report_em(symbol="600519")
print(cashflow_df.head())
你看,三行代码就把茅台的三张表都拿下来了。但有个坑——Akshare返回的字段名是中文的,而且不同接口的命名风格不统一。我建议你拿到数据后,先做个字段映射。
2.3 用Tushare获取财报数据
Tushare的接口更规范,但需要先注册获取token。我个人习惯把token存在环境变量里,而不是硬编码在代码中。
import tushare as ts
# 设置token
ts.set_token('your_token_here')
pro = ts.pro_api()
# 获取利润表
profit_df = pro.income(ts_code='600519.SH', start_date='20200101', end_date='20231231')
print(profit_df.head())
# 获取资产负债表
balance_df = pro.balancesheet(ts_code='600519.SH', start_date='20200101', end_date='20231231')
print(balance_df.head())
# 获取现金流量表
cashflow_df = pro.cashflow(ts_code='600519.SH', start_date='20200101', end_date='20231231')
print(cashflow_df.head())
Tushare返回的数据字段是英文的,而且命名很规范。比如"营业收入"对应"revenue","净利润"对应"n_income"。这一点比Akshare省心不少。
2.4 数据缺失值处理
拿到数据后,你会发现很多缺失值。为什么会这样?原因很多:有的公司刚上市,历史数据不全;有的公司财报披露延迟;还有的字段本身就是可选的。
我处理缺失值的思路是这样的:
- 先看缺失比例:如果某个字段缺失超过50%,直接扔掉。
- 再看缺失模式:是随机缺失,还是系统性缺失?
- 最后决定填充方式:均值填充、中位数填充,还是用行业均值?
import pandas as pd
import numpy as np
# 假设df是合并后的财报数据
def handle_missing_values(df):
# 1. 删除缺失超过50%的列
threshold = len(df) * 0.5
df = df.dropna(thresh=threshold, axis=1)
# 2. 对数值型字段,用行业均值填充
for col in df.select_dtypes(include=[np.number]).columns:
df[col] = df.groupby('industry')[col].transform(lambda x: x.fillna(x.mean()))
# 3. 对类别型字段,用众数填充
for col in df.select_dtypes(include=['object']).columns:
df[col] = df[col].fillna(df[col].mode()[0])
return df
# 处理缺失值
df_clean = handle_missing_values(df)
2.5 行业分类标准化
行业分类是个大问题。不同数据源用的分类标准不一样:Tushare用申万行业,Akshare用证监会行业,还有的用GICS行业分类。
我建议统一用申万一级行业分类。为什么?因为它是国内最常用的,而且很多因子研究都是基于这个分类做的。
# 行业分类映射
industry_map = {
'银行': '银行',
'证券': '非银金融',
'保险': '非银金融',
'房地产开发': '房地产',
'汽车整车': '汽车',
'半导体': '电子',
'白酒': '食品饮料',
'医药商业': '医药生物',
# ... 更多映射
}
def standardize_industry(df, industry_col='industry'):
df['industry_std'] = df[industry_col].map(industry_map)
return df
# 应用行业标准化
df_clean = standardize_industry(df_clean)
你想想看,如果不做标准化,你在做行业中性化处理时,就会遇到"银行"和"商业银行"被当成两个行业的问题。这会导致因子计算出现偏差。
2.6 数据清洗的完整流程
好了,我们把上面这些步骤串起来,形成一个完整的数据清洗流程。
def data_pipeline(start_date, end_date):
# 1. 获取股票列表
stock_list = get_stock_list()
# 2. 获取财报数据
financial_data = []
for stock in stock_list:
try:
profit = ak.stock_profit_sheet_by_report_em(symbol=stock)
balance = ak.stock_balance_sheet_by_report_em(symbol=stock)
cashflow = ak.stock_cash_flow_sheet_by_report_em(symbol=stock)
# 合并三张表
merged = pd.concat([profit, balance, cashflow], axis=1)
financial_data.append(merged)
except:
continue
# 3. 合并所有股票数据
df = pd.concat(financial_data, ignore_index=True)
# 4. 字段映射
df = map_fields(df)
# 5. 处理缺失值
df = handle_missing_values(df)
# 6. 行业标准化
df = standardize_industry(df)
# 7. 去重
df = df.drop_duplicates()
return df
# 执行数据清洗
df_clean = data_pipeline('20200101', '20231231')
这个流程看起来简单,但实际跑起来会遇到各种问题。比如网络超时、数据格式不一致、字段名变化等等。我建议你加上异常处理和日志记录,方便排查问题。
- 数据获取:Tushare和Akshare搭配使用,取长补短
- 缺失值处理:先看比例,再看模式,最后决定填充方式
- 行业标准化:统一用申万一级行业分类,定期更新映射表
- 完整流程:获取→合并→映射→清洗→标准化→去重
这张图把整个流程串起来了。你从数据获取开始,经过合并、映射、清洗、标准化、去重,最后得到干净的数据。每一步都很关键,缺一不可。
好了,数据获取和清洗就讲到这里。记住:数据质量决定了你的策略上限。花80%的时间在数据上,剩下的20%做模型,这才是量化投资的正确姿势。