Vectorisation
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 :
# Somme des logarithmes des 10 premiers entiers
somme_log <- 0
for (i in seq_len(10)) {
somme_log <- somme_log + log(i)
}
somme_log
[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 :
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 :
.margins = 1
: par lignes ;.margins = 2
: par colonnes ;.margins = c(1,2)
: par cellule ;.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.
tableau <- array(1:24, dim = c(3, 4, 2), dimnames = list(ligne = letters[1:3],
colonne = LETTERS[1:4], annee = 2001:2002))
tableau
, , 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
# La moyenne des valeurs pour chaque colonne en ne
# simplifiant pas le résultat
aaply(tableau, 2, mean, .drop = FALSE)
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
# Avec une fonction définie par l'utilisateur
standardise <- function(x) (x - min(x))/(max(x) - min(x))
# Standardiser les valeurs par colonne
aaply(tableau, 2, standardise)
, , 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)
.
chomage <- data.frame(region = rep(c(rep("Bretagne", 4), rep("Corse",
2)), 2), departement = rep(c("Cotes-d'Armor", "Finistere",
"Ille-et-Vilaine", "Morbihan", "Corse-du-Sud", "Haute-Corse"),
2), annee = rep(c(2011, 2010), each = 6), ouvriers = c(8738,
12701, 11390, 10228, 975, 1297, 8113, 12258, 10897, 9617,
936, 1220), ingenieurs = c(1420, 2530, 3986, 2025, 259, 254,
1334, 2401, 3776, 1979, 253, 241))
chomage
# Total chomeurs en Bretagne et en Corse pour les années 2010
# et 2011 sous forme de data.frame
ddply(chomage, .(annee), summarise, total_chomeurs = sum(ouvriers +
ingenieurs))
# sous forme de array
daply(chomage, .(annee), summarise, total_chomeurs = sum(ouvriers +
ingenieurs))
$`2010`
[1] 53025
$`2011`
[1] 55803
# sous forme de list
dlply(chomage, .(annee), summarise, total_chomeurs = sum(ouvriers +
ingenieurs))
$`2010`
total_chomeurs
1 53025
$`2011`
total_chomeurs
1 55803
attr(,"split_type")
[1] "data.frame"
attr(,"split_labels")
annee
1 2010
2 2011
# Total chomeurs pour les années 2010 et 2011, par région du
# data frame
ddply(chomage, .(annee, region), summarise, total_chomeurs = sum(ouvriers +
ingenieurs))
# En utilisant une fonction définie par l'utilisateur
ddply(chomage, .(annee, region), function(x) {
moy_ouvriers <- mean(x$ouvriers)
moy_ingenieurs <- mean(x$ingenieurs)
data.frame(moy_ouvriers = moy_ouvriers, moy_ingenieurs = moy_ingenieurs)
})
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.
set.seed(1)
liste <- list(normale = rnorm(10), logiques = c(TRUE, TRUE, FALSE),
x = c(0, NA, 3))
# Obtenir la longueur de chaque élément de la liste
laply(liste, length)
[1] 10 3 3
$normale
[1] 10
$logiques
[1] 3
$x
[1] 3
normale logiques x
0.1322028 0.6666667 1.5000000
# Appliquer une fonction définie par l'utilisateur
llply(liste, function(x, y) x/mean(x, na.rm = TRUE) + y, y = 2)
$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
.
liste <- list(normale = rnorm(10), logiques = c(TRUE, TRUE, FALSE),
x = c(0, NA, 3))
# Obtenir la liste des longueurs de chaque élément
lapply(liste, length)
$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
# Si la fonction doit posséder plusieurs paramètres
lapply(liste, function(x, y) x/mean(x, na.rm = TRUE) + y, y = 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 :
où 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
# Application de la fonction quantile() à chaque élément pour
# obtenir la médiane et les quartiles Avec lapply()
lapply(x, quantile)
$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
# Pour retourner le résultat sous forme de liste
tapply(iris$Sepal.Length, iris$Species, mean, simplify = FALSE)
$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
.
f <- function(x = 1:3, y) c(x, y)
# On 'vectorise' la fonction f
vf <- Vectorize(f, SIMPLIFY = FALSE)
f(1:3, 1:3)
[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