ROIC 两层拆解
通用方法论,不绑定具体公司。 行次与口径已由用户逐项拍板,除触发提问条件的情形外不要擅自改动。
何时使用
- 用户要求把 ROIC 拆开看驱动因素(是"挣得多"还是"转得快")
- 比较不同公司/不同年份 ROIC 差异的来源
- 做杜邦式分解、资本效率诊断
> 与 roic-calc 的分工:roic-calc 负责算 ROIC 本体(含敏感性、数据库口径对照);
> 本 skill 负责把已算出的 ROIC 拆解成驱动因子。本脚本内部已自带与 roic-calc 完全一致的
> ROIC 计算,无需先跑 roic-calc。两者口径必须一致(见下)。
一、拆解框架
第一层: ROIC = NOPAT/IC = (NOPAT/营收) × (营收/IC)
= 经营净利率 × 投入资本周转率
第二层A(利润表结构):
经营净利率 =(毛利率 − 销售费用率 − 管理费用率 − 研发费用率)×(1 − 实际税率)
注:这是用户选定的【简化口径】,不含税金及附加/财务费用/非经营损益
→ 与 ROIC 口径的严格净利率(NOPAT/营收)有差额,故意不补残差
第二层B(资本占用结构):
IC = 经营性营运资本 NOWC + 经营性长期资本 NLOA
投入资本周转率 = 1 ÷ (NOWC/营收 + NLOA/营收)
关键数学点:资本周转率不能像利润率那样相乘拆解。
两个子周转率的分子都是营收,直接相乘无意义。只能拆分母(资本占用):
营收/IC = 1 ÷ (NOWC/营收 + NLOA/营收),即拆两个"资本密集度"的倒数。
等价权重调和形式:1 ÷ (α/w₁ + β/w₂),α、β 为两类资本占 IC 比重、w 为各自周转率。
第 11、12 行是并列诊断指标,相乘 ≠ 第 5 行。
二、输出行次(定稿,13 行编号、12 行数据)
| 行 | 项目 | 口径 |
|---|---|---|
| 1 | 年份 | 只在表头出现,正文不要重复打印年份行 |
| 2 | ROIC(含商誉) | 商誉保留在 IC |
| 3 | ROIC(剔商誉)主口径 | ω 组全额剔除 |
| 4 | ①经营净利率(简化) | (毛利率−销−管−研)×(1−t) |
| 5 | ②投入资本周转率 | 营收 ÷ IC均值(剔商誉) |
| 6 | 毛利率 | (营收−营业成本)/营收 |
| 7 | 销售费用率 | /营收 |
| 8 | 管理费用率 | /营收 |
| 9 | 研发费用率 | /营收 |
| 10 | 实际税率 | 所得税费用 ÷ 利润总额 |
| 11 | 经营性营运资本周转率 | 营收 ÷ NOWC均值 |
| 12 | 经营性长期资本周转率 | 营收 ÷ NLOA均值 |
| 13 | 投入资本负债率 | 有息负债均值 ÷ IC均值 |
只输出这 12 行——营收/有息负债/IC 等规模量既不进主表、也不单独成表。
> HTML 只输出 12 行主表 + 一条口径注释(2026-09-22 用户明确要求)。
> 原先跟在后面的「参考 · 规模量(均值,亿元)」与「对账 · 恒等式 NOWC + NLOA = IC
> (剔商誉,平均,亿元)」两张表已从 HTML 输出中删除——它们是查错用的中间过程,
> 读者不需要,占版面还打断叙事。
> 但恒等式的计算与终端校验必须保留(终端【对账①】,残差 >1 亿会告警):
> 它是发现"有科目未归类"的唯一防线,删了会静默出错。
三、口径(用户已拍板,不要改)
| 项 | 取值 | 说明 |
|---|---|---|
| 第 4 行净利率 | 简化口径,不算残差 | 与第 2/3 行 ROIC 脱钩。差额来自未扣的税金及附加/财务/非经营。差额在对账②中列出 |
| 营运/长期资本 | 经营性口径 | 剔超额现金与 ω 组金融性资产,与 IC 分母口径一致 |
| 对应口径 | 剔商誉主口径 | 商誉不计入长期资本,与第 3 行闭环 |
| t | 所得税 ÷ 利润总额 | 与 ROIC 一致,不用 25% |
| 减值 | 不加回 | 与 ROIC 一致 |
| θ | 5% | 经营所需现金占营收 |
| 汇兑 | 中性化(剔除) | 与 ROIC 一致 |
ROIC 本体的完整公式与参数见 roic-calc skill(两者必须一致,否则拆解无意义)。
四、执行步骤
- 确认标的与年份:N 年结果需连带取 N−1 年数据(分母取期初期末平均)
- 跑脚本:
bash
python scripts/roic_decomp.py <代码> <年份...> [--name 公司名] [--out 输出目录]
# 例:python scripts/roic_decomp.py SH603259 2018 2019 2020 2021 2022 2023 2024 2025
代码必须带交易所前缀:SH603259 / SZ300759 / SZ300363
3. 必查对账①的残差:NOWC + NLOA 必须等于 IC。残差 > 1 亿说明有科目未归类 →
看【明细】定位,或用未归类科目扫描法(见易错点 2)。残差不是精度问题,是归类漏项。
4. 检查缺失字段行:按 0 处理会失真,需提示用户
5. ω 组触发提问:单项资产 ÷ 期末总资产 > 10% 时停下来问用户性质(经营 vs 金融),不得自行假定
6. 汇报:主表 + 一句话归因(净利率 vs 周转率谁在驱动)+ 提醒 ROIC 需减 WACC 才有结论价值
依赖:akshare、pandas。输出:控制台表格 + roic_decomp_<代码>.html。
> 本机运行环境(Python 解释器、输出目录、本机特有的坑)见同目录 ENV.local.md。
> 该文件每台机器各有一份、不参与跨机同步;本文件(SKILL.md)与 scripts/ 为机器无关内容,两台机器保持一致。
五、经营性资本的归类表(脚本内已实现)
NOWC = 经营性流动资产 + 金融小项 + θ·营收 − 经营性流动负债
经营性流动资产 = 应收票据及账款 + 预付款 + 其他应收款 + 存货 + 合同资产 + 其他流动资产
+ **应收款项融资 FINANCE_RECE**(2019 新准则科目,极易漏!)
经营性流动负债 = 应付票据及账款 + 合同负债 + 应付职工薪酬 + 应交税费 + 其他应付款
+ 应付利息 + 应付股利 + 其他流动负债 + 持有待售负债 + 衍生金融负债
NLOA = 经营性长期资产 − 经营性长期负债
经营性长期资产 = 固定资产 + 在建工程 + 使用权资产 + 无形 + 长摊 + 递延所得税资产
+ 其他非流动资产 (+ 生产性生物资产 + **开发支出 DEVELOP_EXPENSE** + **长期应收款 LONG_RECE**)
经营性长期负债 = 递延收益 + 递延所得税负债 + 其他非流动负债 + 长期应付款
金融小项 = 衍生金融资产 + 一年内到期非流动资产 + 应收利息 + 应收股利 + 持有待售资产
(IC 公式未扣除这几项 → 归入营运资本以保证恒等式成立)
旧准则兜底(≈2018 前,必须**逐年**取非空者,不能全局选列):
合同负债 CONTRACT_LIAB 为空时 → 用「预收款项」ADVANCE_RECEIVABLES(实测 2017 漏 6.04 亿)
交易性金融资产 TRADE_FINASSET_NOTFVTPL 为空时 → 用旧版 FVTPL_FINASSET(并入超额现金)
可供出售金融资产 AVAILABLE_SALE_FINASSET → 属 ω 组,从 IC 中剔除(含商誉口径同)
商誉、长期股权投资、其他非流动金融资产、其他权益工具投资 → 属 ω 组,IC 已剔除,
不进 NOWC/NLOA(剔商誉口径)。
六、易错点(全部踩过)
- 主表末尾不要多加行。营收/有息负债/IC 等规模量用户明确不要进主表,
也不要单独成表进 HTML(2026-09-22 再次强调:连"放到表后独立的【参考】区"都不要)。
且表头已有年份,正文不要再打印一遍"年份"行。 - 恒等式
NOWC + NLOA = IC残差必须查。药明康德首跑 2024 年残差 +25 亿,
定位方法是写脚本把资产负债表所有科目逐一比对归类表,列出未归类且非零的科目。
结果是 45.7 亿负债漏归类:
其他流动负债 34.93 + 持有待售负债 8.66 + 衍生金融负债 2.02 + 应付票据 0.14。
这些负债已通过权益项在 IC 里被扣掉,却不在经营性负债里 → 营运资本虚高。
东财易漏的负债字段:OTHER_CURRENT_LIAB(常是待转销项税)、HOLDSALE_LIAB、DERIVE_FINLIAB。 - 残差影响的是周转率不是 ROIC:IC 没变,ROIC 不变,但营运资本周转率会明显失真
(药明康德 2024 从 4.07 次修正到 5.52 次)。只看 ROIC 对不对发现不了这个问题。
3b. 资产端也会漏项:FINANCE_RECE(应收款项融资)。普洛药业(000739.SZ)首跑
2020–2025 残差 −1.15~−2.53 亿,定位结果全部来自这个科目。
FINANCE_RECE是 2019 年新金融工具准则下的科目,东财字段名独立,
既不在NOTE_ACCOUNTS_RECE里、也不在任何已归类项里 → 直接被丢掉。
注意:NOTE_ACCOUNTS_PAYABLE/NOTE_ACCOUNTS_RECE是合并行,
但东财同时单独提供NOTE_PAYABLE/FINANCE_RECE,容易误判为已包含。
排查套路(最快):拿「未归类资产合计」与残差逐年对照,
残差 ≈ 漏项【两年均值】即为命中——普洛案例 2019 1.22/−1.14、2021 1.34/−1.34、
2022 1.19/−1.15、2024 1.61/−1.57、2025 2.53/−2.53,吻合度极高。
另:DEVELOP_EXPENSE(开发支出)、LONG_RECE(长期应收款)也是常见漏项。
✅ 已修复(2026-09-21):脚本 BS_MAP 已加入fr/devex/lrece三项并计入 oca/onca。
普洛药业 2019–2025 残差已收敛到 ±0.08 亿(修前 −1.15~−2.53 亿)。 - 不要假设
max(...,0)是残差来源。曾假设「超额现金被max(...,0)截断导致
IC 少扣、NOWC 多计」——普洛药业实测证伪:现金 26–38 亿远超营收×5%(4–6 亿),
截断差额恒为 0。先算截断差额再看,别猜。 - 简化净利率 ≠ 严格净利率:第 4 行比 NOPAT/营收 高 0.35~3.88pct,
且差额逐年不稳定。不要试图让第 4 行与第 3 行"对上",用户明确选择脱钩。 - 两条桥都要给:主表给简化口径(用户要的行),对账②给严格闭环
(严格净利率 × 周转率 = ROIC),两者并行不冲突。 - 交易性金融资产字段是
TRADE_FINASSET_NOTFVTPL,不是TRADE_FINASSET(后者常空)。 - 减值在
ASSET/CREDIT_IMPAIRMENT_INCOME(负值为损失),
ASSET_IMPAIRMENT_LOSS全空——sum()会静默返回 0,与金额为 0 无法区分。 - 财务费用取数:加回的是
FE_INTEREST_EXPENSE明细,不是FINANCE_EXPENSE净额
(净额常被利息收入冲抵为负)。 - 年报兜底的触发条件必须含「该年季度记录 <4 条」(2026-09-22 英科医疗实踩) —
原实现只在「季度接口完全没有该年」时补年报接口。
但季度接口的回溯深度因公司而异:英科医疗(300677.SZ)只到 2016Q3,
于是 2016 年有两条记录(Q3+Q4)却不完整,按季度加总把半年当全年 →
2016 年 ROIC 算成 5.28%,正确值 12.90%(差 7.6pct)。
不报错、主表里也看不出异常,比完全缺年份更危险。
→ 已修:触发条件改为qcount.get(y, 0) < 4(季度记录不足 4 条即整年改取年报接口)。
跑新公司前建议先print(df.groupby("Y").size())看一眼各年季度条数。
注:sources-uses全程用年度接口(*_by_yearly_em),不受此坑影响。 -
投资性房地产(
INVEST_REALESTATE)未归类(2026-09-22 英科医疗实踩) —
英科医疗 2019 年起有投资性房地产,2022–2025 达 3.95 / 4.36 / 5.57 / 5.22 亿,
但该科目不在 BS_MAP 里 → 既没进onca、也没被 IC 扣掉,
于是残差随该科目逐年放大:2022 −2.03 / 2023 −4.14 / 2024 −4.96 / 2025 −5.40 亿(约 IC 的 4.5%)。
ROIC 不受影响(IC 没变),但长期资本周转率被高估(2025 0.96 → 修正前 1.02)。
→ 已修:ire并入onca(经营性长期资产)。修复后残差 ±0.04 亿。
排查提示:投资性房地产在制造业里很常见(出租厂房/土地),且金额会逐年长大,
表现为"残差逐年单调放大"而不是恒定值——这是它的指纹。 -
把「规模量表」或「恒等式对账表」输出进 HTML(2026-09-22 用户要求删除) —
用户原话:「参考 · 规模量(均值,亿元)和对账 · 恒等式 NOWC + NLOA = IC
(剔商誉,平均,亿元)这两部分没有必要要」。
→ HTML 现在只输出 12 行主表 + 一条口径注释;终端校验保留。
→ 附带教训(重要):主 skillnon-innovative-annual-review的 4.3 节
2026-09-16 就写过"不要在报告里输出恒等式对账表",但当时只在拼接环节
记了"记得删",没回头改上游roic_decomp.py——
于是此后每次生成的 HTML 都还带着这张表,用户不得不在 9-22 再说一遍(这次连规模量表一起)。
规则:凡"某块内容不要出现在交付物里",先改生成源头,再改拼接说明。
七、验证基准
药明康德 SH603259(2015–2025,含旧准则年份)已跑通,可用作回归测试:
| 年 | ROIC 剔商誉 | 毛利率 | 管理费率 | 营运资本周转 | 长期资本周转 | 投入资本负债率 |
|---|---|---|---|---|---|---|
| 2018 | 29.10% | 39.45% | 11.76% | 17.86 | 1.84 | 17.43% |
| 2021 | 19.52% | 36.28% | 9.62% | 6.97 | 1.45 | 22.45% |
| 2024 | 22.34% | 41.48% | 7.34% | 5.52 | 1.38 | 15.13% |
| 2025 | 34.64% | 47.64% | 5.85% | 5.58 | 1.60 | 18.37% |
2015–2017 补充基准(ROIC 剔商誉):7.97% / 23.51% / 32.34%;2017 年平均 NOWC 为负,
营运资本周转率标 N.M.;2015–2016 利息明细与研发费用缺失,按 0 处理(已知口径瑕疵)。
2018 基准为 29.10%(旧值 27.43% 作废):因 AVAILABLE_SALE_FINASSET(可供出售金融资产)
并入 ω 组,与 roic-calc 同步修复。
第 3 行 ROIC 必须与 roic-calc skill 的输出完全一致(2024 22.34% / 2025 34.64%)。
对账①残差应全部在 ±0.5 亿内。
普洛药业 SZ000739(2003–2025)第二个回归基准(2026-09-17 跑通,2026-09-21 复核):
| 年 | ROIC 剔商誉 | 毛利率 | 三费率合计 | 实际税率 | 投入资本周转 | 长期资本周转 | 投入资本负债率 |
|---|---|---|---|---|---|---|---|
| 2003 | 11.26% | 20.64% | 11.57% | 26.74% | 1.68 | 2.25 | 63.06% |
| 2010 | 3.60% | 21.03% | 15.99% | 26.82% | 1.42 | 1.52 | 42.85% |
| 2020 | 27.19% | 27.96% | 16.59% | 15.03% | 3.03 | 3.54 | 7.31% |
| 2021 | 27.90% | 26.73% | 15.13% | 14.23% | 3.02 | 3.37 | 13.60% |
| 2025 | 16.14% | 26.36% | 16.17% | 16.46% | 2.21 | 2.35 | 28.19% |
英科医疗 SZ300677(2014–2025)第三个回归基准(2026-09-22 跑通)——
专用的两处修复回归样本(季度记录不足 4 条 + 投资性房地产):
| 年 | ROIC 剔商誉 | 毛利率 | 简化净利率 | 营运资本周转 | 长期资本周转 | 投入资本负债率 |
|---|---|---|---|---|---|---|
| 2016 | 12.90% | 24.50% | 8.13% | 7.81 | 2.16 | 36.49% |
| 2020 | 252.25% | 69.03% | 52.68% | 26.97 | 5.86 | 22.56% |
| 2022 | −0.81% | 13.13% | −0.13% | 3.69 | 0.86 | 9.91% |
| 2025 | 6.22% | 24.12% | 8.63% | 5.49 | 0.96 | 127.10% |
- 2014–2015 利息明细与研发费用缺失,按 0 处理(口径瑕疵,同药明 2015–2016)。
- 2016 必须为 12.90%;若看到 5.28%,说明年报兜底触发条件又退回成「完全无数据」了(易错点 10)。
- 残差应 ≤ ±0.05 亿;若 2022–2025 出现 −2~−5 亿的递增残差,说明投资性房地产又漏了(易错点 11)。
- 对账②严格闭环:简化净利率 × 周转率 = 表内 ROIC,12 年逐年吻合。
残差已修复(2026-09-21):早先 2019–2025 残差为 −1.15~−2.53 亿,根因是
FINANCE_RECE(应收款项融资)未归入 NOWC(见易错点 3b)。
脚本补入 FINANCE_RECE + DEVELOP_EXPENSE + LONG_RECE 后,2019–2025 残差全部回到
±0.08 亿内。该漏项对周转率的微调:2021 长期资本周转 3.40→3.37、2025 2.36→2.35。
康龙化成 SZ300759(2023–2025)已验证通用性:ROIC 剔商誉 11.76% / 9.86% / 10.68%,
与 roic-calc 一致;恒等式残差 −0.56 / −0.00 / −0.05 亿。
八、变更记录
- 1.1.0(2026-09-22,用户要求):HTML 输出收敛为「只有 12 行主表 + 一条口径注释」。
① 删除 HTML 里「参考 · 规模量(均值,亿元)」表;
② 删除 HTML 里「对账 · 恒等式 NOWC + NLOA = IC(剔商誉,平均,亿元)」表;
③ 终端的【对账①】【对账②】【明细】【参考】全部保留 ——
恒等式校验是发现"科目未归类"的唯一防线(药明首跑 2024 残差 +25 亿就是靠它抓到的),
删了会静默出错;只是不再进交付物。
④ 修正 §二 与易错点 1 里"另置【参考】区"的旧表述,新增易错点 12。
读数为 12 行主表 + 1 条注释,主表计算未做任何改动。 -
1.0.2(2026-09-22,英科医疗实跑修复两处):
① 年报兜底触发条件补漏——原实现只在「季度接口完全没有该年」时补年报接口,
现改为qcount < 4。英科医疗 2016 年只有 Q3+Q4 两条记录,按季度加总把半年当全年,
2016 ROIC 算成 5.28%(正确 12.90%),不报错也看不出异常。
② 投资性房地产(INVEST_REALESTATE)补入onca——英科医疗 2022–2025 该科目 3.95→5.22 亿,
原未归类导致残差逐年放大到 −5.40 亿(约 IC 4.5%),长期资本周转率被高估。
修复后残差 ±0.04 亿。
回归验证:药明康德 2018–2025 ROIC 与残差逐位不变(该公司投资性房地产为 0);普洛药业、凯莱英亦为 0,不受影响。 -
1.0.1(2026-09-21,双机合并 + 补漏项):以 Gitee 仓库版为基底,并入本机版的
①旧准则逐年兜底(预收款项 / 旧版FVTPL_FINASSET/ 可供出售金融资产)与早年年报接口兜底;
②药明康德 2018 行修正基准(27.43% → 29.10%)与 2015–2017 补充基准。
另把仓库文档中"已诊断但未实现"的FINANCE_RECE漏项在脚本里补齐,
并顺带补上DEVELOP_EXPENSE/LONG_RECE——普洛药业残差由 −1.15~−2.53 亿收敛到 ±0.08 亿。
本机环境信息剥离至ENV.local.md。回归:药明康德 2018–2025 / 康龙化成 2023–2025 / 普洛药业 2019–2025 全部通过。 - 1.0.0:首版。