厚缊

诹图——ggcor简介(十一)

厚缊 / 2020-03-04


ggcor的第一个公开版本以来,可能最不稳定的就是那个组合图的模块了,前面至少有两个完全不一样的版本:第一个版本我想啥都做好,用户就自己加上去就行了,但是参数多的让人想吐;第二个版本一开始我是很满意的,因为把原来的那一堆参数隐藏在了extra.params里面,假装add_link()参数很少,用的越多,越觉得这是个十分糟糕的设计。尽管觉得很糟糕,但是一直没有勇气来想想办法调整,打算将就着过得了。这个时候得感谢Y叔,Y叔说这个实现太不gg了,让我有了必须得找个更gg的实现方法的信念,这就是目前的版本,大体上也会是最后一个版本,至少现在我自己比较满意了(希望打脸不要来得更快)。

安装

新方式需要最新的0.9.4.1版,若是感觉没有用就不要强制更新了。暂时还未删除add_link()相关的内容,等这一轮测试调整完毕全部删除,以后只支持新玩法。

## install.packages("devtools)
devtools::install_github("houyunhuang/ggcor") 
## 安装过老版本的加 force = TRUE 参数
packageVersion("ggcor")
## [1] '0.9.4'

设计理念

从本质上来说,science组合图的连线部分无外乎就是点、线和标签三部分,原来之所以陷入了十分不gg不友好的设计陷阱里面,就是我太想一步到位,感觉这样更简单,显示的情况恰恰相反,一步到位就会在一个函数里面做更多的事情,更多的事情也就意味着更多的参数,更多的参数就是用户不友好。

在重构的过程中,我彻底放弃了一个函数大包大揽的设计,把数据处理过程,添加图形元素(包括点、线、标签)全部拆分成定制的零部件,这样需要什么找到对应的函数,加上去,最多调整一两个参数,就完美出图。我自己认为,这样的设计里面才能说是有点gg的味道吧。当然,目前的数据过程悬在外面还有点丑陋,以后可能也可以尝试一下更友好的快捷方式。

数据过程

说得俗气一点,要处理这样的图至少先要描个点,数据过程就是我尝试帮你描点的过程,点描好了接下来啥都好办,点描不好可能就什么都办不了。主要包括parallel_layout()combination_layout()两个函数(本来计划是有个circle_layout()的,但是想来想去觉得这个用网络图更快捷,暂时没必要了),正如名字看到的,parallel_layout()处理平行坐标的连接图,combination_layout()处理science的那个组合图。

parallel_layout()函数

看上去参数很多,其实常用的就是前三个,一个是指定数据,接下来的start.varend.var指定起点、终点的列名,默认情况下,起点是第一列、终点是第二列。起点、终点都是字符型,若不是字符型会强制转化。

library(ggcor)
args(parallel_layout)
## function (data, start.var = NULL, end.var = NULL, horiz = FALSE, 
##     stretch = TRUE, sort.start = NULL, sort.end = NULL, start.x = NULL, 
##     start.y = NULL, end.x = NULL, end.y = NULL) 
## NULL

平行坐标主要适用于起点、终点不一样的情况,先看个简单的例子。结果前四列是起点终点的坐标,5-6列是节点标签,其它部分后续用到再慢慢讲。

## 构造数据
m1 <- matrix(rnorm(15*10), nrow = 15, dimnames = list(NULL, paste0("mat", 1:10)))
m2 <- matrix(rnorm(15*12), nrow = 15, dimnames = list(NULL, paste0("matrix", 1:12)))
correlate(m1, m2) %>% 
  as_cor_tbl() %>% 
  parallel_layout()
