7.ggplot2——网络图

7.网络图

同地图的空间数据一样,网络图形在可视化领域占据了特殊部分,但空间数据在投影的使用上与常规绘图大不相同,网络图将其自己的数据结构以及自己的可视化模式带入到表格中。由于这些复杂性,ggplot2 不直接支持网络图。可以通过使用ggraph进行网络可视化。具有相同功能的其他软件包有:geomnet、ggnetwork和GGally,他们都用于常规网络图,以及ggtree和ggdendro 专门用于树状图的可视化。

7.1 什么是网络数据?

网络(或其数学概念称为图)是由实体(节点顶点)及其关系(链接)组成的数据。节点和边都可以附加额外的数据,而且根据连接的性质,边还可以被认为是有向或无向的(编码相互友谊的网络将具有无向边,而祖先网络将具有有向边,因为child-of不是对称关系)。

网络数据的本质意味着它不容易在单个数据集中表示,这是将它与ggplot2一起使用的主要复杂性之一。然而,它可以被编码为两个相互关联的数据帧,一个编码节点,一个编码边缘。这是在tidygraph中使用的方法,它是基于ggraph的数据操作包。因此,为了更好地利用ggraph,对tidygraph了解一下是很有帮助的。

7.1.1 简单的网络操作API

tidygraph首先可以被认为是用于网络数据的dplyr API,它允许使用与dplyr相同的语义来操作网络。下面是一个例子,我们使用Erdős-Rényi抽样方法创建一个随机图,给节点分配一个随机标签,并根据其源节点的标签对边进行排序。

library(tidygraph)

graph <- play_erdos_renyi(n = 10, p = 0.2) %>% 
  activate(nodes) %>% 
  mutate(class = sample(letters[1:4], n(), replace = TRUE)) %>% 
  activate(edges) %>% 
  arrange(.N()$class[from])

graph
#> # A tbl_graph: 10 nodes and 17 edges
#> #
#> # A directed simple graph with 1 component
#> #
#> # Edge Data: 17 × 2 (active)
#>    from    to
#>    
#> 1     1     4
#> 2     1     8
#> 3     4     9
#> 4     8     3
#> 5     8     4
#> 6     7     9
#> # … with 11 more rows
#> #
#> # Node Data: 10 × 1
#>   class
#>   
#> 1 a    
#> 2 d    
#> 3 d    
#> # … with 7 more rows

虽然mutate()、arrange()和n()大家都知道,但我们需要解释新的函数:activate(),通知 tidygraph 您希望在网络的哪一部分上工作,是节点(nodes)或边(edges)。此外,我们看到使用`.N(),它允许访问当前图的节点数据,即使在处理边时也是如此(还有一个.E()函数来访问边数据,.G()访问整个图)。

7.1.2 数据转换

网络数据通常以各种不同的格式呈现,具体取决于您从何处获取。tidygraph 了解 R 中用于网络数据的大多数不同类,这些可以使用as_tbl_graph(). 下面是转换编码为边列表的数据帧的示例,以及转换hclust()的结果。

data(highschool, package = "ggraph")
head(highschool)
#>   from to year
#> 1    1 14 1957
#> 2    1 15 1957
#> 3    1 21 1957
#> 4    1 54 1957
#> 5    1 55 1957
#> 6    2 21 1957
hs_graph <- as_tbl_graph(highschool, directed = FALSE)
hs_graph
#> # A tbl_graph: 70 nodes and 506 edges
#> #
#> # An undirected multigraph with 1 component
#> #
#> # Node Data: 70 × 1 (active)
#>   name 
#>   
#> 1 1    
#> 2 2    
#> 3 3    
#> 4 4    
#> 5 5    
#> 6 6    
#> # … with 64 more rows
#> #
#> # Edge Data: 506 × 3
#>    from    to  year
#>     
#> 1     1    13  1957
#> 2     1    14  1957
#> 3     1    20  1957
#> # … with 503 more rows
luv_clust <- hclust(dist(luv_colours[, 1:3]))
luv_graph <- as_tbl_graph(luv_clust)
luv_graph
#> # A tbl_graph: 1313 nodes and 1312 edges
#> #
#> # A rooted tree
#> #
#> # Node Data: 1,313 × 4 (active)
#>   height leaf  label members
#>         
#> 1     0  TRUE  "101"       1
#> 2     0  TRUE  "427"       1
#> 3   778\. FALSE ""          2
#> 4     0  TRUE  "571"       1
#> 5     0  TRUE  "426"       1
#> 6     0  TRUE  "424"       1
#> # … with 1,307 more rows
#> #
#> # Edge Data: 1,312 × 2
#>    from    to
#>    
#> 1     3     1
#> 2     3     2
#> 3     8     6
#> # … with 1,309 more rows

我们可以看到tidygraph在转换的时候自动添加了额外的信息,比如highschool数据中的year列,层次聚类中节点的labelleaf属性。

7.1.3 算法

虽然简单地操作网络很好,但网络的真正好处在于可以使用底层结构对其执行不同的操作。Tidygraph对一系列不同的算法组有丰富的支持,如中心性计算(哪个节点是最中心的)、排序(排序节点使节点靠近它们所连接的节点)、分组(查找网络中的集群)等。 算法 API 被设计为mutate()中使用,并且总是返回一个长度和顺序与节点或边匹配的向量。而且,它不要求您指定要计算的图形或节点,因为这是在mutate()`调用中隐式给出的。例如,我们将使用PageRank算法计算样本图中节点的中心度,并根据该算法对节点进行排序:

