Po co używać purrr :: map zamiast lapply?


171

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 stworzyłem, wydają się wskazywać, że lapplyjest nieco szybszy (powinno być tak samo, jak maptrzeba, aby ocenić wszystkie niestandardowe dane wejściowe do oceny).

Czy jest więc jakiś powód, dla którego w tak prostych przypadkach powinienem rozważyć przejście na purrr::map? Nie pytam tutaj o swoje upodobania lub antypatie dotyczące składni, innych funkcjonalności, które zapewnia mruczenie itp., A stricte o porównanie purrr::mapz lapplyzałożeniem, że stosuje się ocenę standardową, tj map(<list-like-object>, function(x) <do stuff>). Czy jest jakaś korzyść purrr::mappod względem wydajności, obsługi wyjątków itp.? Poniższe komentarze sugerują, że tak nie jest, ale może ktoś mógłby rozwinąć trochę więcej?


8
Rzeczywiście w prostych przypadkach użycia lepiej trzymać się podstawowego R i unikać zależności. Jeśli już załadowałeś tidyverse, możesz skorzystać ze składni %>%~ .x + 1
potoków

49
To w dużej mierze kwestia stylu. Powinieneś jednak wiedzieć, co robią podstawowe funkcje R, ponieważ wszystkie te uporządkowane rzeczy to tylko powłoka na wierzchu. W pewnym momencie ta powłoka pęknie.
Hong Ooi,

