这是voidAlex原创的第一篇博文。
源码在我的GitHub
PageRank算法,又叫佩奇排名。是由Google公司创始人拉里佩奇(Larry Page)发明的一种由搜索引擎根据网页之间相互的超链接计算的技术。
对于某个互联网网页A来说,该网页PageRank的计算基于以下两个基本假设:
- 数量假设:在Web图模型中,如果一个页面节点接收到的其他网页指向的入链数量越多,那么这个页面越重要;
- 质量假设:指向页面A的入链质量不同,质量高的页面会通过链接向其他页面传递更多的权重。所以越是质量高的页面指向页面A,则页面A越重要。
根据上面的两个假设,PageRank的计算步骤如下:
网页通过链接关系构建起Web图,每个页面设置相同的PageRank值,通过若干轮的计算,会得到每个页面所获得的最终PageRank值。随着每一轮的计算进行,网页当前的PageRank值会不断得到更新。
在一轮中更新页面PageRank得分的计算方法:在一轮更新页面PageRank得分的计算中,每个页面将其当前的PageRank值平均分配到本页面包含的出链上,这样每个链接即获得了相应的权值。而每个页面将所有指向本页面的入链所传入的权值求和,即可得到新的PageRank得分。当每个页面都获得了更新后的PageRank值,就完成了一轮PageRank计算。
将PageRank算法应用到球队中后,球队的PageRank分数的计算依然基于两个假设:
- 数量假设:比赛中A在其他球队身上取得的分数(战胜或战平)越多,那么这个球队实力越强;
- 质量假设:取得积分的对手实力强弱不同,实力强的球队会提供更多的权重。所以A取得战胜或战平的球队实力越强,则球队A越强。
利用以上两个假设,PageRank算法刚开始赋予每个球队相同的重要性得分(PR值),通过迭代递归计算来更新每个球队节点的PageRank得分,直到得分稳定为止。
假设一个由4支球队组成的足球联赛:A,B,C和D。给定所有球队一个相同的初始PageRank值PR。在第一轮计算中,对于球队A,假设它在对阵B、C、D球队取得的积分分别为P1、P2、P3,那么它的PageRank值将被更新为:
$$ PR(A) = (PR(B)∙P1+PR(C)∙P2+PR(D)∙P3)/3 $$
同样的,B、C、D队的PageRank值也将通过此方法更新。在进行若干次迭代后,所有球队的PageRank值将会趋向于稳定,也就是收敛状态。这时所有球队的PageRank值就是它们的最终得分。
意义
灵感来源于虎扑的一个帖子,里面提到了球队的硬仗能力,或者球队上限。原贴的作者计算了上赛季的英超的PageRank分,自己看了之后比较感兴趣,就实现了一遍。原贴作者提到:
一支球队的联赛排名是其硬仗水平与虐菜能力的共同作用,然而由联赛排名决定的欧战资格,尤其是淘汰赛阶段,更看中的却是球队的硬仗水平,即话题区里所谓的球队上限。于是问题来了,是否存在有些球队主要靠虐菜能力进入欧战区,然后面对他国豪强一泻千里,给本国联赛拖了后腿的情况?我试着用PageRank算法来验证这一猜想。
一支球队的PageRank评分都是从其他球队手中抢来的,要知道一支球队的PageRank评分就必须先知道其他球队的评分,这是鸡生蛋蛋生鸡的难题。PageRank算法的做法是给每个队一个初始分数,比如1,然后通过交战记录互相贡献分数,这样每支队的分数都会有变化;再拿这些分数重复一遍计算,每支队的分数又会变化;重复N次后,变化会趋于0(有数学证明),这时的分数就是最终结果。
一支球队的所谓硬仗能力或球队上限是比较抽(xuan)象(xue)的东西,PageRank算法可以比较好的量化它。
本文中计算了本赛季(2016-2017赛季)英超、西甲、NBA各个球队的PageRank值。数据来源于虎扑。
核心代码
核心代码有两部分,PageRank算法和爬虫。
用PageRank算法计算球队PageRank值比较简单。就是通过大量的交手记录来更新PageRank值。计算方法在第二部分已经说明。
用爬虫爬取的球队信息和比赛记录存放在文件中。球队信息以JSON的形式存放,比赛结果以文本的形式存放,一行表示一条比赛记录。类似于:
莱加内斯 2-4 皇马
这样的格式。
首先构造TeamItem类来存放球队信息,该类有两个字段:name和pagerank。
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 31 32
| package com.pagerank.core; * 球队类 * Created by 王麟东 on 2017/7/8 0008. */ public class TeamItem { private String name; private double pagerank; public TeamItem(){ this.pagerank = 1; } public String getName() { return name; } public double getPagerank() { return pagerank; } public void setName(String name) { this.name = name; } public void setPagerank(double pagerank) { this.pagerank = pagerank; } }
|
然后构造MatchResult类来存放比赛结果。在MatchResult类中,有获取比赛结果的方法getWeight。根据比赛结果,返回对应的权重。
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| package com.pagerank.core; * 比赛结果类 * Created by 王麟东 on 2017/7/8 0008. */ public class MatchResult { private String teamA; private String teamB; private int scoreA; private int scoreB; public MatchResult(String line) { String temp[] = line.split("-"); this.teamA = temp[0].split(" ")[0]; this.scoreA = Integer.parseInt(temp[0].split(" ")[1]); this.teamB = temp[1].split(" ")[1]; this.scoreB = Integer.parseInt(temp[1].split(" ")[0]); } * 获得权重(比赛结果) * @param team 球队名 * @return 该球队在本条比赛记录中的比赛结果,胜3平1负0,若本条比赛记录中没有这个球队,返回-1 */ public int getWeight(String team){ int weight = -1; if (team.equals(this.teamA)){ if (this.scoreA == this.scoreB){ weight = 1; }else if (this.scoreA > this.scoreB){ weight = 3; }else if (this.scoreA < this.scoreB){ weight = 0; } }else if (team.equals(this.teamB)){ if (this.scoreB == this.scoreA){ weight = 1; }else if (this.scoreB > this.scoreA){ weight = 3; }else if (this.scoreB < this.scoreA){ weight = 0; } } return weight; } * 在获得比赛权重不为-1的情况下,获得对手球队名 * @param team 球队名 * @return 本条比赛记录中的对手球队名 */ public String getOtherTeam(String team){ String otherTeam = this.teamA; if (team.equals(this.teamA)){ otherTeam = this.teamB; } return otherTeam; } }
|
然后是PageRank类。在PageRank类中,构造方法首先将球队信息和比赛结果读取到内存中,并且给每个球队赋初始PageRank值1。
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| package com.pagerank.core; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import java.io.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; * Created by 王麟东 on 2017/7/8 0008. */ public class PageRank { private Map<String, TeamItem> teamMap; private List<MatchResult> matchResultList; private String teamInfopath; private String matchResultPath; private int max; public PageRank(String teamInfopath, String matchResultPath, int max){ this.teamInfopath = teamInfopath; this.matchResultPath = matchResultPath; this.max = max; init(); } * 初始化,将球队信息和比赛结果读取到内存 */ private void init(){ try { BufferedReader teamReader = new BufferedReader(new FileReader(new File(this.teamInfopath))); Gson gson = new Gson(); JsonArray jsonArray = gson.fromJson(teamReader.readLine(), JsonArray.class); teamReader.close(); this.teamMap = new HashMap<String, TeamItem>(); for (JsonElement jsonElement : jsonArray){ TeamItem teamItem = new TeamItem(); teamItem.setName(jsonElement.getAsJsonObject().get("team").getAsString()); this.teamMap.put(teamItem.getName(), teamItem); } BufferedReader matchReader = new BufferedReader(new FileReader(new File(this.matchResultPath))); String line = ""; this.matchResultList = new ArrayList<MatchResult>(); while ((line = matchReader.readLine()) != null){ MatchResult matchResult = new MatchResult(line); this.matchResultList.add(matchResult); } matchReader.close(); }catch (IOException e){ System.out.println("读取失败"); System.exit(1); } } public Map<String, TeamItem> getTeamMap() { return teamMap; } public int getMax() { return max; } }
|
初始化之后,开始递归的计算每个球队的PageRank值:
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 31 32 33
| * PageRank算法迭代器 * @param teamMaps 球队信息 * @param max 迭代次数 */ public void Iteration(Map<String, TeamItem> teamMaps, int max){ Map<String, TeamItem> tmp = new HashMap<String, TeamItem>(); for (TeamItem teamItem : teamMaps.values()){ TeamItem tmpTeam = new TeamItem(); tmpTeam.setName(teamItem.getName()); double pagerank = 0; int count = 0; for (MatchResult matchResult : this.matchResultList){ int weight = matchResult.getWeight(teamItem.getName()); if (weight != -1){ double pr = teamMaps.get(matchResult.getOtherTeam(teamItem.getName())).getPagerank(); pagerank += (weight * pr); count ++; } } tmpTeam.setPagerank(pagerank / count); tmp.put(tmpTeam.getName(), tmpTeam); } max --; this.teamMap = tmp; if (max > 0){ Iteration(this.teamMap, max); } }
|
使用爬取的数据测试:
1 2 3 4 5 6 7 8
| String team = "nba_team_list.json"; String match = "nba_result_list"; int max = 20; PageRank pageRank = new PageRank(team, match, max); pageRank.Iteration(pageRank.getTeamMap(), pageRank.getMax()); pageRank.wirteToFile(); pageRank.print();
|
由于数据量不大,迭代10-20次已经能够达到收敛状态。
爬虫
数据全部来源于虎扑,但是虎扑已经更新到了新赛季(2017-2018赛季)的数据,原来的爬虫失效,所以就不贴出来了。爬取的数据放在我的github。
结果
废话不多说,直接上结果。表格中的变化值为相比较原排名的变化程度。
NBA
数据为2016-2017赛季常规赛的全部数据。
东部联盟
球队 |
PageRank |
排名 |
变化值 |
凯尔特人 |
1.9366 |
1 |
0 |
猛龙 |
1.8867 |
2 |
+1 |
骑士 |
1.8766 |
3 |
-1 |
奇才 |
1.7852 |
4 |
0 |
老鹰 |
1.6612 |
5 |
0 |
公牛 |
1.5974 |
6 |
+2 |
热火 |
1.5771 |
7 |
+2 |
雄鹿 |
1.5465 |
8 |
-2 |
步行者 |
1.5442 |
9 |
-2 |
活塞 |
1.3974 |
10 |
0 |
黄蜂 |
1.2995 |
11 |
0 |
尼克斯 |
1.1173 |
12 |
0 |
魔术 |
1.0865 |
13 |
0 |
76人 |
1.0001 |
14 |
0 |
篮网 |
0.6985 |
15 |
0 |
西部联盟
球队 |
PageRank |
排名 |
变化值 |
勇士 |
2.5695 |
1 |
0 |
马刺 |
2.3791 |
2 |
0 |
火箭 |
2.0306 |
3 |
0 |
快船 |
1.9787 |
4 |
0 |
爵士 |
1.8741 |
5 |
0 |
雷霆 |
1.7872 |
6 |
0 |
灰熊 |
1.7141 |
7 |
0 |
开拓者 |
1.5253 |
8 |
0 |
掘金 |
1.4822 |
9 |
0 |
小牛 |
1.2437 |
10 |
+1 |
鹈鹕 |
1.2392 |
11 |
-1 |
国王 |
1.1998 |
12 |
0 |
森林狼 |
1.1934 |
13 |
0 |
湖人 |
1.0070 |
14 |
0 |
太阳 |
0.9156 |
15 |
0 |
西甲
数据为2016-2017赛季西甲联赛的全部数据。
球队 |
PageRank |
排名 |
变化值 |
巴萨 |
2.3323 |
1 |
+1 |
皇马 |
2.2955 |
2 |
-1 |
马竞 |
1.8039 |
3 |
0 |
塞维利亚 |
1.7653 |
4 |
0 |
比利亚雷亚尔 |
1.6181 |
5 |
0 |
毕尔巴鄂 |
1.4819 |
6 |
+1 |
阿拉维斯 |
1.4205 |
7 |
+2 |
皇家社会 |
1.3817 |
8 |
-1 |
埃瓦尔 |
1.2816 |
9 |
+1 |
西班牙人 |
1.2111 |
10 |
-2 |
马拉加 |
1.1516 |
11 |
0 |
瓦伦西亚 |
1.1018 |
12 |
0 |
塞尔塔 |
0.9893 |
13 |
0 |
拉斯帕尔马斯 |
0.9532 |
14 |
0 |
拉科鲁尼亚 |
0.8892 |
15 |
+1 |
皇家贝蒂斯 |
0.8815 |
16 |
-1 |
莱加内斯 |
0.7624 |
17 |
0 |
希洪竞技 |
0.6651 |
18 |
0 |
格拉纳达 |
0.5086 |
19 |
+1 |
奥萨苏纳 |
0.4939 |
20 |
-1 |
英超
数据为2016-2017赛季英超联赛的全部数据。
球队 |
PageRank |
排名 |
变化值 |
切尔西 |
5.3667 |
1 |
0 |
热刺 |
4.9594 |
2 |
0 |
利物浦 |
4.7785 |
3 |
+1 |
曼城 |
4.2577 |
4 |
-1 |
阿森纳 |
4.1272 |
5 |
0 |
曼联 |
3.9255 |
6 |
0 |
埃弗顿 |
3.3458 |
7 |
0 |
伯恩茅斯 |
2.6264 |
8 |
+1 |
莱斯特 |
2.5141 |
9 |
+3 |
南安普顿 |
2.4357 |
10 |
-2 |
西汉姆联 |
2.4165 |
11 |
0 |
水晶宫 |
2.4097 |
12 |
+2 |
西布朗 |
2.3787 |
13 |
+3 |
斯旺西 |
2.3119 |
14 |
+1 |
伯恩利 |
2.2320 |
15 |
+1 |
沃特福德 |
2.2083 |
16 |
+1 |
斯托克城 |
2.1396 |
17 |
-4 |
胡尔城 |
1.9424 |
18 |
0 |
米德尔斯堡 |
1.4478 |
19 |
0 |
桑德兰 |
1.3255 |
20 |
0 |
至于结果能看出来什么信息,大家就见仁见智了。