Dlaczego warto używać purrr:: map zamiast lapply?

Czy Jest jakiś powód, dla którego powinienem używać

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

Zamiast

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

Wynik powinien być taki sam, a benchmarki, które zrobiłem, wydają się pokazywać, że lapply jest nieco szybszy (powinien być tak, jak map musi ocenić wszystkie niestandardowe dane wejściowe oceny).

Czy jest jakiś powód, dla którego w tak prostych przypadkach powinienem rozważyć przejście na purrr::map? Nie pytam tutaj o upodobania lub niechęci do składni, innych funkcjonalności dostarczanych przez purrr itp., ale ściśle o porównaniu purrr::map z lapply zakładając użycie standardowej oceny, tj. map(<list-like-object>, function(x) <do stuff>). Czy jest jakaś przewaga purrr::map pod względem wydajności, obsługi wyjątków itp.? Poniższe komentarze sugerują, że nie, ale może ktoś mógłby rozwinąć trochę więcej?

 106
Author: Tim, 2017-07-14

3 answers

Jeśli jedyną funkcją używaną przez purrr jest map(), to nie, korzyści nie są znaczące. Jak zaznacza Rich Pauloo, głównym zaletą map() są helpery, które pozwalają na pisanie kompaktowych kod typowych przypadków szczególnych:

  • ~ . + 1 jest odpowiednikiem function(x) x + 1

  • list("x", 1) jest równoważne function(x) x[["x"]][[1]]. Te pomocnicy są nieco bardziej ogólni niż [[ - Zobacz ?pluck po szczegóły. Dla danych rectangling , the .default argument jest szczególnie pomocne.

Ale przez większość czasu nie używasz jednego *apply() / map() funkcji, używasz kilku z nich, a zaletą purrr jest znacznie większa spójność między funkcjami. Na przykład:

  • Pierwszym argumentem do lapply() są dane; pierwszym argumentem do mapply() jest funkcją. Pierwszy argument do wszystkich funkcji map jest zawsze dane.

  • Z vapply(), sapply(), i mapply() możesz wybrać wymazywanie nazw na wyjście z USE.NAMES = FALSE; ale lapply() nie ma takiego argumentu.

  • Nie ma spójnego sposobu przekazywania spójnych argumentów na funkcja mapera. Większość funkcji używa ..., ale mapply() używa MoreArgs (którego można się spodziewać, że będzie się nazywać MORE.ARGS), oraz Map(), Filter() i Reduce() oczekuję, że stworzysz nowy funkcja anonimowa. W funkcjach map zawsze pojawia się argument stały po nazwie funkcji.

  • Prawie każda funkcja purrr jest typu stabilna: można przewidzieć na Typ wyjścia wyłącznie z nazwy funkcji. Nie jest to prawdą dla sapply() lub mapply(). Tak, jest vapply(); ale nie ma odpowiednik dla mapply().

Możesz myśleć, że wszystkie te drobne rozróżnienia nie są ważne (tak jak niektórzy myślą, że nie ma przewagi stringr nad bazowych wyrażeń regularnych R), ale z mojego doświadczenia wynika, że powodują niepotrzebne tarcia podczas programowania (odmienne rozkazy argumentów zawsze używane do trip me up), i sprawiają, że funkcjonalne techniki programowania trudniejsze do ucz się, ponieważ oprócz wielkich pomysłów, Musisz również nauczyć się kilku przypadkowych szczegółów.

Purrr wypełnia również kilka przydatnych wariantów map, których nie ma w bazie R:

  • modify() zachowuje typ danych używając [[<- do modyfikacji "in miejsce". W połączeniu z wariantem _if pozwala to na (IMO piękny) Kod jak modify_if(df, is.factor, as.character)

  • map2() pozwala na jednoczesne mapowanie x i y. To sprawia, że łatwiej jest wyrażać idee takie jak map2(models, datasets, predict)

  • imap() pozwala na jednoczesne mapowanie x i jego indeksów (nazwiska lub stanowiska). To ułatwia (np.) załadowanie wszystkich csv pliki w katalogu, dodając do każdego kolumny filename.

    dir("\\.csv$") %>%
      set_names() %>%
      map(read.csv) %>%
      imap(~ transform(.x, filename = .y))
    
  • walk() zwraca jego wejście niewidocznie; i jest przydatny, gdy jesteś wywołanie funkcji dla jej efektów ubocznych (np. zapisywanie plików do dysk).

Nie wspominając o innych pomocnikach jak safely() i partial().

Osobiście uważam, że kiedy używam purrr, mogę napisać kod funkcjonalny z mniejszym tarciem i większą łatwością; zmniejsza lukę między wymyślanie pomysłu i wdrażanie go. Ale przebieg może się różnić; nie ma potrzeby, aby używać purrr, chyba że rzeczywiście pomaga.

Microbenchmarks

Tak, map() jest nieco wolniejszy niż lapply(). Ale koszt korzystania map() lub lapply() jest napędzany przez to, co mapujesz, a nie nad głową wykonywania pętli. Na microbenchmark poniżej sugeruje, że koszt z map() w porównaniu do lapply() wynosi około 40 ns na pierwiastek, co wydaje się mało prawdopodobne, aby miało to istotny wpływ na większość kodu 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
 172
Author: hadley,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-25 19:17:59

Ten internetowy samouczek purrr podkreśla wygodę braku konieczności jawnego wypisywania anonimowych funkcji podczas korzystania z purrr, co wraz z funkcjami map specyficznymi dla typu czyni go bardzo funkcjonalnym.

1. purrr:: mapa jest składniowo o wiele wygodniejsza niż lapply

Wyodrębnij drugi element listy

map(list, 2)  # and it's done like magic

Który jak zauważył @F. Privé, jest taki sam jak:

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

Z lapply

lapply(list, 2) # doesn't work

Musimy przekazać mu funkcję anonimową

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

Lub, jak zauważył @RichScriven, możemy po prostu przekazać [[ jako argument do lapply

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

W tle, purr przyjmuje wektor liczbowy lub znakowy jako argument i używa go jako funkcji podzbiorowej. Jeśli robisz wiele podzestawów list za pomocą lapply i męczysz się albo definiowaniem niestandardowej funkcji, albo pisaniem anonimowej funkcji dla subsetting, wygoda jest jednym z powodów, aby przejść do purrr.

2. Specyficzne dla typu funkcje mapy po prostu wiele linii kodu

  • map_chr ()
  • map_lgl ()
  • map_int ()
  • map_dbl ()
  • map_df () - Moje ulubione, zwraca ramkę danych.

Każda z tych funkcji map specyficznych dla typu zwraca listę atomową, a nie listę, która map() i lapply() zwracają automatycznie. Jeśli masz do czynienia z listami zagnieżdżonymi, które mają wektory atomowe wewnątrz, możesz użyć tych specyficznych dla danego typu funkcji map do wyciągania wektorów bezpośrednio lub zmuszania wektorów do wektorów int, dbl, chr. Kolejny punkt dla wygody i funkcjonalności.

3. Pomijając wygodę, lapply jest szybszy niż Mapa.

Używanie funkcji purrr convenience, jak zauważył @F. Privé, spowalnia nieco przetwarzanie. Ścigajmy się z każdym z 4 przypadków, które przedstawiłem powyżej.

# 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)

Tutaj wpisz opis obrazka

A zwycięzcą jest....

lapply(list, `[[`, 2)

W sumie, jeśli prędkość jest tym, czego szukasz: base::lapply

If simple syntax is your jam: purrr:: map

 35
Author: Rich Pauloo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-13 01:18:11

Jeśli nie weźmiemy pod uwagę aspektów smaku (inaczej to pytanie powinno być zamknięte) lub spójności składni, stylu itp., odpowiedź brzmi Nie, Nie ma specjalnego powodu, aby używać map zamiast lapply lub innych wariantów rodziny zastosuj, takich jak bardziej rygorystyczne vapply.

PS: do tych ludzi bezinteresownie upominających się, tylko pamiętajcie, że OP napisał:

Nie pytam tutaj o czyjeś upodobania lub niechęci do składni, inne funkcjonalności oferowane przez purrr itp., ale ściśle o porównanie mapy purrr:: z mapą lapply przy założeniu użycia standardu ocena

Jeśli nie bierzesz pod uwagę składni ani innych funkcjonalności purrr, nie ma specjalnego powodu, aby używać map. Sam używam purrr i nie mam nic przeciwko odpowiedzi Hadleya, ale jak na ironię chodzi o to, co powiedział OP z góry, o co nie pytał.

 21
Author: Carlos Cinelli,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-06 18:16:03