Por que usar purrr :: map em vez de lapply?


171

Existe alguma razão para eu usar

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

ao invés de

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

o resultado deve ser o mesmo e os parâmetros de referência que eu fiz parecem mostrar que lapplyé um pouco mais rápido (deve ser o mapnecessário para avaliar todas as informações de avaliação não padronizadas).

Então, existe alguma razão pela qual, para casos tão simples, eu deveria considerar mudar para purrr::map? Não estou pedindo aqui sobre gosta ou não gosta sobre a sintaxe de um, outras funcionalidades fornecidas pelo purrr etc., mas estritamente sobre a comparação de purrr::mapcom lapplyassumindo usando a avaliação padrão, ou seja map(<list-like-object>, function(x) <do stuff>). Existe alguma vantagem purrr::mapem termos de desempenho, tratamento de exceções etc.? Os comentários abaixo sugerem que não, mas talvez alguém possa elaborar um pouco mais?


8
Para casos de uso simples, é melhor ficar com a base R e evitar dependências. Se você já carregar a tidyverseporém, você pode se beneficiar da tubulação %>%e funções anônimas ~ .x + 1sintaxe
Aurèle

49
Isso é praticamente uma questão de estilo. Você deve saber o que as funções básicas R fazem, porque todo esse material arrumado é apenas um shell em cima dele. Em algum momento, esse shell quebrará.
Hong Ooi

