Shixiang Wang

>上士闻道
勤而行之

使用apply函数簇

王诗翔 · 2018-06-12

分类: r  
标签: r   apply  

在实际使用R时,for循环往往是最后的选择。一般每次循环计算都是独立的,所以我们可以使用更简洁更方便的读写方式来实现同样的效果。

举例,如果使用for循环创建一个列表,包含3个相互独立、服从正态分布的随机向量,其长度由len指定:

len <- c(3, 4, 5)
x <- list()
for (i in 1:3){
    x[[i]] <- rnorm(len[i])
}

代码挺简单的,但能不能更简单?我们使用lapply()

lapply(len, rnorm)
#> [[1]]
#> [1]  0.824 -1.987 -0.225
#> 
#> [[2]]
#> [1] -0.755  0.323  0.149  0.140
#> 
#> [[3]]
#> [1] -0.961 -0.305  0.450 -1.108  0.179

apply函数簇中的每个函数都称为高阶函数,高阶函数是以函数为输入的函数

lapply

lapply()接收一个向量和一个函数作为输入参数,它将这个函数应用到向量中的每一个元素,再将结果以列表的形式返回。 这类函数的好处是我们不需要构建一个显示的迭代器来明确迭代步骤的进行。

lapply()不仅适用于向量,也适用于列表。

假设我们有一份学生列表:

students <- list(
    a1 = list(name = "James", age = 25, 
              gender = "M", interest = c("reading", "writing")),
    a2 = list(name = "Jenny", age = 23,
              gender = "F", interest = c("cooking")),
    a3 = list(name = "David", age = 24,
              gender = "M", interest = c("running", "basketball"))
)

现在我们想创建一个字符向量,其中每个元素都由如下形式:

James, 25 year-old man, loves reading, writing.

函数sprintf()通过将占位符替换为相应的输入参数来格式化文本(取自C)。举例:

sprintf("Hello, %s! Your number is %d.", "Tom", 3)
#> [1] "Hello, Tom! Your number is 3."

返回我们的问题,我们使用lapply()解决:

lapply(students, function(s){
    type <- switch(s$gender, "M" = "man", "F" = "woman")
    interest <- paste(s$interest, collapse = ", ")
    sprintf("%s, %d year-old %s, loves %s.", s$name, s$age, type, interest)
})
#> $a1
#> [1] "James, 25 year-old man, loves reading, writing."
#> 
#> $a2
#> [1] "Jenny, 23 year-old woman, loves cooking."
#> 
#> $a3
#> [1] "David, 24 year-old man, loves running, basketball."

sapply

列表并非总是最佳的数据存储容器,有时候我们希望将结果存放在向量或矩阵中,sapply()可以根据结果的结构进行合理简化。

比如,我们如果将平方运算应用到1:10的每个元素,使用lapply()会得到含10个元素的列表,这非常不直观,也常常不是我们想要的,而sapply()可以将其简化为一个向量。

sapply(1:10, function(x) x ^ 2)
#>  [1]   1   4   9  16  25  36  49  64  81 100

如果函数每次循环返回一个多元素的向量,sapply()会将结果存储在一个矩阵中,矩阵的每一列是每次循环产生的向量:

sapply(1:10, function(x) c(x, x^2))
#>      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
#> [1,]    1    2    3    4    5    6    7    8    9    10
#> [2,]    1    4    9   16   25   36   49   64   81   100

vapply

使用sapply有时候会暗藏风险,假设我们有列表

x <- list(c(1, 2), c(2, 3), c(1, 3))

我们想得到一个向量,其中每个元素都是x元素对应数字的平方,那么sapply()就比较适用。

sapply(x, function(x) x ^ 2)
#>      [,1] [,2] [,3]
#> [1,]    1    4    1
#> [2,]    4    9    9

如果输入数据有错误,可能会返回意料之外的结果:

x1 <- list(c(1, 2), c(2, 3), c(1, 3, 3))
sapply(x1, function(x) x^2)
#> [[1]]
#> [1] 1 4
#> 
#> [[2]]
#> [1] 4 9
#> 
#> [[3]]
#> [1] 1 9 9

但如果我们使用vapply(),我们可以设定返回值的模板,以验证结果形式上是不是出问题了。

vapply(x1, function(x) x^2, numeric(2))
#> Error in vapply(x1, function(x) x^2, numeric(2)): values must be length 2,
#>  but FUN(X[[3]]) result is length 3

而对于正确的输入,vapply()sapply()结果一致。

vapply(x, function(x) x^2, numeric(2))
#>      [,1] [,2] [,3]
#> [1,]    1    4    1
#> [2,]    4    9    9

到此,我们已经知道vapply其实就是sapply的安全升级版本。

mapply

相比lapply()sapply()在一个向量上迭代,mapply()可以在多个向量上进行迭代。mapplysapply的多元版本。

mapply(function(a, b, c) a * b + b * c + a * c, 
       a = c(1, 2, 3), b = c(5, 6, 7), c = c(-1, -2 , -3))
#> [1] -1 -4 -9

迭代函数可以返回标量,也可以返回多元素向量。

Map()lapply()的多元版本,它通常返回列表。

Map(function(a, b, c) a * b + b * c + a * c, 
       a = c(1, 2, 3), b = c(5, 6, 7), c = c(-1, -2 , -3))
#> [[1]]
#> [1] -1
#> 
#> [[2]]
#> [1] -4
#> 
#> [[3]]
#> [1] -9

最后,apply作为最常用的函数不在此讲述了,之前的笔记有几篇详细讲过这方面。需要注意的是,在使用apply函数对数据框进行计算时,是不能提取行/列名的。

mat <- data.frame(a = c(1, 2, 3), b = c(5, 6, 7), c = c(-1, -2 , -3))
rownames(mat) <- c("r1", "r2", "r3")
mat
#>    a b  c
#> r1 1 5 -1
#> r2 2 6 -2
#> r3 3 7 -3
apply(mat, 1, function(x) rownames(x))
#> NULL
apply(mat, 1, function(x) colnames(x))
#> NULL