Victor's Code Journey
Victor's Code Journey

目录

指标分解模型:加法、乘法与残差

周会上,老板问了一个看起来很简单的问题:好物优选这个月 GMV 环比涨了 35%,这 35% 里,有多少是访客数 UV 的功劳?

三位分析师给了三张表。

分析师用的方法UV 的贡献CVR 贡献AOV 贡献
ALMDI−35.11%+60.75%+74.36%
B链式替代,UV→CVR→AOV−100 万+180 万+270 万
C链式替代,CVR→UV→AOV−120 万+200 万+270 万

三张表内部都自洽,都能加回同一个 +350 万,谁也没算错。但业务只想知道一个数:UV 到底该背多少?

B 和 C 用的是同一个方法、同一批数据,只因为替换顺序不同,UV 的贡献就从 −100 万变成了 −120 万;A 换了个方法,又是第三个数字。

问题的根子在于:**归因的答案不由数据决定,由你选的分解模型决定。**同一批数据,分解模型不同,拆出来的贡献就不同;模型选错,答案连方向都可能反过来。

本文是系列的第 02 篇,把"分解模型"这件事讲透:三类模型各自的前提、算法和适用边界,以及最容易被一笔带过的那个数——残差。它从哪来、能有多大、怎么处理。这三类方法在能源与环境指标领域已有几十年的综述脉络,Ang 与 Zhang 的调查 梳理了 IDA 方法的谱系,本文只取其中与业务归因最相关的三条线。

章节回答的问题核心结论
一加法型指标怎么拆Delta 法:Vᵢ = ΔXᵢ,零残差,但只对"边际贡献恒为 1"的模型成立
二乘法型指标怎么拆LMDI:取对数把乘法变加法,贡献度恒为 100%,无残差
三乘法模型还能怎么拆链式替代:总和精确,但单项依赖替代顺序
四模型写不出来怎么办泰勒展开、定基代替:只能用近似,必然留残差
五残差从哪来ROI 四因子算例:残差占 83.3%,全部来自交互项
六残差怎么办单列其它 / 权重分摊 / Shapley 修正,三选一
七怎么落地三个可直接运行的 Python 函数

最简单的模型是加法型:好物优选的 GMV 按渠道拆开,就是各个渠道 GMV 之和。

Y = X₁ + X₂ + ... + Xₙ

第一篇 里那张渠道表,其实就是一次 Delta 分解。Delta 法的全部内容只有一行:

Vᵢ = Xᵢ¹ − Xᵢ⁰

因子 i 的绝对贡献,就是它自己的变化量。不需要任何参数、权重或假设。

**为什么这件事在加法模型上成立、换个模型就不成立?**因为加法模型里 Y 对每个 Xᵢ 的偏导恒等于 1:多一块钱就是多一块钱。少一个渠道的 GMV 就是少这么多,没有第二种可能。

第一篇定义了全系列通用的三个口径。把 Delta 的公式代进去,恒等式是结构给的,不是凑出来的:

概念记号Delta 下的公式恒等式一句话读法
绝对贡献VᵢXᵢ¹ − Xᵢ⁰ΣVᵢ = ΔY因子 i 自己变了多少
贡献度CᵢVᵢ / ΔYΣCᵢ = 100%这次变化里因子 i 占几成
相对贡献RᵢVᵢ / Y⁰ΣRᵢ = ΔY / Y⁰相当于基期大盘的多少

三条恒等式互相锁死:Vᵢ = Cᵢ × ΔY,Rᵢ = Vᵢ / Y⁰。第一篇说"相对贡献加起来正好是环比涨幅",在加法模型下不是巧合——ΣRᵢ = ΔY / Y⁰ 就是环比增速的定义式。

两个用惯了容易忽略的点:

**Cᵢ 可以为负,也可以超过 100%。**渠道表里搜索是 −11.43%(拖后腿),直播是 +51.43%。如果某个渠道涨得比大盘还多,它的 Cᵢ 就会超过 100%,同时别的渠道必然是负数——加起来才是 100%。这不是算法出错,是"份额"这个口径在有正有负时必然出现的样子。

