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

大数据文摘出品

编译:睡不着的iris、陈同学、YYY

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

相关报道:

https://www.r-bloggers.com/three-ways-of-visualizing-a-graph-on-a-map/

原文发布于微信公众号 - 大数据文摘(BigDataDigest)

原文发表时间:2018-06-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏用户2442861的专栏

MATLAB 比较好入门书籍有哪些推荐

https://www.zhihu.com/topic/19559252/hot

441
来自专栏量子位

如何自制会跳舞的AI小姐姐?这有一份易上手的开源攻略

身材苗条,动作有力,姿势优美,视频片段里的小姐姐跳得行云流水,颇有C位出道的气势。

833
来自专栏斑斓

Spark 1.4为DataFrame新增的统计与数学函数

Spark一直都在快速地更新中,性能越来越快,功能越来越强大。我们既可以参与其中,也可以乐享其成。 目前,Spark 1.4版本在社区已经进入投票阶段,在Gi...

3457
来自专栏IT派

python代码实现图片噪声去除

今天来给大家分享下怎么做图片的噪声去除。平时其实大家上网都能遇到这样的场景,就是输入讨厌验证码,怎么都输不对。验证码现在可以说是千奇百怪、分外妖娆,为啥要做成这...

933
来自专栏IT大咖说

当Elasticsearch遇见智能客服机器人

摘要 本次分享主要会介绍一下ES是如何帮我们完成NLP的任务的。在做NLP相关任务的时候,ES的相似度算法并不足以支撑用户的搜索,需要使用一些与语义相关的方法进...

4346
来自专栏WeTest质量开放平台团队的专栏

移动平台 Unity3D 应用性能优化(下)

下篇:一些关于移动平台上Unity3D的性能优化经验,供分享。

2880
来自专栏大数据挖掘DT机器学习

Microsoft 线性回归分析算法

前言 在此系列中涵盖了微软在商业智能(BI)模块系统所能提供的所有挖掘算法,当然此框架完全可以自己扩充,可以自定义挖掘算法,不过目前此系列中还不涉及,只涉及微...

2723
来自专栏量子位

众筹项目能否成功?用机器学习预测可以早知道

安妮 编译自 Shrikar Archak 量子位出品 | 公众号 QbitAI Kickstarter是一家美国的众筹平台。自2009年成立至今,已经有36万...

3415
来自专栏数据小魔方

R语言可视化——ggplot图表配色技巧

今天跟大家分享ggplot图表的配色原理与基本技巧。 图表配色是一个很深奥的话题,多亏了R语言平台的众多开发者贡献的配色包,让图表的配色不再深不可测。 这里我暂...

3744
来自专栏落影的专栏

OpenGL ES实践教程(四)VR全景视频播放

教程 OpenGL ES实践教程1-Demo01-AVPlayer OpenGL ES实践教程2-Demo02-摄像头采集数据和渲染 OpenGL ES实践...

3704

扫码关注云+社区