3. 数据获取与清洗:用Python搞定REITs公告与行情数据

做量化选基,最怕什么?不是策略不灵,而是数据不准。我见过太多人花大把时间调模型,结果发现原始数据里藏着几个空值,整个回测全白干了。今天咱们就聊聊,怎么用Python把REITs的公告数据和行情数据老老实实抓下来,再洗得干干净净。

核心逻辑:公告数据看分红意愿,行情数据看市场定价。两者结合,才能算出真实的分红率预期。

3.1 爬取REITs公告数据——从PDF里抠出分红信息

REITs的分红公告,通常以PDF形式挂在交易所或基金公司官网。我早期做这个课题时,手动下载过几百份PDF,眼睛都快瞎了。后来写了个爬虫,专门抓取「收益分配公告」这类文件。

这里分享一个我常用的思路:

  1. 确定数据源:上交所、深交所的REITs信息披露页面,或者天天基金、东方财富的REITs专区。
  2. 解析页面结构:用requests+BeautifulSoup抓取公告列表,找到PDF链接。
  3. 下载并解析PDF:用pdfplumber或PyMuPDF提取文本,再用正则匹配出「每份分红金额」「权益登记日」「除息日」等关键字段。
import requests
from bs4 import BeautifulSoup
import pdfplumber
import re

# 示例:抓取某REIT的公告列表
url = "https://example.com/reit/announcements"
resp = requests.get(url)
soup = BeautifulSoup(resp.text, 'html.parser')

# 找到所有PDF链接
pdf_links = []
for a in soup.find_all('a', href=True):
    if '收益分配' in a.text and a['href'].endswith('.pdf'):
        pdf_links.append(a['href'])

# 下载并解析第一份PDF
pdf_url = pdf_links[0]
pdf_resp = requests.get(pdf_url)
with open('temp.pdf', 'wb') as f:
    f.write(pdf_resp.content)

with pdfplumber.open('temp.pdf') as pdf:
    text = ''
    for page in pdf.pages:
        text += page.extract_text()

# 提取分红金额
pattern = r'每份分红[::]\s*([\d.]+)'
match = re.search(pattern, text)
if match:
    dividend_per_share = float(match.group(1))
    print(f'每份分红:{dividend_per_share}元')

避坑指南:我曾经遇到一个坑——有些PDF里的「每份分红」写成了「每10份分红」,差了10倍。所以拿到数据后,一定要跟历史数据做交叉验证。我习惯用akshare的REITs行情数据反推一下,看分红率是否合理。

3.2 利用akshare获取行情数据——一行代码搞定

akshare这个库,说实话,是我用过的免费数据源里最省心的。它把国内REITs的行情数据封装得特别干净。你想想看,以前我们要从东方财富的接口里拼JSON,现在一行代码就搞定。

import akshare as ak

# 获取所有REITs的实时行情
reit_spot = ak.reit_spot_em()
print(reit_spot.head())

# 获取某只REIT的历史日线数据
reit_hist = ak.fund_reit_hist_em(
    symbol="508099",  # 基金代码
    period="daily",
    start_date="20230101",
    end_date="20231231",
    adjust="qfq"  # 前复权
)
print(reit_hist.head())

这里我特别提醒一下:adjust="qfq"这个参数很关键。REITs分红后会除息,如果不做复权处理,价格曲线会出现断崖式下跌,你算出来的收益率会失真。我个人习惯用前复权,这样历史价格已经包含了分红的影响。

注意:akshare的数据源有时会更新延迟,尤其是盘中数据。我建议在收盘后半小时再拉数据,这样比较稳。另外,如果遇到网络问题,可以加个重试机制,比如用tenacity库。

3.3 数据清洗与缺失值处理——脏数据比没数据更可怕

数据拿到手,别急着算分红率。先看看有没有坑。我总结了一套「三步清洗法」:

  • 第一步:检查缺失值——用df.isnull().sum()快速定位。REITs行情数据里,成交量、换手率偶尔会缺失,尤其是刚上市那几天。
  • 第二步:处理异常值——比如某天价格突然涨了50%,但当天并没有分红或拆细,那很可能是数据错误。我会用前后3天的均值做平滑。
  • 第三步:对齐时间轴——公告数据是事件型的,行情数据是时间序列的。需要把分红公告按「除息日」对齐到行情数据上。
import pandas as pd
import numpy as np

# 假设reit_hist是行情DataFrame,dividend_df是分红公告DataFrame
# 合并:按日期左连接
merged = pd.merge(
    reit_hist,
    dividend_df[['除息日', '每份分红']],
    left_on='date',
    right_on='除息日',
    how='left'
)

# 填充缺失的分红数据(非除息日填0)
merged['每份分红'] = merged['每份分红'].fillna(0)

# 处理价格缺失值:用前向填充
merged['close'] = merged['close'].fillna(method='ffill')

# 处理成交量异常:超过3倍标准差则替换为中位数
vol_mean = merged['volume'].mean()
vol_std = merged['volume'].std()
merged.loc[merged['volume'] > vol_mean + 3*vol_std, 'volume'] = merged['volume'].median()

一个小技巧:我习惯在清洗完后,画一张价格与分红事件的叠加图。用matplotlib把分红日标成红点,一眼就能看出数据是否对齐。如果红点落在价格跳空的位置,说明复权处理对了。

3.4 知识体系总览

下面这张图,是我做这个模块时梳理的流程。说白了,就是从两个源头抓数据,然后汇合到一起,洗出干净的分红率计算基础。

REITs数据获取与清洗流程 公告数据源 交易所/基金公司PDF 行情数据源 akshare/东方财富 爬虫/API获取 数据清洗与对齐 缺失值填充 · 异常值处理 · 时间轴对齐 复权处理 · 分红事件匹配 干净的分红率计算数据集 包含:日期、价格、分红金额、复权因子

嗯,到这里,数据获取和清洗的框架就搭好了。你可能会问:「这些代码跑起来会不会很慢?」说实话,几百只REITs的历史数据,用akshare拉一遍也就几分钟。真正耗时的反而是PDF解析那一步——有些公告排版不规范,需要写额外的规则去适配。我建议你先拿5-10只REITs练手,跑通流程后再批量处理。

核心要点回顾:

  • 公告数据用爬虫+PDF解析,重点抓「每份分红」和「除息日」
  • 行情数据用akshare,记得用前复权
  • 清洗时先处理缺失值,再对齐时间轴,最后检查异常值
  • 最终数据集要包含价格、分红金额、复权因子三要素

数据准备好了,下一节咱们就可以正式算分红率了。不过在那之前,我建议你把今天写的清洗函数封装成一个类,后面反复调用会方便很多。这是我在项目里吃过亏后养成的习惯——代码复用,能省一半时间。

专注资料整理