La version originale de ce chapitre a été écrite par Ewen Gallic dans le cadre de son support de cours d’Ewen Gallic intitulé Logiciel R et programmation, chapitre 4 Boucles et calculs vectoriels.

Les boucles sont des opérations lentes en R. Il est cependant possible, dans de nombreux cas, d’éviter de les employer, en ayant recours à la vectorisation : au lieu d’appliquer une fonction à un scalaire, on l’applique à un vecteur. En fait, nous avons déjà eu recours à maintes reprises aux calculs vectoriels. En effet, lorsque nous avons procédé à des additions, des multiplications, etc. sur des vecteurs, nous avons effectué des calculs vectoriels.

Empruntons un exemple à Burns (2011) : dans des langages comme le C, pour effectuer la somme des logarithmes naturels des n premiers entiers, voici une manière de faire :

[1] 15.10441

Il est possible d’obtenir le même résultat, à la fois d’une manière plus élégante, mais surtout plus efficace en vectorisant le calcul :

[1] 15.10441

Derrière ce code, la fonction log applique la fonction logarithme sur toutes les valeurs du vecteur donné en paramètre. La fonction sum, quant à elle, se charge d’additionner tous les éléments du vecteur qui lui est donné en paramètre. Ces deux fonctions utilisent la vectorisation, mais d’une manière différente : la fonction log applique une opération à chaque élément d’un vecteur, tandis que la fonction sum produit un résultat basé sur l’ensemble du vecteur. L’avantage d’utiliser des fonctions vectorielles plutôt que d’écrire une boucle pour effectuer le calcul, est que ces premières font appel à des fonctions rédigées en C ou FORTRAN, qui utilisent aussi des boucles, mais comme ce sont des langages compilés et non pas interprétés, les itérations sont réalisées dans un temps réduit.

Il existe des fonctions, rédigées en C qui effectuent des boucles for. On leur donne souvent le nom de “fonctions de la famille apply”. Il ne s’agit pas de la vectorisation, mais ces fonctions sont souvent mentionnées dès que l’on parle de ce sujet. Ce sont des fonctionnelles qui prennent une fonction en input et retournent un vecteur en output (Wickham, 2014). Ces fonctions sont très utilisées, mais elles souffrent d’un manque d’uniformité. En effet, elles ont été rédigées par des personnes différentes, ayant chacune leur convention. L’extension plyr remédie à ce problème, et ajoute par la même occasion des fonctions supplémentaires, pour couvrir plus de cas que les “fonctions de la famille apply”.

Nous allons donc présenter dans un premier temps les fonctions du package plyr. Les fonctions du même type du package base seront tout de même présentées par la suite.

Les fonctions de l’extension plyr

Les fonctions que nous allons aborder dans cette section possèdent des noms faciles à se remémorer : la première lettre correspond au format d’entrée des données, la seconde au format de sortie souhaité, et la fin du nom se termine par le suffixe ply. Ainsi, la fonction llpply prend en entrée une liste, effectue une opération sur les éléments, et retourne une liste (Anderson, 2012).

Les différentes fonctions que nous allons passer en revue sont consignées dans le tableau ci-après, où les lignes correspondent aux formats d’entrée, et les lignes aux formats de sortie. Pour y avoir accès, il faut charger le package :

Format de sortie
array data.frame list
Format d’entée array aaply adply alply
data.frame daply ddply dlply
list laply ldply llply

