三種可視化方法,手把手教你用R繪制地圖網(wǎng)絡圖!
大數(shù)據(jù)文摘出品
編譯:睡不著的iris、陳同學、YYY
不知道如何在地圖上可視化網(wǎng)絡圖?下面這篇博客將使用R中的igraph、ggplot2或ggraph包來介紹三種在地圖上可視化網(wǎng)絡圖的方法。在對地理位置以及位置的連接關系進行可視化時,還可以在圖中展示一些屬性。
當我們對節(jié)點(nodes)為地理位置的網(wǎng)絡圖進行可視化時,比較有效的做法是將這些節(jié)點繪制在地圖上并畫出它們之間的連接關系,因為這樣我們可以直接看到網(wǎng)絡圖中節(jié)點的地理分布及其連接關系。
但這與傳統(tǒng)的網(wǎng)絡圖是不同的。在傳統(tǒng)的網(wǎng)絡圖中,節(jié)點的分布取決于使用何種布局算法(layout algorithm),有一些算法可能會使緊密聯(lián)系的那些節(jié)點聚成集群。
下面將介紹三種可視化的方法。
準備工作
首先,我們需要加載下面的庫:
library(assertthat)
library(dplyr)
library(purrr)
library(igraph)
library(ggplot2)
library(ggraph)
library(ggmap)
現(xiàn)在,讓我們加載一些樣本節(jié)點。我隨機選取了幾個國家的地理坐標。
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 = "",
ccol.names = c('id', 'lon', 'lat', 'name'))
我們選取了15個國家作為網(wǎng)絡圖的節(jié)點,每個節(jié)點的信息包括國名、地理坐標(經(jīng)度和緯度)和一個ID?,F(xiàn)在,我將隨機生成這些節(jié)點之間的連接關系:
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, toto = to, weight = weights, category = categories)
})
edges <- edges %>% mutate(category = as.factor(category))
這里每條邊均通過from列和to列里的節(jié)點ID來確定節(jié)點之間的連接關系。此外,我們生成隨機連接關系的類型和強度。這些屬性通常用于圖表分析,之后也可以被可視化。
這樣我們的節(jié)點和邊就充分表現(xiàn)了圖的內(nèi)容?,F(xiàn)在我們可以用igraph庫生成一個圖結(jié)構(gòu)g,這對于以后快速計算每個節(jié)點的等級或其他屬性尤為必要。
g <- graph_from_data_frame(edges, directed = FALSE, vertices = nodes)
我們現(xiàn)在創(chuàng)建一些數(shù)據(jù)結(jié)構(gòu),這些數(shù)據(jù)結(jié)構(gòu)將用于我們將要生成的所有的圖。首先,我們創(chuàng)建一個數(shù)據(jù)框來繪制邊。這個數(shù)據(jù)框?qū)⑴cedges數(shù)據(jù)框類似,但是有額外四列數(shù)據(jù)來定義每條邊的開始點和結(jié)束點(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))
現(xiàn)在我們給每個節(jié)點賦予一個權重,并使用等級作為指標。在地圖上這個指標表現(xiàn)為節(jié)點的大小。
nodes$weight = degree(g)
現(xiàn)在我們定義一個通用的ggplot2 的主題(在ggplot中設置及美化圖形的一個工具)來展示地圖 (無坐標軸和網(wǎng)格線):
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’)實現(xiàn)),采取同一個固定比例的坐標系來限定經(jīng)度和緯度。
country_shapes <- geom_polygon(aes(x = long, y = lat, groupgroup = 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)中的國家多邊形以外,我們還需創(chuàng)建三個幾何對象:使用geom_point將節(jié)點繪制為點,使用geom_text為節(jié)點添加標簽;使用geom_curve將節(jié)點之間的邊繪制成曲線。
在圖中,我們需要為每個幾何對象定義圖形屬性映射(aesthetic mappings,也稱為美學映射,用以“描述數(shù)據(jù)中的變量如何映射到視覺屬性”)。
圖形屬性映射鏈接:http://ggplot2.tidyverse.org/reference/aes.html
對于節(jié)點,我們將它們的地理坐標映射到圖中的x和y位置,并且由其權重所決定節(jié)點的大小(aes(x = lon,y = lat,size = weight))。對于邊,我們傳遞edges_for_plot數(shù)據(jù)框架并使用x, y 和 xend, yend 作為曲線的起點和終點。
此外,每條邊的顏色都取決于它的類別(category),而它的“尺寸”(指它的線寬)取決于邊的權重(一會兒我們會發(fā)現(xiàn)后面這一條沒有實現(xiàn))。
請注意,幾何對象的順序非常重要,因為它決定了哪個對象先被繪制,并可能會被隨后在下一個幾何對象層中繪制的對象所遮擋。因此,我們首先繪制邊,然后節(jié)點,***才是頂部的標簽:
ggplot(nodes) + country_shapes +
geom_curve(aes(xx = x, yy = y, xendxend = xend, yendyend = 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
這時候代碼界面中的控制臺中會顯示一條警告,提示“已顯示‘尺寸’標度,添加其他的標度‘尺寸‘將替換現(xiàn)有的標度?!边@是因為我們兩次使用了“尺寸”的圖形屬性及其標度,一次用于節(jié)點大小,一次用于曲線的寬度。
比較麻煩的是,我們不能在同一個圖形屬性上定義兩種不同的標度,即使這個圖形屬性要用于不同的幾何對象(比如在我們這個例子里:“尺寸”這個圖形屬性被同時用于節(jié)點的大小和邊的線寬)。據(jù)我所知在ggplot2中控制線寬只能通過“size“來實現(xiàn)。
使用ggplot2,我們只需決定要調(diào)整哪一個幾何對象的大小。此處,我選擇使用靜態(tài)節(jié)點大小和動態(tài)線寬:
ggplot(nodes) + country_shapes +
geom_curve(aes(xx = x, yy = y, xendxend = xend, yendyend = 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的擴展包,里面包含專門用于繪制網(wǎng)絡圖的幾何對象和圖形屬性。這樣我們就可以對節(jié)點和邊使用不同的標度了。默認情況下,ggraph將根據(jù)你指定的布局算法放置節(jié)點。但是我們還可以使用地理坐標作為節(jié)點位置來自定義布局:
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的圖形屬性及其標度函數(shù)scale_edge_width進行控制。節(jié)點則沿用之前的size來控制大小。另一個不錯的功能是,geom_node_text可以通過repel = TRUE 來分布節(jié)點標簽,這樣它們就不會互相遮擋太多。
請注意,圖的邊與之前ggplot2的圖采用了不同的繪制方式。由于ggraph采用了不同的布局算法,連接關系仍然相同,只是布局變了。例如,加拿大和日本之間的綠松石色邊線已經(jīng)從最北部轉(zhuǎn)移至南部,并穿過了非洲中心。
圖3:拙劣的方法(疊加數(shù)個ggplot2“plot grobs”)
我不想隱瞞另一個可能被認為是拙劣的方法:通過將它們標注為“grobs”(graphical objects的簡稱),你可以疊加幾個單***建的圖(透明背景)。這可能不是圖形對象標注功能本來的目的,但總之,當你真的需要克服上面圖1中所描述的ggplot2圖形屬性限制時,它隨時可以派上用場。
圖形對象標注鏈接:http://ggplot2.tidyverse.org/reference/annotation_custom.html
如上所述,我們將制作獨立的圖并“堆疊”它們。***個圖就是之前以世界地圖為“背景”的圖。第二個圖是一個只顯示邊的疊加層。***,第三個疊加層圖僅顯示帶有節(jié)點及其標簽的點。這樣設置后,我們便可以分別控制邊線的線寬和節(jié)點的大小,因為它們是在圖中各自單獨生成。
這兩次疊加需要有一個透明的背景,所以我們用一個主題來定義它:
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
現(xiàn)在,我們創(chuàng)建***個疊加層的邊,線寬的大小由邊的權重所決定:
p_edges <- ggplot(edges_for_plot) +
geom_curve(aes(xx = x, yy = y, xendxend = xend, yendyend = 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")
第二個疊加層顯示節(jié)點和標簽:
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
***,我們使用圖形對象標注組合疊加層。請注意,準確定位圖形對象的工作十分繁瑣。我發(fā)現(xiàn)使用ymin可以做得很好,但似乎必須手動調(diào)整參數(shù)。
p <- p_base +
annotation_custom(ggplotGrob(p_edges), ymin = -74) +
annotation_custom(ggplotGrob(p_nodes), ymin = -74)
print(p)
正如前面所述,這是一個拙劣的解決方案,應謹慎使用。但在有些情況下,它還是有用的。例如,當你需要在線圖中使用不同標度的點尺寸和線寬時,或者需要在單個繪圖中使用不同的色彩標度時,可以考慮采用這種方法。
總而言之,基于地圖的網(wǎng)絡圖對于顯示節(jié)點之間的地理尺度上的連接關系十分有用。缺點是,當有很多地理位置接近的點和許多重疊的連接時,它會看起來非常混亂。在僅顯示地圖的某些細節(jié),或者對邊的定位點添加一些抖動時,這種方法可能會很有用。
完整的R腳本可參閱github上的gist。
相關報道:??https://www.r-bloggers.com/three-ways-of-visualizing-a-graph-on-a-map/??
【本文是51CTO專欄機構(gòu)大數(shù)據(jù)文摘的原創(chuàng)譯文,微信公眾號“大數(shù)據(jù)文摘( id: BigDataDigest)”】