用桑基图分析转专业数据
数据来源:西南交通大学教务网
西南交通大学2019年本科生转专业名单公示
西南交通大学2018年本科生转专业名单公示
西南交通大学2017年本科生转专业名单公示
2019和2018的数据下载后都是pdf格式,使用pdf处理网站 ilovepdf 将pdf转化成excel。
什么是桑基图
来自百度百科
桑基图(Sankey diagram),即桑基能量分流图,也叫桑基能量平衡图。它是一种特定类型的流程图,图中延伸的分支的宽度对应数据流量的大小,通常应用于能源、材料成分、金融等数据的可视化分析。因1898年Matthew Henry Phineas Riall Sankey绘制的“蒸汽机的能源效率图”而闻名,此后便以其名字命名为“桑基图”。
所以,桑基图中分支的宽度对应数据流量的大小,是展现数据流动的利器。
来看一个示例,图源 驴说蛙语/数据可视之美 - 桑基图
[图片上传失败...(image-c6f6c4-1641968929253)]
是不是很好看,很厉害,非常的nice?
那我们用它来展示一下转专业数据。
pyecharts
搜索了一下,python中pyecharts绘图包能够实现桑基图的绘制。
使用pip可以很容易的安装。
pip install pyecharts
有一点注意的是,pyecharts 分为 v0.5.X 和 v1 两个大版本,v0.5.X 和 v1 间不兼容。网上的教程有一些是基于v0.5.X的,因此要注意鉴别,推荐去官网查看教程。本文中,pyecharts的版本是1.8.1。
数据读取清洗
首先读取数据。
import pandas as pd
data_2019 = pd.read_excel('转专业2019-已转档.xlsx',skiprows=1) #第一行是标题,skiprows=1跳过第一行
data_2019.head()
out:
看起来还是比较不错的,但是很显然,地球科学与环境工程学院少了一个院字。
再看一下2018和2017的数据。
data_2018 = pd.read_excel('转专业2018-已转档.xlsx',skiprows=1)
data_2018.head()
out:
data_2017 = pd.read_excel('转专业2017-已转档.xls',skiprows=1)
data_2017.head()
out:
首先,将各个数据加上年份,合并。
data_2019['年份'] = 2019
data_2018['年份'] = 2018
data_2017['年份'] = 2017
data = pd.concat([data_2019,data_2018,data_2017],axis=0)
我们想以学院作为单位查看流动情况,那么主要使用到的列就是当前学院、拟转入学院。
看一下这两列的情况。
data['当前学院'].unique()
out:
array(['机械工程学院', '材料科学与工程学院', '生命科学与工程学院', '地球科学与环境工程学', '公共管理与政法学院',
'经济管理学院', '马克思主义学院', '土木工程学院', '力学与工程学院', '交通运输与物流学院', '心理研究与咨询中心',
'数学学院', '物理科学与技术学院', '电气工程学院', '茅以升学院', '信息科学与技术学院', '建筑与设计学院',
'人文学院', '外国语学院', '地球科学与环境工程学\n院', '西南交大-利兹学院', '地球科学与环境工程学院'],
dtype=object)
有带\n
的,还有学院少个院的。
再看下拟转入学院。
data['拟转入学院'].unique()
out:
array(['土木工程学院', '机械工程学院', '电气工程学院', '信息科学与技术学院', '交通运输与物流学院', '经济管理学院',
'人文学院', '外国语学院', '建筑与设计学院', '材料科学与工程学院', '力学与工程学院', '数学学院',
'物理科学与技术学院', '生命科学与工程学院', '地球科学与环境工程学院', '公共管理与政法学院', '茅以升学院',
'心理研究与咨询中心', '信息科学与技术学', '信息科学与技术学\n院', '交通运输与物流学', '交通运输与物流学\n院',
'材料科学与工程学\n院', '材料科学与工程学', '物理科学与技术学\n院', '生命科学与工程学', '地球科学与环境工',
'公共管理与政法学', '西南交大-利兹学院', '心理研究与咨询中', '心理研究与咨询中\n心', '马克思主义学院'],
dtype=object)
有带\n
的,中心少了心,学院少了院,地球科学与环境工直接少了程学院三个字。
处理一下。
data['当前学院'] = data['当前学院'].str.replace(r"\n",'')
data['当前学院'] = data['当前学院'].str.replace('学$','学院')
data['拟转入学院'] = data['拟转入学院'].str.replace(r"\n",'')
data['拟转入学院'] = data['拟转入学院'].replace("地球科学与环境工",'地球科学与环境工程学院')
data['拟转入学院'] = data['拟转入学院'].str.replace('学$','学院')
data['拟转入学院'] = data['拟转入学院'].str.replace('中$','中心')
series
直接replace
是整个的替换,str.replace
是部分匹配替换。
$
的意思是正则匹配从结尾开始匹配,当以学为结尾时替换为学院,当以中为结尾时替换为中心。
看一下处理后的结果。
data['当前学院'].unique()
out:
array(['机械工程学院', '材料科学与工程学院', '生命科学与工程学院', '地球科学与环境工程学院', '公共管理与政法学院',
'经济管理学院', '马克思主义学院', '土木工程学院', '力学与工程学院', '交通运输与物流学院', '心理研究与咨询中心',
'数学学院', '物理科学与技术学院', '电气工程学院', '茅以升学院', '信息科学与技术学院', '建筑与设计学院',
'人文学院', '外国语学院', '西南交大-利兹学院'], dtype=object)
data['拟转入学院'].unique()
out:
array(['土木工程学院', '机械工程学院', '电气工程学院', '信息科学与技术学院', '交通运输与物流学院', '经济管理学院',
'人文学院', '外国语学院', '建筑与设计学院', '材料科学与工程学院', '力学与工程学院', '数学学院',
'物理科学与技术学院', '生命科学与工程学院', '地球科学与环境工程学院', '公共管理与政法学院', '茅以升学院',
'心理研究与咨询中心', '西南交大-利兹学院', '马克思主义学院'], dtype=object)
用集合运算看一下差集,看是不是完全一样。
set(data['当前学院'].unique()) - set(data['拟转入学院'].unique())
out:
set()
set(data['拟转入学院'].unique()) - set(data['当前学院'].unique())
out:
set()
确实一样了。
再加上一个年级,毕竟转专业大二的多,大三的少,可以作为一个点来分析。
data['年级'] = data['学号'].astype(str).str[:4].astype(int)
这行代码的意思是将学号列转为str
取前四位再转为int
,其实除以1e6
也可以。小数据就不纠结性能问题了,怎么方便怎么来。
最后看一下数据。
data.head()
out:
绘图
绘图呢,首先要学示例,桑基图的官方示例在 pyecharts桑基图
主要核心在于定义nodes
与links
,
from pyecharts import options as opts
from pyecharts.charts import Sankey
nodes = [
{"name": "category1"},
{"name": "category2"},
{"name": "category3"},
{"name": "category4"},
{"name": "category5"},
{"name": "category6"},
]
links = [
{"source": "category1", "target": "category2", "value": 10},
{"source": "category2", "target": "category3", "value": 15},
{"source": "category3", "target": "category4", "value": 20},
{"source": "category5", "target": "category6", "value": 25},
]
c = (
Sankey()
.add(
"sankey",
nodes,
links,
linestyle_opt=opts.LineStyleOpts(opacity=0.2, curve=0.5, color="source"),
label_opts=opts.LabelOpts(position="right"),
)
.set_global_opts(title_opts=opts.TitleOpts(title="Sankey-基本示例"))
.render("sankey_base.html")
)
nodes
代表点,有名字,links
代表线,有来源、去处和值。最后render
到html
,使用浏览器打开就能查看了。
拿2019的数据试试手,看看2019年转专业的情况如何。
all_data = data
data = all_data[all_data['年份']==2019]
将原来的数据用all_data
存起来。
nodes = []
out_map = {}
in_map = {}
value_counts = data['当前学院'].value_counts()
for name,value in value_counts.items():
nodes.append({'name':f'转出-{name}-{value}'})
out_map[name] = f'转出-{name}-{value}'
value_counts = data['拟转入学院'].value_counts()
for name,value in value_counts.items():
nodes.append({'name':f'转入-{name}-{value}'})
in_map[name] = f'转入-{name}-{value}'
links = []
out_values = data['当前学院'].value_counts().index
in_values = data['拟转入学院'].value_counts().index
for i in out_values:
for j in in_values:
counts = data[(data['当前学院']==i) & (data['拟转入学院']==j)]
if counts.empty:
continue
else:
links.append({'source': out_map[i], 'target': in_map[j], 'value': counts.shape[0]})
在定义节点名称的时候,我希望把转入转出以及对应的值也写到名称里,所以也就需要一个in_map
和out_map
来做映射。
然后生成pic
,没有直接渲染是因为如果输出到jupyterlab
内部,需要这个对象进行再次render
。
pic = (Sankey(init_opts = opts.InitOpts(width='1200px',height='1000px'))
.add('', nodes,links,
pos_left='16%',pos_right='0%',
node_width = 30,node_gap = 20,
linestyle_opt=opts.LineStyleOpts(opacity = 0.4,curve = 0.7,color = 'source',width=10),
label_opts=opts.LabelOpts (position = 'left',font_family='Times New Roman'))
.set_global_opts(
title_opts=opts.TitleOpts(title = '2019年-转专业',subtitle=' 人数',pos_left='50%',
title_textstyle_opts=opts.TextStyleOpts(font_size=20,font_family='Times New Roman',font_weight='bold'),
subtitle_textstyle_opts=opts.TextStyleOpts(font_size=16,font_family='Times New Roman',font_weight='normal',color='black'))))
我加了一些参数,比如图片的width
和height
,pos_left
和pos_right
是绘制的图到边缘的比例,opacity
透明度,curve
弯曲度,color='source'
表明线的颜色根据source决定,width
是线的长度,position = 'left'
表明标签在节点的左边,还定义了很多字体和TitleOpts
自定义的内容,好的图片就是慢慢的调整才会好看。
使用render
函数渲染到html
。pyecharts图的优势之处在于它是交互式的图标,你可以将鼠标放在上面查看内容。
从图中可以看出,转入信院、电气、交运的人数众多,都有50左右。看来大家都知道学校的优势专业在哪。地院转出人数最多,达到了77。
我coding的时候使用的是jupyterlab
,显然渲染到jupyterlab
内部更友好,pyecharts也提供了这种方式。
最开始需要导入并设置NOTEBOOK_TYPE
。
from pyecharts.globals import CurrentConfig, NotebookType
CurrentConfig.NOTEBOOK_TYPE = NotebookType.JUPYTER_LAB
然后获得pic
后。
pic.load_javascript()
pic.render_notebook()
就可以在jupyterlab
中看到绘制的图形了。
图片的保存稍麻烦,截图当然不是最好的方式。在渲染成html
后,可以使用Chrome F12打开Devtools,然后按ctrl+shift+p,输入capture,选择capture full size screen
,就可以利用Chrome实现全网页截图,不过这个图片有很多白边,还需要进行裁剪。
pyecharts也提供了保存图片的方式,不过需要安装selenium
或者phantomjs
等Web自动化工具,最终实现的还是模拟网页截图,图片可能依然有大白边,因此我没有采用。
转出比例
在我将图发到空间后,有人说转专业的人数并不能代表这个学院的流失率。之前的图表只是反映了哪些学院更热门。
左侧的转出人数相对来说信息量较小,如果以学院的人数为基准进行转出比的计算,就能够非常直观的体现学院的流失率了。
那就开干!
此处感谢马大佬提供学院人数数据,数据来自入学信息,略有不准。
num_of_stu = pd.read_excel('学院人数.xlsx')
num_of_stu.head()
out:
我*,这么早的都有,果然是大佬。
2019年转专业主要是2018级的,将2018级的提出来。
再看看学院能不能对应起来。
num_of_stu_2018 = num_of_stu[num_of_stu['年级']==2018]
set(num_of_stu_2018['学院'].unique()) - set(data['当前学院'].unique())
out:
{'利兹学院', '国际教育学院', '少数民族预科'}
国际教育学院和少数民族预科不在当前学院当中,不用管,利兹名字有错误,改一下。
num_of_stu_2018['学院'].replace("利兹学院",'西南交大-利兹学院',inplace=True) #inplace=True,直接更改原数据
让学院变成index
,易于访问。
num_of_stu_2018.set_index('学院',inplace=True)
然后我们只取2018级的转专业学生。如果不提出来,相当于2017级和2018级一起的转专业人数比2018级人数,数据就偏大了。
data = all_data[(all_data['年份']==2019)&(all_data['年级']==2018)] # &在pandas用于条件且判断,括号必须加
接下来绘图。
比例按百分比展示取小数点后2位,links
里的值按转出人数占当前学院2018级人数的比例。
nodes = []
out_map = {}
in_map = {}
value_counts = data['当前学院'].value_counts()
for name,value in value_counts.items():
ratio = value/num_of_stu_2018.loc[name,"人数"]
nodes.append({'name':f'转出-{name}-{ratio:.2%}'})
out_map[name] = f'转出-{name}-{ratio:.2%}'
value_counts = data['拟转入学院'].value_counts()
for name,value in value_counts.items():
ratio = value/num_of_stu_2018.loc[name,"人数"]
nodes.append({'name':f'转入-{name}-{ratio:.2%}'})
in_map[name] = f'转入-{name}-{ratio:.2%}'
links = []
out_values = data['当前学院'].value_counts().index
in_values = data['拟转入学院'].value_counts().index
for i in out_values:
for j in in_values:
counts = data[(data['当前学院']==i) & (data['拟转入学院']==j)]
if counts.empty:
continue
else:
ratio = counts.shape[0]/num_of_stu_2018.loc[i,"人数"]
links.append({'source': out_map[i], 'target': in_map[j], 'value': ratio})
pic = (Sankey(init_opts = opts.InitOpts(width='1200px',height='1000px'))
.add('', nodes,links,
pos_left='18%',pos_right='0%',
node_width = 30,node_gap = 20,
linestyle_opt=opts.LineStyleOpts(opacity = 0.4,curve = 0.7,color = 'source',width=10),
label_opts=opts.LabelOpts (position = 'left',font_family='Times New Roman'))
.set_global_opts(
title_opts=opts.TitleOpts(title = ' 2019年-2018级-转专业',subtitle='转入或转出人数/当前学院2018级人数',pos_left='50%',
title_textstyle_opts=opts.TextStyleOpts(font_size=20,font_family='Times New Roman',font_weight='bold'),
subtitle_textstyle_opts=opts.TextStyleOpts(font_size=16,font_family='Times New Roman',font_weight='normal',color='black'))))
pic.load_javascript()
pic.render_notebook()
图中,左侧的比例代表转出人数占当前学院人数的比例,右边的比例代表转入人数比拟转入学院原人数。中间线的粗细,代表流动人数占当前学院人数的比例。
从图中可以看出,马院、生命学院和地环学院的学生流失严重,马院达到了惊人的26%,可怕。因为地环学院有一部分学院内部转专业的,所以生命学院比地环学院的实际流失率要严重一些。啊,快跑!
将links
里的值按转入人数占拟转入学院2018级人数的比例试一下。
从图中可以看出,电气学院和交运学院都是非常友好的,很欢迎其他学院转入,信息学院第三,人文学院第四。
马大佬强势出场
按人数和按比例各有千秋,都有信息量,马大佬建议我整合起来。
如果左侧是转出人数占比,右侧是人数,那么一张图就能展示学院流失率与学院喜好。但是由于值的量纲不同,这种操作需要强行修改渲染器,我实力太菜,搞不了。
马大佬决定亲自上手,用matlab从头绘制了一个。
马大佬的图按数值大小排了序,两列节点的标签在两边,在pyecharts中我都没有找到对应的实现方式。
果然自己从头绘制才是定制性最强的,给马大佬鼓掌。
最后
本次只分析了2019年的转专业数据,后续分析等待进一步进行。
你有什么分析建议呢,欢迎留言。