## # A tibble: 120 x 13
##        x     y  xend  yend start.label end.label .start.filter .end.filter
##  * <dbl> <dbl> <dbl> <dbl> <chr>       <chr>     <lgl>         <lgl>      
##  1     0 12        1    12 mat1        matrix1   TRUE          TRUE       
##  2     0 10.8      1    12 mat2        matrix1   TRUE          FALSE      
##  3     0  9.56     1    12 mat3        matrix1   TRUE          FALSE      
##  4     0  8.33     1    12 mat4        matrix1   TRUE          FALSE      
##  5     0  7.11     1    12 mat5        matrix1   TRUE          FALSE      
##  6     0  5.89     1    12 mat6        matrix1   TRUE          FALSE      
##  7     0  4.67     1    12 mat7        matrix1   TRUE          FALSE      
##  8     0  3.44     1    12 mat8        matrix1   TRUE          FALSE      
##  9     0  2.22     1    12 mat9        matrix1   TRUE          FALSE      
## 10     0  1        1    12 mat10       matrix1   TRUE          FALSE      
## # … with 110 more rows, and 5 more variables: .row.names <chr>,
## #   .col.names <chr>, r <dbl>, .row.id <int>, .col.id <int>

现在点是描出来了,可是怎么把这么一份凌乱数据变成图呢?其实很容易了,有一组图层函数,做了很多本来需要你自己做的事情。有朋友跟我说,我绝对不是一个密集恐惧症患者,写的东西全是密集恐惧症慎入。

library(ggplot2)
correlate(m1, m2) %>% 
  as_cor_tbl() %>% 
  parallel_layout() %>% 
  ggplot() + 
  geom_link(aes(colour = r)) +
  geom_start_point(fill = "red", shape = 23, size = 4) +
  geom_end_point(fill = "blue", shape = 21, size = 4) +
  geom_start_label(aes(x = x - 0.05), hjust = 1, size = 5) +
  geom_end_label(aes(x = xend + 0.05), hjust = 0, size = 5) +
  coord_cartesian(xlim = c(-0.15, 1.15)) +
  theme_void()

我们也可以把上图放到,让节点水平排列。

correlate(m1, m2) %>% 
  as_cor_tbl() %>% 
  parallel_layout(horiz = TRUE) %>% 
  ggplot() + 
  geom_link(aes(colour = r)) +
  geom_start_point(fill = "red", shape = 23, size = 4) +
  geom_end_point(fill = "blue", shape = 21, size = 4) +
  geom_start_label(aes(y = y - 0.05), angle = 90, hjust = 1, size = 5) +
  geom_end_label(aes(y = yend + 0.05), angle = 270, hjust = 1, size = 5) +
  coord_cartesian(ylim = c(-0.2, 1.25)) +
  theme_void()

combination_layout()函数

必须得事先说明,我确实不知道那个science的组合图应该叫什么,导致我这几乎不会起名字,然后就暂时性随便用了一个。除了参数,整个函数的作用和parallel_layout()几乎一样。

args("combination_layout")
## function (data, type = NULL, show.diag = NULL, row.names = NULL, 
##     col.names = NULL, start.var = NULL, end.var = NULL, cor_tbl) 
## NULL

typeshow.diagrow.namescol.names都是相关系数矩阵相关的参数,若觉得这些有点多难得处理,完全可以传递一个cor_tbl对象给cor_tbl参数,这样所有的问题完美解决。

library(vegan) ## 获取数据
data("varespec")
data("varechem")
mantel <- mantel_test(varespec, varechem)
combination_layout(mantel, type = "upper", show.diag = FALSE,
                   col.names = names(varechem))
