「杂谈」如何写好R语言apply家族函数

本文讲述R语言中apply家族的函数的使用方法。

一、lapply

最好不要试图一开始就用apply函数,因为它比lapply函数复杂。lapply函数是apply家族的函数中逻辑最简单、版本兼容性、最user-friendly的函数。

你甚至可以用lapply这一个函数打天下。

其基本语法为:lapply(Vector, function(ii) { ... })

例如,有这样一个data.frame,它是dat <- data.frame(A=1:5,B=10:14)。我现在要做一件事:对于A列中的每个元素,找出B列中离它最远的那个数(在一维数轴上)。

像这样的问题,让初学者感受到两层循环的压力时,就是apply家族函数大展拳脚的时机。

初学者会这样写:

result <- c()
for(i in 1:nrow(dat)){
  alldistance <- abs(dat$A[i]-dat$B) #calculate distances
  whichmax <- which(max(alldistance)==alldistance) #which point has the maximal distance
  valuemax <- dat$B[whichmax] #value of point with the maximal distance
  result <- c(result,valuemax)
}

准确来说这不是两层循环,而是外层循环,内层向量运算。

但用lapply函数后,就是这样的:

result <- lapply(1:nrow(dat),function(i){
  alldistance <- abs(dat$A[i]-dat$B)
  whichmax <- which(max(alldistance)==alldistance)
  valuemax <- dat$B[whichmax]
  return(valuemax)
})

但初学者多少会有些不习惯。

但无论如何,我相信用R的绝大多数人都有一个习惯:把一条命令写出来,马上运行,运行结果正确时,就把这条命令放到脚本里;然后再写下一句命令,再运行,结果正确时再把这个命令塞到脚本里,如此往复循环…

这个习惯同样有利于我们快速地写好lapply函数。

Step1: 写好框架lapply(Vector, function(element) { ... })。其中Vector有两种方式。一种是像上面这个例子那样,用索引;另一种干脆直接遍历元素,比如上面这个例子中lapply的第一个元素变成dat$A
Step2: 将循环索引push到工作变量环境中。例如上面这个案例,框架搭好以后,直接在命令行运行i <- 1。接下来编代码就容易了,就像编一个for循环那样,写一句运行一句。
Step3: 返回值。返回任何东西都可以,例如一个标量、向量、数据框等等。一开始接触时建议加return,像我这种老油条必然是懒得加了,因为R语言默认一个函数定义中最后一句话的返回值就是整个函数的返回值。

lapply函数的返回值,永远是个list。因此上述代码的第二个版本中result变量实际上是个list。这个list中的每个组分就是lapply中自定义函数的返回值valuemax。要是valuemax不是个标量,而是个更复杂的东西,比如矩阵,那result这个列表可以更复杂。

二、do.call

接下来,自然而然的疑问就是说,既然你lapply函数返回一个list,那我怎样把这个list转换成一个向量,或者数据框呢?

有这种疑问很正常,因为很多时候我们用一个循环,或者用apply家族函数所得到的最终结果应该是个向量,或者数据框。例如上述案例最终就应该得到一个向量。

怎么办呢?

如果要得到向量,请用do.call(c, <A list object>)

如果要得到数据框,请用do.call(rbind, <A list object>)

因而do.callapply家族函数往往是联合在一起用的。

result <- do.call(c,lapply(1:nrow(dat),function(i){
  alldistance <- abs(dat$A[i]-dat$B)
  whichmax <- which(max(alldistance)==alldistance)
  dat$B[whichmax]
}))

三、让代码更严谨

上述案例中的代码有个缺陷(尽管影响不大):lapply函数中的自定义函数内部使用了dat变量,但这个dat变量实际上采用的是函数外部的dat

在一个良好的、可维护的代码中,这样的风范不值得提倡。因此稍作修改:

result <- do.call(c,lapply(1:nrow(dat),function(i, dat){
  alldistance <- abs(dat$A[i]-dat$B)
  whichmax <- which(max(alldistance)==alldistance)
  dat$B[whichmax]
}, dat = dat))

请注意,如果不改成这样的形式,在R包编译的时候既不会报WARNING,也不会报NOTE,但万一程序一复杂,计算结果不对的话,这种不规范只会导致代码检查的工作量增大。

四、apply家族的其他函数

我单方面认为,综合普适性、用户友好性和实用性,给apply家族排个名,其次序是这样的:lapply>sapply>apply>tapply

lapply可以说是“万能的”,并且关键是版本兼容性好。而那sapplyapply的使用语法在不同版本中有所不同,这会导致使用低版本R语言开发的R包在高版本R环境中报错。

sapplylapply极其相似,但有两处不同:① 函数的第一个参数不是个向量,而是个列表,仅此而已;② sapply函数会自动评估返回值是否能合并成一个向量,或者一个数据框;能合并的,就尽量合并;不能合并的,跟lapply函数一样以列表的形式返回。

功能②看似比较用户友好,但实际上容易画蛇添足。

case1 <- sapply(as.list(1:4),function(xx){
  if(xx==3) rep(999,2) else xx
})

case2 <- sapply(as.list(5:8),function(xx){
  if(xx==3) rep(999,2) else xx
})

运行代码可知,case1是个列表,case2却是个向量。那么,在真正开发软件时,你究竟要不要在外面加一个do.call呢?加,那case2就直接报错;不加,那case1case2根本不是一种数据类型,下游计算仍要出问题。

幸运的是,sapply函数提供了simplify 这个参数。该参数缺省值是TRUE,表示启动②这个功能。你可以将它设置为FALSE,即可关闭它,免得它惹是生非。

apply函数的第一个参数无非是变成了一个矩阵或数据框。使用时,多一个参数MARGINMARGIN = 1代表对每一行施加某一函数;MARGIN = 2代表对每一列施加某一函数。说白了,前者就是以每行为单位搞个循环,后者是以每列为单位搞个循环,没什么新奇的。apply函数跟sapply有一样的毛病,即会自动评估返回值是否要合并成一个向量或者数据框。好在sapply函数还给你simplify这个选项,让你关掉。但apply函数却不给你这个选项。前几个月还有的,到了R 4.0.4这个版本后,apply函数的simplify选项就已经被搞没了,这点让我很困惑,不知道R Core Team是什么想法。

总之,要开发软件包的话,没啥大事情少用apply吧。

tapply也是一样,会自动评估并合并,但好在给了simplify参数。tapply函数的语法是tapply(X, INDEX, function(xx) { ... }),向量X是数据,INDEX相当于是个label,把数据X的每个元素打上分组标签。后面的自定义函数就是对每个分组标签开展一次apply。这种情况的使用场合本来就比lapply少多了,因此其重要性也很次。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HaoranWu_ZJU

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值