ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python合并四大世界大学排名数据:pandas清洗与长表构建

Python合并四大世界大学排名数据:pandas清洗与长表构建

简介:压缩包内含US News、软科、QS、THE四大权威机构2022年世界大学排名数据,以及对应的Python处理脚本,方便高校师生、留学申请者或数据分析初学者对比院校表现、了解排名指标差异。压缩包总大小13.21MB,共15个文件,包括8个py脚本、4个zip数据包和3个txt说明文件;py脚本主要调用requests、BeautifulSoup、pandas等库实现数据抓取与整理,txt文件辅助记录链接与名称信息。目前已有1185人在CSDN学习/浏览了该资源。通过这套脚本,读者既能获取四个排名机构在2021年11月10日更新的最新结果,也可以按需调整抓取逻辑进行二次分析,比如对比不同排名体系的评分维度、输出大学名次变化,或用于爬虫与数据处理练习,是一份兼顾实用与教学的数据分析素材。

1. 数据到手不是终点:先看懂四套榜单再谈合并

做 python 大学排名相关分析或者选校数据采集时,最大的坑往往不是爬虫反爬,而是 US News、软科、QS、THE 四套榜单的口径完全对不上。同一个高校,在软科体系里可能进前一百,在 QS 的评价逻辑里却被明显往后压;这不是数据错了,而是它们各自回答"好大学"的标准不同。这套 2022 年世界大学排名数据配合 Python 脚本,帮你把通常要花一周的数据采集和清洗压缩成半天:从四份来源拉平字段、统一学校名、合并成一张可直接分析的表。适合做科研评估、高校对比、行业报告或想用真实数据练手的数据方向开发者。下面从榜单口径讲起,再拆脚本每一段该做什么。

2. 四榜口径差异与字段盘点:合并前先回答三个问题

在打开脚本之前,先回答三个问题:四份榜单分别排的是什么?原始数据里哪些字段必须保留?统一主键用中文名还是英文名?这三个问题想不清楚,后面所有代码都在白忙。本章先把口径差异讲透,再用一段 pandas 代码把原始数据的结构摸清。

2.1 四大榜单的排名逻辑:都在排名,但"好"不一样

US News 的指标体系里,学术声誉、文献引用和国际合作占的权重很高,所以它覆盖的学校数量通常最大,更像"学术影响力大盘"。软科 ARWU 更极端一些,它把诺奖、高被引科学家、Nature/Science 论文这类硬科研成果放在最重要位置,导致理工科强校在软科体系里往往位次偏高,人文社科强校相对吃亏。QS 是四套里最看重声誉的,尤其是学术声誉和雇主声誉,这两项直接和问卷调查绑定,排名会有一定"主观印象分"。THE 则把教学和研究权重拆得比较平均,还专门计算论文引用和国际化程度,对一所学校的综合稳定性要求更高。

四套榜单的核心差异,我整理成了一张表,建议合并前先对着它确认自己手里的数据是不是也有这些特征:

榜单核心衡量点典型数据特征合并时最容易踩的坑
US News学术声誉、文献引用、国际合作总分是连续数值,覆盖量大合并后往往成为主表,其他表都要向它的学校集合看齐
软科 ARWU诺奖、高被引科学家、Nature/Science 论文总分浮点数,理工科位次偏高用它做趋势分析容易被"重理轻文"误导
QS学术声誉、雇主声誉、师生比单项维度得分齐全学校集合与其他榜重合度较低
THE教学、研究、论文引用、国际化部分学科以百分位段呈现缺失值多,排名字段可能是文本

这张表想说明的不是哪套更权威,而是字段设计完全不同。例如 US News 的总分是连续的数值,可以直接参与排序;THE 的部分学科排名以百分位段给出,直接做数值运算会失真;QS 的单项维度得分很全,但和另外三套的字段没有同名关系。所以你在合并时不能指望"得分"列自动对齐,真正能对齐的只有学校名和排名位次。

2.2 原始字段清单:抓住学校名、排名、国家三列就够了

