halo 的技术博客

返回

所有人都爱高 ROE(净资产收益率)。但高 ROE 有两个完全不同的来源:

一种是「真赚钱」——净利率高、资产周转快,经营效率高; 另一种是「借钱堆出来的」——权益乘数(资产/权益)很高,杠杆把 ROE 放大了。

第二种高 ROE 是脆弱的。经济好时它风光无限,一旦下行周期到来、利率上行或现金流断裂,高杠杆会反噬:ROE 从云端直接砸到地下,股价跌得比低杠杆同行狠得多。

1919 年,杜邦公司(DuPont)的财务团队发明了一套把 ROE 拆成三块的分析框架,后来被命名为杜邦分析(DuPont Analysis)。它的价值不在于算一个 ROE 数字,而在于区分高 ROE 是「经营驱动」还是「杠杆驱动」

这篇文章把杜邦拆解写成一个可交易的「清洁 ROE」因子——奖励经营 ROA、惩罚权益乘数,并用 Python 跑通、回测验证,最后诚实拆穿它真实落地时的四类陷阱。

数据声明:全文为自洽合成(经营 ROA 高的公司未来收益高、权益乘数高的公司未来收益低,仅用于演示方法),目的是把杜邦三因子拆解与「清洁 ROE」因子构建机制跑通、可复现。所有量级均为合成校准,真实市场里会被会计粉饰、幸存者偏差、做空约束、危机放大大幅压缩——重点看方法

一、杜邦三因子:ROE 的加性拆解#

杜邦分析的核心恒等式:

ROE=净利润股东权益=净利润营收净利率 m×营收总资产资产周转率 t×总资产股东权益权益乘数 EM\text{ROE} = \frac{\text{净利润}}{\text{股东权益}} = \underbrace{\frac{\text{净利润}}{\text{营收}}}_{\text{净利率 }m} \times \underbrace{\frac{\text{营收}}{\text{总资产}}}_{\text{资产周转率 }t} \times \underbrace{\frac{\text{总资产}}{\text{股东权益}}}_{\text{权益乘数 EM}}

前两项相乘就是经营 ROA = 净利率 × 资产周转率——这是剔除杠杆后的真实盈利能力。于是:

ROE=经营 ROA×EM\text{ROE} = \text{经营 ROA} \times \text{EM}

对数形式下,三者是加性的(这就是杜邦最妙的地方):

ln(ROE)=ln(m)+ln(t)+ln(EM)\ln(\text{ROE}) = \ln(m) + \ln(t) + \ln(\text{EM})

这意味着高 ROE 不管由哪块贡献,拆开看一目了然。一个 ROE=20% 的公司,如果 EM 高达 5 倍(80% 负债),它的经营 ROA 其实只有 4%——表面光鲜,底下是杠杆撑起来的

二、Python 实现杜邦三因子与清洁 ROE 信号#

下面用合成面板把三因子全部实现,并构造「清洁 ROE」信号 ROA_z − EM_z

平均来看,ROE≈9.6%、经营 ROA≈7.5%、EM≈1.28 倍。用对数加性把平均 ROE 拆开,三块各贡献多少一目了然:

平均 ROE 的加性拆解(杜邦三因子)

三、ROE 与权益乘数:杠杆放大但脆弱#

把某个月的股票按 EM 和 ROE 画散点,颜色深浅表示净利率(盈利能力)。可以看到:EM 越高,ROE 被拉得越高,但点密集落在右下端(高杠杆、低盈利)的脆弱区

t0 = M // 2
# EM[:, t0] vs ROE[:, t0],颜色=margin[:, t0]
python

ROE 与权益乘数:高杠杆放大 ROE,但密集在右下端(脆弱区)

这正是杜邦分析的灵魂:ROE 相同,质量天差地别——一个 ROE=15% 但 EM=1.2(低杠杆、高经营效率)的公司,远比一个 ROE=15% 但 EM=5(高杠杆堆出来)的公司稳健。后者一旦经营 ROA 下滑一点,ROE 会被杠杆放大式地崩。

四、回测:多经营驱动、空杠杆驱动#

把每个月的股票按「清洁 ROE」信号 ROA_z − EM_z 排序,信号高(经营驱动)做多、信号低(杠杆驱动)做空,月度再平衡:

def ls_curve(sig, n=60):
    ret = np.zeros(M)
    for t in range(M):
        order = np.argsort(sig[:, t])
        ret[t] = future[order[-n:], t].mean() - future[order[:n], t].mean()
    return np.cumprod(1 + ret)

nav_ls = ls_curve(signal)
print(f"清洁 ROE 因子 终值: {nav_ls[-1]:.2f}")
python

