Python小案例(八)基于自动节点树进行维度下钻

Python小案例(八)基于自动节点树进行维度下钻

在日常业务中,需要下钻维度查询造成整体波动的细分群体,但是如果维度过多,手动查询就显得繁琐了。这里介绍一种方法,利用自动节点树的方式进行维度下钻,本文参考自《Python数据分析与数据化运营 第2版》。

在开始之前,需要配置下绘图环境,这里通过graphviz绘制流向图

$ brew install graphviz # mac安装graphviz

$ dot -V # 测试安装成功

pip install graphviz # python环境安装graphviz

import datetime
import numpy as np
import pandas as pd
from graphviz import Digraph  # 画图用库 graphviz是一个强大的复杂关系图表库,类似的还有pyechart
# 自动节点树函数
def autoNodeTree(df, date, file_name):
    '''
    自动节点树进行多维度下钻
    df:数据框,要求以日期列开始,标的指标列结尾。维度列均为字符串类型
    date:指定分析的日期
    file_name:保存文件名称
    '''
    
    # 1.计算整体波动量
    day_summary = df.iloc[:, -1].groupby(df.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)  # 整合为完整数据框
    
    # 2. 定义变量
    dimension_list = df.columns[1:-1].to_list()  # 分析的维度列表
    # 分析日期
    the_day = datetime.datetime.strptime(date, "%Y-%m-%d")  # 指定要分析的日期
    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 = df[df.iloc[:,0] == the_day].rename(columns={df.columns[-1]: day_col1})  # 获得指定日期数据
    previous_data = df[df.iloc[:,0] == previous_day].rename(columns={df.columns[-1]: 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] # 变化量
    # 整体对象
    nums, change, change_rate = day_summary_total[day_summary_total.index == the_day].values[0]
    top_nodes = {'total':'整体','change':change,'change_rate':change_rate}
    
    # 3. 自动节点分解
    main_nodes = [] # 主节点
    other_nodes = [] # 其他节点
    hidden_nodes = [] # 潜在节点
    main_edges = [] # 主边
    other_edges = [] # 其他边
    dim_copy = dimension_list+[day_col2,'change']
    for ind,dimension in enumerate(dimension_list):  # 遍历每个维度
        each_data = data_merge[dim_copy[ind:]] # 筛选数据
        each_merge_temp = each_data.groupby([dimension],as_index=False)[[day_col2,'change']].sum() # 计算变化量
        each_merge_temp = each_merge_temp.sort_values(['change']) # 排序
        each_merge_temp['each_change_rate'] = each_merge_temp['change']/each_merge_temp[day_col2] # 环比变化率
        previous_all = each_merge_temp.sum().iloc[1] # 总初始量
        change_all = each_merge_temp.sum().iloc[2] # 总变化量
        each_merge = each_merge_temp.drop(day_col2,axis=1) # 丢弃当日visit数值列
        if change_all<0: # 下降
            # node
            main_values_temp = each_merge_temp.iloc[0].tolist()
            main_values = each_merge.iloc[0].tolist() # 主因子节点
            main_nodes.append(main_values)
            other_nodes.append([f'{dimension}-others',change_all-main_values_temp[2],(change_all-main_values_temp[2])/(previous_all-main_values_temp[1])])
            if each_merge.iloc[-1].tolist()[1]>0:
                hidden_nodes.append(each_merge.iloc[-1].tolist()) # 当其他因子含有上升的计为潜在因子
            else:
                hidden_nodes.append([])
            # 数据过滤
            data_merge = each_data[each_data[dimension]==each_merge.iloc[0].iloc[0]]
        else: # 上升
            # node
            main_values_temp = each_merge_temp.iloc[-1].tolist()
            main_values = each_merge.iloc[-1].tolist() # 其他因子节点
            main_nodes.append(main_values)
            other_nodes.append([f'{dimension}-others',change_all-main_values_temp[2],(change_all-main_values_temp[2])/(previous_all-main_values_temp[1])])        
            if each_merge.iloc[0].tolist()[1]<0: 
                hidden_nodes.append(each_merge.iloc[0].tolist()) # 当其他因子含有下降的计为潜在因子
            else:
                hidden_nodes.append([])
            # 数据过滤
            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)
    
    # 4. 画图展示
    # 定义各个节点的样式
    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 = ['all']+dimension_list
    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 = top_nodes['total']
            node_top_label = node_style.format( 'black',"white",node_name,
                                                   int(top_nodes['change']),
                                                   top_nodes['change_rate'])  # 分别获取顶部节点名称、变化量和变化率
            child_dot.node(node_name, 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])  # 右侧节点显示的信息
        if split_node_hidden!=[]:
            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)  # 增加右侧节点
        if split_node_hidden!=[]:
            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)  # 增加右侧边
        if split_node_hidden!=[]:
            child_dot.edge(split_node_right[0], split_node_hidden[0],label = '潜在因子')  # 增加隐藏节点边

    parent_dot.subgraph(child_dot)
    parent_dot.view(file_name)  # 展示图形结果
    
    
    
