原文链接:https://medium.com/neo4j/finding-the-best-tennis-players-of-all-time-using-weighted-pagerank-6950ed5fc98e
最新版本的Neo4j图形算法库对PageRank算法增加了权重变量的支持。
我的同事Ryan(https://twitter.com/ryguyrg/)最近发表的一篇论文《谁是有史以来最好的网球运动员?基于职业网球历史的复杂网络分析》(https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0017249),在这篇论文中,他使用了一种PageRank的变种算法,于是我就在想,我也是一名网球爱好者,我是不是也可以基本这种算法去做点什么呢?
我原计划要做一些数据的抓取,但是Kevin Lin已经做了一些比较困难的工作,他将到2017年底的所有比赛结果都以csv文件的形式放到了Github《atp-world-tour-tennis-data》(https://github.com/serve-and-volley/atp-world-tour-tennis-data)上.
感谢Kevin的付出。
数据导入
在导入数据之前,我们先创建一些约束,以避免导入一些重复的数据:
CREATE constraint on (p:Player)
ASSERT p.id is unique;
CREATE constraint on (m:Match)
ASSERT m.id is unique;
接下来我们将把数据导入到Neo4j中。先把Kevin创建的CSV文件拷贝到Neo4j的import目录下。
完成之后我们就可以使用Cypher的LOAD CSV命令将数据导入到Neo4j里了。
LOAD CSV FROM "file:///match_scores_1968-1990_UNINDEXED.csv" AS row
MERGE (winner:Player {id: row[8]})
ON CREATE SET winner.name = row[7]
MERGE (loser:Player {id: row[11]})
ON CREATE SET loser.name = row[10]
MERGE (m:Match {id: row[22]})
SET m.score = row[15], m.year = toInteger(split(row[0], "-")[0])
MERGE (m)-[w:WINNER]->(winner) SET w.seed = toInteger(row[13])
MERGE (m)-[l:LOSER]->(loser) SET l.seed = toInteger(row[14]);
LOAD CSV FROM "file:///match_scores_1991-2016_UNINDEXED.csv" AS row
MERGE (winner:Player {id: row[8]})
ON CREATE SET winner.name = row[7]
MERGE (loser:Player {id: row[11]})
ON CREATE SET loser.name = row[10]
MERGE (m:Match {id: row[22]})
SET m.score = row[15], m.year = toInteger(split(row[0], "-")[0])
MERGE (m)-[w:WINNER]->(winner) SET w.seed = toInteger(row[13])
MERGE (m)-[l:LOSER]->(loser) SET l.seed = toInteger(row[14]);
LOAD CSV FROM "file:///match_scores_2017_UNINDEXED.csv" AS row
MERGE (winner:Player {id: row[8]})
ON CREATE SET winner.name = row[7]
MERGE (loser:Player {id: row[11]})
ON CREATE SET loser.name = row[10]
MERGE (m:Match {id: row[22]})
SET m.score = row[15], m.year = toInteger(split(row[0], "-")[0])
MERGE (m)-[w:WINNER]->(winner) SET w.seed = toInteger(row[13])
MERGE (m)-[l:LOSER]->(loser) SET l.seed = toInteger(row[14]);
这个模型非常简单,可以运行下面的请求看到可视化的描述:
CALL db.schema()
可以看到:
看起来不错。在继续写之前,我们先写个简单查询来看一下数据情况:
MATCH p=()<-[:LOSER]-()-[r:WINNER]->()
RETURN p
LIMIT 25
获胜最多的选手
现在,我们想看一下获胜最多选手,这个语句要怎么写呢?
MATCH (p:Player)
WITH p,
size((p)<-[:WINNER]-()) AS wins,
size((p)<-[:LOSER]-()) as defeats
RETURN p.name, wins, defeats,
CASE WHEN wins+defeats = 0 THEN 0
ELSE (wins * 100.0) / (wins + defeats) END
AS percentageWins
ORDER BY wins DESC
LIMIT 10
运行上面的语句,将看到下面的输出:
如果你也是一名网球迷,你可能认识这份名单中的大部分名字。他们中的大部分都被认为是有史以来最优秀的球员,但是仅仅计算赢球的场数好像并不太严谨。
此时,似乎我们可以试试更高级的方法---PageRank算法.....
建立一个可信的投影图
通过一个结点的入口关系来决定这个结点的可信度,这就是PageRank算法的工作原理。例如,在网络的世界里,一个网页通过链接到另一个网页,而给他带来可信度。这个可信度可以通过这个关系的权重属性来决定。
在我们的网球世界里,运动员的可信度则由他们彼此之间比较的胜负数来决定。例如,下面的查询显示了费德勒和纳达尔相互赢了多少次。
MATCH (p1:Player {name: "Roger Federer"}),
(p2:Player {name: "Rafael Nadal"})
RETURN p1.name, p2.name,
size((p1)<-[:WINNER]-()-[:LOSER]->(p2)) AS p1Wins,
size((p1)<-[:LOSER]-()-[:WINNER]->(p2)) AS p2Wins
运行输出结果如下:
我们的投影图应该在费德勒和纳达尔之间建立直接关系,用权重表示他们互相之间比赛赢的次数。从费德勒到纳达尔的关系上权重是23,表示费德勒赢了纳达尔23次。而纳达尔到费德勒的有关系上权重就是15.
我们写下面的查询语句将投影出这个图:
MATCH (p1)<-[:WINNER]-(match)-[:LOSER]->(p2)
WHERE p1.name IN ["Roger Federer", "Rafael Nadal"]
AND p2.name IN ["Roger Federer", "Rafael Nadal"]
RETURN p2.name AS source, p1.name AS target, count(*) as weight
LIMIT 10
这个查询的输出结果如下所示:
接下来我们要做的是删除WHERE条件,使这个查询可以在全图中进行。
使用加权PageRank来发现最好的网球运动员
现在我们通过PageRank算法的weightProperty参数来调用加权PageRank算法。默认情况下,PageRank算法是非加权模式。
下面的语句即是在全图上运行加权PageRank算法:
CALL algo.pageRank.stream(
"MATCH (p:Player) RETURN id(p) AS id",
"MATCH (p1)<-[:WINNER]-(match)-[:LOSER]->(p2)
RETURN id(p2) AS source, id(p1) AS target, count(*) as weight
",
{graph:"cypher", weightProperty: "weight"})
YIELD nodeId, score
RETURN algo.getNodeById(nodeId).name AS player, score
ORDER BY score DESC
LIMIT 10
其运行结果如下:
我们看到,我们排名的头部与Filippo Radicchi论文的排名是不一样的,主要区别是费德勒,纳达尔和德约科维奇进入了前五。这是因为Radicchi的分析仅到2010年,而这三名球员在此后的8年时间里一直非常优秀,所以,这也就是我们排名不太一样的原因。
我们可以模板仅包括2010年之前的比赛,则如下查询语句:
CALL algo.pageRank.stream(
"MATCH (p:Player) RETURN id(p) AS id",
"MATCH (p1)<-[:WINNER]-(match)-[:LOSER]->(p2)
WHERE match.year <= $year
RETURN id(p2) AS source, id(p1) AS target, count(*) as weight
",
{graph:"cypher", weightProperty: "weight", params: {year: 2010}})
YIELD nodeId, score
RETURN algo.getNodeById(nodeId).name AS player, score
ORDER BY score DESC
LIMIT 10
运行效果如下:
注意,在这个查询中,我们将年份值通过params键作为参数传递到Cypher投影查询中。
我们排行榜的前两名现在与Radicche的一样了,但是费德勒目前在第三位并不是Radicche的榜单中是第七位,同时纳达尔和德约科维奇在我们榜单中已经排到前十之外了。
我们还可能查询某一个比赛的PageRank排名,下面的查询是2017年的PageRank排名
CALL algo.pageRank.stream(
"MATCH (p:Player) RETURN id(p) AS id",
"MATCH (p1)<-[:WINNER]-(match)-[:LOSER]->(p2)
WHERE match.year = $year
RETURN id(p2) AS source, id(p1) AS target, count(*) as weight
",
{graph:"cypher", weightProperty: "weight", params: {year: 2017}})
YIELD nodeId, score
RETURN algo.getNodeById(nodeId).name AS player, score
ORDER BY score DESC
LIMIT 10
运行效果如下:
下图是2017年ATP世界巡回赛的年终排名
这个排名与我们的排名完全不同!这是什么原因呢?这是因为官方排名为每场比赛都给予了不同的权重,而我们的PageRank排名在每场比赛上给予的是相同权重。
好,关于使用加权PageRank找史上最优化网球选手的问题就介绍到这,我期待着看到更多人使用加权PageRank去解决其他问题。如是你也用了,请告诉我[email protected]
Enjoy!
译者言:作者仅从应用角度介绍了如果实现这个事,完成没有介绍algo.pageRank.stream这个方法各个参数的功能,以后有空找到相关文章再给大家介绍。