发布于 

指标体系+诊断归因

未来的数据产品设计应该尝试建立完整的决策模型和执行路径,突破图表反应数据价值的局限,尽量介入决策环节,朝着解决问题前进.

指标体系,包含看数、归因、预测、辅助决策四个层级

该平台是数据指标体系的产品化实现,其目的是为了了解现状、定位问题、预测未来,最终做到辅助决策。

起源

项目地址

零、起源

刚毕业那会儿,从事的是传统零售业的采销工作,每天接触的都是进销存相关的业务场景,那会儿上班的第一件事就是计算下最新的进销存数据,那段时间excel的使用进步很大,还学会了vba。再之后,就做数据分析了,总结下来,当时的工作内容三大块,写报表、查问题、做分析,比例大概是5:4:1,其实是偏向于数据开发,业务分析的内容占比很少。
那会儿对接的业务方经常会问我们,为什么跌,哪里跌,同理还有为什么涨,哪里涨,还会涉及怎么判断这个波动是不是正常的等等。虽然查问题的套路都是差不多的,但是每次梳理数据,计算占比还是耗费了大量的时间,也经常怀疑是不是报表做的不够全,每所以次查问题都很麻烦。
在此期间,也经常对接新的业务,在设计报表时,也接触到了指标体系设计这块儿,了解到了如何设计一个完善的指标体系,以满足业务的了解现状、定位问题的需求。
后面的工作就转向了数据产品,就一直在想,是否有可能将查问题这个工作给产品化?偶然间发现了美团的指标逻辑树文章,介绍了一种辅助用户进行数据波动问题定位的方法,名字叫做指标逻辑树,当时复现了下,发现确实可以,第一次发现了查问题可以做到规则化。在之后的时间里,也陆续参考了一些其他方面的文章,如何设计报表,如何定义指标的贡献度等等,最终才有了这个工具。
兜兜转转两年多了,总算写出了个初版,算是实现了当初的想法。

一、典型场景

假设一个场景,你是某部门的数据分析师,你所对接部门的运营同学反馈gmv异常,需要你排查下,你看到起伏不定的30日gmv折线图陷入了沉思。
那么问题来了,运营同学所说的gmv异常,是真的异常吗?怎么定义这种异常呢?
如果真的异常,你又该怎么查找问题?
二、如何衡量指标的波动
当gmv下跌时如何判断是真的下跌还是只是指标的正常波动?在具体业务中一般会通过同比、环比、数据趋势以及历史经验进行判断,一般大的波动大家都能达成共识,但是小的波动就不太容易达成共识了。那么是否有更好的方案去进行判断呢?查找了一些资料,并没有找到很好的方案,这里就简单的借鉴时间序列预测算法的思路,比如Prophet算法,它将整体趋势、年趋势、周趋势、节假日效应解耦开来,并通过历史数据预测给出预测的上限和下限,如果实际值落在预测区间之外,代表当日的数据并不符合以往数据的历史趋势,就可以认为指标的波动存在异常。

三、从指标体系说起

一般而言,排查数据问题时我们大多采用层层拆解的分析思路,当gmv出现问题时,我们会从历史经验出发,按uv转化率客单价进行拆解,也会按照分类目、分区域等对gmv进行拆分,然后看拆解后的指标对gmv的影响,从而判断是否是该指标造成的gmv异常,当然,在拆解指标时,并不是简单的拆解一层就能够定位问题,一般都是继续拆分到某个不能再细分的指标上,然后再从指标上定位是什么业务动作造成的gmv异常。
从这个过程中可以发现,查找问题时具体的拆解思路一般都是按照历史经验,优先从最有可能的维度进行拆解,然后层层分拆直至定位最终的业务动作。这里的拆解思路就是指标体系,通过体系化的组织对应的指标,从而实现对业务的建模,既能完整的描绘业务现状,又能在遇到问题时快速诊断归因。

