估值因子测试实战:数据获取、清洗对齐与因子构建全流程 1. 估值因子测试的起点为什么数据获取比模型本身更折磨人做单因子测试的人十个里有八个会把大部分精力花在回测框架和绩效归因上但真正跑过完整流程的老手都清楚最耗时间、最容易翻车的环节其实是因子数据的获取与清洗。我前后做过几轮估值类因子的测试从市净率、市盈率到市销率、股息率每一次卡壳都不是卡在IC计算或者分层回测上而是卡在“这个字段到底怎么对齐”“这个负值到底该不该保留”这类看似琐碎的问题上。估值类因子和动量、波动率这类纯量价因子有个本质区别它的分母端依赖财务报表数据分子端依赖市场交易数据两套数据的更新频率、披露时点、复权逻辑完全不同。你从行情接口拿到的收盘价是日频的、连续的但从财务数据源拿到的净资产是季频的、离散的而且还有追溯调整的问题。这两者拼在一起如果对齐逻辑没想清楚算出来的PB就是错的后面所有测试结论都是空中楼阁。这篇内容面向的是已经有一定因子测试基础、准备系统性地把估值类因子跑一遍的朋友。我会把整个流程拆成数据获取、字段理解、清洗对齐、异常处理、因子构建、初步验证几个环节重点讲那些文档里不会写、但实操中一定会遇到的坑。你不需要有很深的财务背景但需要对DataFrame操作和基本的回测逻辑有概念。2. 估值因子的数据版图到底需要拉哪些字段2.1 分子端市值数据的口径选择估值因子的分子端通常是总市值或流通市值。总市值等于总股本乘以收盘价流通市值等于流通股本乘以收盘价。听起来简单但这里第一个坑就是股本口径的选择。总股本在财报里是时点数据但交易所披露的总股本可能包含限售股、B股、H股等不同类别。如果你做的是A股全市场测试用哪个口径的总股本会直接影响因子值的横截面排序。我自己的做法是统一用“总股本未复权”乘以“收盘价未复权”这样得到的市值和财报里的净资产口径最匹配。如果你用复权价去乘股本得到的市值会失真因为复权价已经隐含了分红送转的影响而股本本身没有做同步调整。流通市值的选择相对简单但要注意自由流通市值和流通市值是两个概念。自由流通市值剔除了大股东锁定仓位、战略持股等更接近真实可交易部分。对于估值因子测试如果你关注的是定价效率自由流通市值作为分母更合理如果你关注的是整体估值水平流通市值就够了。我一般两个都算对比看差异。2.2 分母端财务数据的三个关键选择分母端是估值因子的核心也是最容易出问题的地方。以PB为例分母是净资产。净资产在财报里有几个不同的口径归母净资产只包含母公司股东应占部分剔除少数股东权益。这是最常用的口径因为市值对应的就是归母部分。所有者权益合计包含少数股东权益。如果你用总市值除以所有者权益合计分子分母口径不一致算出来的PB会偏低。剔除无形资产后的净资产有些深度价值策略会用有形资产净值但这不是标准PB的口径。我的建议是统一用归母净资产和总市值匹配。PE的分母用归母净利润同理。PS的分母用营业收入这个没有归母和非归母的区别但要注意营业收入的确认口径是合并报表还是母公司报表统一用合并报表。股息率的分母是过去12个月每股股息分子是股价。这里要注意股息是税前还是税后以及是否包含特别股息。我一般用税前股息特别股息单独标记测试时可以选择是否剔除。2.3 数据频率与披露时点的对齐这是估值因子测试中最核心的技术问题。财务数据是季频的但股价是日频的。你不能简单地把季报数据往前填充到每一天因为财报有披露延迟。举个例子某公司2023年一季报的截止日是3月31日但实际披露日可能是4月28日。如果你在4月1日就用了一季报的净资产数据那就是用了未来信息回测结果会严重高估。正确的做法是以财报的“实际披露日”为准在披露日之后才使用该期财务数据。具体操作时你需要一张财报披露日期表把每个报告期对应的披露日拉出来然后和日频行情数据做merge_asof方向是“向后对齐”即每个交易日取最近一期已披露的财报数据。这里还有一个细节同一报告期可能有修正公告。比如年报先出快报再出正式报告再出修正报告。我的处理方式是只取最早披露的那一版因为修正公告的披露日通常晚很多如果用修正后的数据会引入额外的未来信息。但如果你做的是基本面量化研究而非实盘回测用修正后的数据也可以接受前提是你清楚这其中的差异。3. 数据清洗的硬骨头负值、缺失与极端值3.1 负净资产和负净利润的处理逻辑估值因子最头疼的问题之一就是负值。PE的分母是净利润如果净利润为负PE就是负数。负PE在经济学上没有意义你不能说一家亏损公司的市盈率是-15倍然后把它和正PE的公司放在一起排序。常见的处理方式有三种处理方式具体操作适用场景剔除净利润为负的样本直接剔除样本量充足时简单干净取倒数用EP净利润/市值替代PE避免负值问题排序更自然截断将负PE统一设为某个极小值保留样本但会扭曲排序我个人的偏好是用EP替代PE。EP等于净利润除以市值净利润为负时EP为负排序时负EP自然排在最后逻辑上自洽。而且EP和PB、PS这些因子的量纲更一致都是比率形式后续做标准化和中性化处理时更方便。负净资产的情况比较少见但确实存在通常是公司累计亏损超过了实收资本。这种样本我建议直接剔除因为负净资产的PB没有经济含义而且这类公司往往处于退市边缘纳入测试会引入噪声。3.2 缺失值的填充与标记财务数据的缺失有两种情况一种是公司确实没有披露该字段比如某些公司不披露季度净资产另一种是数据源的问题比如接口返回为空。对于第一种情况我的做法是向前填充最近一期的有效数据但设置一个最大填充期限。比如最多向前填充4个季度超过4个季度仍然缺失的标记为NaN在因子计算时剔除。这样做的好处是避免了用太陈旧的数据来代表当前状态同时也不会因为个别缺失值导致大量样本被浪费。对于第二种情况需要做数据源交叉验证。我一般会同时从两个独立的数据源拉取财务数据对比差异。如果某个字段在一个源有值、另一个源为空优先信任有值的那个但要做好标记后续如果发现异常可以追溯。注意向前填充时一定要用披露日对齐后的数据不能直接用报告期对齐。否则会出现“用未来数据填充过去”的问题。3.3 极端值的识别与处理估值因子的极端值比量价因子更常见因为财务数据本身波动就大。一家公司从盈利到亏损PE可能从20倍跳到-200倍这种跳变如果不处理会严重干扰因子排序。我通常用MAD中位数绝对偏差方法来识别极端值。具体步骤是先计算因子的中位数和MAD然后将超过中位数加减5倍MAD的样本标记为极端值用中位数加减5倍MAD的值进行截断。这个方法比3σ法更稳健因为财务数据的分布往往不是正态的标准差容易被极端值拉偏。截断之后我还会做一次winsorize把因子值压缩到1%和99%分位数。这两步做完因子的横截面分布会干净很多后续做分层回测时各组的单调性也会更好。4. 从原始字段到可用因子构建流程的完整拆解4.1 数据合并的关键步骤把行情数据和财务数据合并成一张宽表是整个流程中最需要小心的一步。我一般按以下顺序操作准备行情表包含股票代码、交易日期、收盘价、总股本、流通股本、总市值、流通市值。注意这里的市值是用未复权价算的。准备财务表包含股票代码、报告期、披露日、归母净资产、归母净利润、营业收入、每股股息。每个报告期一行。做merge_asof以行情表的交易日期为左键财务表的披露日为右键按股票代码分组向后对齐。这样每个交易日都能拿到最近一期已披露的财务数据。计算因子值用合并后的数据计算PB、PE、PS、股息率等因子。标记异常对负值、缺失值、极端值做标记和处理。这里有一个性能上的坑如果直接用pandas的merge_asof对全市场几千只股票、几千个交易日做操作内存会爆。我的做法是按股票代码分组逐组做merge_asof然后再concat回去。虽然多了一层循环但内存占用可控而且可以利用多核并行加速。4.2 因子计算的细节陷阱计算PB时分子用总市值分母用归母净资产。但这里有一个时间差问题总市值是交易日的实时值归母净资产是最近一期财报的值。这两者之间的时间差可能长达几个月这期间公司的净资产可能已经发生了重大变化但财报还没披露。这不是bug而是估值因子的固有特性。估值因子本身就是用历史财务数据来评估当前价格的相对水平。但你需要意识到在财报披露前的窗口期因子值的有效性会下降。有些研究会把财报披露后的第一个月作为因子有效期之后逐渐衰减就是这个道理。计算股息率时我用的是过去12个月已宣告的每股股息之和。这里要注意除权除息日的处理股息率应该在除息日之后才反映在股价里所以计算时要用除息日之后的股价。如果你用除息日之前的股价股息率会被低估。4.3 因子标准化与中性化原始因子值不能直接用于分层测试因为不同行业的估值水平天然不同。银行股的PB普遍在0.5到1之间而科技股的PB可能在5以上。如果不做行业中性化你的因子测试结果可能只是在区分行业而不是在区分个股。我的标准化流程是去极值用MAD法截断极端值。标准化做z-score标准化使因子均值为0、标准差为1。行业中性化按申万一级行业分组在每个行业内做z-score标准化。这样每个行业内部的因子分布是一致的跨行业比较才有意义。市值中性化用对数市值做回归取残差作为最终因子值。这一步是为了剔除市值对估值因子的影响因为小盘股往往估值更高。做完这四步因子值才算是“干净”的可以进入回测环节。5. 实测中的意外与排查几个真实踩过的坑5.1 财报披露日数据的获取难题财报披露日这个字段看起来简单实际上很难拿到完整准确的数据。有些数据源只提供报告期不提供披露日有些提供披露日但更新不及时还有些把预约披露日和实际披露日混在一起。我试过几种方案一种是用交易所公布的定期报告预约披露时间表但这个是预约时间实际披露可能提前或延后另一种是用财经数据接口的“公告日期”字段这个相对准确但需要付费还有一种是用报告期加固定延迟来估算比如一季报统一按4月30日、年报按4月30日但这样会引入误差。最终我采用的是混合方案优先用公告日期缺失时用预约披露日加3个交易日的缓冲再缺失时用报告期加固定延迟。虽然不完美但比直接用报告期对齐要好得多。5.2 股本变动的处理总股本不是一成不变的增发、回购、股权激励都会导致股本变动。如果你用最新的股本去乘历史股价算出来的历史市值是错的。正确的做法是用“当时”的股本。但历史股本数据同样不好获取。我的处理方式是用财报里的股本数据按披露日对齐和股价数据合并。这样每个交易日用的都是最近一期财报披露的股本。虽然财报披露有延迟但股本的变动通常不大延迟几个月的影响可以接受。对于发生重大股本变动的公司比如刚做完增发我会单独标记在因子计算时做特殊处理或者直接剔除该窗口期的样本。5.3 停牌和涨跌停的处理停牌期间没有收盘价市值数据是缺失的。我的做法是停牌期间向前填充最近的收盘价但标记为停牌状态。在因子测试时停牌样本可以选择剔除或保留取决于你的测试目的。如果测试的是因子选股能力停牌样本应该剔除因为无法交易如果测试的是因子定价能力停牌样本可以保留但要做好标记。涨跌停的处理类似。涨停时股价被限制市值不能反映真实供需。我一般会在因子测试时剔除涨跌停样本或者至少做敏感性分析看剔除前后结果差异大不大。6. 因子初步验证怎么判断数据处理得对不对6.1 因子分布的直观检查在跑正式回测之前我会先做几个简单的分布检查直方图看因子值是否近似正态分布。如果出现双峰或者长尾说明去极值和标准化可能没做好。时序图看因子均值和标准差随时间的变化。如果均值漂移严重说明数据对齐可能有问题。行业分布看各行业的因子均值是否有系统性差异。如果银行股的PB因子均值显著低于其他行业说明行业中性化没做到位。这些检查不需要复杂的代码用pandas的describe和简单的绘图就能完成但能提前发现大部分数据问题。6.2 IC值的快速验证IC值是因子有效性的最直接指标。我一般会先算Rank IC即因子值和下期收益率的Spearman相关系数。对于估值因子IC的绝对值通常不高在0.03到0.08之间就算不错了。如果算出来的IC超过0.15大概率是数据有问题比如用了未来信息。IC的时序稳定性也很重要。我会看IC的月度均值、标准差和胜率。如果IC均值高但标准差也高说明因子在某些时期有效、某些时期失效需要进一步分析原因。6.3 分层回测的单调性检验把股票按因子值分成5组或10组看各组的下期收益率是否单调。对于估值因子通常是低估值组低PB、低PE收益率高于高估值组。如果分组收益率没有单调性或者单调性反转说明因子方向可能搞反了或者数据有严重问题。我还会看多空组合的净值曲线。如果多空组合的净值曲线平稳向上说明因子有稳定的选股能力如果曲线波动很大或者长期横盘说明因子可能只在特定市场环境下有效。7. 一些提高效率的工程化技巧7.1 数据管道的模块化设计估值因子测试涉及的数据处理步骤很多如果全部写在一个脚本里调试起来非常痛苦。我的做法是把整个流程拆成几个独立的模块数据获取模块负责从各个数据源拉取原始数据输出标准化的DataFrame。数据清洗模块负责处理缺失值、异常值、负值输出干净的宽表。因子计算模块负责计算各类估值因子输出因子值矩阵。因子测试模块负责IC计算、分层回测、绩效归因。每个模块之间用parquet文件或数据库表做衔接这样任何一个环节出问题都可以单独调试不需要重跑整个流程。7.2 缓存机制的引入财务数据的更新频率很低季频数据一年只更新四次。如果每次跑测试都重新拉取财务数据既浪费时间又浪费接口调用次数。我的做法是把财务数据缓存到本地设置一个过期时间比如7天。7天内重复跑测试直接用缓存超过7天才重新拉取。行情数据的更新频率高但历史数据是不变的。我一般会把历史行情数据一次性拉取并缓存每天只增量更新最新一天的数据。这样既保证了数据的完整性又减少了重复请求。7.3 并行计算的合理使用全市场几千只股票、几千个交易日的数据处理单线程跑起来可能要几十分钟。用多进程并行可以大幅缩短时间。我的经验是按股票代码分组并行每组独立做merge_asof和因子计算最后合并结果。这样并行粒度合适不会因为数据量太小导致进程间通信开销过大。但要注意并行计算时每个进程的内存占用会叠加。如果单进程需要2GB内存开8个进程就需要16GB。所以并行度要根据机器的内存来调整不是越多越好。8. 估值因子测试中那些容易被忽略的细节8.1 财报数据的追溯调整上市公司发生并购重组时会对历史财务数据进行追溯调整。比如某公司2023年收购了一家子公司2022年的财报可能会被追溯调整把子公司的财务数据并入。如果你在2023年拉取2022年的财报数据拿到的可能是调整后的数据而不是当时披露的原始数据。这个问题在回测中很致命因为你会用到“未来才调整过的历史数据”。我的处理方式是尽量使用原始披露数据如果数据源只提供调整后的数据就在回测时做敏感性分析看追溯调整对结果的影响有多大。8.2 不同报告期的数据可比性一季报、半年报、三季报、年报的数据口径不完全一样。比如营业收入一季报是单季度数据半年报是累计数据三季报也是累计数据年报是全年数据。如果你直接用累计数据做时序比较会发现Q1到Q4的营业收入在递增但这只是累计效应不是真实的增长。我的做法是把累计数据还原成单季度数据。具体操作是Q1单季等于Q1累计Q2单季等于半年报累计减Q1累计Q3单季等于三季报累计减半年报累计Q4单季等于年报累计减三季报累计。这样得到的单季度数据在时序上才可比。8.3 股息率的计算细节股息率的计算看似简单但有几个细节容易出错股息的口径是税前还是税后是普通股息还是包含特别股息时间窗口是过去12个月还是最近一个完整年度除息日的处理股息应该在除息日之后才计入否则会高估股息率。我一般用过去12个月已除息的税前普通股息之和除以当前股价。特别股息单独标记测试时可以选择是否纳入。对于除息日在未来的股息不计入当前股息率。8.4 因子值的频率选择估值因子是日频更新还是月频更新这取决于你的调仓频率。如果你做的是月度调仓因子值只需要在每月最后一个交易日计算一次。如果你做的是日频调仓因子值需要每天计算。我的建议是因子值按日计算但测试时按调仓频率取样。这样既保留了灵活性又避免了不必要的计算。而且日频因子值可以用于分析因子的短期波动特征对理解因子行为有帮助。9. 从数据到结论一个完整的估值因子测试案例9.1 测试标的与时间窗口的选择假设我们要测试PB因子在A股市场的选股能力。测试标的为全市场A股剔除ST、停牌、上市不足6个月的股票。时间窗口为过去5年按月调仓。这个选择有几个考虑剔除ST是因为ST股票的估值逻辑和正常股票不同剔除停牌是因为无法交易剔除上市不足6个月是因为新股的价格发现过程不充分估值数据不稳定。时间窗口选5年是因为要覆盖至少一个完整的市场周期避免只测试了牛市或只测试了熊市。9.2 因子计算与预处理的完整流程按前面讲的步骤先拉取行情数据和财务数据做merge_asof对齐计算PB因子然后做去极值、标准化、行业中性化、市值中性化。每一步的输出都做记录方便追溯。预处理完成后检查因子的基本统计量均值、标准差、偏度、峰度、缺失率。如果缺失率超过20%需要检查数据源是否有问题。如果偏度绝对值超过2说明分布不对称可能需要做非线性变换。9.3 分层回测与结果解读把股票按PB因子值从低到高分成5组计算每组的等权收益率然后看各组收益率的单调性。如果低PB组的收益率显著高于高PB组说明PB因子有选股能力。我还会计算多空组合的夏普比率、最大回撤、换手率等指标。换手率特别重要因为估值因子的换手率通常较低如果算出来换手率很高说明因子值波动太大可能是数据有问题。9.4 常见问题的排查清单如果测试结果不符合预期我会按以下清单逐一排查问题现象可能原因排查方法IC值异常高用了未来信息检查披露日对齐逻辑分组收益率无单调性因子方向搞反检查因子计算逻辑多空组合波动大极端值未处理检查去极值步骤换手率异常高因子值不稳定检查数据频率和填充逻辑行业分布不均未做行业中性化检查中性化步骤这个清单帮我省了很多时间大部分问题都能在前三步定位到。10. 个人经验估值因子测试的长期维护建议做估值因子测试不是一锤子买卖而是一个需要持续维护的工程。财报数据每季度更新股本数据不定期变动数据源的接口也可能变化。我自己的做法是建立一个自动化的数据管道每周跑一次增量更新每月做一次全量校验。全量校验的内容包括检查因子值的分布是否发生漂移检查IC值是否稳定检查分层回测的单调性是否保持。如果发现异常及时排查数据源或处理逻辑的变化。另外估值因子的有效性会随市场环境变化。在成长股占优的市场里低估值因子可能长期跑输在价值股回归的市场里低估值因子又会表现突出。所以不要因为某段时间因子失效就否定它也不要因为某段时间因子有效就过度自信。保持跟踪定期评估才是长期做因子测试的正确姿势。还有一个容易被忽略的点估值因子的定义本身也在演化。传统的PB、PE之外现在还有EV/EBITDA、P/FCF等更复杂的估值指标。这些指标的分子分母涉及更多字段数据处理的复杂度也更高。如果你准备扩展因子库建议先把基础因子的数据处理流程跑通、跑稳再逐步引入新因子不要一上来就铺太大摊子。