r map vector




Por que usar purrr:: map em vez de lapply? (2)

Comparando purrr e lapply resume-se a conveniência e velocidade .

1. purrr::map é sintaticamente mais conveniente que o lapidado

extrair o segundo elemento da lista

map(list, 2)  

qual 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, nós passamos [[ como um argumento em lapply

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

Portanto, se você estiver aplicando funções a muitas listas usando lapply , e lapply de definir uma função personalizada ou escrever uma função anônima, a conveniência é uma das razões para mudar para favorecer a purrr .

2. Funções de mapa específicas do tipo simplesmente muitas linhas de código

  • map_chr()
  • map_lgl()
  • map_int()
  • map_dbl()
  • map_df() - meu favorito, retorna um quadro de dados.

Cada uma dessas funções de mapa específicas do tipo retorna uma lista atômica (vetor), em vez das listas retornadas por map() e lapply() . Se você está lidando com listas aninhadas de vetores atômicos, você pode usar essas funções de mapa específicas do tipo para extrair os vetores diretamente e coagir vetores diretamente em vetores int, dbl, chr. A versão base R seria algo como as.numeric(sapply(...)) , as.character(sapply(...)) , etc. Isto dá purrr outro ponto por conveniência e funcionalidade.

3. Conveniência à parte, o lapply é [ligeiramente] mais rápido que o map

Usando as funções de conveniência do purrr , como @F. Privé apontou retarda o processamento um pouco. Vamos correr cada um dos 4 casos que eu 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)

E o vencedor é....

lapply(list, `[[`, 2)

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

Para sintaxe e expressibilidade simples: purrr::map

Este excelente tutorial purrr destaca a conveniência de não ter que explicitamente escrever funções anônimas ao usar o purrr , e os benefícios de funções de map específicas do tipo.

Existe alguma razão pela qual eu deveria 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 benchmarks que eu fiz parecem mostrar que o lapply é um pouco mais rápido (deve ser como o map precisa avaliar todas as entradas de avaliação não-padrão).

Então, há alguma razão para que casos tão simples eu realmente considere mudar para o purrr::map ? Eu não estou perguntando aqui sobre os gostos ou desgostos sobre a sintaxe, outras funcionalidades fornecidas pelo purrr etc., mas estritamente sobre a comparação do purrr::map com o lapply usando a avaliação padrão, ie map(<list-like-object>, function(x) <do stuff>) . Existe alguma vantagem que o purrr::map tem em termos de desempenho, manipulação de exceções, etc.? Os comentários abaixo sugerem que isso não acontece, mas talvez alguém possa elaborar um pouco mais?


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

  • ~ . + 1 ~ . + 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 que [[ - veja ?pluck Para o retangulo de dados , o argumento .default é particularmente útil.

Mas na maior parte do tempo você não está usando uma única função *apply() / map() , você está usando um monte deles, e a vantagem do purrr é uma consistência muito maior entre as funções. Por exemplo:

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

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

  • Não há maneira consistente de passar 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() esperam que você crie uma nova função anônima. Nas funções do mapa, o argumento constante sempre vem depois do nome da função.

  • Quase todas as funções purrr 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 distinções menores não são importantes (assim como algumas pessoas pensam que não há vantagem em usar expressões regulares), mas na minha experiência elas causam fricção desnecessária ao programar (as diferentes ordens de argumento sempre usadas para tropeçar) e eles tornam as técnicas de programação funcional mais difíceis de aprender, pois, além das grandes ideias, você também precisa aprender um monte de detalhes incidentais.

O Purrr também preenche algumas variantes úteis do mapa que estão ausentes da base R:

  • modify() preserva o tipo de dados usando [[<- para modificar "no lugar". Em conjunto com a variante _if isso permite um código (IMO bonito) como modify_if(df, is.factor, as.character)

  • map2() permite mapear simultaneamente sobre x e y . Isso facilita a expressão de ideias como map2(models, datasets, predict)

  • imap() permite mapear simultaneamente sobre x e seus índices (nomes ou posições). Isso facilita (por exemplo) carregar todos os arquivos csv em um diretório, adicionando uma coluna de filename a cada um deles.

    dir("\\.csv$") %>%
      set_names() %>%
      map(read.csv) %>%
      imap(~ transform(.x, filename = .y))
  • walk() retorna sua entrada de forma invisível; e é útil quando você está chamando uma função para seus efeitos colaterais (ou seja, gravar arquivos em disco).

Para não mencionar os outros ajudantes como safely() e partial() .

Pessoalmente, acho que quando uso purrr, posso escrever código funcional com menos atrito e maior facilidade; diminui a distância entre pensar em uma ideia e implementá-la. Mas sua milhagem pode variar; não há necessidade de usar purrr, a menos que realmente ajude você.

Microbenchmarks

Sim, map() é um pouco mais lento que lapply() . Mas o custo de usar map() ou lapply() é determinado pelo que você está mapeando, não pela sobrecarga de executar o loop. O microbenchmark abaixo sugere que o custo de map() comparado a lapply() é de cerca de 40 ns por elemento, o que parece não ter impacto material sobre 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




purrr