从资源包里解压出来的通常是 CSV 或 Excel,先不用看全部列,真正参与合并的核心字段只有三个:学校名、排名、国家/地区。其余列,比如得分、引用量、师生比,都属于"分析阶段的扩展字段",在合并阶段反而容易造成列冲突。常见字段清单大致如下:

字段类别常见列名类型合并阶段角色
学校标识学校名称 / institution / University字符串主键,必须清洗
排名排名 / Rank / World Rank整数或混合文本核心指标,需转数值
得分总分 / Score / Overall浮点或区间备选,不在合并时强求
地域国家 / 国家/地区 / Country字符串分组维度

我见过有人一上来就把所有列都塞进合并逻辑,结果两张表因为多了一个"引用得分"列,列名对不上就报错,最后还得回到三列模型重来。合并阶段保持精简,把扩展列留在各自原始表里,等长表建好后再按需关联回去。

2.3 动手第一步:用 pandas 把四份原始数据读进来做探针

拿到资源包后,我一般不做任何处理,先写一段探针代码把四份数据读进来,看一眼行数和列名再动手。这段代码不是正式清洗,而是为了让你知道四份表的结构差异到底有多大。

import pandas as pd # 资源包解压后建议保持 data/ 目录结构,路径按实际情况改 file_map = { "usnews": "data/usnews_2022.csv", "arwu": "data/arwu_2022.csv", "qs": "data/qs_2022.csv", "the": "data/the_2022.csv", } frames = {} for key, path in file_map.items(): frames[key] = pd.read_csv(path) print(f"[{key}] 行x列: {frames[key].shape}") print(f"[{key}] 列名: {frames[key].columns.tolist()}\n")

这段代码的逻辑很简单:把四份数据分别读进内存,存到一个字典里,然后逐个打印形状和列名。file_map 的作用是给每份数据一个短名字,后面所有引用都以 usnews、arwu、qs、the 四个 key 为准。路径建议统一放到 data/ 目录下,如果资源包解压后的目录结构不同,改 file_map 里的路径字符串即可,其他代码不用动。

读完后重点看两件事。第一是行数,四个表行数往往差异很大,说明覆盖范围不同;第二是列名,同一个字段在四个表里可能叫法完全不同,比如"国家"这一列,US News 可能叫 Country,软科叫"国家",QS 叫 Country/Region,THE 叫 Location。这些差异就是后面列名映射要处理的。如果资源包里不是 CSV 而是 Excel,把 read_csv 换成 pd.read_excel(path) 就能跑通,但要注意 Excel 多 sheet 时还要带上 sheet_name 参数。

3. 用 Python 把四榜合并成一张长表:从列映射到透视验证

四份原始数据读进来之后,接下来的目标不是立刻透视,而是先把它们合并成一张规范的长表。这一章是资源包脚本的核心,也是你自己复现时最容易改出问题的地方。

3.1 合并策略的选型:为什么是长表而不是一次 merge

四份数据不能直接行拼接,因为列不对齐;也不能一开始就 pd.merge 成宽表,因为学校集合不同,merge 完会丢掉大量只在某个榜单里出现的学校。我一般用"长表"作为唯一中间结果,结构是:school、source、rank_clean、country 四列,每一行代表"某学校在某个榜单里的某个排名"。

选长表而不用宽表的理由有两层。从数据完整性看,长表保留了一所学校的多次记录,哪个榜单缺了就是缺了,不会因为 join 方向选错而静默丢数据。从使用灵活度看,长表想透视随时可以 pivot 成宽表,想筛选某个榜单直接按 source 过滤,而宽表想还原长表反而要 melt,多绕一步。我后来处理所有多来源榜单数据都沿用这个策略,它能省掉很多返工。

3.2 列名映射:把四份数据里的"学校名"统一到一个字段

第 2 章探针输出告诉我们,四份表的列名各不相同。所以第一步是建一个重命名映射,把"学校名称""Institution""University"这类列统一改成 school,把"国家""Country""Location"统一改成 country。映射表的结构如下:

