Python 分析权力游戏图表

发布于 2025-01-28 01:42:13 字数 24227 浏览 4 评论 0

几个月前,数学家 Andrew Beveridge 和 Jie Shan 在 Math Horizon 杂志上发表了 权利的游戏的网络 ,其中,他们分析了小说“冰雨的风暴”,火爆的“冰与火之歌”(权利的游戏电视剧的基础)的第三卷中的角色互动网络。在他们的论文中,他们详细介绍了如何通过使用文本分析和实体抽取,来发现文本中提到的角色,从而构建角色互动网络。然后,他们应用社交网络分析算法到该网络中,以找到该网络中最重要的角色,并且应用社区检测算法来找到角色集群。

分析和可视化是通过使用 Gephi,这一流行的图形分析工具,来完成的。我想,尝试使用 Neo4j 来复制作者的结果,将会很好玩。

导入初始数据到 Neo4j

数据可以在 作者的网站 上找到,你可以在 这里 下载。它看起来像这样:

Source,Target,Weight
Aemon,Grenn,5
Aemon,Samwell,31
Aerys,Jaime,18
...

这里,我们拥有整个文本中角色的邻接表和他们的互动次数。我们将使用一个简单的数据模型 (:Character {name})-[:INTERACTS {weight}]->(:Character {name}) 。带有标签的节点 Character 表示文本中的角色,单个关系类型 INTERACTS 从该角色连接到另一个文本中交互的角色。我们将把角色名作为 node 属性 name 存储,把两个角色之间的交互数作为 relationships 属性 weight 存储。

首先,我们必须创建一个约束来断言我们架构的完整性:

CREATE CONSTRAINT ON (c:Character) ASSERT c.name IS UNIQUE;

一旦创建了约束(该语句也将构建一个索引,它将提高通过角色名查找的性能),我们可以使用 Cypher 的 LOAD CSV 语句来导入数据:

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 = toInt(row.Weight)

我们有一个非常简单的数据模型:

CALL apoc.meta.graph()

权利的游戏图的数据模型。角色节点通过 INTERACTS 关系连接。 relationship.

我们可以看到整个图形,但这并没有给我们关于最重要人物或他们如何交互的许多信息:

MATCH p=(:Character)-[:INTERACTS]-(:Character)
RETURN p

分析网络

我们将使用 Cypher,这一图形查询语言来探索权利的游戏的图,在此过程中应用来自于网络分析的一些工具。这里探索的许多思路在优秀的 网络,人群和市场:思考高度连接的世界 中进行了详细的解释。

角色数

让我们先从简单的部分入手。在我们的图中,出现了多少个角色?

MATCH (c:Character) RETURN count(c)
                                  ╒════════╕
                                  │count(c)│
                                  ╞════════╡
                                  │107     │
                                  └────────┘

概要统计

每个角色交互的其他角色数的概要统计:

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
                ╒═══╤═══╤═════════════════╤═════════════════╕
                │min│max│avg_characters   │stdev            │
                ╞═══╪═══╪═════════════════╪═════════════════╡
                │1  │24 │4.957746478873241│6.227672391875085│
                └───┴───┴─────────────────┴─────────────────┘

网络直径

一个网络的直径(或者测地线)被定义为网络中的最长最短路径。

// Find maximum diameter of network
// maximum shortest path between two nodes
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
      ╒═══╤════════════════════════════════════════════════════════════╕
      │len│path                                                        │
      ╞═══╪════════════════════════════════════════════════════════════╡
      │6  │[Illyrio, Belwas, Daenerys, Robert, Tywin, Oberyn, Amory]   │
      ├───┼────────────────────────────────────────────────────────────┤
      │6  │[Illyrio, Belwas, Daenerys, Robert, Sansa, Bran, Jojen]     │
      ├───┼────────────────────────────────────────────────────────────┤
      │6  │[Illyrio, Belwas, Daenerys, Robert, Stannis, Davos, Shireen]│
      ├───┼────────────────────────────────────────────────────────────┤
      │6  │[Illyrio, Belwas, Daenerys, Robert, Sansa, Bran, Luwin]     │
      └───┴────────────────────────────────────────────────────────────┘