graph %>% 
  activate(nodes) %>% 
  mutate(centrality = centrality_pagerank()) %>% 
  arrange(desc(centrality))
#> # A tbl_graph: 10 nodes and 17 edges
#> #
#> # A directed simple graph with 1 component
#> #
#> # Node Data: 10 × 2 (active)
#>   class centrality
#>         
#> 1 d         0.188 
#> 2 c         0.186 
#> 3 c         0.123 
#> 4 c         0.0926
#> 5 a         0.0904
#> 6 a         0.0891
#> # … with 4 more rows
#> #
#> # Edge Data: 17 × 2
#>    from    to
#>    
#> 1     5     6
#> 2     5     8
#> 3     6     2
#> # … with 14 more rows

为了理解ggraph,这只是对tidygraph的简单介绍。如果您想了解更多信息,tidygraph 网站提供了软件包中所有功能的概述:https://tidygraph.data-imaginist.com/

7.2 可视化网络

ggraph 建立在 tidygraph 和 ggplot2 之上,为网络数据提供了一个完整而熟悉的图形语法。尽管如此,它与大多数ggplot2扩展包还是有一点不同,因为它使用的是另一种数据类型,而这种数据类型从根本上不同于表格数据。更重要的是,大多数网络可视化并不关心将变量映射到x和y的美观性,因为它们关心的是显示网络拓扑而不是两个变量之间的关系。为了表现网络拓扑结构,引入了布局的概念。布局是一种算法,它使用网络结构来计算(通常是任意的)每个节点的x和y值,然后用于可视化目的。用另一种方式,当绘制表格数据x和y图形属性几乎总是映射到现有的变量的数据(或统计转换现有的数据)而当策划网络数据x和y是映射到值来源于网络的拓扑和本身毫无意义。

7.2.1设置可视化

普通的ggplot2图形是通过调用ggplot()来初始化的,而ggraph图形是通过调用ggraph()来初始化的。第一个参数是data,它可以是tbl_graph或任何可转换为tbl_graph的对象。第二个参数是一个布局函数,任何进一步的参数都将传递给该函数。默认的布局将选择一个合适的布局基于您提供的图表类型,但它通常是一个不错的起点你应该控制和探索不同的布局网络是臭名昭著的能力显示不存在或夸张的关系在某些布局。除了此处所描述的,还有更多关于布局的内容。入门指南的布局将展示更多的信息,并展示由GGRAPH提供的所有不同的布局。

7.2.1.1 指定布局

布局参数可以接受一个字符串或一个函数。如果提供了一个字符串,名称将与布局中的一个构建(有许多)相匹配。如果提供了一个函数,则假定该函数接受一个tbl_graph,并返回一个数据帧,该数据帧至少有一个x和y列,且行数与输入图中的节点相同。下面我们可以看到使用默认布局的例子,指定一个特定的布局,并为布局提供参数(在输入图的上下文中计算):

library(ggraph)
ggraph(hs_graph) + 
  geom_edge_link() + 
  geom_node_point()