**三个恒等式只在"各部分可加"时成立。**比率型指标(转化率、ROI)拆开加不回去,所以这几个恒等式在它身上全部失效。换模型不是"换个更好看的算法",是换一套前提。

现实里很少有系数全是 1 的加法。更常见的是加权求和:会员分层的加权客单价,Y = Σ aᵢXᵢ,aᵢ 是各层级的占比,Xᵢ 是各层级的客单价。

若 aᵢ 固定(各层级占比没变),Delta 直接推广:

ΔY = Σ aᵢ ΔXᵢ

**这里有个高频错误:很多资料写成 ΔY = Σ ΔaᵢXᵢ。**两项一换,被求和的对象就变了——ΔaᵢXᵢ 量的是"权重本身变了",不是"权重乘的那个量变了"。一个最直观的反证:权重固定时 Δaᵢ 全为零,这个式子给出 ΔY = 0,而指标明明动了。

权重也在变的时候,完整展开是三项:

ΔY = Σ (aᵢ⁰ΔXᵢ + ΔaᵢXᵢ⁰ + ΔaᵢΔXᵢ)
        规模效应   结构效应     交互项

拿会员分层加权客单价算一遍。基期金卡占比 20%、客单价 800 元,银卡 50%、400 元,普通 30%、150 元;当期变为 25% / 900 元、50% / 400 元、25% / 140 元。

层级基期占比 a⁰当期占比 a¹基期客单价 X⁰当期客单价 X¹
金卡20%25%800 元900 元
银卡50%50%400 元400 元
普通30%25%150 元140 元

加权客单价从 405 元涨到 460 元,ΔY = +55 元。三项拆开:

分项含义金额
规模效应 Σa⁰ΔXᵢ各层级客单价自身变动+17.00
结构效应 ΣΔaᵢXᵢ⁰占比变化(高客单层级占比上升)+32.50
交互项 ΣΔaᵢΔXᵢ占比与客单价同时变动+5.50
合计+55.00

误写的 ΣΔaᵢXᵢ 算出来是 +32.50(X 换成当期值则为 +38.00),既不是 ΔY = +55,也不是三项里的任何一项——它只是碰巧等于结构效应那一行。

结构效应这一项,正是很多归因报告里"高价值用户占比提升带动了大盘“那句话的出处。它和 Delta 的 Vᵢ 不是一回事:Vᵢ 说的是"这个维度自己动了多少”,结构效应说的是"结构变了、由此带来多少"。

这三个分项在第五章会原样再出现一次——交互项换个名字,叫残差。

好物优选的 GMV 换个维度拆,是乘法:

GMV = UV × CVR × AOV

三个原因让 Delta 法在这里彻底用不上。

**第一,量纲不一致。**UV 的单位是"万",CVR 是"%",AOV 是"元"。Delta 法的 ΔXᵢ 只有在同量纲下才能相加,"−10 万 + 1 个百分点 + 50 元"不是一个数。

**第二,也是更本质的一条:边际贡献不恒定。**Delta 法成立的前提是 dY/dXᵢ ≡ 1。乘法模型里:

∂GMV/∂UV = CVR × AOV

UV 少了 10 万访客,损失多少 GMV,取决于当时的 CVR 和 AOV 分别是多少——是个变量,不是常数。乘法模型天生带交互,Delta 那套"一个因子一个固定贡献"的记账法接不住。

第三,乘法模型不能写成加法。(1 + u₁)(1 + u₂)(1 + u₃) ≠ 1 + u₁ + u₂ + u₃,交叉项无法摊回到加法账上。

LMDI(Logarithmic Mean Divisia Index,对数平均狄氏指数)的全部招数是一行取对数:

lnY = Σ lnXᵢ   ⟹   ΔlnY = Σ ΔlnXᵢ

乘法变成了加法,而且是恒等式,不是近似。既然 ΔlnY 严格等于各项之和,贡献度就只能是这么定义:

Cᵢ = ΔlnXᵢ / ΔlnY

