Perché usare purrr :: map invece di lapply?


171

C'è qualche motivo per cui dovrei usare

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

invece di

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

l'output dovrebbe essere lo stesso e i benchmark che ho fatto sembrano mostrare che lapplyè leggermente più veloce (dovrebbe essere come mapnecessità per valutare tutti gli input di valutazione non standard).

Quindi c'è qualche motivo per cui per casi così semplici dovrei davvero prendere in considerazione il passaggio a purrr::map? Non sto chiedendo qui riguardo ai propri gusti o antipatie sulla sintassi, altre funzionalità fornite da Purrr ecc., Ma rigorosamente sul confronto purrr::mapcon l' lapplyassunzione usando la valutazione standard, cioè map(<list-like-object>, function(x) <do stuff>). C'è qualche vantaggio purrr::mapin termini di prestazioni, gestione delle eccezioni ecc.? I commenti qui sotto suggeriscono che non lo è, ma forse qualcuno potrebbe elaborare un po 'di più?


8
Per casi d'uso semplici, meglio attenersi alla base R ed evitare dipendenze. Se si carica già il tidyversesebbene, si può beneficiare della sintassi di pipe %>%e funzioni anonime~ .x + 1
Aurèle

49
Questa è praticamente una questione di stile. Dovresti sapere cosa fanno le funzioni R di base, perché tutta questa roba ordinata è solo una shell sopra di essa. Ad un certo punto, quel guscio si romperà.
Hong Ooi,

