# - 依赖库:datetime、numpy、pandas、graphviz
# - 程序输入:advertising_data.csv
# - 程序输出:打印输出并保存节点树图change_analysis_tree.png
# 程序
# 导入库
import datetime
import numpy as np
import pandas as pd
from graphviz import Digraph # 画图用库
# 读取数据
raw_data = pd.read_csv('advertising_data.csv')
# 数据审查
# 数据概览
print('{:*^60}'.format('Data overview:'), '\n', raw_data.tail(2)) # 打印原始数据后2条
print('{:*^60}'.format('Data dtypes:'), '\n', raw_data.dtypes) # 数据类型
# 缺失值审查
na_cols = raw_data.isnull().any(axis=0) # 查看每一列是否具有缺失值
print('{:*^60}'.format('NA Cols:'))
print(na_cols[na_cols] == True) # 查看具有缺失值的列
print('Total NA lines is: {0}'.format(raw_data.isnull().any(axis=1).sum())) # 查看具有缺失值的行总记录数
# 数据预处理
# 替换字符为0然后转换为整数型
raw_data['visit'] = raw_data['visit'].replace('-', 0).astype(np.int64)
print('{:*^60}'.format('Data overview:'))
print(raw_data.tail(2))
# 将字符串转换为日期格式
raw_data['date'] = pd.to_datetime(raw_data['date'], format='%Y/%m/%d')
print('{:*^60}'.format('Data dtypes:'))
print(raw_data.dtypes)
# 计算整体波动量
day_summary = raw_data.iloc[:, -1].groupby(raw_data.iloc[:, 0]).sum() # 按天求和汇总
day_change_value = day_summary.diff(1).rename('change') # 通过差分求平移1天后的变化量
day_change_rate = (day_change_value.shift(-1) / day_summary).round(3).rename('change_rate').shift(1) # 求相对昨天的环比变化率
day_summary_total = pd.concat((day_summary, day_change_value, day_change_rate), axis=1) # 整合为完整数据框
print('{:*^60}'.format('Data change summary:'))
print(day_summary_total.head())
# 指定日期自动下探分解
# 定义变量
# 分解对象
dimension_list = ['source', 'site', 'channel', 'media'] # 指定要分析的维度:4个层级
# 日期对象
the_day = pd.datetime(2018, 6, 7) # 指定要分析的日期
previous_day = the_day - datetime.timedelta(1) # 自动获取前1天日期
# 日期列名
day_col1 = datetime.datetime.strftime(the_day, '%Y-%m-%d')
day_col2 = datetime.datetime.strftime(previous_day, '%Y-%m-%d')
# 数据对象
the_data = raw_data[raw_data['date'] == the_day].rename(columns={'visit': day_col1}) # 获得指定日期数据
previous_data = raw_data[raw_data['date'] == previous_day].rename(columns={'visit': day_col2}) # 获得前1天日期数据
# 数据合并计算
# 合并两天的数据
data_merge = the_data.iloc[:, 1:].merge(previous_data.iloc[:, 1:], on=dimension_list, how='outer')
# 替换没有匹配的数据为0
data_merge = data_merge.fillna(0)
# 计算相对昨天的环比变化率
data_merge['change'] = data_merge[day_col1] - data_merge[day_col2] # 变化量
data_merge.head()
# 整体对象
visit, change, change_rate = day_summary_total[day_summary_total.index == the_day].values[0]
top_nodes = {'source': 'all site', 'change': change, 'change_rate': change_rate}
# 自动节点分解
main_nodes = [] # 主节点
other_nodes = [] # 其他节点
hidden_nodes = [] # 潜在节点
main_edges = [] # 主边
other_edges = [] # 其他边
dim_copy = ['source', 'site', 'channel', 'media', day_col2, 'change']
for ind, dimension in enumerate(dimension_list): # 遍历每个维度
each_data = data_merge[dim_copy[ind:]] # 筛选数据
each_merge = each_data.groupby([dimension], as_index=False)[day_col2, 'change'].sum() # 计算变化量
each_merge = each_merge.sort_values(['change']) # 排序
each_merge['each_change_rate'] = each_merge['change'] / each_merge[day_col2] # 环比变化率
each_merge = each_merge.drop(day_col2, axis=1) # 丢弃当日visit数值列
change_all = each_merge.sum().iloc[1] # 总变化量
if change_all < 0: # 下降
# node
main_values = each_merge.iloc[0].tolist() # 主因子节点
main_nodes.append(main_values)
other_nodes.append([f'{dimension}-others', change_all - main_values[1], 1 - main_values[2]])
hidden_nodes.append(each_merge.iloc[-1].tolist())
# 数据过滤
data_merge = each_data[each_data[dimension] == each_merge.iloc[0].iloc[0]]
else: # 上升
# node
main_values = each_merge.iloc[-1].tolist() # 其他因子节点
main_nodes.append(main_values)
other_nodes.append([f'{dimension}-others', change_all - main_values[1], 1 - main_values[2]])
hidden_nodes.append(each_merge.iloc[0].tolist())
# 数据过滤
data_merge = each_data[each_data[dimension] == each_merge.iloc[-1].iloc[0]]
# edge
edge_values = main_values[1] / float(change_all)
main_edges.append(edge_values)
other_edges.append(1 - edge_values)
# 画图展示自动下探结果
# 定义各个节点的样式
node_style = '<<table border="0"><tr><td width="20"><table border="1" cellspacing="0" VALIGN="MIDDLE"><tr><td bgcolor="{0}"><font color="{1}"><B>{2}</B></font></td></tr><tr><td>环比变化量:{3:d}</td></tr><tr><td>环比变化率:{4:.2%}</td></tr></table></td></tr></table>>'
edge_style = '<<table border="0"><tr><td><table border="0" cellspacing="0" VALIGN="MIDDLE" bgcolor="#ffffff"><tr><td>{0}</td></tr><tr><td>贡献率:{1:.0%}</td></tr></table></td></tr></table>>'
attr_node = {'fontname': "SimHei", 'shape': 'box', 'penwidth': '0'} # 定义node节点样式
attr_edge = {'fontname': "SimHei"} # 定义edge节点样式
attr_graph = {'fontname': "SimHei", 'splines': 'ortho', 'nodesep': '2'} # Graph的总体样式
# 定义左侧父级图
parent_dot = Digraph(format='png', graph_attr=attr_graph, node_attr={'shape': 'plaintext', 'fontname': 'SimHei'})
features = ['全站', 'source', 'site', 'channel', 'media']
parent_edge = [(features[i], features[i + 1]) for i in range(len(features) - 1)]
parent_dot.edges(parent_edge)
# 定义右侧子级图
child_dot = Digraph(node_attr=attr_node, edge_attr=attr_edge) # 创建有向图
for tree_depth in range(len(main_nodes)): # 循环读取每一层
split_node_left = main_nodes[tree_depth]
split_node_right = other_nodes[tree_depth]
split_node_hidden = hidden_nodes[tree_depth]
if tree_depth == 0:
# 增加顶部节点
node_name = '汇总值'
node_top_label = node_style.format('black', "white", node_name,
int(top_nodes['change']),
top_nodes['change_rate']) # 分别获取顶部节点名称、变化量和变化率
child_dot.node('汇总值', label=node_top_label) # 增加顶部节点
else:
node_name = main_nodes[tree_depth - 1][0] # 将上级左侧分裂节点作为下级节点的source
# 增加node信息
node_label_left = node_style.format("#184da5", "white", split_node_left[0],
int(split_node_left[1]),
split_node_left[2]) # 左侧节点显示的信息
node_label_right = node_style.format("#d3d3d3", "black", split_node_right[0],
int(split_node_right[1]),
split_node_right[2]) # 右侧节点显示的信息
node_label_hidden = node_style.format("#72a518", "black", split_node_hidden[0],
int(split_node_hidden[1]),
split_node_hidden[2]) # 潜在节点显示的信息
# 增加边信息
edge_label_left = edge_style.format('主因子', main_edges[tree_depth]) # 左侧边的标签信息
edge_label_right = edge_style.format('其他因子', other_edges[tree_depth]) # 右侧边的标签信息
# 节点和边画图
child_dot.node(split_node_left[0], label=node_label_left) # 增加左侧节点
child_dot.node(split_node_right[0], label=node_label_right) # 增加右侧节点
child_dot.node(split_node_hidden[0], label=node_label_hidden) # 增加隐藏节点
child_dot.edge(node_name, split_node_left[0], label=edge_label_left) # 增加左侧边
child_dot.edge(node_name, split_node_right[0], label=edge_label_right) # 增加右侧边
child_dot.edge(split_node_right[0], split_node_hidden[0], label='潜在因子') # 增加隐藏节点边
parent_dot.subgraph(child_dot)
parent_dot.view('change_analysis_tree') # 展示图形结果
注:数据文件链接: https://pan.baidu.com/s/16RsmM7KEiJkazBzKW18Q4g 密码: cuck