R语言中的apply系列函数功能强大, 能够有效的减少显式循环. 本文介绍其中的一部分.
apply, lapply, sapply, tapply, vapply, mapply
apply
apply函数是最该系列中最基础的一个, 使用方法:
- apply(X, MARGIN, FUN, …)
- 输入X为数组和矩阵, 平时矩阵使用地更频繁一些, 也更容易理解
- MARGIN, 可以等于 1 或者 2, 分别指代对行做操作, 还是对列做操作
- FUN, 对X的行或者列做什么操作, 可以调用R自带函数, 也可以自己编写函数
- FUN如果返回单个数字(这也是最常见的), 则apply返回的是一个向量
- FUN如果返回一个长度为n的向量, 则apply返回的是一个矩阵
这里需要先说明一下: apply系列函数中一般都会含有一个FUN参数, 根据实际问题不同, 这个FUN函数可以很复杂, 因此一般是自己编写的函数, 而应该传递给该FUN函数的参数, 就放到后面的 “…” 中(之后在使用方法中不再列出”…”的说明). 接下来通过实例可以看出如何使用FUN参数来实现自己的目标.
以下生成了具有30行2列的一个矩阵x, 可以认为是30个人的身高(单位cm)和体重(单位kg)数据, 我们想求这30个人的平均身高和平均体重, 那么就要对每一列求均值, 这就用到了apply函数.
1 2 3
| set.seed(1234) x <- cbind(height = rnorm(30, 170, 10), weight = rnorm(30, 60, 5)) class(x)
|
1 2 3 4 5 6 7
| ## height weight ## [1,] 157.9293 65.51149 ## [2,] 172.7743 57.62203 ## [3,] 180.8444 56.45280 ## [4,] 146.5430 57.49371 ## [5,] 174.2912 51.85453 ## [6,] 175.0606 54.16190
|
1
| apply(x, MARGIN = 2, mean)
|
1 2
| ## height weight ## 167.03575 57.24191
|
如果想求每个人的BMI指数(体重(kg)除以身高(m)的平方, 即$kg/m^2$), 你会发现R中并无直接可以调用的函数, 于是需要自己编写自定义函数并赋给apply的FUN参数了
1 2 3 4 5 6 7
| bmi <- function(data){ height <- data[1]/100 weight <- data[2] return(weight/(height^2)) } bmindex <- apply(x, MARGIN = 1, FUN = bmi) length(bmindex)
|
1
| ## [1] 26.26587 19.30325 17.26137 26.77257 17.07008 17.67329
|
我们也可以把计算出的BMI指数赋给矩阵的第三列.
在刚才的例子中, 编写的bmi函数只需要传进来一个长度为 2 的向量(分别为身高和体重数据)即可, 而这正好对应了矩阵的一行数据. 因此并不需要更多参数就能完成BMI的计算.
如果除了BMI之外, 任务中还要计算其他量, 则可以一起写进函数中, 并传递给apply函数的FUN参数, 只不过此时apply函数也会返回一个矩阵了. 由于此种用法不多, 我们就不再赘述, 有需要的请看帮助文件.
lapply 和 sapply 以及 vapply
这三个放到一起说, 是因为其本质功能是类似的, 只不过稍有差别.
下面按序介绍
lapply
- lapply(X, FUN, …)
- 输入X为列表或向量, 但是大多数情况是列表
- FUN, 当X是列表时, 对X的每个元素做什么操作. 可以调用R自带函数, 也可以自己编写函数
- lapply返回值仍然是一个列表, 且元素个数和X相等
这里注意一点: 使用列表作为数据结构来存储数据, 本来就是因为数据具有异质性且可能长度不同(普通的异质性如果长度相同的话可以使用数据框, 如姓名, 性别, 身高等等), 那么lapply的FUN参数就要放一个能够对长度不同的异质性数据都可操作的函数. 这样的函数在R里自带的并不多, 大都需要自己编写.
先举一个帮助文件里的例子:
1 2
| xl <- list(a = 1:10, beta = exp(-3:3), logic = c(TRUE,FALSE,FALSE,TRUE)) xl
|
1 2 3 4 5 6 7 8 9
| ## $a ## [1] 1 2 3 4 5 6 7 8 9 10 ## ## $beta ## [1] 0.04978707 0.13533528 0.36787944 1.00000000 2.71828183 7.38905610 ## [7] 20.08553692 ## ## $logic ## [1] TRUE FALSE FALSE TRUE
|
1 2 3 4 5 6 7 8
| ## $a ## [1] 5.5 ## ## $beta ## [1] 4.535125 ## ## $logic ## [1] 0.5
|
由上述代码运行结果可以看出, xl是一个拥有三个元素的列表, 每个元素都是长度不同(分别是10, 7, 4)且异质的数据(分别为integer, numeric, logical). 上述论断其实也可以通过lapply来得出结果. 求xl的每个元素的长度, 相当于对列表的每个元素运行length函数(如果直接运行length(xl), 你一定不会得到想要的结果)
1 2 3 4 5 6 7 8
| ## $a ## [1] 10 ## ## $beta ## [1] 7 ## ## $logic ## [1] 4
|
想要知道xl的每个元素的数据类型, 相当于对列表的每个元素运行class函数
1 2 3 4 5 6 7 8
| ## $a ## [1] "integer" ## ## $beta ## [1] "numeric" ## ## $logic ## [1] "logical"
|
最后, 我们在例子中使用的是: 对列表的每个元素运行mean函数. 这样做可以得到正确的结果是因为, integer, numeric, logical 这三种数据类型, 本质上还是数字(logical存储为0和1), 是可以进行求均值的操作的. 正是由于均为数字, 因此求分位数也是可以的. 下面我们再引用一个帮助文档中的例子, 重点说明参数列表中的”…”如何使用:
1
| lapply(xl, quantile, probs = 1:3/4)
|
1 2 3 4 5 6 7 8 9 10 11
| ## $a ## 25% 50% 75% ## 3.25 5.50 7.75 ## ## $beta ## 25% 50% 75% ## 0.2516074 1.0000000 5.0536690 ## ## $logic ## 25% 50% 75% ## 0.0 0.5 1.0
|
注意到probs参数在lapply的调用中, 就是处于”…”的位置, 因此probs参数事实上是传递给quantile的. 那么这句代码做的事情就可以翻译为: 对列表xl的每一个元素, 计算其下四分位数, 中位数, 上四分位数.
如果”…”这里需要传递进来的参数不止一个, 那么只要用逗号隔开即可. 这里放多少个参数都无所谓, 最后都是传给FUN的.
sapply
sapply和lapply的关系有点类似于read.csv和read.table的关系.
read.csv只是read.table把某些参数固定后的定制版. sapply的功能也是基于lapply修改而成的.
- 使用方法: sapply(X, FUN, …, simplify = TRUE, USE.NAMES = TRUE)
- 使用情形: 当lapply对每个元素的计算结果均为长度相等的向量时, 我们想要把这些元素以更加整齐的方式组织起来
- simplify = TRUE, 即默认把这些计算结果组织成向量(当每个元素的计算结果长度为1时)或矩阵(当每个元素的计算结果长度大于1时)
- USE.NAMES = TRUE, 用处不是很大, 可以暂时不用管
为了说明sapply的使用情形和方法, 并与lapply函数作比较, 我们仍然沿用上面lapply中的例子
1 2 3 4 5 6 7 8 9
| ## $a ## [1] 1 2 3 4 5 6 7 8 9 10 ## ## $beta ## [1] 0.04978707 0.13533528 0.36787944 1.00000000 2.71828183 7.38905610 ## [7] 20.08553692 ## ## $logic ## [1] TRUE FALSE FALSE TRUE
|
1
| lapply(xl, quantile, probs = 1:3/4)
|
1 2 3 4 5 6 7 8 9 10 11
| ## $a ## 25% 50% 75% ## 3.25 5.50 7.75 ## ## $beta ## 25% 50% 75% ## 0.2516074 1.0000000 5.0536690 ## ## $logic ## 25% 50% 75% ## 0.0 0.5 1.0
|
1
| sapply(xl, quantile, probs = 1:3/4)
|
1 2 3 4
| ## a beta logic ## 25% 3.25 0.2516074 0.0 ## 50% 5.50 1.0000000 0.5 ## 75% 7.75 5.0536690 1.0
|
注意到lapply函数中的quantile对列表xl的每个元素都会得到长度为3的计算结果(分别是三个分位数). 由于计算结果长度相等, 所以我们可以把它们以更加整齐的方式组织起来(在这里, 是以一个矩阵的方式), 于是使用sapply会得到同样的计算结果, 但是格式上更加”user-friendly”(按帮助文档的叫法).
事实上, 如果是普通的统计函数的话, 函数返回值经常会和输入数据长度没任何关系. 比如mean函数, 无论传进来的向量多长, 返回的总是一个数. 上面举例的quantile函数也是如此. 那么这种计算结果, 就可以被整理成整齐的格式, 也就是说, 我们可以使用sapply去处理. 反之, 如果一个函数的输入长度与计算结果相关, 那么还得使用lapply(经过测试, 此时使用sapply也没用, 系统还是会调用lapply的). 我们可以通过一个简单的例子来说明:
1 2 3 4 5
| difflength <- list(a = 1:2, b = 1:3) calc.seq <- function(data){ seq(min(data), max(data), by = 0.5) } lapply(difflength, calc.seq)
|
1 2 3 4 5
| ## $a ## [1] 1.0 1.5 2.0 ## ## $b ## [1] 1.0 1.5 2.0 2.5 3.0
|
1
| sapply(difflength, calc.seq)
|
1 2 3 4 5
| ## $a ## [1] 1.0 1.5 2.0 ## ## $b ## [1] 1.0 1.5 2.0 2.5 3.0
|
上述代码可以看到, sapply并没有返回一个矩阵, 这是因为对于difflength的不同元素, calc.seq的计算结果是不同的, 所以sapply仍然只能作为列表返回, 那么这就和lapply是相同的功能了.
不管怎么说, 能用sapply的地方还是用sapply, 能返回向量和矩阵的, 就不要返回列表.
vapply
- 使用方法: vapply(X, FUN, FUN.VALUE, …, USE.NAMES = TRUE)
- 和sapply的相同之处: 仍然是对列表每个元素调用FUN, 并返回一个向量和矩阵
- 和sapply的不用之处: 返回矩阵时, 给每个元素起好了名字, 于是矩阵就有了行名
vapply的用处并不是很大, 于是直接拿帮助文档的例子展示一下即可:
1 2
| i39 <- sapply(3:9, seq) sapply(i39, fivenum)
|
1 2 3 4 5 6
| ## [,1] [,2] [,3] [,4] [,5] [,6] [,7] ## [1,] 1.0 1.0 1 1.0 1.0 1.0 1 ## [2,] 1.5 1.5 2 2.0 2.5 2.5 3 ## [3,] 2.0 2.5 3 3.5 4.0 4.5 5 ## [4,] 2.5 3.5 4 5.0 5.5 6.5 7 ## [5,] 3.0 4.0 5 6.0 7.0 8.0 9
|
1 2
| vapply(i39, fivenum, c(Min. = 0, "1st Qu." = 0, Median = 0, "3rd Qu." = 0, Max. = 0))
|
1 2 3 4 5 6
| ## [,1] [,2] [,3] [,4] [,5] [,6] [,7] ## Min. 1.0 1.0 1 1.0 1.0 1.0 1 ## 1st Qu. 1.5 1.5 2 2.0 2.5 2.5 3 ## Median 2.0 2.5 3 3.5 4.0 4.5 5 ## 3rd Qu. 2.5 3.5 4 5.0 5.5 6.5 7 ## Max. 3.0 4.0 5 6.0 7.0 8.0 9
|
以上代码可以看出, vapply和sapply一样, 都返回了一个矩阵, 只不过sapply的矩阵没有行名, 而vapply专门指定了行名. 其实给矩阵的行命名是一件容易的事情, 所以没有必要为此多记一个函数(apply系列函数实在太多, 区分是很困难的, 能少记一个就少记一个吧). 另外, 上面的i39变量的赋值正好是sapply对向量(通常都是对列表)的隐式循环调用, 可以从中看到sapply的第一个参数为向量时, 是如何计算的.
mapply
使用方法:
mapply(FUN, …, MoreArgs = NULL, SIMPLIFY = TRUE,
USE.NAMES = TRUE)
mapply是sapply的多维版本. 什么叫做多维版本呢? 回顾一下上一节的最后, sapply的第一个参数为向量的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ## [[1]] ## [1] 1 2 3 ## ## [[2]] ## [1] 1 2 3 4 ## ## [[3]] ## [1] 1 2 3 4 5 ## ## [[4]] ## [1] 1 2 3 4 5 6 ## ## [[5]] ## [1] 1 2 3 4 5 6 7 ## ## [[6]] ## [1] 1 2 3 4 5 6 7 8 ## ## [[7]] ## [1] 1 2 3 4 5 6 7 8 9
|
我们可以认为上一行代码是将seq循环调用到3至9这几个数字上. 3:9的这个向量的每一个分量, 其实就是作为seq的”一个”参数依次传进来, 此时传进来的参数维度为 1. 如果给seq传进来两个参数, 并且这两个参数都是依照不同向量的分量依次传进来呢? 我们就需要R语言中的循环法则了, 这个时候传进来的参数维度就是多维的了.
1
| mapply(seq,1:3,c(4,6,5))
|
1 2 3 4 5 6 7 8
| ## [[1]] ## [1] 1 2 3 4 ## ## [[2]] ## [1] 2 3 4 5 6 ## ## [[3]] ## [1] 3 4 5
|
如上例所示, mapply调用的函数是序列生成函数seq, 后面的1:3, c(4,6,5)均是传到seq的参数, 这就可以看做是sapply的二维版本.
在参数传递的过程中, 特别需要注意的是, 1:3和c(4,6,5)的长度都是3, 是相等的, 那么按照R的循环法则, 此时两个向量的第一个分量同时传入seq, 那么函数执行的是
接下来循环到第二个和第三个分量时, 函数又分别调用了两次, 执行的是
最后mapply返回的是一个列表(一般情况下循环调用了FUN, 可以认为每次返回值不必都相等). 在特殊情况下, 由于默认的 SIMPLIFY = TRUE, 那么返回值是可以尽可能的合并成一个格式整齐的矩阵的. 比如
1 2 3 4 5
| ## [,1] [,2] [,3] ## [1,] 1 2 3 ## [2,] 2 3 4 ## [3,] 3 4 5 ## [4,] 4 5 6
|
综上, mapply使用的场景主要为, 我们要对某个具有多个参数的函数进行不同参数取值的多次调用. 其余可以参见帮助文档.
tapply
使用方法:
tapply(X, INDEX, FUN = NULL, …, simplify = TRUE)
- 函数形式 tapply(x,INDEX,FUN=f)
- 其中 x 是向量 , INDEX 是一个因子 , 且需要和 x 的长度一样
- 将函数 f, 按照 INDEX 的分类的角标 , 对 x 操作
1 2
| fac <- factor(rep(1:3, 4), levels = 1:5) fac
|
1 2
| ## [1] 1 2 3 1 2 3 1 2 3 1 2 3 ## Levels: 1 2 3 4 5
|
1 2
| ## 1 2 3 4 5 ## 22 26 30 NA NA
|
如上, fac变量为一个因子, 并与1:12的长度相等, 都为12. tapply函数对1:12这个向量, 按照因子fac的分组顺序进行分组, 然后对每组进行FUN = sum的操作. 具体请参见帮助文档.