多经营驱动、空杠杆驱动的长短因子长期为正(合成终值约 9.1x)——直觉成立:市场确实给「杠杆脆弱性」定了一个价,经营驱动的高 ROE 公司长期跑赢杠杆驱动的高 ROE 公司:

杜邦清洁 ROE 因子:长期为正

五、单调性:从杠杆驱动到经营驱动收益一路抬升#

按信号十分位看未来 1 月收益,验证信号是不是连续的梯度:

dec_avg = np.zeros(10)
for t in range(M):
    order = np.argsort(signal[:, t])
    for d in range(10):
        idx = order[d*60:(d+1)*60]
        dec_avg[d] += future[:, t][idx].mean()
dec_avg /= M
print("十分位 未来收益 % =", np.round(dec_avg*100, 3))
python

清洁 ROE 信号十分组:单调递增,经营驱动组显著跑赢

从 D1(杠杆驱动)到 D10(经营驱动)收益严格单调递增(约 −0.1% → +1.4%)。这条曲线告诉我们:清洁 ROE 因子的价值在于**「剥离杠杆后看真实经营质量」**——裸 ROE 高低不重要,重要的是 ROE 由什么驱动。

值得强调的是,这种单调性是「因子有效性」的硬证据:如果信号只是被某个极端组偶然撑起,十分位曲线会坑坑洼洼;而严格单调说明它是一条连续的 alpha 梯度。对量化研究者来说,先画十分位单调性、再谈因子,是避免被噪声骗的基本功——杜邦清洁 ROE 在这里站住了。

六、进阶:清洁 ROE 该用在哪一层#

和 Piotroski F-score、Ohlson O-score 一样,杜邦拆解最适合做质量过滤层,而不是独立裸跑:

# 假设已有原始 alpha(如价值、动量信号)
alpha = rng.normal(0, 1, size=(N, M))
# 用清洁 ROE 信号做「质量加权」:高经营质量加权、高杠杆减权
quality_w = np.clip(signal, -1, 1)        # 限制在 [-1,1]
blended = alpha + 0.5 * quality_w         # 把质量叠加进原始信号
python

实务上,杜邦清洁 ROE 是质量因子(QMJ)家族的核心构件——Asness 的质量因子里,「盈利能力」「低杠杆」两条维度,本质上就是杜邦的「经营 ROA 高」和「权益乘数低」。把它叠加进价值/动量策略,能在不显著增加波动的前提下,把「高 ROE 但靠杠杆堆出来」的脆弱股剔除。

七、诚实拆穿四类真实陷阱#

1. 会计粉饰陷阱:净利率、资产周转率都能被短期操纵——通过压费用美化净利率、通过处置资产虚增一次性营收拉高周转率。更隐蔽的是表外杠杆:公司把债务挪到未并表实体,EM 看着低,实际杠杆极高。解法:用「经营现金流/总资产」替代净利润口径的 ROA、结合表外负债排查。

2. 幸存者偏差:高杠杆公司大量在危机中违约、退市、消失,回测里只看到「活下来的」。结果是杠杆驱动组的真实亏损被低估,因子的空头端收益偏小,实盘里空头踩雷可能比回测更惨。必须接入含退市、含违约的全样本。

3. 做空不可得:杠杆驱动组里大量是高负债、低流动性、融资融券标的外的股票,A 股做空几乎不可得。实证上**「只做多经营驱动的高质量股」比多空组合更可落地**,但多头会重新暴露价值/小盘 beta,需要回到第五步做中性化检验。

4. 危机放大陷阱(杜邦特有):清洁 ROE 因子在平静期表现一般,但危机期会被显著放大——高杠杆组在下行周期崩得最狠,因子的空头端收益集中爆发。这既是它的优点(危机保护),也是它的风险:因子的收益高度集中在少数危机月,平静期可能长期横盘,对持有体验和调仓纪律要求很高。


一句话总结:杜邦分析 ROE 拆解是基本面量化里最经典「去伪存真」工具——把高 ROE 拆成经营驱动与杠杆驱动,构建「清洁 ROE」信号,核心价值在**「奖励真赚钱、惩罚借钱堆 ROE」**。但它建立在会计报表真实性的假设上,任何一座「会计粉饰 / 幸存者 / 做空不可得 / 危机放大」的大山没翻过,回测里的漂亮曲线都会在实盘里打折。

(全文为自洽合成演示,量级非真实市场数据;真实落地需接入 wind/聚源财务表、处理退市与违约样本,并叠加高频预警信号与 regime 加权。)

杜邦分析 ROE 拆解因子:把高 ROE 拆成「经营驱动」与「杠杆驱动」
https://blog.halo26812.eu.org/blog/dupont-roe-decomposition
Author halo
Published at 2026年7月20日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