R

R 中的 apply 系列函数

apply Functions in R

Posted by J Leaves on January 3, 2021

R中apply相关函数有不少,有的不常见,比如eapply,就不展开说了。常见的有apply,lapply,sapply,tapply。

1 apply

1.1 apply 的用法

查看R语言中apply函数的帮助文档,对apply函数是这样说明的:

Returns a vector or array or list of values obtained by applying a function to margins of an array or matrix.

这句话的意思就是apply会把一个函数同时作用于一个数组或者矩阵的一个margin,然后返回值存在一个向量或者数组中,也就是说把每一个margin作为一个函数的输入,对应一个输出,所有的输出放在一起返回来。
那么这个margin如何理解?margin可以是数组的每一行/每一列。值得注意的是这里的数组未必是2维的,更高维也可以。

一个具体例子,求一个2维数组每一列和每一行的平均值:

1
2
3
4
5
6
7
8
9
10
11
12
> x <- array(rnorm(12),c(3,4))
> x
           [,1]       [,2]      [,3]       [,4]
[1,]  0.8972996 -0.8049946 -1.870765 -0.5850348
[2,]  0.7341293 -0.3148228  1.023150 -0.2756939
[3,] -2.0388308 -2.7536031 -1.500011  0.9308724
> y <- apply(x,1,mean)
> y
[1] -0.5908738  0.2916906 -1.3403932
> y <- apply(x,2,mean)
> y
[1] -0.13580066 -1.29114016 -0.78254226  0.02338122

apply第一个参数是输入数据。
apply第二个参数,指定是哪一种margin,1对应每一列,2对应每一行。

1.2 一些说明

1.2.1 关于轴

R 中的高维数组与 Python 中 Numpy 模块中的高维数组表现有所差异。在 Python 中指定的轴是 index 被改造的轴,而 R 中指定的轴是 index 保持不变的轴。
比如同样对于一个 2*3*4 的数组 m,同样指定的是第一个维度:

  • Python 中 np.average(m, axis=0) ,在遍历元素时是第一维度(axis=0)的 index 被改造,该维度上每一个两元素向量被求平均从而转化为一个标量,而第二和第三维度的 index 保持不变,最终得到一个 3*4 的数组
  • R 中的 apply(m, 1, mean),在遍历元素时是第一维度(第二个参数 1)的 index 保持不变,而其余维度(即第一和第三维度)的 index 被改造:求平均、转化为标量,最终得到一个形状为 2 的一维数组

如果有更高维,以此类推,比如对3维情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
> x <- array(c(1:24),c(2,3,4))
> x
, , 1

     [,1] [,2] [,3]
[1,]    1    3    5
[2,]    2    4    6

, , 2

     [,1] [,2] [,3]
[1,]    7    9   11
[2,]    8   10   12

, , 3

     [,1] [,2] [,3]
[1,]   13   15   17
[2,]   14   16   18

, , 4

     [,1] [,2] [,3]
[1,]   19   21   23
[2,]   20   22   24

请思考一下以下代码的结果

1
2
3
apply(x, 1, mean)
apply(x, 2, mean)
apply(x, 3, mean)

答案是

1
2
3
4
5
6
> apply(x, 1, mean)
[1] 12 13
> apply(x, 2, mean)
[1] 10.5 12.5 14.5
> apply(x, 3, mean)
[1]  3.5  9.5 15.5 21.5

以上例子,apply(x, 1, mean) 计算的是1到23的所有奇数的平均值。apply第三个参数,指定了具体应用什么函数,上面例子计算平均值,其实可以用更简单办法,colMeans 和 rowMeans 函数。

1.2.2 自建函数

那如果是一个自己写的函数呢?看下面例子:

1
2
3
> myFun <- function(x){sum(x^2)}
> apply(x,1,myFun)
[1]  5.295192  1.760902 14.855718

上面例子计算每一行的平方和。

如果一个函数fun1有多个参数,margin只是这个函数的一个参数,想固定其他参数怎么办?很简单,自己定义一个只有一个参数的新函数fun2,在fun2里面调用fun1,并且对其他参数赋值,然后把fun2传递给apply。

通常情况大家使用apply之后是需要把apply的返回值作为输入在其他代码中使用的,这里尤其重要一点是apply的返回值的维度。上面例子计算每一行/列的mean,使用apply之后返回的都是一个向量,并不会因为apply计算行(列)的mean就会自动返回一个列(行)向量。

下面我们用apply函数来实现一个R的内置函数scale,就是标准化一个数组/矩阵。具体来说,是把每一列先减去这一列的中心,然后除以这一列的标准差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> x <- array(1:20,c(4,5))
> x
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    5    9   13   17
[2,]    2    6   10   14   18
[3,]    3    7   11   15   19
[4,]    4    8   12   16   20
> scale(x, center=T, scale=T)
           [,1]       [,2]       [,3]       [,4]       [,5]
