13  Manipulation de listes avec purrr

Si vous n’avez besoin que de purrr, library(purrr) charge le package. Si vous utilisez tout le tidyverse, purrr sera chargée après :

library(tidyverse)

13.1 Les listes c’est la vie

z <- list(wagon1="pomme", wagon2=1:3, wagon3=c(TRUE, FALSE))
length(z)
[1] 3
names(z)
[1] "wagon1" "wagon2" "wagon3"
str(z)    # structure of z
List of 3
 $ wagon1: chr "pomme"
 $ wagon2: int [1:3] 1 2 3
 $ wagon3: logi [1:2] TRUE FALSE
# View(z) # RStudio Viewer
z         # simple print method
$wagon1
[1] "pomme"

$wagon2
[1] 1 2 3

$wagon3
[1]  TRUE FALSE

Ne soyez pas effrayé·e par les listes : elles sont simplement des conteneurs pouvant accueillir des objets de classes différentes (ou non) et de longueur différentes (ou non).

On peut se figurer une liste comme un train dont les éléments seraient des wagons dont chacun contiendrait des conteneurs plus petits qui eux contiennent des éléments :

  --  wagon 1  --        --  wagon 2  --       --  wagon 3  --
[    ["pomme"]    ] -- [    [ 1:3 ]     ] - [    [ c(TRUE, FALSE) ]    ]

Une liste s’indexe de plusieurs façons, positionnellement ou avec le nom de se(s) élément(s)-wagons s’ils sont nommés :

z[2]        # using positionnal index
$wagon2
[1] 1 2 3
z["wagon2"] # using ["name"]
$wagon2
[1] 1 2 3
class(z[2])
[1] "list"

Notons que la syntaxe z$wagon2 fonctionne aussi.

Si l’on utilise les doubles crochets, on accède directement au contenu des caisses dans les wagons. Ces derniers ont perdu leurs natures de “wagon”, ne sont plus des listes :

z[[2]]        # using positionnal index
[1] 1 2 3
z[["wagon2"]] # using ["name"]
[1] 1 2 3
class(z[[2]])
[1] "integer"

Les assignations list[index] <- et list[[index]] <- fonctionnent comme à l’acoutumée.

13.2 map à la vanille

Le package purrr s’articule autour de la famille map dont il existe plusieurs variantes dont la première à voir est map tout court.

L’idée est simple : appliquer une fonction à tous les éléments d’une liste et retourner une liste. La liste sera le premier argument de map, la fonction le second. Le nom de fonction est passé sans parenthèses :

map(z, class)
$wagon1
[1] "character"

$wagon2
[1] "integer"

$wagon3
[1] "logical"

13.3 map_* et ses autres parfums

Dans l’exemple ci-dessus, si l’on ne veut pas de liste mais un vecteur (quand cela est possible), on peut utiliser unlist :

z %>% map(class) %>% unlist()
     wagon1      wagon2      wagon3 
"character"   "integer"   "logical" 

Mais quand la classe de sortie d’une fonction map est homogène et connue, on utilisera plutôt les variantes de map de la forme map_*. * pouvant prendre les valeurs int et dbl pour retourner des numeric sous forme d’entiers ou de doubles, lgl pour les vectuers logiques, df, dfr et dfc pour retourner des data.frame, éventuellement combinés par row ou colonnes.

z %>% map_chr(class)
     wagon1      wagon2      wagon3 
"character"   "integer"   "logical" 
list(1:2, 4:9) %>% map_dbl(length)
[1] 2 6

13.4 ~ et \(x) : les fonctions anonymes sont vos amies

Dans purrr et plus largement dans le tidyverse, on peut déclarer des fonctions anonymes, “à la volée” c’est à dire qu moment où l’on en a besoin. purrr mobilise l’opérateur ~ et la variable .x.

R “de base” a repris l’idée dans une saveur un peu différente avec \(x). Ces trois approches sont équivalentes :

x <- 1:5 # create a vector
# option 1 : using a named function
square <- function(x) x^2
map_dbl(x, square)
[1]  1  4  9 16 25
# option 2 : purrr style anonymous function
map(x, ~.x^2)    
[[1]]
[1] 1

[[2]]
[1] 4

[[3]]
[1] 9

[[4]]
[1] 16

[[5]]
[1] 25
# option 3 : R base anonymous function
map(x, \(x) x^2)  
[[1]]
[1] 1

[[2]]
[1] 4

[[3]]
[1] 9

[[4]]
[1] 16

[[5]]
[1] 25

13.5 map2 et généralisation pmap

Imaginons que vous ayez deux listes z1 et z2 et que chaque élément de z1 doivent être élevé à la puissance de chaque élément de z2. purrr généralise map avec deux listes avec les fonctions map2_* :

z1 <- list(4:5, c(3, 2, 4.25), 1:3)
z2 <- list(2, 1, 3)
map2(z1, z2, \(x, y) x^y)
[[1]]
[1] 16 25

[[2]]
[1] 3.00 2.00 4.25

[[3]]
[1]  1  8 27

Si vous avez plus de trois listes, vous pouvez utiliser pmap_, comme suit. Le premier argument sera une liste de toutes vos listes. Imaginons qu’après avoir élevé à la puissance de z2 nous voulions ajouter une valeur dans z3 :

z3 <- list(10, -5, 0)
pmap(list(z1, z2, z3), \(x, y, z) x^y + z)
[[1]]
[1] 26 35

[[2]]
[1] -2.00 -3.00 -0.75

[[3]]
[1]  1  8 27

13.6 Opérations sur listes

13.7 cheat sheet

❤ Placé dans le domaine public par Vincent Bonhomme