我们可以看到,在网络中,有许多长度为 6 的路径。

最短路径

我们可以使用 Cypher 中的 shortestPath 函数来查找图中任意两个角色的最短路径。让我们找找从 Catelyn Stark 到 Kahl Drogo 的最短路径:

// Shortest path from Catelyn Stark to Khal Drogo
MATCH (catelyn:Character {name: "Catelyn"}), (drogo:Character {name: "Drogo"})
MATCH p=shortestPath((catelyn)-[INTERACTS*]-(drogo))
RETURN p

所有最短路径

可能还有其他具有相同长度的连接 Catelyn 和 Drogo 的路径。我们可以使用 allShortestPaths Cypher 函数来查找:

// All shortest paths from Catelyn Stark to Khal Drogo
MATCH (catelyn:Character {name: "Catelyn"}), (drogo:Character {name: "Drogo"})
MATCH p=allShortestPaths((catelyn)-[INTERACTS*]-(drogo))
RETURN p

关键节点

如果节点位于网络中的其它两个节点之间的所有最短路径之中,那么该节点被认为是关键的。我们可以找到网络中的所有关键节点:

// Find all pivotal nodes in network
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
                          ╒═══════╤═══════╤═══════════╕
                          │a.name │b.name │PivotalNode│
                          ╞═══════╪═══════╪═══════════╡
                          │Aegon  │Thoros │Daenerys   │
                          ├───────┼───────┼───────────┤
                          │Aegon  │Thoros │Robert     │
                          ├───────┼───────┼───────────┤
                          │Drogo  │Ramsay │Robb       │
                          ├───────┼───────┼───────────┤
                          │Styr   │Daario │Daenerys   │
                          ├───────┼───────┼───────────┤
                          │Styr   │Daario │Jon        │
                          ├───────┼───────┼───────────┤
                          │Styr   │Daario │Robert     │
                          ├───────┼───────┼───────────┤
                          │Qhorin │Podrick│Jon        │
                          ├───────┼───────┼───────────┤
                          │Qhorin │Podrick│Sansa      │
                          ├───────┼───────┼───────────┤
                          │Orell  │Theon  │Jon        │
                          ├───────┼───────┼───────────┤
                          │Illyrio│Bronn  │Belwas     │
                          └───────┴───────┴───────────┘

如果我们翻阅了有趣结果的结果表,那么可以看到,对于 Drogo 和 Ramsay 来说,Robb 是一个关键节点。这意味着,所有连接 Drogo 和 Ramsay 的最短路径都经过 Robb。我们可以通过看看连接 Drogo 和 Ramsay 的所有最短路径,可视化的验证这点:

MATCH (a:Character {name: "Drogo"}), (b:Character {name: "Ramsay"})
MATCH p=allShortestPaths((a)-[:INTERACTS*]-(b))
RETURN p

中心性度量

中心性度量 为我们提供了网络中重要性的相对措施。有许多不同的中心性度量,而每种度量不同类型的“重要性”。

度中心性(Degree Centrality)

度中心性仅是一个节点在网络中的连接数。在权利的游戏的图的上下文中,一个角色的度中心性是该角色交互的其他角色数。我们可以像这样使用 Cypher 来计算度中心性:

MATCH (c:Character)
RETURN c.name AS character, size( (c)-[:INTERACTS]-() ) AS degree ORDER BY degree DESC
                              ╒═════════╤══════╕
                              │character│degree│
                              ╞═════════╪══════╡
                              │Tyrion   │36    │
                              ├─────────┼──────┤
                              │Jon      │26    │
                              ├─────────┼──────┤
                              │Sansa    │26    │
                              ├─────────┼──────┤
                              │Robb     │25    │
                              ├─────────┼──────┤
                              │Jaime    │24    │
                              ├─────────┼──────┤
                              │Tywin    │22    │
                              ├─────────┼──────┤
                              │Cersei   │20    │
                              ├─────────┼──────┤
                              │Arya     │19    │
                              ├─────────┼──────┤
                              │Joffrey  │18    │
                              ├─────────┼──────┤
                              │Robert   │18    │
                              └─────────┴──────┘

