Les boucles avec R (et au delà)

Author

Noé Barthelemy - ISEE

I) Introduction

Salut à tous ! Aujourd’hui, on va se plonger dans le monde passionnant des boucles avec RStudio.

Imaginez que vous devez répéter une tâche spécifique plusieurs fois. Par exemple, disons que vous voulez calculer la moyenne des notes de toute une classe. Vous pourriez le faire manuellement pour chaque élève, mais avec une boucle, c’est comme donner à RStudio le feu vert pour le faire à votre place, en mode automatique. Vous vous en doutez, ça va vous sauver pas mal de temps et d’effort.

Dans ce cours, on va explorer les différents types de boucles en R : for, while, et même comment éviter les boucles pour privilégier des fonctions comme apply quand c’est plus efficace. On va voir des exemples concrets pour que vous puissiez voir ces boucles en action et comprendre comment et quand les utiliser.

Allez, c’est parti !



II) Boucles

Pas besoin de réinventer la roue, je vous laisse suivre ce lien pour en apprendre plus à propos des boucles classiques : Cours de l’INSEE sur les boucles

Je vous invite à prêter une attention particulière au point 17.3.4 à propos de “Quand ne pas boucler”, que voilà en résumé :

Première chose à savoir : beaucoup de fonctions en R sont “vectorisées”, ce qui signifie qu’elles peuvent traiter un vecteur entier d’un coup, plutôt que de devoir passer par chaque élément un par un. C’est plus rapide et plus propre. Par exemple, plutôt que de faire une boucle for pour ajouter 10 à chaque élément d’un vecteur, on fera juste un simple x + 10. Rapide, facile, et on garde notre code bien lisible.

Prenez les chaînes de caractères. Si on veut remplacer “X” par “o” dans un vecteur, au lieu de s’embêter avec une boucle, str_replace_all() fait le job en une ligne, de manière vectorisée. Et pour les valeurs manquantes dans un vecteur, plutôt que de lancer une boucle pour les remplacer, on utilise l’opérateur [] pour faire tout ça plus rapidement.

Mais ce n’est pas tout, même pour les choses un peu plus complexes, comme compter les voyelles dans une série de mots, au lieu d’utiliser une boucle, des fonctions comme map_int de purrr peuvent vous simplifier la vie. On évite les boucles (longues, parfois difficiles à lire) et on garde un code qui reste clair et efficace.

Donc, même si les boucles ont leur place et peuvent être utiles, la plupart du temps, avec les bonnes fonctions, on peut faire beaucoup sans elles.



III) Eviter les boucles avec “Apply”

Vous l’avez compris, les boucles sont utiles, mais parfois absolument pas nécessaires. Il est même probable que vous en utilisiez de moins en moins au fil du temps et de votre progression.

Vous serez donc amenés à utiliser les fonctions de la famille apply ! Ces fonctions sont des outils extrêmement puissants et flexibles qui vous aideront à écrire des codes plus efficaces et plus propres, surtout lorsque vous travaillez avec des collections de données comme des vecteurs, des matrices ou des listes.


A) Qu’est-ce que la famille apply ?

Les fonctions apply permettent d’appliquer une fonction à des éléments ou à des ensembles d’éléments de structures de données plus complexes. Cela vous évite d’avoir à écrire des boucles explicites, ce qui peut souvent rendre votre code plus rapide à écrire, à exécuter, et plus facile à comprendre.


B) Les principales fonctions de la famille apply

  1. apply() : Utilisée principalement pour des matrices. Vous spécifiez la matrice, l’indice (1 pour les lignes ou 2 pour les colonnes), et la fonction à appliquer. Par exemple, apply(ma_matrice, 1, mean) calcule la moyenne de chaque ligne de la matrice.

  2. lapply() : Parfaite pour les listes. Cette fonction prend une liste et applique une fonction à chaque élément de cette liste. Par exemple, lapply(ma_liste, length) retournera une nouvelle liste contenant la longueur de chaque élément.

  3. sapply() : Une variante de lapply qui simplifie le résultat en vecteur ou en matrice si possible, ce qui peut être plus pratique selon ce que vous voulez faire avec le résultat. Par exemple, sapply(ma_liste, sum) additionne les éléments de chaque sous-liste et retourne un vecteur des sommes.

  4. tapply() : Utile pour appliquer une fonction à des sous-groupes de vecteurs. Vous fournissez un vecteur, un facteur de groupement, et une fonction à appliquer. Cela peut être utilisé, par exemple, pour calculer des moyennes de groupe.

  5. mapply() : Une version multivariée de lapply. mapply permet d’appliquer une fonction à plusieurs listes simultanément. Par exemple, si vous avez deux listes de nombres et que vous voulez leur somme élément par élément, mapply(sum, liste1, liste2) fera l’affaire.


