Fork me on GitHub

可视化神器Plotly玩转桑基图

可视化神器Plotly玩转桑基图

本文介绍的是利用Plotly绘制一种相对少见的可视化图形:桑基图,这个图形是展现数据流动的利器。

第一次接触桑基图的时候,使用Pyehcarts(以后会专门介绍这个国产的可视化神器)绘制过,本文将介绍如何使用Plotly来实现这个图形。

桑基图简介

什么是桑基图

桑基图(Sankey diagram),即桑基能量分流图,也叫做桑基能量平衡图。它描述的是一组值到另一组值的流向,是一种特定类型的流向图。桑基,其实是一个人名,全名叫:马修·亨利·菲尼亚斯·里尔·桑基(Matthew Henry Phineas Riall Sankey),是一名爱尔兰裔工程师,也是英国皇家陆军工兵的上尉。

在1898年的时候,他就使用这种图形来表示蒸汽机的能源效率, 在土木工程师学会会报纪要的一篇关于蒸汽机能源效率的文章中首次推出了第一个能量流动图,此后便以其名字命名为 Sankey 图,中文音译为桑基图:

下图为1869年,查尔斯米纳德(Charles Minard)绘制的1812年拿破仑征俄图(Map of Napolean’s Russian Campaign of 1812),这是一个在地图上覆盖桑基图的流程图,图形表示的是拿破仑军队进攻和撤退的军队力量对比:

桑基图特点

桑基图主要的特点:

  1. 起始流量和结束流量是相同的,所有主支宽度和所有分出去的分支宽度总和是相等的,保持能量的守恒
  2. 在桑基图的内部,不同的线条代表了不同的流量分布情况,节点不同的宽度代表了特定状态下的流量大小

桑基图构成的3要素:节点、流量、边

桑基图常用于能源、材料成分、金融等领域的可视化数据分析。本文最后会讲解一个实际的生活例子来说明桑基图的运用。

在看一个桑基图的例子:http://ecowest.org/2013/05/06/sankey-energy/

基础桑基图

下面的案例介绍的是基于plotly.graph_objects实现的基础桑基图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import pandas as pd
import numpy as np

import plotly_express as px
import plotly.graph_objects as go

# 构造数据

label = ["节点0", "节点1", "节点2", "节点3", "节点4", "节点5"]
# source和target是label中对应元素的索引值,python列表从0开始
source = [0, 0, 0, 1, 1, 0] # 可以看做父级节点
target = [2, 3, 5, 4, 5, 4] # 子级节点
value = [9, 3, 6, 2, 7, 8] # value是连接source和target之间的值

# 生成绘图需要的字典数据
link = dict(source = source, target = target, value = value)
node = dict(label = label, pad=200, thickness=20) # 节点数据,间隔和厚度设置

# 添加绘图数据
data = go.Sankey(link = link, node=node)

# 绘图并显示
fig = go.Figure(data)
fig.show()

解释一下上面的绘图代码,我们需要准备的数据有:

  • label:每个节点的名字,自己命名即可
  • soure:父节点,在plotly中是通过节点的索引来表示的,python中所用从0开始
  • target:数据流向的子节点
  • value:连接父节点和子节点的值

另外一种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fig = go.Figure(data=[go.Sankey(
node = dict(
pad = 200,
thickness = 20,
line = dict(color = "black", width = 0.1),
label = ["节点0", "节点1", "节点2", "节点3", "节点4", "节点5"],
color = "blue"
),
link = dict(
source = [0, 0, 0, 1, 1, 0], # 对应label的索引值
target = [2, 3, 5, 4, 5, 4],
value = [9, 3, 6, 2, 7, 8]
))])

fig.update_layout(title_text="Plotly绘制桑基图", font_size=10)
fig.show()

基于json文件格式数据的桑基图

在plotly官网中有这样的一个例子:从给定的一个网站上下载json文件来绘制桑基图,分步骤来讲解下:

1、读取json文件并转成python字典数据

1
2
3
4
5
import urllib, json  # 同时导入多个库包

url = 'https://raw.githubusercontent.com/plotly/plotly.js/master/test/image/mocks/sankey_energy.json'
response = urllib.request.urlopen(url) # 获取json文件
data = json.loads(response.read()) # json文件转成python字典

如何将字典格式的数据输出成json文件,并美化格式?

1
2
3
4
5
6
with open("sankey.json","a",encoding="utf-8") as f:
json.dump(data, # 待写入数据
f, # File对象
indent=2, # 空格缩进符,写入多行
sort_keys=True, # 键的排序
ensure_ascii=False) # 显示中文

美化后文件的大致格式(部分截图):

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
opacity = 0.6   # 透明度设置

fig = go.Figure(data=[go.Sankey(
valueformat = ".0f",
valuesuffix = "TWh",

# 节点定义
node = dict(
pad = 15, # 间隔
thickness = 15, # 边的宽度
line = dict(color = "black", width = 0.5),
label = data['data'][0]['node']['label'], # 数据中对应的标签和颜色
color = data['data'][0]['node']['color']
),

# 连接数据
link = dict( # 父节点、子节点、流量的值、节点名称、颜色设置
source = data['data'][0]['link']['source'],
target = data['data'][0]['link']['target'],
value = data['data'][0]['link']['value'],
label = data['data'][0]['link']['label'],
color = data['data'][0]['link']['color']
))])

# 重点:标题中可以使用html标签
fig.update_layout(title_text="Plotly读取json文件绘制桑基图 via <a href='https://bost.ocks.org/mike/sankey/'>Mike Bostock</a>",
font_size=10)
fig.show()

