Opérations en ligne dans le {tidyverse}

Moissonneuse batteuse en plein récolte avec un déplacement horizontal, ligne par ligne
Author : Vincent Guyader
Tags : astuces, données, tidyverse
Date :

On nous demande souvent comment réaliser des opérations par lignes dans un data.frame (ou un tibble) la réponse est, comme souvent, “ca dépend” 🙂

Voyons ensemble quelques cas de figure qui devraient correspondre à vos besoins.

library(tidyverse)

Fabriquons un jeu de données d’exemple :

base <- tibble::tibble(
  a = 1:10,
  b = 1:10,
  c = 21:30
) %>% head()
base
## # A tibble: 6 × 3
##       a     b     c
##   <int> <int> <int>
## 1     1     1    21
## 2     2     2    22
## 3     3     3    23
## 4     4     4    24
## 5     5     5    25
## 6     6     6    26

Imaginons que l’on souhaite ajouter une colonne new dont la valeur va dépendre du contenu, par ligne, des colonnes a, b et c de notre base d’exemple

Comme ceci :

# A tibble: 6 x 4
      a     b     c new      
  <int> <int> <int> <chr>    
1     1     1    21 a vaut 1 
2     2     2    22 autre cas
3     3     3    23 autre cas
4     4     4    24 autre cas
5     5     5    25 c vaut 25
6     6     6    26 autre cas

Avec case_when()

base %>%
  mutate(
    new = case_when(
      a == 1 ~ "a vaut 1",
      c == 25 ~ "c vaut 25",
      TRUE ~ "autre cas"
    )
  )
## # A tibble: 6 × 4
##       a     b     c new      
##   <int> <int> <int> <chr>    
## 1     1     1    21 a vaut 1 
## 2     2     2    22 autre cas
## 3     3     3    23 autre cas
## 4     4     4    24 autre cas
## 5     5     5    25 c vaut 25
## 6     6     6    26 autre cas

case_when() c’est bien, c’est bien plus lisible que des ifelse() imbriqués, mais ça peut vite se complexifier.
Fabriquons alors une fonction qui, en fonction des valeurs de a, b, c, nous retourne la valeur attendue.

En fonction du cas de figure (et de vos compétences) vous aurez parfois une fonction vectorisée et parfois une fonction non vectorisée. Il faut toujours privilégier la création de fonction vectorisée, mais ce n’est pas toujours possible.
Une fonction vectorisée est une fonction que l’on peut directement appliquer à un ensemble de vecteurs et qui retourne un vecteur de réponse.

Exemple de fonction vectorisée qui reprend les opérations du case_when() précédent :

fonction_vectorise <- function(a, b, c, ...){
  ifelse(a == 1 , "a vaut 1",
         ifelse(c == 25 , "c vaut 25",
                "autre cas"
         ))
}
fonction_vectorise(a = 1, c = 25, b = "R")
## [1] "a vaut 1"
fonction_vectorise(a = c(1, 1, 3), c = 27:25, b = "R")
## [1] "a vaut 1"  "a vaut 1"  "c vaut 25"

Voici la “même” fonction, mais non vectorisée :

fonction_non_vectorise <- function(a, b, c, ...){
  if ( a == 1  ) { return("a vaut 1") }
  if ( c == 25 ) { return("c vaut 25") }
  return("autre")
}
fonction_non_vectorise(a = 1, c = 25, b = "R")
## [1] "a vaut 1"
fonction_non_vectorise(a = c(1, 1, 3), c = 27:25, b = "R") # ne fonctionne pas
## Warning in if (a == 1) {: la condition a une longueur > 1 et seul le
## premier élément est utilisé
## [1] "a vaut 1"

Avec une fonction vectorisée

C’est le cas le plus simple, le plus rapide aussi.
Vous pouvez l’utiliser en l’état dans un mutate() :

base %>%
  mutate(
    new = fonction_vectorise(a = a, b = b, c = c)
  )
## # A tibble: 6 × 4
##       a     b     c new      
##   <int> <int> <int> <chr>    
## 1     1     1    21 a vaut 1 
## 2     2     2    22 autre cas
## 3     3     3    23 autre cas
## 4     4     4    24 autre cas
## 5     5     5    25 c vaut 25
## 6     6     6    26 autre cas

Avec une fonction NON vectorisée

Le résultat retourné par un mutate() n’est pas correct (la première valeur retournée est répétée…)

base %>%
  mutate(
    new = fonction_non_vectorise(a = a, b = b, c = c)
  )
## Warning in if (a == 1) {: la condition a une longueur > 1 et seul le
## premier élément est utilisé
## # A tibble: 6 × 4
##       a     b     c new     
##   <int> <int> <int> <chr>   
## 1     1     1    21 a vaut 1
## 2     2     2    22 a vaut 1
## 3     3     3    23 a vaut 1
## 4     4     4    24 a vaut 1
## 5     5     5    25 a vaut 1
## 6     6     6    26 a vaut 1

Changeons donc de stratégie.

Avec rowwise()

rowwise() est redevenue d’actualité dans le monde de {dplyr} et elle est spécifiquement conçue pour ce cas de figure :

base %>%
  rowwise() %>% 
  mutate(
    new = fonction_non_vectorise(a = a, b = b, c = c)
  )
## # A tibble: 6 × 4
## # Rowwise: 
##       a     b     c new      
##   <int> <int> <int> <chr>    
## 1     1     1    21 a vaut 1 
## 2     2     2    22 autre    
## 3     3     3    23 autre    
## 4     4     4    24 autre    
## 5     5     5    25 c vaut 25
## 6     6     6    26 autre

Avec pmap()

base %>%
  mutate(
    new = pmap_chr(list(a = a, b = b, c = c), fonction_non_vectorise)
  )
## # A tibble: 6 × 4
##       a     b     c new      
##   <int> <int> <int> <chr>    
## 1     1     1    21 a vaut 1 
## 2     2     2    22 autre    
## 3     3     3    23 autre    
## 4     4     4    24 autre    
## 5     5     5    25 c vaut 25
## 6     6     6    26 autre

Bonus avec Vectorize()

La fonction Vectorize() permet de vectoriser une fonction…
C’est un peu de la triche, mais ça peut dépanner 🙂

base %>%
  mutate(
    new = Vectorize(fonction_non_vectorise)(a = a, b = b, c = c)
  )
## # A tibble: 6 × 4
##       a     b     c new      
##   <int> <int> <int> <chr>    
## 1     1     1    21 a vaut 1 
## 2     2     2    22 autre    
## 3     3     3    23 autre    
## 4     4     4    24 autre    
## 5     5     5    25 c vaut 25
## 6     6     6    26 autre

A vous les opérations en lignes !

Expérimentez et dîtes-nous quelles sont vos pratiques !

Pour aller plus loin: https://dplyr.tidyverse.org/articles/rowwise.html


À propos de l'auteur

Vincent Guyader

Vincent Guyader

Codeur fou, formateur et expert logiciel R


Comments


Also read

{golem} 0.3.2

2022-03-11 / Colin Fay