C) Pourquoi utiliser les fonctions apply ?

  • Efficacité : Ces fonctions sont souvent plus rapides que les boucles équivalentes, surtout pour les grands ensembles de données.
  • Clarté : Utiliser des fonctions apply rend votre code plus compact et souvent plus facile à lire, en vous permettant de vous concentrer sur ce que vous faites plutôt que sur la manière de le faire.
  • Pratique : Elles réduisent le besoin de coder des boucles explicites, ce qui diminue les risques d’erreurs et simplifie la maintenance du code.


D) Exemple pratique

Imaginons que vous avez une liste de vecteurs numériques et que vous souhaitez calculer la moyenne de chaque vecteur. Avec sapply, c’est simple :

vecteurs <- list(c(1, 2, 3), c(4, 5, 6), c(7, 8, 9))
moyennes <- sapply(vecteurs, mean)
print(moyennes)
[1] 2 5 8

Ce petit bout de code remplacerait une boucle for plus complexe et moins claire.


E) Boucles vs Apply

Vous êtes sceptiques ? Vous vous dites “oh, allez, la flemme, mes boucles elle marchent très bien et en plus j’ai mis du temps à les coder” ? Je comprend !

Pour vous convaincre, voici des exemples ou l’utilisation des fonctions de la famille apply est plus efficace que l’utilisation de boucles :

Exemple 1: Analyse de données sur le commerce extérieur

Vous avez un ensemble de données sur le commerce extérieur par pays pour plusieurs années. Vous voulez calculer la moyenne des importations et des exportations pour chaque pays sur toutes les années.

Avec une boucle :

# Supposition initiale que `donnees` est une dataframe contenant les colonnes 'Pays', 
# 'Annee', 'Importations', 'Exportations'

# Création d'un dataframe vide appelé `resultats` pour stocker les résultats finaux.
resultats <- data.frame()  

# Extraction d'une liste de tous les pays uniques présents dans le dataframe `donnees`.
pays_unique <- unique(donnees$Pays)  

for (p in pays_unique) {  # Boucle qui parcourt chaque pays unique dans `pays_unique`.
  # Création d'un sous-ensemble de données pour le pays courant `p`.
  subset_pays <- donnees[donnees$Pays == p,]  
  
  # Calcul de la moyenne des importations pour le pays courant.
  moy_importations <- mean(subset_pays$Importations)  
  
  # Calcul de la moyenne des exportations pour le pays courant.
  moy_exportations <- mean(subset_pays$Exportations)  

  # Ajout des résultats calculés au dataframe `resultats`, incluant le pays et les 
  # moyennes des importations et des exportations.
  resultats <- rbind(resultats, data.frame(Pays = p, Moy_Import = moy_importations, 
                                           Moy_Export = moy_exportations))
}

Avec tapply() :

# Utilisation de `tapply` pour calculer la moyenne des importations par pays.
moy_importations <- tapply(donnees$Importations, donnees$Pays, mean)

# Utilisation de `tapply` pour calculer la moyenne des exportations par pays.
moy_exportations <- tapply(donnees$Exportations, donnees$Pays, mean)

# Création d'un nouveau dataframe appelé `resultats` qui combine les résultats
# des deux opérations précédentes.
# 'Pays = names(moy_importations)' utilise les noms des éléments dans le vecteur de moyennes comme noms de pays,
# 'Moy_Import = moy_importations' et 'Moy_Export = moy_exportations' ajoutent les vecteurs de moyennes calculées pour les importations et exportations respectivement.
resultats <- data.frame(Pays = names(moy_importations), Moy_Import = moy_importations, Moy_Export = moy_exportations)