而且我们看到,Tyrion 与网络中的角色具有最多的互动。鉴于他的心计,我觉得这是有道理的。

加权度中心性

我们存储一对角色之间的交互数,作为 INTERACTS 关系上的 weight 属性。对角色的所有 INTERACTS 关系上的该权重求和,我们可以获得他们的加权度中心性,或者他们参与的交互总数。同样,我们可以使用 Cypher 来为所有的角色计算该度量:

MATCH (c:Character)-[r:INTERACTS]-()
RETURN c.name AS character, sum(r.weight) AS weightedDegree ORDER BY weightedDegree DESC
                        ╒═════════╤══════════════╕
                        │character│weightedDegree│
                        ╞═════════╪══════════════╡
                        │Tyrion   │551           │
                        ├─────────┼──────────────┤
                        │Jon      │442           │
                        ├─────────┼──────────────┤
                        │Sansa    │383           │
                        ├─────────┼──────────────┤
                        │Jaime    │372           │
                        ├─────────┼──────────────┤
                        │Bran     │344           │
                        ├─────────┼──────────────┤
                        │Robb     │342           │
                        ├─────────┼──────────────┤
                        │Samwell  │282           │
                        ├─────────┼──────────────┤
                        │Arya     │269           │
                        ├─────────┼──────────────┤
                        │Joffrey  │255           │
                        ├─────────┼──────────────┤
                        │Daenerys │232           │
                        └─────────┴──────────────┘

中介中心性(Betweenness Centrality)

一个网络中的一个节点的 中介中心性(Betweenness Centrality) 是,网络中所有的节点对之间通过该节点的最短路径数。中介中心性是一项重要的指标,因为它可以用于识别网络中的“信息代理”,或者那些连接不同集群的节点。

红色的节点具有较高的中介中心性,并且是集群的连接点。 图片来源

要计算中介中心性,我们将使用新的 用于 Neo4j 3.x 或 apoc 库的棒棒哒的程序 . 一旦我们安装了 apoc,我们就可以调用 Cypher 中 170+个程序中的任意一个:

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
                        ╒════════╤══════════════════╕
                        │name    │score             │
                        ╞════════╪══════════════════╡
                        │Jon     │1279.7533534055322│
                        ├────────┼──────────────────┤
                        │Robert  │1165.6025171231624│
                        ├────────┼──────────────────┤
                        │Tyrion  │1101.3849724234349│
                        ├────────┼──────────────────┤
                        │Daenerys│874.8372110508583 │
                        ├────────┼──────────────────┤
                        │Robb    │706.5572832464792 │
                        ├────────┼──────────────────┤
                        │Sansa   │705.1985623519137 │
                        ├────────┼──────────────────┤
                        │Stannis │571.5247305125714 │
                        ├────────┼──────────────────┤
                        │Jaime   │556.1852522889822 │
                        ├────────┼──────────────────┤
                        │Arya    │443.01358430043337│
                        ├────────┼──────────────────┤
                        │Tywin   │364.7212195528086 │
                        └────────┴──────────────────┘

接近中心性(Closeness centrality)

