Comment faire des boucles en R… ou pas !

Bien / Pas bien ou avec modération (Les Inconnus)
Tags : Ressources, Actualités
Date :

“Comment je fais … avec une boucle ?” Une question qui revient souvent. Et dans la majorité des cas, la meilleure réponse donnée consiste en une résolution SANS boucle. Parfois, c’est le temps d’exécution qui pose un problème, et là encore, les alternatives dont dispose R comme les fonctions de la famille apply, le package {purrr}, ou simplement le fait de reconsidérer le problème en prenant en compte que R est un langage dont la logique est vectorielle, vous permet d’avoir un code optimal.

Les boucles dans R

Histoire de ne pas décevoir les personnes qui lisent cet article dans le but d’apprendre à faire des boucles, on va les détailler ici, mais j’ose espérer que vous continuerez votre lecture car dans bien des cas, il est possible de les remplacer par des solutions plus élégantes.

La boucle for

La syntaxe s’écrit comme suit :

for (variable in vector) {
  expr
}

C’est très intuitif, on comprend : pour chaque élément variable du vecteur vector, exécuter expr. Par exemple, je crée un vecteur de taille 100 échantillonné dans une loi normale centrée réduite, et je souhaite calculer les valeurs au carré :

# Vecteur échantillon dans une loi normale N(0,1) :
v_norm <- rnorm(n = 100, mean = 0, sd = 1)
# Vecteur qui stockera les valeurs au carré : initialisation
v_normcarre <- rep(x = NA, times = 100)
# Boucle qui fait le job :
for (i in 1:100) {
  v_normcarre[i] <- v_norm[i]^2
}

D’ailleurs, pour être plus générique, on préférera l’utilisation de seq_along() :

# Boucle avec seq_along
for (i in seq_along(v_norm)) {
  v_normcarre[i] <- v_norm[i]^2
}

Autre exemple : je souhaite calculer la somme de chaque ligne d’une matrice :

# Ma matrice :
my_mat <- matrix(rnorm(100), nrow = 10, ncol = 10)
# Vecteur qui contiendra la somme de chaque ligne
vec_sum <- rep(NA, times = 10)
# Boucle sur les lignes
for (i in 1:nrow(my_mat)) {
  vec_sum[i] <- sum(my_mat[i,])
}

Facile non ? Oui, mais vraiment loin d’être optimal…

La boucle while (“tant que”)

Pour la syntaxe, on a :

while (condition) {
  expr
}

Comme pour la boucle for, l’utilisation de la boucle while est assez intuitive. Ici, on pourrait traduire cela par : tant que la condition est vraie TRUE, exécuter expr. Reprenons l’exemple de notre vecteur à mettre au carré :

# Vecteur échantillon dans une loi normale N(0,1) :
v_norm <- rnorm(n = 100, mean = 0, sd = 1)
# Vecteur qui stockera les valeurs au carré : initialisation
v_normcarre <- rep(x = NA, times = 100)
# Boucle qui fait le job :
i <- 1 # initialisation
while (i <= length(v_norm)) {
  v_normcarre[i] <- v_norm[i]^2
  i <- i + 1 # incrementation du compteur
}

La valeur de i doit être initialisée dans un premier temps, puis est incrémentée à chaque tour de boucle. Nous l’utilisons comme compteur. Pour l’exemple de la somme de chaque ligne d’une matrice, cela nous donne de manière similaire :

# Ma matrice :
my_mat <- matrix(rnorm(100), nrow = 10, ncol = 10)
# Vecteur qui contiendra la somme de chaque ligne
vec_sum <- rep(NA, times = 10)
# Boucle sur les lignes
i <- 1 # initialisation
while (i <= nrow(my_mat)) {
  vec_sum[i] <- sum(my_mat[i,])
  i <- i + 1 # incrementation du compteur
}

Voilà grosso-modo comment fonctionnent les boucles for et while dans R. Alors oui, je pourrais m’étendre un peu plus sur le sujet et vous parler des boucles repeat, des sorties de boucles avec break, du passage à la prochaine itération avec next, mais ce qui m’intéresse ici c’est plutôt de vous montrer comment éviter de faire des boucles à tout va.

Utiliser la puissance de la vectorisation