还可以对图形的背景色进行设置:

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
import plotly.graph_objects as go
import urllib, json

# 在线读取数据并转成字典格式
url = 'https://raw.githubusercontent.com/plotly/plotly.js/master/test/image/mocks/sankey_energy.json'
response = urllib.request.urlopen(url)
data = json.loads(response.read())

# 设置图片的参数
fig = go.Figure(data=[go.Sankey(
valueformat = ".0f",
valuesuffix = "TWh",
node = dict(
pad = 15,
thickness = 15,
line = dict(color = "black", width = 0.5),
label = data['data'][0]['node']['label'],
color = data['data'][0]['node']['color']
),
link = dict(
source = data['data'][0]['link']['source'],
target = data['data'][0]['link']['target'],
value = data['data'][0]['link']['value'],
label = data['data'][0]['link']['label']
))])

# 进行背景色的设置
fig.update_layout(
hovermode = 'x',
title="桑基图绘制_改变背景色",
font=dict(size = 10, color = 'white'),
plot_bgcolor='green',
paper_bgcolor='black' # 整个图的背景(黑色部分)
)

fig.show()

特色桑基图

自定义位置的“桑基图”

在这里绘制的桑基图,是通过xy来自定义节点的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import plotly.graph_objects as go

fig = go.Figure(go.Sankey(
arrangement = "snap",
node = {
"label": ["节点0", "节点1", "节点2", "节点3", "节点4", "节点5"], # 节点名称
"x": [0.2, 0.1, 0.5, 0.7, 0.3, 0.5], # xy来决定位置
"y": [0.6, 0.5, 0.2, 0.4, 0.2, 0.5],
'pad':1}, # 间隔
link = {
"source": [0, 0, 1, 2, 3, 4, 3, 5], # 父子节点和流量值
"target": [5, 3, 4, 3, 0, 2, 2, 3],
"value": [8, 12, 12, 11, 11, 10, 11, 12]}))

fig.show()

通过观察图形,整体画布的坐标原点应该是在左上角,横轴向右为正,纵轴向下为正。

自定义节点和边的颜色

通过color_mode和color_link参数能够自定义桑基图的节点和边的颜色:

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
import plotly.graph_objects as go

# 构造节点数据

label = ["节点0", "节点1", "节点2", "节点3", "节点4", "节点5"]
source = [0, 0, 0, 1, 1, 0]
target = [2, 3, 5, 4, 5, 4]
value = [9, 3, 6, 2, 7, 8]


# 自定义颜色
color_node = ['#E8C9B0', '#48C9B0', '#A8C9B0','#AF7AC5', '#AF7AC5', '#AF7AC5']

color_link = ['#A6E3D7', '#D6E3D7', '#A6E3D7','#CBB4D5', '#CBB4D5', '#CBB4D5']

# 生成绘图需要的字典数据
link = dict(source = source, target = target, value = value, color=color_link)
node = dict(label = label, pad=200, thickness=20, color=color_node) # 节点数据,间隔和厚度设置

# 添加绘图数据
data = go.Sankey(link = link, node=node)

# 绘图并显示
fig = go.Figure(data)
fig.show()

桑基图_月度开销

下面通过小明同学一个月的总开支消费来讲解如何在实际数据中绘制桑基图。

1、首先我们看看小明同学整理的消费数据(虚拟数据)

小明同学的开支主要分为5大块:住宿、餐饮、餐饮、交通、服装、红包,每个块中又分为各自的子块,以及对应的消费。

2、整理数据,表明父级到子级的消费情况

因为桑基图的绘制是需要父级和子级节点之间的数据,所以我们需要先整体下数据:

下面的图形是5大主块的整理数据:

下面的图形是各个子块对应的父级和子级数据整理:

3、读取数据

然后将上面的两个数据放在一起,我们通过pandas读进来:

4、找到数据的父类和子类中总共有多少个不同的元素,并进行索引的设置

将父类和子类的中元素全部加起来,再用集合set去重,找出全部的节点名称

1
2
3
4
5
6
# set集合去重

labels = list(set(df["父类"].tolist() + df["子类"].tolist()))
labels

# len(labels) 23

接下来我们需要对每个节点进行索引的设置:

将节点和索引进行字典形式的组合:

分别根据父类节点和子类节点来生成对应的索引数据:

1
2
3
df["父类索引"] = df["父类"].map(index)
df["子类索引"] = df["子类"].map(index)
df

终于看到了胜利的曙光,找到了我们需要绘图的数据:数据+父类索引+子类索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import plotly.graph_objects as go

# 标签就是index字典中的key键
label = list(index.keys())
# 父类和子类
source = df["父类索引"].tolist()
target = df["子类索引"].tolist()
# 流量的值
value = df["数据"].tolist()

# 生成绘图需要的字典数据
link = dict(source = source, target = target, value = value)
node = dict(label = label, pad=200, thickness=40)

data = go.Sankey(link = link, node=node)

fig = go.Figure(data)

fig.update_layout(title=dict(text="打工人月度开销——桑基图",x=0.5,y=0.97))
fig.show()

看下最终的效果图:

本文标题:可视化神器Plotly玩转桑基图

发布时间:2021年06月27日 - 19:06

原始链接:http://www.renpeter.cn/2021/06/27/%E5%8F%AF%E8%A7%86%E5%8C%96%E7%A5%9E%E5%99%A8Plotly%E7%8E%A9%E8%BD%AC%E6%A1%91%E5%9F%BA%E5%9B%BE.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Coffee or Tea