functions - ¿Por qué usar purrr:: map en lugar de lapply?
r apply lapply sapply mapply (3)
La comparación de
lapply
y
lapply
reduce a
conveniencia
y
velocidad
.
1.
purrr::map
es sintácticamente más conveniente que lapply
extraer el segundo elemento de la lista
map(list, 2)
que como @F. Privé señaló, es lo mismo que:
map(list, function(x) x[[2]])
con
lapply
lapply(list, 2) # doesn''t work
necesitamos pasar una función anónima ...
lapply(list, function(x) x[[2]]) # now it works
... o como señaló @RichScriven, pasamos
[[
como argumento a
lapply
lapply(list, `[[`, 2) # a bit more simple syntantically
Entonces, si se encuentra aplicando funciones a muchas listas usando
lapply
, y
lapply
de definir una función personalizada o escribir una función anónima, la conveniencia es una razón para moverse a favor de
purrr
.
2. El mapa específico del tipo funciona simplemente con muchas líneas de código
-
map_chr()
-
map_lgl()
-
map_int()
-
map_dbl()
-
map_df()
: mi favorito, devuelve un marco de datos.
Cada una de estas funciones de mapa específicas de tipo devuelve una lista atómica (vector), en lugar de las listas devueltas por
map()
y
lapply()
.
Si se trata de listas anidadas de vectores atómicos dentro, puede utilizar estas funciones de mapa específicas de tipo para extraer los vectores directamente y coaccionar los vectores directamente en vectores int, dbl, chr.
La versión base R se parecería a
as.numeric(sapply(...))
,
as.character(sapply(...))
, etc. Esto le da a
purrr
otro punto de conveniencia y funcionalidad.
3. Conveniencia aparte,
lapply
es [ligeramente] más rápido que el
map
Usando las funciones de conveniencia de
purrr
, como @F.
Privé señaló que ralentiza un poco el procesamiento.
Vamos a competir con cada uno de los 4 casos que presenté anteriormente.
# 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)
Y el ganador es....
lapply(list, `[[`, 2)
En resumen, si lo que buscas es velocidad bruta:
base::lapply
(aunque no es mucho más rápido)
Para sintaxis y
purrr::map
simples:
purrr::map
Este excelente tutorial de
purrr
destaca la
conveniencia
de no tener que escribir explícitamente funciones anónimas cuando se usa
purrr
, y los beneficios de las funciones de
map
específicas del tipo.
¿Hay alguna razón por la que debería usar
map(<list-like-object>, function(x) <do stuff>)
en vez de
lapply(<list-like-object>, function(x) <do stuff>)
la salida debería ser la misma y los puntos de referencia que hice parecen mostrar que el
lapply
es un poco más rápido (debería ser como el
map
necesita evaluar toda la entrada de evaluación no estándar).
Entonces, ¿hay alguna razón por la cual para casos tan simples debería considerar cambiar a
purrr::map
?
No estoy preguntando aquí sobre los gustos o disgustos de uno sobre la sintaxis, otras funcionalidades proporcionadas por purrr, etc., sino estrictamente sobre la comparación de
purrr::map
con
lapply
suponiendo que use la evaluación estándar, es decir,
map(<list-like-object>, function(x) <do stuff>)
.
¿Hay alguna ventaja que tenga
purrr::map
en términos de rendimiento, manejo de excepciones, etc.?
Los comentarios a continuación sugieren que no es así, pero ¿tal vez alguien podría elaborar un poco más?
Si la única función que está utilizando de purrr es
map()
, entonces no, las ventajas no son sustanciales.
Como señala Rich Pauloo, la principal ventaja de
map()
son los ayudantes que le permiten escribir código compacto para casos especiales comunes:
-
~ . + 1
~ . + 1
es equivalente a lafunction(x) x + 1
-
list("x", 1)
es equivalente a lafunction(x) x[["x"]][[1]]
. Estos ayudantes son un poco más generales que[[
- see?pluck
para más detalles. Para el rectángulo de datos , el argumento.default
es particularmente útil.
Pero la mayoría de las veces no estás usando una sola función
*apply()
/
map()
, estás usando un montón de ellas, y la ventaja de purrr es una consistencia mucho mayor entre las funciones.
Por ejemplo:
-
El primer argumento para
lapply()
son los datos; El primer argumento paramapply()
es la función. El primer argumento para todas las funciones del mapa son siempre los datos. -
Con
vapply()
,sapply()
ymapply()
puede elegir suprimir nombres en la salida conUSE.NAMES = FALSE
; perolapply()
no tiene ese argumento. -
No hay una forma consistente de pasar argumentos consistentes a la función del mapeador. La mayoría de las funciones usan
...
peromapply()
usaMoreArgs
(que esperaría que se llamaraMORE.ARGS
), yMap()
,Filter()
yReduce()
esperan que cree una nueva función anónima. En las funciones de mapa, el argumento constante siempre viene después del nombre de la función. -
Casi todas las funciones de purrr son de tipo estable: puede predecir el tipo de salida exclusivamente a partir del nombre de la función. Esto no es cierto para
sapply()
omapply()
. Sí, hayvapply()
; pero no hay equivalente paramapply()
.
Puede pensar que todas estas distinciones menores no son importantes (al igual que algunas personas piensan que no hay ninguna ventaja en encadenar sobre las expresiones regulares de base R), pero en mi experiencia causan fricciones innecesarias al programar (las diferentes órdenes de argumentos siempre solían dispararse Me up), y hacen que las técnicas de programación funcional sean más difíciles de aprender porque además de las grandes ideas, también tienes que aprender un montón de detalles incidentales.
Purrr también completa algunas variantes de mapas útiles que están ausentes de la base R:
-
modify()
conserva el tipo de datos usando[[<-
para modificar "en el lugar". En combinación con la variante_if
, esto permite código (IMO hermoso) comomodify_if(df, is.factor, as.character)
-
map2()
permite mapear simultáneamente sobrex
ey
. Esto facilita la expresión de ideas comomap2(models, datasets, predict)
-
imap()
permite mapear simultáneamente sobrex
sus índices (ya sea nombres o posiciones). Esto facilita (por ejemplo) cargar todos los archivoscsv
en un directorio, agregando una columna defilename
a cada uno.dir("//.csv$") %>% set_names() %>% map(read.csv) %>% imap(~ transform(.x, filename = .y))
-
walk()
devuelve su entrada de forma invisible; y es útil cuando se llama a una función por sus efectos secundarios (es decir, escribir archivos en el disco).
Sin mencionar los otros ayudantes como
safely()
y
partial()
.
Personalmente, encuentro que cuando uso purrr, puedo escribir código funcional con menos fricción y mayor facilidad; disminuye la brecha entre pensar una idea e implementarla. Pero tu kilometraje puede variar; no hay necesidad de usar ronroneo a menos que realmente lo ayude.
Microbenchmarks
Sí,
map()
es un poco más lento que
lapply()
.
Pero el costo de usar
map()
o
lapply()
de lo que esté mapeando, no de la sobrecarga de realizar el ciclo.
El microbenchmark a continuación sugiere que el costo de
map()
comparación con
lapply()
es de alrededor de 40 ns por elemento, lo que parece poco probable que afecte materialmente a la mayoría del código 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
Si no consideramos los aspectos del gusto (de lo contrario, esta pregunta debería cerrarse) o la consistencia de la sintaxis, el estilo, etc., la respuesta es no, no hay ninguna razón especial para usar el
map
lugar de
lapply
u otras variantes de la familia de aplicación, como la más estricta
vapply
PD: Para aquellas personas que votan negativamente, solo recuerden que el OP escribió:
No estoy preguntando aquí sobre los gustos o disgustos de uno sobre la sintaxis, otras funcionalidades proporcionadas por purrr, etc., sino estrictamente sobre la comparación de purrr :: map con lapply suponiendo que use la evaluación estándar
Si no considera la sintaxis ni otras funcionalidades de
purrr
, no hay ninguna razón especial para usar
map
.
Yo uso
purrr
y estoy de acuerdo con la respuesta de Hadley, pero irónicamente repasa las mismas cosas que el OP declaró por adelantado que no estaba preguntando.