On vous l’a beaucoup répété, R est un langage vectoriel. Le vecteur est en effet la structure élémentaire dans R. Mais la vectorisation, c’est quoi ? On pourrait traduire cela par la conversion d’une répétition d’opérations sur des éléments simples en une unique opération sur des vecteurs. Bon nombre de fonctions sont vectorisées, c’est-à-dire faites pour s’appliquer à des vecteurs. On peut citer :

  • Les fonctions les plus simples sont les opérateurs arithmétiques de base +, -, *, / :
Ajout d’une valeur à un vecteur avec une boucle for – PAS BIEN

x1 <- 1:10
valeur <- 100
ajout_10 <- rep(NA, times = 10)
for (i in seq_along(x1)) {
  ajout_10[i] <- x1[i] + valeur
}
Ajout d’une valeur à un vecteur avec une opération vectorisée – BIEN

x1 <- 1:10
valeur <- 100
ajout_10 <- x1 + valeur

A priori, il ne devrait pas vous venir à l’idée de faire l’opération précédente avec une boucle. Alors pourquoi le faire avec plusieurs vecteurs ?

Somme de 2 vecteurs avec une boucle for – PAS BIEN

x1 <- 1:10
x2 <- 11:20
sum_x <- rep(NA, times = 10)
for (i in 1:10) {
  sum_x[i] <- x1[i] + x2[i]
}
Somme de 2 vecteurs avec une opération vectorisée – BIEN

x1 <- 1:10
x2 <- 11:20
sum_x <- x1 + x2
  • Autres fonctions vectorisées utiles, les opérateurs logiques (renvoie TRUE ou FALSE) : <, >, <=, >=, ==, !=, %in%
Comparaison de 2 vecteurs avec une boucle for – PAS BIEN

x1 <- 1:10
x2 <- 10:1
inferieur <- logical(length = 10)
for (i in 1:10) {
  inferieur[i] <- x1[i] < x2[i]
}
Comparaison de 2 vecteurs avec une opération vectorisée – BIEN

x1 <- 1:10
x2 <- 11:20
inferieur <- x1 < x2

Notez que les opérations vectorisées présentées ici avec les opérateurs arithmétiques et logiques ne sont correctes que parce que les deux vecteurs sont de même taille. Pour éviter les problèmes, faites en sorte que ce soit le cas.

  • Et pour n’en citer que quelques unes : log(), exp(), paste(), … s’appliquent à des vecteurs. Si je souhaite par exemple créer un vecteur c("Obs. 1", "Obs. 2", ..., "Obs. 10") :
Avec une boucle for – PAS BIEN

v_obs <- character(length = 10)
for (i in seq_along(v_obs)) {
  v_obs[i] <- paste("Obs.", i)
}
Avec une opération vectorisée – BIEN

v_obs <- paste("Obs.", 1:10)
  • Les calculs matriciels sont également vectorisés, par exemple pour la multiplication terme à terme :
Avec une boucle for – PAS BIEN

mat1 <- matrix(1:4, nrow = 2, ncol = 2)
mat2 <- matrix(rep(10, 4), nrow = 2, ncol = 2)
mat_produit <- matrix(NA, nrow = 2, ncol = 2)
for (i in 1:nrow(mat1)) {
  for (j in 1:ncol(mat1)) {
    mat_produit[i,j] <- mat1[i,j] * mat2[i,j]
  }
}
Avec une opération vectorisée – BIEN

mat1 <- matrix(1:4, 2, 2)
mat2 <- matrix(rep(10, 4), 2, 2)
mat_produit <- mat1 * mat2

(pour info, la multiplication matricielle s’écrit %*%)

  • Autres exemples non exhaustifs de fonctions qui peuvent s’appliquer à des matrices : rowSums(), colSums(), rowMeans(), colMeans(). Si on reprend notre exemple précédent dans lequel on calcule la somme des éléments de chaque ligne :
Avec une boucle for – PAS BIEN

# Ma matrice :
my_mat <- matrix(rnorm(100), nrow = 10, ncol = 10)
# Vecteur qui contiendra la somme de chaque ligne
vec_sum <- rep(NA, times = 10)
# Boucle sur les lignes
for (i in 1:nrow(my_mat)) {
  vec_sum[i] <- sum(my_mat[i,])
}
Avec une opération vectorisée – BIEN

# Ma matrice :
my_mat <- matrix(rnorm(100), nrow = 10, ncol = 10)
# Vecteur qui contiendra la somme de chaque ligne
vec_sum <- rowSums(my_mat)