于是 ΣCᵢ ≡ 100%。这个 100% 是除出来的,不是调出来的——无残差是 LMDI 最大的优点,而且它是结构性的保证。

要拿到绝对贡献,再乘一个对数均值权重:

L(Y⁰, Y¹) = (Y¹ − Y⁰) / (lnY¹ − lnY⁰)
Vᵢ = L(Y⁰, Y¹) · ΔlnXᵢ

因为 L(Y⁰,Y¹) · ΔlnY = ΔY,所以 ΣVᵢ = ΔY 严格成立。相对贡献 Rᵢ = Vᵢ / Y⁰ 照样加回环比增速,和 Delta 下的情形一模一样。

符号上要注意:Cᵢ > 0 表示因子的变化方向与指标一致(ΔlnXᵢ 与 ΔlnY 同号)。指标整体在跌时,“同向"的因子读作跟着一起跌。

还有一个容易误读的地方:**Cᵢ 是"对数变化的份额”,不是"绝对金额的份额"。**UV 自身只跌了 10%,C 却是 −35.11%。别把它读成"UV 拖垮了 35% 的 GMV"——它说的是"在 GMV 的对数变化里,UV 的对数变化占了 −35.11%"。要谈金额,得看 Vᵢ。

因子上月本月Δln X贡献度 Cᵢ
UV(访客,万)10090−0.105361−35.11%
CVR(支付转化率)5%6%+0.182322+60.75%
AOV(客单价,元)200250+0.223144+74.36%
GMV(万元)10001350+0.300105100%

折算成金额和相对贡献:

因子绝对贡献 Vᵢ(万元)相对贡献 Rᵢ
UV−122.88−12.29%
CVR+212.63+21.26%
AOV+260.24+26.02%
合计+350.00+35.00%

ΔGMV = +350 万,ΣVᵢ = +350 万,ΣCᵢ = 100%,ΣRᵢ = +35.00%——和 GMV 的环比涨幅一致。

三列数字要分开读。Cᵢ 是"对数变化的份额",UV 的 −35.11% 意思是"UV 的对数变化占 GMV 对数变化的 −35.11%",不是"UV 拖走了 35% 的 GMV"。Vᵢ 才是金额:CVR 和 AOV 合计把 GMV 推上去 472.87 万,UV 拉回来 122.88 万,净额 +350 万。UV 是唯一的拖累项,但它的绝对量比任何一个正向因子都小。

和第一篇对照一下会发现一个有意思的地方:同一个 +350 万,按渠道拆是"直播贡献 51.43%",按经营因子拆是"AOV 贡献 74.36%"。两句话都成立,因为它们回答的不是同一个问题——渠道维度回答"哪个渠道",因子维度回答"哪个经营杠杆"。汇报之前先说清楚你拆的是哪个维度,否则两个数字会在同一场会上打架。

三个必须写进文档的前提。

**因子必须为正。**取对数的定义域不允许零或负数。UV 为 0、CVR 为 0 时 LMDI 直接失效,得换方法或做 log 修正。

**LMDI 是记账口径,不是因果。**Cᵢ 度量的是"这个因子在对数尺度上动了多少",它不告诉你"如果 UV 不动,GMV 会怎样"。UV 和 CVR 本身相关时(比如流量涨、转化率跌),Cᵢ 的解读要格外小心。

**模型必须是纯乘积。**Y = ΠXᵢ 才行。有加减结构的指标(比如收入减成本)得先改写成乘积形式,或者用别的方法。

链式替代(chain substitution,也叫连环替代法)的规则只有一条:定一个替换顺序,每次只把一个因子换成当期值,每一步 Y 的变化算作这一步因子的贡献。

flowchart TB
    A["基期 Y⁰ = 1000 万"] --> B["替换 UV → 900 万"]
    B --> C["替换 CVR → 1080 万"]
    C --> D["替换 AOV → 1350 万"]
    D --> E["三步差值合计 +350 万 = ΔGMV"]

以 UV → CVR → AOV 的顺序为例:

步骤中间值本步贡献
基期100 × 5% × 200 = 1000—
换 UV = 9090 × 5% × 200 = 900−100
换 CVR = 6%90 × 6% × 200 = 1080+180
换 AOV = 25090 × 6% × 250 = 1350+270
合计+350

链式替代的一个漂亮性质是:不管用什么顺序,每一步的贡献加起来必然等于 ΔY。

这不需要推导就能看出来——每一步贡献是相邻两个中间值之差,三个差值求和时中间值两两抵消,最后剩下 Y³ − Y⁰ = ΔY。这是个望远镜求和,对任何函数形式都成立,链式替代从来不留残差。

但单项贡献依赖顺序,这一点在乘法模型上可以精确写出来。设 Y = X₁X₂X₃,替换顺序为 1 → 2 → 3:

V₁ = X₂⁰X₃⁰(X₁¹ − X₁⁰)
V₂ = X₁¹X₃⁰(X₂¹ − X₂⁰)
V₃ = X₁¹X₂¹(X₃¹ − X₃⁰)

规律很清楚:**因子 i 的贡献 = ΔXᵢ × 已替换因子的当期值 × 未替换因子的基期值。**换句话说,一个因子被折算成金额时,用的是"哪一期的价格",取决于它在替换序列里的位置。

替换顺序UV 贡献CVR 贡献AOV 贡献合计
UV → CVR → AOV−100+180+270+350
CVR → UV → AOV−120+200+270+350
AOV → UV → CVR−125+225+250+350

拿 UV 的三次数值验一下上面那条规律。UV 掉了 10 万访客,损失多少 GMV 取决于按哪期的转化率和客单价折算:

替换顺序UV 前面已换了谁折算方式计算结果
UV→CVR→AOV无另两因子都在基期−10 × 5% × 200−100
CVR→UV→AOVCVR用当期 CVR、基期 AOV−10 × 6% × 200−120
AOV→UV→CVRAOV用当期 AOV、基期 CVR−10 × 250 × 5%−125

同一个 UV 掉 10 万访客,在三种顺序下被折算成 −100、−120、−125 万,差 25%。这不是计算错误,是"这 10 万访客按什么价折算"这个隐含假设不同。

实践上有两条建议。**顺序要按业务因果链定,并且写进报告。**UV → CVR → AOV 符合"先有流量、再有转化、最后才有客单价"的业务顺序,反过来读会别扭。跨期对比时顺序必须固定,否则这个月的 −100 和上个月的 −125 放在一起没有意义。

不过更省心的做法是:如果指标本身是纯乘积形式,直接用 LMDI——它的贡献度与顺序无关,从根上绕开这个问题。

指标既不是加法也不是纯乘积时(比如 ROI 这种比率),可以用一阶泰勒展开近似。原理是"其它因子不动时,这个因子动一点,指标动多少":

ΔY ≈ Σ (∂f/∂Xᵢ)(X⁰) · ΔXᵢ

拿 ROI = (CU × ARPU) / (UV × CAC) 算,在基期点求偏导:

因子偏导 ∂f/∂XᵢΔXᵢ一阶贡献
CUARPU/(UV·CAC) = +0.00127273+200+0.254545
ARPUCU/(UV·CAC) = +0.01818182−10−0.181818
UV−CU·ARPU/(UV²·CAC) = −0.00011570−1000+0.115703
CAC−CU·ARPU/(UV·CAC²) = −0.25454545+1−0.254545
合计−0.066115

ΔROI = −0.072727,泰勒合计 −0.066115,残差 −0.006612,占 9.1%。

三个必须知道的局限。

**第一,只在因子小幅变化时成立。**其它三个因子照旧,只把 UV 的目标值从 10000 换成 5000(UV 自身的变化幅度从 −9.1% 变成 −54.5%),泰勒残差从 9.1% 涨到 66.5%。用泰勒拆一个剧烈变化的指标,等于用直线去拟合曲线。

**第二,在哪个点展开会影响结果。**同一组数据,改在当期点求偏导,合计变成 −0.080000,残差 +0.007273——残差不但从 −0.0066 变成 +0.0073,连符号都反了(前者与 ΔROI 同号,后者反号)。