# 每份数据里学校所在列名可能不同,先做列名映射 rename_map = { "usnews": {"学校名称": "school", "Country": "country"}, "arwu": {"大学名称": "school", "国家": "country"}, "qs": {"Institution": "school", "Country/Region": "country"}, "the": {"University": "school", "Location": "country"}, } for key, cols in rename_map.items(): frames[key] = frames[key].rename(columns=cols)

注意,rename_map 里的键值不是固定的,要以你实际 print 出来的列名为准,这里只是示范结构。rename 本身不会报错——哪怕映射的列不存在,pandas 也只是忽略它——但如果你手误写错列名,后面 frames[key]["school"] 会抛 KeyError,所以这段跑完最好再打印一次列名确认。统一成 school 和 country 后,四份数据就可以走同一套清洗函数了。

3.3 学校名归一化与排名清洗

列名统一之后,真正花时间的是学校名清洗。四份榜单来源不同,同一所学校可能出现 "The University of Alpha" 和 "Alpha University" 两种写法;中文榜单里可能写"某高校",英文榜单里写 "Alpha University";还有的后面跟着校区或城市信息,比如 "Alpha University (Campus A)"。这些问题在第一章的代码里集中解决:

import re def normalize_school(name) -> str: """学校名归一化:去首尾空格、转小写、移除括号内容""" if pd.isna(name): return "" text = str(name).strip().lower() # 去掉括号及其中的内容,例如 "alpha university (beijing)" -> "alpha university" text = re.sub(r"\(.*?\)", "", text).strip() return text def clean_rank(value) -> int: """排名清洗:处理并列标记和区间写法,返回最小位次""" if pd.isna(value): return None text = str(value).strip() # 去掉 "=" 后缀,取区间 "50-52" 的起始值 text = text.replace("=", "").split()[0] if text else "" try: return int(float(text)) except ValueError: return None for key in frames: frames[key]["school"] = frames[key]["school"].map(normalize_school) if "排名" in frames[key].columns: frames[key]["rank_clean"] = frames[key]["排名"].map(clean_rank)

normalize_school 做两件事:把大小写和首尾空格统一,再把括号里的注释去掉。我特意没有直接删掉 "university" 后缀,因为那会把学校专名信息一起丢掉,比如 "alpha university" 和 "alpha" 在归一化后都变成 "alpha",合并时反而制造假匹配。真正差异大的名称,靠后面一个手工映射表兜底。clean_rank 处理的是排名列里的特殊写法:QS 有时把并列排名写成 "50=",US News 偶尔写成 "50-52" 或 "50–52",这个函数把 "=" 去掉、取区间起始值、再转 int。转换失败返回 None,不会让整行崩溃。

参数说明:re.sub 的第一个参数是匹配括号内容的正则,第二个参数是空字符串,表示直接删掉匹配部分;split()[0] 的用途是截取去 "=" 后的第一个连续字符段,如果排名文本里还有空格,只保留第一个词。把排名区间取起始值的处理方式,意味着后续分析默认按"最好情况排名"计算,这一点我通常会在分析时另留一列原始排名文本,避免丢掉区间信息。

3.4 核心合并:堆叠成一张长表并透视验证

四份数据都处理出 school、rank_clean、country 三个核心列之后,合并本身反而最简单:

all_ranks = [] for key in frames: tmp = frames[key][["school", "rank_clean", "country"]].copy() tmp["source"] = key # 标记来源 all_ranks.append(tmp) long_table = pd.concat(all_ranks, ignore_index=True) long_table = long_table.dropna(subset=["school"]) print(long_table.shape) print(long_table.head())

这段代码的逻辑:从每个 frame 里只抽三列,加一个 source 列记录榜单来源,最后用 concat 纵向拼起来。ignore_index=True 表示重新生成 0 到 N-1 的行号,如果不加,合并后的行号会保留各分表原来的索引,虽然不影响后续分析,但调试时看起来很乱。dropna 的 subset 参数只检查 school 一列,把没有学校名的噪声行删掉;如果写 dropna() 不加参数,任何一个字段为空都会被删,容易把只有国家缺失的合法行误删。