## # A tibble: 14 x 12
##        x     y  xend  yend start.label end.label .start.filter .end.filter
##  * <dbl> <dbl> <dbl> <int> <chr>       <chr>     <lgl>         <lgl>      
##  1  3.02   4.7    13     1 spec        N         TRUE          TRUE       
##  2  3.02   4.7    12     2 spec        P         FALSE         TRUE       
##  3  3.02   4.7    11     3 spec        K         FALSE         TRUE       
##  4  3.02   4.7    10     4 spec        Ca        FALSE         TRUE       
##  5  3.02   4.7     9     5 spec        Mg        FALSE         TRUE       
##  6  3.02   4.7     8     6 spec        S         FALSE         TRUE       
##  7  3.02   4.7     7     7 spec        Al        FALSE         TRUE       
##  8  3.02   4.7     6     8 spec        Fe        FALSE         TRUE       
##  9  3.02   4.7     5     9 spec        Mn        FALSE         TRUE       
## 10  3.02   4.7     4    10 spec        Zn        FALSE         TRUE       
## 11  3.02   4.7     3    11 spec        Mo        FALSE         TRUE       
## 12  3.02   4.7     2    12 spec        Baresoil  FALSE         TRUE       
## 13  3.02   4.7     1    13 spec        Humdepth  FALSE         TRUE       
## 14  3.02   4.7     0    14 spec        pH        FALSE         TRUE       
## # … with 4 more variables: spec <chr>, env <chr>, r <dbl>, p.value <dbl>

不手动处理其它繁琐的参数,发挥一下cor_tbl对象的魔力。

corr <- correlate(varechem, cor.test = TRUE) %>% 
  as_cor_tbl(type = "upper", show.diag = FALSE)
combination_layout(mantel, cor_tbl = corr)
## # A tibble: 14 x 12
##        x     y  xend  yend start.label end.label .start.filter .end.filter
##  * <dbl> <dbl> <dbl> <int> <chr>       <chr>     <lgl>         <lgl>      
##  1  3.02   4.7     0    14 spec        N         TRUE          TRUE       
##  2  3.02   4.7     1    13 spec        P         FALSE         TRUE       
##  3  3.02   4.7     2    12 spec        K         FALSE         TRUE       
##  4  3.02   4.7     3    11 spec        Ca        FALSE         TRUE       
##  5  3.02   4.7     4    10 spec        Mg        FALSE         TRUE       
##  6  3.02   4.7     5     9 spec        S         FALSE         TRUE       
##  7  3.02   4.7     6     8 spec        Al        FALSE         TRUE       
##  8  3.02   4.7     7     7 spec        Fe        FALSE         TRUE       
##  9  3.02   4.7     8     6 spec        Mn        FALSE         TRUE       
## 10  3.02   4.7     9     5 spec        Zn        FALSE         TRUE       
## 11  3.02   4.7    10     4 spec        Mo        FALSE         TRUE       
## 12  3.02   4.7    11     3 spec        Baresoil  FALSE         TRUE       
## 13  3.02   4.7    12     2 spec        Humdepth  FALSE         TRUE       
## 14  3.02   4.7    13     1 spec        pH        FALSE         TRUE       
## # … with 4 more variables: spec <chr>, env <chr>, r <dbl>, p.value <dbl>

只看数据当然并不能很好地看出这个函数做了哪些工作,看图吧。

combination_layout(mantel, cor_tbl = corr) %>% 
  ggplot() + 
  geom_link(aes(colour = r), curvature = 0.05) +
  geom_start_point(fill = "red", shape = 23, size = 4) +
  geom_end_point(fill = "blue", shape = 21, size = 4) +
  geom_start_label(aes(x = x - 0.5), hjust = 1, size = 5) +
  geom_end_label(aes(x = xend + 0.5), hjust = 0, size = 5) +
  coord_cartesian() +
  theme_void()

多个群落分组的情况是完全一样的,只要mantel检验的时候处理好多个分组就行。

mantel2 <- mantel_test(varespec, varechem, 
                       spec.select = list(Spec01 = 1:7,
                                          Spec02 = 8:18,
                                          Spec03 = 19:30))

combination_layout(mantel2, cor_tbl = corr) %>% 
  ggplot() + 
  geom_link(aes(colour = r), curvature = 0.05) +
  geom_start_point(fill = "red", shape = 23, size = 4) +
  geom_end_point(fill = "blue", shape = 21, size = 4) +
  geom_start_label(aes(x = x - 0.5), hjust = 1, size = 5) +
  geom_end_label(aes(x = xend + 0.5), hjust = 0, size = 5) +
  coord_cartesian(xlim = c(-5, 14)) +
  theme_void()

