基本面因子库:四大族与口径陷阱
财报一季度才更新一次,而且晚到几十天——怎么把它正确铺到日频
技术指标的输入是价格,天天都有。基本面因子的输入是财报——一个季度才更新一次,而且晚到几十天才公布(p1.7 实测平均 54 天)。
于是它有两个技术指标没有的问题:
- 怎么铺到日频?用
effective_date(=披露日)前向填充。实测:如果用报告期对齐,52.5% 的交易日会拿到还没公告的数字 - 口径陷阱。同一个"ROE"有几种算法,同一个"净利润"有含不含非经常损益之分——而数据源不会警告你
这一节产出一张日频、PIT 对齐的因子面板,供下一节做 IC 与分层。
月薪是月底发的,但它描述的是整个月的劳动。
如果你做家庭现金流分析,把 10 月的工资记在 10 月 1 日——你会以为自己整个 10 月都很宽裕,而实际上前 30 天你一直在花积蓄。
财报也一样:它描述的是过去一个季度,但你拿到它是在几十天之后。
一、PIT 财务面板长什么样
30 只标的、961 条季度记录(2018-03-31 ~ 2025-12-31),三个日期列缺一不可:
| symbol | stat_date | pub_date | effective_date | roeAvg | YOYNI |
|---|---|---|---|---|---|
| sh.600000 | 2018-03-31 | 2018-04-28 | 2018-04-28 | 0.033516 | -0.012835 |
| sh.600000 | 2018-06-30 | 2018-08-30 | 2018-08-30 | 0.065959 | 0.013253 |
| sh.600000 | 2018-09-30 | 2018-10-31 | 2018-10-31 | 0.098112 | 0.029908 |
| sh.600000 | 2018-12-31 | 2019-03-26 | 2019-03-26 | 0.124674 | 0.027508 |
看第四行:2018 年 12 月 31 日的年报,2019 年 3 月 26 日才公布——中间隔了 85 天。
一个真实的多版本案例
面板里有 2 条记录属于同一个 (symbol, stat_date):
| symbol | stat_date | pub_date | roeAvg |
|---|---|---|---|
| sh.600018 | 2019-12-31 | 2020-01-10 | NaN |
| sh.600018 | 2019-12-31 | 2020-04-10 | 0.115 |
1 月 10 日先出了一份(业绩快报性质,ROE 字段还是空的),4 月 10 日才是完整数据。
这正是 p1.7 纪律三的实例:按新的 pubDate 再插一条,不要覆盖旧的。因为在 4 月 10 日之前,你当时能看到的确实是那份不完整的版本。如果你用今天的完整数据去回测 2020 年 2 月的决策——那还是未来函数。
二、四个口径陷阱
① ROE:平均净资产 vs 期末净资产
baostock 的 roeAvg 用的是平均净资产。若与用期末净资产的数据源混用,同一家公司同一期能差出几个百分点——而你不会收到任何警告。
② 净利润:含不含非经常性损益
netProfit 含非经常性损益(卖房、政府补贴、资产处置)。用它算成长因子,一次卖楼就能造出一个"高成长"公司。
做因子应优先用扣非口径;若数据源只有含非经常的,必须在文档里写明。
③ 单季 vs TTM vs 年化
单季数据有季节性(Q4 常有集中结算),直接同比会误判;TTM 平滑但滞后。
规则:同一个因子库里,所有字段必须用同一种口径,并把口径写进列名。roe_ttm 和 roe_q 是两个字段,不是一个字段的两种叫法。
④ 缺失值:fillna(0) 是灾难
本面板各因子的缺失率:
| 因子 | 缺失率 |
|---|---|
| gpMargin | 16.75% |
| quickRatio | 16.75% |
| currentRatio | 16.75% |
| YOYAsset / YOYEquity | 0.21% |
| roeAvg / npMargin / YOYNI / liabilityToAsset | 0.10% |
负债率缺失填 0 = 假装这家公司没有负债。
而在按因子排序选股时,"零负债"会被排到最优的一端——你的策略会系统性地偏好数据缺失的公司。
正确做法:缺失就是缺失,让它保持 NaN。横截面排序时它自然被排除,而不是被塞进最好或最坏的那一端。
同理,fillna(median) 也不是免费的:它会让缺失样本聚集在中位数附近,压缩因子的分布尾部。任何填充都要有理由,并写进文档。
🔧 动手做:查一查你的基本面因子,是不是"提前知道"了(5 分钟)
本节的 PIT 财务面板实测:披露滞后平均 53 天、最长 121 天。跑 python fundamental_factors.py 看它怎么用 merge_asof 按生效日(披露日) 把季度财报铺到日频。
回去查你(或任何)的基本面因子:那个 ROE / PE,是按财报的报告期贴到日线上的,还是按披露日?如果是报告期,你在每个季度末就"用上"了一个还要几十天才公布的数字。
想明白:基本面因子最隐蔽的坑不是算错,是对齐错——用报告期对齐 = 提前几十天偷看业绩,一种价格检查抓不到的未来函数。正确做法是每个财务值带上披露日,用 merge_asof 只贴"披露日 ≤ 当日"的那条。基本面因子,先问:对齐的是报告期还是披露日?
三、铺到日频:用 merge_asof 按生效日对齐
# 关键:按 effective_date 向后对齐 —— t 日只能看到 t 日及之前公布的报告
m = pd.merge_asof(prices, factors.sort_values("effective_date"),
left_on="date", right_on="effective_date",
direction="backward")
direction="backward" 是这行代码的灵魂:它保证每一个交易日拿到的,都是在那一天之前最近一次公布的财报。
实测结果:日频面板 30 只标的、116504 行,各因子覆盖率 42%–51%(覆盖率不满 100% 是因为价格从 2010 年起,而财务面板从 2018 年起)。
对照:如果按报告期对齐
全 panel:ROE 取值不同 31774 / 116504 (27.3%)
只看有因子值的行:31774 / 60500 (52.5%)
⚠️ 全 panel 那个 27.3% 是被稀释过的——2018 年之前两种对齐都是 NaN、算作"相同"。
看第二行才是真实影响面:52.5%。与 p1.7 那节的 47.8% 属同一量级:错误对齐会让一半左右的交易日,握着还没公告的数字。
📌 免责:本课为技术教学,标的为沪深300 成分股按代码序取前 30 只(非精选),仅用于方法演示,不构成投资建议。
四件事:
- 三个日期列缺一不可:stat_date(描述哪一期)、pub_date、effective_date(下游只许用它 join)
- 多版本要保留:实测面板里就有一例——2020-01-10 的快报(ROE 为空)与 2020-04-10 的完整年报。按新 pubDate 再插一条,不覆盖
- 四个口径陷阱:ROE 的分母、净利润含不含非经常、单季/TTM/年化、以及
fillna(0)会让策略系统性偏好数据缺失的公司 - 铺到日频用
merge_asof(direction="backward")。错误对齐的影响面:52.5% 的有效交易日
下一节是 P2 章的压轴,也是分清"专业"与"教程"的地方:因子评价体系。前六节我们一直在问"这个信号后面涨不涨"——那是事件研究的问法。做因子要换一个问法:在同一天的所有标的里,这个因子能不能把未来收益排出顺序?
IC、ICIR、分层、衰减、换手——一个因子要同时过这五关,才有资格进入 P5 的策略。
🔎 来源与核验· 4 条,点开核对