Dans ce cas, utiliser tapply() simplifie le code, le rendant plus court et plus lisible. De plus, il est généralement plus rapide que d’utiliser une boucle explicite, surtout si le nombre de pays est important.

Exemple 2: Suivi de l’emploi salarié par secteur

Imaginez maintenant que vous souhaitez suivre l’évolution de l’emploi salarié dans différents secteurs économiques. Vous disposez de données mensuelles et vous voulez calculer la croissance moyenne annuelle de l’emploi pour chaque secteur.

Avec une boucle :

# Supposons que `emploi` est une dataframe avec les colonnes 'Secteur', 'Mois', 'Annee', 'Emplois'

# Extraction d'une liste des secteurs uniques présents dans la dataframe `emploi`.
secteurs_unique <- unique(emploi$Secteur)  

# Création d'un dataframe vide appelé `resultats` pour stocker les résultats finaux.
resultats <- data.frame()  

# Début de la première boucle qui parcourt chaque secteur unique stocké dans `secteurs_unique`.
for (s in secteurs_unique) {
  # Création d'un sous-ensemble de données pour le secteur courant `s`.
  subset_secteur <- emploi[emploi$Secteur == s,]  

  # Début de la deuxième boucle qui parcourt chaque année unique dans le sous-ensemble du secteur.
  for (annee in unique(subset_secteur$Annee)) {
    # Création d'un sous-ensemble de données pour l'année courante `annee` dans le secteur courant.
    subset_annee <- subset_secteur[subset_secteur$Annee == annee,]  
    
    # Calcul de la moyenne du nombre d'emplois pour l'année et le secteur courant.
    moy_emplois <- mean(subset_annee$Emplois)  

    # Ajout des résultats au dataframe `resultats`, incluant le secteur, l'année et la moyenne des emplois.
    resultats <- rbind(resultats, data.frame(Secteur = s, Annee = annee, Moy_Emplois = moy_emplois))
  }
}

Avec tapply() dans une fonction sapply() :

# Définition d'une fonction `calcul_moyenne` qui calcule la moyenne des emplois pour chaque secteur.
# `annee_data` représente un sous-ensemble de données pour une année donnée.
calcul_moyenne <- function(annee_data) {
  # Utilisation de `tapply` pour appliquer la fonction `mean` aux emplois, groupés par secteur.
  tapply(annee_data$Emplois, annee_data$Secteur, mean)
}

# `split(emploi, emploi$Annee)` divise le dataframe `emploi` en sous-groupes selon les années.
# `sapply` applique la fonction `calcul_moyenne` à chaque sous-groupe d'année.
resultats <- sapply(split(emploi, emploi$Annee), calcul_moyenne)

Dans cet exemple, combiner sapply() et tapply() permet de traiter efficacement les données par année et par secteur, rendant le traitement des données non seulement plus rapide mais également plus simple à maintenir.

Ces exemples montrent comment les fonctions de la famille apply peuvent non seulement améliorer la performance du code en réduisant la complexité et le temps d’exécution mais aussi rendre le code plus lisible et plus facile à gérer, ce qui est crucial dans des environnements professionnels où l’efficacité et la clarté sont primordiales.


F) Conclusion

En résumé, les fonctions de la famille apply sont des outils essentiels dans la boîte à outils du programmeur R. Elles vous aideront à exploiter la puissance de R pour manipuler des données de manière efficace et élégante. Utilisez-les à bon escient pour rendre votre code plus propre, plus rapide et plus robuste. Bonne continuation dans votre apprentissage et pratique de R !

PS: Je me dois de parler des fonctions de purrr qui sont très pratiques, mais que je n’utilise pas personnellement. Je vous invite à y jeter un coup d’oeil si jamais les fonctions apply ne vous conviennent pas (Cours de l’INSEE sur purrr).


G) Plus d’exemples :

C’était un peu trop compliqué pour toi ? T’inquiètes, voila des exemples qui t’aideront à y voir plus clair :