Penser “vecteurs” avec la famille apply

Si vous avez déjà pu supprimer un bon nombre de vos boucles grâce aux opérations vectorisées, les fonctions de la famille apply vont vous motiver à continuer la chasse aux boucles for et while. L’appel à ces fonctions vous évitera l’utilisation plus fastidieuse et plus propice aux erreurs des boucles for ou while et peut vous permettre d’accélérer les calculs.

L’idée est d’appliquer une fonction FUN aux éléments :

  • d’une matrice, avec la fonction apply(). On utilisera le paramètre MARGIN = 1 pour appliquer la fonction aux lignes de la matrice, MARGIN = 2 pour les colonnes. Reprenons l’exemple du calcul de la somme sur les lignes :
Avec une boucle for – PAS BIEN

# Ma matrice :
my_mat <- matrix(rnorm(100), nrow = 10, ncol = 10)
# Vecteur qui contiendra la somme de chaque ligne
vec_sum <- rep(NA, times = 10)
# Boucle sur les lignes
for (i in 1:nrow(my_mat)) {
  vec_sum[i] <- sum(my_mat[i,])
}
avec apply – BIEN

# Ma matrice :
my_mat <- matrix(rnorm(100), nrow = 10, ncol = 10)
# Vecteur qui contiendra la somme de chaque ligne
vec_sum <- apply(my_mat, MARGIN = 1, FUN = sum)
  • d’une liste ou d’un vecteur (ou même d’un data frame), avec la fonction lapply(). Dans certains cas, lapply() renvoie une liste. Pour renvoyer un vecteur, on utilisera la fonction sapply(), ou encore vapply(). J’ai par exemple une liste de matrices dont je souhaite calculer la somme de tous les éléments :
avec une boucle for – PAS BIEN

my_list <- list(
  m1 = matrix(1:4, nrow = 2, ncol = 2),
  m2 = matrix(5:8, nrow = 2, ncol = 2)
)
# Vecteur qui contiendra la somme de tous les éléments de chaque matrice
sum_mat <- rep(NA, length(my_list))
# Boucle sur les matrices
for (i in seq_along(my_list)) {
  sum_mat[i] <- sum(my_list[[i]])
}
avec sapply – BIEN

# sapply pour renvoyer un vecteur :
sum_mat <- sapply(my_list, FUN = sum)
  • La fonction mapply() est la version multi-variables de sapply() et lapply(). Comme rien ne vaut un exemple pour aider à la compréhension : je souhaite obtenir un vecteur contenant 1 fois la lettre “A”, 2 fois “B”, …, 5 fois “E” en appliquant la fonction rep().
avec une boucle for – PAS BIEN

fois <- 1:5
lettres <- LETTERS[1:5]
# Vecteur resultat
res_lettres <- character()
# Boucle for
for (i in seq_along(fois)) {
  res_lettres <- c(res_lettres, rep(lettres[i], times = fois[i]))
}
avec mapply – BIEN

fois <- 1:5
lettres <- LETTERS[1:5]
# Vecteur resultat : unlist pour obtenir un vecteur
res_lettres <- unlist(mapply(FUN = rep, lettres, fois))

Note : Il faudra bien faire attention à la sortie de vos fonctions apply : selon les cas, cela pourra être un vecteur ou une liste.

La stratégie “split-apply-combine” (dépassée) de {plyr}

Avant que n’existe les packages {dplyr} et {purrr}, les fonctions de {plyr} étaient une bonne alternative aux boucles. Mais comme il y a mieux maintenant, on ne va pas trop entrer dans le détail. Ce qu’il faut savoir, c’est que {plyr} dispose d’une douzaine de fonctions permettant d’appliquer une fonction à un ensemble d’éléments. Ces fonctions s’écrivent sous la forme {X}{Y}ply(){X} et {Y} seront différents selon les formats d’entrée ({X}) et de sortie ({Y}) :

  • a pour array, ou matrice
  • d pour dataframe
  • l pour liste
  • **_** pour rien – seulement valable pour Y, quand la fonction ne renvoie rien

Par exemple, si j’ai une liste en entrée et que je souhaite un dataframe en sortie, j’appellerai la fonction ldply().

Maîtriser le format de sortie avec {purrr}

