web-dev-qa-db-fra.com

Pourquoi utiliser purrr :: map au lieu de lapply?

Y a-t-il une raison pour laquelle je devrais utiliser

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

au lieu de

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

la sortie devrait être la même et les repères que j'ai établis semblent montrer que lapply est légèrement plus rapide (il devrait être comme map doit évaluer toutes les entrées d'évaluation non standard).

Donc, y a-t-il une raison pour laquelle, pour de tels cas simples, je devrais réellement envisager de passer à purrr::map? Je ne pose pas ici la question de ce que l’on aime ou n’aime pas à propos de la syntaxe, des autres fonctionnalités fournies par purrr, etc., mais strictement de la comparaison de purrr::map avec lapply en utilisant l’évaluation standard, c.-à-d. map(<list-like-object>, function(x) <do stuff>) . Existe-t-il un avantage de purrr::map en termes de performances, de gestion des exceptions, etc.? Les commentaires ci-dessous suggèrent que ce n'est pas le cas, mais peut-être que quelqu'un pourrait en dire un peu plus?

142
Tim

Si la seule fonction que vous utilisez avec purrr est map(), alors non, les avantages ne sont pas substantiels. Comme le souligne Rich Pauloo, le principal avantage de map() réside dans les aides qui vous permettent d'écrire du code compact pour les cas particuliers les plus courants:

  • ~ . + 1 est équivalent à function(x) x + 1

  • list("x", 1) est équivalent à function(x) x[["x"]][[1]]. Ces aides sont un peu plus générales que [[ - voir ?pluck pour plus de détails. Pour data rectangling , l'argument .default est particulièrement utile.

Mais la plupart du temps, vous n'utilisez pas une seule fonction *apply()/map(), vous en utilisez plusieurs, et l'avantage de purrr est une cohérence beaucoup plus grande entre les fonctions. Par exemple:

  • Le premier argument de lapply() est la donnée; le premier argument de mapply() est la fonction. Le premier argument de toutes les fonctions de la carte est toujours les données.

  • Avec vapply(), sapply() et mapply(), vous pouvez choisir de supprimer les noms sur la sortie avec USE.NAMES = FALSE; mais lapply() n'a pas cet argument.

  • Il n'y a aucun moyen cohérent de transmettre des arguments cohérents à la fonction mappeur. La plupart des fonctions utilisent ... mais mapply() utilise MoreArgs (ce qui devrait s'appeler MORE.ARGS) et Map(), Filter() et Reduce() attendez-vous à créer une nouvelle fonction anonyme. Dans les fonctions de la carte, les arguments constants viennent toujours après le nom de la fonction.

  • Presque toutes les fonctions purrr sont de type stable: vous pouvez prédire le type de sortie exclusivement à partir du nom de la fonction. Ce n'est pas vrai pour sapply() ou mapply(). Oui, il y a vapply(); mais il n'y a pas d'équivalent pour mapply().

Vous pensez peut-être que toutes ces distinctions mineures ne sont pas importantes (tout comme certaines personnes pensent qu'il n'y a aucun avantage à faire passer les expressions stringr de base R), mais d'après mon expérience, elles causent des frictions inutiles lors de la programmation (les ordres d'arguments différents toujours utilisés me up), et elles rendent les techniques de programmation fonctionnelle plus difficiles à apprendre car, outre les grandes idées, vous devez également apprendre un tas de détails accessoires.

