为什么使用purrr :: map而不是lapply?


170

有什么理由我应该使用

map(<list-like-object>, function(x) <do stuff>)

代替

lapply(<list-like-object>, function(x) <do stuff>)

输出应该是相同的,并且我所做的基准测试似乎表明lapply速度稍快(应该根据map需要评估所有非标准评估输入)。

那么,在这种简单情况下,我是否应该考虑转用任何理由purrr::map?我在这里不是在问语法的好恶,而是由purrr等提供的其他功能,而是严格地purrr::maplapply假设使用标准评估进行比较map(<list-like-object>, function(x) <do stuff>)purrr::map在性能,异常处理等方面有什么优势吗?下面的评论建议不要这样做,但是也许有人可以详细说明一下?


8
实际上,对于简单的用例,最好坚持使用R并避免依赖。如果你已经装载tidyverse虽然,你可能会受益于管道%>%和匿名函数~ .x + 1的语法
Aurèle

49
这几乎是一个风格问题。您应该知道基本R函数的作用,因为所有这些整洁的东西都只是位于其之上的外壳。在某个时候,该外壳会破裂。
Hong Ooi

9
~{}快捷拉姆达(带或不带{}密封交易对我来说平淡purrr::map()。的种类执法purrr::map_…()是很方便的,少钝比vapply()purrr::map_df()是一个超级昂贵的功能,但它也简化了代码。但绝对没有错与基础R坚持[lsv]apply(),虽然。
hrbrmstr

4
谢谢您提出的问题-我也看过这种东西。我使用R已有10多年了,并且绝对不会也不会使用purrr东西。我的观点是:tidyverse非常适合分析/交互/报告内容,而不适合编程。如果您必须使用,lapply或者map您正在编程,那么可能最终需要创建一个程序包。然后,依赖性越少越好。另外:我有时会看到人们使用map了晦涩难懂的语法。现在,我看到了性能测试:如果您习惯了apply家庭生活:坚持下去。
埃里克·勒科特

4
蒂姆,您写道:“我在这里不是问一个人对语法,purrr等提供的其他功能的喜好,而是严格地将purrr :: map与使用标准评估的lapply进行比较”,您接受的答案是完全按照您说的去做,您不希望其他人去做。
卡洛斯·辛纳利

Answers:


231

如果您从purrr中使用的唯一功能是map(),则否,优势并不明显。正如Rich Pauloo所指出的那样,的主要优点map()是帮助程序,它们使您可以为常见的特殊情况编写紧凑的代码:

  • ~ . + 1 相当于 function(x) x + 1

  • list("x", 1)等同于function(x) x[["x"]][[1]]。这些帮助程序比[[- 更通用- ?pluck有关详细信息,请参见。对于数据重组,该 .default参数特别有用。

但是在大多数情况下,您并没有使用单个*apply()/ map() 函数,而是使用了一堆/ 函数,而purrr的优点是这些函数之间的一致性更高。例如:

  • 的第一个参数lapply()是数据;mapply()函数的第一个参数 。所有映射函数的第一个参数始终是数据。

  • 使用vapply()sapply()和,mapply()您可以选择使用USE.NAMES = FALSE; 在输出中取消显示名称。但 lapply()没有那个论点。

  • 没有一致的方法将一致的参数传递给mapper函数。大多数函数都使用,...但是mapply()使用 MoreArgs(您希望将其称为MORE.ARGS),和 Map()Filter()Reduce()希望您创建一个新的匿名函数。在映射函数中,常量参数始终位于函数名称之后。

  • 几乎每个purrr函数都是类型稳定的:您可以从函数名称中专门预测输出类型。对于sapply()或,情况并非如此 mapply()。是的,有vapply();但没有等效项mapply()

您可能会认为所有这些次要区别并不重要(就像有些人认为对基R正则表达式进行字符串化没有优势),但以我的经验,它们在编程时会引起不必要的摩擦(不同的参数顺序常用于跳闸)我起来),它们使函数式编程技术变得更难学习,因为除了大胆的主意之外,您还必须学习很多附带的细节。

Purrr还填写了基本R中缺少的一些方便的地图变体:

  • modify()保留[[<-用于修改“就地” 的数据类型。结合_if变体,这允许使用(IMO漂亮的)代码,例如modify_if(df, is.factor, as.character)

  • map2()允许您同时通过x和映射y。这样可以更轻松地表达想法 map2(models, datasets, predict)

  • imap()允许您同时映射x其索引(名称或位置)。这样可以很容易地(例如)加载csv目录中的所有 文件,并filename为每个文件添加一列。

    dir("\\.csv$") %>%
      set_names() %>%
      map(read.csv) %>%
      imap(~ transform(.x, filename = .y))
  • walk()不可见地返回其输入;并且在因副作用而调用函数时(即将文件写入磁盘)很有用。

更不用说其他帮助者,例如safely()partial()

就个人而言,我发现使用purrr时,我可以以更少的摩擦和更大的便利来编写功能代码;它缩小了构想和实施想法之间的差距。但是您的里程可能会有所不同;除非有实际帮助,否则无需使用purrr。

微基准

是的,map()比慢一点lapply()。但是使用map()或的成本 lapply()由您要映射的内容决定,而不是执行循环的开销。下面的微基准测试表明,与之map()相比,lapply()每个元素的成本约为40 ns,这似乎不太可能对大多数R代码产生实质性影响。

library(purrr)
n <- 1e4
x <- 1:n
f <- function(x) NULL

mb <- microbenchmark::microbenchmark(
  lapply = lapply(x, f),
  map = map(x, f)
)
summary(mb, unit = "ns")$median / n
#> [1] 490.343 546.880

2
您是否打算在该示例中使用transform()?如在基本R transform()中一样,还是我缺少什么?transform()为您提供文件名作为因素,当您(自然)希望将行绑定在一起时会生成警告。mutate()给了我想要的文件名的字符列。是否有理由不在那里使用它?
doctorG

2
是的,更好地使用mutate(),我只是想要一个没有其他部门的简单示例。
hadley '17

类型专用性不应该出现在此答案中吗?map_*是什么让我载入purrr了许多脚本。它帮助我实现了代码(stopifnot(is.data.frame(x)))的某些“控制流”方面。
神父

2
ggplot和data.table很棒,但是对于R中的每个函数,我们真的需要一个新包吗?
adn bps

57

比较purrrlapply归结为方便速度


1. purrr::map在语法上比lapply更方便

提取列表的第二个元素

map(list, 2)  

这是@F。Privé指出,与以下内容相同:

map(list, function(x) x[[2]])

lapply

lapply(list, 2) # doesn't work

我们需要传递一个匿名函数...

lapply(list, function(x) x[[2]])  # now it works

...或正如@RichScriven指出的那样,我们[[作为参数传递给lapply

lapply(list, `[[`, 2)  # a bit more simple syntantically

因此,如果发现自己使用来将函数应用于许多列表lapply,而又厌倦了定义自定义函数或编写匿名函数,那么便利便成为了青睐的原因之一purrr

2.特定于类型的映射函数仅需多行代码

  • map_chr()
  • map_lgl()
  • map_int()
  • map_dbl()
  • map_df()

每个这些类型特定的地图功能返回的原子列表(矢量),而不是列出了由返回map()lapply()。如果要处理其中的原子向量的嵌套列表,则可以使用这些特定于类型的映射函数直接提取向量,并将向量直接强制转换为int,dbl和chr向量。该基础R版本看起来是这样的as.numeric(sapply(...))as.character(sapply(...))等等。

map_<type>功能还具有有用的质量,如果他们不能返回所指示的类型的原子矢量,他们失败了。这在定义某些控制流时很有用,在该控制流中,如果函数[以某种方式]生成错误的对象类型,则希望函数失败。

3.除了便利,lapply比[稍微]快map

使用purrr的便利功能,如@F。Privé指出,这会减慢处理速度。让我们对上面介绍的4种情况进行竞赛。

# devtools::install_github("jennybc/repurrrsive")
library(repurrrsive)
library(purrr)
library(microbenchmark)
library(ggplot2)

mbm <- microbenchmark(
lapply       = lapply(got_chars[1:4], function(x) x[[2]]),
lapply_2     = lapply(got_chars[1:4], `[[`, 2),
map_shortcut = map(got_chars[1:4], 2),
map          = map(got_chars[1:4], function(x) x[[2]]),
times        = 100
)
autoplot(mbm)

在此处输入图片说明

最终获胜者是....

lapply(list, `[[`, 2)

总之,如果您追求的是原始速度:(base::lapply尽管速度没有那么快)

为了简单的语法和可表达性: purrr::map


这个出色的purrr教程强调了使用时不必显式写出匿名函数purrr的便利以及特定于类型的map函数的好处。


2
请注意,如果您使用function(x) x[[2]]而不是just 2,则速度会较慢。所有这些额外的时间是由于lapply不执行检查所致。
F.Privé17年

17
您不需要“匿名”功能。 [[是一个功能。你可以的lapply(list, "[[", 3)
Rich Scriven's

@RichScriven很有道理。这确实简化了在purrr上使用lapply的语法。
Rich Pauloo

37

如果我们不考虑口味方面(否则应关闭此问题)或语法一致性,样式等,则答案是否定的,没有特殊的理由使用map而不是lapplyapply系列的或其他变体,例如更严格vapply

PS:对于那些无缘无故投票的人,只需记住OP写道:

我不是在这里询问有关purrr等提供的语法或其他功能的喜好,而是严格地假设使用标准评估来比较purrr :: map与lapply的比较

如果您不考虑语法或的其他功能purrr,则无需使用特殊理由map。我用purrr自己,对哈德利的回答表示满意,但具有讽刺意味的是,这超出了OP事先未提出的要求。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.