三种可视化方法,手把手教你用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 条评论
登录 后参与评论

相关文章

来自专栏python读书笔记

python 数据分析基础 day19-使用statsmodels进行逻辑回归

今天是读《python数据分析基础》的第19天,读书笔记内容为使用statsmodels进行逻辑回归。 以下代码将按数据清洗、训练模型、得出测试集的预测值这三...

1.2K7
来自专栏专知

【重磅】深度学习顶会 ICLR 2018 匿名提交论文列表(附pdf下载链接)

【导读】ICLR,全称为「International Conference on Learning Representations」(国际学习表征会议),201...

60810
来自专栏人工智能

Redis-ML简介(第5部分)

原文地址:https://dzone.com/articles/an-introduction-to-redis-ml-part-five-redis-labs

9728
来自专栏新智元

PyTorch 最新版发布:API 变动,增加新特征,多项运算和加载速度提升

【新智元导读】PyTorch 发布了最新版,API 有一些变动,增加了一系列新的特征,多项运算或加载速度提升,而且修改了大量bug。官方文档也提供了一些示例。 ...

5737
来自专栏Python数据科学

Seaborn从零开始学习教程(二)

在Seaborn的使用中,是可以针对数据类型而选择合适的颜色,并且使用选择的颜色进行可视化,节省了大量的可视化的颜色调整工作。

1382
来自专栏数据小魔方

sparklines迷你图系列4——Evolution(Area)

今天接着分享Evolution图表类型中的Area图表。 其实就是我们常见的区域图(或者叫面积图),它与折线图(昨天讲到的)都是用来呈现时间序列中的趋势走向和波...

2624
来自专栏懒人开发

(7.1)James Stewart Calculus 5th Edition:Integration by Parts

注意: 这样做,目的是为了 降阶, 如果转换后,对应的没有起到 降阶 的作用,就没有什么意义了

1001
来自专栏开源FPGA

MATLAB数字图像处理学习笔记

   我们都知道一幅图片就相当于一个二维数组,可以用一个矩阵来表示,而MATLAB可以说就是为矩阵运算而生的,所以学习图像处理,学习MATLAB势在必行!  一...

2285
来自专栏逍遥剑客的游戏开发

Nebula3中的模型

1287
来自专栏生信宝典

R语言学习 - 火山图

火山图 火山图用于展示基因表达差异的分布,横轴为Log2 Fold Change,越偏离中心差异倍数越大;纵轴为(-1)*Log10 P_adjust,值越大差...

3247

扫码关注云+社区