【问题标题】:Why use purrr::map instead of lapply?为什么使用 purrr::map 而不是 lapply?
【发布时间】:2017-12-19 10:35:37
【问题描述】:

我有什么理由应该使用

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

而不是

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

输出应该是相同的,我所做的基准测试似乎表明lapply 稍微快一些(应该是map 需要评估所有非标准评估输入)。

那么对于这种简单的情况,我是否应该考虑切换到purrr::map?我不是在这里询问一个人对语法的好恶,purrr 提供的其他功能等,而是严格地比较purrr::maplapply 假设使用标准评估,即map(&lt;list-like-object&gt;, function(x) &lt;do stuff&gt;)purrr::map 在性能、异常处理等方面有什么优势吗?下面的 cmets 表明它没有,但也许有人可以详细说明一下?

【问题讨论】:

  • 对于简单的用例,最好坚持使用基础 R 并避免依赖。如果您已经加载了tidyverse,您可能会受益于管道%&gt;% 和匿名函数~ .x + 1 语法
  • 这几乎是一个风格问题。你应该知道基本的 R 函数是做什么的,因为所有这些 tidyverse 的东西只是它上面的一个外壳。在某些时候,那个外壳会破裂。
  • ~{} 快捷方式 lambda(有或没有 {} 对我来说是简单的purrr::map() 的交易。purrr::map_…() 的类型强制比vapply() 更方便且不那么迟钝.purrr::map_df() 是一个超级昂贵的函数,但它也简化了代码。不过,坚持使用 base R [lsv]apply() 绝对没有错。
  • 谢谢你的问题——我也看过一些东西。我使用 R 已有 10 多年了,绝对不会也不会使用 purrr 的东西。我的观点如下:tidyverse 非常适合分析/交互/报告的东西,而不是编程。如果您不得不使用lapplymap,那么您正在编程并且可能有一天会创建一个包。那么依赖越少越好。另外:我有时会看到人们使用 map 之后的语法非常晦涩。现在我看到了性能测试:如果你习惯了applyfamily:坚持下去。
  • Tim 你写道:“我不是在问一个人对语法的好恶,purrr 提供的其他功能等,而是严格地比较 purrr::map 与 lapply 假设使用标准评估”,而您接受的答案正是您所说的不希望人们过去的答案。

标签: r purrr


【解决方案1】:

如果您在 purrr 中使用的唯一函数是 map(),那么不, 优势不大。正如 Rich Pauloo 所指出的,主要 map() 的优点是可以让你编写紧凑的助手 常见特殊情况代码:

  • ~ . + 1 等价于function(x) x + 1

  • list("x", 1) 等价于function(x) x[["x"]][[1]]。这些 helpers 比[[ 更通用一些 - 有关详细信息,请参阅?pluck。 对于data rectangling.default 参数特别有用。

但大多数时候你并没有使用单个*apply()/map() 函数,你使用了一堆,而 purrr 的优点是 功能之间的一致性更高。例如:

  • lapply() 的第一个参数是数据;第一个论点 mapply() 是函数。所有地图函数的第一个参数 始终是数据。

  • 使用vapply()sapply()mapply(),您可以选择 使用USE.NAMES = FALSE 抑制输出上的名称;但 lapply() 没有这个论点。

  • 没有一致的方法可以将一致的参数传递给 映射器功能。大多数函数使用 ...mapply() 使用 MoreArgs(你希望被称为MORE.ARGS),和 Map()Filter()Reduce() 期待你创造一个新的 匿名函数。在 map 函数中,常量参数总是出现 在函数名之后。

  • 几乎每个 purrr 函数都是类型稳定的:您可以预测 仅从函数名称输出类型。这不是真的 sapply()mapply()。是的,有vapply();但是没有 相当于mapply()

您可能认为所有这些细微的区别都不重要 (就像有些人认为 stringr 没有优势 base R 正则表达式),但根据我的经验,它们会导致不必要的 编程时的摩擦(不同的参数顺序总是用于 绊倒我),它们使函数式编程技术更难 学习,因为除了伟大的想法,你还必须学习很多 附带的细节。

Purrr 还填充了一些基本 R 中没有的方便的地图变体:

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

  • map2() 允许您同时映射xy。这 更容易表达想法,例如 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

【讨论】:

  • 你的意思是在那个例子中使用 transform() 吗?就像在基础 R 变换()中一样,还是我错过了什么? transform() 为您提供文件名作为一个因素,当您(自然)想要将行绑定在一起时会生成警告。 mutate() 给了我想要的文件名的字符列。有理由不在那里使用它吗?
  • 是的,最好使用mutate(),我只是想要一个没有其他部门的简单示例。
  • 不应该在这个答案的某个地方出现类型特异性吗? map_* 是我在许多脚本中加载 purrr 的原因。它帮助我处理了代码的一些“控制流”方面 (stopifnot(is.data.frame(x)))。
  • ggplot 和 data.table 很棒,但我们真的需要为 R 中的每个函数都提供一个新包吗?
  • fwiw 您可以轻松地返回一个列表,同时抑制基本 R 中的名称:sapply(1:10, function(x){x}, simplify=FALSE, USE.NAMES=FALSE)
【解决方案2】:

比较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_&lt;type&gt; 函数还具有有用的特性,即如果它们不能返回指定类型的原子向量,它们就会失败。这在定义严格的控制流时很有用,您希望函数在 [不知何故] 生成错误的对象类型时失败。

3。除了方便之外,lapplymap [略] 快

使用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


This excellent purrr tutorial 强调了使用purrr 时不必显式写出匿名函数的方便,以及特定类型的map 函数的好处。

【讨论】:

  • 请注意,如果您使用function(x) x[[2]] 而不仅仅是2,它的速度会慢一些。所有这些额外的时间都是由于检查lapply 没有做。
  • 你不需要“匿名”函数。 [[ 是一个函数。你可以做lapply(list, "[[", 3)
  • @RichScriven 这很有意义。这确实简化了在 purrr 上使用 lapply 的语法。
  • as.numeric(sapply(...)) 是一件很奇怪的事情。使用vapplyvapply(..., FUN.VALUE = numeric(1))。这是从应用函数返回向量的基本 R 方法,并且还强制类型(如果您的函数没有返回正确的类型,则会引发错误)。这也导致比sapply/lapply 更好的性能,因为可以一次分配整个向量。 map_type 的唯一额外优势是它更适合初学者。
【解决方案3】:

如果我们不考虑品味(否则这个问题应该关闭)或语法一致性、风格等方面,答案是否定的,没有特殊理由使用map 代替lapply 或其他变体申请family,比如更严格的vapply

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

我不是在这里询问一个人对语法的喜欢或不喜欢, purrr 等提供的其他功能,但严格来说 purrr::map 与 lapply 的比较假设使用标准 评价

如果您不考虑 purrr 的语法或其他功能,则没有特殊理由使用 map。我自己使用purrr,我对哈德利的回答很好,但具有讽刺意味的是,它超越了 OP 预先声明的他没有问的事情。

【讨论】:

    【解决方案4】:

    tl;博士

    我不是在询问某人对 purrr 提供的语法或其他功能的好恶。

    选择与您的用例相匹配的工具,并最大限度地提高您的生产力。对于优先考虑速度的生产代码使用*apply,对于需要小内存占用的代码使用map。基于人体工程学,map可能更适合大多数用户和大多数一次性任务。

    方便

    2021 年 10 月更新 由于接受的答案和投票第二多的帖子都提到语法方便

    R 版本 4.1.1 和更高版本现在支持速记匿名函数 \(x) 和管道 |&gt; 语法。要检查您的 R 版本,请使用 version[['version.string']]

    library(purrr)
    library(repurrrsive)
    lapply(got_chars[1:2], `[[`, 2) |>
      lapply(\(.) . + 1)
    #> [[1]]
    #> [1] 1023
    #> 
    #> [[2]]
    #> [1] 1053
    map(got_chars[1:2], 2) %>%
      map(~ . + 1)
    #> [[1]]
    #> [1] 1023
    #> 
    #> [[2]]
    #> [1] 1053
    

    如果您的任务涉及对类似列表的对象进行 2 次以上的操作,purrr 方法的语法通常会更短。

    nchar(
    "lapply(x, fun, y) |>
          lapply(\\(.) . + 1)")
    #> [1] 45
    nchar(
    "library(purrr)
    map(x, fun) %>%
      map(~ . + 1)")
    #> [1] 45
    

    考虑到一个人可能在他们的职业生涯中写了数万或数十万个这样的电话,这种句法长度差异可能等同于写 1 或 2 部小说(av.小说80 000 个字母),如果输入了代码。进一步考虑 您的 代码输入速度(每分钟约 65 个字?),您的 输入准确度(您是否发现您经常错误输入某些语法 (\"&lt; ?),您的函数参数的回忆,然后您可以使用一种样式或两者的组合对您的生产力进行公平比较。

    另一个考虑因素可能是您的目标受众。就我个人而言,我发现 purrr::map 之所以比 lapply 更努力地工作,正是因为它的语法简洁。

    1 |>
      lapply(\(.z) .z + 1)
    #> [[1]]
    #> [1] 2
    
    1 %>%
      map(~ .z+ 1)
    #> Error in .f(.x[[i]], ...) : object '.z' not found
    
    but,
    1 %>%
      map(~ .+ 1)
    #> [[1]]
    #> [1] 2
    

    速度

    通常在处理类似列表的对象时,会执行多个操作。讨论中的细微差别purrr 的开销在大多数代码中是微不足道的 - 处理大型列表和用例。

    got_large <- rep(got_chars, 1e4) # 300 000 elements, 1.3 GB in memory
    bench::mark(
      base = {
        lapply(got_large, `[[`, 2) |>
          lapply(\(.) . * 1e5) |>
          lapply(\(.) . / 1e5) |>
          lapply(\(.) as.character(.))
      },
      purrr = {
        map(got_large, 2) %>%
          map(~ . * 1e5) %>%
          map(~ . / 1e5) %>%
          map(~ as.character(.))
      }, iterations = 100,
    )[c(1, 3, 4, 5, 7, 8, 9)]
    
    # A tibble: 2 x 7
      expression   median `itr/sec` mem_alloc n_itr  n_gc total_time
      <bch:expr> <bch:tm>     <dbl> <bch:byt> <int> <dbl>   <bch:tm>
    1 base          1.19s     0.807    9.17MB   100   301      2.06m
    2 purrr         2.67s     0.363    9.15MB   100   919      4.59m
    

    执行的操作越多,这就会产生分歧。如果您正在编写一些用户经常使用的代码或依赖于它的软件包,那么速度可能是您在 base R 和 purr 之间进行选择时需要考虑的重要因素。注意purrr 的内存占用略低。

    但是有一个反驳:如果你想要速度,请使用较低级别的语言。

    【讨论】:

      猜你喜欢
      • 2019-06-25
      • 1970-01-01
      • 2010-10-26
      • 1970-01-01
      • 2017-07-09
      • 1970-01-01
      • 1970-01-01
      • 2014-05-05
      • 1970-01-01
      相关资源
      最近更新 更多