合并后的长表大概长这样:

schoolrank_cleancountrysource
alpha university15甲区qs
alpha university28甲区usnews
beta university90乙区the

把长表透视成宽表,是验证合并结果最快的方式:

wide_table = long_table.pivot_table( index="school", columns="source", values="rank_clean" ) wide_table = wide_table[["usnews", "arwu", "qs", "the"]] # 检查每个来源有多少非空排名 print(wide_table.notna().sum()) # 至少出现在两个榜单里的学校数量 multi_cover = wide_table.dropna(thresh=2) print(multi_cover.shape[0])

pivot_table 会以 school 为索引,把 source 的值展开成列,值为 rank_clean。这里的坑是:如果一所学校在同一个榜单里出现两次(比如同时上了主榜和分榜),pivot_table 默认聚合方式会报错,所以在透视之前最好先检查 long_table 是否有重复行,有就去重。dropna(thresh=2) 表示只保留至少有两个非空排名列的学校,这一行能快速告诉你四榜交集大概多大;如果交集数太小,说明学校名归一化还不够,要回去补映射表,而不是继续往下分析。

3.5 保存输出

long_table.to_csv("output/world_rank_2022_long.csv", index=False) wide_table.to_csv("output/world_rank_2022_wide.csv") print("长表行数:", long_table.shape[0]) print("宽表学校数:", wide_table.shape[0])

输出时把长表和宽表都存一份,是给后面不同分析场景留的后路:画散点用宽表方便,做筛选和统计用长表灵活。index=False 是为了避免把行号写进 CSV,否则下次读回来会多一列"Unnamed: 0",虽然不致命但容易让人困惑。

4. 排名数据合并的常见问题排查:五处翻车现场

这一章把我实际处理排名数据时踩过的坑按"现象 → 原因 → 解决"整理出来,你复现时大概率会撞上其中几个。

4.1 学校名直接 merge 导致大面积 NA

现象:把四份表按学校名做 inner merge,结果明明同一所学校,却出现两条记录,一个叫"某高校",一个叫"Some University",merge 之后行数比预期少了一大半。

原因:四份数据的来源不同,有的用中文全称,有的用英文官方名,还有的带上了城市后缀或校区名。直接拿原始字符串当 join key,等于默认所有榜单对同一所学校的写法完全一致,这个假设基本不成立。

解决:先用 normalize_school 做统一化,再针对差异大的学校建一个手工映射表。映射表的策略是只映射差异最明显的几十条,比如把 "The University of Alpha" 这类带冠词的写法统一成 "alpha university",不用追求全部覆盖。通常处理完几十条特殊名称后,交集数量就能从惨不忍睹变成可接受。

4.2 排名是"50="或"50-52",int 转换直接崩

现象:某榜单的排名列读完是 object 类型,里面有的值是 "50=",有的是 "50-52",直接 astype(int) 抛 ValueError。

原因:爬虫抓下来或手动录入时,并列排名被写成了带等号或区间的文本。pandas 读 CSV 时无法自动识别这种混合格式,于是整列都变成字符串。

解决:用 clean_rank 函数先清洗再转换,处理 "=" 和 "-" 之后取区间起始值。如果既要保留原始写法,又想做数值运算,就在清洗时单独留一列 rank_raw 存原始字符串,分析的时候按需取用。

4.3 四套榜单的"2022"根本不是同一时间口径

现象:把四份 2022 年数据放在一起做横向比较,发现某所学校在两个榜单里的位次差了快一百名,于是怀疑数据有错。

原因:四套榜单虽然都标着 2022 版,但发布时间和统计周期都不一样。有的在前一年年底就发布了,有的到次年中旬才更新;权重和样本调整也会让同一所学校在不同时间点的结果产生明显差异。这不是数据错误,是由于榜单自带的时滞效应。

解决:在分析时把"榜单版本年份"和"发布时间"分开记录,不要默认它们完全对齐。如果论文或报告里要引用排名,必须标注是哪套榜单的哪个版本,不能只写"2022 年排名"。