上图是Garter划分的商业分析的四个层级,分别是发生了什么(描述现状)->为什么发生(诊断归因)->以后可能会发生什么(预测未来)->应该怎么做才能实现目标(辅助决策)。指标体系除了描绘现状和诊断归因之外,也应当实现后两个层级。
指标体系的搭建已经有很成熟的方法论,如OSM、AAARR等,这方面可以参考growingIO的相关文章,核心思路一般都是先定义北极星指标(结果性指标),然后按业务流程进行拆分,构造出一个个过程性指标,通过对过程指标的把控,从而影响结果指标。如gmv可拆解为uv转化率客单价,gmv就是结果性指标,通过对uv、转化率、客单价的业务策略,从而实现gmv的影响。

四、诊断归因的方法

在查找gmv异常的场景中,有3个核心问题需要我们去解决

  1. 如何拆解指标?如 gmv可拆解为uv转化率客单价,这样当gmv这样的结果指标出现变化时,我们就能够从其他几个过程性指标进行问题查找
  2. 如何计算每个拆解指标对结果的贡献度?当gmv拆解为uv转化率客单价之后,我们还需要计算每个指标对gmv的贡献度,这样才能更精确的评估影响,而不仅仅只是通过数据的同比和环比来进行定性判断。
  3. 如何定位出问题的维度?gmv的拆分可能是多维度的,除了通过uv拆分之外,还能通过分类目的gmv,分地区的gmv,分品牌的gmv等进行多维度拆分。维度下每个维度值对gmv的贡献度都是不同的,有的多,有的少,当贡献度计算完成之后,我们会发现真正出问题的维度,其内部的某个维度值的贡献度会异常的高。

4.1 拆解指标

指标体系的搭建不再赘述,但是无论哪种搭建方式,都会涉及指标间的关系。一般而言,指标间的关系可以定义为四种,加减乘除。我们经常使用加法和乘法关系较多,比如gmv场景中的分类目拆解就是加法,uv转化率客单价拆解就是乘法。
以gmv为例,可以拆分为如下所示的指标体系:

4.2 各指标贡献度的计算

如何计算各指标的贡献度?
加法和减法拆解:进行简单的四则运算即可,计算步骤与定基替代法一致。
乘法和除法拆解:这里引入会计概念中的因素分析法,调研了定基替代法和连环替代法,两种方案都存在各种问题,直到我们发现了LMDI方法。
关于连环替代法和LMDI的区别可以参考这篇文章:http://www.kjlww.com/m/article-36845.html
关于LMDI的详细介绍,可参考这篇文章:https://zhuanlan.zhihu.com/p/412117828,这篇文章对指标拆解也讲解的很细,推荐阅读下。
详细计算步骤可参考文末附上的python代码。

4.3 基尼系数定位异常维度

在我们分析某个维度是不是出问题的维度时,用到的方法是看该维度下具体指标值的变动幅度是否均匀,比如在查找gmv异常下跌时,分类目拆分之后发现F类目异常下跌了50%,而其他几个类目的变动幅度都在5%以内,那么gmv的异常波动就可以定位到类目维度下的F类目了。

基尼系数系数是衡量财富分配是否均匀的指标,将拿到的收入数据从小到大排列,x轴代表人数占比的累加,y轴代表收入占比的累加,绘制出一条洛伦兹曲线,计算图中A区域的面积占比,该占比就是基尼系数。同样的我们也可以用基尼系数来衡量每个维度下具体指标值的波动是否均匀,基尼系数越高就代表该维度下指标值的波动更大,也就意味着问题更有可能出现在该维度。
基于此,构建基于基尼系数的定位维度问题的方案,分别计算每个维度下各个指标的贡献度,从小到大排序,x轴代表各个值的累计占比,y轴代表各个值的波动的累计占比,然后再计算基尼系数,此时应当有,某个维度下的波动越集中于某个指标,那么基尼系数越大,则意味着最有可能出问题的是该维度下的某个指标。
值得注意的是,这里使用贡献度作为计算的基础,而不是每个指标值本身的波动,这样在计算时加减法拆解和乘除法拆解计算的口径保持了一致,可以横向对比基尼系数。
具体计算逻辑参考附录部分的计算代码