**第三,高阶项被整体丢掉,全部进了残差。**泰勒不是"更精确的 Delta",它是"带残差的近似"。

这套思路在工业界有真实应用。广告系统的收入调试工具 Adtributor 面对的正是这类问题——CPC 这种派生指标不是各元素的加总,没法直接拆。它的做法是用有限差分偏导近似每个元素的贡献,再归一化到 100%,见 NSDI 2014 原文。第 07 篇会专门讲这个算法。

还有一个细节值得留意:在这个 ROI 上,泰勒和定基代替对 CU、ARPU 给出的贡献完全相同(+0.254545 和 −0.181818),只有 UV 和 CAC 不同。因为 ROI 对 CU、ARPU 是线性的(一阶展开对这个因子是精确的),对分母上的 UV、CAC 是非线性的(一阶展开必然漏掉曲率)。这提示了下一节:选方法前,先看指标对哪个因子线性。

定基代替(base substitution)的规则更朴素:把其它所有因子冻结在基期,只把一个因子换成当期值,看指标变成多少。

Vᵢ = f(其它因子取基期, Xᵢ 取当期) − f(全部取基期)

它的优点很实在:不需要偏导,对任意函数都成立(含分段函数、含 max 的模型也能用),而且含义直白——“如果只有 UV 变了,ROI 会变成多少”。

代价也很实在:各因子的贡献是各自"孤立"算出来的,加起来通常不等于 ΔY,差额就是残差。

ROI = (CU × ARPU) / (UV × CAC),四个因子:

项基期当期
CU 转化用户数10001200
ARPU 单用户收入(元)7060
UV 流量数1100010000
CAC 单流量用户成本(元)56
收入 I = CU × ARPU7000072000
成本 C = UV × CAC5500060000
ROI = I / C1.2727271.200000

定基代替,四个因子逐个替换:

只替换的因子算出的 ROI贡献 Vᵢ
CU = 12001200×70/55000 = 1.527273+0.254545
ARPU = 601000×60/55000 = 1.090909−0.181818
UV = 100001000×70/50000 = 1.400000+0.127273
CAC = 61000×70/66000 = 1.060606−0.212121
合计−0.012121

ΔROI = 1.200000 − 1.272727 = −0.072727。四个因子的贡献合计只有 −0.012121。

残差 = ΔROI − ΣVᵢ = −0.072727 − (−0.012121) = −0.060606
残差占比 = 0.060606 / 0.072727 = 83.3%

很多资料在表格末尾加一行"残差:舍入误差,可忽略",一笔带过。**这个说法在这里是错的。**残差占了 83.3%,意味着这四个因子各自单独的影响加起来,只解释了 ROI 变化的 16.7%。

这个数字有直接的业务后果。CU 单独把 ROI 抬高了 0.254545,是 ΔROI 绝对值的 3.5 倍——用单因子的贡献除以总变化,会得到 350% 这种荒谬的百分比,因为分母太小。根本原因是:真实变化里八成来自"几个因子一起动",而单因子视角按定义看不见这种联合效应。在残差占八成的模型上,“某因子贡献了总变化的百分之多少"这个问法本身就不成立,能说的只有"如果只有这个因子动了,指标会变成多少”。

残差不是算错的零头,它有精确的定义。把 ROI 改写成纯乘积形式:

ROI = CU × ARPU × (1/UV) × (1/CAC)

四个变量做完全展开(乘积展开成 16 项,减去恒定项后 15 项),ΔROI 恰好等于这 15 项之和——1 阶 4 项、2 阶 6 项、3 阶 4 项、4 阶 1 项。逐阶算下来:

阶数含义金额
1 阶(4 项)各因子单独变化,即定基代替那四项−0.012121
2 阶(6 项)两个因子同时变化的联合效应−0.062424
3 阶(4 项)三个因子同时变化+0.001212
4 阶(1 项)四个因子同时变化+0.000606
合计−0.072727

