Warum purrr :: map anstelle von lapply verwenden?

170

Gibt es einen Grund, warum ich verwenden sollte

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

anstatt

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

Die Ausgabe sollte dieselbe sein, und die von mir erstellten Benchmarks scheinen zu zeigen, dass sie lapplyetwas schneller sind (dies sollte so sein, wie es maperforderlich ist, um alle nicht standardmäßigen Bewertungseingaben zu bewerten).

Gibt es also einen Grund, warum ich in solch einfachen Fällen tatsächlich in Betracht ziehen sollte, zu zu wechseln purrr::map? Ich frage hier nicht nach den Vorlieben oder Abneigungen bezüglich der Syntax, anderer Funktionen, die von purrr usw. bereitgestellt werden, sondern ausschließlich nach dem Vergleich purrr::mapmit der lapplyAnnahme, dass die Standardbewertung verwendet wird, d map(<list-like-object>, function(x) <do stuff>). H. Gibt es einen Vorteil purrr::mapin Bezug auf Leistung, Ausnahmebehandlung usw.? Die Kommentare unten deuten darauf hin, dass dies nicht der Fall ist, aber vielleicht könnte jemand etwas mehr ausarbeiten?

Tim
quelle
8
Halten Sie sich in der Tat für einfache Anwendungsfälle besser an Basis R und vermeiden Sie Abhängigkeiten. Wenn Sie bereits die Last tidyverseobwohl, können Sie aus dem Rohr profitieren %>%und anonymen Funktionen ~ .x + 1Syntax
Aurèle
49
Das ist so ziemlich eine Frage des Stils. Sie sollten jedoch wissen, was die Basis-R-Funktionen tun, da all dieses ordentliche Zeug nur eine Hülle darüber ist. Irgendwann wird diese Schale brechen.
Hong Ooi
9
~{}Abkürzung Lambda (mit oder ohne die {}Dichtungen für mich den Deal für Ebene purrr::map(). Die Art-Durchsetzung der purrr::map_…()ist handlich und weniger stumpf als vapply(). purrr::map_df()eine super teuer Funktion ist aber auch vereinfacht Code. Es gibt absolut nichts falsch mit mit Sockel R kleben [lsv]apply(), obwohl .
hrbrmstr
4
Vielen Dank für die Frage - solche Sachen, die ich mir auch angesehen habe. Ich benutze R seit mehr als 10 Jahren und benutze definitiv nichts purrr. Mein Punkt ist folgender: tidyverseist fabelhaft für Analysen / interaktive / Berichte, nicht für die Programmierung. Wenn Sie verwenden möchten lapplyoder mapdann programmieren, können Sie eines Tages ein Paket erstellen. Je weniger Abhängigkeiten, desto besser. Plus: Ich sehe manchmal Leute, die danach mapmit ziemlich obskurer Syntax arbeiten. Und jetzt, wo ich Leistungstests sehe: Wenn Sie an applyFamilie gewöhnt sind, bleiben Sie dabei.
Eric Lecoutre
4
Tim, den Sie geschrieben haben: "Ich frage hier nicht nach den Vorlieben oder Abneigungen gegenüber der Syntax, anderen Funktionen von purrr usw., sondern ausschließlich nach dem Vergleich von purrr :: map mit der Annahme, dass die Standardbewertung verwendet wird", und die Antwort, die Sie akzeptiert haben, lautet derjenige, der genau das durchgeht, was Sie gesagt haben, Sie wollten nicht, dass die Leute darüber hinweggehen.
Carlos Cinelli

Antworten:

230

Wenn die einzige Funktion, die Sie von purrr verwenden map(), nein ist, sind die Vorteile nicht wesentlich. Wie Rich Pauloo betont, map()sind die Helfer der Hauptvorteil, mit denen Sie kompakten Code für häufig auftretende Sonderfälle schreiben können:

  • ~ . + 1 ist äquivalent zu function(x) x + 1

  • list("x", 1)ist äquivalent zu function(x) x[["x"]][[1]]. Diese Helfer sind etwas allgemeiner als [[- siehe ?pluckfür Details. Für Datenrechtecke ist das .defaultArgument besonders hilfreich.

Aber die meiste Zeit verwenden Sie keine einzelne *apply()/ map() Funktion, sondern eine Reihe von Funktionen, und der Vorteil von purrr ist eine viel größere Konsistenz zwischen den Funktionen. Beispielsweise:

  • Das erste Argument dafür lapply()sind die Daten; Das erste Argument dafür mapply()ist die Funktion. Das erste Argument für alle Kartenfunktionen sind immer die Daten.

  • Mit vapply(), sapply(), und mapply()können Sie zu unterdrücken Namen auf den Ausgang wählen , mit USE.NAMES = FALSE; hat aber lapply()dieses Argument nicht.

  • Es gibt keine konsistente Möglichkeit, konsistente Argumente an die Mapper-Funktion weiterzugeben. Die meisten Funktionen verwenden ...aber mapply()verwendet MoreArgs(von denen Sie erwarten würden, dass sie aufgerufen werden MORE.ARGS), und und erwarten Map(), dass Sie eine neue anonyme Funktion erstellen. In Kartenfunktionen steht das konstante Argument immer hinter dem Funktionsnamen.Filter()Reduce()

  • Fast jede Purrr-Funktion ist typstabil: Sie können den Ausgabetyp ausschließlich anhand des Funktionsnamens vorhersagen. Dies gilt nicht für sapply()oder mapply(). Ja, das gibt es vapply(); aber es gibt kein Äquivalent für mapply().

Sie mögen denken, dass all diese kleinen Unterschiede nicht wichtig sind (so wie manche Leute denken, dass es keinen Vorteil hat, Stringr gegenüber regulären Ausdrücken der Basis R zu verwenden), aber meiner Erfahrung nach verursachen sie unnötige Reibung beim Programmieren (die unterschiedlichen Argumentreihenfolgen, die immer zum Auslösen verwendet wurden me up), und sie erschweren das Erlernen funktionaler Programmiertechniken, da Sie neben den großen Ideen auch eine Reihe von zufälligen Details lernen müssen.

Purrr füllt auch einige praktische Kartenvarianten aus, die in Basis R fehlen:

  • modify()behält den Datentyp bei, mit [[<-dem "an Ort und Stelle" geändert wird. In Verbindung mit der _ifVariante ermöglicht dies (IMO schöne) Code wiemodify_if(df, is.factor, as.character)

  • map2()ermöglicht es Ihnen, gleichzeitig über xund abzubilden y. Dies macht es einfacher, Ideen wie auszudrücken map2(models, datasets, predict)

  • imap()Ermöglicht die gleichzeitige Zuordnung zu xund seinen Indizes (entweder Namen oder Positionen). Dies macht es einfach, (z. B.) alle csvDateien in ein Verzeichnis zu laden und jeder filenameSpalte eine Spalte hinzuzufügen .

    dir("\\.csv$") %>%
      set_names() %>%
      map(read.csv) %>%
      imap(~ transform(.x, filename = .y))
  • walk()gibt seine Eingabe unsichtbar zurück; und ist nützlich, wenn Sie eine Funktion wegen ihrer Nebenwirkungen aufrufen (dh Dateien auf die Festplatte schreiben).

Ganz zu schweigen von den anderen Helfern wie safely()und partial().

Persönlich finde ich, dass ich mit purrr Funktionscode mit weniger Reibung und größerer Leichtigkeit schreiben kann. es verringert die Kluft zwischen dem Ausdenken und der Umsetzung einer Idee. Ihr Kilometerstand kann jedoch variieren. Es ist nicht nötig, purrr zu verwenden, es sei denn, es hilft Ihnen tatsächlich.

Mikrobenchmarks

Ja, map()ist etwas langsamer als lapply(). Die Kosten für die Verwendung von map()oder lapply()hängen jedoch von dem ab, was Sie zuordnen, und nicht vom Aufwand für die Durchführung der Schleife. Das unten stehende Mikrobenchmark deutet darauf hin, dass die Kosten im map()Vergleich zu lapply()etwa 40 ns pro Element liegen, was die meisten R-Codes wahrscheinlich nicht wesentlich beeinflusst.

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
Hadley
quelle
2
Wollten Sie in diesem Beispiel transform () verwenden? Wie in Base R transform (), oder fehlt mir etwas? transform () gibt Ihnen den Dateinamen als Faktor an, der Warnungen generiert, wenn Sie (natürlich) Zeilen zusammenbinden möchten. mutate () gibt mir die Zeichenspalte der gewünschten Dateinamen. Gibt es einen Grund, es dort nicht zu benutzen?
DoctorG
2
Ja, besser zu verwenden mutate(), ich wollte nur ein einfaches Beispiel ohne andere Deps.
Hadley
Sollte die Typspezifität nicht irgendwo in dieser Antwort auftauchen? map_*hat mich dazu gebracht, purrrviele Skripte zu laden . Es hat mir bei einigen Aspekten des Kontrollflusses meines Codes geholfen ( stopifnot(is.data.frame(x))).
Fr.
2
ggplot und data.table sind großartig, aber brauchen wir wirklich ein neues Paket für jede einzelne Funktion in R?
adn bps
57

Vergleichen purrrund lapplyauf Komfort und Geschwindigkeit hinauslaufen .


1. purrr::mapist syntaktisch bequemer als lapply

Extrahieren Sie das zweite Element der Liste

map(list, 2)  

welche als @F. Privé wies darauf hin, ist das gleiche wie:

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

mit lapply

lapply(list, 2) # doesn't work

wir müssen eine anonyme Funktion übergeben ...

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

... oder wie @RichScriven betonte, gehen wir [[als Argument inlapply

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

Wenn Sie also feststellen, dass Sie Funktionen auf viele Listen anwenden lapplyund entweder eine benutzerdefinierte Funktion definieren oder eine anonyme Funktion schreiben möchten, ist die Bequemlichkeit ein Grund, sich für eine Bevorzugung zu entscheiden purrr.

2. Typspezifische Kartenfunktionen einfach viele Codezeilen

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

Jede dieser typspezifischen Kartenfunktionen gibt eine atomare Liste (Vektor) zurück und nicht die von map()und zurückgegebenen Listen lapply(). Wenn Sie mit verschachtelten Listen von Atomvektoren arbeiten, können Sie diese typspezifischen Kartenfunktionen verwenden, um die Vektoren direkt herauszuziehen und Vektoren direkt in int-, dbl- und chr-Vektoren zu zwingen. Die Basis R - Version würde in etwa so aussehen as.numeric(sapply(...)), as.character(sapply(...))etc.

Die map_<type>Funktionen haben auch die nützliche Eigenschaft, dass sie fehlschlagen, wenn sie keinen Atomvektor des angegebenen Typs zurückgeben können. Dies ist nützlich, wenn Sie einen Kontrollfluss definieren, bei dem eine Funktion fehlschlagen soll, wenn sie [irgendwie] den falschen Objekttyp generiert.

3. Bequemlichkeit beiseite, lapplyist [etwas] schneller alsmap

Verwenden purrrder Komfortfunktionen als @F. Privé wies darauf hin, dass die Verarbeitung etwas langsamer wird. Lassen Sie uns jeden der 4 Fälle, die ich oben vorgestellt habe, fahren.

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

Geben Sie hier die Bildbeschreibung ein

Und der Gewinner ist....

lapply(list, `[[`, 2)

In der Summe, wenn Sie nach roher Geschwindigkeit suchen: base::lapply (obwohl es nicht viel schneller ist)

Für einfache Syntax und Ausdruckbarkeit: purrr::map


Dieses hervorragende purrrTutorial zeigt die Bequemlichkeit , anonyme Funktionen bei der Verwendung nicht explizit ausschreiben zu müssen purrr, und die Vorteile typspezifischer mapFunktionen.

Reicher Pauloo
quelle
2
Beachten Sie , dass es weniger langsam ist , wenn Sie function(x) x[[2]]statt nur verwenden 2. All diese zusätzliche Zeit ist auf Überprüfungen zurückzuführen, lapplydie nicht funktionieren.
F. Privé
17
Sie "brauchen" keine anonymen Funktionen. [[ist eine Funktion. Das kannst du machen lapply(list, "[[", 3).
Rich Scriven
@RichScriven das macht Sinn. Dies vereinfacht die Syntax für die Verwendung von lapply over purrr.
Rich Pauloo
37

Wenn wir Aspekte des Geschmacks (andernfalls sollte diese Frage geschlossen werden) oder der Syntaxkonsistenz, des Stils usw. nicht berücksichtigen, lautet die Antwort "Nein". Es gibt keinen besonderen Grund, mapanstelle lapplyoder anderer Varianten der Apply-Familie, wie z. B. der strengeren, zu verwenden vapply.

PS: Denken Sie daran, dass das OP für diese Leute, die unentgeltlich abstimmen, geschrieben hat:

Ich frage hier nicht nach den Vorlieben oder Abneigungen bezüglich der Syntax, anderer Funktionen von purrr usw., sondern ausschließlich nach dem Vergleich von purrr :: map mit der Annahme, dass die Standardbewertung verwendet wird

Wenn Sie weder die Syntax noch andere Funktionen von berücksichtigen purrr, gibt es keinen besonderen Grund für die Verwendung map. Ich benutze purrrmich selbst und bin mit Hadleys Antwort einverstanden, aber es geht ironischerweise genau um die Dinge, die das OP im Voraus angegeben hat, die er nicht gefragt hat.

Carlos Cinelli
quelle