简介
好的数据分析都应该具有灵活性这一优点。如果数据发生变化,或者出现一些很不利于基本假设的信息时,这时候应该能够快速、便捷地更改之前地图形。重复地代码时灵活性地主要障碍。如果代码中很多都是重复、冗余的,那么当发生变化时就需要一处一处地去修改。这些繁杂地修改工作往往是很令人抓狂、沮丧地。通过展示如何用ggplot2编程,这一章将教导如何解决这些问题
为了让代码更具灵活性,需要通过编写函数来减少重复地代码。当发现自己在不断地做同样的事情的时候,就需要归纳相同的功能,把它写成一个函数。如果不是很熟悉R里面函数的运作方式
这一章会展示如何编写能创建以下对象的函数的方法
- 单个ggplot2组件
- 多个ggplot2组件
- 一幅完整的图像
单个组件
每一个ggplot2图像的组件都是一个对象。通常,创建完一个组件之后就会直接把它添加到图像里面,但是着并不是必要要求。相反,可以(通过命名)把任意组件保存成一个变量,然后把它添加到不同的图像当中
bestfit <- geom_smooth(
method = "lm",
se = FALSE,
color = alpha("steelblue", 0.5),
size = 2)
ggplot(mpg, aes(cty, hwy)) + geom_point() + bestfit
ggplot(mpg, aes(displ, hwy)) + geom_point() + bestfit
这是一种很好的减少简单代码重复的方法(比“复制粘贴”要好多了!),不过这种方法要求每次被添加的组件都完全一样。如果想要更加灵活,也可以把可复用的代码片段封装成一个函数。比如,可以把bestfit对象扩展成一个更普适的函数,新函数用于向图像添加拟合度最好的线条。以下代码创建了geom_lm()函数,这个函数有三个参数:模型formula、线条颜色color和线条粗细size
geom_lm <- function(formula = y ~ x,
color = alpha("steelblue", 0.5), size = 2, ...){
geom_smooth(formula = formula, se = FALSE,
method = "lm", color = color, size = size, ...)}
ggplot(mpg, aes(displ, 1 / hwy)) + geom_point() + geom_lm()
ggplot(mpg, aes(displ, 1 / hwy)) + geom_point() +
geom_lm(y ~ poly(x, 2), size = 1, color = "red")
请注意…参数。函数定义里面的…参数意味着这个函数可以接收不定数目的附加参数。在函数内部,可以使用…把那些不确定的附加参数传递到另外的函数里面。这里把…传到geom_smooth()里面,那么用户就依然可以修改没有显式覆盖到的参数。编写自己组件函数的时候,推荐这样使用…
最后,请注意,只可以向图像添加组件,而不可以修改或者移除已有的对象
多个组件
单个组件不一定总是足够的。幸运的是,ggplot2能够使用列表来方便地向图像一次性添加多个组件。以下函数会添加两个图层:一个用于展示平均值,另一个用于展开标准误
geom_mean <- function() {
list(stat_summary(fun.y = "mean", geom = "bar", fill = "grey70"),
if(se)
stat_summary(fun.data = "mean_cl_normal",
geom = "errorbar", width = 0.4))}
ggplot(mpg, aes(drv, cty)) + geom_mean()
ggplot(mpg, aes(drv, cty)) + geom_mean(se = FALSE)
1. 绘图组件
以上方法不仅仅可以用来添加图层,也可以在列表中添加以下类型的对象
- 一个数据框,它用于覆盖图像原有的数据集(如果只添加数据框,需要用到 %+%,但是当数据框在列表里面的时候,就不需要这个特别的运算符了)
- 一个aes()对象,它会和已有的默认图形属性结合起来
- 标度,它会覆盖已有的标度,如果用户已经设置了标度,添加新标度对象的时候会有警告
- 坐标系和分面设置,它们会覆盖已有的设置
- 主题组件,它会覆盖已有的相关组件
2. 注解
向图像添加标准注解通常是有好处。这里,函数也会在图层函数里面设定数据,而不是从图像中继承过来。这时,还需要设置另外两个选项,它们保证了图层是独立的
- inherit.aes = FALSE 阻止了图层从原图像中继承图形属性。这个设置保证了无论图像中还有什么东西注解都能正常工作
- show.legend = FALSE 保证了注解不会出现在图例区域里
ggplot2内置的borders()函数运用了这些技术。它用于向maps包里面的数据集添加地图边界
borders <- function(database = "world", regions = ".", fill = NA,
color = "grey50", ...){
df <- map_data(database, regions)
geom_polygon(
geom_(~lat, ~long, group = ~group),
data = df, fill = fill, color = color, ...,
inherit.aes = FALSE, show.legend = FALSE
)
}
3. 附加参数
使用…向函数内部组件传递附加参数并不是一个好的选择:它不能把不同的参数传递到不同的组件当中。相反,需要思考函数应该如何工作,把握好“拥有可以做所有事情的函数”和“拥有一个难以理解的复杂函数”之间的平衡
为了帮助上手,以下方法使用了modiyList()和do.call()
geom_mean <- function(...,
bar.params = list(), errorbar.params = list()) {
params <- list(...)
bar.params <- modifyList(params, errorbar.params)
bar <- do.call("stat_summary", modifyList(
list(fun.y = "mean", geom = "bar", fill = "grey70"),
bar.params))
errorbar <- do.call("stat_summary", modifyList(
list(fun.data = "mean_cl_normal",
geom = "errorbar", width = 0.4,
errorbar.params))
list(bar, errorbar)}
ggplot(mpg, aes(class, cty)) +
geom_mean(color = "steelblue",
errorbar.params = list(width = 0.5, size = 1))
ggplot(mpg, aes(class, cty)) +
geom_mean(bar.params = list(fill = "steelblue"),
errorbar.params = list(color = "blue"))
如果需要更复杂的行为,创建自定义的几何对象或统计变换或许更加容易。ggplot2包内置的说明Extending ggplot2介绍了对应的技术。可以通过运行vignette(“extending-ggplot2”)来阅读它
绘图函数
创建小型的可复用组件非常符合ggplot2的精神:灵活重组组件来绘制任意图像。但是某些时候只需要重复地创建相同地图像,而不需要那些灵活的性质。这样的话,也许不想编写组件,而是向编写一个接受数据和参数返回一个完整图像的函数
比如说,可以把所有相关代码都放到一个函数里面,这个函数用来绘制一幅饼图
piechart <- function(data, mapping){
ggplot(data, mapping) +
geom_bar(width = 1) +
coord_polar(theta = "y") +
xlab(NULL) + ylab(NULL)
}
piechart(mpg, aes(factor(1), fill = class))
和组合组件的方法相比,以上方法丧失不少灵活性,但是相应地,它获得了简洁性。需要注意的是,这里返回了绘图对象,而不是直接在函数里面把图像画出来。这样的话,可以向这个对下个添加其它的ggplot2组件
类似的方法可用于绘制平行坐标图(parallel coordnates plot, PCP)。平行坐标图要求对数据进行变换,因此简易编写两个函数:一个用于数据变换,另一个用于创建图像。如果想在不同的可视化任务中采取同样的变换方式,把这两个功能切分成两个函数能大大地简化以后的工作量
pcp_data <- function(df) {
is_numeric <- vapply(df, is.numeric, logical(1))
# 每一列的数值调整到相同的范围
rescale01 <- function(x){
rng <- range(x, na.rm = TRUE)
(x - rng[1]) / (rng[2] - rng[1])}
df[is_numeric] <- lappy(df[is_numeric], rescale01)
# 行名作为行识别信息
df$.row <- rownames(df)
# 把数值变量变成value(即所测量到的)变量
# gather_ 是gather函数的标准求值(standard-evaluation)版本,
# 一般来说更加容易使用
tidyr::gather_(df, "variable", "value", names(df)[is_numeric])}
pcp <- function(df, ...){
df <- pcp_data(df)
ggplot(df, aes(variable, value, group = .row)) + geom_line(...)}
pcp(mpg)
pcp(mpg, aes(color - drv))
qplot()完整地探索了这个思想,它对常见的ggplot()选项提供了相对深入的封装。可以通过学习qplot()的源代码,来观察这些基本的技术能够提供怎样深入的用途
1. 间接地引用变量
上述的piechart()函数有一点不好:它要求用户明确地知道如何使用aes()函数来创建饼图。如果只要用户指定所需的变量名,就可以把图像绘制出来的话,这就更加方便了。因而需要学习一些有关aes()如何运作的知识
aes()使用了非标准求值(non-standard evaluation):它关注参数的表达式,而不是参数的值。这就使得变成更加困难,因为没有办法存储对象里的变量名,接着在之后的代码引用它
x_var <- "displ"
aes(x_var)
#> * x -> x_var
相反,下面使用aes_():它使用常规的求值方式。用aes_()创建一个映射规则有两种基本方法:
- 使用quote()、substitute()、as.name()或parse()所创建的被引用调用(quoted call)
aes_(quote(displ))
#> * x -> displ
aes_(as.name(x_var))
#> * x -> displ
aes_(parse(text = x_var)[[1]])
#> * x -> displ
f <- function(x_var){
aes_(substitute(x_var))}
f(displ)
#> * x -> displ
as.name()和parse()有很细微的差异。如果x_var是”a + b“的话,as.name()会把它变成一个名为`a + b`的变量,而parse()会把它变成一个函数调用 a + b。(如果对此感到很异或,参考http://adv-r.had.co.nz/Expressions.html也许会有些帮助)
- 使用~所创建的公式
aes_(~ displ)
#> * x -> displ
对于用户提供变量的方法,aes_()给了三个选项:提供字符串、提供公式、提供纯表示式。以下是三个选项的用法
piechart1 <- function(data, var, ...){
piechart(data, aes_(~factor(1), fill = as.name(var)))}
piechart1(mpg, "class") + theme(legend.position = "none")
piechart2 <- function(data, var, ...){
piechart(data, aes_(~factor(1), fill = var))}
piechart2(mpg, ~class) + theme(legend.position = "none")
piechart3 <- function(data, var, ...){
piechart(data, aes_(~factor(1), fill = substitute(var)))}
piechart3(mpg, class) + theme(legend.position = "none")
如果在一个软件包中编写与ggplot2图像相关的函数,aes_()相对于aes()还有一个有点:使用aes_(~x, ~y)取代aes(x, y)后能避免R CMD check里出现的NOTE全局变量的事项
2. 绘图环境
随着创建越来越复杂的绘图函数,将要理解更多的ggplot2的作用域(scoping)规则。在还没有完全理解非标准求值的复杂之处之前,作者就开始创造ggplot2了,因此ggplot2有一个非常简单的作用域系统。如果在data里面找不到某个变量,就在所对应的那个绘图环境里面寻找它。一个图像只有一个环境(而不是一个图层有一个环境),就是调用ggplot()函数的那个环境(比如说,parent.frame())
这意味着以下函数不能运行,因为n没有被存储在aes()的环境当中
f <- function(){
n <- 10
geom_line(aes(x / n))
}
df <- data.frame(x = 1:3, y = 1:3)
ggplot(df, aes(x, y)) + f()
#> Error in x/n: 二进列运算符中有非数值参数
注意,这个问题只出现在mapping参数有关的代码当中。所有其它的参数都会立刻被求值,所以它们的值(而不是只想名字的引用)会被存储在绘图对象内部。这意味着以下代码是可以工作的
f <- function(){
color <- "blue"
geom_line(color = color)}
ggplot(df, aes(x, y)) + f()
如果需要不同的绘图环境,可以用ggplot()里的environment参数来指定它。在创建接受用户所提供的数据的绘图函数的时候,都需要这样做。qplot()函数可以作为一个例子
函数式编程
因为ggplot2对象知识普通的R对象,所以可以把它们放到列表当中。着意味着可以使用R里面所有的好用的函数式编程工具。比如说,如果想向同一个基础图像添加不同的几何对象,可以把这些几何对象放到列表当中,然后使用lappy()
geoms <- list(
geom_point(),
geom_boxplot(aes(group = cut_width(displ, 1))),
list(geom_point(), geom_smooth())
)
p <- ggplot(mpg, aes(displ, hwy))
lapply(geoms, function(g) p + g)
如果不是很熟悉函数式编程,可以阅读http://adv-r.had.co.nz/Functional-programming.html,然后思考一下如何使用函数式编程技术来优化重复的图像代码