4.4 CSV 编码问题:read_csv 打不开

现象:在 Windows 下直接 pd.read_csv("qs_2022.csv") 抛 UnicodeDecodeError,但用记事本打开文件却能看到中文正常显示。

原因:资源包里的 CSV 可能由不同系统导出,有的是 UTF-8,有的是 GBK,还有的带 BOM 头。pandas 默认按 UTF-8 解析,遇到 GBK 编码文件就会直接报错。

解决:读文件时统一加 encoding="utf-8-sig",它能兼容带 BOM 的 UTF-8 文件;如果还报错,再尝试 encoding="gbk" 或 encoding="latin1"。最稳妥的写法是写一个小循环逐个尝试编码,把成功打开的结果存下来,不要手动改文件编码——手动另存为可能会引入新问题。

4.5 榜单覆盖学校差异大,没上榜不等于差

现象:A 校在 US News 榜单里没出现,某同事直接下结论说这所学校排不进世界前几名。

原因:US News、软科、QS、THE 四套榜单的覆盖范围差异很大。有的榜单覆盖量大,有的榜单本身就有入围筛选规则,没上榜的原因可能只是数据来源缺失或学校没参评,并不代表真实水平。

解决:合并时用 left join 或 outer join 保留所有学校的记录,不要用 inner join 把缺席的学校静默丢掉。哪怕某所学校只在四榜中出现一次,也要保留那一行。最终分析时显式标记哪些榜单缺了这个学校,"缺席原因"本身也可以作为分析维度。

5. 进阶验证:用排名差散点图找出榜单分歧最大的学校

长表和宽表都生成之后,最有价值的产品不是排名本身,而是"四榜之间的位次差"。同一所学校的 QS 排名减去 US News 排名,如果差值很大,说明它在这两套评价体系里受到的待遇截然不同。这个差值就是做研究选题、找案例分析对象的入口。

先基于宽表计算两个维度:

import numpy as np # 计算 QS 与 US News 的位次差 wide_table["qs_usnews_diff"] = wide_table["qs"] - wide_table["usnews"] # 计算四榜平均位次,作为综合基准 wide_table["rank_mean"] = wide_table[["usnews", "arwu", "qs", "the"]].mean(axis=1)

qs_usnews_diff 为正,说明这所学校在 QS 里相对靠后,在 US News 里更受认可;为负则相反。rank_mean 给出一所学校的四榜平均位置,用来消除单一榜单的极端值。注意 wide_table 里有些学校只在部分榜单出现,mean 会默认跳过 NaN,但学校样本量不足四榜时,rank_mean 的参考价值有限,必要时再加一列统计有效榜单数量。

画散点图的方式也很直接:

import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(10, 6)) ax.scatter(wide_table["rank_mean"], wide_table["qs_usnews_diff"], alpha=0.6) ax.axhline(0, color="gray", linestyle="--") ax.set_xlabel("四榜平均排名(越小越靠前)") ax.set_ylabel("QS排名 - US News排名(负值代表QS更靠前)") plt.show()

散点里靠近横轴零线的学校,说明两套榜单的看法比较接近;离零线越远的点,就是评价体系分歧越大的学校。找出这些离群点后,回到长表看它们在其他两个榜单里的位次,能进一步判断它是被某种评价维度的偏好推上去的,还是恰好踩在了某个统计口径的边缘。我通常还会把散点图按国家分组着色,用来快速定位某个地区整体被哪套体系更认可。

最后提一个我养成的习惯:处理此类排名数据时,一定先把长表跑通,再做任何可视化。第一次做这类分析时我图省事,直接拿四张原始表 merge 成宽表,结果学校名没统一,画出来的图看起来像随机噪声,排查了半天才发现是 join key 的问题。从那以后我每次做排名数据分析都强制先走一遍长表流程:列名映射、名称归一化、排名清洗、堆叠、透视验证,全部跑完再看数。这套流程独立于具体数据源,换一年、换一个榜单组合都通用。希望这份资源能直接帮你跳过我最开始反复返工那几天,少走几步弯路。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进