五、具体设计思路



具体设计上,还是以指标体系的搭建作为出发点,将指标体系定位为三种基础组件的组合

  1. 指标卡片,承载单个指标看数以及波动告警、数据预测的功能
  2. 维度分析组件,承载加减法指标拆解及诊断分析场景
  3. 杜邦分析组件,承载乘除法指标拆解及诊断分析场景

六、实际使用步骤

以电商场景举例

6.1 数据输入

通过excel简单导入对应的数据,数据格式如下

6.2 画布上搭建指标体系

从一个北极星指标gmv进行层层拆解,通过点选搭建出对应的指标体系。

点击指标卡片,选择对应的指标

6.3 查看数据

重复以上步骤,点击保存后即可选择数据日期查看对应的数据

6.4 诊断归因

如果有诊断归因的需求,首先在右上角选择数据日期和需要对比的日期,点击归因分析按钮,待分析完成之后,点击对应的指标拆解组就能够看到诊断归因的结论。

七、总结及后续计划

这个只是闲暇时间写的,本身并没有在实际的业务场景中应用,也就收集不到多少使用反馈,所以本工具目前还仅仅只是一个小工具,期望大家能够在业务场景中试用。

注意,本网站不会保留任何计算数据,所以存储均为本地存储,仅会对用户的操作行为进行埋点,所以大家可以放心导入实际的业务数据进行测试。

后续计划

  1. 收集反馈,优化交互
  2. 指标波动衡量功能上线
  3. 指标体系搭建方式的优化
  4. 优化下指标录入的方式
  5. 贡献度计算支持多层级计算
  6. 加入增强分析的逻辑

八、附录

反馈留言

https://txc.qq.com/products/467607/

计算代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
import numpy as np
from functools import reduce

class MetricSys():
def base_gini_computed(self, nowDict: dict, baseDict: dict) -> dict:
"""
Args:
nowDict:当期字典
baseDict:基期字典
Return:
dict:x_list:x轴点,y_list:y轴点,A:Gini图中的A,B:Gini图中的B,G:基尼系数
"""
# 排序数组
sortValue = []

# 计算波动值并传入到waveDict中
waveDict = {}
for k, v in nowDict.items():
wave = abs(int(v)-int(baseDict.get(k))) # type: ignore
sortValue.append(wave)

waveDict[k] = {}
waveDict[k]['wave'] = wave

# print(waveDict)

# 对sortValue的值按照从小到大排列
sortValue.insert(0, 0)
sortValue.sort()

# x_list从低到高的每个指标的累计数量
xcnt = np.array(range(0, len(sortValue)))
x_list = list( xcnt / (len(xcnt) - 1) )

# print('x_list',x_list)

# y_list是每个维度波动值占总波动值得累计占比
wave_sum = np.cumsum(sortValue)[-1]
# print('wave_sum',wave_sum)
y_list = list(np.cumsum(sortValue) / wave_sum)

# print('y_list',y_list)

# 计算曲线下面积
B = np.trapz(y=y_list, x=x_list)

# 边为1的直角三角形总面积 0.5
A = 0.5 - B

resDict = {}
resDict['x_list'] = x_list
resDict['y_list'] = y_list
resDict['G'] = A / (A + B)
resDict['A'] = A
resDict['B'] = B
# print(resDict)
return resDict

def gini_computed(self, changeDict: dict) -> dict:
"""
Args:
changeDict:每个值的波动值 dict
Return:
dict:x_list:x轴点,y_list:y轴点,A:Gini图中的A,B:Gini图中的B,G:基尼系数
"""
sortValue = []
for v in changeDict.values():
sortValue.append(abs(v))

# 添加0
sortValue.insert(0, 0)

# 对sortValue的值按照从小到大排列
sortValue.sort()

# x_list从低到高的占比
xcnt = np.array(np.arange(0, len(sortValue)))
x_list = list(xcnt / (len(xcnt) -1) )