Dans R, les boucles (comme les boucles for) sont souvent utilisées pour appliquer une fonction à chaque élément d’une collection (vecteur, liste, dataframe, etc.). Cependant, lapply et sapply sont des alternatives plus efficaces et concises. Voyons comment ces fonctions peuvent remplacer les boucles avec des exemples.

Exemple 1 : Appliquer une Fonction à Chaque Élément d’un Vecteur

Avec une Boucle for :

Imaginons que tu as un vecteur de nombres et que tu veux calculer le carré de chaque nombre.

nombres <- c(1, 2, 3, 4, 5)
carres <- numeric(length(nombres))  # Crée un vecteur vide pour stocker les résultats

for (i in 1:length(nombres)) {
  carres[i] <- nombres[i]^2
}

print(carres)
[1]  1  4  9 16 25

Avec sapply :

On peut obtenir le même résultat plus facilement avec sapply :

nombres <- c(1, 2, 3, 4, 5)
carres <- sapply(nombres, function(x) x^2)

print(carres)
[1]  1  4  9 16 25


Exemple 2 : Appliquer une Fonction à Chaque Élément d’une Liste

Avec une Boucle for :

Supposons que tu as une liste de vecteurs et que tu veux calculer la somme de chaque vecteur.

liste_vecteurs <- list(c(1, 2, 3), c(4, 5, 6), c(7, 8, 9))
somme_vecteurs <- numeric(length(liste_vecteurs))

for (i in 1:length(liste_vecteurs)) {
  somme_vecteurs[i] <- sum(liste_vecteurs[[i]])
}

print(somme_vecteurs)
[1]  6 15 24

Avec lapply :

Avec lapply, c’est plus simple :

liste_vecteurs <- list(c(1, 2, 3), c(4, 5, 6), c(7, 8, 9))
somme_vecteurs <- lapply(liste_vecteurs, sum)

print(somme_vecteurs)
[[1]]
[1] 6

[[2]]
[1] 15

[[3]]
[1] 24

Explication :

  • lapply applique la fonction sum à chaque élément de la liste liste_vecteurs. Le résultat est une liste où chaque élément est la somme d’un vecteur. Si tu veux un vecteur en sortie au lieu d’une liste, tu peux utiliser sapply :
somme_vecteurs <- sapply(liste_vecteurs, sum)
print(somme_vecteurs)
[1]  6 15 24

Exemple 3 : Appliquer une Fonction à Chaque Colonne d’un Dataframe

Avec une Boucle for :

Imagine que tu veux calculer la moyenne de chaque colonne d’un dataframe.

df <- data.frame(a = 1:5, b = 6:10, c = 11:15)
moyennes <- numeric(ncol(df))

for (i in 1:ncol(df)) {
  moyennes[i] <- mean(df[, i])
}

print(moyennes)
[1]  3  8 13

Avec sapply :

Avec sapply, on peut faire cela beaucoup plus rapidement :

df <- data.frame(a = 1:5, b = 6:10, c = 11:15)
moyennes <- sapply(df, mean)

print(moyennes)
 a  b  c 
 3  8 13 

Explication :

  • sapply applique la fonction mean à chaque colonne du dataframe df. Le résultat est un vecteur contenant les moyennes de chaque colonne.


Pourquoi Remplacer les Boucles par lapply et sapply ?

  • Lisibilité et Concision : Les fonctions lapply et sapply rendent le code plus lisible et plus facile à écrire, surtout pour des opérations répétitives.

  • Performance : lapply et sapply sont souvent plus rapides que les boucles for, car ils sont optimisés en interne pour gérer des opérations vectorisées.

  • Moins d’Erreurs : En réduisant la quantité de code à écrire, il y a moins de risque de faire des erreurs, comme mal indexer un vecteur ou un dataframe..


En résumé :

  • lapply est idéal lorsque tu veux appliquer une fonction à chaque élément d’une liste ou d’une collection et obtenir une liste en retour.

  • sapply fait la même chose mais essaie de simplifier le résultat en un vecteur ou une matrice si possible.

  • Ces fonctions sont des alternatives élégantes aux boucles for, rendant le code plus concis, lisible, et souvent plus performant.