Purrr remplit également certaines variantes de carte utiles qui sont absentes de la base R:

  • modify() préserve le type des données en utilisant [[<- pour modifier "in place". En conjonction avec la variante _if, cela permet d’obtenir un code (beau comme un IMO) comme modify_if(df, is.factor, as.character)

  • map2() vous permet de mapper simultanément sur x et y. Cela facilite l’expression d’idées telles que map2(models, datasets, predict)

  • imap() vous permet de mapper simultanément sur x et ses index (noms ou positions). Cela facilite, par exemple, le chargement de tous les fichiers csv d'un répertoire, en ajoutant une colonne filename à chacun.

    dir("\\.csv$") %>%
      set_names() %>%
      map(read.csv) %>%
      imap(~ transform(.x, filename = .y))
    
  • walk() renvoie son entrée de manière invisible; et est utile lorsque vous appelez une fonction pour ses effets secondaires (c'est-à-dire l'écriture de fichiers sur le disque).

Sans parler des autres aides comme safely() et partial().

Personnellement, je trouve que lorsque j'utilise purrr, je peux écrire du code fonctionnel avec moins de friction et une plus grande facilité. cela réduit l’écart entre la conception d’une idée et sa mise en œuvre. Mais votre kilométrage peut varier. il n’est pas nécessaire d’utiliser purrr sauf s’il vous aide réellement.

Micro-repères

Oui, map() est légèrement plus lent que lapply(). Mais le coût d'utilisation de map() ou lapply() dépend de ce que vous mappez et non de la surcharge de l'exécution de la boucle. Le micro-repère ci-dessous suggère que le coût de map() par rapport à lapply() est d'environ 40 ns par élément, ce qui semble peu susceptible d'avoir un impact significatif sur la plupart des codes 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
203
hadley

Comparer purrr et lapply revient à commodité et vitesse.


1. _purrr::map_ est syntaxiquement plus pratique que lapply

extraire le deuxième élément de la liste

_map(list, 2)  
_

qui comme @F. Privé a souligné, est le même que:

_map(list, function(x) x[[2]])
_

avec lapply

_lapply(list, 2) # doesn't work
_

nous devons passer une fonction anonyme ...

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

... ou comme l'a souligné @RichScriven, nous passons _[[_ comme argument dans lapply

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

Donc, si vous vous retrouvez à appliquer des fonctions à plusieurs listes à l'aide de lapply, et que vous en avez assez de définir une fonction personnalisée ou d'écrire une fonction anonyme, la commodité est l'une des raisons pour lesquelles vous préférez privilégier purrr.

2. Les fonctions de carte spécifiques à un type comportent simplement plusieurs lignes de code

  • map_chr()
  • map_lgl()
  • map_int()
  • map_dbl()
  • map_df() - mon préféré, retourne un cadre de données.

Chacune de ces fonctions de carte spécifiques à un type renvoie une liste atomique (vecteur), plutôt que les listes renvoyées par map() et lapply(). Si vous avez à l'intérieur des listes imbriquées de vecteurs atomiques, vous pouvez utiliser ces fonctions de carte spécifiques au type pour extraire directement les vecteurs et contraindre les vecteurs directement dans les vecteurs int, dbl, chr. La version de base R ressemblerait à quelque chose comme as.numeric(sapply(...)), as.character(sapply(...)), etc. Ceci donne purrr un autre point pour la commodité et la fonctionnalité.

3. La commodité mise à part, lapply est [légèrement] plus rapide que map

Utilisation des fonctions pratiques de purrr, sous la forme @F. Privé a souligné ralentit un peu le traitement. Faisons la course à chacun des 4 cas que j'ai présentés ci-dessus.

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

enter image description here

Et le gagnant est....

_lapply(list, `[[`, 2)
_

En résumé, si vous recherchez une vitesse brute: base::lapply (bien que ce ne soit pas beaucoup plus rapide)

Pour une syntaxe et une expressibilité simples: purrr::map


Cet excellent tutoriel purrr met en évidence le commodité de ne pas avoir à écrire explicitement les fonctions anonymes lors de l'utilisation de purrr, et les avantages des types spécifiques map fonctions.

46
Rich Pauloo

Si nous ne tenons pas compte des aspects du goût (sinon la question devrait être fermée) ou de la cohérence de la syntaxe, du style, etc., la réponse est non, il n'y a pas de raison particulière d'utiliser map au lieu de lapply ou d'autres variantes de la famille à appliquer, telle que la règle plus stricte vapply.

PS: Pour ceux qui votent gratuitement, rappelez-vous que le PO a écrit:

Je ne pose pas de questions sur ce que l’on aime ou n’aime pas sur la syntaxe, les autres fonctionnalités fournies par purrr, etc., mais strictement sur la comparaison de purrr :: map avec lapply en utilisant l’évaluation standard

Si vous ne tenez pas compte de la syntaxe ni des autres fonctionnalités de purrr, il n’ya aucune raison particulière d’utiliser map. J’utilise purrr moi-même et la réponse de Hadley me convient, mais il va ironiquement sur les choses que le PO a énoncées dès le départ, il ne demandait pas.

29
Carlos Cinelli