{purrr} est un package du {tidyverse} que nous avons déjà abordé il y a quelques années dans “Un code qui ronronne avec {purrr}”. C’est un package avec très peu de dépendances. Installation et chargement du package :

install.packages("purrr")
library(purrr)

Comme le disait Colin dans son article :

Toutes les fonctions de {purrr} sont construites en suivant un même schéma : function(.x, .f) où :

.x est un vecteur

.f est une fonction pure (des fonctions opérant en l’absence de “side effects” — qui ne se reposent pas sur des données externes, et qui ne changent pas les données qui existent en dehors)

– le résultat est toujours un vecteur du même type que .x

Ses fonctions map permettent d’appliquer la même fonction .f à chaque élément d’une liste ou d’un vecteur .x. Elles permettent une meilleure maîtrise sur les formats de sortie que les fonctions de la famille apply. Quatre fonctions principales, selon le contexte :

  • map(.x, .f, …) : applique la fonction .f à tous les éléments du vecteur (ou de la liste) .x
  • map2(.x, .y, .f, …) : version multivariée de map, .f doit admettre 2 paramètres et sera appliquée aux paires d’éléments des vecteurs (ou listes) .x et .y
  • pmap(.l, .f, …) : version multivariée de map, .f admettant 2 paramètres ou plus sera appliquée aux n-uplets des éléments des vecteurs (ou listes) contenus dans la liste .l
  • imap(.x, .f, ...) : applique la fonction .f à tous les éléments du vecteur (ou de la liste) .x et à son index.

Ces fonctions renvoient toutes des listes, mais si on souhaite un format de sortie différent, on utilisera l’extension :

  • _chr : pour renvoyer un vecteur de chaines de caractères
  • _dfc : pour renvoyer un vecteur numérique
  • _dfc : pour renvoyer un dataframe (construction par colonne bind_cols())
  • _dfr : pour renvoyer un dataframe (construction par ligne bind_rows())
  • _int : pour renvoyer un vecteur d’entiers
  • _lgl : pour renvoyer un vecteur de booléens

Exemple : pmap_int() pour appliquer, sur une liste de 3 variables, une fonction a 3 paramètres qui renvoie un nombre entier. La sortie de pmap_int() sera un vecteur d’entiers.

Appliquer une fonction sur chaque élément d’une liste ou d’un vecteur avec map()

Exemple : Je souhaite importer tous les fichiers “csv” de mon répertoire et stocker les dataframes dans une liste.

# Créer des fichiers csv dans mon répertoire
readr::write_csv(iris, "iris.csv")
readr::write_csv(mtcars, "mtcars.csv")
readr::write_csv(cars, "cars.csv")
# Vecteur contenant les fichiers à importer
files <- list.files(".", pattern = ".csv")
files
## [1] "cars.csv"   "iris.csv"   "mtcars.csv"
Avec une boucle for – PAS BIEN

# Liste resultat 
# il n'est pas nécessaire d'initialiser la taille de la liste
list_df <- list()
# Boucle for
for (i in seq_along(files)) {
  list_df[[i]] <- readr::read_delim(files[i], delim = ",")
}
Avec map – BIEN

list_df <- map(files, readr::read_delim, delim = ",")

Notez comme le paramètre de la fonction est listé dans map.

La fonction map() peut également s’appliquer avec trois autres suffixes, permettant de conditionner l’appel à la fonction concernée :

  • map_if(.x, .p, .f, .else = NULL) : applique .f aux éléments de .x qui vérifient .p, ou la fonction .else si renseignée sinon. Par exemple :
Avec une boucle for – PAS BIEN

# Liste de valeurs
liste <- list(1, 2, 3, "a", "b")
# Liste resultat
liste_res <- list()
# Boucle for
for (i in seq_along(liste)) {
  liste_res[[i]] <- 
    ifelse(
      is.numeric(liste[[i]]), # if
      exp(liste[[i]]), # then
      0 # else
    )
}
Avec map_if – BIEN

# Liste de valeurs
liste <- list(1, 2, 3, "a", "b")
# resultat
liste_res <- map_if(
  liste,
  is.numeric, # if
  exp, # then
  .else = function(x) return(0)
)
  • map_at(.x, .at, .f) : applique .f aux éléments de .x uniquement aux positions définies par .at, le reste est conservé.
Avec une boucle for – PAS BIEN

