大学大比拼

标题图片 高考刚刚结束,成绩陆续公布,接下来就要填报志愿了,如何挑选一个合适的大学几乎是每个考生面临的问题,多年前,为填报志愿通宵翻阅招生指南的情形还历历在目,今天我们从中国大学综合排名以及其他指标为视角,对大学进行分析对比,也许能对目标大学有更好的认识

先看下部分结果图

指标对比

排名走势

下面将以构建过程为顺序,进行说明,如果只想要结果,翻到文后,查看并回复关键字,获取代码运行即可

数据采集

数据分析的第一步是数据采集,搜索查找后,发现 最好大学网 上有公布的中国大学数据,从 2015 年至今,从十个不同维度对学校进行记录,用来分析对比再好不过

爬取时会有些障碍点:

  • 2015 年数据,分了将 10 个指标分为 3 大类,每一类用单独的网页来呈现,而后的几年只用一个页面呈现了所有指标数据
  • 有些年份数据中,双标题中包含了换行符或者回车符,需要识别并处理
  • 还有些标题并非单纯文字,包含了 Html 标记,获取到后需要单独处理,从中取到文字部分

这是数据抓取的部分核心代码:

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
from bs4 import BeautifulSoup as Bs
import pandas as pd
import numpy as np

def dataProcessing(html, num):
    rows = Bs(html, 'html.parser').table.find_all('tr', limit=num, recursive=True)

    thf = []
    for th in rows[0].find_all('th', limit=10):
        if th.text.find('\r\n') == -1 and th.text.find('\n') == -1:
            thf.append(th.contents)
    for i in [op.contents for op in rows[0].find_all('option', recursive=True)]:
        thf.append(i)
    thf = ["".join(th) for th in thf]

    universityList = []
    for tr in rows[1:]:
        tds = tr.find_all('td')
        if len(tds) == 0:
            continue
        contents = [td.contents for td in tds]
        if len(contents[0]) > 1:
            contents[0] = [contents[0][0]]
        contents[1] = contents[1][0].contents
        universityList.append(["".join(attr) for attr in contents])

    return pd.DataFrame(np.array(universityList), columns=thf)
  • dataProcessing 方法接收一个字符串,和一个数值,字符串为目标页面的 Html 文本,数值表示需要获取的大学数量,分析数据后,2020 年记录的大学最全,共有 549 个
  • 先将 html 字符串 转换为 BeautifulSoup 对象,再从 table 对象中筛选出 tr 即列表行来
  • 第一行为标题,除了 2015 年,其他年份处理方式一样,先获取简单标题,之后,从指标选择框中获取指标标题,存放在 thf
  • 接下来处理记录,从中提取每一列数据,存放在 universityList
  • 最后将记录数据转换为 numpy.array 结构,用标题做列,将数据创建成 pandas 的 DataFrame 结构并返回

转换为 DataFrame 结构主要是为了方便存储为 Excel,调用 DataFrame.to_excel 方法,指定文件名,就能将数据保存到 Excel 文件

特别提醒: 数据分析中,原始数据至关重要,无论做什么分析,请确保原始数据不被修改,这也是为什么不直接对获取的 DataFrame 进行处理,而是先存储为 Excel 的原因

数据整理

原始数据常常需要清理后才能被使用,因为可能一些问题,直接分析,可能出错,或者影响分析结果

对数据检查后发现,存在以下问题:

  • 存在空缺值,比如就业率,有些学校的没有被统计
  • 存在指标空缺或不一致,比如 2015、2016 年度,没有留学生比例指标
  • 存在非数值字符,比如科研质量指标有些数字前有 >
  • 不同指标采用的数值范围不同,比如社会声誉范围从 0 到 百万,而科研质量数值范围仅在 0 ~ 2 之间
  • 原始数据的指标名称较长,比如 生源质量(新生高考成绩得分),太长的名称不利于图表展示,需要对指标名称进行处理