[1,] -1.1618950 -1.1618950 -1.1618950 -1.1618950 -1.1618950
[2,] -0.3872983 -0.3872983 -0.3872983 -0.3872983 -0.3872983
[3,]  0.3872983  0.3872983  0.3872983  0.3872983  0.3872983
[4,]  1.1618950  1.1618950  1.1618950  1.1618950  1.1618950
attr(,"scaled:center")
[1]  2.5  6.5 10.5 14.5 18.5
attr(,"scaled:scale")
[1] 1.290994 1.290994 1.290994 1.290994 1.290994
1
2
3
4
5
6
7
8
9
10
11
> myScale <- function(x) {
+   x.Mean=apply(x, 2, mean)
+   x.sd=apply(x, 2, sd)
+   t((t(x) - x.Mean) / x.sd)
+ }
> myScale(x)
           [,1]       [,2]       [,3]       [,4]       [,5]
[1,] -1.1618950 -1.1618950 -1.1618950 -1.1618950 -1.1618950
[2,] -0.3872983 -0.3872983 -0.3872983 -0.3872983 -0.3872983
[3,]  0.3872983  0.3872983  0.3872983  0.3872983  0.3872983
[4,]  1.1618950  1.1618950  1.1618950  1.1618950  1.1618950

可见myScale于scale函数返回值一样。

在上面例子中,最关键的代码是

1
t((t(x)-x.Mean)/x.sd)

因为在R语言中,一个矩阵和一个向量运算,向量是作为列向量,与矩阵的每一个元素(从一列开始,而不是第一行)进行运算。所以我们先转置矩阵,变成矩阵的元素按行来与向量的元素运算,然后把结果再转置回去。

2 lapply 和 sapply

这两个 apply 函数很像,都是应用于一个 vector/list 上面,上面的 apply 是用于一个数组/矩阵。所以通常apply需要三个参数,而 lapply/sapply 一般需要两个参数,第一个参数是输入数据,第二个是函数。

两者的参数:

1
2
lapply(X, FUN, ...)
sapply(X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)

两者的区别在于返回值上面,lapply 返回的是一个 list;但是 sapply 返回的值可能是 vector/matrix,且若指定 sapply 中 simplify = "array" 则返回的是 array。

例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> x <- c(1:5)
> sapply(x, function(x) {x^2})
[1]  1  4  9 16 25
> lapply(x, function(x) {x^2})
[[1]]
[1] 1

[[2]]
[1] 4

[[3]]
[1] 9

[[4]]
[1] 16

[[5]]
[1] 25

sapply还有更复杂的用法,要使用第三个参数。在上面例子中,sapply应用的函数返回值是一个元素,也就是一个数值,然后sapply把这些数值放在一个向量中返回,但是如果sapply应用的函数返回的不是一个元素呢?这时sapply会把返回值作为一个vector,见下面例子:

1
2
3
4
5
6
> sapply(1:5,function(x) matrix(x,2,2))
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    2    3    4    5
[2,]    1    2    3    4    5
[3,]    1    2    3    4    5
[4,]    1    2    3    4    5

显然上面例子的返回值并非我们想要的,要处理返回值是数组/矩阵的情况,在sapply里面使用第三个参数simplify:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
> sapply(1:5,function(x) matrix(x,2,2), simplify = "array")
, , 1

     [,1] [,2]
[1,]    1    1
[2,]    1    1

, , 2

     [,1] [,2]
[1,]    2    2
[2,]    2    2

, , 3

     [,1] [,2]
[1,]    3    3
[2,]    3    3

, , 4

     [,1] [,2]
[1,]    4    4
[2,]    4    4

, , 5

     [,1] [,2]
[1,]    5    5
[2,]    5    5

3 tapply

tapply通常也有三个参数,第一个指定输入数据,第二个是指定输入数据如何分组,第三个参数指定在每一个分组内,应用什么函数,所以tapply的功能就是把数据按照某种分组,在每个组内进行某个运算,见下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
> medical.example
   patient      age treatment
1        1 54.70208 Treatment
2        2 64.91654 Treatment
3        3 51.80260 Treatment
4        4 61.63020 Treatment
5        5 72.79920 Treatment
6        6 59.99898 Treatment
7        7 40.85752 Treatment
8        8 51.00623 Treatment
9        9 64.25137 Treatment
10      10 59.97824 Treatment
11      11 58.19639   Control
12      12 34.42546   Control
13      13 53.11569   Control
14      14 31.19479   Control
15      15 67.27431   Control
16      16 64.89839   Control
17      17 67.29181   Control
18      18 61.83809   Control
19      19 82.79484   Control
20      20 68.93832   Control
> tapply(medical.example$age,medical.example$treatment,mean)
Treatment   Control 
 58.19430  58.99681 

Acknowledgement

https://www.zhihu.com/question/39843392/answer/83364326

http://blog.fens.me/r-apply/