今天看啥  ›  专栏  ›  Jachin111

25Apply 函数族管道操作

Jachin111  · 简书  ·  · 2020-12-26 19:49

基础数据结构

image.png

apply 族函数概述
apply 族函数是 R 语言数据处理的一组核心函数,它们对 array(包括 vector、matrix)、data frame 或 list 按照元素或元素构成的子集合进行迭代,并将当前元素或子集合作为参数调用某个指定函数,从而实现对数据的循环、分组、过滤等操作。 apply 族函数包括 apply,lapply,sapply,vapply,rapply,mapply,tapply,eapply 函数。每个函数用途不同,处理的数据类型也有所差异。
image.png

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 变得困难。
并非是线性结构的问题,而是复杂的非线性结构。




原文地址:访问原文地址
快照地址: 访问文章快照