针对上述问题,做了个配置,定义不同指标的处理方式,另外可以在处理逻辑中减少对具体的业务问题的依赖,使代码更简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## 读取文件 选择特定的列,队列进行归一化,之后对整个数值进行归一化
config = {
    '学校名称': {'name': '学校'},
    '省市': {'name': '省市'},
    '排名': {'name': '排名', 'fun': lambda x: 500-int(x) if x is not np.nan else int(x)},
    '总分': {'name': '总分'},
    '生源质量(新生高考成绩得分)':{'name': '生源质量', 'norm': True},
    '培养成果(本科毕业生就业率)':{'name': '就业', 'alias': '培养结果(毕业生就业率)', 'norm': True, 'fun': lambda x: float(x[:-1])/100 if x is not np.nan else float(x)},
    '社会声誉(社会捐赠收入·千元)':{'name': '声誉', 'norm': True},
    '科研规模(论文数量·篇)':{'name': '科研规模', 'norm': True},
    '科研质量(论文质量·FWCI)':{'name': '科研质量', 'norm': True, 'fun': lambda x: float(x) if str(x).find(">")==-1 else float(x[1:]) },
    '顶尖成果(高被引论文·篇)':{'name': '顶尖成果', 'norm': True},
    '顶尖人才(高被引学者·人)':{'name': '顶尖人才', 'norm': True},
    '科技服务(企业科研经费·千元)':{'name': '科技服务', 'norm': True},
    '成果转化(技术转让收入·千元)':{'name': '成果转化', 'norm': True},
    '学生国际化(留学生比例)':{'name': '学生国际化', 'norm': True, 'fun': lambda x: float(x) if str(x).find("%")==-1 else float(x[:-1])}
}
  • 配置是一个 Dirt 对象,Key 为原始数据的列名,值也是一个 Dirt 对象,用于说明应该如何处理原始数据
  • name 表示需要将原始列名换成的列名
  • norm 表示是否要对该列做归一化处理(后面会解释)
  • alias 表示列的别名,在原始数据中,存在不同年份同一指标使用不同列名的现象,为此而设立
  • fun 表示一个对原始值进行处理的方法,比如取消百分号,大于号或者做数据反转等,采用 lambda 表达式,使用定义好的方法名也可以

这里着重说明下归一化

归一化的基本含义是将原始的数值,转换为从 0 ~ 1 之间的某个数值,为什么要这么做呢?是为了让不同指标可对比,想象一下,如果不处理,数值在 1 ~ 10 的指标就没法和数值在 1000 ~ 10000 的指标进行对比

归一化的主要方式一般有 3 种,线性转换、对数函数转换反余切函数转换

这里采用线性转换,公式是:

线性转换

用 pandas 实现代码为:

1
 dft = (dft - dft.min())/(dft.max() - dft.min())

dft 为一个指标的数值集合

处理完成数据后,将处理结果存入 Excel 文件(注意不要覆盖原始数据文件),之所以将数据存入文件,是为了后续查询时,不必每次都将原始数据处理一遍

数据分析及可视化

数据分析首先需要明确目标,我们就以展示不同大学在多个指标上的能力对比,以及某个指标在各年份的走势为目标

分析

因为数据整理中,对各个指标做了归一化处理,所有指标之间具备了可比性

结合业务对数据进行研究,得到以下结论:

  • 目前各个学校,很重视就业率,采用各种方式会将就业率提高,数据中的体现是,无论学校好坏,就业率都在 90% 以上
  • 中国大学,除了专门的外国语学校,外国留学生很少,数据中,学生国际化指标普遍较低,鉴于本分析是针对国内考生的,此指标意义不大
  • 成果转换,表示的是年度技术转让收入,目前各个学校都是以培养实用型人才为目标,纯理论科研水平和意识较低,成果转换普遍较低

基于以上结论,在指标对比分析中,去除掉 就业率、学生国际化 和 成果转换 三个指标

可视化