#> Using `stress` as default layout
image
ggraph(hs_graph, layout = "drl") + 
  geom_edge_link() + 
  geom_node_point()
image
hs_graph <- hs_graph %>% 
  activate(edges) %>% 
  mutate(edge_weights = runif(n()))
ggraph(hs_graph, layout = "stress", weights = edge_weights) + 
  geom_edge_link(aes(alpha = edge_weights)) + 
  geom_node_point() + 
  scale_edge_alpha_identity()
image

为了显示上面的图形,我们使用了geom_edge_link()和geom_node_point()函数,它们的作用是:将节点绘制为点,将边绘制为直线。

7.2.1.2 环型网络图

有些布局可能同时用于线性和圆形版本。在ggplot2中更改这一点的正确方法是使用coord_polar()来更改坐标系统,但由于我们只想更改布局中节点的位置,而不影响边缘,因此这是布局的一个函数。以下可以显示两者的区别:

ggraph(luv_graph, layout = 'dendrogram', circular = TRUE) + 
  geom_edge_link() + 
  coord_fixed()
image
ggraph(luv_graph, layout = 'dendrogram') + 
  geom_edge_link() + 
  coord_polar() + 
  scale_y_reverse()
image

结果是,使用coord_polar()会弯曲我们的边缘,这个选择不太好。

7.2.2 绘图节点

在图中存储的两种数据类型中,节点是迄今为止与我们所使用的绘图最相似的。毕竟,它们通常以点的形式显示,就像观测结果在散点图中显示一样。虽然概念上很简单,但我们仍然不会涵盖关于节点的所有知识,感兴趣的读者可以直接阅读节点入门指南以了解更多信息。在ggraph中,所有的节点绘制图形都以geom_node_作为前缀,您最可能使用的是geom_node_point()。虽然它表面上看起来很像geom_point(),但它有一些附加的特性,它与所有的节点和边缘几何体共享。首先,您不需要指定x和y图形属性。这些是由布局给出的,它们的映射是隐式的。其次,您可以使用filter属性,它允许您关闭特定节点的绘制。第三,您可以使用aes()中的任何整理图算法,它们将在被显示的图上得到评估。为了看到这一点,我们再次绘制highschool的图,但这次只显示了超过2个连接的节点,并通过它们的权力中心着色:

ggraph(hs_graph, layout = "stress") + 
  geom_edge_link() + 
  geom_node_point(
    aes(filter = centrality_degree() > 2, 
        colour = centrality_power()),
    size = 4
  )
image

能够直接在可视化代码中使用算法是迭代可视化的有效方式,因为您无需返回并更改输入图。

除了点之外,还有更专业的几何体,其中许多与特定类型的布局相关联。如果希望绘制树状图,则需要使用geom_node_tile():

ggraph(luv_graph, layout = "treemap") + 
  geom_node_tile(aes(fill = depth))
#> Warning: Existing variables `height`, `leaf` overwritten by layout variables
image

7.2.3 绘制边界

边缘几何体比节点几何体有更多的附加功能,主要是因为有太多不同的方式可以连接两个东西。没有办法涵盖所有内容,既包括不同类型的geom,也包括它们所具有的共同功能。入门指南将给出一个完整的概述。

我们已经看到了实际的geom_edge_link(),它在连接的节点之间绘制一条直线,但是它可以做的比我们已经了解到的更多。它将在一堆小碎片分裂的线,它是可能使用的梯度沿边缘绘制,例如显示方向:

ggraph(graph, layout = "stress") + 
  geom_edge_link(aes(alpha = after_stat(index)))
image

如果你要画很多边,这个扩展可能会变得非常耗时,并且ggraph提供了一个0后缀的版本,将它作为一个简单的geom(并且不允许你绘制渐变)。此外,对于你想在端点(例如节点上的变量)的两个值之间插入的特殊情况,也存在2后缀的版本:

ggraph(graph, layout = "stress") + 
  geom_edge_link2(
    aes(colour = node.class), 
    width = 3,
    lineend = "round")
image

node.class变量的使用可能会让您感到惊讶。边缘几何体可以通过特殊的前缀变量访问终端节点的变量。对于标准版本和0版本这些都可以通过node1.node2.前缀的变量获得,对于2版本它们都可以通过node.预先指定的变量(如以上使用)。边缘几何体的三个版本对于所有边缘几何类型都是通用的,而不仅仅是geom_edge_link()。

