2020-006 用桑基图分析转专业数据

用桑基图分析转专业数据

数据来源:西南交通大学教务网

西南交通大学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:

image-20200727200929898

看起来还是比较不错的,但是很显然,地球科学与环境工程学院少了一个院字。

再看一下2018和2017的数据。

data_2018 = pd.read_excel('转专业2018-已转档.xlsx',skiprows=1)
data_2018.head()

out:

image-20200727202307456
data_2017 = pd.read_excel('转专业2017-已转档.xls',skiprows=1) 
data_2017.head()

out:

image-20200727202549887

首先,将各个数据加上年份,合并。

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:

image-20200727204748581

绘图

绘图呢,首先要学示例,桑基图的官方示例在 pyecharts桑基图

主要核心在于定义nodeslinks

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代表线,有来源、去处和值。最后renderhtml,使用浏览器打开就能查看了。

拿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_mapout_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')))) 

我加了一些参数,比如图片的widthheightpos_leftpos_right是绘制的图到边缘的比例,opacity透明度,curve弯曲度,color='source'表明线的颜色根据source决定,width是线的长度,position = 'left'表明标签在节点的左边,还定义了很多字体和TitleOpts自定义的内容,好的图片就是慢慢的调整才会好看。

使用render函数渲染到html。pyecharts图的优势之处在于它是交互式的图标,你可以将鼠标放在上面查看内容。

2019年转专业-加水印

从图中可以看出,转入信院、电气、交运的人数众多,都有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实现全网页截图,不过这个图片有很多白边,还需要进行裁剪。

image-20200727211211923

pyecharts也提供了保存图片的方式,不过需要安装selenium或者phantomjs等Web自动化工具,最终实现的还是模拟网页截图,图片可能依然有大白边,因此我没有采用。

image-20200727211523263

转出比例

在我将图发到空间后,有人说转专业的人数并不能代表这个学院的流失率。之前的图表只是反映了哪些学院更热门。

左侧的转出人数相对来说信息量较小,如果以学院的人数为基准进行转出比的计算,就能够非常直观的体现学院的流失率了。

那就开干!

此处感谢马大佬提供学院人数数据,数据来自入学信息,略有不准。

num_of_stu = pd.read_excel('学院人数.xlsx')
num_of_stu.head()

out:

image-20200727213226761

我*,这么早的都有,果然是大佬。

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()
2019年2018级按转出-加水印

图中,左侧的比例代表转出人数占当前学院人数的比例,右边的比例代表转入人数比拟转入学院原人数。中间线的粗细,代表流动人数占当前学院人数的比例。

从图中可以看出,马院、生命学院和地环学院的学生流失严重,马院达到了惊人的26%,可怕。因为地环学院有一部分学院内部转专业的,所以生命学院比地环学院的实际流失率要严重一些。啊,快跑!

links里的值按转入人数占拟转入学院2018级人数的比例试一下。

2019年2018级按转入-加水印

从图中可以看出,电气学院和交运学院都是非常友好的,很欢迎其他学院转入,信息学院第三,人文学院第四。

马大佬强势出场

按人数和按比例各有千秋,都有信息量,马大佬建议我整合起来。

如果左侧是转出人数占比,右侧是人数,那么一张图就能展示学院流失率与学院喜好。但是由于值的量纲不同,这种操作需要强行修改渲染器,我实力太菜,搞不了。

马大佬决定亲自上手,用matlab从头绘制了一个。

2019_转专业_按转出学院基数归一化(2)-加水印

马大佬的图按数值大小排了序,两列节点的标签在两边,在pyecharts中我都没有找到对应的实现方式。

果然自己从头绘制才是定制性最强的,给马大佬鼓掌。

最后

本次只分析了2019年的转专业数据,后续分析等待进一步进行。

你有什么分析建议呢,欢迎留言👉。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270