# print('x_list',x_list)

# y_list是每个维度波动值占总波动值得累计占比
wave_sum = np.cumsum(sortValue)[-1]
# print('wave_sum',wave_sum)
y_list = list(np.cumsum(sortValue) / wave_sum)

# print('y_list',y_list)

# 计算曲线下面积
B = np.trapz(y=y_list, x=x_list)

# 边为1的直角三角形总面积 0.5
A = 0.5 - B

resDict = {}
resDict['x_list'] = x_list
resDict['y_list'] = y_list
resDict['G'] = A / (A + B)
resDict['A'] = A
resDict['B'] = B
# print(resDict)
return resDict

def addition_computed(self, data_t: dict, data_0: dict, yt: float, y0: float) -> dict:
"""
Args:
data_t:t期结果值,example
data_0:基期结果值
yt: t期结果值
y0: 基期结果值
Returns:
object['x']['key'] 代表x的key,x为自变量
object['x']['key']['x0']: 基期值
object['x']['key']['xt']: t期值
object['x']['key']['change']: 变动值
object['x']['key']['changeRate']: 变动率,即同比值
object['x']['key']['contribute']: 计算得贡献度
object['x']['key']['contributeRate']: 基期贡献率, 计算得贡献度 / y0
object['x']['key']['changeContributeRate']: 变动值贡献率, 计算得贡献度 / object['y']['change']
object['y'] y代表因变量
object['y']['y0']: 基期值
object['y']['yt']: 本期值
object['y']['change']: 变动值
object['y']['changeRate']: 变动率,即同比值
Raises:
ValueError: data_t/data_0的内容相加不等于yt/y0, data_t和data_0的key名称及数量不相等

"""

# 对参数进行校验
data_0_comp = reduce(lambda x, y: x+y, data_0.values())
data_t_comp = reduce(lambda x, y: x+y, data_t.values())

if (data_t_comp - yt > 1 or data_t_comp - yt < -1): # 考虑到float的计算精度,这里放了gap值不能大于1
raise ValueError('data_t的内容相乘不等于tt')
elif (data_0_comp - y0 > 1 or data_0_comp - y0 < -1):
raise ValueError('data_0的内容相乘不等于tt')
elif data_t.keys() != data_0.keys():
raise ValueError('data_t和data_0的key名称及数量不相等')

x = {}
for key in data_t.keys():
x[key] = {}
x[key]['x0'] = data_0[key]
x[key]['xt'] = data_t[key]
x[key]['change'] = data_t[key] - data_0[key]
x[key]['changeRate'] = 0 if data_0[key] == 0 or data_0[key] == 0 or data_0[key] == "" else (
data_t[key] - data_0[key]) / data_0[key]
x[key]['contribute'] = x[key]['change']
x[key]['contributeRate'] = 0 if y0 == 0 else x[key]['contribute'] / y0
x[key]['changeContributeRate'] = 0 if yt - \
y0 == 0 else x[key]['contribute'] / (yt-y0)

y = {}
y['y0'] = y0
y['yt'] = yt
y['change'] = yt - y0
y['changeRate'] = 0 if yt == 0 or y0 == 0 else (yt-y0)/yt

result = {
"x": x,
"y": y
}
return result

def multi_computed(self, data_t: dict, data_0: dict, yt: float, y0: float) -> dict:
"""
Args:
data_t:t期结果值,example
data_0:基期结果值
yt: t期结果值
y0: 基期结果值
Returns:
object['x']['key'] 代表x的key,x为自变量
object['x']['key']['x0']: 基期值
object['x']['key']['xt']: t期值
object['x']['key']['change']: 变动值
object['x']['key']['changeRate']: 变动率,即同比值
object['x']['key']['contribute']: LMDI计算得贡献度
object['x']['key']['contributeRate']: 基期贡献率, LMDI计算得贡献度 / y0
object['x']['key']['changeContributeRate']: 变动值贡献率, LMDI计算得贡献度 / object['y']['change']
object['y'] y代表因变量
object['y']['y0']: 基期值
object['y']['yt']: 本期值
object['y']['change']: 变动值
object['y']['changeRate']: 变动率,即同比值
Raises:
ValueError: data_t/data_0的内容相乘不等于yt/y0, data_t和data_0的key名称及数量不相等

"""

