Подтвердить что ты не робот

Зачем использовать purrr:: map вместо lapply?

Есть ли причина, по которой я должен использовать

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

вместо

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

вывод должен быть таким же, и результаты тестов, которые я сделал, показывают, что lapply немного быстрее (он должен быть как map должен оценивать все входные данные нестандартной оценки).

Итак, есть ли причина, почему для таких простых случаев я должен действительно рассмотреть возможность переключения на purrr::map? Я не спрашиваю здесь о симпатиях или симпатиях к синтаксису, других функциях, предоставляемых purrr и т.д., Но строго о сравнении purrr::map с lapply, предполагая использование стандартной оценки, т.е. map(<list-like-object>, function(x) <do stuff>). Есть ли преимущество, которое purrr::map имеет в плане производительности, обработки исключений и т.д.? Комментарии ниже показывают, что это не так, но, может быть, кто-то может разработать немного больше?

4b9b3361

Ответ 1

Если единственной функцией, которую вы используете из purrr, является map(), то нет, преимущества не существенны. Как указывает Рич Пауло, главное преимущество map() - это помощники, которые позволяют писать компактный код для общих особых случаев:

  • ~. + 1 ~. + 1 эквивалентно function(x) x + 1

  • list("x", 1) эквивалентен function(x) x[["x"]][[1]]. Эти помощники являются более общими, чем [[ - смотрите ?pluck для деталей. Для прямоугольника .default аргумент .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, если это не поможет вам.

Microbenchmarks

Да, map() немного медленнее, чем lapply(). Но стоимость использования map() или lapply() зависит от того, что вы отображаете, а не от затрат на выполнение цикла. Микробенчмарк ниже показывает, что стоимость map() по сравнению с lapply() составляет около 40 нс на элемент, что вряд ли окажет существенное влияние на большую часть кода 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

Сравнение purrr и lapply сводится к удобству и скорости.


1. purrr::map синтаксически удобнее, чем lapply

извлечь второй элемент списка

map(list, 2)  

который как @F. Приве указал, так же, как:

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(...)) и т.д. Это дает purrr еще один момент для удобства и функциональности.

3. Удобство в стороне, lapply, [немного] быстрее, чем map

Используя удобные функции purrr, как @F. Приве отметил, что немного замедляет обработку. Пусть гонятся за каждым из 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)

enter image description here

И победитель....

lapply(list, '[[', 2)

В общем, если вам base::lapply скорость: base::lapply (хотя она не намного быстрее)

Для простого синтаксиса и выразимости: purrr::map


Это превосходное purrr пособие по purrr подчеркивает удобство отсутствия явной записи анонимных функций при использовании purrr и преимущества map функций, зависящих от типа.

Ответ 3

Если мы не рассматриваем аспекты вкуса (иначе этот вопрос должен быть закрыт) или согласованность синтаксиса, стиль и т.д., ответ отрицательный, нет специальной причины использовать map вместо lapply или других вариантов примените семейство, например, более строгий vapply.

PS: Для тех, кто безвозмездно бежит, просто помните, что OP писал:

Я не спрашиваю здесь об одном симпатии или неприязнь к синтаксису, другие функции, предоставляемые purrr и т.д., но строго о сравнение purrr:: map с lapply, предполагающее использование стандартного оценка

Если вы не рассматриваете синтаксис и другие функции purrr, нет никакой особой причины использовать map. Я использую purrr сам, и я в порядке с ответом Хэдли, но он иронизирует те самые вещи, о которых заявлял OP заранее, о которых он не спрашивал.