除了简单的直线,还有更多的方法来画边。有些是特定于树或特定布局的,但许多是通用的。另一种边类型的一个特定用例是在同一节点之间运行多条边。将它们画成直线将掩盖边的多样性,例如,这在highschool图形中是明显的,其中有多个平行边,但在上面的图形中是不可见的。为了显示平行边,你可以使用geom_edge_fan()或geom_edge_parallel():

ggraph(hs_graph, layout = "stress") + 
  geom_edge_fan()
image
ggraph(hs_graph, layout = "stress") + 
  geom_edge_parallel()
image

很明显,这些几何图形应该只用于相对简单的图形,因为它们会增加图中的混乱和过度绘制的数量。查看树,特别是树状图,一种常用的边缘类型是肘部边缘:

ggraph(luv_graph, layout = "dendrogram", height = height) + 
  geom_edge_elbow()
image

geom_edge_bend()和geom_edge_diagonal()是运行更流畅的版本。

7.2.3.1 修剪节点周围的边

一个常见问题,尤其是在使用箭头显示边的方向时,节点将与边重叠,因为它运行到节点的中心,而不是显示节点的点的边。这可以在下面看到:

ggraph(graph, layout = "stress") + 
  geom_edge_link(arrow = arrow()) + 
  geom_node_point(aes(colour = class), size = 8)
image

显然,我们希望边缘在到达点之前停止,这样箭头就不会被遮挡。这可以在 ggraph 中使用start_capend_cap图形属性实现,它允许您在终端节点周围指定一个剪切区域。为了修复上面的图,我们将在每个节点周围设置一个正确大小的圆形裁剪区域:

ggraph(graph, layout = "stress") + 
  geom_edge_link(
    arrow = arrow(), 
    start_cap = circle(5, "mm"),
    end_cap = circle(5, "mm")
  ) + 
  geom_node_point(aes(colour = class), size = 8)
image

7.2.3.2 边不是一条线

虽然很自然地认为边是连接点的不同类型的线,但这只适用于特定的网络图类型。我们应该始终注意到节点和边是抽象概念,可以通过多种方式可视化。作为一个例子,我们可以看一下矩阵图,它通过行和列的位置隐式地显示节点,并以点或块的形式显示边缘。

ggraph(hs_graph, layout = "matrix", sort.by = node_rank_traveller()) + 
  geom_edge_point()
image

7.2.4 分面

分面不是一个经常应用于网络可视化的概念,但它处理网络和表格数据一样强大。虽然 ggplot2 中的标准分面函数在技术上与 ggraph 一起工作,但它们不是在概念层面上,因为节点和边是连接的,并且在多个子图上拆分节点将自动移动边,即使这些边的数据中没有分面变量。正因为如此,ggraph 提供了它自己的facet_wrap()和facet_grid()版本。facet_nodes()和facet_edges()将以节点或边为目标,并以与facet_wrap()相同的方式包裹面板。对于facet_nodes()`,约定是如果一条边位于同一面板中的两个节点之间,它将显示在该面板中,但是如果它在多个面板之间分裂,它将被删除。对于facet_edges()节点将在所有面板中重复。为了看到它的实际效果,我们可以查看我们的highschool图表,看看他们的友谊多年来是如何发展的。

ggraph(hs_graph, layout = "stress") + 
  geom_edge_link() + 
  geom_node_point() + 
  facet_edges(~year)
image

随着分面变得非常清晰,我们看到友谊从两个完全独立的群体明显演变为一个更加混合的单一群体。

由于 faceting 也接受 tidygraph 算法,因此它是一种很好的方法来评估例如动态分组的结果。

ggraph(hs_graph, layout = "stress") + 
  geom_edge_link() + 
  geom_node_point() + 
  facet_nodes(~ group_spinglass())
image

最后一个包含的 facet 类型facet_graph()用作facet_grid(),但允许您指定行和列应该在哪个部分,边缘或节点。

这只是对 ggraph 中部分使用方法。如果您想更加深入了解,请访问https://tidygraph.data-imaginist.com和https://ggraph.data-imaginist.com。了解tidygraph基础和API会增加你对ggraph的掌握和理解,所以我们要一起学习。

你可能感兴趣的:(7.ggplot2——网络图)