多项指标对比,使用雷达图比较合适,可以直观的展示出一个学校的能力范围,另外,雷达图能同时对多个学校进行对比,代码如下:

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
def indicatorChart(schools, indicators=['生源质量','声誉','科研规模',
'科研质量','顶尖成果','顶尖人才','科技服务',], year=2019):
    """
    指定学校,年份,以及需要对比的指标,显示出雷达图
    """
    year = str(year) + '年'
    result = pd.DataFrame(index=schools,columns=indicators)
    df = data[year][indicators]
    for school in schools:
        if school in df.index:
            result.loc[school] = df.loc[school]

    labels=result.columns.values #特征值
    kinds = list(result.index) #成员变量
    result = pd.concat([result, result[[labels[0]]]], axis=1) # 由于在雷达图中,要保证数据闭合,这里就再添加第一列,并转换为np.ndarray
    centers = np.array(result.iloc[:,:])
    n = len(labels)
    angle = np.linspace(0, 2 * np.pi, n, endpoint=False)# 设置雷达图的角度,用于平分切开一个圆面
    angle = np.concatenate((angle, [angle[0]]))#为了使雷达图一圈封闭起来,需要下面的步骤
    fig = plt.figure()
    ax = fig.add_subplot(111, polar=True) # 参数polar, 以极坐标的形式绘制图

    for i in range(len(kinds)):
        ax.plot(angle, centers[i], linewidth=1.5, label=kinds[i])
        plt.fill(angle,centers[i] , 'r', alpha=0.05)

    ax.set_thetagrids(angle * 180 / np.pi, labels)
    plt.title(year + ' 指标对比')
    plt.legend()
    plt.show()
  • pd 为 pandas,np 为 numpy
  • 代码中的 data 是以年份为 key 的,值为对应年份结构为 DataFrame 的数据,方便按年获取相应数据
  • 极坐标实际上就是二维坐标系的变形,即将两个 y 轴合并一起,所以要在数据最末列添加上数据的第一列
  • 雷达图是用 matplotlib 极坐标图表绘制的
  • 其他代码基本是绘制雷达图固定的,只要计算出 labels、kinds、centers就好了

指标走势,采用折线图,对指定的学校,展示出某个指标的走势,之间利用 pandas 的 plot 工具来绘制就可以,需要特别处理的是,将年份作为列,学校作为索引,对应的值为某个指标的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def trendChart(schools, indicator='总分'):
    """
    给定学校(数组),得到这些学校5年的趋势图
    """
    ret = pd.DataFrame(index=[str(x)+"年" for x in range(2015,2020)],columns=schools)

    for year in data:
        df = data[year][indicator]
        for school in schools:
            if school in df.index:
                ret.loc[year, school] = df[school]
            else:
                ret.loc[year, school] = np.nan

    ret.plot.line(title="%s 走势" % indicator, linewidth=1.5, yticks=[])
    plt.show()

代码相对简单,不做赘述

注意:上面两个图表方法中,都对不存的学校做了处理,否则执行中可能会提示找不到对应的列或者索引

使用

代码分为三部分,dataCapture.py 数据抓取、dataProcess.py 数据整理 和 dataShow.py 可视化

每个部分都是独立的,他们之间通过数据的 Excel 文件关联,数据抓取产生原始数据文件,数据整理产生整理后的数据文件,可视化读取整理后的数据文件做展示

如果从头开始,依次执行代码文件,注意执行命令的当前路径需要在代码目录下

如果只想展示结果,待分析数据我已处理好,只需执行 dataShow.py 就行,更换不同的学校名称以及指标,就可以得到不同的结果:

1
2
3
4
5
# 显示不同大学指标对比雷达图
indicatorChart(['清华大学', '北京大学','上海交通大学', '浙江大学', '复旦大学'])

# 显示不同学校总体排名走势图
trendChart(['北京理工大学', '西北工业大学', '东华大学', '福州大学', '中国矿业大学'], '排名')

总结

今天我们从数据分析的角度对中国的大学做了简单对比,学习数据分析过程的同时,可以对要选择学校的朋友,通过不同角度的参考。当然分析过程比较简略,数据展示过于粗糙,如果你有兴趣,可以尝试将展示结果 Web 化,做成一个 Web 应用,以及可以增加更多的分析方式,提升技能的同时,还可以帮助更多的人。

特别感谢滴滴出行数据科学家 Rose 的大力支持和帮助

参考

示例代码:https://github.com/JustDoPython/python-examples/tree/master/taiyangxue/university

Python Geek Tech wechat
欢迎订阅 Python 技术,这里分享关于 Python 的一切。