美文网首页
用桑基图分析转专业数据

用桑基图分析转专业数据

作者: SSSimonYang | 来源:发表于2020-08-11 17:17 被阅读0次

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

    西南交通大学2019年本科生转专业名单公示

    西南交通大学2018年本科生转专业名单公示

    西南交通大学2017年本科生转专业名单公示

    2019和2018的数据下载后都是pdf格式,使用pdf处理网站 ilovepdf 将pdf转化成excel。

    什么是桑基图

    来自百度百科

    桑基图(Sankey diagram),即桑基能量分流图,也叫桑基能量平衡图。它是一种特定类型的流程图,图中延伸的分支的宽度对应数据流量的大小,通常应用于能源、材料成分、金融等数据的可视化分析。因1898年Matthew Henry Phineas Riall Sankey绘制的“蒸汽机的能源效率图”而闻名,此后便以其名字命名为“桑基图”。

    所以,桑基图中分支的宽度对应数据流量的大小,是展现数据流动的利器。

    来看一个示例,图源 驴说蛙语/数据可视之美 - 桑基图

    [图片上传失败...(image-2082b3-1597058940616)]

    是不是很好看,很厉害,非常的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年的转专业数据,后续分析等待进一步进行。

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

    相关文章

      网友评论

          本文标题:用桑基图分析转专业数据

          本文链接:https://www.haomeiwen.com/subject/abtsdktx.html