25Apply 函数族管道操作

基础数据结构

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容