基础数据结构
apply 族函数概述
apply 族函数是 R 语言数据处理的一组核心函数,它们对 array(包括 vector、matrix)、data frame 或 list 按照元素或元素构成的子集合进行迭代,并将当前元素或子集合作为参数调用某个指定函数,从而实现对数据的循环、分组、过滤等操作。 apply 族函数包括 apply,lapply,sapply,vapply,rapply,mapply,tapply,eapply 函数。每个函数用途不同,处理的数据类型也有所差异。
apply
apply 函数是最常用的替代 for 和 while 循环的函数,它可以对 matrix, array, data frame 结构的数据按行(MARGIN = 1)或列(MARGIN = 2)进行循环,并把元素构成的子集合作为 FUN 函数的参数,返回计算结果。
我们利用 rnorm 函数生成一个随机的成绩矩阵,共有 6 个学生,3 个科目。
> set.seed(42)
> x <- matrix(rnorm(18, mean=100, sd=2), 6, 3)
> rownames(x) <- paste("undergraduate", 1:6)
> colnames(x) <- paste("usub", 1:3)
> x
usub 1 usub 2 usub 3
undergraduate 1 102.74192 103.02304 97.22228
undergraduate 2 98.87060 99.81068 99.44242
undergraduate 3 100.72626 104.03685 99.73336
undergraduate 4 101.26573 99.87457 101.27190
undergraduate 5 100.80854 102.60974 99.43149
undergraduate 6 99.78775 104.57329 94.68709
用 3 种方法来计算三个科目的平均成绩。
> a <- NULL
> for (j in 1:3){
+ m <- mean(x[,j])
+ a <- c(a, m)
+ }
> a
[1] 100.70013 102.32136 98.63142
> apply(x, 2, mean)
usub 1 usub 2 usub 3
100.70013 102.32136 98.63142
> colMeans(x)
usub 1 usub 2 usub 3
100.70013 102.32136 98.63142
for 循环基于 R 语言本身实现,效率最低,而 apply 和 colMeans 函数都基于底层的 C 语言实现,利用了向量化计算的特点,效率更高。 为了验证我们的猜想,计算三种方法在性能上的消耗。
> rm(list=ls())
>
> fun1 <- function(x){
+ a <- NULL
+ for (j in 1:3){
+ m <- mean(x[,j])
+ a <- c(a, m)
+ }
+ }
>
> fun2 <- function(x){
+ apply(x, 2, mean)
+ }
>
> fun3 <- function(x){
+ colMeans(x)
+ }
>
> set.seed(42)
> x <- matrix(rnorm(18, mean=100, sd=2), 6, 3)
> rownames(x) <- paste("undergraduate", 1:6)
> colnames(x) <- paste("usub", 1:3)
> system.time(fun1(x))
用户 系统 流逝
0.02 0.00 0.01
> system.time(fun2(x))
用户 系统 流逝
0 0 0
> system.time(fun3(x))
用户 系统 流逝
0 0 0
for 循环耗时最长,基于向量化计算的 apply 函数和 colMeans 函数几乎不耗时。这给我们的启示是,应该优先考虑 R 语言内置的向量计算函数,如果没有对应函数则使用 apply 函数,尽量避免使用 for 和 while 循环。
lapply
lapply 函数可以看作运用在 list 上的 apply 函数。
我们先构建一个包含本科生、硕士和博士各科成绩的 list。
> set.seed(42)
> y <- matrix(rnorm(36, mean=150, sd=3), 9, 4)
> rownames(y) <- paste("master", 1:9)
> colnames(y) <- paste("msub", 1:4)
>
> z <- matrix(rnorm(6, mean=300, sd=9), 3, 2)
> rownames(z) <- paste("phd", 1:3)
> colnames(z) <- paste("psub", 1:2)
>
> scorelist <- list(undergraduate=x, master=y, phd=z)
> scorelist
$undergraduate
usub 1 usub 2 usub 3
undergraduate 1 102.74192 103.02304 97.22228
undergraduate 2 98.87060 99.81068 99.44242
undergraduate 3 100.72626 104.03685 99.73336
undergraduate 4 101.26573 99.87457 101.27190
undergraduate 5 100.80854 102.60974 99.43149
undergraduate 6 99.78775 104.57329 94.68709
$master
msub 1 msub 2 msub 3 msub 4
master 1 154.1129 149.8119 142.6786 144.7105
master 2 148.3059 153.9146 153.9603 151.3803
master 3 151.0894 156.8599 149.0801 148.0800
master 4 151.8986 145.8334 144.6561 151.3664
master 5 151.2128 149.1636 149.4842 152.1145
master 6 149.6816 149.6000 153.6440 153.1053
master 7 154.5346 151.9079 155.6856 148.1732
master 8 149.7160 149.1472 148.7086 151.5149
master 9 156.0553 142.0306 149.2282 144.8490
$phd
psub 1 psub 2
phd 1 292.9399 300.3251
phd 2 292.3418 301.8540
phd 3 278.2721 296.7505
这个 list 由 3 个大小不同的 matrix 构成
> lapply(scorelist, colMins)
$undergraduate
usub 1 usub 2 usub 3
98.87060 99.81068 94.68709
$master
msub 1 msub 2 msub 3 msub 4
148.3059 142.0306 142.6786 144.7105
$phd
psub 1 psub 2
278.2721 296.7505
sapply
sapply 函数是简化结果的 lapply 函数
> sapply(scorelist, min)
undergraduate master phd
94.68709 142.03063 278.27213
rapply
rapply 函数是递归版本的 lapply 函数,它对 list 中的每个元素进行递归遍历。 假如因为今年试题难度加大,3 组学生的平均成绩都比去年低 5 分左右,此时教务处想做宏观调控,把每个学生的每科成绩都增加 5 分。
> rapply(scorelist, function(x) x+5, how="list")
$undergraduate
usub 1 usub 2 usub 3
undergraduate 1 107.7419 108.0230 102.22228
undergraduate 2 103.8706 104.8107 104.44242
undergraduate 3 105.7263 109.0368 104.73336
undergraduate 4 106.2657 104.8746 106.27190
undergraduate 5 105.8085 107.6097 104.43149
undergraduate 6 104.7878 109.5733 99.68709
$master
msub 1 msub 2 msub 3 msub 4
master 1 159.1129 154.8119 147.6786 149.7105
master 2 153.3059 158.9146 158.9603 156.3803
master 3 156.0894 161.8599 154.0801 153.0800
master 4 156.8986 150.8334 149.6561 156.3664
master 5 156.2128 154.1636 154.4842 157.1145
master 6 154.6816 154.6000 158.6440 158.1053
master 7 159.5346 156.9079 160.6856 153.1732
master 8 154.7160 154.1472 153.7086 156.5149
master 9 161.0553 147.0306 154.2282 149.8490
$phd
psub 1 psub 2
phd 1 297.9399 305.3251
phd 2 297.3418 306.8540
phd 3 283.2721 301.7505
mapply
mapply 函数是多变量的 sapply 函数,它可以定义一个函数的多个参数。 假如我们想一次性生成 3 组学生的成绩,则可以用 mapply 函数。
> set.seed(42)
> n <- c(6, 9, 3)
> m <- c(100, 150, 300)
> sd <- c(2, 3, 9)
> mapply(rnorm, n, m, sd)
[[1]]
[1] 102.74192 98.87060 100.72626 101.26573 100.80854
[6] 99.78775
[[2]]
[1] 154.5346 149.7160 156.0553 149.8119 153.9146 156.8599
[7] 145.8334 149.1636 149.6000
[[3]]
[1] 305.7236 297.4417 276.0919
tapply
tapply 函数可以先根据 INDEX 参数将数据分组,再进行各组的循环计算。 我们给本科生的成绩单加上性别变量。
> sex <- c('f', 'f', 'm', 'm', 'f', 'm')
> xx <- data.frame(x, sex)
> xx
usub.1 usub.2 usub.3 sex
undergraduate 1 102.74192 103.02304 97.22228 f
undergraduate 2 98.87060 99.81068 99.44242 f
undergraduate 3 100.72626 104.03685 99.73336 m
undergraduate 4 101.26573 99.87457 101.27190 m
undergraduate 5 100.80854 102.60974 99.43149 f
undergraduate 6 99.78775 104.57329 94.68709 m
> tapply(xx$usub.3, xx$sex, mean)
f m
98.69873 98.56412
从计算结果可以看出,在科目 3 上,女生的平均成绩为 98.69873,男生的平均成绩为98.56412
管道操作
如果我们想对不同的数据进行相同的操作,我们可能会使用循环语句,或者向量化计算。 如果我们想对相同的数据进行一系列不同的操作,那么管道(pipe)将是一个强大的工具,它的作用是让代码更具可读性。
%>% 的原理很简单,它将左边的值管道输出为右边函数的第一个参数,但是,如果我们想把左边的值输送给函数的其它参数,%>% 无能为力,这时候可以使用 pipeR 中的 %>>% 操作。
> set.seed(42)
> population <- rnorm(1000000, 0, 0.3)
> loss <- sample(population, 100, replace=F)
> VaR <- quantile(loss, 0.95)
> VaR
95%
0.4696037
这是我们最常使用的方法。但是,如果中间变量对我们来说是无用的,或者我们不希望有太多中间变量,我们可能采用如下操作:
> set.seed(42)
> a <- rnorm(1000000, 0, 0.3)
> a <- sample(population, 100, replace=F)
> a <- quantile(loss, 0.95)
> a
95%
0.4696037
通过不断覆盖原变量的操作,我们避免了过多的中间变量,但是会使 debug 变得异常痛苦。 除此之外,我们还可以一步到位,使用函数的组合嵌套。
> set.seed(42)
> VaR <- quantile(sample(rnorm(1000000, 0, 0.3), 100, replace=F), 0.95)
> VaR
95%
0.4696037
这种方法也避免了过多的中间变量,但是可读性差,必须从内往外读,不符合人类的阅读习惯。 最后,我们尝试用 pipe 来解决这个问题。
> library(magrittr)
> set.seed(42)
> VaR <- rnorm(1000000, 0, 0.3) %>% sample(100, replace=F) %>% quantile(0.95)
> VaR
95%
0.4696037
%>%管道操作既解决了中间变量过多的问题,又兼顾了可读性,这是它深受喜爱的原因,但这种第一参数管道操作适用面较窄,如果我们想把前面的结果赋给后面函数的第二、三个参数或者同时赋给多个参数,问题就出现了,这时我们可以使用papeR package 的 %>>%。
> set.seed(42)
> population <- rnorm(1000000, 0, 0.3)
> loss <- sample(population, 100, replace=F)
> sloss <- sort(loss)
> ES <- sum(sloss[length(sloss):round(0.95*length(sloss))])/(length(sloss) - round(0.95*length(sloss)))
> ES
[1] 0.6485668
在计算 ES 的那一步中,sloss 在函数中的多个地方出现了,%>% 不再适用。
> library(pipeR)
> set.seed(42)
> ES <- rnorm(1000000, 0, 0.3) %>>%
+ sample(size = 100, replace = FALSE) %>>%
+ sort %>>%
+ (sum(.[length(.):round(0.95*length(.))])/(length(.)-round(0.95*length(.))))
%>>% 的想法也很简单,用.代替前一步的结果出现在后面的函数中。 我们勉强使用 %>>% 解决了这个问题,但从上面已经可以看出,即使使用了 %>>%,代码的可读性也不高,最后一行仍然是多重嵌套。 出现这个问题的根本原因在于,我们把一个非线性的问题强行处理成了线性问题。
> set.seed(42)
> population <- rnorm(1000000, 0, 0.3)
> loss <- sample(population, 100, replace=F)
> sloss <- sort(loss)
> n1 <- length(sloss)
> n2 <- round(0.95*n1)
> ES <- sum(sloss[n1:n2])/(n1 - n2)
增加了两个中间变量,代码的可读性增强了。
当以下情况出现时,管道操作可能是不合适的。
步数大于 10 步, pipe 将使 debug 变得困难。
并非是线性结构的问题,而是复杂的非线性结构。