接近中心性(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
                        ╒═══════╤═════════════════════╕
                        │name   │score                │
                        ╞═══════╪═════════════════════╡
                        │Tyrion │0.004830917874396135 │
                        ├───────┼─────────────────────┤
                        │Sansa  │0.004807692307692308 │
                        ├───────┼─────────────────────┤
                        │Robert │0.0047169811320754715│
                        ├───────┼─────────────────────┤
                        │Robb   │0.004608294930875576 │
                        ├───────┼─────────────────────┤
                        │Arya   │0.0045871559633027525│
                        ├───────┼─────────────────────┤
                        │Jaime  │0.004524886877828055 │
                        ├───────┼─────────────────────┤
                        │Stannis│0.004524886877828055 │
                        ├───────┼─────────────────────┤
                        │Jon    │0.004524886877828055 │
                        ├───────┼─────────────────────┤
                        │Tywin  │0.004424778761061947 │
                        ├───────┼─────────────────────┤
                        │Eddard │0.004347826086956522 │
                        └───────┴─────────────────────┘

使用 python-igraph

我爱关于 Neo4j 的事情之一是,它与其他工具,例如 R 和 Python 数据科学工具配合良好。我们可以继续使用 apoc 来运行 PageRank社区检测 算法,但是,让我们切换到使用 python-igraph 来计算一些分析。Python-igraph 移植自 R 的 igraph 图形分析库。仅需使用 pip install python-igraph ,即可安装它。

从 Neo4j 构建一个 igraph 实例

要在我们的权利游戏的数据图上使用 igraph,所需要做的第一件事是从 Neo4j 中取出数据,然后在 Python 中构建一个 igraph 实例。我们可以使用 py2neo ,用于 Neo4j 的 Python 驱动之一,来做到这点。我们可以将一个 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) 算法。

PageRank: 每个节点的大小正比于网络中指向它的其他节点的数目和大小。图片来源 wikipedia CC-BY

这里,我们将在我们的 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)

现在,我们可以查询在 Neo4j 中的图,以找到具有最高 PageRank 得分的节点:

MATCH (n:Character)
RETURN n.name AS name, n.pagerank AS pagerank ORDER BY pagerank DESC LIMIT 10
                        ╒════════╤════════════════════╕
                        │name    │pagerank            │
                        ╞════════╪════════════════════╡
                        │Tyrion  │0.042884981999963316│
                        ├────────┼────────────────────┤
                        │Jon     │0.03582869669163558 │
                        ├────────┼────────────────────┤
                        │Robb    │0.03017114665594764 │
                        ├────────┼────────────────────┤
                        │Sansa   │0.030009716660108578│
                        ├────────┼────────────────────┤
                        │Daenerys│0.02881425425830273 │
                        ├────────┼────────────────────┤
                        │Jaime   │0.028727587587471206│
                        ├────────┼────────────────────┤
                        │Tywin   │0.02570016262642541 │
                        ├────────┼────────────────────┤
                        │Robert  │0.022292016521362864│
                        ├────────┼────────────────────┤
                        │Cersei  │0.022287327589773507│
                        ├────────┼────────────────────┤
                        │Arya    │0.022050209663844467│
                        └────────┴────────────────────┘

社区检测

图片来源

社区检测算法用以查找图中的集群。我们将使用 igraph 中实现的 walktrap 方法 ,来找到那些在社区之中频繁交互,但在社区之外不存在太多互动的角色的社区。

我们将运行 walktrap 社区检测算法,然后将新发现的社区数写回到 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)

然后,我们可以查询 Neo4j,看看最后我们有多少个社区(或者集群),以及每个社区中的成员:

MATCH (c:Character)
WITH c.community AS cluster, collect(c.name) AS  members
RETURN cluster, members ORDER BY cluster ASC
  ╒═══════╤═══════════════════════════════════════════════════════════════════════════╕
  │cluster│members                                                                    │
  ╞═══════╪═══════════════════════════════════════════════════════════════════════════╡
  │0      │[Aemon, Alliser, Craster, Eddison, Gilly, Janos, Jon, Mance, Rattleshirt, S│
  │       │amwell, Val, Ygritte, Grenn, Karl, Bowen, Dalla, Orell, Qhorin, Styr]      │
  ├───────┼───────────────────────────────────────────────────────────────────────────┤
  │1      │[Aerys, Amory, Balon, Brienne, Bronn, Cersei, Gregor, Jaime, Joffrey, Jon A│
  │       │rryn, Kevan, Loras, Lysa, Meryn, Myrcella, Oberyn, Podrick, Renly, Robert, │
  │       │Robert Arryn, Sansa, Shae, Tommen, Tyrion, Tywin, Varys, Walton, Petyr, Eli│
  │       │a, Ilyn, Pycelle, Qyburn, Margaery, Olenna, Marillion, Ellaria, Mace, Chata│
  │       │ya, Doran]                                                                 │
  ├───────┼───────────────────────────────────────────────────────────────────────────┤
  │2      │[Arya, Beric, Eddard, Gendry, Sandor, Anguy, Thoros]                       │
  ├───────┼───────────────────────────────────────────────────────────────────────────┤
  │3      │[Brynden, Catelyn, Edmure, Hoster, Lothar, Rickard, Robb, Roose, Walder, Je│
  │       │yne, Roslin, Ramsay]                                                       │
  ├───────┼───────────────────────────────────────────────────────────────────────────┤
  │4      │[Bran, Hodor, Jojen, Luwin, Meera, Rickon, Nan, Theon]                     │
  ├───────┼───────────────────────────────────────────────────────────────────────────┤
  │5      │[Belwas, Daario, Daenerys, Irri, Jorah, Missandei, Rhaegar, Viserys, Barris│
  │       │tan, Illyrio, Drogo, Aegon, Kraznys, Rakharo, Worm]                        │
  ├───────┼───────────────────────────────────────────────────────────────────────────┤
  │6      │[Davos, Melisandre, Shireen, Stannis, Cressen, Salladhor]                  │
  ├───────┼───────────────────────────────────────────────────────────────────────────┤
  │7      │[Lancel]                                                                   │
  └───────┴───────────────────────────────────────────────────────────────────────────┘

可视化 - 把它们放在一起

权利的游戏之图。节点大小正比于中介中心性,颜色表示节点集群,由 walktrap 方法测定,而边缘厚度正比于两个角色之间的互动数。

现在,我们已经计算出了该图的这些分析项,让我们创建一个可视化,使我们能够让这些数据有意义。

Neo4j 浏览器对于可视化 Cypher 查询的结果是棒棒哒,但如果我们想要在另一个应用中嵌入我们的图形可视化,我们可能需要使用许多用于图形可视化的伟大的 Javascript 库中的一个。 Vis.js 就是这样的一个库,它允许我们构建互动式图形可视化。要进行从 Neo4j 抽取数据,并使用 vis.js 构建图形可视化的过程,我一直使用 neovis.js ,它将 vis.js 和 Neo4j 官方 Javascript 驱动 组合在一起。Neovis.js 提供了一个简单的 API,允许配置可视化。例如,下面的 Javascript 是生成上面的可视化所需的:

var config = {
  container_id: "viz",
  server_url: "localhost",
  labels: {
    "Character": "name"
  },
  label_size: {
    "Character": "betweenness"
  },
  relationships: {
    "INTERACTS": null
  },
  relationship_thickness: {
    "INTERACTS": "weight"
  },
  cluster_labels: {
    "Character": "community"
  }
};

var viz = new NeoVis(config);
viz.render();

请注意,我们已经指定:

  • 在可视化中,我们想要包含带标签 Character 的节点,使用合适的 name 作为其标题
  • 节点大小应该正比于它们的 betweenness 属性
  • 在可视化中,我们想要包含 INTERACTS 关系
  • 关系的厚度应该正比于 weight 属性值
  • 节点应该根据 community 属性值来着色,该属性标识网络中的集群
  • localhost 上的一个 Neo4j 服务器中抽取数据
  • 在一个 id 为 viz 的 DOM 元素中展示可视化

Neovis.js 需要从 Neo4j 拉取数据,并且基于我们的最低配置创建可视化。

资源

所有代码 可以在 Github 上找到

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

如梦亦如幻

暂无简介

文章
评论
27 人气
更多

推荐作者

动次打次papapa

文章 0 评论 0

我是有多爱你

文章 0 评论 0

linces

文章 0 评论 0

玍銹的英雄夢

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文