# 读取数据
raw_data = pd.read_csv('advertising_data.csv')
# 数据预览
raw_data.head()

以上数据如果有需要的同学可关注公众号HsuHeinrich,回复【Python08】自动获取~

datesourcesitechannelmediavisit
02018/5/15品牌营销_品牌词品牌词产品播放器播放标签PC17600
12018/5/15手机_品牌营销_品牌词品牌词广告15秒前贴片_appapp15865
22018/5/15SEO百度WAP-10858
32018/5/15手机_品牌营销_品牌词品牌词运营移动端_乐见app9768
42018/5/15SEO百度PC-9228
# 数据处理要求:缺失值填充、日期字段转日期格式、维度字段转字符串格式

# 替换字符为0然后转换为整数型 本案例无缺失值,如果有缺失值需要额外处理
raw_data['visit'] = raw_data['visit'].replace('-', 0).astype(np.int64)
# 将日期字段转换为日期格式
raw_data['date'] = pd.to_datetime(raw_data['date'])
# 维度列转为字符串格式
col2str_list = ['source','site', 'channel', 'media']
raw_data[col2str_list] = raw_data[col2str_list].astype(str)
print('{:*^60}'.format('数据类型:'))
print(raw_data.dtypes)
***************************数据类型:****************************
date       datetime64[ns]
source             object
site               object
channel            object
media              object
visit               int64
dtype: object
# 绘制节点树图
autoNodeTree(raw_data, '2018-06-07', 'structure_dim_node_tree')

node_tree

这张图能清晰的知道,在2018-06-07日整体流量下降了18.8个百分点,主要是因为CRM渠道造成的,而CRM环比下降了17591基本都是准会员下降造成的。以此类推,直至最后一层。而且针对逆势上涨最明显的细分群体标记为潜在因子,提醒相关人员注意。

共勉~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于信息熵进行划分的决策算法是一种常用的机器学习算法,用于解决分类问题。下面是一个使用Python实现并绘制图形的示例。 首先,我们需要导入所需的库。在Python中,可以使用Scikit-learn库来实现决策算法,并使用Graphviz库来绘制决策图。 ```python from sklearn import datasets from sklearn.tree import DecisionTreeClassifier from sklearn import tree import graphviz ``` 接下来,我们需要准备用于训练和测试决策的数据集。在这个示例中,我们使用Scikit-learn库中的鸢尾花数据集(Iris dataset)。 ```python # 导入数据集 iris = datasets.load_iris() X = iris.data y = iris.target ``` 然后,我们可以使用决策分类器进行模型训练,并利用训练好的模型对新的数据进行预测。 ```python # 创建决策分类器 clf = DecisionTreeClassifier() # 训练决策模型 model = clf.fit(X, y) # 预测新的数据 new_data = [[5.0, 3.6, 1.4, 0.2]] prediction = model.predict(new_data) print("预测结果:", prediction) ``` 最后,我们可以使用Graphviz库来绘制决策图。 ```python dot_data = tree.export_graphviz(clf, out_file=None, feature_names=iris.feature_names, class_names=iris.target_names, filled=True, rounded=True, special_characters=True) graph = graphviz.Source(dot_data) graph.render("决策图", format="png", cleanup=True) # 打开决策图 graph.view() ``` 运行以上代码后,将生成一张名为"决策图.png"的图片,并自动打开该图片来显示决策的结构。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值