Il est possible d’avoir plusieurs paramètres en input au lieu d’un seul objet. Les fonctions mlply, mdply et maply. Si à la place du m, la première lettre est un r, il s’agit alors de fonction de réplications. Enfin, si la seconde lettre est un trait de soulignement (_), alors le résultat retourné n’est pas affiché (le code utilise la fonction invisible.

Tous les paramètres de ces fonctions commencent par un point (.), afin d’éviter des incompatibilités avec la fonction à appliquer.

Array en input : a*ply

Les fonctions aaply, adply et alply appliquent une fonction à chaque portion d’un array et ensuitent joignent le résultat sous forme d’un array, d’un data.frame ou d’une list respectivement.

Un array peut être vu comme un vecteur à plusieurs dimensions. Comme pour un vecteur, toutes les valeurs doivent être du même type. Un vecteur n’est finalement qu’un array à une seule dimension. De même, un array à deux dimensions correspond à ce qu’on appelle usuelement une matrice.

Le paramètre .margins détermine la manière de découper le tableau. Il y en a quatre pour un tableau en deux dimensions :

  1. .margins = 1 : par lignes ;
  2. .margins = 2 : par colonnes ;
  3. .margins = c(1,2) : par cellule ;
  4. .margins = c() : ne pas faire de découpement.

Pour un tableau en trois dimensions, il y a trois découpages possibles en deux dimensions, trois en une dimension et une en zéro dimension (voir (Wickham, 2011)) au besoin.

, , annee = 2001

     colonne
ligne A B C  D
    a 1 4 7 10
    b 2 5 8 11
    c 3 6 9 12

, , annee = 2002

     colonne
ligne  A  B  C  D
    a 13 16 19 22
    b 14 17 20 23
    c 15 18 21 24
   a    b    c 
11.5 12.5 13.5 
$`1`
[1] 11.5

$`2`
[1] 12.5

$`3`
[1] 13.5

attr(,"split_type")
[1] "array"
attr(,"split_labels")
  ligne
1     a
2     b
3     c
       
colonne  1
      A  8
      B 11
      C 14
      D 17
     colonne
ligne A  B  C  D
    a 7 10 13 16
    b 8 11 14 17
    c 9 12 15 18
, , annee = 2001

       ligne
colonne a          b         c
      A 0 0.07142857 0.1428571
      B 0 0.07142857 0.1428571
      C 0 0.07142857 0.1428571
      D 0 0.07142857 0.1428571

, , annee = 2002

       ligne
colonne         a         b c
      A 0.8571429 0.9285714 1
      B 0.8571429 0.9285714 1
      C 0.8571429 0.9285714 1
      D 0.8571429 0.9285714 1

Data.frame en input : d*ply

Dans le cas de l’analyse d’enquêtes, on utilise principalement des tableaux de données ou data.frame. Aussi, la connaissance des fonction daply, ddply et dlply peut être utile. En effet, elles sont très utiles pour appliquer des fonctions à des groupes basés sur des combinaisons de variables, même si dans la majorité des cas il est maintenant plus facile de passer par les extensions dplyr ou data.table avec les opérations groupées (voir la section sur groub_by de dplyr ou encore celle sur le paramètre by de data.table.

Avec les fonctions d*ply, il est nécessaire d’indiquer quelles variables, ou fonctions de variables on souhaite utiliser, en l’indiquant au paramètre .variables. Elles peuvent être contenue dans le data frame fourni au paramètre .data, ou bien provenir de l’environnement global. R cherchera dans un premier temps si la variable est contenue dans le data.frame et, s’il ne trouve pas, ira chercher dans l’environnement global.

Pour indiquer que l’on désire faire le regroupement selon une variable – mettons variable_1 – il faudra fournir l’expression .(variable_1) au paramètre .variables. Si on souhaite effectuer les regroupement selon les interactions de plusieurs variables – variable_1, variable_2 et variable_3, il faut alors utiliser l’expression suivante : .(variable_1, variable_2, variable_3).

$`2010`
[1] 53025

$`2011`
[1] 55803
$`2010`
  total_chomeurs
1          53025

$`2011`
  total_chomeurs
1          55803

attr(,"split_type")
[1] "data.frame"
attr(,"split_labels")
  annee
1  2010
2  2011

List en input : l*ply

Les fonctions du type l*ply prennent une liste en entrée. Il n’y a donc pas de paramétrage à effectuer pour choisir un découpage, il est déjà fait.

[1] 10  3  3
$normale
[1] 10

$logiques
[1] 3

$x
[1] 3
  normale  logiques         x 
0.1322028 0.6666667 1.5000000 
$normale
 [1] -2.7385827  3.3891033 -4.3208096 14.0669232  4.4924421
 [6] -4.2061356  5.6869803  7.5847895  6.3552892 -0.3099997

$logiques
[1] 3.5 3.5 2.0

$x
[1]  2 NA  4

Calcul parallèle

En utilisant plusieurs processeurs, on peut effectuer des calculs parallèles, ce qui accélère les calculs dans certains cas. En effet, quand il est possible de fractionner les opérations à effectuer en morceaux, on peut en réaliser une partie sur un processeur, une autre sur un second processeur, et ainsi de suite. Les résultats obtenus sont ensuite rassemblés avant d’être retournés. Le package doMC (ou doSMP sur Windows) peut être chargé pour utiliser la fonction de calcul parallèle proposé par les fonctions **ply. Il suffit de préciser le nombre de coeurs souhaité en faisant appel à la fonction registerDoMC, et de fixer la valeur TRUE au paramètre .parallel de la fonction **ply.

Les fonctions de la famille apply du package base

Le tableau ci-après recense les fonctions principales de la famille apply du package base.

Fonction Input Output
apply Matrice ou tableau Vecteur ou tableau ou liste
lapply Liste ou vecteur Liste
sapply Liste ou vecteur Vecteur ou matrice ou liste
vapply Liste ou vecteur Vecteur ou matrice ou liste
tapply Tableau et facteurs Tableau ou liste
mapply Listes et/ou vecteurs Vecteur ou matrice ou liste

La fonction lapply

La fonction lapply applique à chaque élément du premier paramètre qui lui est donné une fonction indiquée en second paramètre et retourne le résultat sous forme de liste. La syntaxe est la suivante :

avec X la liste ou le vecteur donné en paramètre sur lequel on désire appliquer la fonction FUN. La paramètre ... permet de fournir des paramètres à une fonction imbriquée, en l’occurance à celle que l’on souhaite appliquer à tous les éléments de X.

$normale
[1] 10

$logiques
[1] 3

$x
[1] 3
$normale
[1] 0.248845

$logiques
[1] 0.6666667

$x
[1] 1.5

On peut créer une fonction à l’intérieur de l’appel à lapply. Le premier paramètre est nécessairement un élément du vecteur auquel on souhaite appliquer la fonction.

$normale
 [1]  6.07519277  1.56661087 -2.49649643 -8.89991820
 [5]  4.52060941 -0.18056868 -0.06506164  3.79286833
 [9]  3.30013177  2.38663180

$logiques
[1] 1.5 1.5 0.0

$x
[1]  0 NA  2
$normale
 [1]  8.0751928  3.5666109 -0.4964964 -6.8999182  6.5206094
 [6]  1.8194313  1.9349384  5.7928683  5.3001318  4.3866318

$logiques
[1] 3.5 3.5 2.0

$x
[1]  2 NA  4

On peut appliquer la lapply sur des tableaux de données, dans la mesure où ces derniers sont des listes. Cela s’avère pratique pour réaliser des opérations pour chaque colonne d’un tableau de données. Afin de prendre moins de place dans l’affichage, l’exemple suivant utilise la fonction unlist pour aplatir la liste obtenue.

    speed      dist 
"numeric" "numeric" 
speed  dist 
15.40 42.98 

Attention, ce qui suit relève plus d’un tour de passe-passe que de la programmation élégante.

Si la fonction que l’on souhaite appliquer aux éléments de notre vecteur retourne un vecteur ligne de même longueur pour chaque élément, la fonction do.call peut devenir un outil très pratique pour créer une data frame. Voyons-le à travers un exemple.

[[1]]
     valeur lettre
[1,] "1"    "A"   

[[2]]
     valeur lettre
[1,] "2"    "B"   

[[3]]
     valeur lettre
[1,] "3"    "C"   

L’appel de do.call("rbind", x) revient à faire rbind(x[[1]], x[[2]], ..., x[[n]]) avec x une liste de taille n.

La fonction sapply

La fonction sapply applique une fonction aux éléments d’un vecteur ou d’une liste et peut retourner un vecteur, une liste ou une matrice. Elle possède la syntaxe suivante :

X est le vecteur ou la liste auquel on souhaite appliquer la fonction FUN. Lorsque simplify vaut FALSE, le résultat est retourné sous forme de liste, exactement comme lapply (la fonction sapply s’appuie sur la fonction lapply). Lorsque simplify vaut TRUE (par défaut), le résultat est retourné dans une forme simplifiée, si cela est possible. Si tous les éléments retournés par la fonction FUN sont des scalaires, alors sapply retourne un vecteur ; sinon, si les éléments retournés ont la même taille, sapply retourne une matrice avec une colonne pour chaque élément de X auquel la fonction FUN est appliquée. Le paramètre USE.NAMES, quand il vaut TRUE (par défaut), et si X est de type character, utilise X comme nom pour le résultat, à moins que le résultat possède déjà des noms.

$a
 [1]  1  2  3  4  5  6  7  8  9 10

$beta
[1]  0.04978707  0.13533528  0.36787944  1.00000000
[5]  2.71828183  7.38905610 20.08553692

$logic
[1]  TRUE FALSE FALSE  TRUE
$a
   0%   25%   50%   75%  100% 
 1.00  3.25  5.50  7.75 10.00 

$beta
         0%         25%         50%         75%        100% 
 0.04978707  0.25160736  1.00000000  5.05366896 20.08553692 

$logic
  0%  25%  50%  75% 100% 
 0.0  0.0  0.5  1.0  1.0 
         a        beta logic
0%    1.00  0.04978707   0.0
25%   3.25  0.25160736   0.0
50%   5.50  1.00000000   0.5
75%   7.75  5.05366896   1.0
100% 10.00 20.08553692   1.0
A B C 
1 1 1 
[1] 1 1 1

La fonction vapply

La fonction vapply est similaire à sapply, mais elle possède un type de valeurs spécifié, ce qui peut rendre l’utilisation plus sûre (et parfois plus rapide). Lorsqu’on lui fournit un data.frame, vapply retourne le même résultat que sapply. Cependant, quand on lui fournit une liste vide, vapply retourne un vecteur logique de longueur nulle (ce qui est plus sensé que la liste vide que returne sapply).

avec X, FUN, ... et USE.NAMES les mêmes paramètres que pour sapply. Le paramètre FUN.VALUE doit être un vecteur, un masque pour la valeur retournée par la fonction de FUN.

speed  dist 
 TRUE  TRUE 
speed  dist 
 TRUE  TRUE 
list()
logical(0)

La fonction apply

La fonction apply possède la syntaxe suivante :

avec X une matrice ou un tableau, MARGIN indiquant si on souhaite appliquer la fonction FUN aux lignes (MARGIN = 1) ou aux colonnes (MARGIN = 2), et ... des paramètres supplémentaires éventuels à passer à la fonction FUN.

     [,1] [,2] [,3]
[1,]    1    4    7
[2,]    2    5    8
[3,]    3    6    9
[1] 12 15 18
[1]  6 15 24
[1] 0.2666667 0.3333333 0.4000000

La fonction tapply

La fonction tapply s’applique à chaque cellule d’un tableau, sur des regroupements définis par les variables catégorielles fournies. La syntaxe est la suivante :

avec X le tableau de données, INDEX une liste d’un ou plusieurs facteurs, chacun de même taille que X. Le paramètre FUN renseigne la fonction que l’on souhaite appliquer. Si simplify vaut FALSE, le résultat est un tableau de mode list. Sinon (par défaut), le résultat est un tableau de scalaires.

    setosa versicolor  virginica 
     5.006      5.936      6.588 
$setosa
[1] 5.006

$versicolor
[1] 5.936

$virginica
[1] 6.588

La fonction mapply

La fonction mapply applique une fonction à plusieurs listes ou vecteurs. La syntaxe est la suivante :

avec FUN la fonction à appliquer aux vecteurs ou listes fournies (grâce à ...), MoreArgs une liste de paramètres supplémentaires à fournir à la fonction à appliquer. Les paramètres SIMPLIFY et USE.NAMES ont le même usage que pour la fonction sapply.

$a
[1] 1 2 3 4 5

$b
[1]  6  7  8  9 10
$c
[1] 11 12 13 14 15

$d
[1] 16 17 18 19 20
[1] 34 38 42 46 50
$a
[1] 1 2 3 4 5

$b
 [1]  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20
 [1] 34 38 42 46 50 39 43 47 51 55 44 48 52 56 60

La fonction Vectorize

La fonction Vectorize permet de convertir une fonction scalaire en une fonction vectorielle. Attention, cela ne permet pas d’améliorer la rapidité d’exécution du code. Par contre, son utilisation assez intuitive permet de gagner du temps. Il s’agit donc de faire l’arbitrage entre le temps passé à trouver un moyen élégant et efficace pour effectuer une opération en passant par de réels calculs vectoriels et le gain d’exécution que ce calcul vectoriel apporte vis-à-vis d’une boucle. La syntaxe de la Vectorize est la suivante :

avec FUN une fonction à appliquer, vectorize.args un vecteur de paramètres (de type caractère) qui devraient être vectorisés (par défaut, tous les paramètre de FUN). Les paramètres SIMPLIFY et USE.NAMES on le même emploi que dans la fonction sapply.

[1] 1 2 3 1 2 3
[[1]]
[1] 1 1

[[2]]
[1] 2 2

[[3]]
[1] 3 3
[[1]]
[1] 1 2 3 1

[[2]]
[1] 1 2 3 2

[[3]]
[1] 1 2 3 3