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

map(, function(x) )

вместо

lapply(, function(x) )

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

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

Tim

Ответов: 4

Ответы (4)

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

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

  • list ("x", 1) эквивалентно function (x) x [["x"]] [[1]]. Эти помощники немного более общие, чем [[ - подробности см. в ? pluck. Для данных прямоугольник, Аргумент.default особенно полезен.

Но в большинстве случаев вы не используете единственную * apply ()/map () функции, вы используете несколько из них, а преимущество мурлыканья заключается в гораздо большая согласованность между функциями. Например:

  • The first argument to lapply() is the data; the first argument to mapply() is the function. The first argument to all map functions is always the data.

  • With vapply(), sapply(), and mapply() you can choose to suppress names on the output with USE.NAMES = FALSE; but lapply() doesn't have that argument.

  • There's no consistent way to pass consistent arguments on to the mapper function. Most functions use ... but mapply() uses MoreArgs (which you'd expect to be called MORE.ARGS), and Map(), Filter() and Reduce() expect you to create a new anonymous function. In map functions, constant argument always come after the function name.

  • Almost every purrr function is type stable: you can predict the output type exclusively from the function name. This is not true for sapply() or mapply(). Yes, there is vapply(); but there's no equivalent for mapply().

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

Purrr также заполняет некоторые удобные варианты карт, которые отсутствуют в базовом R:

  • modify () сохраняет тип данных, используя [[<- для изменения »в place ". В сочетании с вариантом _if это позволяет (IMO красивый) код вроде modify_if (df, is.factor, as.character)

  • map2 () позволяет отображать одновременно более x и y. Этот упрощает выражение таких идей, как map2 (модели, наборы данных, прогноз)

  • imap () позволяет отображать одновременно более x и его индексы (имена или должности). Это упрощает (например) загрузку всех csv файлы в каталоге, добавляя к каждому столбец имя файла.

    dir ("\\. Csv $")%>%
      set_names ()%>%
      карта (read.csv)%>%
      imap (~ преобразование (.x, имя_файла = .y))
    
  • walk () возвращает свой ввод невидимо; и полезно, когда ты вызов функции для ее побочных эффектов (т.е. запись файлов в диск).

Не говоря уже о других помощниках, таких как Safe () и partial ().

Лично я считаю, что когда я использую purrr, я могу писать функциональный код. с меньшим трением и большей легкостью; это уменьшает разрыв между придумывает идею и воплощает ее в жизнь. Но ваш пробег может отличаться; нет необходимости использовать purrr, если он вам не поможет.

Тесты микробенчмарков

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

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

PS: тем людям, которые безвозмездно отказываются от голосования, просто помните, что ОП написало:

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

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

tl; dr

Я не спрашиваю, нравится или не нравится синтаксис или другие функции, предоставляемые purrr.

Выберите инструмент, который соответствует вашему варианту использованияи максимизирует вашу производительность. Для производственного кода с приоритетом скорости используйте *, примените, для кода, требующего небольшого объема памяти, используйте map. С точки зрения эргономики map вероятно предпочтительнее для большинства пользователей и большинства одноразовых задач.

Удобство

обновление октябрь 2021 г. Поскольку и принятый ответ, и второй по количеству голосов синтаксис упоминания сообщения удобство:

Версии R 4.1.1 и выше теперь поддерживают сокращенную анонимную функцию \ (x) и синтаксис трубы |>. Чтобы проверить свою версию 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

Синтаксис для подхода purrr обычно короче для ввода, если ваша задача включает более двух манипуляций со списковыми объектами.

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

Учитывая, что человек мог бы написать десятки или сотни тысяч таких звонков за свою карьеру, эта разница в длине синтаксиса может равняться написанию 1 или 2 романов (средний роман 80 000 букв), учитывая набранный код. Далее рассмотрите вашу скорость ввода кода (~ 65 слов в минуту?), вашу точность ввода (вы обнаруживаете, что вы часто ошибочно набираете определенный синтаксис (\ "<?), ваш отзыв аргументов функции, тогда вы можете провести честное сравнение своей производительности, используя один стиль или их комбинацию.

Еще одним соображением может быть ваша целевая аудитория. Лично я нашел объяснение, как 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
              
1 base          1.19s     0.807    9.17MB   100   301      2.06m
2 purrr         2.67s     0.363    9.15MB   100   919      4.59m

Чем больше действий выполняется, тем больше расходится. Если вы пишете код, который обычно используется некоторыми пользователями, или пакеты зависят от этого, скорость может быть важным фактором, который следует учитывать при выборе между базовым R и мурлыканьем. Обратите внимание: purrr имеет немного меньший объем памяти.

Однако есть контраргумент: Если вам нужна скорость, перейдите на язык более низкого уровня.

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


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

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

map(list, 2)  

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

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

с внахлест

lapply(list, 2) # doesn't work

we need to pass an anonymous function...

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_ также обладают тем полезным качеством, что, если они не могут вернуть атомарный вектор указанного типа, они терпят неудачу. Это полезно при определении строгого потока управления, когда вы хотите, чтобы функция завершилась ошибкой, если она [каким-то образом] сгенерирует неправильный тип объекта.

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 (хотя это не намного быстрее)

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


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

2022 WebDevInsider