R语言原生管道绘图

前言

最近写论文的时候又一次用到了R。这次我是对Java有一定程度了解后再次转向R,才真正认识到R这门语言在统计编程和数据可视化领域的优雅和快速。

首先可以看一段Java的stream代码:

redisUtils.opsForHashValues(Const.COOP_PREFIX.getInfo() + msg.getBlogId(), msg.getFromId().toString()).stream().
                map(userStr -> redisUtils.readValue(userStr, UserEntityVo.class)).
                filter(user -> !fromId.equals(user.getId())).
                forEach(user -> {
                    msg.setToOne(user.getId());
                    rabbitTemplate.convertAndSend(
                            CoopRabbitConfig.WS_TOPIC_EXCHANGE,
                            CoopRabbitConfig.WS_BINDING_KEY + user.getServerMark(),
                            msg);
                });

这段代码大概就是将缓存的数据读取出来,反序列化成Java的对象,然后进行筛选,然后把筛选出的每个对象做一次消息的发送。在这里"->"这个箭头就是Java的lambda表达式,它支持了Java的函数式编程。这和R有什么关系呢?

函数式编程往往是和链式调用结合的,能让代码可读性得到增强。在些R的画图案例之前,先执行这段R代码:

c('tidyverse', 'stargazer', 'plm', 'sandwich', 'lmtest', 'ggpubr', 'showtext', 'rticles', 
'maps', 'see','bookdown') |> 
  lapply(\(pkg) {
    if (system.file(package = pkg) == '') {
      install.packages(pkg)
    }
    library(pkg, character.only = TRUE)
  })

这里把lapply()这个base R的函数换成purrr::map()也是可以的(我发现Hadley Wickham似乎有某种代码洁癖喜欢重写很多本来可以将就用的api)。这段逻辑也很简单,构造一个数组,数组里是一些R的常用包,对于这些包依次迭代(依次就是lapply函数的作用),对于已经安装的就加载,没有安装的就安装后加载。这里的"|>"就是R官方native的管道符号,是最近R 4.1.0更新加入的。通过"|>"可以将数组传入到lapply函数的第一个参数位置,第一个参数由此就可以省略了。lapply函数第二个参数是一个FUN,也就是一个函数,这里直接传入了一个匿名函数,去判断系统安装这个包没。从这里也能看出R和Java的异同:Java不管是任何方法都不可能直接将一个方法作为方法的参数,只能通过匿名实现类这种方式模拟出函数式编程的效果,而R这种函数式语言当然是可以的,现在的新语言比如Golang也是可以直接往函数里传一个函数的。尽管在R4.1.0以前,"|>"一般是被magrittr这个第三方库使用"%>%"去实现,但是我相信大多数人写这段代码时都会写成:

lapply(c('tidyverse', 'stargazer', 'plm', 'sandwich', 'lmtest', 'ggpubr', 'showtext', 'rticles', 'maps', 'see','bookdown'), function(pkg) {
  if (system.file(package = pkg) == '') {
    install.packages(pkg)
  }
  library(pkg, character.only = TRUE)
  showtext_auto()
})

从上面这段代码看不到任何函数式编程的感觉,或者说没有了管道符号的加持代码显得不美。不过这也是没办法的事情,因为R原生就不支持lambda风格的管道符,但是为了写一些基于base R的代码就把magrittr这个包导进来显得很狼狈。可以说R 4.1.0推出的"|>"符号会大幅度优美化这个语言。

绘图代码

使用R绘图,如果不出意外应该还是基于ggplot2去作图,除了初学者以外现在国际主流应该是不推荐使用诸如plot(data, x, y)这种函数了。在绘图前一般需要对数据进行处理,基于函数式编程当然是使用dplyr。

直接画图

例如我需要画一幅折线图,其中这张图有三条折线,三条折线都是来源与这张表的三个变量,需要三个变量随时间的变化趋势:

表格类似于这个样子:

Year HighTech HighLab HighCap
1998 48.37 32.96 29.26
1999 51.84 31.74 32.06
2000 55.57 29.61 36.55
2001 55.82 30.59 36.91

代码为:

read_csv("data/tech-lab.csv") |>
  ggplot() +
    geom_line(aes(Year, HighTech / 100, col = "高技术人力密集度")) +
    geom_line(aes(Year, HighLab / 100, col = "高劳力密集度")) +
    geom_line(aes(Year, HighCap / 100, col = "高资本密集度")) +
    ylab("百分位点") +
    xlab("年份") +
    scale_y_continuous(labels = scales::percent) +
    scale_x_continuous(breaks = seq(1998, 2021, 3)) +
    labs(col = "类型")

这里我将"%>%"替换成了"|>",这两者当然是有区别的,但是现在不推荐写"%>%"了。实际上purrr官网已经不推荐写"%>%"管道符,它注定会逐渐退出历史舞台。首先通过read_csv()将磁盘的文件读入内存,然后用"|>"传递给ggplot()的第一个参数,所以ggplot()也就可以空参了。每一个geom_line()函数其实就是一个图层,熟悉ps应该了解这个概念。三个geom_line()其实就是三个图层叠加在一起。aes()中需要指定x轴和y轴放什么变量,col其实是colour的缩写(也可以是color的缩写),ylab和xlab指定了横坐标和纵坐标,scale_y_continuous和scale_x_continuous指定y轴和x轴显示规则,最终生成的图片如下:

图1

数据处理后画图

当然很多时候我们可能希望玩的更优雅一些,能不能先加工一波数据然后再画图,其实也是一样的道理:

State Export Inport FDI Year
A国 48.37 32.96 29.26 1999
B国 51.84 31.74 32.06 2000
C国 55.57 29.61 36.55 2001
D国 55.82 30.59 36.91 2002
data |> 
  filter(State %in% ASEAN, !is.na(Export), !is.na(Inport), !is.na(FDI)) |> 
  select(State, Export, Inport, FDI, Year) |>  
  group_by(Year) |>  
  mutate(expSum = sum(Export), inpSum = sum(Inport), fdiSum = sum(FDI)) |> 
  ggplot() +
    geom_line(aes(Year, expSum, col = '进口')) +
    geom_line(aes(Year, inpSum, col = '出口')) +
    geom_line(aes(Year, fdiSum, col = '投资')) +
    ylab("数额(千美元)") +
    xlab("年份") +
    scale_x_continuous(breaks = seq(1981, 2019, 3)) +
    labs(col = "地区")

这里希望画一幅东协国家进口、出口和投资的数额随年份变化的时间序列图。于是首先将数据data使用"|>"传入到filter()的第一个参数,后面的参数就指定筛选规则:国家在东协且三个核心变量不为缺失值。然后选取State, Export, Inport, FDI, Year这几列,其他列在这幅图不考虑,然后根据年份group_by()分组,然后使用mutate()将每年的总进口、出口、和投资生成三个新的变量列。这一步做完,其实新列每个国家都是相同的,理论上我们只需要挑选出东协任意国再进行绘图。但是ggplot不需要显式处理这步,它自己会帮你做了。接下来将数据传下去让ggplot()绘图。这里看出每一步都使用"|>"的效果就是每一个函数的第一个参数都不需要写,非常的优雅。图片如下:

图2

画个地图

有时候也希望玩一些高级操作,比如画个地图。画地图有个坑就是地图本质是依赖经纬度数据,一定要注意这个地图的经纬度数据有没有问题(一些众所周知的担忧)。

world <- map_data("world") |> 
  filter(region != "Antarctica")
data1993 <- data |> 
  filter(Year == 1993, !is.na(Export)) |> 
  select(State, Export, Year) |> 
  mutate(百分比 = (Export / sum(Export)) * 100)

data2016 <- data |> 
  filter(Year == 2016, !is.na(Export)) |> 
  select(State, Export, Year) |> 
  mutate(百分比 = (Export / sum(Export)) * 100)

data2016 <- data2016 |> 
  filter(data2016$State %in% data1993$State)

data1993 <- data1993 |> 
  filter(data1993$State %in% data2016$State)

suppressMessages(bind_cols(data1993$State, data2016$百分比 - data1993$百分比)) |> 
  rename(地区 = ...1, 百分比变动 = ...2)  |> 
  ggplot(aes(map_id = 地区)) +
    geom_map(aes(fill = 百分比变动), map = world) +
    scale_fill_distiller(palette = "Set2",direction= 1) + 
    expand_limits(x = world$long, y = world$lat) +
    xlab("经度") +
    ylab("纬度")

这里做的并不像之前的图那么优雅,因为这个图的数据复杂一些。map_data("world")这个数据对于中国边界的经纬度数据是由一些问题的,如果要出版的话要尤为注意:

图3

总结

由此,在R 4.1.0推出原生管道符以后,R的语法实际上已经无懈可击了。正如GraalVM也支持R一样可以看出顶级开发者对R的重视。R若仅仅是画图功能也不显得出众,结合R Markdown以后才是极大提升了生产力。我比较喜欢这种布局,将各个章节分散在不同的.rmd文件中:

---
title: ''
author:
  - C
header-includes:
  - \usepackage{lscape}
  - \usepackage{ctex}
papersize: 'a4'
geometry: 'margin=1.75in'
keywords:
  - A
  - B
indent: true  
output:
  bookdown::pdf_document2:
    latex_engine: xelatex
    fig_caption: yes
    number_sections: yes
    toc_depth: 2
    toc: yes
bibliography: bibliography.bib
--- 

```{r, include=FALSE}
c('tidyverse', 
  'stargazer', 
  'plm', 
  'sandwich', 
  'lmtest', 
  'ggpubr', 
  'showtext', 
  'rticles', 
  'maps', 
  'see',
  'bookdown') |> 
  lapply(\(pkg) {
    if (system.file(package = pkg) == '') {
      install.packages(pkg)
    }
    library(pkg, character.only = TRUE)
  })
showtext_auto()
data <- read_csv('data/data.csv')
ASEAN <- c('Malaysia', 'Indonesia', 'Thailand', 'Philippines', 'Singapore', 'Vietnam', 'Brunei', 'Laos', 'Myanmar', 'Cambodia')
\``` 

```{r child = ' 00-abstract.Rmd '}
\```

\newpage
```{r child = ' 01-intro.Rmd '}
\```

\newpage
```{r child = ' 02-review.Rmd '}
\```

\newpage
# 参考文献 

posted @ 2023-02-14 01:09  imissinstagram  Views(83)  Comments(0Edit  收藏  举报