3、数据获取与清洗:使用Python获取A股红利数据,处理缺失值、异常值、复权价格计算

做量化选股,数据是地基。地基不稳,楼盖得再高也得塌。

我个人习惯,拿到一个策略思路后,第一件事不是写模型,而是先看数据长什么样。尤其是红利因子,涉及分红、送转、除权除息,数据坑特别多。今天咱们就手把手把这块硬骨头啃下来。

3.1 数据源选择与获取

A股红利数据,主流来源有几个:Tushare、AkShare、Baostock。我项目中用得最多的是Tushare Pro,权限够用,字段也全。

不过要注意,Tushare的积分机制有点烦人。刚开始做的时候,我为了省积分,只拉近三年的数据,结果回测结果跟实际偏差很大。后来才发现,分红数据必须拉满上市以来所有记录,复权计算才准。

核心原则:红利数据至少覆盖5年以上,最好包含完整的牛熊周期。分红政策在熊市更稳定,数据质量更高。

下面是一个获取A股分红数据的示例代码:

import tushare as ts
import pandas as pd

# 初始化接口
pro = ts.pro_api('你的token')

# 获取某只股票的分红数据
df_dividend = pro.dividend(
    ts_code='600519.SH',  # 贵州茅台
    fields='ts_code,div_proc,stk_div,stk_bo_rate,stk_co_rate,cash_div_tax,record_date,ex_date'
)

print(df_dividend.head())

这里有个坑:div_proc字段表示分红进度。只有进度为“实施”的记录才是真正到账的。预披露、股东大会通过这些阶段,钱还没到账,不能算。

注意:分红数据中经常出现“预案”、“股东大会通过”等状态。一定要过滤掉,只保留“实施”状态的数据。否则你的红利因子会虚高。

3.2 缺失值处理

数据拿到手,第一件事就是检查缺失值。我见过不少新手,上来就跑模型,结果因为缺失值导致因子值全是NaN,白白浪费时间。

缺失值主要有三种情况:

  • 分红数据缺失:某一年没有分红记录。这很正常,很多公司不是每年都分。
  • 财务数据缺失:比如每股收益、净资产等字段为空。这通常是因为新股上市时间短,历史数据不全。
  • 价格数据缺失:停牌、退市等导致某天没有交易数据。

我的处理原则很简单:

  • 分红缺失:直接填0,表示当年没分红。
  • 财务数据缺失:用最近一期数据向前填充。比如2023年Q1数据缺失,就用2022年年报数据。
  • 价格数据缺失:用前一天的收盘价填充。停牌期间价格不变。
# 处理缺失值示例
df['cash_div_tax'] = df['cash_div_tax'].fillna(0)  # 分红缺失填0
df['stk_div'] = df['stk_div'].fillna(0)            # 送股缺失填0
df['eps'] = df['eps'].ffill()                      # 每股收益向前填充
df['close'] = df['close'].ffill()                  # 收盘价向前填充

小技巧:我习惯在填充前先打印缺失值比例。如果某列缺失超过50%,我会直接删除该列,而不是强行填充。数据质量比数据量更重要。

3.3 异常值检测与处理

异常值在红利数据中很常见。比如某天分红率突然飙到100%以上,或者股息率变成负数。这些明显是数据录入错误。

我常用的检测方法有两种:

  • 3σ原则:超过均值±3倍标准差的值视为异常。
  • IQR方法:低于Q1-1.5*IQR或高于Q3+1.5*IQR的值视为异常。

我个人更倾向IQR方法,因为它不受极端值影响,更稳健。