于是残差 = 二阶及以上各项之和 = −0.062424 + 0.001212 + 0.000606 = −0.060606,与 5.2 算出的残差分毫不差。这不是巧合,是恒等式:残差恰好等于所有交互项之和。

flowchart TB
    A["ROI = CU × ARPU × (1/UV) × (1/CAC)"] --> B["1 阶 4 项
定基代替那四个贡献
合计 −0.012121"] B --> C["2 阶 6 项
两两交互
合计 −0.062424"] C --> D["3 阶 4 项 + 4 阶 1 项
多元交互
合计 +0.001818"] D --> E["合计 = ΔROI = −0.072727
残差 = 二阶及以上 = −0.060606"]

交互项不是抽象概念,换成业务语言就能看见。下面两个例子在收入和成本的尺度上展示它的形态——注意这是元,不是 ROI,ROI 上的交互项还要再经过比值关系的放大。

**收入端:**转化用户数 +200 和 ARPU −10 是同时发生的。单独看 CU,收入贡献 +200 × 70 = +14000;单独看 ARPU,收入贡献 1000 × (−10) = −10000。两者加起来 +4000。但实际收入只从 70000 涨到 72000,即 +2000。差额 −2000 = ΔCU × ΔARPU,就是这两个因子互相对冲掉的部分。

**成本端:**流量 −1000 和 CAC +1 也是同时发生的。单独看 UV,成本贡献 −1000 × 5 = −5000;单独看 CAC,成本贡献 11000 × 1 = +11000。加起来 +6000,但实际成本只从 55000 涨到 60000,即 +5000。差额 −1000 = ΔUV × ΔCAC。

所以残差的来源可以一句话说完:**残差是"多个因子一起动"产生的联合效应,它天然无法归给任何单一因子。**任何只看单因子的方法都会有残差,残差的大小则取决于单因子效应在总变化里占多少。

先说一个反直觉的结论:**残差是分解模型的属性,不是指标的固有属性。**同一个 ROI,换成 LMDI 拆(因为 ROI = CU × ARPU × (1/UV) × (1/CAC) 是纯乘积),残差直接归零:

因子LMDI 贡献定基代替贡献
CU+0.225351+0.254545
ARPU−0.190531−0.181818
UV+0.117804+0.127273
CAC−0.225351−0.212121
合计−0.072727−0.012121

选对模型,残差可以为零。选错了,就得处理。下面三种处置按"诚实程度"递增。

做法:把残差作为单独一行写进归因表,标签叫"其它/未解释项"。

因子绝对贡献
CU+0.254545
ARPU−0.181818
UV+0.127273
CAC−0.212121
其它(交互项)−0.060606
合计−0.072727

优点是诚实:表格加总对得上,而且明确告诉读者"这 83.3% 不是我拆出来的"。

缺点是它在这个例子里几乎没用:83.3% 的变化落在"其它"这一行里,这一行比 CU、ARPU、UV、CAC 中的任何一项都大,读者拿到的信息量约等于零。而且"其它"是个垃圾桶标签,下游一定会问"其它里面到底是什么"。

**残差确实小的时候(经验上低于 10%),单列其它是完全合理的做法。**它保留了全部信息,只是把未解释的部分摆在明面上。

做法:定一套权重,把残差按权重摊回各因子,让贡献加总刚好等于 ΔY。最常见的权重是按 |Vᵢ| 的占比:

因子定基代替 Vᵢ权重分摊残差分摊后
CU+0.25454532.81%−0.019886+0.234659
ARPU−0.18181823.44%−0.014205−0.196023
UV+0.12727316.41%−0.009943+0.117330
CAC−0.21212127.34%−0.016572−0.228693
合计−0.012121100%−0.060606−0.072727

加总对上了,饼图也能画了。问题是权重是人定的:换成按业务重要性定权重、或者按因子的绝对变化量定权重,结论就跟着变。更要命的是,“把交互项摊给单个因子"在数学上是把一件本质上属于多方的效应,硬塞给某一方,抹掉的恰恰是最重要的信息——ROI 这八成的变化是"转化数涨但 ARPU 跌、流量跌但 CAC 涨"两两对冲出来的,任何单因子视角都无法表达它。

这一招的正当用途只有一个:**残差很小、且你需要的只是一张能对账的饼图。**那时分摊带来的信息损失可以忽略。

第三条路是换一把更严格的刀。Shapley 值来自合作博弈论:把每个因子当成一个玩家,在所有可能的加入顺序上取平均边际贡献。

它的四条公理正好命中我们的痛点:有效性(各因子贡献之和恰好等于 ΔY)、对称性(两个贡献相同的因子拿到相同的份额)、虚拟性(毫无作用的因子分到零)、可加性(可加性的游戏可以拆开分)。其中"与顺序无关"是"对所有顺序取平均"的直接后果——链式替代最严重的那个毛病,被结构性地消掉了。

四个因子的 ROI,精确枚举全部加入顺序得到:

因子Shapley 值 φᵢ占比
CU+0.227424−312.7%
ARPU−0.191970+264.0%
UV+0.118838−163.4%
CAC−0.227020+312.2%
合计−0.072727100%

和定基代替的数字接近但不相同——Shapley 把交互项按"每个因子在所有顺序中的平均参与度"重新分摊回各因子,而不是简单按权重摊。完整的推导、公理证明和实现见第五篇。

代价是计算量:n 个因子要枚举 n! 个顺序(或者用 2ⁿ 个子集的等价形式)。四个因子毫无压力,几十个因子就得采样近似。

三个函数分别对应本文的三种分解模型,跑的是上面两组数据。

from math import log, prod


def delta(base: dict, cur: dict) -> dict:
    """加法型指标 Y = ΣXᵢ:Delta 法,残差恒为零。"""
    y0, y1 = sum(base.values()), sum(cur.values())
    dy = y1 - y0
    out = {}
    for k in base:
        v = cur[k] - base[k]                       # 绝对贡献 Vᵢ = ΔXᵢ
        out[k] = {"V": v, "C": v / dy, "R": v / y0}
    return {"dY": dy, "factors": out}


def lmdi(base: dict, cur: dict) -> dict:
    """乘法型指标 Y = ΠXᵢ:LMDI,ΣV = ΔY,无残差。"""
    y0, y1 = prod(base.values()), prod(cur.values())
    lmean = (y1 - y0) / (log(y1) - log(y0))       # 对数均值 L(Y⁰, Y¹)
    dln_y = log(y1) - log(y0)
    out = {}
    for k in base:
        dln_x = log(cur[k]) - log(base[k])         # 该因子的对数变化
        v = lmean * dln_x                          # 绝对贡献 Vᵢ
        out[k] = {"dln": dln_x, "V": v, "C": dln_x / dln_y, "R": v / y0}
    return {"dY": y1 - y0, "factors": out}

定基代替要处理任意函数,所以把模型本身当参数传进去:

def base_substitution(f, base: dict, cur: dict) -> dict:
    """定基代替:其它因子固定在基期,只替换当前这一个因子。"""
    y0 = f(**base)
    out = {k: f(**{**base, k: cur[k]}) - y0 for k in base}
    residual = f(**cur) - y0 - sum(out.values())   # 残差:交互项
    return {"contrib": out, "residual": residual}


def roi(CU, ARPU, UV, CAC):
    return CU * ARPU / (UV * CAC)

跑三组数据。Delta 法用在第一篇那张渠道表上——UV、CVR、AOV 三个因子量纲不同,本来就不能相加,Delta 在它们身上是失效的:

# ① 加法型:GMV 按渠道拆(Delta 法)
chan0 = {"搜索": 400, "推荐": 300, "直播": 200, "私域": 100}
chan1 = {"搜索": 360, "推荐": 420, "直播": 380, "私域": 190}
d = delta(chan0, chan1)
print(f"Delta  ΔGMV={d['dY']:+.0f} 万")
for k, v in d["factors"].items():
    print(f"  {k:4s} V={v['V']:+.0f} 万  C={v['C']:+.2%}  R={v['R']:+.2%}")

# ② 乘法型:GMV = UV × CVR × AOV(LMDI)
gmv0 = {"UV": 100, "CVR": 0.05, "AOV": 200}
gmv1 = {"UV": 90, "CVR": 0.06, "AOV": 250}
m = lmdi(gmv0, gmv1)
print(f"LMDI   ΔGMV={m['dY']:+.0f} 万  ΣC={sum(v['C'] for v in m['factors'].values()):.2%}")
for k, v in m["factors"].items():
    print(f"  {k:4s} Δln={v['dln']:+.6f}  V={v['V']:+.2f} 万  C={v['C']:+.2%}  R={v['R']:+.2%}")

# ③ 非线性:ROI = (CU × ARPU) / (UV × CAC)(定基代替 + 残差)
r0 = dict(CU=1000, ARPU=70, UV=11000, CAC=5)
r1 = dict(CU=1200, ARPU=60, UV=10000, CAC=6)
b = base_substitution(roi, r0, r1)
dy = roi(**r1) - roi(**r0)
for k, v in b["contrib"].items():
    print(f"  {k:5s} V={v:+.6f}")
print(f"  ΣV={sum(b['contrib'].values()):+.6f}  ΔROI={dy:+.6f}  "
      f"残差={b['residual']:+.6f}(占 {abs(b['residual']) / abs(dy):.1%})")

输出:

Delta  ΔGMV=+350 万
  搜索   V=-40 万  C=-11.43%  R=-4.00%
  推荐   V=+120 万  C=+34.29%  R=+12.00%
  直播   V=+180 万  C=+51.43%  R=+18.00%
  私域   V=+90 万  C=+25.71%  R=+9.00%
LMDI   ΔGMV=+350 万  ΣC=100.00%
  UV   Δln=-0.105361  V=-122.88 万  C=-35.11%  R=-12.29%
  CVR  Δln=+0.182322  V=+212.63 万  C=+60.75%  R=+21.26%
  AOV  Δln=+0.223144  V=+260.24 万  C=+74.36%  R=+26.02%
  CU    V=+0.254545
  ARPU  V=-0.181818
  UV    V=+0.127273
  CAC   V=-0.212121
  ΣV=-0.012121  ΔROI=-0.072727  残差=-0.060606(占 83.3%)

上线前有三条自检规则,对应本文的三种模型:

方法加起来等于 ΔY贡献度合计顺序无关残差量级
Delta是100%是0
LMDI是100%是0
链式替代是不适用否0
定基代替否不适用是视模型而定,本例 83.3%

**先确认指标形态,再选方法,最后检查残差。**顺序反了,报出去的数字就是错的——而且越复杂的模型越有说服力,越容易把错误结论包装成深刻洞察。

**归因的答案由分解模型决定,不由数据决定。**加法用 Delta、乘法用 LMDI、其余用泰勒或定基代替;而凡是单因子视角算出来的贡献,都会有一个残差,它的大小就是"因子之间互相影响"的量级。

如果只能带走五句话:

  1. **Delta 法只对加法型模型成立。**推广到 Y = ΣaᵢXᵢ 时是 ΔY = ΣaᵢΔXᵢ,不是 ΣΔaᵢXᵢ——两项一换,被求和的对象就变了。
  2. **乘法型指标先取对数。**LMDI 的贡献度恒为 100%、残差恒为零,而且与替换顺序无关,是乘法模型的首选。
  3. **链式替代的总和精确等于 ΔY,但单项依赖顺序。**因子贡献 = ΔXᵢ × 已替换因子的当期值 × 未替换因子的基期值,顺序就是隐含假设,必须写进报告。
  4. **残差不是误差,是交互项。**ROI 四因子例子里残差占 83.3%,意味着八成的变化来自"几个因子一起动”,单因子视角按定义看不见这部分。
  5. **残差是分解模型的属性,不是指标的固有属性。**同一个 ROI 用 LMDI 拆残差为零;真要处理残差,按权重分摊只是把联合效应硬塞给某一方,Shapley 值才是有公理保证的分配规则。

相关内容