Cours
Remarque sur la famille Apply de R
La famille Apply de R regroupe des fonctions qui permettent d’appliquer une fonction aux éléments d’un vecteur, d’une liste ou d’une matrice. Toutefois, il s’agit d’une fonctionnalité historique qui n’est pas recommandée pour du nouveau code. Pour toute logique de boucles en R, privilégiez le package purrr. purrr propose une syntaxe cohérente pour travailler avec des fonctions à entrées et sorties multiples, ce qui facilite l’écriture et la lecture du code. De plus, purrr offre un ensemble de fonctions optimisées pour les environnements de calcul modernes, le rendant plus rapide et plus efficace que la famille Apply. En résumé, même si la famille Apply peut encore fonctionner pour certains cas, il est conseillé d’utiliser purrr pour tous vos nouveaux projets R.
Les fonctions Apply comme alternatives aux boucles
Ce billet vous montre comment utiliser la fonction R apply(), ses variantes comme mapply() ainsi que quelques fonctions « cousines », appliquées à différentes structures de données. Évidemment, nous ne pourrons pas couvrir toutes les variantes ; quand c’est pertinent, nous présenterons leur usage combiné à travers quelques exemples un peu plus étoffés.
Vous pouvez aussi consulter ce cours d’introduction à R pour mieux comprendre les listes, vecteurs, tableaux et data frames, même si vous n’avez pas besoin de l’avoir terminé pour suivre ce post.
La famille apply()
La famille apply() fait partie de R base et réunit des fonctions permettant de manipuler de façon répétitive des tranches de données de matrices, tableaux, listes et data frames. Elles permettent de parcourir les données de plusieurs manières et d’éviter l’usage explicite de boucles. Elles agissent sur une liste, une matrice ou un tableau en entrée et y appliquent une fonction donnée, avec un ou plusieurs arguments optionnels.
La fonction appelée peut être :
- une fonction d’agrégation, par exemple la moyenne ou la somme (qui renvoient un nombre ou un scalaire) ;
- d’autres fonctions de transformation ou de création de sous-ensembles ; et
- d’autres fonctions vectorisées, qui produisent des structures plus complexes comme des listes, vecteurs, matrices et tableaux.
Les fonctions apply() servent de base à des combinaisons plus complexes et permettent d’effectuer des opérations en très peu de lignes. Plus précisément, la famille comprend les fonctions apply(), lapply(), sapply(), vapply(), mapply(), rapply() et tapply().
Mais comment et quand les utiliser ?
Tout dépend de la structure des données à traiter et du format de sortie souhaité.
Comment utiliser apply() en R
Commençons par le « patriarche » de la famille, apply(), qui opère sur les tableaux (arrays). Pour simplifier, ce tutoriel se limite aux tableaux 2D, autrement dit les matrices.
Le manuel de R base indique l’appel suivant : apply(X, MARGIN, FUN, ...)
où :
Xest un tableau ou une matrice si la dimension du tableau est 2 ;MARGINdéfinit la manière d’appliquer la fonction : avecMARGIN=1, on parcourt les lignes, et avecMARGIN=2, les colonnes. Notez queMARGIN=c(1,2)s’applique aux lignes et aux colonnes ; etFUNest la fonction à appliquer aux données. Il peut s’agir de n’importe quelle fonction R, y compris une fonction définie par l’utilisateur (UDF).
Les débutants ont parfois du mal à visualiser ce qu’il se passe : un schéma et un peu de code vous aideront à y voir clair.
Construisons une matrice 5 x 6 et imaginons que vous souhaitiez sommer les valeurs de chaque colonne.
Vous pouvez écrire quelque chose comme ceci :
# Construct a 5 x 6 matrix
my_matrix <- matrix(1:30, nrow = 5, ncol = 6)
# Calculate the sum of each column
col_sums <- apply(my_matrix, 2, sum)
# Print the result
print(col_sums)
La fonction matrix() crée une matrice 5 x 6 avec des valeurs de 1 à 30. Nous utilisons ensuite la fonction apply() pour appliquer sum() à chaque colonne de la matrice (2 indique que l’on applique par colonne). Le vecteur résultant des sommes par colonne est stocké dans la variable col_sums et affiché dans la console.
À retenir : en R, une matrice peut être vue comme une collection de vecteurs-lignes lorsqu’on la parcourt de haut en bas (selon la dimension ou marge 1), ou comme une liste de vecteurs-colonnes lorsqu’on la parcourt de gauche à droite (dimension ou marge 2).
Cela signifie que l’instruction que vous venez d’exécuter, illustrée dans la figure 1, se traduit par : « appliquez la fonction ‘sum’ à la matrice X le long de la marge 2 (par colonne), en additionnant les valeurs de chaque colonne ».
Pour alléger la figure, une seule colonne est mise en évidence.
Vous obtenez un vecteur-ligne contenant les sommes des valeurs de chaque colonne.
La sortie ci-dessus, un vecteur-ligne, aurait également été obtenue si vous aviez sommé par lignes. C’est ainsi que R affiche le résultat.
Petit rappel pour la suite : dans la plupart des cas, R peut renvoyer une valeur même si vous ne l’assignez pas explicitement à une variable. R retourne simplement le dernier objet évalué. En pratique, pour vérifier la valeur de retour et enchaîner d’autres opérations, il est préférable d’assigner explicitement les résultats d’une fonction à une variable.
La fonction lapply()
Vous souhaitez appliquer une fonction à chaque élément d’une liste et obtenir une liste en sortie. Quand vous exécutez ?lapply, vous verrez que la syntaxe ressemble à celle de apply().
La différence :
- Elle s’applique aussi à d’autres objets comme les data frames, les listes ou les vecteurs ; et
- La sortie est une liste (d’où le « l » de son nom), qui comporte autant d’éléments que l’objet passé en entrée.
Pour voir cela, créons quelques matrices et extrayons, pour chacune, une colonne donnée.
C’est une opération courante sur des données réelles pour comparer ou agréger des informations provenant de différents data frames.
Notre exemple jouet, illustré en figure 2, peut s’écrire ainsi :