这里有个细节需要注意,对于这个图,默认的起点都是群落,终点是环境,当然完全可以通过start.varend.var参数来改变默认设置。结合下三角的情形我们看看怎么处理。注意:这个layout都是为science组合图设计的,所以你的终点(即环境)一定是和环境矩阵对应,这也就是为什么可以直接传入cor_tbl对象的原因,当不对应时,匹配失败,可能结果就只有几个孤立的点。

corr2 <- correlate(varechem, cor.test = TRUE) %>% 
  as_cor_tbl(type = "lower", show.diag = FALSE)
mantel2$env2 <- paste0(mantel2$spec, "_A")
combination_layout(mantel2, cor_tbl = corr2,
                   start.var = env2) %>% 
  ggplot() + 
  geom_link(aes(colour = r), curvature = 0.05) +
  geom_start_point(fill = "red", shape = 23, size = 4) +
  geom_end_point(fill = "blue", shape = 21, size = 4) +
  geom_start_label(aes(x = x + 0.5), hjust = 0, size = 5) +
  geom_end_label(aes(x = xend - 0.5), hjust = 1, size = 5) +
  coord_cartesian(xlim = c(0.5, 22)) +
  theme_void()

组合

这里可能是目前设计的逻辑里面,相对麻烦一点的,感觉是两幅不同的图要拼接在一起,但是对于传统的方方正正的图,拼接起来也就是放在AI里面拉一拉就行了,但是这个倾斜的三角,要拼起来可能还是得花一丢丢时间,这里就看个人喜好了,我觉得R可以解决,就用R整了,你觉得AI更好,那就用AI,无所谓优劣,能快速出图就行。

从本质上说,相关系数矩阵热图是根据cor_tbl对象画的,这个连接图是根据combination_layout()计算的坐标画的,那么我们就完全有办法把这几个对象结合起来,只不过有的图层需要单独指定数据。

方案一

保留原来的风格,相关系数矩阵热图用quickcor()函数解决,连接图添加额外的图层。有个小技巧:我们可以通过全局变量ggcor.link.inherit.aes来改变link相关的图层映射参数继承。

df <- combination_layout(mantel2, cor_tbl = corr) ## 这里需要存储下结果
options(ggcor.link.inherit.aes = FALSE)
quickcor(corr) + geom_square() +
  geom_link(aes(colour = r), data = df) + # 添加连接线
  geom_start_point(fill = "red", shape = 23, size = 4, data = df) +
  geom_end_point(fill = "blue", shape = 21, size = 4, data = df) +
  geom_start_label(aes(x = x - 0.5), hjust = 1, size = 5, data = df) +
  geom_end_label(aes(x = xend + 0.5), hjust = 0, size = 3.8, data = df) +
  expand_axis(x = c(-6, 14.5)) +
  remove_y_axis()

方案二

我们也可以先画连接图,然后把相关性矩阵热图叠加在上面。

ggplot(df) +
  geom_link(aes(colour = r)) + # 添加连接线
  geom_start_point(fill = "red", shape = 23, size = 4) +
  geom_end_point(fill = "blue", shape = 21, size = 4) +
  geom_start_label(aes(x = x - 0.5), hjust = 1, size = 5) +
  geom_end_label(aes(x = xend + 0.5), hjust = 0, size = 3.8) +
  geom_square(mapping = aes(x = .col.id, y = .row.id, r0 = r, fill = r),
              data = corr) +
  geom_panel_grid(data = corr) +
  scale_x_continuous(breaks = 2:14, labels = names(varechem)[-1], 
                     position = "top") +
  scale_fill_gradient2n() +
  coord_fixed(xlim = c(-6, 14.5), ylim = c(0.5, 14.5), expand = FALSE) +
  remove_y_axis() +
  theme_cor()

小结

这一期就讲到这里,下一期可以讲讲怎么控制更多的细节。