9
~{}atalho lambda (com ou sem os {}selos, o negócio para mim é simples purrr::map(). A aplicação do tipo purrr::map_…()é útil e menos obtusa que vapply(). purrr::map_df()é uma função muito cara, mas também simplifica o código. Não há absolutamente nada de errado em ficar com a base R [lsv]apply(), no entanto #
21717 java #

4
Obrigado pela pergunta - tipo de coisa que eu também olhei. Eu estou usando R há mais de 10 anos e definitivamente não usa e não usará purrrcoisas. Meu argumento é o seguinte: tidyverseé fabuloso para análises / material interativo / de relatórios, não para programação. Se você precisa usar lapplyou mapestá programando e pode acabar um dia criando um pacote. Então, quanto menos dependências, melhor. Além disso: às vezes vejo pessoas usando mapsintaxe bastante obscura depois. E agora que vejo testes de desempenho: se você está acostumado à applyfamília: cumpra-o.
Eric Lecoutre

4
Tim: aquele que repassa exatamente o que você disse que não queria que as pessoas repassassem.
Carlos Cinelli

Respostas:


232

Se a única função que você está usando do purrr for map(), então não, as vantagens não são substanciais. Como Rich Pauloo aponta, a principal vantagem map()são os auxiliares que permitem escrever código compacto para casos especiais comuns:

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

  • list("x", 1)é equivalente a function(x) x[["x"]][[1]]. Esses ajudantes são um pouco mais gerais do que [[- veja ?pluckpara detalhes. Para retangling de dados , o .defaultargumento é particularmente útil.

Mas na maioria das vezes você não está usando um único *apply()/ map() função, você está usando um monte deles, e a vantagem de purrr é muito maior coerência entre as funções. Por exemplo:

  • O primeiro argumento para lapply()é os dados; o primeiro argumento para mapply()é a função O primeiro argumento para todas as funções do mapa são sempre os dados.

  • Com vapply(), sapply()e mapply()você pode optar por nomes suprimir sobre a saída com USE.NAMES = FALSE; mas lapply()não tem esse argumento.

  • Não há uma maneira consistente de transmitir argumentos consistentes para a função mapeador. A maioria das funções usa ...mas mapply()usa MoreArgs(que você esperaria ser chamado MORE.ARGS) e Map(), Filter()e Reduce()espera que você crie uma nova função anônima. Nas funções de mapa, o argumento constante sempre vem após o nome da função.

  • Quase todas as funções do ronronador são do tipo estável: você pode prever o tipo de saída exclusivamente a partir do nome da função. Isso não é verdade para sapply()ou mapply(). Sim existe vapply(); mas não há equivalente para mapply().

Você pode pensar que todas essas pequenas distinções não são importantes (assim como algumas pessoas pensam que não há vantagem em se expressar sobre as expressões regulares R), mas, na minha experiência, elas causam atritos desnecessários na programação (as diferentes ordens de argumentos sempre usadas para disparar). ), e elas dificultam o aprendizado das técnicas de programação funcional, pois além das grandes idéias, você também precisa aprender vários detalhes incidentais.

O Purrr também preenche algumas variantes úteis de mapas ausentes da base R:

  • modify()preserva o tipo de dados usando [[<-para modificar "no local". Em conjunto com a _ifvariante, isso permite códigos (IMO bonitos) comomodify_if(df, is.factor, as.character)

  • map2()permite mapear simultaneamente sobre xe y. Isso facilita a expressão de idéias como map2(models, datasets, predict)

  • imap()permite mapear simultaneamente xe seus índices (nomes ou posições). Isso facilita (por exemplo) carregar todos os csvarquivos em um diretório, adicionando uma filenamecoluna a cada um.

    dir("\\.csv$") %>%
      set_names() %>%
      map(read.csv) %>%
      imap(~ transform(.x, filename = .y))
  • walk()retorna sua entrada invisivelmente; e é útil quando você está chamando uma função por seus efeitos colaterais (por exemplo, gravando arquivos em disco).

Sem mencionar os outros ajudantes como safely()e partial().

Pessoalmente, acho que, quando uso o purrr, posso escrever código funcional com menos atrito e maior facilidade; diminui a diferença entre pensar em uma idéia e implementá-la. Mas sua milhagem pode variar; não é necessário usar o ronronar, a menos que ele realmente o ajude.

Microbenchmarks

Sim, map()é um pouco mais lento que lapply(). Mas o custo do uso map()ou lapply()é determinado pelo que você está mapeando, não pela sobrecarga de executar o loop. A marca de microbench abaixo sugere que o custo map()comparado a lapply()é de cerca de 40 ns por elemento, o que parece improvável de afetar materialmente a maioria dos códigos 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
Você queria usar transform () nesse exemplo? Como na base R transform (), ou estou faltando alguma coisa? transform () fornece o nome do arquivo como um fator, que gera avisos quando você (naturalmente) deseja vincular linhas. mutate () fornece a coluna de caracteres dos nomes de arquivos que eu quero. Existe uma razão para não usá-lo lá?
doctorG

2
Sim, melhor usar mutate(), eu só queria um exemplo simples, sem outros deps.
hadley #

A especificidade de tipo não deve aparecer em algum lugar nesta resposta? map_*foi o que me fez carregar purrrem muitos scripts. Isso me ajudou com alguns aspectos de 'controle de fluxo' do meu código ( stopifnot(is.data.frame(x))).
Pe.

2
O ggplot e o data.table são ótimos, mas será que realmente precisamos de um novo pacote para cada função no R?
precisa

58

Comparando purrre se lapplyresume a conveniência e velocidade .


1. purrr::mapé sintaticamente mais conveniente que lapply

extrair o segundo elemento da lista

map(list, 2)  

que como @F. Privé apontou, é o mesmo que:

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

com lapply

lapply(list, 2) # doesn't work

precisamos passar uma função anônima ...

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

... ou como @RichScriven apontou, passamos [[como argumento paralapply

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

Portanto, se você se encontra aplicando funções a muitas listas usando lapplye se cansa de definir uma função personalizada ou escrever uma função anônima, a conveniência é um dos motivos a favor purrr.

2. O mapa específico do tipo funciona simplesmente com muitas linhas de código

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

Cada uma dessas funções de mapa específicas do tipo retorna um vetor, em vez das listas retornadas por map()e lapply(). Se você estiver lidando com listas aninhadas de vetores, poderá usar essas funções de mapa específicas do tipo para extrair os vetores diretamente e coagir os vetores diretamente nos vetores int, dbl, chr. A versão base de R ficaria algo como as.numeric(sapply(...)), as.character(sapply(...)), etc.

As map_<type>funções também têm a qualidade útil de que, se não puderem retornar um vetor atômico do tipo indicado, elas falharão. Isso é útil ao definir o fluxo de controle estrito, no qual você deseja que uma função falhe se [de alguma forma] gerar o tipo de objeto errado.

3. Conveniência à parte, lapplyé [ligeiramente] mais rápida quemap

Usando purrras funções de conveniência, como @F. Privé destacou que retarda um pouco o processamento. Vamos correr cada um dos 4 casos que apresentei acima.

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

insira a descrição da imagem aqui

E o vencedor é....

lapply(list, `[[`, 2)

Em suma, se você procura velocidade bruta: base::lapply(embora não seja muito mais rápido)

Para sintaxe e expressibilidade simples: purrr::map


Este excelente purrrtutorial destaca a conveniência de não precisar escrever explicitamente funções anônimas ao usar purrre os benefícios de mapfunções específicas de tipo .


2
Observe que se você usar em function(x) x[[2]]vez de apenas 2, seria menos lento. Todo esse tempo extra é devido a verificações que lapplynão funcionam.
F. Privé

17
Você não "precisa" de funções anônimas. [[é uma função. Você pode fazer lapply(list, "[[", 3).
Rich Scriven

@ RichScriven que faz sentido. Isso simplifica a sintaxe para usar lapply over ronronar.
Rich Pauloo

37

Se não considerarmos aspectos de paladar (caso contrário, essa pergunta deve ser encerrada) ou consistência de sintaxe, estilo etc., a resposta é não, não há motivo especial para usar em mapvez de lapplyou outras variantes da família de aplicativos, como as mais rigorosas vapply.

PS: Para as pessoas que votaram gratuitamente, lembre-se de que o OP escreveu:

Não estou perguntando aqui sobre os gostos ou aversões de alguém sobre a sintaxe, outras funcionalidades fornecidas pelo purrr etc., mas estritamente sobre a comparação do purrr :: map com suposição de lapply usando a avaliação padrão

Se você não considerar a sintaxe nem outras funcionalidades de purrr, não há motivo especial para usá-lo map. Eu purrrme uso e estou bem com a resposta de Hadley, mas isso ironicamente aborda exatamente as coisas que o OP declarou antecipadamente que não estava perguntando.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.