¿Por qué usar purrr :: map en lugar de lapply?


171

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

el resultado debería ser el mismo y los puntos de referencia que realicé parecen mostrar que lapplyes un poco más rápido (debería ser como mapevaluar todas las entradas de evaluación no estándar).

Entonces, ¿hay alguna razón por la cual para casos tan simples debería considerar cambiarme purrr::map? No estoy pidiendo aquí sobre gustos o disgustos sobre la sintaxis de uno, otras funcionalidades proporcionadas por purrr etc., pero estrictamente sobre la comparación de purrr::mapla lapplysuponiendo el uso de la evaluación estándar, es decir map(<list-like-object>, function(x) <do stuff>). ¿Hay alguna ventaja purrr::mapen 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?


8
De hecho, para casos de uso simples, mejor quédese con la base R y evite las dependencias. Sin embargo, si ya carga el tidyverse, puede beneficiarse de la sintaxis de canalización %>%y funciones anónimas~ .x + 1
Aurèle

49
Esto es más o menos una cuestión de estilo. Sin embargo, debe saber qué hacen las funciones de base R, porque todo este tidyverse es solo un caparazón encima. En algún momento, ese caparazón se romperá.
Hong Ooi

9
~{}lambda acceso directo (con o sin los {}sellos de la oferta para mí por llano purrr::map(). El tipo de aplicación de la purrr::map_…()son muy útiles y menos obtusos que vapply(). purrr::map_df()es una función caro súper pero también simplifica código. No hay absolutamente nada de malo en la pervivencia de la base R [lsv]apply(), aunque .
hrbrmstr

44
Gracias por la pregunta, tipo de cosas que también miré. Estoy usando R desde hace más de 10 años y definitivamente no uso y no usaré purrrcosas. Mi punto es el siguiente: tidyversees fabuloso para análisis / interactivo / informes, no para programación. Si tiene que usar lapplyo mapestá programando y puede terminar algún día creando un paquete. Entonces, cuanto menos dependencias, mejor. Además: a veces veo personas que usan mapuna sintaxis bastante oscura después. Y ahora que veo pruebas de rendimiento: si estás acostumbrado a la applyfamilia: mantente firme.
Eric Lecoutre

44
Tim, usted 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" y la respuesta que aceptó es el que pasa exactamente por lo que dijiste que no querías que la gente pasara.
Carlos Cinelli

Respuestas:


232

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()los ayudantes es que le permiten escribir código compacto para casos especiales comunes:

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

  • list("x", 1)es equivalente a function(x) x[["x"]][[1]]. Estos ayudantes son un poco más generales que [[- ver ?pluckpara más detalles. Para el rectángulo de datos , el .defaultargumento 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 ronronear es una consistencia mucho mayor entre las funciones. Por ejemplo:

  • El primer argumento para lapply()es los datos; El primer argumento mapply()es la función. El primer argumento para todas las funciones del mapa son siempre los datos.

  • Con vapply(),, sapply()y mapply()puede optar por suprimir nombres en la salida con USE.NAMES = FALSE; pero lapply()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 ...pero mapply()usan MoreArgs(que esperaría que se llamara MORE.ARGS), y Map(), Filter()y Reduce()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()o mapply(). Sí lo hay vapply(); pero no hay equivalente para mapply().

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 argumento 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()preserva el tipo de los datos utilizando [[<-para modificar "en el lugar". En combinación con la _ifvariante, esto permite un código (IMO hermoso) comomodify_if(df, is.factor, as.character)

  • map2()le permite mapear simultáneamente sobre xy y. Esto hace que sea más fácil expresar ideas como map2(models, datasets, predict)

  • imap()le permite mapear simultáneamente xy sus índices (ya sea nombres o posiciones). Esto facilita (por ejemplo) cargar todos los csvarchivos en un directorio, agregando una filenamecolumna 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 te ayude.

Microbenchmarks

Sí, map()es un poco más lento que lapply(). Sin embargo, el coste de utilización map()o lapply()es impulsado por lo que se aplica la relación, no la carga de realizar el bucle. El microbench de abajo sugiere que el costo de map()comparación 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

2
¿Querías usar transform () en ese ejemplo? Como en base R transform (), ¿o me falta algo? transform () le proporciona un nombre de archivo como factor, que genera advertencias cuando (naturalmente) desea unir filas. mutate () me da la columna de caracteres de los nombres de archivo que quiero. ¿Hay alguna razón para no usarlo allí?
doctorG

2
Sí, mejor usar mutate(), solo quería un ejemplo simple sin otros deps.
Hadley

¿No debería aparecer la especificidad de tipo en alguna parte de esta respuesta? map_*es lo que me hizo cargar purrren muchos scripts. Me ayudó con algunos aspectos de 'flujo de control' de mi código ( stopifnot(is.data.frame(x))).
p.

2
ggplot y data.table son geniales, pero ¿realmente necesitamos un nuevo paquete para cada función en R?
Adn bps

58

Comparando purrry se lapplyreduce a conveniencia y velocidad .


1. purrr::mapes 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 alapply

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

Entonces, si se encuentra aplicando funciones a muchas listas usando lapply, y se cansa de definir una función personalizada o escribir una función anónima, la conveniencia es una de las razones a favor 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()

Cada una de estas funciones de mapa específicas de tipo devuelve un vector, en lugar de las listas devueltas por map()y lapply(). Si se trata de listas anidadas de vectores, puede usar 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 de la base R sería algo como as.numeric(sapply(...)), as.character(sapply(...)), etc.

Las map_<type>funciones también tienen la calidad útil de que si no pueden devolver un vector atómico del tipo indicado, fallarán. Esto es útil al definir un flujo de control estricto, donde desea que una función falle si [de alguna manera] genera el tipo de objeto incorrecto.

3. Conveniencia aparte, lapplyes [ligeramente] más rápido quemap

Utilizando purrrlas funciones de conveniencia, 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)

ingrese la descripción de la imagen aquí

Y el ganador es....

lapply(list, `[[`, 2)

En resumen, si la velocidad bruta es lo que buscas: base::lapply(aunque no es mucho más rápido)

Para sintaxis simple y expresibilidad: purrr::map


Este excelente purrrtutorial resalta la conveniencia de no tener que escribir explícitamente funciones anónimas cuando se usa purrr, y los beneficios de las mapfunciones específicas de tipo .


2
Tenga en cuenta que si usa en function(x) x[[2]]lugar de solo 2, sería menos lento. Todo este tiempo extra se debe a los controles que lapplyno funcionan.
F. Privé

17
No "necesita" funciones anónimas. [[Es una función. Puedes hacer lapply(list, "[[", 3).
Rich Scriven

@RichScriven que tiene sentido. Eso simplifica la sintaxis para usar lapply sobre purrr.
Rich Pauloo

37

Si no consideramos 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 una razón especial para usar en maplugar de lapplyotras 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 razón especial para usar map. Me uso purrry 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.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.