4 Boucles et calculs vectoriels
Il existe deux sortes de boucles dans R
. Celles pour lesquelles les itérations continuent tant qu’une condition n’est pas invalidée (while()
), et celles pour lesquelles le nombre d’itérations est défini au moment de lancer la boucle (for()
).
Avant de présenter chacune de ces fonctions, il est nécessaire de préciser que les boucles ne sont pas le point fort de R
. Dès que l’on souhaite appliquer une fonction à chaque élément d’un vecteur, et/ou que le résultat de chaque itération ne dépend pas de l’itération précédente, il est préférable de vectoriser les calculs (voir Section 4.2).
4.1 Les boucles
4.1.1 Les boucles avec while()
Quand on souhaite répéter un calcul tant qu’une condition est satisfaite, on utilise la fonction while()
, avec la syntaxte suivante :
avec condition
un logique, comme vu dans la Section @ref(manip_operateurs_logiques), et instruction
du code, qui peut être entouré d’accolades si on souhaite évaluer plusieurs instructions.
## [1] FALSE
## [1] 1.234568
4.1.2 Les boucles avec for()
Quand on connaît le nombre d’itérations à l’avance, on peut utiliser la fonction for()
. La syntaxe est la suivante :
avec variable
le nom d’une variable locale à la fonction for()
, vector
un vecteur à n
éléments définissant les valeurs que prendra variable
pour chacun des n
tours, et instruction
le code à exécuter à chaque itération.
## [1] "Sonia"
## [1] "Anne-Hélène"
## [1] "Julien-Yacine"
On peut utiliser la fonction for()
pour remplir les éléments d’une liste, ou d’un vecteur. à chaque itération, R
doit trouver le vecteur de destination en mémoire, créer un nouveau vecteur qui permettra de contenir plus de données, copier données depuis l’ancien vecteur pour les insérer dans le nouveau, et enfin supprimer l’ancien vecteur (Ross 2014). C’est une opération coûteuse en temps. Un moyen de rendre cette allocation plus efficace est de créer a priori le vecteur ou la liste en le remplissant avec des données manquantes. Ainsi, R
n’aura pas besoin de ré-allouer la mémoire à chaque itération.
## [1] 1 2 3
## [1] 1 2 3
4.1.3 Les conditions
On peut soumettre l’exécution de codes en R
à conditions que certaines conditions soient honorées.
4.1.3.1 Les instructions if
… else
Les instructions if
et else
fournissent un moyen d’exécuter du code si une condition est respectée ou non. La syntaxe prend deux formes :
# Première forme (pas de code si condition == FALSE)
if (condition) instruction
# Seconde forme
if (condition) instruction si vrai else instruction si faux
avec condition
un logique et instruction
du code à évaluer si la condition est satisfaite. à nouveau, on peut avoir recours aux accolades pour créer des regroupements.
## [1] "Hello"
x <- 3
if(x == 2) print("Hello")
# Avec des instructions dans le cas contraire
if(x == 2) print("Hello") else print("x est différent de 2")
## [1] "x est différent de 2"
if(x == 2){
print("Hello")
} else {# x != 2
x <- x-1
print(paste0("La nouvelle valeur de x : ", x))
}
## [1] "La nouvelle valeur de x : 2"
if
et else
, il est nécessaire d’écrire le mot else
sur la même ligne que la parenthèse fermante du groupe d’instructions à réaliser dans le de la condition du if
vérifiée.
4.1.3.2 La fonction switch()
Avec la fonction switch()
, on peut indiquer à R
d’exécuter un code en fonction du résultat obtenu lors d’un test. La syntaxe est la suivante :
avec valeur_test
un nombre ou une chaîne de caractères. Si valeur_test
vaut cas_1
, alors uniquement instruction_cas_1
sera évaluée, si valeur_test
vaut cas_2
, alors ce sera instruction_cas_2
qui le sera, et ainsi de suite. On peut rajouter une valeur par défaut en utilisant la syntaxte suivante :
switch(valeur_test,
cas_1 = {
instruction_cas_1
},
cas_2 = {
instruction_cas_2
},
{
instruction_defaut
}
)
Voici un exemple d’utilisation, issu de la page d’aide de la fonction.
centre <- function(x, type) {
switch(type,
mean = mean(x),
median = median(x))
}
x <- rcauchy(10)
centre(x, "mean")
## [1] 0.792937
## [1] 0.2491921
4.1.4 L’instruction repeat
… break
L’instruction repeat
, … break
permet de répéter une expression. Il est nécessaire d’ajouter un test d’arrêt, à l’aide de l’instruction break
.
## [1] 3
4.1.5 L’instruction next
… break
L’instruction next
… break
autorise de passer immédiatement à l’itération suivante d’une boucle for
, while
ou repeat
resul <- rep(NA, 10)
for(i in 1:10) {
if(i == 5) next
resul[i] <- i
}
# Le 5e élément de resul est resté non-disponible
resul
## [1] 1 2 3 4 NA 6 7 8 9 10
4.1.6 Barre de progression
Lorsque l’exécution d’une boucle prend du temps, il peut être intéressant d’avoir une idée de l’état d’avancement des itérations. Pour cela, il est bien sûr possible d’afficher une valeur dans la console à chaque tour, chaque 10 tours, etc.
La fonction txtProgressBar()
du package {utils
} permet un affichage d’une barre de progression dans la console. Il suffit de lui fournir une valeur minimale et maximale, et de la mettre à jour à chaque itération. l’argument style
autorise de surcroît à choisir un “style” pour la barre. Le style numéro 3 affiche un pourcentage de progression, et est utile lorsque d’autres résultats sont affichés dans la console lors de l’exécution de la boucle, dans la mesure où la barre est de nouveau affichée au complet dans la console si nécessaire.
Dans l’exemple qui suit, à chacun des dix tours, une pause \(0.1\) seconde est effectuée, puis la barre de progression est mise à jour.
nb_tours <- 10
p_b <- txtProgressBar(min = 1, max = nb_tours, style = 3)
for(i in 1:nb_tours){
Sys.sleep(0.1)
setTxtProgressBar(p_b, i)
}
Si l’exécution est vraiment longue, et qu’on est impatient de connaître les résultats, il existe de plus une fonction amusante dans le package beepr
, qui porte le nom de beep()
. Plusieurs sons peuvent être utilisés (voir la page d’aide de la fonction).
4.2 La vectorisation
Comme indiqué plus haut, 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
Ou bien, en utilisant l’opérateur Pipe, afin d’éviter la lecture pénible de cette composée de fonctions :
## [1] 15.10441
Derrière ce code, la fonction log()
applique la fonction logarithme sur toutes les valeurs du vecteur donné en argument. La fonction sum()
, quant à elle, se charge d’additionner tous les éléments du vecteur qui lui est donné en argument. 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 la totalité 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. Le package {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’’. Plus récemment, le package {purrr
} a également introduit des fonctions de ce type qui seront présentées ici.
Nous allons donc nous attarder dans un premier temps les fonctions du package {purr
}. Puis, afin d’en conserver une trace, sur les fonctions du package {purrr
} ainsi que celles du package {base
}.
4.3 Avec {purrr
}
Le package {purrr
} propose de nombreuses fonctions permettant de manipuler les listes et les tableaux de données. Dans cette partie, nous allons nous concentrer sur les fonctions de ce package qui permettent d’appliquer des opérations sur chaque élément d’une liste ou sur chaque colonne d’un tableau de données. Le nom de ces fonctions commence par le préfixe map
. Si la structure du résultat voulu est différente d’une liste, le nom de la fonction à utiliser se poursuit par un suffixe désignant la structure désirée.
Fonction | Résultat |
---|---|
map() |
Liste |
map_chr() |
Vecteur de caractères |
map_dbl() |
Vecteur de doubles |
map_int() |
Vecteur d’entiers |
map_lgl() |
Vecteur de booléens |
map_dfr() |
data frame créé en concaténant par les lignes (rows ) à chaque itération |
map_dfc() |
data frame créé en concaténant par les colonnes (columns ) à chaque itération |
Ces fonctions prennent les arguments suivants :
.x
: une liste ou un vecteur ;.f
: une fonction, une formule (qui sera transformée automatiquement en fonction) ou un vecteur / une liste ;...
: des arguments additionnels passés à la fonction appliquée à chaque élément.
4.3.1 Listes ou vecteurs en input
Lorsque l’on donne une liste ou un vecteur à l’argument .x
des fonctions de type map
, la fonction (ou la formule) donnée en argument .f
est appliquée à chaque élément de la liste ou du vecteur d’entrée. La structure du résultat dépend de la fonction map
utilisée, qui est précisée à l’aide du suffixe du nom de la fonction.
Prenons un exemple. Admettons que l’on dispose d’une liste de longueur 100, dont chaque élément contient 50 valeurs numériques. On peut faire face à ce type de données lorsqu’on souhaite réaliser des simulations par ré-échantillonnage, par exemple. Dans cet exemple, nous aurions au préalable réalisé 100 rééchantillonnage différents. Admettons que l’on souhaite connaître la moyenne de chaque ré-échantillon.
Dans un premier temps, générons une liste contenant 50 éléments dans un vecteur contenant 100 éléments. Les 50 valeurs seront tirées ici selon une loi Normale dont les arguments sont aléatoirement tirées.
set.seed(263)
simulations <- vector(mode = "list", length = 100)
for(i in 1:length(simulations)){
simulations[[i]] <- rnorm(n = 50,
mean = sample(-10:10, 1),
sd = runif(1, min=1, max = 5))
}
length(simulations)
## [1] 100
## [1] -10.233237 -8.904499 -10.737834 -7.454745 -9.909407 -8.800618
Nous souhaitons donc calculer la moyenne de chacun des 100 éléments de cette liste. Avec une boucle for
, on pourrait faire comme suit :
mean_samples <- vector(mode="numeric", length = 100)
for(i in 1:length(simulations))
mean_samples[[i]] <- mean(simulations[[i]])
head(mean_samples)
## [1] -10.220707 9.918496 -8.248388 -6.879007 -7.120338 -7.047220
De manière équivalente, et avec un temps d’exécution comparable, on peut utiliser une des fonctions du package {purrr
}. Ici, nous voulons appliquer à une liste en entrée, une fonction (mean()
) sur chacun des éléments, puis obtenir le résultat final sous la forme d’un vecteur de numériques. Nous pouvons de fait utiliser la fonction map_dbl()
comme suit :
## [1] -10.220707 9.918496 -8.248388 -6.879007 -7.120338 -7.047220
Cette méthode a l’avantage de produire un code plus élégant et plus rapide à lire.
On peut également faire appel à l’opérateur Pipe pour rendre la lecture encore plus aisée :
Admettons maintenant que nous voulons calculer le premier quartile empirique de chaque élément de simulations
. Pour ce faire, il faut appliquer la fonction quantile()
, en précisant à l’aide de l’argument probs
que nous voulons le quantile d’ordre 0.25. Si on désire utiliser la fonction map_dbl()
, il faut ajouter un argument à cette dernière pour que celui-ci soit transmis à la fonction quantile()
(il s’agit de fait d’appeler l’argument ...
de la fonction map_dbl()
).
## [1] -11.110399 8.129051 -11.295354 -8.150094 -9.325573 -8.541390
Si à présent, nous souhaitons appliquer plusieurs fonctions à chaque élément de notre liste et récupérer le résultat sous la forme d’un tableau de données, on peut utiliser la fonction map_df()
. Admettons que l’on souhaite obtenir, pour chaque élément, la moyenne des valeurs, le premier et le troisième quartiles. On peut procéder comme suit :
simulations %>%
map_df(function(x){
tibble(mean = mean(x),
q1 = quantile(x, probs = .25),
q3 = quantile(x, probs = .75))
})
## # A tibble: 100 × 3
## mean q1 q3
## <dbl> <dbl> <dbl>
## 1 -10.2 -11.1 -9.37
## 2 9.92 8.13 12.0
## 3 -8.25 -11.3 -5.24
## 4 -6.88 -8.15 -5.33
## 5 -7.12 -9.33 -5.17
## 6 -7.05 -8.54 -5.38
## 7 4.93 3.44 6.22
## 8 1.74 0.946 2.52
## 9 -4.57 -6.14 -2.94
## 10 9.12 5.77 12.1
## # … with 90 more rows
Dans l’exemple précédent, à chaque itération, les fonctions mean()
et quantile()
ont été évaluées sur l’élément courant que nous avons décidé d’appeler x
. Le résultat de l’évaluation de ces fonctions a été, à chaque itération, consigné dans un tableau, à l’aide de la fonction tibble()
.
Afin d’éviter de devoir écrire function(x)
, il est possible d’utiliser un raccourci : ~
. L’élément courant auquel on applique une fonction est alors désigné par le symbole .
:
simulations %>%
map_df(~tibble(
mean = mean(.),
q1 = quantile(., probs = .25),
q3 = quantile(., probs = .75)
))
## # A tibble: 100 × 3
## mean q1 q3
## <dbl> <dbl> <dbl>
## 1 -10.2 -11.1 -9.37
## 2 9.92 8.13 12.0
## 3 -8.25 -11.3 -5.24
## 4 -6.88 -8.15 -5.33
## 5 -7.12 -9.33 -5.17
## 6 -7.05 -8.54 -5.38
## 7 4.93 3.44 6.22
## 8 1.74 0.946 2.52
## 9 -4.57 -6.14 -2.94
## 10 9.12 5.77 12.1
## # … with 90 more rows
On peut noter que dans le résultat final, les tableaux créés à chaque itération ont été collés par lignes. C’est le cas puisque nous avons fait appel à la fonction map_df()
. Si en revanche on supprime le suffixe _df()
de cette fonction, et que nous utilions uniquement map()
, alors le résultat final sera rétribué dans une liste de la même longueur que celle donnée en input à l’argument .x
(c’est-à-dire simulations
dans notre exemple) :
tmp <-
simulations %>%
map(~tibble(
mean = mean(.),
q1 = quantile(., probs = .25),
q3 = quantile(., probs = .75))
)
length(tmp)
## [1] 100
## # A tibble: 1 × 3
## mean q1 q3
## <dbl> <dbl> <dbl>
## 1 -10.2 -11.1 -9.37
4.3.2 Tibbles en input
À présent, considérons le cas où l’objet donné en entrée aux fonctions de type map
est un tableau de données. Dans ce cas, la fonction (ou la formule) donnée à l’argument .f
de la fonction de type map
sera appliquée à chacune des colonnes du tableau de données.
Par exemple, pour connaître la moyenne des colonnes de type numérique dans le tableau de données iris
, on peut dans un premier temps sélectionner uniquement les colonnes numériques à l’aide de la fonction select_if()
en précisant que l’on souhaite conserver uniquement les colonnes pour lesquelles l’application de la fonction is.numeric()
retournerala valeur TRUE
. Ensuite, si on applique la fonction map_df()
au tableau de données iris
dont seules les colonnes numériques ont été conservées, en fournissant à l’argument .f
la fonction mean()
, on obtient la moyenne pour chaque colonne de ce tableau :
## # A tibble: 1 × 4
## Sepal.Length Sepal.Width Petal.Length Petal.Width
## <dbl> <dbl> <dbl> <dbl>
## 1 5.84 3.06 3.76 1.20
Si on applique une fonction à l’aide de map()
aux colonnes d’un tableau de données, et que cette fonction retourne plusieurs valeurs, il est possible de faire à nouveau appel à une fonction de type map
sur le résultat pour extraire un des résultats. Admettons par exemple que l’on souhaite effectuer un test de normalité sur chacune des colonnes (numériques) du tableau de données iris
, puis extraire la valeur-p associée au test (en effectuant, par exemple un test de Shapiro-Wilk). La valeur-p est stocjée dans un élément du résutat retourné par la fonction shapiro.test()
nommé p.value
## Sepal.Length Sepal.Width Petal.Length Petal.Width
## 1.018116e-02 1.011543e-01 7.412263e-10 1.680465e-08
Comme avec les listes (cf plus haut), il est possible d’appliquer plusieurs fonctions à chaque colonne du tableau de données et de retourner sous la forme d’un tibble chaque sous-résultat. On peut par ailleurs ajouter à la fonction map_df()
l’argument .id
auquel on indique la valeur "variable"
pour faire figurer une colonne indiquant le nom de chaque colonne.
iris %>%
select_if(is.numeric) %>%
map_df(~tibble(
mean = mean(.),
q1 = quantile(., probs = .25),
q3 = quantile(., probs = .75)
),
.id = "variable"
)
## # A tibble: 4 × 4
## variable mean q1 q3
## <chr> <dbl> <dbl> <dbl>
## 1 Sepal.Length 5.84 5.1 6.4
## 2 Sepal.Width 3.06 2.8 3.3
## 3 Petal.Length 3.76 1.6 5.1
## 4 Petal.Width 1.20 0.3 1.8
Evidemment, il est possible d’appliquer certaines fonctions sur des variables factorielles. Par exemple, si on désire connaître le nombre de valeurs distinctes pour chaque variable catégorielle d’un tableau de données :
iris %>%
select_if(is.factor) %>%
map_df(~tibble(
n_dist = n_distinct(.),
class = class(.x)
),
.id = "variable"
)
## # A tibble: 0 × 0
Astuce : pour connaître le type de données de chaque colonne, on peut faire appel à la fonction map_chr()
en appelant la fonction class()
sur chaque colonne :
## Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## "numeric" "numeric" "numeric" "numeric" "character"
4.4 Avec {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 llapply()
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-dessous, où les lignes correspondent aux formats d’entrée, et les lignes aux formats de sortie. Pour y avoir accès, il est possible de charger le package. Il est également possible de faire précéder l’appel des fonctions par le nom du package (il existe de nombreux conflits de noms de fonctions entre celle de {plyr
} et d’autres packages…)
Type | array |
data.frame |
list |
---|---|---|---|
array |
aaply() |
adply() |
alply() |
data.frame |
daply() |
ddply() |
dlply() |
list |
laply() |
ldply() |
llply() |
input
au lieu d’un seul objet. Les fonctions mlply()
, mdply()
et maply()
s’en chargent. 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 arguments de ces fonctions commencent par un point (.
), afin d’éviter des incompatibilités avec la fonction à appliquer.
4.4.1 Array en input
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.
l’argument .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
# La moyenne des valeurs pour chaque ligne
plyr::aaply(tableau, 1, mean) # résultat sous forme de tableau
## a b c
## 11.5 12.5 13.5
## ligne V1
## 1 a 11.5
## 2 b 12.5
## 3 c 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 longitude
# en ne simplifiant pas le résultat
plyr::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
## ligne colonne V1
## 1 a A 7
## 2 b A 8
## 3 c A 9
## 4 a B 10
## 5 b B 11
## 6 c B 12
## 7 a C 13
## 8 b C 14
## 9 c C 15
## 10 a D 16
## 11 b D 17
## 12 c D 18
# L'affichage prend beaucoup de place
# alply(tableau, c(1,2), mean)
# Avec une fonction définie par l'utilisateur/rice
standardise <- function(x) (x - min(x)) / (max(x) - min(x))
# Standardiser les valeurs par colonne
plyr::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
4.4.2 Data frame en input
En économétrie, les data frames sont très présents. Aussi, la connaissance des fonction daply()
, ddply()
et dlply()
s’avère pratique. En effet, elles sont très utiles pour appliquer des fonctions à des groupes basés sur des combinaisons de variables. Actuellement, il est toutefois, selon moi, plus pratique d’avoir recours aux fonctions du package {dplyr
} (group_by()
, summarise()
, mutate()
, …, comme présentées dans la Section @ref(tableaux_de_donnees)). À titre informatif, ces notes de cours présentent toutefois les fonctions du package {plyr
}.
Avec les fonctions d*ply()
, il est nécessaire d’indiquer quelles variables, ou fonctions de variables on souhaite utiliser, en l’indiquant à l’argument .variables
. Elles peuvent être contenue dans le data frame fourni à l’argument .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)
à l’argument .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 <-
tibble(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
## # A tibble: 12 × 5
## region departement annee ouvriers ingenieurs
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 Bretagne Cotes-d'Armor 2011 8738 1420
## 2 Bretagne Finistere 2011 12701 2530
## 3 Bretagne Ille-et-Vilaine 2011 11390 3986
## 4 Bretagne Morbihan 2011 10228 2025
## 5 Corse Corse-du-Sud 2011 975 259
## 6 Corse Haute-Corse 2011 1297 254
## 7 Bretagne Cotes-d'Armor 2010 8113 1334
## 8 Bretagne Finistere 2010 12258 2401
## 9 Bretagne Ille-et-Vilaine 2010 10897 3776
## 10 Bretagne Morbihan 2010 9617 1979
## 11 Corse Corse-du-Sud 2010 936 253
## 12 Corse Haute-Corse 2010 1220 241
# Total chomeurs en Bretagne et en Corse pour les années 2010 et 2011
# Sous forme de data frame
plyr::ddply(chomage, plyr::.(annee), summarise, total_chomeurs = sum(ouvriers + ingenieurs))
## annee total_chomeurs
## 1 2010 53025
## 2 2011 55803
# Sous forme de tableau
plyr::daply(chomage, plyr::.(annee), summarise, total_chomeurs = sum(ouvriers + ingenieurs))
## $`2010`
## [1] 53025
##
## $`2011`
## [1] 55803
# Sous forme de liste
plyr::dlply(chomage, plyr::.(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
plyr::ddply(chomage, plyr::.(annee, region), summarise,
total_chomeurs = sum(ouvriers + ingenieurs))
## annee region total_chomeurs
## 1 2010 Bretagne 50375
## 2 2010 Corse 2650
## 3 2011 Bretagne 53018
## 4 2011 Corse 2785
## annee region V1
## 1 2010 Bretagne 4
## 2 2010 Corse 2
## 3 2011 Bretagne 4
## 4 2011 Corse 2
# En utilisant une fonction définie par l'utilisateur/rice
plyr::ddply(chomage, plyr::.(annee, region), function(x){
moy_ouvriers <- mean(x$ouvriers)
moy_ingenieurs <- mean(x$ingenieurs)
data.frame(moy_ouvriers = moy_ouvriers, moy_ingenieurs = moy_ingenieurs)
})
## annee region moy_ouvriers moy_ingenieurs
## 1 2010 Bretagne 10221.25 2372.50
## 2 2010 Corse 1078.00 247.00
## 3 2011 Bretagne 10764.25 2490.25
## 4 2011 Corse 1136.00 256.50
4.4.3 List en input
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
plyr::laply(liste, length)
## [1] 10 3 3
## .id V1
## 1 normale 10
## 2 logiques 3
## 3 x 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/rice
plyr::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 -4.2061356
## [7] 5.6869803 7.5847895 6.3552892 -0.3099997
##
## $logiques
## [1] 3.5 3.5 2.0
##
## $x
## [1] 2 NA 4
4.4.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 cœurs souhaité en faisant appel à la fonction registerDoMC()
, et de fixer la valeur TRUE
à l’argument .parallel
de la fonction **ply()
.
df <-
data.frame(valeur_1 = rnorm(12000),
id = rep(month.abb, each = 1000))
# Fonction lente (et inutile)
f <- function(x){
res <- rep(NA, nrow(x))
for(i in 1:length(res)){
res[i] <- mean(x[1:i, "valeur_1"],)
}
mean(res)
}
library(doMC)
# Définir le parallel backend avec le package foreach
registerDoMC(cores=4)
system.time(plyr::ddply(df, plyr::.(id), f))
## user system elapsed
## 0.101 0.005 0.105
## user system elapsed
## 0.116 0.096 0.063
4.5 Avec {base
}
Le tableau ci-dessous 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 |
4.5.1 La fonction lapply
La fonction lapply()
applique à chaque élément du premier argument qui lui est donné une fonction indiquée en second argument et retourne le résultat sous forme de liste. La syntaxe est la suivante :
avec X
la liste ou le vecteur donné en argument sur lequel on désire appliquer la fonction FUN
. L’argument ...
permet comme expliqué dans une précédente remarque, de fournir des arguments à 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.04319732
##
## $logiques
## [1] 0.6666667
##
## $x
## [1] 1.5
On peut créer une fonction à l’intérieur de l’appel à la fonction lapply()
. Le premier argument est nécessairement un élément du vecteur auquel on souhaite appliquer la fonction.
## $normale
## [1] -17.375456 -6.647068 19.251983 -12.199524 -42.424943 24.224455
## [7] -12.030570 24.388223 31.518789 1.294111
##
## $logiques
## [1] 1.5 1.5 0.0
##
## $x
## [1] 0 NA 2
# Si la fonction doit posséder plusieurs arguments
lapply(liste, function(x, y) x / mean(x, na.rm = TRUE) + y, y = 2)
## $normale
## [1] -15.375456 -4.647068 21.251983 -10.199524 -40.424943 26.224455
## [7] -10.030570 26.388223 33.518789 3.294111
##
## $logiques
## [1] 3.5 3.5 2.0
##
## $x
## [1] 2 NA 4
On peut appliquer la fonction 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.
## 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. Par exemple, définissons une liste à l’aide de lapply()
: l <- lapply(1:3, function(x) cbind(valeur = x, lettre = LETTERS[x]))
. Ensuite, pour coller les lignes entre-elles : do.call("rbind", l) %>% data.frame()
.
do.call("rbind", x)
revient à faire rbind(x[1], x[2], ..., x[n])
avec x
un vecteur de taile \(n\).
4.5.2 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. l’argument 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.
(x <- list(a = 1:10, beta = exp(-3:3), logic = c(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)
# Avec sapply
sapply(x, quantile)
# Exemple avec USE.NAMES
sapply(LETTERS[1:3], nchar)
sapply(LETTERS[1:3], nchar, USE.NAMES = FALSE)
4.5.3 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 arguments que pour sapply()
. l’argument 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)
4.5.4 La fonction apply()
La fonction apply()
possède la syntaxe suivante :
<<boucles_vectorisation_apply_apply_syntaxe, eval = FALSE>>=
apply(X, MARGIN, FUN, …)
@
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 arguments 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
4.5.5 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 taleau de données, INDEX
une liste d’un ou plusieurs facteurs, chacun de même taille que X
. l’argument 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.
## Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1 5.1 3.5 1.4 0.2 setosa
## 2 4.9 3.0 1.4 0.2 setosa
## 3 4.7 3.2 1.3 0.2 setosa
## 4 4.6 3.1 1.5 0.2 setosa
## 5 5.0 3.6 1.4 0.2 setosa
## 6 5.4 3.9 1.7 0.4 setosa
## 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
4.5.6 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 d’arguments supplémentaires à fournir à la fonction à appliquer. Les arguments 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
4.5.7 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 fonction Vectorize()
est la suivante :
avec FUN
une fonction à appliquer, vectorize.args
un vecteur d’arguments (de type caractère) qui devraient être vectorisés (par défaut, tous les arguments de FUN
). Les arguments 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
4.6 Exercices
Exercice 1. Boucle while()
- à l’aide de la fonction
while()
, créer une boucle qui permet de calculer la factorielle d’un nombre ; - Réutiliser le code de la question précédente pour en faire une fonction qui, lorsqu’on lui donne un nombre, retourne sa factorielle. Comparer le résultat avec la fonction
factorial()
.
Exercice 2. Boucles while()
et for()
- Choisir un nombre mystère entre 1 et 100, et le stocker dans un objet que l’on nommera
nombre_mystere
. Ensuite, créer une boucle qui à chaque itération effectue un tirage aléatoire d’un entier compris entre 1 et 100. Tant que le nombre tiré est différent du nombre mystère, la boucle doit continuer. à la sortie de la boucle, une variable que l’on appelleranb_tirages
contiendra le nombre de tirages réalisés pour obtenir le nombre mystère ; - Utiliser le code de la question précédente pour réaliser la fonction
trouver_nombre
, qui, lorsqu’on lui donne un nombre compris entre 1 et 100, retourne le nombre de tirages aléatoires d’entiers compris entre 1 et 100 nécessaires avant de tirer le nombre mystère ; - En utilisant une boucle
for
, faire appel 1000 fois à la fonctiontrouver_nombre()
qui vient d’être créée. à chaque itération, stocker le résultat dans un élément d’un vecteur que l’on appelleranb_essais_rep
. Enfin, afficher la moyenne du nombre de tirages nécessaires pour retrouver le nombre magique.
nb_essais_rep <- rep(NA, 1000)
Exercice 3. Boucles for()
- Parcourir les entiers de 1 à 20 à l’aide d’une boucle
for
en affichant dans la console à chaque itération si le nombre courant est pair ; - L’objet
month.name
est un vecteur contenant les noms des mois du calendrier, en anglais. Parcourir chacun des éléments de ce vecteur, et afficher dans la console pour chacun des mois si le nombre de caractères composant le nom du mois est pair ou impair.
Exercice 4. Suite de Fibonacci
Utiliser une boucle for
pour reproduire la suite de Fibonacci jusqu’à son dixième terme (la séquence \(F_n\) est définie par la relation de récurrence suivante : \(F_{n} = F_{n-1} + F_{n-2}\) ; les valeurs initiales sont : \(F_0 = 0\) et \(F_{1} = 1\)).
Exercice 5. Barre de progression
Considérons le vecteur de chaînes de caractèresids
ainsi que la liste res
:
library(magrittr)
n <- 1000
ids <-
str_c(sample(LETTERS, n, replace = TRUE),
sample(letters, n, replace = TRUE)) %>%
unique()
res <- vector("list", length(ids))
Parcourir les éléments du vecteur à l’aide d’une boucle for()
. à chaque itération, stocker dans l’élément de la liste res
dont la position correspond à celle de l’identifiant courant dans ids
les informations suivantes : (i) l’identifiant courant et (ii) la somme de \(50 000\) tirages aléatoires selon une \(\mathcal{N}(0,1)\).
Afficher l’état d’avancement de la boucle à l’aide d’une barre de progression.
Exercice 6. Fonctions appliquées aux éléments d’une liste
Soit une liste nommée twittos
, disponible à l’adresse suivante : http://egallic.fr/Enseignement/R/Exercices/donnees/twittos.rda. Elle contient des informations fictives sur des utilisateur•rice•s de Twitter ; chaque élément de cette liste est une liste dont les éléments sont les suivants :
screen_name
: nom d’utilisateur•rice ;nb_tweets
: nombre de tweets ;nb_followers
: nombre de followers ;nb_friends
: nombre de followings ;created_at
: date de création du compte ;location
: ville renseignée.
- Importer le contenu du fichier dans la session
R
; - Utiliser la fonction map appropriée sur
twittos
pour récupérer une liste contenant uniquement les noms d’utilisateur•rice•s. Faire de même pour le nombre de followers, puis appliquerunlist()
au résultat ; - Créer une fonction qui, quand on lui fournit un élément de la liste
twittos
, c’est-à-dire les informations sous forme de liste d’un•e seul•e utilisateur•rice, retourne ces informations sous forme de tableau de données. Nommer cette fonctiontwittos_to_df
; - Appliquer la fonction
twittos_to_df()
au premier élément de la listetwittos
, puis utiliser la fonction map appropriée pour appliquer la fonctiontwittos_to_df()
à tous les éléments de la liste. Stocker ce dernier résultat dans un objet appeléres
; - Quelle est la structure de l’objet
res
obtenu à la question précédente ? - Importer le fichier disponible à cette adresse dans la session
R
: http://egallic.fr/Enseignement/R/Exercices/donnees/dates_tw.rda. Il s’agit d’une liste donc chaque élément contient une liste indiquant le nom d’un•e utilisateuret la date de chacun de ses tweets. - Appliquer la fonction map appropriée à la liste
dates_tw
qui vient d’être importée dansR
, pour afficher l’heure moyenne des tweets pour chaque utilisateur•rice, puis faire de même pour l’écart-type.
Exercice 7. Fonctions appliquées aux éléments d’une matrice
- Créer une matrice de dimension \(100\times 5\), donc chaque vecteur colonne est composé de tirages issus d’une loi Normale centrée réduite ;
- Utiliser la fonction
apply()
pour calculer la moyenne des valeurs de chaque colonne ; - Utiliser la fonction
apply()
pour calculer l’écart-type des valeurs de chaque colonne.