在流量数据化运营中,数据异常波动时,就需要分析师找到主要影响因素,并分析原因。这种异常数据检测的场景包括:监测网站平均订单价值、订单量、订单转化率波动;注册或登录的异常变化;某个登录页面浏览量趋势;正在投入巨额广告费的渠道效果波动;网站跳出率情况是否波动正常等。
但在寻找影响异常波动的主要因素时,有时需要下探的层级较多,实施起来非常费时。比如大型公司的广告投放渠道,可能包含以下层级:一级渠道包括SEM、AD、CPS、Social、导航等;
二级渠道以SEM为基准,包括百度、谷歌、360等;
三级渠道以百度为基准,包括关键字、网盟等;
四级渠道以关键字为基准,包括不同的广告计划;
五级渠道以广告计划为基准,可以细分到不同的广告组;
六级渠道以广告组为基准,可以细分到不同的关键字。
一、自动节点树的原理及特点
本案例来自宋天龙老师的《python数据分析与数据化运营》第7章,介绍了一种专门针对需要下探多个层级的分析场景,用自动化细分的方式找到主要影响因素的方法通过树形图的展示,可以形象直观地获取每个层级重要信息,并快速定位问题所在。主要影响因素节点树形图
这个树形图跟决策树的树形图看上去很像,但跟决策树规则得到的结果逻辑不同,区别表现在:分裂节点下一层的左右分支仅仅是分裂节点信息的一部分,而非全部,所以分裂节点左右两侧的变化量的总和不等于上层级的分裂节点的变化量的总和。
右侧贡献节点但环比变化量与上一层级但环比变量数据不一致,并且其贡献率不一定是100%,原因是反向节点上会有正向值抵消一部分下降量。
自动节点树的实现思路是:找到每个层级上影响最大的因素,并依次做下一因素的细分,直到最后一个因素。具体过程如下:步骤1:统计全站在一周内、阿特定指标下的数据环比变化量和环比变化率。
步骤2: 指定要分析日期并该日期及其前一天的数据。
步骤3: 以全站数据为基准,下探第一层级维度并对指定日期和其前1天的数据做分类汇总。
步骤4: 计算第一层级维度下分类汇总后的两天数据的差值并得到环比变化量和环比变化率。
步骤5: 对第一层级维度下的变化量排序,并分别获得环比变化量最大和最小情况下的维度名称、变化量和变化率。
步骤6: 计算下一层级变化量与上一层级变化量的比值,变化量最大值和最小值的比例被定义为正向贡献率和负向贡献率。
步骤7: 循环上述步骤,直至所有层级都计算完成。
步骤8: 使用树形图展示所有层级下的变化量最大和最小的维度信息,包括维度名称、环比变化量、环比变化率、贡献率等信息。
二、案例数据及应用特点
案例数据来自某企业的网站分析系统,数据地址:
数据概况:维度数:6
数据记录数:10693
是否有NA值:无
是否有异常值:有
6个维度的详细说明:date: 日期
source: 流量来源一级分类,来源于业务部门的定义。
site: 流量来源二级分类,来源于业务部门的定义。
channel: 流量来源三级分类,来源于业务部门的定义。
media:流量来源四级分类,来源于业务部门的定义。
visit: 访问量
数据处理:找到每个节点的极值并计算极值的贡献,通过python“手动”实现。
数据展示:基于Graphviz的节点树展示。python安装Graphviz的方法:pip install python-graphviz;如果已经安装了anaconda, 推荐使用 conda install python-graphviz 安装。
本案例的应用的特点:实现自动按照日期找到影响的最大因素,并可层层分解找到对应层级的上一层和下一层关联影响因素以及对应的贡献量。
在节点树中除了关注正向影响,还增加了负向影响因素的信息,可以帮助分析师找到被整体波动埋藏的负向规律。
三、python实现
'''步骤1 导入库'''
import datetime
import pandas as pd
import numpy as np
from graphviz import Digraph
'''步骤2 读取数据'''
raw_data = pd.read_csv('advertising_data.csv')
'''步骤3 数据审查和校验'''
raw_data.head(2) #数据概览
raw_data.dtypes #数据类型
'''步骤4 数据预处理'''
raw_data['visit'] = raw_data['visit'].replace('-', 0).astype(np.int64) #替换字符并转换为整数
raw_data['date'] = pd.to_datetime(raw_data['date'], format='%Y-%m-%d') #将字符串转换为日期格式
'''步骤5 计算整体波动量。'''
#现将每天的数据和前1天的数据做环比变化统计
day_summary = raw_data.iloc[:,-1].groupby(raw_data.iloc[:,0]).sum() #按天求和汇总
day_change_value = day_summary.diff(1).rename('change') #通过差分求平移一天后的变化量
day_change_rate = (day_change_value/day_summary).round(3).rename('change_rate') #相对昨天的环比变化率
day_summary_total = pd.concat((day_summary, day_change_value, day_change_rate), axis=1) #整合为完整数据框
'''步骤6 指定日期自动下探分解。'''
#获得指定日期、前1天以及各自对应的数据
the_day = pd.datetime(2017,6,7) #指定要分析的日期
previous_day = the_day - datetime.timedelta(1) #自动获取前一天日期
the_data_tmp = raw_data[raw_data['date'] == the_day].rename(columns={'visit':the_day}) #获得指定日期数据
previous_data_tmp = raw_data[raw_data['date'] == previous_day].rename(columns={'visit':previous_day}) #获得前一天日期数据
#定义要使用的变量
dimension_list = ['source', 'site', 'channel', 'media'] #指定要分析的维度:4个层级
split_node_list = ['全站'] #每层分裂节点名称列表
change_list = list() #每层分裂节点对应的总变化量
increase_node_list = [] #每层最大增长贡献最大的1个维度
decrease_node_list = [] #每层最小增长贡献最小的1个维度
#整个分裂过程的计算
for dimension in dimension_list: #遍历每个维度
#part 1 计算指定维度下对数据
the_data_merge = the_data_tmp[[dimension, the_day]] #获得指定日期特定维度和访问量
previous_data_merge = previous_data_tmp[[dimension, previous_day]] #获得指定日期前1天的特定维度和访问量
#对指定日期特定维度汇总求和
the_day_groupby = pd.DataFrame(the_data_merge.iloc[:,-1].groupby(the_data_merge.iloc[:, 0]).sum())
#对指定日期前一天特定维度汇总求和
previous_day_groupby = pd.DataFrame(
previous_data_merge.iloc[:,-1].groupby(previous_data_merge.iloc[:,0]).sum())
#part2 将两天的数据合并,然后计算其变化量和变化率
merge_data = pd.merge(the_day_groupby, previous_day_groupby, how='outer',
left_index=True, right_index=True) #合并两天的数据
merge_data = merge_data.fillna(0) #将缺失值(没有匹配值)替换为0
merge_data['change'] = merge_data[the_day] - merge_data[previous_day] #计算环比变量
merge_data['change_rate'] = merge_data['change'] / merge_data[previous_day] #环比变化率
total_change = merge_data['change'].sum()
change_list.append(total_change)
#part3 计算当前维度下变化量最大值对应的各项信息
merge_data = merge_data.sort_values(by='change') #按环比变量正向排序
max_increase_node = merge_data.iloc[-1].name #获得增长变量最大值节点名称
max_value, max_rate = merge_data.iloc[-1][2:4] #获得最大节点变量及变化率
increase_node_list.append([max_increase_node, int(max_value), max_rate])#将最大值信息追加到列表
#part4 计算当前维度下变量最小值对应的各项信息
min_increase_node = merge_data.iloc[0].name #增长变化量最小值节点名称
min_value, min_rate = merge_data.iloc[0][2:4] #增长变化量最小值节点变化量及变化率
decrease_node_list.append([min_increase_node, int(min_value), min_rate])#将最小值信息追加到列表
#part5 针对增长趋势的数据做逐层数据过滤
if total_change >= 0: #判断为增长方向
split_node_list.append(max_increase_node) #将分裂节点定义为增长最大值节点
rules_len = len(split_node_list) #通过分裂节点的个数判断所处分裂层级
if rules_len == 2: #第二层source, 第一层为全站整体
#以source为维度过滤出指定日期符合最大节点条件的数据
the_data_tmp = the_data_tmp[the_data_tmp['source'] == max_increase_node]
#以source为维度过滤出指定日期前一天符合最大节点条件的数据
previous_data_tmp = previous_data_tmp[previous_data_tmp['source'] == max_increase_node]
elif rules_len == 3: #第三层site
the_data_tmp = the_data_tmp[the_data_tmp['site'] == max_increase_node]
previous_data_tmp = previous_data_tmp[previous_data_tmp['site'] == max_increase_node]
elif rules_len == 4: #第四曾channel
the_data_tmp = the_data_tmp[the_data_tmp['channel'] == max_increase_node]
previous_data_tmp = previous_data_tmp[previous_data_tmp['channel'] == max_increase_node]
elif rules_len == 5: #第五层 media
the_data_tmp = the_data_tmp[the_data_tmp['media'] == max_increase_node]
previous_data_tmp = previous_data_tmp[previous_data_tmp['media'] == max_increase_node]
#part6 针对下降趋势的数据做逐层数据过滤
else: #判断为下降方向
split_node_list.append(min_increase_node) #将分裂节点定义为增长最小值节点
rules_len = len(split_node_list) #通过分裂节点的个数判断所处分裂层级
if rules_len == 2: #第二层source
the_data_tmp = the_data_tmp[the_data_tmp['source'] == min_increase_node]
previous_data_tmp = previous_data_tmp[previous_data_tmp['source'] == min_increase_node]
elif rules_len == 3: #第三层site
the_data_tmp = the_data_tmp[the_data_tmp['site'] == min_increase_node]
previous_data_tmp = previous_data_tmp[previous_data_tmp['site'] == min_increase_node]
elif rules_len == 4: #第四层channel
the_data_tmp = the_data_tmp[the_data_tmp['channel'] == min_increase_node]
previous_data_tmp = previous_data_tmp[previous_data_tmp['channel'] == min_increase_node]
elif rules_len ==5: #第五层media
the_data_tmp = the_data_tmp[the_data_tmp['media'] == min_increase_node]
previous_data_tmp = previous_data_tmp[previous_data_tmp['media'] == min_increase_node]
'''步骤7 画图展示自动下探结果'''
#part1 定义节点树形图用到的属性和样式
node_style = {'fontname':'SimSun', 'shape':'box'} #定义node节点样式
edge_style = {'fontname':'SimHei', 'fontsize':'11'} #定义edge节点样式
top_node_style = '<
{0} |
环比变化量:{1:d} |
环比变化率:{2:.0%} |
left_node_style = '<
{0} |
环比变化量:{1} |
环比变化率:{2:.0%} |
right_node_style = '<
{0} |
环比变化量:{1} |
环比变化率:{2:.0%} |
dot = Digraph(format='png', node_attr=node_style, edge_attr=edge_style) #创建有向图
#备选颜色:chartreuse
#part2 获得每一层分裂节点相关信息
for i in range(4):
node_name = split_node_list[i] #获得分裂节点名称
node_left, max_value, max_rate = increase_node_list[i] #获得增长最大值名称、变化量、变化率
node_right, min_value, min_rate = decrease_node_list[i] #获得增长最小值名称、变化量、变化率
node_change = change_list[i] #获得分裂节点的总变量-非分裂节点变量
node_label_left = left_node_style.format(node_left, max_value, max_rate) #左侧节点显示信息
node_label_right = right_node_style.format(node_right, min_value, min_rate) #右侧节点显示信息
#part3 定义顶级节点的信息
if i == 0: #如果是顶部节点,则单独增加顶部节点信息
day_data = day_summary_total[day_summary_total.index == the_day] #获得顶部节点的数据
former_data = day_data.iloc[0,1] #获得全站总变化量
node_label = top_node_style.format(node_name, int(former_data),day_data.iloc[0,2])
dot.node(node_name, label=node_label) #增加顶部节点
#par4 分别定义左侧和右侧边显示的贡献率信息
contribution_rate_1 = float(max_value) / former_data #获得左侧变化量贡献率
contribution_rate_2 = float(min_value) / former_data #获得右侧变化量贡献率
if node_change >= 0: #如果为增长,则左侧为正向
edge_label_left = '正向贡献率:{0:.0%}'.format(contribution_rate_1)
edge_label_right = '反向贡献率:{0:.0%}'.format(contribution_rate_2)
former_data = max_value
else: #如果为下降,则右侧为下降
edge_label_left = '反向贡献率:{0:.0%}'.format(contribution_rate_1)
edge_label_right = '正向贡献率:{0:.0%}'.format(contribution_rate_2)
former_data = min_value
#part5 建立节点、边并输出图形
dot.node(node_left, label=node_label_left) #增加左侧节点
dot.node(node_right, label=node_label_right) #增加右侧节点
dot.edge(node_name, node_left, label=edge_label_left, color='lightpink') # 左侧边
dot.edge(node_name, node_right, label=edge_label_right, color='lightblue') #右侧边
dot.view('change summary')
完整源码下载:
四、案例数据结论
运行代码后就得到了文章开头得到的节点树图。通过图片我们可以得到如下结果:全站访问量下降18455,下降比例达到23%,主要的source源是CRM,其下降量为17591,“贡献“了95%的主要因素;而导致CRM下降的主要site源是准会员,其下降量为17575,“贡献”了几乎100%的下降因素;再进一步细分,在影响准会员的channel中,电视源的下降量达到了19090,“贡献”了109%的比例。与此同时,某些来源渠道的流量与全站的下降趋势相反,呈现良好的增长趋势,这在全站的下降主要因子中表现良好,包括source源中的公众号流量环比增长2444,增长率达到171%;s准会员中的商城部分的流量增长1378,增长率为29%。
五、案例应用和部署
本案例是一个非常实用的应用。可以部署到公司的日常应用中,作为日常数据报告内容主要波动原因探查的主要途径和方法。
大多数情况下,我们会针对昨天的数据与前天的数据做对比分析。因此,在应用部署时默认设置昨日的数据与前1日数据做对比,即在代码中指定分析的日期:
the_day = pd.datetime(2017,6,7)
将指定分析的日期替换为如下代码即可:
the_day = datetime.date.today()-datetime.timedelta(days=1)
这样系统就会默认指定昨天作为分析日期与前天的数据做对比,因此每天早晨上班只需跑一下程序即可。