文章目錄
  1. 1. apply
  2. 2. lapply 和 sapply 以及 vapply
    1. 2.1. lapply
    2. 2.2. sapply
    3. 2.3. vapply
  3. 3. mapply
  4. 4. tapply

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
## [1] "matrix"
1
head(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] 30
1
head(bmindex)
1
## [1] 26.26587 19.30325 17.26137 26.77257 17.07008 17.67329

我们也可以把计算出的BMI指数赋给矩阵的第三列.

1
x <- cbind(x, bmindex)

在刚才的例子中, 编写的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
lapply(xl, mean)
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
lapply(xl, length)
1
2
3
4
5
6
7
8
## $a
## [1] 10
##
## $beta
## [1] 7
##
## $logic
## [1] 4

想要知道xl的每个元素的数据类型, 相当于对列表的每个元素运行class函数

1
lapply(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
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
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) # list of vectors
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
sapply(3:9, seq)
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, 那么函数执行的是

1
seq(1,4)
1
## [1] 1 2 3 4

接下来循环到第二个和第三个分量时, 函数又分别调用了两次, 执行的是

1
seq(2,6)
1
## [1] 2 3 4 5 6
1
seq(3,5)
1
## [1] 3 4 5

最后mapply返回的是一个列表(一般情况下循环调用了FUN, 可以认为每次返回值不必都相等). 在特殊情况下, 由于默认的 SIMPLIFY = TRUE, 那么返回值是可以尽可能的合并成一个格式整齐的矩阵的. 比如

1
mapply(seq, 1:3,4:6)
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
tapply(1:12, fac, sum)
1
2
##  1  2  3  4  5 
## 22 26 30 NA NA

如上, fac变量为一个因子, 并与1:12的长度相等, 都为12. tapply函数对1:12这个向量, 按照因子fac的分组顺序进行分组, 然后对每组进行FUN = sum的操作. 具体请参见帮助文档.

文章目錄
  1. 1. apply
  2. 2. lapply 和 sapply 以及 vapply
    1. 2.1. lapply
    2. 2.2. sapply
    3. 2.3. vapply
  3. 3. mapply
  4. 4. tapply