# 对参数进行校验
data_0_comp = reduce(lambda x, y: x*y, data_0.values())
data_t_comp = reduce(lambda x, y: x*y, data_t.values())

if (data_t_comp - yt > 1 or data_t_comp - yt < -1): # 考虑到float的计算精度,这里放了gap值不能大于1
raise ValueError('data_t的内容相乘不等于tt')
elif (data_0_comp - y0 > 1 or data_0_comp - y0 < -1):
raise ValueError('data_0的内容相乘不等于tt')
elif data_t.keys() != data_0.keys():
raise ValueError('data_t和data_0的key名称及数量不相等')

def Delta_XX(*, yt, y0, xt, x0):
# 计算LMDI中每个参数的Δ值
def L(yt, y0):
if yt == y0:
return 0
else:
return (yt-y0)/(np.log(yt) - np.log(y0))
return L(yt, y0)*np.log(xt/x0)

x = {}
for key in data_t.keys():
x[key] = {}
x[key]['x0'] = data_0[key]
x[key]['xt'] = data_t[key]
x[key]['change'] = data_t[key] - data_0[key]
x[key]['changeRate'] = 0 if data_0[key] == 0 or data_0[key] == 0 or data_0[key] == "" else (
data_t[key] - data_0[key]) / data_0[key]
x[key]['contribute'] = Delta_XX(
yt=yt, y0=y0, xt=data_t[key], x0=data_0[key])
x[key]['contributeRate'] = 0 if y0 == 0 else x[key]['contribute'] / y0
x[key]['changeContributeRate'] = 0 if yt - \
y0 == 0 else x[key]['contribute'] / (yt-y0)

y = {}
y['y0'] = y0
y['yt'] = yt
y['change'] = yt - y0
y['changeRate'] = 0 if yt == 0 or y0 == 0 else (yt-y0)/yt

result = {
"x": x,
"y": y
}
return result

if __name__ == '__main__':
ms = MetricSys()
nowDict = {
"指标1": "66",
"指标2": "1159",
"指标3": "1189",
"指标4": "455",
"指标5": "896",
"指标6": "30474",
"指标7": "11747"
}
baseDict = {
"指标1": "17",
"指标2": "1174",
"指标3": "987",
"指标4": "868",
"指标5": "801",
"指标6": "20287",
"指标7": "10610"
}
changeDict = {
"指标1": 66-17,
"指标2": 1159 - 1174,
"指标3": 1189 - 987,
"指标4": 455 - 868,
"指标5": 896 - 801,
"指标6": 30474 - 20287,
"指标7": 11747 - 10610
}
d3 = {
"1": -4055.0 ,
"2": -4984.0 ,
"3": -10057.0 ,
"4": -1223.0
}
gini = ms.base_gini_computed(nowDict, baseDict)
print('gini1###', gini)
print('gini2###', ms.gini_computed(d3))

# 计算结果
y0 = 6946 # 基期结果值
yt = 7152 # t期结果值
data_t = {
"1": 1355, "2": 1496, "3": 1652, "4": 1455, "5": 1194,
} # t期分解值集合
data_0 = {
"1": 1139, "2": 1467, "3": 1449, "4": 1796, "5": 1095
} # 基期分解值集合
ms.addition_computed(data_t=data_t, data_0=data_0, yt=yt, y0=y0)

# 计算结果
y0 = 1078122 # 基期结果值
yt = 1469699 # t期结果值
data_t = {
"uv": 19087,
"m": 0.25,
"d": 308
} # t期分解值集合
data_0 = {
"uv": 20032,
"m": 0.23,
"d": 234
} # 基期分解值集合
ms.multi_computed(data_t=data_t, data_0=data_0, yt=yt, y0=y0)