# Liste de valeurs
liste <- list(1, 2, 3, "a", "b")
# Liste de resultats copie de la liste originale
liste_res <- liste
# Boucle for
for (i in 1:3) {
  liste_res[[i]] <- exp(liste[[i]])
}
Avec map_at – BIEN

# Liste de valeurs
liste <- list(1, 2, 3, "a", "b")
# resultat
liste_res <- map_at(liste, 1:3, exp)
  • map_depth(.x, .depth, .f) : applique .f aux éléments de .x qui sont au niveau .depth. Par exemple :
# Liste de valeurs
liste <- list(
  a = list(
    a1 = c(1, 2, 3),
    a2 = c(4, 5, 6)
  ),
  b = list(
    b1 = c(7, 8, 9)
  )
)
liste
## $a
## $a$a1
## [1] 1 2 3
## 
## $a$a2
## [1] 4 5 6
## 
## 
## $b
## $b$b1
## [1] 7 8 9
Avec une boucle for – PAS BIEN

# Liste resultat, copie de la liste originale
liste_res <- liste
# Boucle for
for (i in seq_along(liste)) {
  for(j in seq_along(liste[[i]])) {
    liste_res[[i]][[j]] <- sum(liste[[i]][[j]])
  }
}
avec map_depth – BIEN

liste <- list(a = list(a1 = c(1, 2, 3),
                       a2 = c(4, 5, 6)),
              b = list(b1 = c(7, 8, 9)))
# Liste resultat
liste_res <- map_depth(liste, 2, sum)

La fonction map2() pour appliquer une fonction sur 2 objets de même longueur

Reprenons l’exemple donné pour illustrer la fonction mapply() : je souhaite obtenir un vecteur contenant 1 fois la lettre “A”, 2 fois “B”, …, 5 fois “E” en appliquant la fonction rep()

Avec une boucle for – PAS BIEN

fois <- 1:5
lettres <- LETTERS[1:5]
# Vecteur resultat
res_lettres <- character()
# Boucle for
for (i in seq_along(fois)) {
  res_lettres <- c(res_lettres, rep(lettres[i], times = fois[i]))
}
avec map2 – BIEN

fois <- 1:5
lettres <- LETTERS[1:5]
# Vecteur resultat : unlist obtenir un vecteur de char
res_lettres <- unlist(map2(lettres, fois, rep))

Bon, en vrai, dans ce cas, la fonction rep() est elle-même déjà vectorisée. D’ailleurs c’est un peu le sens de cet article : vérifiez d’abord si la fonction que vous souhaitez utiliser n’est pas déjà vectorisée avant de vous lancer dans des opérations compliquées. Ainsi, avec rep(), on peut faire :

rep(lettres, times = fois)

Appliquer une fonction avec plusieurs paramètres en utilisant pmap()

Exemple : je souhaite appliquer la fonction gsub() qui permet de remplacer un morceau de chaîne de caractère par un autre. La fonction gsub() prend 3 paramètres : pattern remplacé par replacement dans la chaîne de caractères x.

Notez qu’une fois encore, nous nous intéressons à des utilisations terme à terme, c’est-à-dire que les vecteurs utilisés ensembles doivent être de même longueur. Assurez-vous d’être dans ces conditions à chaque fois, comme ça vous êtes sûrs de ce que vous êtes en train de manipuler et de ce que vous comparez à quoi. En effet, R utilise ce qu’on appelle le recyclage. Si les objets ne font pas la même taille, il recycle les valeurs et là, vous ne savez plus forcément ce qu’il se passe. Regardez par exemple le résultat de 1:3 + 1:4, malgré le warning, il y a quand même un résultat utilisable…

Donc, nous construisons trois vecteurs de même taille. Les modifications seront appliquées terme à terme : montagne avec pattern = n et replacement = N.

vec_texte <- c("montagne", "campagne", "littoral")
pattern <- c("n", "a", "t")
replacement <- c("N", "A", "T")
Avec une boucle for – PAS BIEN

# Vecteur resultat
res <- character(3)
# Boucle for
for (i in seq_along(vec_texte)) {
  res[i] <- gsub(pattern[i], replacement[i], vec_texte[i])
}
avec pmap – BIEN

# Vecteur resultat : pmap_chr obtenir un vecteur de char
res <- pmap_chr(list(pattern, replacement, vec_texte), gsub)
res
## [1] "moNtagNe" "cAmpAgne" "liTToral"

