一、概要
数学家 Andrew Beveridge 和Jie Shan在数学杂志上发表过一篇名叫《权力的网络》的论文,主要分析畅销小说《冰与火之歌》第三部《冰雨的风暴》中人物关系,其已经拍成电视剧《权力的游戏》系列。他们在论文中介绍了如何通过文本分析和实体提取构建人物关系的网络。紧接着,使用社交网络分析算法对人物关系网络分析找出最重要的角色;应用社区发现算法来找到人物聚类。
其中的分析和可视化是用Gephi做的。本文参考了一些大牛的帖子,主要使用neo4j进行分析及可视化。
Notes:如何从文本提取到人物角色的关系网路(图)数据?
首先需要编制一份详尽的字符及其昵称列表,用于对文本的检索。每次人物的名字(或昵称)在段落出现时,若在间距为15个单词的空间内出现另一个人物名字(或昵称),则认为他们之前有联系。两个人物名称出现的情况有很多种,但在各类情形下,共现就已经暗示着两者有某种联系。
更多细节见论文作者的网站(别问我是怎么知道的~)
二、工具准备
-
本地安装并配置好neo4j数据库附参考安装教
-
安装python包py2neo、igraph
三、具体步骤
1.导入基础数据
先启动neo4j ,有时候neo4j start 不管用,neo4j.bat console能行。
可以选择已经准备好的网络数据,导入节点关系数据。
数据格式如:
LOAD CSV WITH HEADERS FROM "https://www.macalester.edu/~abeverid/data/stormofswords.csv" AS row
MERGE (src:Character {name: row.Source})
MERGE (tgt:Character {name: row.Target})
MERGE (src)-[r:INTERACTS]->(tgt) ON CREATE SET r.weight = toInteger(row.weight)
数据库中的图由“Character”节点和“INTERACTS”关系构成,我们可以运行以下命令在neo4j中得到简单的可视化结果
MATCH p=(:Character)-[:INTERACTS]->(:Character)
RETURN p
但是这个图直观展示的信息较少,我们想让可视化满足以下要求:
- 节点的大小与其PageRank分数成比例,使得我们可以直观判断网络中的重要节点
- 节点的颜色由其社区属性(comunity)决定,我们一眼就能判断社区的分布以及节点的社区所在
- 节点之间联系厚度(thickness)与关系权重(weight)成比例
接下来我们主要使最终的可视化结果满足以上要求。
2.导入/计算 PR、community数据
导入网络上整理好的PageRank数据和社区搜寻结果数据
LOAD CSV WITH HEADERS FROM "https://raw.githubusercontent.com/johnymontana/neovis.js/master/examples/data/got-centralities.csv" AS row
MATCH (c:Character {name: row.name})
SET c.community = toInteger(row.community),
c.pagerank = toFloat(row.pagerank)
或者我们可以利用python的图分析库将PR和community直接计算出来。
使用python-igraph
从Neo4j构建一个igraph实例
为了在《权力的游戏》的数据的图分析中使用igraph,首先需要从Neo4j拉取数据,用Python建立igraph实例。作者使用 Neo4j 的Python驱动库py2neo。我们能直接传入Py2neo查询结果对象到igraph的TupleList构造器,创建igraph实例:
from py2neo import Graph
from igraph import Graph as IGraph
graph = Graph()
query = '''
MATCH (c1:Character)-[r:INTERACTS]->(c2:Character)
RETURN c1.name, c2.name, r.weight AS weight
'''
ig = IGraph.TupleList(graph.run(query), weights=True)
现在有了igraph对象,可以运行igraph实现的各种图算法。
PageRank
使用igraph运行的第一个算法是PageRank。PageRank算法源自Google的网页排名。它是一种特征向量中心性(eigenvector centrality)算法。
在igraph实例中运行PageRank算法,然后把结果写回Neo4j,在角色节点创建一个pagerank属性存储igraph计算的值:
pg = ig.pagerank()
pgvs = []
for p in zip(ig.vs, pg):
print(p)
pgvs.append({"name": p[0]["name"], "pg": p[1]})
pgvs
write_clusters_query = '''
UNWIND {nodes} AS n
MATCH (c:Character) WHERE c.name = n.name
SET c.pagerank = n.pg
'''
graph.run(write_clusters_query, nodes=pgvs)
随机游走算法( walktrap)
社区发现算法用来找出图中的社区聚类。作者使用igraph实现的随机游走算法( walktrap)来找到在社区中频繁有接触的角色社区,在社区之外角色不怎么接触。
在igraph中运行随机游走的社区发现算法,然后把社区发现的结果导入Neo4j,其中每个角色所属的社区用一个整数来表示:
clusters = IGraph.community_walktrap(ig, weights="weight").as_clustering()
nodes = [{"name": node["name"]} for node in ig.vs]
for node in nodes:
idx = ig.vs.find(name=node["name"]).index
node["community"] = clusters.membership[idx]
write_clusters_query = '''
UNWIND {nodes} AS n
MATCH (c:Character) WHERE c.name = n.name
SET c.community = toInt(n.community)
'''
graph.run(write_clusters_query, nodes=nodes)
至此两种数据方式讲解完毕,接下来是可视化部分
3.可视化
Neovis.js通过结合Neo4j的JavaScript驱动程序和vis.js可视化库,我们可以构建这种可视化。
<!doctype html>
<html>
<head>
<title>Neovis.js Simple Example</title>
<style type="text/css">
html, body {
font: 16pt arial;
}
#viz {
width: 900px;
height: 700px;
border: 1px solid lightgray;
font: 22pt arial;
}
</style>
<!-- FIXME: load from dist -->
<!-- <script src="https://cdn.neo4jlabs.com/neovis.js/v1.3.0/neovis.js"></script> -->
<script src="https://cdn.neo4jlabs.com/neovis.js/v1.2.0/neovis.js"></script></script>
<script
src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.2.1.min.js"
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
<script type="text/javascript">
// define config car
// instantiate nodevis object
// draw
var viz;
function draw() {
var config = {
container_id: "viz",
server_url: "bolt://localhost:7687",
server_user: "neo4j",
server_password: "sorts-swims-burglaries",
labels: {
// "Character": "name",
"Character": {
"caption": "name",
"size": "pagerank",
"community": "community",
"title_properties": [
"name",
"pagerank"
]
// "sizeCypher": "MATCH (n) WHERE id(n) = {id} MATCH (n)-[r]-() RETURN sum(r.weight) AS c"
}
},
relationships: {
"INTERACTS": {
"thickness": "weight",
"caption": false
}
},
initial_cypher: "MATCH (n:Character)-[r:INTERACTS]->(m) RETURN n,r,m"
};
viz = new NeoVis.default(config);
viz.render();
console.log(viz);
}
</script>
</head>
<body onload="draw()">
<div id="viz"></div>
Cypher query: <textarea rows="4" cols=50 id="cypher"></textarea><br>
<input type="submit" value="Submit" id="reload">
<input type="submit" value="Stabilize" id="stabilize">
</body>
<script>
$("#reload").click(function() {
var cypher = $("#cypher").val();
if (cypher.length > 3) {
viz.renderWithCypher(cypher);
} else {
console.log("reload");
viz.reload();
}
});
$("#stabilize").click(function() {
viz.stabilize();
})
</script>
</html>
neovs.js给出的一个可视化实例:参见此链接
本贴代码中,
- Neovis.default(config) – 根据config配置构建一个Neovis对象
- config – 配置对象,定义了如何与neo4j数据库连接(必需的)、用于装载可视化数据的初始Cypher查询(可选)、应该在其中呈现可视化的DOM元素(必需的)、如何样式化可视化的元素(标签和关系)(必需的)
官网示例给到的jquery.js和neovs.js貌似在网络不好的情况下容易加载不了,可以替换成国内的资源,或者也可以本地安装这些依赖。(菜狗路过~)
最终的效果图如下:
三、分析
- 统计人物数量
MATCH (c:Character) RETURN count(c)
- 统计每个角色接触的其它角色的数目
MATCH (c:Character)-[:INTERACTS]->()
WITH c, count(*) AS num
RETURN min(num) AS min, max(num) AS max, avg(num) AS avg_characters, stdev(num) AS stdev
- 网络的直径或者测底线或者最长最短路径:
MATCH (a:Character), (b:Character) WHERE id(a) > id(b)
MATCH p=shortestPath((a)-[:INTERACTS*]-(b))
RETURN length(p) AS len, extract(x IN nodes(p) | x.name) AS path
ORDER BY len DESC LIMIT 4
- 最短路径。作者使用Cypher 的shortestPath函数找到图中任意两个角色之间的最短路径。让我们找出凯特琳·史塔克(Catelyn Stark )和卓戈·卡奥(Kahl Drogo)之间的最短路径:
MATCH (catelyn:Character {name: "Catelyn"}), (drogo:Character {name: "Drogo"})
MATCH p=shortestPath((catelyn)-[INTERACTS*]-(drogo))
RETURN p
- 所有最短路径
联结凯特琳·史塔克(Catelyn Stark )和卓戈·卡奥(Kahl Drogo)之间的最短路径可能还有其它路径,我们可以使用Cypher的allShortestPaths函数来查找:
MATCH (catelyn:Character {name: "Catelyn"}), (drogo:Character {name: "Drogo"})
MATCH p=allShortestPaths((catelyn)-[INTERACTS*]-(drogo))
RETURN p
- 关键节点
在网络中,如果一个节点位于其它两个节点所有的最短路径上,即称为关键节点。
MATCH (a:Character), (b:Character)
MATCH p=allShortestPaths((a)-[:INTERACTS*]-(b)) WITH collect(p) AS paths, a, b
MATCH (c:Character) WHERE all(x IN paths WHERE c IN nodes(x)) AND NOT c IN [a,b]
RETURN a.name, b.name, c.name AS PivotalNode SKIP 490 LIMIT 10
- 度中心性(Degree Centrality)
度中心性是最简单度量,即为某个节点在网络中的联结数。在《权力的游戏》的图中,某个角色的度中心性是指该角色接触的其他角色数。
MATCH (c:Character)-[:INTERACTS]-()
RETURN c.name AS character, count(*) AS degree ORDER BY degree DESC
- 加权度中心性(Weighted Degree Centrality)
作者存储一对角色接触的次数作为INTERACTS关系的weight属性。对该角色的INTERACTS关系的所有weight相加得到加权度中心性。
MATCH (c:Character)-[r:INTERACTS]-()
RETURN c.name AS character, sum(r.weight) AS weightedDegree ORDER BY weightedDegree DESC
- 介数中心性(Betweenness Centrality)
介数中心性:在网络中,一个节点的介数中心性是指其它两个节点的所有最短路径都经过这个节点,则这些所有最短路径数即为此节点的介数中心性。介数中心性是一种重要的度量,因为它可以鉴别出网络中的“信息中间人”或者网络聚类后的联结点。
MATCH (c:Character)
WITH collect(c) AS characters
CALL apoc.algo.betweenness(['INTERACTS'], characters, 'BOTH') YIELD node, score
SET node.betweenness = score
RETURN node.name AS name, score ORDER BY score DESC
- 紧度中心性(Closeness centrality)
紧度中心性是指到网络中所有其他角色的平均距离的倒数。在图中,具有高紧度中心性的节点在聚类社区之间被高度联结,但在社区之外不一定是高度联结的。
MATCH (c:Character)
WITH collect(c) AS characters
CALL apoc.algo.closeness(['INTERACTS'], characters, 'BOTH') YIELD node, score
RETURN node.name AS name, score ORDER BY score DESC