9
~{}scorciatoia lambda (con o senza i {}sigilli l'affare per me in modo chiaro purrr::map(). L'applicazione del tipo purrr::map_…()è pratica e meno ottusa di vapply(). purrr::map_df()è una funzione super costosa ma semplifica anche il codice. Non c'è assolutamente nulla di sbagliato nel rimanere con la base R [lsv]apply(), sebbene .
hrbrmstr

4
Grazie per la domanda - tipo di cose che ho anche visto. Sto usando R da più di 10 anni e definitivamente non uso e non userò purrrcose. Il mio punto è il seguente: tidyverseè favoloso per analisi / contenuti interattivi / report, non per la programmazione. Se ti stai impegnando lapplyo mapstai programmando e potresti finire un giorno con la creazione di un pacchetto. Quindi meno dipendenze sono, meglio è. Inoltre: a volte vedo persone che usano mapcon sintassi abbastanza oscura dopo. E ora che vedo i test delle prestazioni: se sei abituato alla applyfamiglia: atteniti ad esso.
Eric Lecoutre,

4
Tim ha scritto: "Non sto chiedendo qui sui propri gusti o antipatie sulla sintassi, altre funzionalità fornite da purrr ecc., Ma rigorosamente sul confronto di purrr :: map con lapply ipotizzando di utilizzare la valutazione standard" e la risposta accettata è quello che ripercorre esattamente quello che hai detto che non volevi che la gente ripassasse.
Carlos Cinelli,

Risposte:


232

Se l'unica funzione che stai usando da Purrr è map(), allora no, i vantaggi non sono sostanziali. Come sottolinea Rich Pauloo, il vantaggio principale map()è rappresentato dagli helper che consentono di scrivere codice compatto per casi speciali comuni:

  • ~ . + 1 è equivalente a function(x) x + 1

  • list("x", 1)è equivalente a function(x) x[["x"]][[1]]. Questi helper sono un po 'più generali di [[- vedi ?pluckper i dettagli. Per il rettangolo di dati , l' .defaultargomento è particolarmente utile.

Ma la maggior parte delle volte non stai usando una singola *apply()/ map() funzione, ne stai usando un sacco, e il vantaggio di purrr è una consistenza molto maggiore tra le funzioni. Per esempio:

  • Il primo argomento a lapply()sono i dati; il primo argomento a mapply()è la funzione. Il primo argomento di tutte le funzioni della mappa sono sempre i dati.

  • Con vapply(), sapply()e mapply()puoi scegliere di sopprimere i nomi sull'output con USE.NAMES = FALSE; ma lapply()non ha questo argomento.

  • Non esiste un modo coerente per passare argomenti coerenti alla funzione mapper. La maggior parte delle funzioni utilizzano ..., ma mapply()usi MoreArgs(che ci si aspetta di essere chiamato MORE.ARGS), e Map(), Filter()e Reduce()si aspettano di creare una nuova funzione anonima. Nelle funzioni della mappa, l'argomento costante segue sempre il nome della funzione.

  • Quasi ogni funzione purrr è di tipo stabile: è possibile prevedere il tipo di output esclusivamente dal nome della funzione. Questo non è vero per sapply()o mapply(). Sì, c'è vapply(); ma non esiste un equivalente per mapply().

Potresti pensare che tutte queste distinzioni minori non siano importanti (proprio come alcune persone pensano che non ci sia alcun vantaggio nel dare forza alle espressioni regolari di base R), ma nella mia esperienza causano attriti inutili durante la programmazione (i diversi ordini di argomenti usati sempre per inciampare me up), e rendono le tecniche di programmazione funzionale più difficili da imparare perché oltre alle grandi idee, devi anche imparare un sacco di dettagli accidentali.

Purrr compila anche alcune utili varianti di mappe che sono assenti dalla base R:

  • modify()conserva il tipo di dati usando [[<-per modificare "sul posto". In combinazione con la _ifvariante, ciò consente un codice (IMO bello) comemodify_if(df, is.factor, as.character)

  • map2()consente di mappare contemporaneamente su xe y. Questo rende più facile esprimere idee come map2(models, datasets, predict)

  • imap()ti permette di mappare simultaneamente sopra xe i suoi indici (nomi o posizioni). Ciò semplifica il caricamento (ad es.) Di tutti i csvfile in una directory, aggiungendo una filenamecolonna a ciascuno.

    dir("\\.csv$") %>%
      set_names() %>%
      map(read.csv) %>%
      imap(~ transform(.x, filename = .y))
  • walk()restituisce il suo input in modo invisibile; ed è utile quando si chiama una funzione per i suoi effetti collaterali (ovvero la scrittura di file su disco).

Per non parlare degli altri aiutanti come safely()e partial().

Personalmente, trovo che quando uso purrr, posso scrivere codice funzionale con meno attrito e maggiore facilità; diminuisce il divario tra il concepimento di un'idea e la sua attuazione. Ma il tuo chilometraggio può variare; non è necessario usare purrr a meno che non ti aiuti davvero.

Microbenchmarks

Sì, map()è leggermente più lento di lapply(). Ma il costo dell'utilizzo map()o lapply()è determinato da ciò che stai mappando, non dal sovraccarico di eseguire il ciclo. Il microbenchmark di seguito suggerisce che il costo di map()rispetto a lapply()è di circa 40 ns per elemento, il che sembra improbabile che abbia un impatto sostanziale sulla maggior parte del codice 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
Intendevi usare transform () in quell'esempio? Come in base R transform (), o mi manca qualcosa? transform () fornisce il nome file come fattore, che genera avvisi quando (naturalmente) si desidera unire le righe. mutate () mi dà la colonna di caratteri dei nomi di file che desidero. C'è un motivo per non usarlo lì?
dottore G,

2
Sì, meglio usare mutate(), volevo solo un semplice esempio senza altri deps.
Hadley,

La specificità del tipo non dovrebbe apparire da qualche parte in questa risposta? map_*è quello che mi ha fatto caricare purrrin molti script. Mi ha aiutato con alcuni aspetti del "flusso di controllo" del mio codice ( stopifnot(is.data.frame(x))).
P.

2
ggplot e data.table sono fantastici, ma abbiamo davvero bisogno di un nuovo pacchetto per ogni singola funzione in R?
Adn bps,

58

Confronto purrre lapplysi riduce a convenienza e velocità .


1. purrr::mapè sintatticamente più conveniente di lapply

estrae il secondo elemento dell'elenco

map(list, 2)  

che come @F. Privé ha sottolineato, è lo stesso di:

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

con lapply

lapply(list, 2) # doesn't work

dobbiamo passare una funzione anonima ...

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

... o come sottolineato da @RichScriven, passiamo [[come argomento inlapply

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

Quindi, se ti ritrovi ad applicare funzioni a molte liste usando lapplye stanco di definire una funzione personalizzata o di scrivere una funzione anonima, la convenienza è una delle ragioni per favorire purrr.

2. La mappa specifica per tipo funziona semplicemente con molte righe di codice

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

Ognuna di queste funzioni cartografiche specifiche del tipo restituisce un vettore, anziché gli elenchi restituiti da map()e lapply(). Se hai a che fare con elenchi di vettori nidificati, puoi utilizzare queste funzioni cartografiche specifiche per tipo per estrarre direttamente i vettori e forzare i vettori direttamente in vettori int, dbl, chr. La versione base R sarebbe simile as.numeric(sapply(...)), as.character(sapply(...))e così via

Le map_<type>funzioni hanno anche la qualità utile che se non possono restituire un vettore atomico del tipo indicato, falliscono. Ciò è utile quando si definisce un flusso di controllo rigoroso, in cui si desidera che una funzione fallisca se [in qualche modo] genera un tipo di oggetto errato.

3. La convenienza a parte, lapplyè [leggermente] più veloce dimap

Utilizzando purrrle funzioni di convenienza, come @F. Privé ha sottolineato che rallenta un po 'l'elaborazione. Corriamo ciascuno dei 4 casi che ho presentato sopra.

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

inserisci qui la descrizione dell'immagine

E il vincitore è....

lapply(list, `[[`, 2)

In breve, se la velocità pura è ciò che stai cercando: base::lapply(anche se non è molto più veloce)

Per sintassi ed espressibilità semplici: purrr::map


Questo eccellente purrrtutorial evidenzia la comodità di non dover scrivere esplicitamente funzioni anonime durante l'utilizzo purrre i vantaggi di mapfunzioni specifiche per tipo .


2
Si noti che se si utilizza function(x) x[[2]]invece di solo 2, sarebbe meno lento. Tutto questo tempo extra è dovuto a controlli che lapplynon lo fanno.
F. Privé,

17
Non hai "bisogno" di funzioni anonime. [[è una funzione. Si può fare lapply(list, "[[", 3).
Rich Scriven

@RichScriven ha senso. Ciò semplifica la sintassi per l'utilizzo di lapply su purrr.
Rich Pauloo,

37

Se non consideriamo gli aspetti del gusto (altrimenti questa domanda dovrebbe essere chiusa) o la coerenza della sintassi, lo stile ecc., La risposta è no, non vi è alcun motivo speciale da utilizzare al mapposto di lapplyo altre varianti della famiglia di applicazione, come la più rigorosa vapply.

PS: A quelle persone che effettuano gratuitamente il downvoting, basta ricordare che l'OP ha scritto:

Non sto chiedendo qui sui propri gusti o antipatie sulla sintassi, altre funzionalità fornite da purrr ecc., Ma rigorosamente sul confronto di purrr :: map con lapply ipotizzando di utilizzare la valutazione standard

Se non consideri la sintassi né altre funzionalità di purrr, non c'è motivo speciale da usare map. Uso purrrme stesso e sto bene con la risposta di Hadley, ma ironizza ironicamente sulle cose che l'OP ha dichiarato in anticipo che non stava chiedendo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.