# IQR方法检测异常值
Q1 = df['dividend_yield'].quantile(0.25)
Q3 = df['dividend_yield'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 标记异常值
df['is_outlier'] = (df['dividend_yield'] < lower_bound) | (df['dividend_yield'] > upper_bound)

# 处理异常值:用中位数填充
median_yield = df['dividend_yield'].median()
df.loc[df['is_outlier'], 'dividend_yield'] = median_yield

嗯,这里要注意:不要直接删除异常值。我曾经在回测中删除了某只银行股的异常分红数据,结果导致那一年因子值缺失,策略直接空仓。后来我改成用中位数填充,效果好了很多。

3.4 复权价格计算

复权价格是红利因子的核心。如果不做复权,分红除息日股价会跳空下跌,导致收益率计算失真。

复权有两种方式:

  • 前复权:调整历史价格,使价格连续。当前价格不变,历史价格按比例缩小。
  • 后复权:调整当前价格,使历史价格不变。当前价格被放大。

做红利因子,我建议用后复权。为什么?因为后复权价格真实反映了分红再投资的收益。你想想看,红利策略的核心就是分红再投资,后复权正好能体现这个逻辑。

后复权价格的计算公式:

后复权价 = 当日收盘价 × 累计复权因子

累计复权因子 = 1 + 所有历史分红/送转的累计影响

具体实现:

def calc_adj_factor(df_dividend, df_price):
    """
    计算后复权因子
    df_dividend: 分红数据,包含除权除息日、每股分红、送转比例
    df_price: 日线价格数据
    """
    # 合并分红和价格数据
    df = pd.merge(df_price, df_dividend, left_on='trade_date', right_on='ex_date', how='left')
    
    # 计算每日复权因子
    df['adj_factor'] = 1.0
    for i in range(1, len(df)):
        # 如果有分红,更新复权因子
        if pd.notna(df.iloc[i]['cash_div_tax']):
            # 分红导致股价下跌,复权因子需要放大
            df.iloc[i, df.columns.get_loc('adj_factor')] = df.iloc[i-1]['adj_factor'] * (1 + df.iloc[i]['stk_div'] + df.iloc[i]['cash_div_tax'] / df.iloc[i-1]['close'])
        else:
            df.iloc[i, df.columns.get_loc('adj_factor')] = df.iloc[i-1]['adj_factor']
    
    # 计算后复权价格
    df['adj_close'] = df['close'] * df['adj_factor']
    
    return df

重要提醒:复权计算时,一定要考虑送转股(stk_div)和现金分红(cash_div_tax)两个因素。很多开源库只考虑现金分红,忽略了送转,导致复权价格偏差。我踩过这个坑,回测结果差了5个点。

3.5 数据清洗流程总结

说了这么多,咱们把整个流程串起来。下面这张图是我做数据清洗的标准流程:

红利数据清洗流程图 1. 数据获取 2. 缺失值处理 3. 异常值检测 4. 复权价格计算 5. 数据输出 Tushare/AkShare fillna / ffill IQR / 3σ 后复权公式 CSV / 数据库 ⚠ 过滤非“实施”状态 ⚠ 分红缺失填0 ⚠ 用中位数填充 ⚠ 考虑送转+现金 ⚠ 检查数据完整性

这张图我贴在了工位旁边,每次做数据清洗都对照着来一遍。流程走完,数据基本就干净了。

3.6 实战中的避坑指南

最后分享几个我踩过的坑,希望能帮你省点时间:

  • 分红数据的时间戳:分红记录中的“除权除息日”才是真正的生效日期。不要用“股权登记日”,那天股价还没调整。
  • 送转股的处理:送转股虽然不涉及现金,但会导致股价除权。复权时必须考虑。我见过有人只做现金分红复权,结果送转后股价看起来暴跌,其实没亏。
  • 停牌期间的分红:如果股票停牌期间发生了分红,复权时要特殊处理。停牌期间没有交易价格,复权因子需要基于停牌前收盘价计算。
  • 数据对齐:分红数据和日线数据的时间频率可能不一致。分红是按事件记录的,日线是按天记录的。合并时要用左连接,以日线为主表。

我的习惯:每次清洗完数据,我都会随机抽几只股票,手动计算一下复权价格,跟系统结果对比。虽然麻烦,但能发现很多隐蔽的bug。数据这东西,再怎么小心都不为过。

好了,数据获取与清洗这块就聊到这儿。记住一句话:数据清洗花的时间,会在模型训练和回测中加倍还给你。别偷懒,每一步都做扎实了。


公众号:蓝海资料掘金营,微信deep3321