Je vous mets ici le lien vers la cheatsheet de {purrr} (en anglais), c’est toujours utile de l’avoir sous les yeux : https://github.com/rstudio/cheatsheets/blob/master/purrr.pdf

Appliquer une fonction itérativement avec do.call()

do.call(what, args) est une autre alternative aux boucles qui applique la fonction what à TOUS les éléments de la liste args, dans leur ensemble, et 1 par 1 comme pour les fonctions vues précédemment. Par exemple, j’ai une liste de vecteurs dont je souhaite faire une matrice :

ma_liste <- list(
  v1 = 1:10,
  v2 = 11:20,
  v3 = 21:30,
  v4 = 31:40
)
Avec une boucle for – PAS BIEN

# Vecteur resultat - initiation avec le premier élément
res <- ma_liste[[1]]
# Boucle for
for (i in 2:length(ma_liste)) {
  res <- cbind(res, ma_liste[[i]])
}
Avec do.call – BIEN

# Vecteur resultat 
res <- do.call(cbind, ma_liste)
res
##       v1 v2 v3 v4
##  [1,]  1 11 21 31
##  [2,]  2 12 22 32
##  [3,]  3 13 23 33
##  [4,]  4 14 24 34
##  [5,]  5 15 25 35
##  [6,]  6 16 26 36
##  [7,]  7 17 27 37
##  [8,]  8 18 28 38
##  [9,]  9 19 29 39
## [10,] 10 20 30 40

Appliquer une fonction sur différents groupes séparément

Supposons que j’ai un dataframe contenant une variable de groupes et que je souhaite faire la même opération sur les différents groupes. Plusieurs solutions s’offrent à moi. Prenons par exemple le jeu de données starwars du package {dplyr} qui recense les personnage des films :

library(dplyr)
data("starwars")
starwars
## # A tibble: 87 x 14
##    name  height  mass hair_color skin_color eye_color birth_year sex   gender homeworld species films
##    <chr>  <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr>  <chr>     <chr>   <lis>
##  1 Luke~    172    77 blond      fair       blue            19   male  mascu~ Tatooine  Human   <chr~
##  2 C-3PO    167    75 <NA>       gold       yellow         112   none  mascu~ Tatooine  Droid   <chr~
##  3 R2-D2     96    32 <NA>       white, bl~ red             33   none  mascu~ Naboo     Droid   <chr~
##  4 Dart~    202   136 none       white      yellow          41.9 male  mascu~ Tatooine  Human   <chr~
##  5 Leia~    150    49 brown      light      brown           19   fema~ femin~ Alderaan  Human   <chr~
##  6 Owen~    178   120 brown, gr~ light      blue            52   male  mascu~ Tatooine  Human   <chr~
##  7 Beru~    165    75 brown      light      blue            47   fema~ femin~ Tatooine  Human   <chr~
##  8 R5-D4     97    32 <NA>       white, red red             NA   none  mascu~ Tatooine  Droid   <chr~
##  9 Bigg~    183    84 black      light      brown           24   male  mascu~ Tatooine  Human   <chr~
## 10 Obi-~    182    77 auburn, w~ fair       blue-gray       57   male  mascu~ Stewjon   Human   <chr~
## # ... with 77 more rows, and 2 more variables: vehicles <list>, starships <list>

On souhaite connaître la taille min, max, moyenne en fonction du sexe.

  • Avec une boucle forPAS BIEN
groupes <- na.omit(unique(starwars$sex))
# Initier un tableau vide de stockage des résultats
taille_by_sex <- tibble()
# Boucle for
for (g in groupes) {
  taille_groupe <- starwars %>%
    filter(sex == g) %>%
    pull(height)
  taille_by_sex <- rbind(
    taille_by_sex, 
    tibble(
      sex = g,
      height.min = min(taille_groupe, na.rm = TRUE),
      height.max = max(taille_groupe, na.rm = TRUE),
      height.mean = mean(taille_groupe, na.rm = TRUE)
    )
  )
}
  • Avec la fonction aggregate()BIEN
aggregate(height~sex,
          data = starwars, 
          FUN = function(x) 
            c(
              min = min(x, na.rm = TRUE),
              max = max(x, na.rm = TRUE), 
              mean = mean(x, na.rm = TRUE)
            )
)
  • Avec la fonction summarise() du package dplyr()MIEUX
