Nous allons voir dans ce chapitre des éléments de programmation qui permettent de gagner en clarté et simplicité dans le code et parfois en temps de calcul.
Ce document a été généré directement depuis RStudio en utilisant l’outil Markdown. La version .pdf se trouve ici.
Packages à installer
install.packages((c("ggplot2", # Graphiques ggplot2
"kableExtra", # Insérer des tables dans Markdown
"Matrix", # Matrice creuse
"microbenchmark", # Temps de calcul
"pryr", # Mieux comprendre R
"Rcpp", # Faire appel à du code C++
"reticulate" # Interface vers Python
)))
On montre un exemple pour utiliser du code Python. Pour cela, on aura besoin de la bibliothèque pandas:
::py_install("pandas") reticulate
1 Quelques règles de style
R n’a pas de PEP 8 comme c’est le cas pour Python. En revanche, pour des raisons évidentes de clarté et de lisibilité pour soi-même et pour les éventuels collègues qui vont lire notre code, il est important d’essayer de respecter si possible quelques règles d’écritures de codes. On s’est inspiré ici de ces deux documents :
“Style guide”” de Hadley Wickham dans le livre “R advanced”
1.1 Le nom des fichiers de codes
Appeler vos fichiers de code avec l’extension .R et donner des noms clairs à vos fichiers de codes, en utilisant éventuellement le symbole underscore ou le trait d’union entre les différents mots. Par exemple :
-base.R
traitement-utiles.R
fonctions
exploration.R prediction.R
1.2 Le nom des objets
Eviter d’appeler vos objets avec des noms de fonctions existants ou de mots clés. Le nombre de caratères devrait être restreint même si le nom doit avoir un sens pour le lecteur. On peut utiliser le symbole underscore ou le point pour séparer des mots entre eux :
arbre_reg
rf_reg glm_reg
1.3 Espace entre les opérateurs
Aérer au maximum les opérations (affectation, calcul, utilisation de fonctions, etc.) par des espaces :
<- c(5, NA, 4, 3)
a mean(a, na.rm = TRUE)
Exception faite pour les opérateurs :, :: et ::: où il ne faut pas d’espace :
1:10
::lm stats
1.4 Les conditions
Quand on utilise les conditions if/else, for, while :
laisser un espace après le mot clé,
l’accolade ouvrante se trouve à la fin de la ligne contenant le mot clé, l’accolande fermante se trouve sur une nouvelle ligne,
mettre deux espaces en début de ligne (indentation) à partir de la seconde ligne jusqu’à la fin de la condition,
si des conditions sont imbriquées, la nouvelle condition devrait se trouver sur la même ligne que l’accolade fermante de la première condition.
if (y < 0 && debug) {
message("Y is negative")
}
if (y == 0) {
log(x)
else {
} ^ x
y }
1.5 La taille d’une ligne
Essayer de limiter 80 caractères à une ligne de code.
1.6 Affectation
Pour affecter une valeur à un objet essayer d’utiliser l’opérateur <- plutôt que =.
<- 5 a
Remarque : sur RStudio quelques-une des règles ci-dessus sont implémentées par défaut, notamment les indentations après les boucles ou les fonctions.
Exercice 2.1
Mettre le code suivant en utilisant les règles de style présentées ci-dessus:
=function(x)
my_mean
{# verification
if(!is.numeric(x))
stop("x must be a numeric vector")
# initialization
= length(x)
n=0
res for(k in 1:n)
{= res+x[k]
res
}# return res
return(res/ n)
}
2 Fixer la taille des vecteurs (si on la connaît à l’avance)
On a le problème suivant : on souhaite obtenir un vecteur de taille \(n\) contenant des valeurs simulées issues d’une loi gaussienne centrée et réduite, mais on ne souhaite garder que les valeurs supérieures à un paramètre a (égal à 0 par défaut). Dans la fonction trunc_rnorm.1() ci-dessous, nous ne précisons pas la taille du vecteur x qui sera retourné. A chaque fois qu’une valeur répond au critère, on la concatène au vecteur x en utilisant la commande c(). Le défaut de cette méthode est que R est sans arrêt en train d’allouer un nouvel espace mémoire pour le vecteur x qu’on modifie à chaque fois puisqu’on change sa taille.
.1 <- function(n, a = 0) {
trunc_rnorm<- numeric(0)
x <- 1
i while (i <= n) {
<- rnorm(1)
r if (r > a) {
<- c(x, r)
x = i + 1
i
}
}
x }
C’est pourquoi lorsqu’on connaît à l’avance la taille du vecteur qu’on souhaite créer, on le créé avec la bonne taille et l’espace mémoire alloué à ce vecteur est fixée dès le début. C’est la seule différence avec la fonction trunc_rnorm.2() ci-dessous où on a précisé la taille de x dès le départ. On affecte ensuite à chaque élément de x la valeur qu’on souhaite garder.
.2 <- function(n, a = 0) {
trunc_rnorm<- numeric(n)
x <- 1
i while (i <= n) {
<- rnorm(1)
r if (r > a) {
<- r
x[i] <- i + 1}
i
}
x }
On constate que les différences de temps de calcul sont assez importantes :
system.time(trunc_rnorm.1(10000))
## utilisateur système écoulé
## 0.130 0.020 0.151
system.time(trunc_rnorm.2(10000))
## utilisateur système écoulé
## 0.031 0.000 0.032
3 Temps de calcul et mémoire
3.1 Pour mesurer les temps de calcul efficacement
Si vous répétez les instructions précédentes successivement, vous constaterez que les temps de calculs sont à chaque fois différents. Ceci s’explique par le fait que les machines sur lesquelles on travaille exécutent plusieurs tâches à la fois, qui ne sont pas toujours les mêmes à l’instant \(t\). Aussi, une façon de rendre robuste ces temps de calcul est de répéter un certain nombre de fois ces mêmes commandes et de présenter ensuite les résultats statistiques sur ces temps de calcul.
Pour cela, on peut bien évidemment utiliser une boucle for ainsi et faire ensuite des statistiques de base
<- data.frame(time_1 = numeric(10),
my_time time_2 = numeric(10))
for (b in 1:10) {
$time_1[b] <- system.time(trunc_rnorm.1(10000))[3]
my_time$time_2[b] <- system.time(trunc_rnorm.2(10000))[3]
my_time
}summary(my_time)
## time_1 time_2
## Min. :0.1100 Min. :0.02600
## 1st Qu.:0.1153 1st Qu.:0.02725
## Median :0.1185 Median :0.02850
## Mean :0.1185 Mean :0.02880
## 3rd Qu.:0.1210 3rd Qu.:0.02975
## Max. :0.1250 Max. :0.03400
Sinon, on utilise la fonction microbenchmark() du package microbenchmark (Mersmann et al., 2019). En général, on regarde la moyenne.
<- microbenchmark::microbenchmark(
mbm trunc_rnorm.1(n = 10000),
trunc_rnorm.2(n = 10000),
times = 10L)
mbm
## Unit: milliseconds
## expr min lq mean median uq
## trunc_rnorm.1(n = 10000) 111.43848 113.39500 118.93426 116.61849 120.07285
## trunc_rnorm.2(n = 10000) 26.79778 28.71063 30.23288 30.60795 31.59715
## max neval
## 141.24738 10
## 32.95456 10
Par ailleurs, il est possible de représenter graphiquement ces résultats avec la fonction autoplot() du package ggplot2.
::autoplot(mbm) ggplot2
3.2 Pour comprendre la gestion de la mémoire
Lorsqu’on crée un vecteur sous R, celui-ci est stocké sur un espace mémoire que l’on peut identifier avec la fonction address() du package pryr (Wickham, 2019).
Par exemple :
library("pryr")
<- numeric(10)
x address(x)
## [1] "0x555911a74e78"
Lorsqu’on attribue de nouvelles valeurs à ce vecteur, l’adresse ne change pas :
for (i in 1:10) {
<- ifelse(rnorm(1) > 0, 1, 0)
x[i] print(address(x))
}
## [1] "0x55590e6cc628"
## [1] "0x55590e6cc628"
## [1] "0x55590e6cc628"
## [1] "0x55590e6cc628"
## [1] "0x55590e6cc628"
## [1] "0x55590e6cc628"
## [1] "0x55590e6cc628"
## [1] "0x55590e6cc628"
## [1] "0x55590e6cc628"
## [1] "0x55590e6cc628"
En revanche, si on change la taille de ce vecteur, R va systématiquement réserver un nouvel espace mémoire, ce qui est une opération coûteuse en temps calcul, d’où des temps de calcul plus long. Par exemple :
for (i in 11:20) {
<- ifelse(rnorm(1) > 0, 1, 0)
x[i] print(address(x))
}
## [1] "0x5559107cba68"
## [1] "0x55591064a078"
## [1] "0x55591064a548"
## [1] "0x55591064a968"
## [1] "0x55591064b3b8"
## [1] "0x55591064b468"
## [1] "0x55590cfdbae0"
## [1] "0x555911c794a0"
## [1] "0x555911c5ce70"
## [1] "0x55590bdc8590"
Exercice 2.2 Comparer les temps de calcul des trois expressions suivantes. Représenter graphiquement la variation des temps de calcul.
<- 10 ^ 6
n # expression 1
<- numeric(n)
x for (k in 1:n)
<- (5 == sample(1:10, 1))
x[k] mean(x)
# expression 2
<- NULL
x for (k in 1:n)
<- c(x, (5 == sample(1:10, 1)))
x mean(x)
# expression 3
<- 0
x for (k in 1:n)
<- x + (5 == sample(1:10, 1))
x mean(x)
4 Fonction vectorisée
R est un langage interprété et de ce fait, l’exécution de boucles est coûteuse en temps calcul, contrairement à des langages compilés. Pour palier ce problème, les concepteurs du langage R ont créé de nombreuses fonctions vectorisées, c’est-à-dire qui peuvent s’appliquer à des vecteurs et qui font en général appel à des fonctions codées en C ou Fortran pour gagner en rapidité. Pour comprendre ce concept, on va comparer les deux méthodes suivantes dont le but est de calculer la somme des éléments d’un vecteur simulée selon une loi \(\mathcal{N}(0,1)\).
- Méthode 1 : on utilise des boucles depuis R
<- rnorm(1000000)
vec <- function(x) {
my_sum <- 0
res for (k in seq_along(vec)) {
<- res + vec[k]
res
} return(res)
}
- Méthode 2 : on utilise la fonction sum() sur un vecteur. Cette dernière va bien entendu effectuer une boucle, mais elle sera effectuée en langage compilé, donc plus rapidement.
::microbenchmark(
microbenchmarkmy_sum(vec),
sum(vec),
times = 10L)
## Unit: microseconds
## expr min lq mean median uq max
## my_sum(vec) 50118.724 51462.426 53335.9377 52877.1530 54465.026 60293.264
## sum(vec) 767.259 800.842 988.1575 878.5295 1043.569 1801.766
## neval
## 10
## 10
Remarque : il existe de nombreuses fonctions de base qui sont vectorisées et qui permettent de gagner en temps de calcul et on les utilise souvent sans s’en rendre compte. Par exemple, les fonctions log(), cos(), exp(), etc. Dès lors qu’on est amener à faire des boucles, cela peut valoir le coup de se poser la question : est-ce qu’il n’existe pas de fonctions prédéfinies et vectorisées qui feraient déjà ce qu’on souhaite faire, en un temps plus rapide.
Exercise 2.3
Ecrire la fonction my_sd() qui calcule l’écart-type d’un vecteur numeric. La fonction ne pourra pas faire appel ni à la fonction sum() ni mean() et ne pourra avoir qu’une seule boucle. Comparer les temps de calcul avec sd().
5 Ecrire un code en C++
Lorsqu’on a identifié certaines parties d’un code qui sont coûteuses en temps calcul, il peut être intéressant de coder ces parties sous formes de fonctions écrites en C++ et d’appeler ensuite ces fonctions depuis R.
Par exemple, on va convertir la fonction my_sum() écrite en R en code C++. On présente ci-dessous le code inclus dans le fichier sumcplusplus.cpp.
#include <Rcpp.h> // pour utiliser des fonctions de base
using namespace Rcpp;
// La commande suivante permet d'appeler ensuite les fonctions
// crées depuis R
// [[Rcpp::export]]
// début de la function
double sum_rcpp(NumericVector x) {
double res = 0;
int n = x.size();
for(int i = 0; i < n; i++) {
= res + x(i);
res }
return res;
}
L’algorithme utilisé dans la fonction sum_rcpp() présente la même syntaxe que la fonction my_sum() écrite en R. Par contre, il faut utiliser les particularités du langage C++. Parmi ces particularités :
il faut définir le type de l’objet qui sera retourné par la fonction,
de manière générale, il faut définir le type de tous les objets créés et/ou utilisés. En effet, on a du définir le type de tous les paramètres d’entrée. Même dans la boucle for, on a définir le type de i qui sert d’incrément,
les lignes de code se terminent par le point virgule,
l’opérateur d’affectation est le symbole =,
la boucle for s’utilise différemment que du code R,
pour connaître la taille d’un vecteur, on a utilisé la méthode .size() qui retourne un entier,
l’indexation du vecteur commence à 0 et on utiliser les parenthèses pour accéder à un élément du vecteur.
Une fois la fonction écrite, il faut la charger sous R. Pour cela, on utilise la fonction sourceCpp() du package Rcpp (Eddelbuettel et al., 2019) qui est le package R le plus utilisé par d’autres packages.
require("Rcpp")
## Le chargement a nécessité le package : Rcpp
sourceCpp("sumcplusplus.cpp")
On peut ensuite appeler directement la fonction créée :
sum_rcpp(vec)
## [1] 916.6986
Depuis R Markdown, il est possible d’insérer du code écrit C++ directement à l’intérieur d’un chunk. Pour cela, il suffit de cliquer depuis RStudio sur l’onglet “Insert”, puis “Rcpp” et cela aura pour effet d’insérer une balise de code qui sera compilée au moment de son évaluation (autrement dit, sourceCpp() est automatiquement exécutée).
Si on compare les temps de calcul avec my_func() et la fonction sum(), on constate que sum_rcpp() est presque 20 fois plus rapides que my_sum() et 4 fois plus lentes que sum().
::microbenchmark(
microbenchmarkmy_sum(vec),
sum(vec),
sum_rcpp(vec),
times = 10L)
## Unit: microseconds
## expr min lq mean median uq max
## my_sum(vec) 50852.607 53288.213 59041.438 57175.607 62240.429 75514.070
## sum(vec) 790.731 868.438 1000.808 967.256 1049.452 1489.220
## sum_rcpp(vec) 3371.074 3427.827 3564.028 3489.439 3652.352 3970.837
## neval
## 10
## 10
## 10
Pour en savoir plus sur l’utilisation de code C++ depuis R, on recommande le document suivant : http://adv-r.had.co.nz/Rcpp.html
Exercise 2.4
Ecrire la fonction my_sd_cpp() en C++ qui calcule l’écart-type d’un vecteur numeric. Comparer les temps de calcul avec sd().
Remarque : en C++, on utilise pow(a, b) pour calculer \(a^b\)
6 Insérer du code Python dans un document Markdown
Il est possible d’exécuter du code Python dans un document R Markdown et d’importer ensuite les objets créés sous R pour les utiliser.
Pour cela, il suffit de cliquer depuis RStudio sur l’onglet Insert, puis Python et cela aura pour effet d’insérer une balise de code Python
```{python}
```
Par exemple, on va executer les lignes de commande Python suivantes dans lesquelles on a créé l’objet flights :
import pandas
= pandas.read_csv("http://www.thibault.laurent.free.fr/cours/R_avance/flights.csv")
flights = flights[flights['Dest'] == "TPA"]
flights = flights[['UniqueCarrier', 'DepDelay', 'ArrDelay']]
flights = flights.dropna() flights
Pour rappatrier l’objet Python dans R, il suffit ensuite d’utiliser la commande py$ du package reticulate (Ushey et al., 2019) suivi de l’objet à rappatrier :
library("reticulate")
library("ggplot2")
ggplot(py$flights, aes(UniqueCarrier, ArrDelay)) +
geom_point() +
geom_jitter()
7 Eviter si possible les boucles (apply(), lapply(), replicate(), colSums(), etc.)
On va parler ici des fonctions apply(), lapply, sapply(), mapply(), tapply(). Les différences en temps de calcul ne sont pas significatives par rapport à l’utilisation de boucles, dans la mesure où implicitement, ces fonctions font le même travail que les boucles. L’idée de ces fonctions est de réduire le nombre de lignes de code qu’on utiliserait en faisant des boucles à la place.
On va voir qu’on utilise une de ces fonctions plutôt qu’une autre selon le type de l’objet considéré en entrée et selon le type d’objets que l’on souhaite en sortie. On verra également la fonction replicate() qui utilise les propriétés de la fonction sapply(), mais pour un usage un peu différent.
On présentera également les fonctions colSums(), rowSums(), colMeans(), rowMeans() qui sont plus performantes qu’utiliser la fonction apply() car elles sont vectorisées.
7.1 apply() pour les matrix/array
Une matrix est une forme particulière d’un array. Une matrix posssède deux dimensions (espace des lignes et espace des colonnes), un array peut avoir autant de dimensions que souhaitées. Les \(D\)-dimensions sont données sous forme d’un vecteur de taille \(D\), chaque élément \(i\) donnant la taille de la dimension \(i\). En général, on utilise peu les array à plus de 3 dimensions. Une caractéristique d’un array est que tous ses éléments sont du même type : numeric, integer, logical, character (rarement complex).
Commençons par créer un array à trois dimensions de taille \(3 \times 2 \times 2\). Pour simplifier la compréhension d’un array, on va donner des noms à chaque composante de chaque dimension. La 1ère dimension correspondra aux nom des individus, la 2ème celle des variables, la 3ème celle des années :
<- array(c(100, 90, 110, 25, 24, 28, 175, 180, 190, 68, 74, 85),
tab dim = c(3, 2, 2))
dimnames(tab)[[1]] <- c("Luc", "Pierre", "Pedro")
dimnames(tab)[[2]] <- c("taille", "poids")
dimnames(tab)[[3]] <- c("2000", "2010")
tab
## , , 2000
##
## taille poids
## Luc 100 25
## Pierre 90 24
## Pedro 110 28
##
## , , 2010
##
## taille poids
## Luc 175 68
## Pierre 180 74
## Pedro 190 85
La fonction apply() permet de faire des calculs :
- sur une seule dimension. On précise alors le numéro de la dimension dans le deuxième argument et le troisième argument correspond à la fonction qu’on souhaite appliquer sur chaque élément appartenant à cette dimension. Dans ce cas, le résultat est un vecteur de longueur la taille de la dimension choisie. Par exemple, si on souhaite faire la moyenne des éléments en considérant la 2ème dimension.
apply(tab, 2, mean)
## taille poids
## 140.83333 50.66667
Remarque : en réalité, selon la fonction qu’on applique à apply(), le résultat n’est pas forcément un vecteur. Essayer par exemple de remplacer la fonction mean() par range() dans l’exemple ci-dessus.
Equivalent avec les boucles : si on avait utilisé les boucles plutôt que la fonction apply(), on aurait utilisé le code suivant :
<- numeric(dim(tab)[2])
res for (k in seq_along(res))
<- mean(tab[, k, ])
res[k] res
## [1] 140.83333 50.66667
- sur deux dimensions :
apply(tab, c(2, 3), mean)
## 2000 2010
## taille 100.00000 181.66667
## poids 25.66667 75.66667
Equivalent avec les boucles : si on avait utilisé les boucles pour faire ce calcul, on aurait fait :
<- array(0, dim = dim(tab)[c(2, 3)])
res for (i in 1:dim(tab)[2])
for (j in 1:dim(tab)[3])
<- mean(tab[, i, j])
res[i, j] res
## [,1] [,2]
## [1,] 100.00000 181.66667
## [2,] 25.66667 75.66667
7.2 Les fonctions colSums(), rowSums(), colMeans(), rowMeans()
Ces fonctions sont équivalentes à la fonction apply() en utilisant FUN=sum ou FUN=mean et en appliquant les bonnes dimensions (1 pour lignes, 2 pour colonnes), mais sont plus rapides car elles ont été codées en langage compilé.
Pour comparer les temps de calcul :
<- matrix(runif(10e6), nc = 5)
x ::microbenchmark(
microbenchmarkapply(x, 2, mean),
colMeans(x),
times = 10L)
## Unit: milliseconds
## expr min lq mean median uq
## apply(x, 2, mean) 98.79311 103.48479 131.653359 111.286857 181.312863
## colMeans(x) 7.93473 8.31074 8.652441 8.695388 8.888168
## max neval
## 197.150815 10
## 9.406257 10
7.3 La fonction lapply()
Même si la fonction lapply() s’applique aussi bien sur les vecteurs que les list, on présentera ici son utilisation pour les list/data.frame. L’intérêt de l’appliquer sur des vecteurs sera présenté avec la fonction replicate().
On rappelle ici qu’une list est caractérisée par un certain nombre d’éléments pouvant être de types différents. On accède aux éléments d’une liste par le symbole $ suivi du nom de l’élément de la liste et s’il n’y a pas de nom, on y accède avec les doubles crochets avec l’indice de l’élément auquel on souhaite accèder. Un data.frame est une forme particulière d’une list dans la mesure où les éléments peuvent être vus comme étant les variables (on y accède avec le symbole $).
La fonction lapply() consiste à appliquer la même opération sur chaque élément de la list. Cette opération pouvant être donnée par une fonction de base de R ou bien pouvant être une fonction programmée.
On considère le jeu de données mtcars accessible par défaut sous R et contenant un certaine nombre de variables sur des moteurs de voitures de différentes marques. Pour connaître les caractéristiques moyennes, on applique la fonction mean() à chaque élément de mtcars, autrement dit à chaque variable. Le résultat est retourné sous forme de list de même longeur que la list de départ.
Ici, on a utilisé la fonction unlist() sur le résultat retourné afin de présenter le résultat sous forme d’un vecteur, plus lisible à lire :
unlist(lapply(mtcars, mean))
## mpg cyl disp hp drat wt qsec
## 20.090625 6.187500 230.721875 146.687500 3.596563 3.217250 17.848750
## vs am gear carb
## 0.437500 0.406250 3.687500 2.812500
Cette fonction nous permet ainsi d’éviter de faire la boucle suivante :
<- numeric(length(mtcars))
res for (k in seq_along(res))
<- mean(mtcars[[k]])
res[k] res
## [1] 20.090625 6.187500 230.721875 146.687500 3.596563 3.217250
## [7] 17.848750 0.437500 0.406250 3.687500 2.812500
7.4 La fonction sapply()
On a vu que la fonction lapply() retournait une list. Or, si on sait par avance que le résultat retourné pour chaque élément de la liste sera identique pour chaque élément, la fonction sapply() va concaténer les résultats de chaque élément sous forme d’un vecteur ou d’un tableau. Si on reprend l’exemple précédent :
sapply(mtcars, mean)
## mpg cyl disp hp drat wt qsec
## 20.090625 6.187500 230.721875 146.687500 3.596563 3.217250 17.848750
## vs am gear carb
## 0.437500 0.406250 3.687500 2.812500
On présente ici un autre exemple où on va créer notre propre fonction qui consiste à retourner, le minimum, le maximum, la moyenne, l’écart-type et la moyenne de chaque élément. On code la fonction suivante où l’argument x sera interpété comme représentant un élément de la list.
<- function(x)
f c(min = min(x), max = max(x), mean = mean(x), med = median(x), sd = sd(x))
On applique ensuite la fonction f() à chaque élément de la list avec la fonction sapply(). On transpose le résultat car c’est en général ainsi que sont présentées les statistiques descriptives des variables :
t(sapply(mtcars, f))
## min max mean med sd
## mpg 10.400 33.900 20.090625 19.200 6.0269481
## cyl 4.000 8.000 6.187500 6.000 1.7859216
## disp 71.100 472.000 230.721875 196.300 123.9386938
## hp 52.000 335.000 146.687500 123.000 68.5628685
## drat 2.760 4.930 3.596563 3.695 0.5346787
## wt 1.513 5.424 3.217250 3.325 0.9784574
## qsec 14.500 22.900 17.848750 17.710 1.7869432
## vs 0.000 1.000 0.437500 0.000 0.5040161
## am 0.000 1.000 0.406250 0.000 0.4989909
## gear 3.000 5.000 3.687500 4.000 0.7378041
## carb 1.000 8.000 2.812500 2.000 1.6152000
Par ailleurs, pour insérer une table dans un document Markdown, on peut utiliser la fonction kbl() du package kableExtra dont on verra d’autres exemples dans le chapitre sur la visualisation de données.
::kbl(t(round(sapply(mtcars, f), 3))) kableExtra
min | max | mean | med | sd | |
---|---|---|---|---|---|
mpg | 10.400 | 33.900 | 20.091 | 19.200 | 6.027 |
cyl | 4.000 | 8.000 | 6.188 | 6.000 | 1.786 |
disp | 71.100 | 472.000 | 230.722 | 196.300 | 123.939 |
hp | 52.000 | 335.000 | 146.688 | 123.000 | 68.563 |
drat | 2.760 | 4.930 | 3.597 | 3.695 | 0.535 |
wt | 1.513 | 5.424 | 3.217 | 3.325 | 0.978 |
qsec | 14.500 | 22.900 | 17.849 | 17.710 | 1.787 |
vs | 0.000 | 1.000 | 0.438 | 0.000 | 0.504 |
am | 0.000 | 1.000 | 0.406 | 0.000 | 0.499 |
gear | 3.000 | 5.000 | 3.688 | 4.000 | 0.738 |
carb | 1.000 | 8.000 | 2.812 | 2.000 | 1.615 |
Remarque : si on on souhaite appliquer la fonction f() pour les éléments qui sont de type numeric et la fonction table() pour les éléments de type factor, alors la fonction sapply() retournera le même résultat que la fonction lapply(), à savoir une list, car les résultats retournés pour chaque élément ne seront pas les mêmes :
sapply(iris, function(x){
if (is.numeric(x))
f(x)
else
table(x)
})
## $Sepal.Length
## min max mean med sd
## 4.3000000 7.9000000 5.8433333 5.8000000 0.8280661
##
## $Sepal.Width
## min max mean med sd
## 2.0000000 4.4000000 3.0573333 3.0000000 0.4358663
##
## $Petal.Length
## min max mean med sd
## 1.000000 6.900000 3.758000 4.350000 1.765298
##
## $Petal.Width
## min max mean med sd
## 0.1000000 2.5000000 1.1993333 1.3000000 0.7622377
##
## $Species
## x
## setosa versicolor virginica
## 50 50 50
7.5 La fonction replicate()
On va partir de l’exemple suivant : on souhaitre simuler 5 échantillons de taille 10 distribués selon une loi uniforme \(U_{[0,1]}\).
- La première solution serait de faire une boucle :
<- vector("list", 5)
res for (k in 1:5)
<- runif(10) res[[k]]
- La deuxième solution consiste à appliquer la fonction lapply() sur un vecteur quelconque de taille 5 (ici, le plus simple est de faire 1:5, mais on aurait pu prendre n’importe quel vecteur de taille 5) et de lui appliquer une fonction qui retourne un vecteur simulé selon une \(U_{[0,1]}\). Un vecteur est donc considéré ici comme une liste où chaque élément du vecteur serait un élement d’une liste.
<- lapply(1:5, function(x) runif(10)) res
Remarque : dans le deuxième argument, la fonction qu’on va appliquer sur chaque élément d’une vecteur prend comme argument d’entrée x (un élément du vecteur), mais on ne l’utilise pas à l’intérieur de la fonction, car on n’en a pas besoin pour faire ce qu’on souhaite faire.
- La troisième solution consiste à utiliser la fonction replicate(), qui comme la fonction sapply() va retourner le résultat sous une forme plus simplifiée qu’une list :
<- replicate(5, runif(10)) res
Remarque : la fonction replicate() fait précisément appel à la fonction sapply(). Pour cela, elle créé un vecteur de 0L de taille le premier argument de replicate().
7.6 La fonction mapply()
Pour illustrer cette fonction, on considère 5 échantillons de taille 10 issus d’une loi \(U_{[0,1]}\). Pour calculer la moyenne par échantilon, on peut utiliser la fonction lapply() :
<- replicate(5, runif(10), simplify = FALSE)
xs lapply(xs, mean)
## [[1]]
## [1] 0.4905019
##
## [[2]]
## [1] 0.493022
##
## [[3]]
## [1] 0.4635896
##
## [[4]]
## [1] 0.4134804
##
## [[5]]
## [1] 0.3997965
A présent, supposons qu’on souhaite pondérer les moyennes par un vecteur de poids qui est différent selon chaque échantillon. On créé ici les poids associés à chaque échantillon sous forme d’une liste :
<- replicate(5, rpois(10, 5) + 1, simplify = FALSE) ws
Une façon de faire avec des boucles serait la suivante :
<- numeric(length(xs))
res for (k in seq_along(res))
<- sum(xs[[k]] * ws[[k]]) / sum(ws[[k]])
res[k] res
## [1] 0.5105359 0.4561600 0.5012219 0.4297210 0.3654479
Le problème de la fonction lapply() est qu’on ne peut pas appliquer une fonction sur deux list simultanément. C’est ce que fait la fonction mapply() qui prend comme 1er argument d’entrée une fonction à appliquer sur les éléments d’autant de list que l’on souhaite. La fonction qu’on applique contient donc deux arguments d’entrée x et y qui correspondent aux éléments de chacune des deux listes :
mapply(function(x, y) sum(x * y) / mean(y), xs, ws)
## [1] 5.105359 4.561600 5.012219 4.297210 3.654479
7.7 La fonction tapply()
On considère les deux vecteurs suivants :
<- c(rnorm(5, 165, 10), rnorm(5, 175, 10))) (taille
## [1] 177.2044 181.6881 167.4184 179.6390 170.8696 161.6642 184.5007 181.4993
## [9] 171.5086 172.9919
<- rep(c("M", "F"), each = 5)) (sexe
## [1] "M" "M" "M" "M" "M" "F" "F" "F" "F" "F"
On souhaite calculer la moyenne dans chaque sous-groupe “M” et “F”. Une façon de faire est de créer une list contenant deux éléments, le premier correspondant au vecteur de taille du sous-groupe “M” et le second correspondant au vecteur de taille du sous-groupe “F”. C’est ce que fait la fonction split() ci-dessous :
<- split(taille, sexe)) (res.split
## $F
## [1] 161.6642 184.5007 181.4993 171.5086 172.9919
##
## $M
## [1] 177.2044 181.6881 167.4184 179.6390 170.8696
Ensuite, il suffit d’appliquer la fonction sapply() à cette list :
sapply(res.split, mean)
## F M
## 174.4329 175.3639
La fonction tapply() permet de faire ce calcul en une seule ligne :
tapply(taille, sexe, mean)
## F M
## 174.4329 175.3639
Remarque : la fonction tapply() ne s’appliquant que sur des vecteurs, la fonction by() permet de généraliser la fonction tapply() aux data.frame.
Exercise 2.5
Créer deux fonctions mean_rnorm.1() et mean_rnorm.2() qui prennent en entrée les paramètres n et p. Ces fonctions permettent de simuler n échantillons de taille p de lois normales centrées réduites et retournent pour chaque échantillon la moyenne. mean_rnorm.1() utilisera des boucles, mean_rnorm.2() la fonction replicate().
Comparer les temps de calcul de ces 2 fonctions.
8 Améliorer ses fonctions
8.1 Créer des sous-fonctions
Il ne faut pas hésiter à créer des petites fonctions qui peuvent être
locales si elles ne sont utilisées qu’à l’intérieur d’une seule fonction principale
globales si elles ont à vocation d’être uilisées dans plusieurs fonctions.
Supposons par exemple qu’on souhaite calculer une densité non paramétrique de la densité en utilisant un des noyaux suivants :
noyau biweight \(K(x) = \frac{15}{16}(1-(\frac{x}{h})^2)^21_{(\frac{x}{h})^2\leq 1}\)
noyau triweight \(K(x) = \frac{35}{32}(1-(\frac{x}{h})^2)^31_{(\frac{x}{h})^2\leq 1}\)
noyau gaussien \(K(x) = \frac{1}{\sqrt{2\pi}}\exp(-0.5(\frac{x}{h})^2))\)
où h est le paramètre de lissage. Dans un premier temps, pn peut alors programmer une fonction par noyau :
<- function(x, h) {
biweight return(15 / 16 * (1 - (x / h) ^ 2) ^ 2 * ifelse((x / h) ^ 2 <= 1, 1, 0))
}<- function(x, h) {
triweight return(35 / 32*(1 - (x / h) ^ 2) ^ 3 * ifelse((x / h) ^ 2 <= 1, 1, 0))
}<- function(x, h) {
gaussian return(1 / sqrt(2 * pi) * exp(-0.5 * (x / h) ^ 2))
}
8.2 Structure de contrôle
Les structures de contrôle classiques (for, while, repeat, if/else) sont bien entendu disponibles dans R (voir les document “Pour se donner un peu d’R” et “Introduction à R”. Nous nous intéressons ici à la notion d’aiguillage (fonction switch()) moins couramment utilisée bien que très pratique.
Celle-ci permet d’éviter d’emboîter des if/else lorsqu’on a plusieurs options possibles. Supposons par exemple qu’on souhaite calculer une densité non paramétrique de la densité en utilisant un des noyaux suivants :
noyau biweight \(K(x) = \frac{15}{16}(1-(\frac{x}{h})^2)^21_{(\frac{x}{h})^2\leq 1}\)
noyau triweight \(K(x) = \frac{35}{32}(1-(\frac{x}{h})^2)^31_{(\frac{x}{h})^2\leq 1}\)
noyau gaussien \(K(x) = \frac{1}{\sqrt{2\pi}}\exp(-0.5(\frac{x}{h})^2))\)
où h est le paramètre de lissage. On peut alors programmer sous R la fonction f_noyau() suivante, prenant en entrée les arguments x, h et type où type est un caractère prenant les valeurs “bi”, “tri” et “gauss”.
8.2.1 Solution avec if/else
La façon traditionnelle pour gérer différents cas de figure est d’utiliser les conditions if/else :
<- function(x, h, type = "bi") {
f_noyau if (type == "bi") {
biweight(x, h)
else {
} if (type == "tri") {
triweight(x, h)
else {
} gaussian(x, h)
}
} }
Un inconvénient est qu’en emboîtant des conditions if/else, on peut alors vite se perdre dans la lecture du code.
8.2.2 Solution avec switch()
La fonction switch() s’applique sur l’argument type et on donne ensuite pour chaque valeur possible de type le type de calcul à effectuer. On ajoute ici un cas qui correspondrait à toutes autres valeurs de type.
.2 <- function(x, h, type = "bi") {
f_noyauswitch(type, bi = biweight(x, h),
tri = triweight(x, h),
gauss = gaussian(x, h),
"Préciser une autre valeur de type")
}
Application :
<- seq(-1, 1, 0.01)
x plot(x, f_noyau.2(x, 0.3, type = "bi"), type = "l", ylab = "", ylim = c(0, 1.2))
lines(x, f_noyau.2(x, 0.3, type = "tri"), lty = 2)
lines(x, f_noyau.2(x, 0.3, type = "gauss"), lty = 3)
f_noyau.2(x, 0.3, type = "unif")
## [1] "Préciser une autre valeur de type"
8.3 Fonction stopifnot()
On connaît la fonction stop() qui s’insère en général à l’intérieur des conditions if/else.
La fonction stopifnot() permet de tester simultanément plusieurs conditions et évite donc d’avoir recours aux if/else. Elle est particulièrement utile à l’intérieur d’une fonction pour, par exemple, vérifier la conformité des valeurs passées en paramètres. Un exemple d’uitlisation :
stopifnot(1 < 2, length(1:2) == 2, pi < 2, cos(pi) > 3)
## Error: pi < 2 n'est pas TRUE
Si plusieurs conditions ne sont pas respectées, c’est la première non respectée qui est renvoyée comme source de l’erreur.
Exercise 2.6
Modifier la fonction f_noyau.2() définie précédemment pour vérifier que :
le paramètre x est numeric
h est de taille 1 et de type numeric
type est une chaîne de caractères (on supprimera la possibilité de passer un entier pour choisir le type).
8.4 Les arguments dans une fonction
8.4.1 Supprimer les paramètres inutiles
Dans l’exemple ci-dessous, les paramètres d et e ne sont pas utilisés dans la fonction et pourtant ils ont un coût de stockage et un coût en temps de calcul car ils sont évalués par défaut dans la fonction.
<- function(a = 5, b = 4, d = 3, e = 1)
f_1 + b)^2 (a
On compare les temps de calcul avec la même fonction mais qui n’utilise pas les paramètres inutilisés :
<- function(a = 5, b = 4)
f_2 + b)^2 (a
::microbenchmark(
microbenchmarkf_1(),
f_2()
)
## Unit: nanoseconds
## expr min lq mean median uq max neval
## f_1() 307 318.5 12118.81 337.5 356 1173445 100
## f_2() 243 253.0 11388.95 269.5 285 1109342 100
Contrairement à d’autres langages, R ne signale pas les paramètres qui ne sont pas utiles, qu’il s’agisse de paramètres utilisés en argument d’entrée ou de paramètres créés à l’intérieur de la fonction. C’est donc à celui qui programme d’être vigilent à ne pas créer des paramètres inutiles.
8.4.2 Utiliser des fonctions comme argument d’entrée
On a vu dans la section précédente que la fonction lapply() avait dans ses argument d’entrée une fonction. On peut si on le souhaite créer nos propres fonctions qui ont comme argument d’entrée une fonction. Dans l’exemple suivant, on applique une fonction choisie par l’utilisateur sur un échantillon simulée selon une loi uniforme :
<- function(f) f(runif(1e3)) randomise
Pour l’utiliser, on remplace l’argument f par le nom de la fonction qui nous intéresse :
randomise(mean)
## [1] 0.4929316
randomise(mean)
## [1] 0.4926691
randomise(sum)
## [1] 486.2975
8.4.3 Utiliser des fonctions comme argument de sortie
Il est également possible de faire retourner une fonction par une fonction. C’est ce que fait la fonction f_power() ci-dessous :
<- function(exponent)
f_power function(x) x^exponent
Pour utiliser cette fonction :
f_power(2)(1:5)
## [1] 1 4 9 16 25
f_power(3)(1:5)
## [1] 1 8 27 64 125
On peut également créer les fonctions f_square() et f_cube() à partir de la fonction f_power() :
<- f_power(2)
f_square <- f_power(3)
f_cube class(f_square)
## [1] "function"
class(f_cube)
## [1] "function"
Et pour obtenir le calcul souhaité :
f_square(1:5)
## [1] 1 4 9 16 25
f_cube(1:5)
## [1] 1 8 27 64 125
8.4.4 Utiliser les … comme argument d’entrée
Lorsqu’un utilisateur définit une fonction, il peut permettre à sa fonction d’utiliser toutes les options d’une autre fonction sans les lister une à une. Prenons l’exemple de la fonction plot_reg() définie ci-dessous.
<- function(x, y, np = TRUE, ...) {
plot_reg plot(y ~ x, ...)
abline(lm(y ~ x), col = "blue")
if (np) {
<- loess(y ~ x)
np.reg <- seq(min(x), max(x), length.out = 25)
x.seq lines(x.seq, predict(np.reg, x.seq), col = "red")
} }
L’utilisation de la syntaxe … permet de préciser que la fonction plot_reg() pourra, si besoin, faire appel à n’importe quelle option de la fonction plot().
Pour illustrer la chose, exécuter les instructions suivantes.
<- rnorm(100)
x <- x*runif(100) y
plot_reg(x, y, np = FALSE)
plot_reg(x, y, pch = 16, col = "pink",
xlab = "variable explicative", ylab = "variable à expliquer")
Exercice 2.7
Q1 Que fait la fonction plot_reg() ?
Q2 Ecrire une fonction hist_extrm() qui prend en argument d’entrée un entier n, un entier B et … qui correspondra aux options utilisées dans la fonction hist(). Cette fonction devra mettre en oeuvre l’algorithme ci-dessous :
Répèter B fois l’opération suivante :
- simulation d’un vecteur x de taille n selon une \(N(0,1)\),
Résultat On comptabilise sur les B boucles le pourcentages de boucles où il existe au moins un élément de x supérieur en valeur absolue à 1.96. On stockera les valeurs concernées et on représentera l’histogramme des valeurs extrêmes.
9 A quoi servent les fonctions substitute()/quote() et eval() ?
On considère l’exemple ci-dessous :
<- 5
a identical(a, "a")
## [1] FALSE
a et “a” sont bien entendu deux entités différentes. a est un objet qui pointe sur la valeur 5 alors que “a” est une chaîne de caractère.
Losqu’on charge une librairie, on a la possibilité d’utiliser les deux commandes suivantes :
require("tidyverse")
require(tidyverse)
Dans le deuxième cas, tidyverse (sans guillemet) n’est a priori pas un objet (à moins qu’au cours d’une session on ait créé un objet avec ce nom). C’est donc qu’il a été interprété comme étant quelque chose de différent d’un objet.
9.1 Les fonctions quote() et substitute()
Les fonctions quote() et substitute() permettent de capturer une instruction sans l’évaluer. Pour généraliser, on va appeler cette capture une expression. La différence entre eval() et substitute() est décrite dans le chapitre Non-standard evaluation de l’ouvrage d’Hadley Wickham, mais pour résumer quote() ne fait que retourner l’expression qu’on lui donne en entrée alors que substitute est un peu plus complexe.
quote(1:10)
## 1:10
quote(f(1:10))
## f(1:10)
quote(f(x))
## f(x)
quote(f(x + y^2))
## f(x + y^2)
substitute(1:10)
## 1:10
substitute(f(1:10))
## f(1:10)
substitute(f(x))
## f(x)
substitute(f(x + y^2))
## f(x + y^2)
Une fois qu’on a fait cette capture de code, le but est d’évaluer cette expression dans un environnement spécifique et au moment où on le souhaitera avec la fonction eval().
9.2 La fonction eval()
Cette fonction permet d’évaluer une expression. Par défaut, elle est évaluée dans l’environnement courant de R. Autrement dit, si on considère l’expression suivante :
<- quote(f.eval(x.eval)) a
Si on veut évaluer cette expression, il faut que la fonction f.eval() et l’objet x.eval existent tous les deux dans l’environnement où on souhaite exécuter cette expression. Par défaut, eval() ira chercher dans l’environnement courant. Ici, on obtient le message d’erreur suivant car la fonction f.eval() et l’objet x.eval n’ont pas été définies au préalables :
eval(a)
## Error in f.eval(x.eval): impossible de trouver la fonction "f.eval"
On considère un autre exemple avec l’expression suivante :
<- quote(Species == "setosa") a
La commande suivante donnera un message d’erreur :
eval(a)
## Error in eval(a): objet 'Species' introuvable
En revanche, on peut préciser qu’on va trouver le nom Species dans l’environnement du data.frame nommé iris :
eval(a, envir = iris)
## [1] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
## [13] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
## [25] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
## [37] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
## [49] TRUE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [61] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [73] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [85] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [97] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [109] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [121] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [133] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [145] FALSE FALSE FALSE FALSE FALSE FALSE
9.2.1 Et en pratique ?
Elles sont utilisées dans beaucoup de fonction sans qu’on s’en apperçoive nécessairement. On va voir quelques fonctions qui utilisent ces expressions :
- dans la fonction subset(), le deuxième argument est considérée comme une expression :
subset(iris, Species == "setosa" & Sepal.Length > 5.5)
## Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 15 5.8 4.0 1.2 0.2 setosa
## 16 5.7 4.4 1.5 0.4 setosa
## 19 5.7 3.8 1.7 0.3 setosa
Ainsi, l’expression Species == “setosa” & Sepal.Length > 5.5 sera évaluée dans le jeu de données iris ce qui permet d’éviter de faire :
$Species == "setosa" & iris$Sepal.Length > 5.5, ] iris[iris
## Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 15 5.8 4.0 1.2 0.2 setosa
## 16 5.7 4.4 1.5 0.4 setosa
## 19 5.7 3.8 1.7 0.3 setosa
les différentes fonctions filter(), arrange(), etc. du package dplyr présentées dans le chapitre précédent utilisent le même genre de syntaxe que celui de la fonction subset().
la fonction plot.defaut() qui représente un nuage de points :
plot(1:10, rnorm(10))
Les arguments par défaut pour représenter la légende en abscisse et en ordonnées (xlab et ylab) correspondent aux expressions des paramètres x et y données en entrée.
9.3 Débugger une fonction
9.3.1 Analyser le message d’erreur
Lorsqu’un message d’erreur (ou d’avertissement) apparaît en rouge dans la console, la première chose à faire est d’essayer de le comprendre. La plupart du temps, le message est suffisament implicite pour qu’on puisse trouver d’où provient le problème.
sum(c("a", "b"))
## Error in sum(c("a", "b")): 'type' (character) de l'argument incorrect
Dans cet exemple, le message d’erreur n’intervient pas dans le coeur de la fonction, mais dans l’appel des arguments. Lorsque l’erreur est dûe à l’exécution d’une commande à l’intérieur d’une fonction, il est parfois plus difficile d’identifier d’où vient le problème.
9.3.2 La fonction traceback()
Le principe de la fonction traceback() est le suivant : si un message d’erreur est apparu à cause de l’appel d’une commande, l’historique qui a conduit à l’appel de cette commande sera affiché.
Ici, on créé une fonction ex_bug() à partir des fonctions mean_rnorm.1() et mean_rnorm.2() créées précédemment, dans laquelle on a glissé une erreur.
<- function(n, p) {
ex_bug <- as.character(n)
n2 length(which(mean_rnorm.1(n2, p) > mean_rnorm.2(n, p)))
}
On obtient le message d’erreur suivant :
ex_bug(5, 10)
## Error in mean_rnorm.1(n2, p): impossible de trouver la fonction "mean_rnorm.1"
On appelle la fonction traceback() dans la foulée du message d’erreur :
traceback()
4: matrix(0, n, 1) at #2
3: mean_rnorm.1(n2, p) at #3
2: which(mean_rnorm.1(n2, p) > mean_rnorm.2(n, p)) at #3
1: ex_bug(5, 10)
Comment interpéter le résultat de la fonction traceback(). Le message d’erreur est intervenu au moment d’appeler la commande de la ligne 4. Cette commande a été appelée à la suite de l’appel de la commande de la ligne 3. La commande de la ligne 3 a été appelée suite à la commande de la ligne 2. La ligne 1 correspond bien entend à l’appel de la fonction de départ ex_bug().
9.3.3 La fonction debugonce()
La fonction debugonce() a pour objectif de rentrer dans le coeur de la fonction considérée et d’exécuter les lignes de code l’une après l’autre. Après chaque exécution, il est possible d’utiliser la fonction ls(), print(), etc. pour connaître le résultat des étapes intermédiaires. On n’est pas obligé d’utiliser cette fonction uniquement après l’affichage d’un message d’erreur. Parfois, on n’a pas de message d’erreur, mais le résultat retourné n’est pas celui attendu et il peut s’agir d’un problème de programmation. Dans ce cas là, l’idée est d’exécuter son code ligne après ligne et de vérifier les étapes intermédiaires.
On considère la fonction suivante :
.2 <- function(x){
ex_bug<- x + 1
x <- x^3
x <- log(x)
x
x }
Pour utiliser la fonction debugonce() :
debugonce(ex_bug.2)
ex_bug.2(-5)
En exécutant pas à pas la fonction ex_bug.2(-5), on peut demander à chaque étape la valeur que prend la variable x en cours d’exécution ; il faut pour cela saisir la variable x devant le prompt du débugger (Browse[2]>).
9.3.4 Négliger un message d’erreur
Une erreur entraîne l’interruption automatique du processus en cours d’execution. Parfois, on peut avoir une erreur, mais on ne souhaite pas que cela interrompe le processus.
9.3.4.1 La fonction try()
Si on connaît l’instruction qui peut entraîner une erreur, il est possible de lui appliquer la fonction try() :
.1 <- function(x) {
f_errortry(x <- log(x))
x }
Ci-dessous, on constate que le message d’erreur est affiché, mais cela n’a pas interrompu la fonction.
f_error.1("10")
## Error in log(x) : argument non numérique pour une fonction mathématique
## [1] "10"
Bibliographie : pour plus de d’informations sur le débuggage, on renvoie le lecteur au chapitre “Debugging, condition handling, and defensive programming” du livre “Advanced R” d’Hadley Wickham.
10 Programmation orientée : S3/S4
10.1 La norme S3
Nous nous sommes inspirés du document suivant pour décrire la programmation orientée qui utilise la norme S3 : http://www.duclert.org/r-divers/classes-S3-R.php. Pour expliquer son principe, nous allons prende un exemple concret.
On voudrait calculer l’aire de différentes figures géométriques :
- un carré, dont il nous faut connaître la longueur d’un côté, par exemple :
<- 3 squ
- un rectangle, défini par sa longueur et sa largeur, par exemple :
<- c(5, 6) rec
- un cercle, caractérisé par son rayon, par exemple :
<- sqrt(10) cir
Pour ce faire, on souhaiterait utiliser une fonction qui s’appelle getArea() et qui puisse reconaître quand on l’applique à un carré, un rectangle ou un cercle afin d’utiliser la bonne formule. Ceci consiste à définir une méthode générique getArea qui renvoie à la bonne implémentation en fonction de la classe de l’argument d’entrée. C’est ce qu’on appelle le polymorphisme.
10.1.1 Définition d’une nouvelle classe
Pour pouvoir être identifié comme étant un objet particulier, les trois objets ci-dessus doivent être définis comme appartenant à une certaine classe, que l’utilisateur, dans la norme S3, peut lui-même créer. On définit ainsi ci-dessous les trois types de classe carre, rectangle et cercle en utilisant la fonction class() :
class(cir) <- "cercle"
cir
## [1] 3.162278
## attr(,"class")
## [1] "cercle"
class(rec) <- "rectangle"
rec
## [1] 5 6
## attr(,"class")
## [1] "rectangle"
class(squ) <- "carre"
squ
## [1] 3
## attr(,"class")
## [1] "carre"
Remarque : à ce stade, nous n’avons créé aucune fonction qui puisse s’appliquer spécifiquement à ces objets. Cependant, comme ils ont été construits à la base comme des vecteurs, on peut toujours appliquer toutes les fonctions qui s’appliquent à des vecteurs de numeric.
Par ailleurs, pour calculer l’aire de ces objets, on pourrait écrire la fonction suivante qui traite le cas de chaque classe:
<- function(x) {
area switch(class(x),
carre = x ^ 2,
rectangle = x[1] * x[2],
cercle = pi * x ^ 2,
"class should be among carre/rec/cercle")
}
Cependant, si on considère une nouvelle classe d’objet (un triangle par exemple), on devra reprogrammer la fonction area().
C’est pourquoi on définit une méthode générique afin de de pouvoir greffer autant de nouvelles classes d’objet sur celle-ci (par exemple, plot est une méthode sur laquelle on peut appliquer différents types d’objets).
10.1.2 Définition d’une méthode générique
On va créer la fonction getArea() qui va s’appliquer à ces classes d’objet en 3 étapes : getArea sera définie comme étant une méthode générique.
- La première étape consiste à définir la fonction getArea() comme étant une méthode générique. Autrement dit, on signale à R que cette fonction sera ensuite implémentée sous différentes versions :
<- function(obj)
getArea UseMethod("getArea", obj)
- La deuxième étape consiste à créer une fonction par défaut qui sera appliquée dans le cas où on appliquerait à la fonction getArea() une classe d’objet autre que carre, rectangle ou cercle.
<- function(obj) {
getArea.default stop("Méthode getArea non définie pour ce type d'objet")
}
- Enfin, on va créer les 3 “sous” fonctions de getArea() en ajoutant simplement à getArea le suffixe .cercle, .rectangle et .carre :
<- function(obj) {
getArea.cercle * obj[1] ^ 2
pi
}
<- function(obj) {
getArea.rectangle 1] * obj[2]
obj[
}
<- function(obj) {
getArea.carre 1]^2
obj[ }
Voici un exemple d’application d’utilisation de la méthode getArea qu’on vient de créer :
getArea(cir)
## [1] 31.41593
getArea(rec)
## [1] 30
getArea(squ)
## [1] 9
Si on applique la méthode getArea à une classe d’objet non défini :
<- 5
a getArea(a)
## Error in getArea.default(a): Méthode getArea non définie pour ce type d'objet
Exercice 2.8
Ajouter à la méthode getArea, la classe triangle
10.1.3 Exemple de méthodes génériques
Il existe de nombreuses méthodes sous R qui sont déclarées comme génériques dans la norme S3. Par exemple, lorsqu’on utilise la fonction plot() ci-dessous, on l’applique dans le premier cas sur deux vecteurs numeric et dans le deuxième cas, on l’applique sur le résultat de la fonction lm() :
plot(rnorm(10), runif(10))
plot(lm(Sepal.Length ~ Sepal.Width, data = iris))
De même, pour savoir si un objet appartient à la norme S3, ce qui sous-entend que des méthodes génériques pourront s’appliquer sur cet objet, on peut utiliser la fonction otype() du package pryr :
library("pryr")
<- data.frame(x = 1:10, y = letters[1:10])
df ::otype(df) pryr
## [1] "S3"
On constate qu’un data.frame appartient à la norme S3, ce qui sous entend qu’un nombre de méthodes génériques pourront s’appliquer dessus. La plupart du temps, ces objets qui appartiennent à la norme S3 sont créés à partir de list. Pour savoir comment ils sont constitués, on peut donc simplement utiliser la fonction str().
10.1.3.1 Pour connaître les objets qui s’appliquent sur une méthode générique
On utilise la fonction methods() appliquée au nom de la méthode. Par exemple :
methods("plot")
## [1] plot,ANY-method plot,color-method plot.acf*
## [4] plot.data.frame* plot.decomposed.ts* plot.default
## [7] plot.dendrogram* plot.density* plot.ecdf
## [10] plot.factor* plot.formula* plot.function
## [13] plot.ggplot* plot.gtable* plot.hcl_palettes*
## [16] plot.hclust* plot.histogram* plot.HoltWinters*
## [19] plot.isoreg* plot.lm* plot.medpolish*
## [22] plot.mlm* plot.numpy.ndarray* plot.ppr*
## [25] plot.prcomp* plot.princomp* plot.profile.nls*
## [28] plot.R6* plot.raster* plot.shingle*
## [31] plot.spec* plot.stepfun plot.stl*
## [34] plot.table* plot.trans* plot.trellis*
## [37] plot.ts plot.tskernel* plot.TukeyHSD*
## see '?methods' for accessing help and source code
10.1.3.2 Pour connaître les méthodes qui s’appliquent sur une classe d’objet
On utilise la fonction methods() en précisant l’argument class =. Par exemple :
methods(class = "lm")
## [1] add1 alias anova case.names coerce
## [6] confint cooks.distance deviance dfbeta dfbetas
## [11] drop1 dummy.coef effects extractAIC family
## [16] formula fortify hatvalues influence initialize
## [21] kappa labels logLik model.frame model.matrix
## [26] nobs plot predict print proj
## [31] qr residuals rstandard rstudent show
## [36] simulate slotsFromS3 summary variable.names vcov
## see '?methods' for accessing help and source code
Pour en savoir plus : le lecteur pourra consulter la dernière partie du lien suivant qui donne un exemple de création de méthode générique plus complet, avec la possibilité de définir les commandes +, [], etc. sur de nouvelles classes d’objet : http://www.duclert.org/r-divers/classes-S3-R.php.
10.2 La norme S4
Ici, on présentera uniquement les grandes lignes de la norme S4. Cette section est inspirée du chapitre OO field guide du livre d’Hadley Wickham.
La norme S4 s’inspire de la norme S3 auxquelles s’ajoutent certaines caractéristiques telles que :
- les classes sont définies en ajoutant un certain nombre de règles et de précisions les concernant. Pour mieux comprendre, dans l’exemple ci-dessous, on peut définir l’objet a comme appartenant à la classe lm dans la norme S3, alors qu’il n’en possède pas les caractéristiques :
<- 5
a class(a) <- "lm"
- on utilisera un opérateur spécial @ pour extraire des éléments d’un objet ayant la norme S4.
Pour savoir si un objet appartient à la norme S4, on peut utiliser la fonction isS4(). La plupart du temps, la norme S4 n’a pas été utilisée pour définir des objets et fonctions qui appartiennant à l’environnement de base. Ils sont donc plutôt présents dans des nouveaux packages qu’on va charger.
Les principales fonctions utilisées pour travailler sur la norme S4 sont :
- la fonction setClass() qui permet de définir une nouvelle classe. Le 1er argument est le nom de la classe, le second est le résultat de la fonction representation() qui définit le nom et le type des éléments que la classe contient et le troisième argument est le résultat de la fonction prototype() qui donne des valeurs par défaut. Par exemple :
setClass("Personne", representation(nom = "character", age = "numeric"),
prototype(nom = NA_character_, age = NA_real_))
- la fonction new() permet de créer un nouvel objet.
<- new("Personne", nom = "Hadley", age = 31) hadley
Remarque : on accède aux éléments d’un objet de norme S4 avec la commande spéciale @ :
@age hadley
## [1] 31
Les autres fonctions utiles dont vous trouverez des exemples d’utilisation dans http://adv-r.had.co.nz/S4.html sont :
la fonction setMethods() permettant de définir des nouvelles méthodes.
les fonctions as() et setAs() pour passer d’une classe d’objet à une autre (quand cela est possible).
les fonctions setValidity() et validObject() pour vérifier la validité.
les fonctions showClass(), showMethods() et getMethod() pour accéder aux propriétés des objets et méthodes créées.
Bilbiographie : la présentation de F. Leisch à useR! 2004 (http://www.ci.tuwien.ac.at/Conferences/useR-2004/Keynotes/Leisch.pdf) ainsi que le manuel de C. Genolini (https://cran.r-project.org/doc/contrib/Genolini-PetitManuelDeS4.pdf).
11 Visualiser le code source d’une fonction
Nous nous sommes inspirés du document suivant pour écrire cette section : “Visualiser le code d’une fonction”.
11.1 La fonction est dans l’environnement courant
Vous tapez le nom de la fonction dans la console et le code apparaît :
sapply
## function (X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)
## {
## FUN <- match.fun(FUN)
## answer <- lapply(X = X, FUN = FUN, ...)
## if (USE.NAMES && is.character(X) && is.null(names(answer)))
## names(answer) <- X
## if (!isFALSE(simplify))
## simplify2array(answer, higher = (simplify == "array"))
## else answer
## }
## <bytecode: 0x55590c947910>
## <environment: namespace:base>
11.2 La fonction est une méthode générique de type S3
Vous tapez le nom d’une fonction lambda() et vous obtenez une ligne UseMethod(“lambda”). Ceci signifie que lambda est une méthode générique pour des objets issus de la classe S3. Il existe donc plusieurs fonctions associées à la fonction lambda(). Pour connaître quelles sont les différentes fonctions associées à la fonction lambda(), on utilise la fonction methods(). Par exemple, pour la fonction summary(), si on tape le nom de la fonction dans la console, on obtient :
summary
## function (object, ...)
## UseMethod("summary")
## <bytecode: 0x55590df4b3a8>
## <environment: namespace:base>
Pour obtenir les différentes fonctions associées à summary(), on utilise donc la fonction methods().
methods("summary")
## [1] summary,ANY-method summary,DBIObject-method
## [3] summary,diagonalMatrix-method summary,sparseMatrix-method
## [5] summary.aov summary.aovlist*
## [7] summary.aspell* summary.check_packages_in_dir*
## [9] summary.connection summary.data.frame
## [11] summary.Date summary.default
## [13] summary.ecdf* summary.factor
## [15] summary.ggplot* summary.glm
## [17] summary.hcl_palettes* summary.infl*
## [19] summary.lm summary.loess*
## [21] summary.manova summary.matrix
## [23] summary.microbenchmark* summary.mlm*
## [25] summary.nls* summary.packageStatus*
## [27] summary.pandas.core.frame.DataFrame* summary.pandas.core.series.Series*
## [29] summary.POSIXct summary.POSIXlt
## [31] summary.ppr* summary.prcomp*
## [33] summary.princomp* summary.proc_time
## [35] summary.python.builtin.object* summary.rlang_error*
## [37] summary.rlang_trace* summary.shingle*
## [39] summary.srcfile summary.srcref
## [41] summary.stepfun summary.stl*
## [43] summary.table summary.trellis*
## [45] summary.tukeysmooth* summary.vctrs_sclr*
## [47] summary.vctrs_vctr* summary.warnings
## see '?methods' for accessing help and source code
Pour lire le code des fonctions affichées ci-dessus, il y a deux options :
- si il n’y pas d’étoile à côté de la fonction, vous pouvez obtenir directement le code de la fonction en tapant son nom complet :
summary.proc_time
## function (object, ...)
## {
## if (!is.na(object[4L]))
## object[1L] <- object[1L] + object[4L]
## if (!is.na(object[5L]))
## object[2L] <- object[2L] + object[5L]
## object <- object[1L:3L]
## names(object) <- c(gettext("user"), gettext("system"), gettext("elapsed"))
## object
## }
## <bytecode: 0x55590df4b808>
## <environment: namespace:base>
- s’il y a une étoile, c’est que la fonction se trouve dans un package et qu’on doit indiquer le nom du package suivi de ::: :
:::summary.ecdf stats
Si on ne connaît pas le nom du package, on peut alors utiliser la fonction getAnywhere() qui va chercher dans toutes les librairies chargées :
getAnywhere(summary.ecdf)
## A single object matching 'summary.ecdf' was found
## It was found in the following places
## registered S3 method for summary from namespace stats
## namespace:stats
## with value
##
## function (object, ...)
## {
## n <- length(eval(expression(x), envir = environment(object)))
## header <- paste("Empirical CDF:\t ", n, "unique values with summary\n")
## structure(summary(knots(object), ...), header = header, class = "summary.ecdf")
## }
## <bytecode: 0x5559119d7b88>
## <environment: namespace:stats>
11.3 La fonction fait appel à du code C
Si on affiche le code la fonction sum(), on constate que le calcul n’est pas directement fait dans le corps de la fonction. C’est ce que nous avons vu précédemment, à savoir qu’il existe un grand nombre de fonctions qui font appel à du code compilé pour gagner en performances. Lorsqu’on voit dans une fonction les instructions .Primitive() ou .Internal(), cela signifie que R va en réalité exécuter des fonctions qui ont été compilées en C. La différence entre .Primitive() ou .Internal() vient du fait que dans le deuxième cas, on peut appeler des fonctions qui sont codées en langage R.
sum
## function (..., na.rm = FALSE) .Primitive("sum")
On peut donc faire appel à du code C de plusieurs façons. On verra ici les deux principales façons de le faire.
11.3.1 .Primitive() ou .Internal()
Il est difficile d’accéder directement au code source de la fonction exécutée par .Primitive() ou .Internal(). Il s’agit des fonctions qui font en général partie du package base. On peut utiliser la fonction show_c_source() du package pryr qui va effectuer une recherche sur github où a été déposé le source de l’ensemble des fichiers de codes pré-compilés utilisés dans R :
::show_c_source(.Internal(mean(x))) pryr
11.3.2 .Call()
Dans d’autres situtations, une fonction fait appel à du code C via la fonction .Call(). Par exemple :
qnorm
## function (p, mean = 0, sd = 1, lower.tail = TRUE, log.p = FALSE)
## .Call(C_qnorm, p, mean, sd, lower.tail, log.p)
## <bytecode: 0x555914856098>
## <environment: namespace:stats>
A priori, le code source est disponible dans les répertoires nommés src : soit dans R, soit dans les packages installés (dans les deux cas, les codes sources doivent donc être accessibles depuis les répertoires). Sinon, on peut faire une reherche en ligne en tapant dans le moteur de recherche les instructions suivantes :
site:https://svn.r-project.org/R/trunk/src qnorm
Biliographie : pour en savoir plus sur le langage compilé, on pourra lire le chapitre R’s C interface du livre d’Hadley Wickham, “Advanced R”.
11.4 La fonction est une méthode générique de type S4
C’est le cas des fonctions programmées dans le package Matrix.
require("Matrix")
Si on utiliser la fonction methods() sur la méthode dim, pour identifier les méthodes génériques qui s’appliquent à des objets S4, on constate que le nom de la fonction est suivi d’une virgule, suivi alors de la classe de l’objet sur laquelle s’applique la méthode. Dans le cas de dim(), elle peut s’appliquer sur les classes d’objet MatrixFactorization ou Matrix.
methods("dim")
## [1] dim,Matrix-method dim,MatrixFactorization-method
## [3] dim.data.frame dim.gtable*
## [5] dim.layout* dim.pandas.core.frame.DataFrame*
## [7] dim.pandas.core.series.Series* dim.scipy.sparse.base.spmatrix*
## [9] dim.trellis*
## see '?methods' for accessing help and source code
Pour obtenir le code source de la fonction dim() qui s’applique à la classe d’objet MatrixFactorization, on utilise la fonction getMethod() ainsi :
getMethod("dim", "MatrixFactorization")
## Method Definition:
##
## function (x)
## x@Dim
## <bytecode: 0x55590f4ed988>
## <environment: namespace:Matrix>
##
## Signatures:
## x
## target "MatrixFactorization"
## defined "MatrixFactorization"
Pour connaître les méthodes S4 qui s’appliquent sur une classe d’objets particuliers, on utilise la fonction showMethods() :
showMethods(class = "MatrixFactorization")
##
## Function "asJSON":
## <not an S4 generic function>
##
## Function "complete":
## <not an S4 generic function>
##
## Function "coords":
## <not an S4 generic function>
##
## Function "dbAppendTable":
## <not an S4 generic function>
##
## Function "dbBegin":
## <not an S4 generic function>
##
## Function "dbBind":
## <not an S4 generic function>
##
## Function "dbCallProc":
## <not an S4 generic function>
##
## Function "dbCanConnect":
## <not an S4 generic function>
##
## Function "dbClearResult":
## <not an S4 generic function>
##
## Function "dbColumnInfo":
## <not an S4 generic function>
##
## Function "dbCommit":
## <not an S4 generic function>
##
## Function "dbConnect":
## <not an S4 generic function>
##
## Function "dbCreateTable":
## <not an S4 generic function>
##
## Function "dbDataType":
## <not an S4 generic function>
##
## Function "dbDisconnect":
## <not an S4 generic function>
##
## Function "dbDriver":
## <not an S4 generic function>
##
## Function "dbExecute":
## <not an S4 generic function>
##
## Function "dbExistsTable":
## <not an S4 generic function>
##
## Function "dbFetch":
## <not an S4 generic function>
##
## Function "dbGetConnectArgs":
## <not an S4 generic function>
##
## Function "dbGetException":
## <not an S4 generic function>
##
## Function "dbGetInfo":
## <not an S4 generic function>
##
## Function "dbGetQuery":
## <not an S4 generic function>
##
## Function "dbGetRowCount":
## <not an S4 generic function>
##
## Function "dbGetRowsAffected":
## <not an S4 generic function>
##
## Function "dbGetStatement":
## <not an S4 generic function>
##
## Function "dbHasCompleted":
## <not an S4 generic function>
##
## Function "dbiDataType":
## <not an S4 generic function>
##
## Function "dbIsReadOnly":
## <not an S4 generic function>
##
## Function "dbIsValid":
## <not an S4 generic function>
##
## Function "dbListConnections":
## <not an S4 generic function>
##
## Function "dbListFields":
## <not an S4 generic function>
##
## Function "dbListObjects":
## <not an S4 generic function>
##
## Function "dbListResults":
## <not an S4 generic function>
##
## Function "dbListTables":
## <not an S4 generic function>
##
## Function "dbQuoteIdentifier":
## <not an S4 generic function>
##
## Function "dbQuoteLiteral":
## <not an S4 generic function>
##
## Function "dbQuoteString":
## <not an S4 generic function>
##
## Function "dbReadTable":
## <not an S4 generic function>
##
## Function "dbRemoveTable":
## <not an S4 generic function>
##
## Function "dbRollback":
## <not an S4 generic function>
##
## Function "dbSendQuery":
## <not an S4 generic function>
##
## Function "dbSendStatement":
## <not an S4 generic function>
##
## Function "dbSetDataMappings":
## <not an S4 generic function>
##
## Function "dbUnloadDriver":
## <not an S4 generic function>
##
## Function "dbUnquoteIdentifier":
## <not an S4 generic function>
##
## Function "dbWithTransaction":
## <not an S4 generic function>
##
## Function "dbWriteTable":
## <not an S4 generic function>
## Function: dim (package base)
## x="MatrixFactorization"
##
## Function: expand (package Matrix)
## x="MatrixFactorization"
##
##
## Function "fetch":
## <not an S4 generic function>
##
## Function "functions":
## <not an S4 generic function>
##
## Function "isSQLKeyword":
## <not an S4 generic function>
##
## Function "make.db.names":
## <not an S4 generic function>
##
## Function "plot":
## <not an S4 generic function>
## Function: show (package methods)
## object="MatrixFactorization"
##
## Function: solve (package base)
## a="MatrixFactorization", b="ANY"
## a="MatrixFactorization", b="missing"
## a="MatrixFactorization", b="numeric"
##
##
## Function "sqlAppendTable":
## <not an S4 generic function>
##
## Function "sqlCreateTable":
## <not an S4 generic function>
##
## Function "sqlData":
## <not an S4 generic function>
##
## Function "sqlInterpolate":
## <not an S4 generic function>
##
## Function "SQLKeywords":
## <not an S4 generic function>
##
## Function "sqlParseVariables":
## <not an S4 generic function>