L’opération est montrée dans la partie gauche de la figure 2.
Vous commencez par préciser l’objet d’intérêt, la liste Mylist. Vous utilisez l’opérateur de sélection R standard [ et omettez le premier paramètre (ce qui revient à « tout », d’où les deux virgules).
Ensuite, vous indiquez le second paramètre, 2 : notre marge est la colonne. Vous extrayez donc la deuxième colonne de toutes les matrices de la liste.
Quelques remarques :
- La notation
[est l’opérateur de sélection. Par exemple, pour extraire tous les éléments de la troisième ligne de B :B[3,]; - La notation
[[ ]]traduit que l’on manipule des listes : [[2]] signifie le deuxième élément de la liste. C’est visible dans la sortie de R ; - La sortie est une liste avec autant d’éléments que l’entrée ; et
- Vous auriez aussi pu extraire un seul élément par matrice, ainsi :
lapply(MyList,"[", 1, 2)
Dans la partie droite de la figure 2, vous voyez une extraction alternative : cette fois, vous omettez le premier paramètre et récupérez la première ligne de chaque matrice.
À vous de jouer ! Sélectionnez la deuxième colonne de chaque matrice dans la liste :
La fonction sapply()
La fonction sapply() fonctionne comme lapply(), mais tente de simplifier la sortie vers la structure de données la plus élémentaire possible. En effet, sapply() est un « wrapper » autour de lapply().
Un exemple aide à comprendre : répétons l’extraction d’un élément unique comme précédemment, mais en prenant cette fois, pour chaque matrice, le premier élément de la deuxième ligne (index 2 et 1).
Avec lapply(), on obtiendrait une liste, sauf si vous passez simplify=FALSE à sapply(). Dans ce cas, une liste sera renvoyée. Voyez le fonctionnement ci-dessous :
Inversement, une fonction comme unlist() peut demander à lapply() de vous renvoyer un vecteur !
Pour éviter toute confusion, il est préférable d’utiliser ces fonctions dans leur « format natif » et d’éviter les conversions, sauf nécessité.
La fonction rep()
Une fonction souvent utilisée avec apply() est rep(). Appliquée à un vecteur ou un facteur x, elle réplique ses valeurs un nombre de fois donné.
Reprenons l’un des vecteurs générés ci-dessus avec lapply() dans MyList.
Cette fois, vous sélectionnez uniquement les éléments de la première ligne et de la première colonne de chaque élément de la liste MyList (et vous utilisez sapply() pour obtenir un vecteur) :
Vous voyez que le code ci-dessus réplique les valeurs de Z un nombre de fois défini par c(3,1,2) : trois fois la première, une fois la deuxième et deux fois la troisième.
Pratique, non ?
La fonction mapply()
La fonction mapply() signifie « multivariée ». Elle permet de vectoriser les arguments d’une fonction qui, à l’origine, n’accepte pas des vecteurs en arguments.
En bref, mapply() applique une fonction à plusieurs listes ou plusieurs vecteurs d’arguments.
Voyons un exemple avec mapply() où vous créez une matrice 4 x 4 en appelant rep() de manière répétée :
Vous voyez qu’il existe une manière plus efficace d’assembler les résultats de rep() qu’avec c() : en appelant mapply(), vous vectorisez l’action de la fonction rep().
Fonctions apparentées à apply()
Des fonctions structurées de manière similaire sont parfois utilisées avec les éléments de la famille apply() : ce tutoriel en présente un bref aperçu.
La fonction Sweep()
La fonction sweep() est probablement la plus proche de la famille apply(). On l’utilise lorsque l’on veut reproduire des opérations différentes sur les éléments MARGIN choisis (en se limitant ici au cas matriciel).
Un scénario typique se rencontre en clustering, où l’on doit produire de façon répétée des données centrées et « normalisées ».
Qu’est-ce que cela signifie ?
Supposons que vous ayez plusieurs points dans un groupe de données. Vous commencez par trouver le centre de ces données (« centre de gravité ») et mesurez leur dispersion par rapport à ce centre. Deux quantités de base fournissent cette information : la moyenne et l’écart-type.
Considérez vos points comme des vecteurs-colonnes d’une matrice de données et réutilisons la matrice B créée au début du post, désormais appelée dataPoints.
Vous calculez d’abord les moyennes par colonne et la dispersion (écart-type) à l’aide d’une des fonctions apply(). Puis vous recentrez tous les points par rapport à leur centre. Autrement dit, la moyenne calculée sert à normaliser les données par leur écart-type :
Vous avez produit les points centrés avec un seul appel à sweep(). Cette fonction attend :
- un tableau en entrée, ici une matrice ;
- une MARGE,
2pour indiquer les colonnes ; - une statistique de synthèse (ici
mean) ; et - un opérateur/fonction à appliquer. Ici, l’opérateur arithmétique
« - »pour la soustraction.
Autrement dit : « prenez les éléments des colonnes du jeu de données MyPoints et soustrayez la moyenne dataPoints_means à chacun d’eux ».
Ensuite, vous rappelez sweep() pour diviser toutes les valeurs obtenues par leur propre écart-type. Cette étape est la « normalisation ». Là encore, vous choisissez MARGIN=2, puis vous fournissez le vecteur des écarts-types, dataPoints_sdev, comme opérande. Enfin, vous passez l’opérateur « diviser par » « / ».
La demande à R est la suivante : « prenez les éléments des colonnes du nouvel objet dataPoints_Trans1 et divisez-les (« / ») par leur écart-type dataPoints_sdev ».
Bien sûr, vous auriez pu obtenir le même résultat plus rapidement et plus concisément (comme souvent en R !) et sans multiplier les noms, avec un seul appel imbriqué à sweep() :
Statistiquement, vous venez de créer une matrice de corrélation, et des données standardisées sont à la base de nombreuses méthodes avancées (réduction de dimension via ACP, analyse de signaux, etc.).
La fonction aggregate()
Cette fonction appartient au package stats et s’utilise ainsi : aggregate(x, by, FUN, ..., simplify = TRUE).
En d’autres termes, elle fonctionne de manière similaire à apply() : vous indiquez l’objet, la fonction et si vous souhaitez simplifier, comme avec sapply(). La différence clé est l’usage de l’argument by, qui définit la variable (ou la colonne d’un data frame) selon laquelle effectuer l’agrégation.
La section suivante illustre son fonctionnement.
Exemple avec aggregate()
Considérons un petit jeu de données appelé Mydf, qui contient des ventes d’un produit, et où certaines valeurs de la colonne DepPC (département) se répètent.
Cette variable classe les données par zone géographique, comme une partie d’un code postal (ici, les numéros correspondent aux départements de l’Île-de-France).
Vous souhaitez réaliser quelques statistiques sur les colonnes de ventes. Ce sont DProgr, un numéro progressif dans l’ordre chronologique, et les ventes du produit (Qty), plus une variable logique, Delivered, indiquant si le produit a été livré (T) ou non (F).
Commencez par quelques opérations simples pour vous familiariser avec le jeu de données, au-delà de l’afficher en entier en tapant simplement son nom (ici 120 enregistrements, mais imaginez un vrai fichier avec des milliers de lignes !).
Explorons les données :
Remarque : pour connaître le nombre de lignes et de colonnes du data frame, vous pouvez aussi appeler nrow(Mydf) et ncol(Mydf).
De nombreuses autres explorations sont possibles.
Ici, vous voulez savoir dans quel département le produit se vend le mieux. Regroupez donc les données par département et sommez les ventes Qty pour chaque département DepPC à l’aide de aggregate() :
Ainsi, aggregate() indique à R de sommer toutes les Qty appartenant au même département.
Notez que R assigne la somme à une variable « x » puisque vous n’avez pas indiqué autre chose.
La sortie est lisible telle quelle, mais avec un plus grand nombre de départements, cela peut devenir moins clair. Dans ce cas, vous pouvez recourir à une visualisation : tracez les résultats avec l’un des systèmes graphiques de R, conjointement à aggregate() :
Vous obtenez ainsi les ventes par département.
Vous pouvez poser la même question, mais uniquement pour les produits livrés. Pour cela, commencez par créer un sous-ensemble des données où Delivered vaut vrai (T) en utilisant l’opérateur de sous-ensemble "[".
Remarque : ici, vous assignez le résultat à une nouvelle variable Y, un nouveau data frame qui hérite des mêmes noms de colonnes que le parent Mydf. Cela évite de répéter l’instruction d’agrégation dans l’appel au tracé, pour plus de lisibilité :
Vous pouvez ainsi poser différentes questions aux données de manière vectorisée avec aggregate(), souvent en l’associant à un système de visualisation pratique comme ggplot2. Vous avez l’idée.
Notez qu’il ne faut que quelques lignes de code pour obtenir cela.
La vectorisation comme alternative aux boucles et aux fonctions Apply ?
Vous avez vu plusieurs variantes autour du même thème : « agir de manière répétée sur un jeu de données structuré ». En ce sens, ces fonctions constituent non seulement une alternative aux boucles, mais aussi une manière « vectorisée » de procéder.
« Vectorisée » au sens large ici : nous n’entrerons pas dans le débat pour savoir si – et lesquelles des – fonctions apply() sont réellement vectorisées (voir par exemple la discussion ici).
En pratique, pour choisir quelle fonction apply() utiliser, tenez compte des éléments suivants :
- Le type de données en entrée : l’objet à traiter (vecteur, matrice, tableau, liste, data frame, ou une combinaison) ;
- Ce que vous souhaitez faire : la fonction
FUNà passer ; - Le sous-ensemble de données : lignes, colonnes, ou l’ensemble ?
- Le type de sortie souhaité : vous devrez peut-être enchaîner d’autres opérations (souhaitez-vous un nouvel objet, ou transformer l’objet d’entrée directement ?)
Ce sont des questions générales que vous pouvez également poser pour les fonctions associées, comme aggregate(), by(), sweep(), etc.
Mais il y en a bien d’autres ! Continuez d’explorer. Consultez le tutoriel DataCamp Arrays in R.
Pour aller plus loin, suivez les cours Introduction to R ou Intermediate R de DataCamp.
