查看原文
其他

三种可视化方法,手把手教你用R绘制地图网络图!

慧天地 2020-09-16


点击左上方蓝色字体“慧天地”即可订阅

(点击图片可放大观看,更多精彩请留意文末推荐)

不知道如何在地图上可视化网络图?下面这篇博客将使用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)

正如前面所述,这是一个拙劣的解决方案,应谨慎使用。但在有些情况下,它还是有用的。例如,当你需要在线图中使用不同标度的点尺寸和线宽时,或者需要在单个绘图中使用不同的色彩标度时,可以考虑采用这种方法。


总而言之,基于地图的网络图对于显示节点之间的地理尺度上的连接关系十分有用。缺点是,当有很多地理位置接近的点和许多重叠的连接时,它会看起来非常混乱。在仅显示地图的某些细节,或者对边的定位点添加一些抖动时,这种方法可能会很有用。

完整的R脚本可参阅github上的gist。


请点击阅读原文查看相关报道


来源:大数据文摘编译:睡不着的iris、陈同学、YYY,版权归原作者及刊载媒体所有)


荐读

点击下文标题即可阅读

学会这10种阅读方法,碾压你的知识焦虑!

一种利用空间信息与数字技术专业知识的“互联网+”创新创业方法

以建成区提取为目标的Landsat8影像融合方法研究

编辑 / 白杨  审核 / 游志龙  卞艺潼

指导:万剑华教授(微信号wjh18266613129)

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存