starwars %>%
  filter(!is.na(sex)) %>%
  group_by(sex) %>%
  summarise(
    height.min = min(height, na.rm = TRUE), 
    height.max = max(height, na.rm = TRUE), 
    height.mean = mean(height, na.rm = TRUE),
    .groups = "drop"
  )
## # A tibble: 4 x 4
##   sex            height.min height.max height.mean
##   <chr>               <int>      <int>       <dbl>
## 1 female                150        213        169.
## 2 hermaphroditic        175        175        175 
## 3 male                   66        264        179.
## 4 none                   96        200        131.

Les questions / réponses autour des boucles for sur Internet

Nous avons écumé les questions quant à l’utilisation des boucles for sur StackOverflow, et il se trouve que même si la solution n’a pas toujours été donnée sans boucle, nous avons une solution sans !

Ci-dessous un top 3 des questions “for-loop” en R, classées par le nombre de votes :

1 : une boucle itérative trop lente

Le code donné dans cet exemple est le suivant : une fonction qui met beaucoup trop de temps à tourner

  • Avec une boucle forAVEC MODERATION
dayloop2 <- function(temp) {
  for (i in 1:nrow(temp)) {
    temp[i, 10] <- i
    if (i > 1) {
      if ((temp[i, 6] == temp[i - 1, 6]) & (temp[i, 3] == temp[i - 1, 3])) {
        temp[i, 10] <- temp[i, 9] + temp[i - 1, 10]
      } else {
        temp[i, 10] <- temp[i, 9]
      }
    } else {
      temp[i, 10] <- temp[i, 9]
    }
  }
  return(temp)
}

Beaucoup de réponses de qualité ont été données, mais aucune sans boucle for,

Alors sincèrement je me suis un peu cassé les dents sur cette fonction purement itérative qui mériterait bien sa boucle for sans l’existence de la fonction accumulate() du package {purrr}, je vous propose donc cette solution :

  • Avec accumulateMIEUX
dayloop_accumulate <- function(temp) {
  temp %>%
    as_tibble() %>%
    mutate(cond = c(FALSE, (V6 == lag(V6) & V3 == lag(V3))[-1])) %>%
    mutate(V10 = V9 %>%
      purrr::accumulate2(.y = cond[-1], .f = function(.i_1, .i, .y) {
        if (.y) {
          .i_1 + .i
        } else {
          .i
        }
      }) %>% unlist()) %>%
    select(-cond)
}
# Utilisation
n <- 20
X <- as.data.frame(matrix(sample(1:10, n*9, TRUE), n, 9))
dayloop_accumulate(X)

Lien vers la question

2 : supprimer certaines valeurs d’un vecteur

La question ici est la suivante : on souhaite supprimer les valeurs de v_suppr d’un vecteur numérique vec.

Question : avec une boucle forPAS BIEN

vec <- sample(seq(1, 100), 1000, replace = TRUE)
v_suppr <-  sample(seq(1, 100), 100, replace = TRUE)
# Boucle for
for (val in a_suppr) {
  vec <- vec[!vec == val]
}
Réponse : vectorisation – BIEN

vec[!vec %in% v_suppr]

Lien vers la question

3 : une même modification pour certaines colonnes d’un data.table

On souhaite par exemple multiplier par -1 les éléments de certaines colonnes d’un data.table :

library(data.table)
dt <- data.table(a = 1:3, b = 1:3, d = 1:3)
cols <- c("a", "b")
Question : avec une boucle forPAS BIEN

for (col in 1:length(cols)) {
   dt[ , eval(parse(text = paste0(cols[col], ":=-1*", cols[col])))]
}
Réponse : sans boucle for – BIEN

dt[ , (cols) := lapply(.SD, "*", -1), .SDcols = cols]

ou :

dt[ , (cols) := map(.SD, function(x) -x), .SDcols = cols]

Lien vers la question

Conclusion

Et voilà pour les boucles ! Je vous laisse faire vos tests, comparer les performances du code et modifier votre vision du fonctionnement de R. J’espère qu’à partir de maintenant vous réfléchirez à deux fois avant de faire une boucle !

Article rédigé par Elena Salette, avec la participation de Sébastien Rochette.


À propos de l'auteur


Commentaires


À lire également