9
~{}skrót lambda (z {}pieczęciami lub bez, to dla mnie sprawa dla zwykłego purrr::map(). Egzekwowanie typów purrr::map_…()jest poręczne i mniej rozwlekłe niż vapply(). purrr::map_df()jest bardzo kosztowną funkcją, ale także upraszcza kod. Nie ma absolutnie nic złego w trzymaniu się bazy R [lsv]apply(), chociaż ,
hrbrmstr

4
Dziękuję za pytanie - takie rzeczy też przyjrzałem się. Używam R od ponad 10 lat i definitywnie nie używam i nie będę używać purrrrzeczy. Chodzi mi o to: tidyversejest fantastyczny do analiz / interaktywnych / raportów, a nie do programowania. Jeśli musisz używać lapplylub mapprogramujesz i pewnego dnia możesz skończyć z utworzeniem pakietu. Im mniej zależności, tym lepiej. Plus: czasami widzę ludzi używających mappóźniej dość niejasnej składni. A teraz, gdy widzę testy wydajności: jeśli jesteś przyzwyczajony do applyrodziny: trzymaj się tego.
Eric Lecoutre

4
Tim pisałeś: "Nie pytam tutaj o czyjeś upodobania lub antypatie dotyczące składni, innych funkcjonalności dostarczanych przez mruczenie itp., Ale stricte o porównanie mruczenia :: map z lapply zakładając użycie standardowej oceny" i odpowiedzią, którą zaakceptowałeś jest ten, który dokładnie omawia to, co powiedziałeś, że nie chcesz, aby ludzie przeszli.
Carlos Cinelli

Odpowiedzi:


232

Jeśli jedyną funkcją, której używasz w mruczeniu, jest map(), to nie, korzyści nie są znaczące. Jak podkreśla Rich Pauloo, główną zaletą programu map()są pomocniki, które pozwalają pisać zwarty kod dla typowych, specjalnych przypadków:

  • ~ . + 1 jest równa function(x) x + 1

  • list("x", 1)jest równoważne function(x) x[["x"]][[1]]. Te pomocniki są nieco bardziej ogólne niż [[- zobacz ?pluckszczegóły. Dla rectangling danych The .defaultargument jest szczególnie pomocne.

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

  • Pierwszym argumentem lapply()są dane; pierwszym argumentem mapply()jest funkcja. Pierwszym argumentem wszystkich funkcji mapowania są zawsze dane.

  • Z vapply(), sapply()i mapply()można wybrać do tłumienia nazwisk na wyjściu z USE.NAMES = FALSE; ale lapply()nie ma tego argumentu.

  • Nie ma spójnego sposobu przekazywania spójnych argumentów do funkcji mapowania. Większość funkcji korzystać ..., ale mapply()zastosowania MoreArgs(co można oczekiwać, aby nazwać MORE.ARGS), i Map(), Filter()i Reduce()oczekiwać, aby utworzyć nową funkcję anonimową. W funkcjach odwzorowujących stały argument zawsze występuje po nazwie funkcji.

  • Prawie każda funkcja mruczenia jest stabilna: typ wyniku można przewidzieć wyłącznie na podstawie nazwy funkcji. Nie dotyczy to sapply()lub mapply(). Tak, jest vapply(); ale nie ma odpowiednika dla mapply().

Możesz pomyśleć, że wszystkie te pomniejsze rozróżnienia nie są ważne (tak jak niektórzy myślą, że nie ma żadnej przewagi nad wyrażeniami regularnymi typu R), ale z mojego doświadczenia wynika, że ​​powodują one niepotrzebne tarcia podczas programowania (różne porządki argumentów zawsze używane ja w górę) i utrudniają naukę technik programowania funkcjonalnego, ponieważ oprócz wielkich pomysłów trzeba też nauczyć się wielu przypadkowych szczegółów.

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

  • modify()zachowuje typ danych przy użyciu [[<-do modyfikacji „w miejscu”. W połączeniu z _ifwariantem pozwala to na (piękny IMO) kod taki jakmodify_if(df, is.factor, as.character)

  • map2()pozwala na jednoczesne mapowanie nad xi y. Ułatwia to wyrażanie pomysłów, takich jak map2(models, datasets, predict)

  • imap()umożliwia jednoczesne mapowanie nad xi jego indeksów (nazw lub pozycji). Ułatwia to (np.) Ładowanie wszystkich csvplików w katalogu, dodając filenamedo każdego kolumnę.

    dir("\\.csv$") %>%
      set_names() %>%
      map(read.csv) %>%
      imap(~ transform(.x, filename = .y))
  • walk()zwraca swoje dane wejściowe niewidocznie; i jest przydatne, gdy wywołujesz funkcję ze względu na jej skutki uboczne (np. zapisywanie plików na dysku).

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

Osobiście uważam, że kiedy używam mruczenia, mogę pisać funkcjonalny kod z mniejszym tarciem i większą łatwością; zmniejsza dystans między wymyśleniem pomysłu a jego wdrożeniem. Ale Twój przebieg może się różnić; nie ma potrzeby używania mruczenia, chyba że faktycznie ci to pomaga.

Microbenchmarks

Tak, map()jest nieco wolniejszy niż lapply(). Ale koszt używania map()lub lapply()jest zależny od tego, co mapujesz, a nie od kosztów ogólnych związanych z wykonywaniem pętli. Poniższy mikropunkt sugeruje, że koszt w map()porównaniu do lapply()wynosi około 40 ns na element, co wydaje się mało prawdopodobne, aby miało 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

2
Czy chodziło Ci o użycie transform () w tym przykładzie? Jak w podstawowej transformacji R (), czy czegoś mi brakuje? transform () podaje nazwę pliku jako czynnik, który generuje ostrzeżenia, gdy (naturalnie) chcesz połączyć ze sobą wiersze. mutate () daje mi kolumnę znaków nazw plików, które chcę. Czy jest powód, aby go tam nie używać?
doctorG

2
Tak, lepiej użyć mutate(), chciałem tylko prostego przykładu bez innych deps.
hadley

Czy gdzieś w tej odpowiedzi nie powinna pojawić się specyfika typu? map_*jest tym, co sprawiło, że ładowałem się purrrw wielu skryptach. Pomogło mi to z niektórymi aspektami „przepływu kontroli” w moim kodzie ( stopifnot(is.data.frame(x))).
ks.

2
ggplot i data.table są świetne, ale czy naprawdę potrzebujemy nowego pakietu dla każdej funkcji w R?
adn bps

58

Porównywanie purrri lapplysprowadza się do wygody i szybkości .


1. purrr::mapjest wygodniejszy syntaktycznie niż lapply

wyodrębnij drugi element listy

map(list, 2)  

który jako @F. Privé wskazał, jest tym samym, co:

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

z lapply

lapply(list, 2) # doesn't work

musimy przekazać anonimową funkcję ...

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

... lub jak wskazał @RichScriven, przekazujemy [[jako argument dolapply

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

Jeśli więc stosujesz funkcje do wielu list przy użyciu lapplyi męczysz się definiowaniem funkcji niestandardowej lub pisaniem funkcji anonimowej, wygoda jest jednym z powodów do faworyzowania purrr.

2. Mapa specyficzna dla typu działa po prostu w wielu wierszach kodu

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

Każda z tych funkcji mapowania specyficznych dla typu zwraca wektor, a nie listy zwracane przez map()i lapply(). Jeśli masz do czynienia z zagnieżdżonymi listami wektorów, możesz użyć tych funkcji mapowania specyficznych dla typu, aby bezpośrednio wyciągnąć wektory i przekształcić wektory bezpośrednio do wektorów int, dbl, chr. Wersja bazowa R będzie wyglądać podobnie as.numeric(sapply(...)), as.character(sapply(...))itp

Te map_<type>funkcje mają również użyteczną jakość, jeśli nie mogą powrócić wektor atomowy wskazanego typu, one niepowodzeniem. Jest to przydatne podczas definiowania ścisłej kontroli przepływu, gdy chcesz, aby funkcja zakończyła się niepowodzeniem, jeśli [w jakiś sposób] generuje niewłaściwy typ obiektu.

3. Pomijając wygodę, lapplyjest [nieco] szybszy niżmap

Korzystanie purrrz wygodnych funkcji, takich jak @F. Privé zwrócił nieco uwagę na spowolnienie przetwarzania. Wyścig 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)

wprowadź opis obrazu tutaj

A zwycięzcą jest....

lapply(list, `[[`, 2)

Podsumowując, jeśli szukasz surowej prędkości: base::lapply(chociaż nie jest to dużo szybsze)

Aby uzyskać prostą składnię i wyrazistość: purrr::map


Ten doskonały purrrsamouczek podkreśla wygodę wynikającą z braku konieczności jawnego pisania anonimowych funkcji podczas używania purrroraz zalety mapfunkcji specyficznych dla typu .


2
Zauważ, że jeśli użyjesz function(x) x[[2]]zamiast just 2, będzie to mniej wolne. Cały ten dodatkowy czas wynika z kontroli, które się lapplynie sprawdzają .
F. Privé

17
Nie potrzebujesz "anonimowych" funkcji. [[jest funkcją. Możesz to zrobić lapply(list, "[[", 3).
Rich Scriven

@RichScriven to ma sens. To upraszcza składnię używania lapply zamiast purrr.
Rich Pauloo

37

Jeśli nie weźmiemy pod uwagę aspektów gustu (w przeciwnym razie pytanie to powinno zostać zamknięte) lub spójności składni, stylu itp., Odpowiedź brzmi nie, nie ma specjalnego powodu, aby używać mapzamiast lapplylub innych wariantów rodziny stosowanej, takich jak bardziej rygorystyczne vapply.

PS: Dla tych ludzi, którzy niesłusznie przegłosowali, pamiętajcie tylko, że OP napisał:

Nie pytam tutaj o czyjeś upodobania lub antypatie dotyczące składni, innych funkcjonalności dostarczanych przez mruczenie itp., Ale stricte o porównanie mruczenia :: map z lapply zakładając użycie standardowej oceny

Jeśli nie bierzesz pod uwagę składni ani innych funkcji purrr, nie ma specjalnego powodu, aby używać map. Używam purrrsiebie i nie przeszkadza mi odpowiedź Hadleya, ale ironicznie omawia ona te same rzeczy, które OP stwierdził z góry, o które nie pytał.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.