不知道如何在地图上可视化网络图?下面这篇博客将使用R中的igraph、ggplot2或ggraph包来介绍三种在地图上可视化网络图的方法。在对地理位置以及位置的连接关系进行可视化时,还可以在图中展示一些属性。
当我们对节点(nodes)为地理位置的网络图进行可视化时,比较有效的做法是将这些节点绘制在地图上并画出它们之间的连接关系,因为这样我们可以直接看到网络图中节点的地理分布及其连接关系。
但这与传统的网络图是不同的。在传统的网络图中,节点的分布取决于使用何种布局算法(layout algorithm),有一些算法可能会使紧密联系的那些节点聚成集群。
下面将介绍三种可视化的方法。
准备工作
首先,我们需要加载下面的库:
library(assertthat)
library(dplyr)
library(purrr)
library(igraph)
library(ggplot2)
library(ggraph)
library(ggmap)
现在,让我们加载一些样本节点。我随机选取了几个国家的地理坐标。
country_coords_txt <- "
1 3.00000 28.00000 Algeria
2 54.00000 24.00000 UAE
3 139.75309 35.68536 Japan
4 45.00000 25.00000 'Saudi Arabia'
5 9.00000 34.00000 Tunisia
6 5.75000 52.50000 Netherlands
7 103.80000 1.36667 Singapore
8 124.10000 -8.36667 Korea
9 -2.69531 54.75844 UK
10 34.91155 39.05901 Turkey
11 -113.64258 60.10867 Canada
12 77.00000 20.00000 India
13 25.00000 46.00000 Romania
14 135.00000 -25.00000 Australia
15 10.00000 62.00000 Norway"
# nodes come from the above table and contain geo-coordinates for some
# randomly picked countries
nodes <- read.delim(text = country_coords_txt, header = FALSE,
quote = "'", sep = "",
col.names = c('id', 'lon', 'lat', 'name'))
我们选取了15个国家作为网络图的节点,每个节点的信息包括国名、地理坐标(经度和纬度)和一个ID。现在,我将随机生成这些节点之间的连接关系:
set.seed(123) # set random generator state for the same output
N_EDGES_PER_NODE_MIN <- 1
N_EDGES_PER_NODE_MAX <- 4
N_CATEGORIES <- 4
# edges: create random connections between countries (nodes)
edges <- map_dfr(nodes$id, function(id) {
n <- floor(runif(1, N_EDGES_PER_NODE_MIN, N_EDGES_PER_NODE_MAX+1))
to <- sample(1:max(nodes$id), n, replace = FALSE)
to <- to[to != id]
categories <- sample(1:N_CATEGORIES, length(to), replace = TRUE)
weights <- runif(length(to))
data_frame(from = id, to = to, weight = weights, category = categories)
})
edges <- edges %>% mutate(category = as.factor(category))
这里每条边均通过from列和to列里的节点ID来确定节点之间的连接关系。此外,我们生成随机连接关系的类型和强度。这些属性通常用于图表分析,之后也可以被可视化。
这样我们的节点和边就充分表现了图的内容。现在我们可以用igraph库生成一个图结构g,这对于以后快速计算每个节点的等级或其他属性尤为必要。
g <- graph_from_data_frame(edges, directed = FALSE, vertices = nodes)
我们现在创建一些数据结构,这些数据结构将用于我们将要生成的所有的图。首先,我们创建一个数据框来绘制边。这个数据框将与edges数据框类似,但是有额外四列数据来定义每条边的开始点和结束点(x, y 和 xend, yend):
edges_for_plot <- edges %>%
inner_join(nodes %>% select(id, lon, lat), by = c('from' = 'id')) %>%
rename(x = lon, y = lat) %>%
inner_join(nodes %>% select(id, lon, lat), by = c('to' = 'id')) %>%
rename(xend = lon, yend = lat)
assert_that(nrow(edges_for_plot) == nrow(edges))
现在我们给每个节点赋予一个权重,并使用等级作为指标。在地图上这个指标表现为节点的大小。
nodes$weight = degree(g)
现在我们定义一个通用的ggplot2 的主题(在ggplot中设置及美化图形的一个工具)来展示地图 (无坐标轴和网格线):
maptheme <- theme(panel.grid = element_blank()) +
theme(axis.text = element_blank()) +
theme(axis.ticks = element_blank()) +
theme(axis.title = element_blank()) +
theme(legend.position = "bottom") +
theme(panel.grid = element_blank()) +
theme(panel.background = element_rect(fill = "#596673")) +
theme(plot.margin = unit(c(0, 0, 0.5, 0), 'cm'))
所有的图将会应用同一个主题,并使用相同的世界地图作为“背景”(用map_data(‘world’)实现),采取同一个固定比例的坐标系来限定经度和纬度。
country_shapes <- geom_polygon(aes(x = long, y = lat, group = group),
data = map_data('world'),
fill = "#CECECE", color = "#515151",
size = 0.15)
mapcoords <- coord_fixed(xlim = c(-150, 180), ylim = c(-55, 80))
图1:仅ggplot2
让我们从ggplot2开始入门吧!
除了世界地图(country_shapes)中的国家多边形以外,我们还需创建三个几何对象:使用geom_point将节点绘制为点,使用geom_text为节点添加标签;使用geom_curve将节点之间的边绘制成曲线。
在图中,我们需要为每个几何对象定义图形属性映射(aesthetic mappings,也称为美学映射,用以“描述数据中的变量如何映射到视觉属性”)。
图形属性映射链接:
http://ggplot2.tidyverse.org/reference/aes.html
对于节点,我们将它们的地理坐标映射到图中的x和y位置,并且由其权重所决定节点的大小(aes(x = lon,y = lat,size = weight))。对于边,我们传递edges_for_plot数据框架并使用x, y 和 xend, yend 作为曲线的起点和终点。
此外,每条边的颜色都取决于它的类别(category),而它的“尺寸”(指它的线宽)取决于边的权重(一会儿我们会发现后面这一条没有实现)。
请注意,几何对象的顺序非常重要,因为它决定了哪个对象先被绘制,并可能会被随后在下一个几何对象层中绘制的对象所遮挡。因此,我们首先绘制边,然后节点,最后才是顶部的标签:
ggplot(nodes) + country_shapes +
geom_curve(aes(x = x, y = y, xend = xend, yend = yend, # draw edges as arcs
color = category, size = weight),
data = edges_for_plot, curvature = 0.33,
alpha = 0.5) +
scale_size_continuous(guide = FALSE, range = c(0.25, 2)) + # scale for edge widths
geom_point(aes(x = lon, y = lat, size = weight), # draw nodes
shape = 21, fill = 'white',
color = 'black', stroke = 0.5) +
scale_size_continuous(guide = FALSE, range = c(1, 6)) + # scale for node size
geom_text(aes(x = lon, y = lat, label = name), # draw text labels
hjust = 0, nudge_x = 1, nudge_y = 4,
size = 3, color = "white", fontface = "bold") +
mapcoords + maptheme
这时候代码界面中的控制台中会显示一条警告,提示“已显示‘尺寸’标度,添加其他的标度‘尺寸‘将替换现有的标度。”这是因为我们两次使用了“尺寸”的图形属性及其标度,一次用于节点大小,一次用于曲线的宽度。
比较麻烦的是,我们不能在同一个图形属性上定义两种不同的标度,即使这个图形属性要用于不同的几何对象(比如在我们这个例子里:“尺寸”这个图形属性被同时用于节点的大小和边的线宽)。据我所知在ggplot2中控制线宽只能通过“size“来实现。
使用ggplot2,我们只需决定要调整哪一个几何对象的大小。此处,我选择使用静态节点大小和动态线宽:
ggplot(nodes) + country_shapes +
geom_curve(aes(x = x, y = y, xend = xend, yend = yend, # draw edges as arcs
color = category, size = weight),
data = edges_for_plot, curvature = 0.33,
alpha = 0.5) +
scale_size_continuous(guide = FALSE, range = c(0.25, 2)) + # scale for edge widths
geom_point(aes(x = lon, y = lat), # draw nodes
shape = 21, size = 3, fill = 'white',
color = 'black', stroke = 0.5) +
geom_text(aes(x = lon, y = lat, label = name), # draw text labels
hjust = 0, nudge_x = 1, nudge_y = 4,
size = 3, color = "white", fontface = "bold") +
mapcoords + maptheme
图2:ggplot2+ggraph
幸运的是,ggplot2有一个名为ggraph的扩展包,里面包含专门用于绘制网络图的几何对象和图形属性。这样我们就可以对节点和边使用不同的标度了。默认情况下,ggraph将根据你指定的布局算法放置节点。但是我们还可以使用地理坐标作为节点位置来自定义布局:
node_pos <- nodes %>%
select(lon, lat) %>%
rename(x = lon, y = lat) # node positions must be called x, y
lay <- create_layout(g, 'manual',
node.positions = node_pos)
assert_that(nrow(lay) == nrow(nodes))
# add node degree for scaling the node sizes
lay$weight <- degree(g)
我们使用先前定义的布局lay和拓展包ggraph中的几何对象geom_edge_arc及geom_node_point来作图:
ggraph(lay) + country_shapes +
geom_edge_arc(aes(color = category, edge_width = weight, # draw edges as arcs
circular = FALSE),
data = edges_for_plot, curvature = 0.33,
alpha = 0.5) +
scale_edge_width_continuous(range = c(0.5, 2), # scale for edge widths
guide = FALSE) +
geom_node_point(aes(size = weight), shape = 21, # draw nodes
fill = "white", color = "black",
stroke = 0.5) +
scale_size_continuous(range = c(1, 6), guide = FALSE) + # scale for node sizes
geom_node_text(aes(label = name), repel = TRUE, size = 3,
color = "white", fontface = "bold") +
mapcoords + maptheme
边的宽度可以通过edge_width的图形属性及其标度函数scale_edge_width进行控制。节点则沿用之前的size来控制大小。另一个不错的功能是,geom_node_text可以通过repel = TRUE 来分布节点标签,这样它们就不会互相遮挡太多。
请注意,图的边与之前ggplot2的图采用了不同的绘制方式。由于ggraph采用了不同的布局算法,连接关系仍然相同,只是布局变了。例如,加拿大和日本之间的绿松石色边线已经从最北部转移至南部,并穿过了非洲中心。
图3:拙劣的方法(叠加数个ggplot2“plot grobs”)
我不想隐瞒另一个可能被认为是拙劣的方法:通过将它们标注为“grobs”(graphical objects的简称),你可以叠加几个单独创建的图(透明背景)。这可能不是图形对象标注功能本来的目的,但总之,当你真的需要克服上面图1中所描述的ggplot2图形属性限制时,它随时可以派上用场。
图形对象标注链接:
http://ggplot2.tidyverse.org/reference/annotation_custom.html
如上所述,我们将制作独立的图并“堆叠”它们。第一个图就是之前以世界地图为“背景”的图。第二个图是一个只显示边的叠加层。最后,第三个叠加层图仅显示带有节点及其标签的点。这样设置后,我们便可以分别控制边线的线宽和节点的大小,因为它们是在图中各自单独生成。
这两次叠加需要有一个透明的背景,所以我们用一个主题来定义它:
theme_transp_overlay <- theme(
panel.background = element_rect(fill = "transparent", color = NA),
plot.background = element_rect(fill = "transparent", color = NA)
)
底图或“背景”图制作十分方便,且仅显示地图:
p_base <- ggplot() + country_shapes + mapcoords + maptheme
现在,我们创建第一个叠加层的边,线宽的大小由边的权重所决定:
p_edges <- ggplot(edges_for_plot) +
geom_curve(aes(x = x, y = y, xend = xend, yend = yend, # draw edges as arcs
color = category, size = weight),
curvature = 0.33, alpha = 0.5) +
scale_size_continuous(guide = FALSE, range = c(0.5, 2)) + # scale for edge widths
mapcoords + maptheme + theme_transp_overlay +
theme(legend.position = c(0.5, -0.1),
legend.direction = "horizontal")
p_nodes <- ggplot(nodes) +
geom_point(aes(x = lon, y = lat, size = weight),
shape = 21, fill = "white", color = "black", # draw nodes
stroke = 0.5) +
scale_size_continuous(guide = FALSE, range = c(1, 6)) + # scale for node size
geom_text(aes(x = lon, y = lat, label = name), # draw text labels
hjust = 0, nudge_x = 1, nudge_y = 4,
size = 3, color = "white", fontface = "bold") +
mapcoords + maptheme + theme_transp_overlay
最后,我们使用图形对象标注组合叠加层。请注意,准确定位图形对象的工作十分繁琐。我发现使用ymin可以做得很好,但似乎必须手动调整参数。
p <- p_base +
annotation_custom(ggplotGrob(p_edges), ymin = -74) +
annotation_custom(ggplotGrob(p_nodes), ymin = -74)
print(p)
正如前面所述,这是一个拙劣的解决方案,应谨慎使用。但在有些情况下,它还是有用的。例如,当你需要在线图中使用不同标度的点尺寸和线宽时,或者需要在单个绘图中使用不同的色彩标度时,可以考虑采用这种方法。
总而言之,基于地图的网络图对于显示节点之间的地理尺度上的连接关系十分有用。缺点是,当有很多地理位置接近的点和许多重叠的连接时,它会看起来非常混乱。在仅显示地图的某些细节,或者对边的定位点添加一些抖动时,这种方法可能会很有用。
原文发布时间为:2018-06-15
本文作者:睡不着的iris、陈同学、YYY