This is an old revision of the document!


Série d'ateliers en R du CSBQ

Cette série de 10 ateliers guide les participants à travers les étapes requises afin de maîtriser le logiciel R pour une grande variété d’analyses statistiques pertinentes en recherche en biologie et en écologie. Ces ateliers en libre accès ont été créés par des membres du CSBQ à la fois pour les membres du CSBQ et pour la grande communauté d’utilisateurs de R.

Le contenu de cet atelier a été révisé par plusieurs membres du CSBQ. Si vous souhaitez y apporter des modifications, veuillez SVP contacter les coordonnateurs actuels de la série, listés sur la page d'accueil

Atelier 5: Programmation en R

Développé par: Johanna Bradie, Sylvain Christin, Ben Haller, Guillaume Larocque

Résumé: Cet atelier vise à vous apprendre les bases de la programmation en R. Vous apprendrez à utiliser des structures de contrôle (boucles for, if, while) afin d'éviter la répétition de code, de faciliter l'organisation et d'effectuer des simulations. Vous apprendrez également à écrire vos propres fonctions et quelques astuces pour programmer plus efficacement. La dernière partie de l'atelier portera sur des librairies de R qui peuvent être très utiles pour les participants, mais qui ne seront pas couvertes ailleurs dans la série d'ateliers en R du CSBQ.

Lien vers la nouvelle présentation Rmarkdown (en anglais seulement, la version française viendra sous peu!)

S'il vous plaît essayez-la et dites aux coordonnateurs des ateliers R ce que vous en pensez!

Lien vers l'ancienne présentation Prezi

Téléchargez le script R pour cet atelier:

  1. Structures de contrôle
  2. Écriture de fonctions en R
  3. Réduire le temps d’exécution des codes
  4. Paquets utilises pour les biologistes

En programmation, le contrôle de flux est simplement l'ordre dans lequel le programme est exécuté.

Pourquoi est-il avantageux de structurer nos programmes?

  • Réduit la complexité et la durée de la tâche en question
  • Une structure logique améliore la clarté du code
  • Plusieurs programmeurs peuvent aussi travailler sur un même programme

Tout ceci augmente la productivité


On peut utiliser des organigrammes pour planifier nos programmes et leur structure.

Les deux composantes de base de programmation sont:

La sélection

Exécuter des commandes conditionnellement en utilisant:

if
if else

L'itération

Répéter l'exécution d'une commande tant qu'une condition n'est pas satisfaite.

for
while
repeat

Commandes if et if/else

Les commandes if et if/else sont utiles pour:

  • Vérifier s'il y a des problèmes ou le non-respect de conditions.
  • Traiter des sous-ensembles de vos données de façons différentes.
  • Vérifier l'existence d'un fichier ou d'une variable.

Commande if

if(condition) {
  expression
}

Commande if else

if(condition) {
  expression 1
} else {
  expression 2
}

Comment peut-on tester plus qu'une condition?

  • if et if else testent une seule condition
  • On peut aussi utiliser la commande ifelse pour:
    • tester un vecteur de conditions;
    • effectuer une opération seulement selon certaines conditions.

Par exemple,

a <- 1:10
ifelse(a > 5, "oui", "non")
a <- (-4):5
sqrt(ifelse(a >= 0, a, NA))


Commandes if else nichés

if (test_expression1) {
statement1
} else if (test_expression2) {
statement2
} else if (test_expression3) {
statement3
} else {
statement4
}


Exercice 1

Minou <- "chat"
Pitou <- "chien"
Filou <- "chat"
animaux <- c(Minou, Pitou, Filou)

1. Utilisez une commande “if” pour afficher “meow” si Animal a la valeur “chat”.

Exercice 1.1 : Réponse

2. Utilisez une commande if/else pour afficher “woof” si Animal a la valeur “chien” et “meow” sinon. Essayez en d'abord la valeur Animal=“chien” et ensuite “lion”.

Exercice 1.2 : Réponse

3. Utilisez la commande ifelse pour afficher “woof” si les animaux sont des chiens et “meow” pour les chats.

Exercice 1.3 : Réponse


Attention à la syntaxe!

Les accolades { } sont utilisées pour indiquer à R que la commande est à exécuter au complet. Par exemple, essayez:

if ((2 + 1) == 4) print("Les maths, c'est logique!.") 
else print("Houston, on a un problème.")

La commande else ne fonctionne pas, parce que R évalue la première ligne sans reconnaître que votre commande n'est pas complète.

Utilisez plutôt:

if ((2 + 2) == 4) {
  print("Les maths, c'est logique!.")  # R n'évalue pas encore cette expression puisque l'accolade n'est pas fermée. 
} else {
  print("Houston, on a problem.")
}  # Comme toutes les accolades sont fermées, R va évaluer les commandes en entier. 

Rappel: opérateurs logiques

== égal à
!= pas égal à
!x non x
< plus petit que
< = plus petit que ou égal à
> plus grand que
>= plus grand que ou égal à
x & y x ET y
x|y x OU y
isTRUE(x) est-ce que X est vrai?

Itération

Une boucle permet de répéter une ou des opérations.

Les boucles sont utiles pour:

  • faire quelque chose pour chaque élément d'un objet.
  • faire quelque chose jusqu'à la fin du traitement de données.
  • faire quelque chose pour chaque fichier dans un répertoire.
  • faire quelque chose qui peut échouer, jusqu'à ce que ça marche.
  • faire des calculs itératifs jusqu'à convergence.

Boucles "for"

La boucle for exécute un nombre fixe d'itérations d'un bloc de commande(s) .

for (variable in séquence) {
  expression
}

La lettre “i” peut être remplacée par n'importe quelle nom de variable et la séquence peut être à peut prêt n'importe quoi, même une liste de vecteurs.

Essayez:

for (a in c("Bonjour", "programmeurs", "en R")) {
  print(a) 
}
 
for (z in 1:30) {
  a <- rnorm(n = 1, mean = 5, sd = 2) # obtenir une valeur aléatoire provenant d'une distribution normale avec une moyenne de 5 et un écart type de 2. 
  print(a)
}
 
elements <- list(1:3, 4:10)
for (element in elements) {
  print(element)
}

Dans l'exemple qui suit, R évaluerait l'expression 5 fois:

Par exemple:

for (i in 1:5) {
  print(i)
}


Dans cet exemple, la variable m est remplacé successivement par chaque chiffre de 1 à 10, jusqu'au dernier élément de la séquence.

for(m in 1:10) {
  print(m*2)
}
for(m in 1:5) {
  print(m*2)
}
for(m in 6:10) {
  print(m*2)
}
x <- c(2,5,3,9,6)
count <- 0
for (val in x) {
  if(val %% 2 == 0) {
    count = count+1 
  }
}
print(count)


Les boucles for sont souvent utilisées pour exécuter des opérations successivement sur un jeu de données. Nous utiliserons ces boucles pour évaluer des fonctions sur le jeu de données CO2, qui est inclu dans R. Notez que c'est le même jeu de données utilisé pour l'atelier 2.

data(CO2) # ceci charge le jeu de données dans R
for (i in 1:length(CO2[,1])) { # pour chaque ligne du jeu de donnée CO2
  print(CO2$conc[i]) # affiche les concentrations de CO2
}
 
for (i in 1:length(CO2[,1])) { # pour chaque ligne du jeu de donnée CO2
  if(CO2$Type[i] == "Quebec") { # si le type est Quebec
    print(CO2$conc[i]) # affichez les concentrations de CO2 }
  }
}


Truc 1. Pour exécuter une boucle sur chaque ligne d'un jeu de donnée, on utilise la fonction nrow().

for (i in 1:nrow(CO2)) { # pour chaque ligne du jeu de donnée CO2
  print(CO2$conc[i]) # affichez les concentrations de CO2
}


Truc 2. On peut itérer des opérations sur une seule colonne.

for (i in CO2$conc) { # pour chacune des valeurs de concentration de CO2
  print(i) # afficher cette valeur
}


Truc 3. La partie “expression” de la boucle peut contenir plusieurs lignes de commandes différents.

for (i in 4:5) { # pour i de 4 à 5
  print(colnames(CO2)[i])  
  print(mean(CO2[,i])) # afficher les moyennes de cette colonne 
}

Boucles "for" nichées

Dans certains cas, vous voudrez peut-être utiliser des boucles nichées pour accomplir une tâche. Dans ce cas, il est important d'utiliser un nom de variable d'itération différent pour chaque boucle (ici on utilise i et n).

for (i in 1:5) {
  for (n in 1:5) {
    print (i*n)
  }
}

Encore mieux: utiliser la famille "apply()"

La famille de fonctions apply() consiste de fonctions vectorisées qui réduisent le besoin de créer des boucles de façon explicite.

apply() est utilisé pour appliquer des fonctions sur une matrice.

(height <- matrix(c(1:10, 21:30), 
                 nrow = 5, 
                 ncol = 4))
}
 
apply(X = height, 
      MARGIN = 1, 
      FUN = mean)
 
?apply  
 

lapply()

lapply() applique une fonction sur chaque élément d'une liste.

lapply() peut aussi être utilisé avec d'autres objects, comme des trames de données (“dataframe”), listes, ou vecteurs.

La sortie est une liste (d'où le “l” dans lapply) ayant le même nombre d'éléments que l'objet d'entrée.

SimulatedData <- list(
  SimpleSequence = 1:4, 
  Norm10 = rnorm(10),
  Norm20 = rnorm(20, 1),
  Norm100 = rnorm(100, 5))
 
# Applique mean() sur chaque élément de la liste 
lapply(SimulatedData, mean)

sapply()

sapply() est une fonction 'wrapper' pour lapply() qui produit une sortie simplifiée en vecteur, au lieu d'une liste.

SimulatedData <- list(SimpleSequence = 1:4, 
             Norm10 = rnorm(10), 
             Norm20 = rnorm(20, 1), 
             Norm100 = rnorm(100, 5))
 
# Apply mean to each element of the list 
sapply(SimulatedData, mean)

mapply()

mapply() est une version multivariée de sapply().

mapply() applique une fonction premièrement sur le premier élément de chaque argument, et ensuite sur le deuxième élément, et ainsi de suite.

lilySeeds <- c(80, 65, 89, 23, 21)
poppySeeds <- c(20, 35, 11, 77, 79)
 
# Output
mapply(sum, lilySeeds, poppySeeds)

tapply()

tapply() applique une fonction sur des sous-ensembles d'un vecteur.

tapply() est surtout utilisé quand un jeu de données contient différents groupes (ou niveaux/facteurs), et qu'on veut appliquer une fonction sur chaque groupe.

head(mtcars)
 
# get the mean hp by cylinder groups
tapply(mtcars$hp, mtcars$cyl, FUN = mean)

Exercice 2

Vous avez réalisé que votre outil pour mesurer le l'absorption de CO2 n'était pas bien calibré aux sites situés au Québec, et toutes les mesures sont donc deux unités trop élevées.

  1. Utilisez une boucle pour corriger les mesures pour tous les sites aux Québec.
  2. Utilisez la famille de fonctions apply() pour calculer la moyenne de l'absorption de CO2 dans les deux groupes de sites.

Exercice 2 : Réponse


Assurez-vous de bien recharger le jeu de données pour ainsi travailler avec les données originales pour le reste de l'exercice:

data(CO2)


Modifications aux boucles

Normalement, les itérations s'exécutent successivement jusqu'à la dernière.

Il est parfois intéressant d'arrêter l'exécution de la boucle quand une certaine condition est satisfaite ou quand l'itération a atteint un élément.

On peut aussi arrêter l'exécution de l'itération courante pour passer à la boucle suivante.

Pour ceci, on introduit break, while, et next.

Modifications aux boucles: "break"

for(val in x) {
  if(condition) { break }
  statement
}


Modifications aux boucles: "next"

for(val in x) {
  if(condition) { next }
  statement
}


Par exemple, on veut afficher les concentrations de CO2 pour les traitements “chilled” et garder le compte du nombre total d'itérations accomplies.

count <- 0 # la valeur de count est mise à zéro pour pouvoir modifier la valeur dans la boucle. 
 
for (i in 1:length(CO2[,1])) {
  if (CO2$Treatment[i] == "nonchilled") next 
  # Passer à l'itération suivante si c'est "nonchilled"
  count <- count + 1
  print(CO2$conc[i])
}
print(count) # Les fonctions count et print ont été exécutées 42 fois.
 
sum(CO2$Treatment == "nonchilled")


Modifications aux boucles: "break"

Ceci pourrait être écrit de façon équivalente en utilisant une boucle repeat et break:

count <- 0
i <- 0
repeat {
      i <- i + 1
      if (CO2$Treatment[i] == "nonchilled") next  # sauter cette itération
      count <- count + 1
      print(CO2$conc[i])
      if (i == length(CO2[,1])) break     # arrêter l'itération
    }  
 
print(count) 

Modifications aux boucles: "while"

On pourrait écrire ceci avec une boucle while.

i <- 0
count <- 0
while (i < length(CO2[,1]))
{
  i <- i + 1
  if (CO2$Treatment[i] == "nonchilled") next  # sauter cette itération
  count <- count + 1
  print(CO2$conc[i])
}
print(count) 

Exercice 3

Vous venez de réaliser que votre outil pour mesurer la concentration ne fonctionne pas correctement.

Aux sites situés au Mississippi, les concentrations de moins de 300 sont bien mesurés, mais les concentrations de plus de 300 étaient surestimées par 20 unités.

Votre mission est d'écrire une boucle pour corriger ces mesures pour les sites du Mississippi.

Truc: Assurez-vous que vous travaillez avec les données originales pour le reste de l'exercice:

data(CO2)

Exercice 3 : Réponse


Visualization de données avec "for" et "if"

Nous voulons créer un graphique à partir des données de concentration et absorption où chaque point est associé à un type (Québec ou Mississippi) et un traitement (“chilled” et “nonchilled”), et nous voulons représenter ces points différemment.

head(CO2) # Voir les données
unique(CO2$Type) 
unique(CO2$Treatment)
 
# Créer le graphique dans lequel chaque type et traitement a une couleur différente
 
plot(x=CO2$conc, y=CO2$uptake, type="n", cex.lab=1.4, xlab="CO2 concentration", ylab="CO2 uptake") # Type "n" dit à R de ne pas créer le graphique
 
for (i in 1:length(CO2[,1])) {
  if (CO2$Type[i] == "Quebec" & CO2$Treatment[i] == "nonchilled") {
    points(CO2$conc[i], CO2$uptake[i], col="red",type="p")
  }
  if (CO2$Type[i] == "Quebec" & CO2$Treatment[i] == "chilled") {
    points(CO2$conc[i], CO2$uptake[i], col="blue")
  }
  if (CO2$Type[i] == "Mississippi" & CO2$Treatment[i] == "nonchilled") {
    points(CO2$conc[i], CO2$uptake[i], col="orange")
  }
  if (CO2$Type[i] == "Mississippi" & CO2$Treatment[i] == "chilled") {
    points(CO2$conc[i], CO2$uptake[i], col="green")
  }
}

Exercice 4

Créez un graphique montrant les concentrations en fonction de l'absorption et où chaque plante est représenté par des points de différents couleurs.

Bonus: Essayez de le faire avec une boucle nichée!

Exercice 4 : Réponse


Pourquoi créer ses fonctions?

La plupart du travail lourd dans R est effectué par les fonctions. Elles sont utiles pour:

  • répéter une même tâche mais en changeant ses paramètres
  • rendre votre code plus lisible
  • rendre votre code plus facile à modifier et à maintenir
  • partager du code entre différentes analyses
  • partager votre code avec d'autres personnes
  • modifier les fonctionalités par défaut de R

Mais qu'est ce qu'une fonction au juste? Une fonction, c'est essentiellement une boîte noire qui transforme des données. Elle prend en entrée des valeurs - appelées arguments -, utilise du code R pour les traiter et renvoie optionnellement une valeur de retour.

200

Syntaxe d'une fonction

nom_de_la_fonction <- function(argument1, argument2, ...) {
  expression...  # Ce que la fonction fait
  return(value)  # Optionnel, pour accéder au résultat de la fonction
}

Arguments d'une fonction

Les arguments sont les données fournies en entrée à votre fonction. Il s'agit de l'information dont votre fonction a besoin pour opérer correctement.

Une fonction peut avoir entre 0 et une infinité d'arguments.

Par exemple, créons une fonction qui prend un premier nombre (number1), l'additionne à un second (number2), multiplie le résultat par un troisième nombre (number3) et enfin affiche le résultat.

operations <- function(number1, number2, number3) {
  result <- (number1 + number2) * number3
  print(result)
}
 
operations(1, 2, 3)
operations(17, 23, 2)

Exercice 5

En utilisant ce que vous avez vu précédemment sur les structures de contrôle, créez une fonction appelée print_animal qui prend un animal en argument et donne les résultats suivants:

Scruffy <- "dog"
Paws <- "cat"
 
print_animal(Scruffy)
[1] "woof"
 
print_animal(Paws)
[1] "meow"

Exercice 5 : Réponse


Valeurs par défaut dans une fonction

Les arguments peuvent également être optionnels, auquel cas on peut leur donner une valeur par défaut.

Ceci peut s'avérer utile si l'on prévoit d'utiliser fréquemment une fonction avec les mêmes paramètres pour éviter d'avoir à les réécrire à chaque fois, mais si l'on veut tout de même garder la possibilité de changer leur valeur si nécessaire.

operations <- function(number1, number2, number3 = 3) {
  result <- (number1 + number2) * number3
  print(result)
}
 
operations(1, 2, 3) # est équivalent à
operations(1, 2)
operations(1, 2, 2) # on peut toujours changer la valeur de number3


Argument "..."

L'argument spécial “…” vous permet de passer des arguments à une autre fonction utilisée à l'intérieur de votre fonction.
Par exemple, créons une fonction à partir de notre exemple précédent où nous traçons l'absorption de CO2 en fonction de la concentration. Nous allons tracer nos graphes avec deux couleurs différentes selon la région. Ici, les paramètres de plot() et points() seront passés via “…”.

plot.CO2 <- function(CO2, ...) {
  plot(x=CO2$conc, y=CO2$uptake, type="n", ...)  # On utilise ... pour passer les arguments a plot(). 
 
  for (i in 1:length(CO2[,1])){
     if (CO2$Type[i] == "Quebec") {
       points(CO2$conc[i], CO2$uptake[i], col="red", type="p", ...) # idem pour points()
     } else if (CO2$Type[i] == "Mississippi") {
       points(CO2$conc[i], CO2$uptake[i], col="blue", type="p", ...) # idem pour points()
     }
  }
}
 
plot.CO2(CO2, cex.lab=1.4, xlab="CO2 concentration", ylab="CO2 uptake")
plot.CO2(CO2, cex.lab=1.4, xlab="CO2 concentration", ylab="CO2 uptake", pch=20)


L'argument spécial autorise l'utilisateur à entrer un nombre indéfini d'arguments. La valeur de chaque argument devra alors être récupérée manuellement. Par exemple, créons une fonction somme qui accepte un nombre indéfini d'arguments.

sum2 <- function(...){
  args <- list(...)
  result <- 0
  for (i in args)  {
    result <- result + i
  }
  return (result)
}
 
sum2(2, 3)
sum2(2, 4, 5, 7688, 1)

Valeurs de retour

La dernière expression évaluée dans une fonction sera la valeur de retour, même sans la fonction return().

myfun <- function(x) {
  if (x < 10) {
    0
  } else {
    10
  }
}
 
myfun(5)
myfun(15)

Utiliser return() peut être utile si la boucle doit terminer tôt, sortir de la fonction, et sortir une valeur.

simplefun1 <- function(x) {
  if (x<0) 
  return(x)
}

Une seule valeur de retour peut être renvoyée par une fonction. Si vous désirez renvoyer plus d'un objet, vous devez utiliser des objets tels que des listes ou des dataframes. Par ailleurs, il est important de noter que l'execution de la fonction se termine dès qu'elle atteint le mot clé return().

simplefun2 <- function(x, y) {
  z <- x + y 
  return(list("result" = z,
              "x" = x,
              "y" = y))
}
 
simplefun2(1, 2)

Exercice 6

En utilisant ce que vous avez appris jusqu'ici sur les fonctions et les structures de contrôle, créez une fonction bigsum qui prend deux arguments a et b et :

  • sort 0 si la somme de a et b est strictement inférieure à 50
  • sinon, sort la somme de a et b

Défi 6 : Réponse


Accessibilité des variables

Il est essentiel de pouvoir situer nos variables, et de savoir si elles sont définies et accessibles.

  • Les variables définies à l'intérieur d'une fonction ne sont pas accessibles en dehors de la fonction!
  • Les variables définies à l'extérieur d'une fonction sont accessibles à l'intérieur. Cependant, ce n'est JAMAIS une bonne idée de les utiliser à l'intérieur, car votre fonction pourrait arrêter de fonctionner si la variable est effacée.
var1 <- 3     # var1 est définie à l'extérieur de la fonction
vartest <- function() {
  a <- 4      # a est définie a l'intérieur
  print(a)    # affiche a
  print(var1) # affiche var1
}
a             # affiche a. Ceci ne fonctionne pas, 
              # a est seulement visible dans la fonction
vartest()     # vartest() affiche a et var1 
rm(var1)      # supprime var1
vartest()     # la fonction ne fonctionne plus, 
              # car var1 n'existe plus


Utilisez donc des arguments!!

Dans une fonction, les noms d'arguments remplaceront les noms des autres variables.

var1 <- 3     # var1 est définie à l'extérieur de la fonction
vartest <- function(var1) {
  print(var1) # affiche var1
}
vartest(8)     # Dans notre fonction, var1 est maintenant notre argument et prend sa valeur
var1          # var1 a toujours la meme valeur


Faites très attention lorsque vous créez des variables à l'intérieur d'une condition, car la variable pourrait ne jamais être créée et causer des erreurs parfois imperceptibles.

a <- 3
if (a > 5) {
  b <- 2
} 
a + b    # Erreur! b n'existe pas!


Si b avait déjà une valeur différente assignée dans l'environnement, on aurait un gros problème!

R ne trouverait pas d'erreur, et la valeur de a + b serait entièrement différente!


Bonnes pratiques

Voici quelques conseils de programmation qui peuvent vous faciliter la vie, vous aider à avoir un code plus lisible et qui rendent le partage et la réutilisation de votre code bien moins difficile. Avoir un code facile à lire aide à réduire le temps que vous passeriez à essayer de le comprendre, donc ce n'est jamais du temps perdu.

Gardez un code beau et propre

L'une des choses qui aide le plus lors que l'on lit un code informatique, c'est d'avoir un code bien formatté, bien espacé et bien indenté. Certains standards de programmation existent pour vous aider à obtenir une plus grande consistance, mais cela dépend souvent ultimement des préférences de chacun. Voici quelques trucs pour vous aider:

  • Mettez des espaces avant et après vos opérateurs
  • Utilisez toujours le même opérateur d'assignation. `←` est souvent préférable, `=` est ok mais ne changez pas tout le temps entre les deux
  • Utilisez des crochets pour encadrer vos structures de contrôle, même si c'est juste pour une ligne. Chaque ligne de code à l'intérieur des crochets devrait être indentée d'au moins deux espaces. Les crochets de fermeture occupent généralement leur propre ligne, sauf s'ils précèdent une condition else. Ces pratiques adent grandement lorsque l'on veut déterminer ou l'on se trouve, en particulier si l'on a beaucoup de conditions/boucles imbriquées les unes dans les autres.
  • Définissez chaque variable sur sa propre ligne

Voici un exemple de code difficile à lire

a<-4;b=3
if(a<b){
if(a==0)print("a zero") } else {
if(b==0){print("b zero")} else print(b)}

Voici une version plus aisée. Elle prend plus d'espace mais le flot du code est plus facile à voir.

a <- 4
b <- 3
if(a < b){
  if(a == 0) {
    print("a zero")
  }
} else {
  if(b == 0){
    print("b zero")
  } else {
    print(b)
  }
}

Certains guides de style peuvent être trouvés sur internet. En voici un exemple: https://google.github.io/styleguide/Rguide.xml

Utilisez des fonctions si possible

Maintenant que vous savez comment créer une fonction, n'hésitez pas a les utiliser. Dès que vous aperceve une portion de code qui est répétée plus que deux fois dans votre code, vous devriez vous dire “Hmmm… est ce que ça ne serait pas mieux d'écrire une fonction à la place?”. Si seulement une portion de ce code change, essayez de penser à des façons d'utiliser des arguments dans une fonction à la place. Ceci vous aidera à réduire le nombre d'erreurs réalisées en faisant des copier/coller, réduira le temps passé a les corriger et facilitera les modifications futures éventuelles. Par exemple, modifions l'exemple du défi 3 et supposons que toutes les absorptions de CO2 du Mississipi étaient surestimées de 20 et que celles du Québec étaient sous-estimées de 50. Nous pourrions écrire ceci.

for (i in 1:length(CO2[,1])) {
  if(CO2$Type[i] == "Mississippi") {
    CO2$conc[i] <- CO2$conc[i] - 20 
  }
}
for (i in 1:length(CO2[,1])) {
  if(CO2$Type[i] == "Quebec") {
    CO2$conc[i] <- CO2$conc[i] + 50 
  }
}

Ou alors nous pourrions faire ceci à la place.

recalibrate <- function(CO2, type, bias) {
  for (i in 1:nrow(CO2)) {
    if(CO2$Type[i] == type) {
      CO2$conc[i] <- CO2$conc[i] + bias 
    }
  }
  # On doit retourner notre nouveau jeu de donnees car l'original n'est pas modifie
  return (CO2)
}
newCO2 <- recalibrate(CO2, "Mississipi", -20)
# Notez que nous recalibrons ici notre variable newCO2 parce que le CO2 original n'est pas modifie
newCO2 <- recalibrate(newCO2, "Quebec", +50)

Et maintenant, nous réalisons que ce que l'on a modifié dans nos exemples précédents n'était l'absorption, mais la concentration… Maintenant on doit changer toutes les occurences de CO2\$conc[i] par CO2\$uptake[i]. Dans le premier cas, cela veut dire que l'on a à le changer 4 fois, contre seulement deux fois dans notre fonction! (Bon ok, vous pouvez vous dire ici que cela ne vaut pas vraiment le coup, qu'avec un simple rechercher/remplacer vous le faites super vite et effectivement vous auriez raison. Mais ce n'est qu'un simple exemple! Imaginez si vous aviez à le remplacer 10 fois au lieu de 2. Un bon programmeur est un programmeur paresseuz. Et aussi, admettez le, ca en jette plus avec une fonction…)

Donnez des noms qui ont du sens à vos variables et fonctions

Ceci aide à voir au premier coup d'oeil qui fait quoi. Soyez encore plus prudents dans le choix du nom de vos arguments quand vous créez une fonction car c'est ce que les utilisateurs voient. Toutefois il est parfois judicieux de choisir des noms courts pour éviter d'avoir à les taper tout le temps et ainsi éviter les fautes de frappe, donc un bon équilibre doit être choisi.

Voici ce à quoi notre exemple précédent pourrait ressembler avec des noms vagues. Comprendre ce que cette fonction fait demande maintenant un peu plus d'efforts.

rc <- function(c, t, b) {
  for (i in 1:nrow(c)) {
    if(c$Type[i] == t) {
      c$uptake[i] <- c$uptake[i] + b 
    }
  }
  return (c)
}

Commentaires

Même avec des noms évidents, ce n'est jamais une mauvaise chose d'ajouter des commentaires pour décrire tout ce que votre code fait, que ce soit le but de la fonction, comment utiliser ses arguments ou une description détaillée de la fonction étape par étape.

## Recalibre le jeu de donnees CO2 en modifiant l'absorption de CO2
## d'une valeur fixe selon la region
# Arguments
# CO2: le jeu de donnees CO2
# type: le type de donnees qui doivent etre recalibrees. Valeurs: "Mississippi" ou "Quebec"
# bias: la quantite a ajouter a l'absorption. Utilisez des valeurs negative pour les surestimations 
recalibrate <- function(CO2, type, bias) {
  for (i in 1:nrow(CO2)) {
    if(CO2$Type[i] == type) {
      CO2$uptake[i] <- CO2$uptake[i] + bias 
    }
  }
  # On doit retourner notre nouveau jeu de donnees car l'original n'est pas modifie
  return (CO2)
}

Ici sont présentés quelques trucs pour programmer de manière plus efficace avec R et vous aider à obtenir de meilleures performances et un code plus rapide. Toutefois, avant d'optimiser votre code, il est important de s'assurer que vous avez d'abord un code qui fonction. Un code lent qui fonctionne sera toujours meilleur qu'un code rapide qui ne marche pas. Par ailleurs, parfois, cela ne sert à rien d'optimiser. Passer 2 heures à réécrire des lignes de code pour gagner quelques secondes à l'execution n'est pas forcément la solution la plus efficace.

Avant de commencer : évaluer nos performances

Si l'on veut optimiser notre code, la première étape est de savoir combien de temps chaque tâche prend.

La façon la plus simple de le faire est d'utiliser la fonction system.time(expression)

system.time({
a <- 0
  for (i in 1:1000) {
    a <-  a + i
  }
})

Notez que la plupart du temps, R travaille vraiment rapidement et il faut avoir des tâches qui demandent vraiment beaucoup de puissance ou le temps risque de ne même pas être enregistré. C'est pourquoi il est recommandé de répéter plusieurs fois la tâche que l'on veut évaluer ou alors de travailler sur de gros jeux de données.

system.time(replicate(1000, {
  a <- 0
  for (i in 1:1000) {
    a <-  a + i
  }
}))

Un autre outil simple et utile est la fonction Rprof(). L'avantage principal de Rprof() est qu'elle enregistre de l'infromation sur le temps passé dans chaque fonction dans un fichier auquel on peut accéder plus tard. Voici comment l'utiliser.

Rprof("profile.txt")  # on peut changer profile.txt par le nom de fichier desire
for (i in 1:1000) {
    a <- 0
    for (i in 1:1000) {
      a <-  a + i
    }
  }
Rprof()               # Ceci termine le profilage
summaryRprof("profile.txt")  # Utilisez le nom de fichier enregistre precedemment pour afficher le resume

Enfin, si vous voulez comparer l'efficacite de plusieurs fonctions côte à côte, un très bon outil est le package microbenchmark

install.packages("microbenchmark")
library(microbenchmark)
 
f1 <- function() {
  a <- 0
  for (i in 1:1000) {
    a <-  a + i
  }
}
 
microbenchmark(f1(), times=1000) # l'argument times nous permet de determiner le nombre d'iterations voulues

Première étape : réfléchir un peu!

Si vous regardez attentivement votre code, souvent vous realiserez qu'il existe d'autres façons plus simples, plus efficaces de faire ce que vous désirez et que certaines opérations peuvent facilement être supprimées pour gagner du temps.

Par exemple, créons une fonction qui prend un nombre a. Nous allons ajouter a à chaque nombre de 1 à 100, et si a est inférieur à 5, alors nous ajouterons 2*a à la place. Ensuite, nous additionnerons ensemble tous les elements de la sequence.

Voici une façon de le faire

f2 <- function(a) {
  # initialisation du résultat
  result <- 0
  # on itere sur la sequence de 1 à 100
  for (i in 1:100) {
    if (a < 5) {
      # a est < 5, on ajoute 2*a a la sequence. On met le tout dans result 
      result <- result + i + (2*a)
    } else {
      # a est >= 5, on n'ajoute que a
      result <- result + i + a
    }
  }
  return(result)
}
f2(4)

Notre fonction fait ce que l'on désire et est une solution tout à fait acceptable. Cependant, nous avons plein d'étapes inutiles dans notre code. Par exemple, nous n'avons pas besoin d'effectuer notre condition à chaque itération, car le résultat sera toujours le même. On peut donc la sortir de la boucle.

f3 <- function(a) {
  # initialisation du résultat
  result <- 0
 
  # On verifie si a < 5, si oui, a vaut maintenant 2*a
  if (a < 5) {
   a <- 2 * a
  } 
  # nous n'avons meme pas besoin de tester l'alternative puisque a reste identique
 
  # on itere sur la sequence de 1 à 100
  for (i in 1:100) {
      result <- result + i + a 
  }
  return(result)
}
 
f3(4)
microbenchmark(f2(4), 
               f3(4), times=1000)

Nous avons effectué seulement une simple modification mais nous avons ici réussi à accélerer notre code d'environ 40% (les résultats peuvent varier selon les ordinateurs). De plus, notre code est plus facile à lire et à comprendre. Parfois, on peut gagner à la fois vitesse et lisibilité juste en réflechissant à la place de nos conditions et à ce qu'elles testent.

Mais en utilisant les forces de R, on peut faire encore mieux!

f4 <- function(a, n) {
  result <- 0
 
  if (a < 5) {
    a <- a + 1
  } 
  result <- sum(1:n + a)
  return(result)
}
 
f4(4)
microbenchmark(f3(4), 
               f4(4), times=1000)

Wow, ici notre modification est beaucoup plus efficace… Mais qu'est ce qui s'est passé exactement? Ceci nous amène à notre prochain point.

Vectorisation

Cette partie est un rappel de choses que vous avez probablement déjà vues lors des premiers ateliers. Cependant, ces notions sont souvent oubliées et les mauvaises performances de R peuvent fréquemment être attribuées à une mauvaise vectorisation. R est conçu pour travailler avec les vecteurs et par conséquent, de nombreuses fonctions sont optimisées pour la vectorisation. Pour comprendre ceci, il est d'abord important de comprendre comment R fonctionne. R est un langage interprété, ce qui veut dire que lorsque vous executez votre code R, en réalité vous envoyez vos instructions à des fonctions programmées dans un autre langage (le langage C). Ceci ralentit l'execution de vos programmes puisque le code R doit d'abord être décodé puis envoyé à d'autres fonctions. Lorsque vous créez une boucle, vous devez décoder chaque itération puis la transférer. Les fonctions vectorisées d'un autre côté sont des fonctions qui travaillent directement avec des vecteurs. Elles executent elles aussi une boucle sur votre vecteur, mais la grosse différence est qu'elles le font directement en C, ce qui est beaucoup plus rapide. la fonction sum() est un exemple de fonction vectorisée. L'un des plus gros challenges de R est d'apprendre à penser et à programmer avec des vecteurs et non avec des éléments simples. Par exemple, la plupart des opérations de base peuvent être faites sur des vecteurs.

v1 <- 1:5
v2 <- 2:6
v3 <- 1:3
v1 + 2      # Addition sur un vecteur : ajoute 2 a tous les elements
v1 + v2     # Ajoute chaque element de v2 a v1
v1 + v3     # v1 et v3 ne sont pas de la meme taille, on recommence a additionner a partir du debut de v3
sum(v1)     # Additionne tous les elements de v1 ensemble
sum(v1, v2) # Fait la somme de tous les elements de v1 et v2 
mean(v1)    # Fait la moyenne de v1
mean(c(v1, v2)) # Moyenne des elements de v1 et v2. Contrairement a sum(), on doit les combiner avant

Extraire des sous-ensembles

Pour vectoriser efficacement, il est également important d'être capable d'extraire des valeurs de nos données rapidement. R offre des outils de sélection de sous-ensembles pour appliquer un traitement sur des élements spécifiques de vecteurs ou de dataframes qui sont parfois plus efficaces et plus faciles à écrire que des boucles et des conditions.

L'extraction de sous-ensembles est faite vis les opérateurs [ et $ (pour un dataframe). Nous pouvons insérer directement nos condition dans la partie [] pour extraire rapidement des valeurs de nos données. Il est également possible d'utiliser la fonction which() pour tester une condition. which() retourne les indexs des élements qui remplissent la condition.

v1 <- 1:10
v1[7]      # Extrait la 7eme valeur
v1[v1 > 5] # Extrait les valeurs > 5 seulement
v1[which(v1 > 5)]  # pareil que precedemment

Dans les dataframe, $ permet d'acceder à une colonne par nom. Nous pouvons également le faire en fournissant directement le nom de la colonne.

CO2 <- read.csv("co2_good.csv")
CO2$Type  # Affiche la colonne Type
CO2[, "Type"] # Idem
CO2[CO2$Type == "Quebec", ] #Extrait toutes les lignes de CO2 dont le Type est "Quebec" 

Défi 7

Créez une nouvelle fonction recalibrate2(), qui est une réécriture de la fonction recalibrate vue précedemment, en utilisant des techniques de vectorisation et d'extraction de sous-ensembles. La nouvelle fonction ne devrait pas faire plus de 3 lignes.
Rappel:

recalibrate <- function(CO2, type, bias) {
  for (i in 1:nrow(CO2)) {
    if(CO2$Type[i] == type) {
      CO2$uptake[i] <- CO2$uptake[i] + bias 
    }
  }
  return (CO2)
}

Défi 7 : Réponse


Les objets qui grossissent

Vectoriser est une bonne chose, mais cela s'avère parfois difficile et il se peut que cela vous prenne plus de temps que d'écrire une simple boucle. Parfois, les boucles sont absolument nécessaires et il ne faut pas se restreindre de les utiliser. Cependant, dans votre boucle, si vous désirez avoir des performances décentes, vous devrez faire attention aux objets qui grossissent. C'est à dire les objets qui deviennent de plus en plus gros à chaque itération. Illustrons ceci simplement en créant une fonction qui itère sur une séquence et crée un vecteur avec. Nous comparerons deux façons de faire: en laissant notre objet grandir ou en préallouant notre objet résultat et en le modifiant à chaque itération.

growing <- function(n) {
  # on declare notre objet resultat
  result <- NULL
  for (i in 1:n) {
    # on cree notre resultat en le faisant grandir a chaque iteration
    result <- c(result, i)
  }
  return(result)
}
 
growing2 <- function(n) {
  # on declare notre resultat : ici on cree un vecteur de taille n avec des 0 dedans
  result <- numeric(n)
  for (i in 1:n) {
    # maintenant on modifie juste la valeur au lieu de recreer le vecteur
    result[i] <- i
  }
  return(result)
}

Maintenant comparons leurs vitesses respectives

system.time({
  growing(10000)
})
system.time({
  growing2(10000)
})

Avec un vecteur de 10000 éléments, les vitesses sont encore comparables et prennent moins d'une seconde. Maintenant utilisons 50000 éléments

system.time({
  growing(50000)
})
system.time({
  growing2(50000)
})

En multipliant le nombre d'objet par seulement 5, cela nous prend maintenant plusieurs secondes pour créer le vecteur par itération alors que modifier un vecteur prédéfini est toujours quasiment instantané. Qu'est ce qui s'est passé ici? La raison est que lorsque vous appelez une fonction, les arguments sont tout d'abord copiés avant d'être passés à la fonction. Donc lorsque vous écrivez result ← c(result, i) , à chaque fois result est copié avant d'être passé à c(). Au fur et à mesure que result grossit, à chaque itération cela prend de plus en plus de temps à le copier. Plus l'objet final est gros, plus cela prendra de temps. C'est pourquoi il est toujours préférable de créer votre objet résultat avant votre boucle si vous savez la taille qu'il aura.

Ceci est particulièrement valide lorsque l'on travaille avec des dataframes et des fonctions telles que rbind() et cbind(). Malheuresement, définir au préalable des dataframes ne marche pas si bien que ça et il existe une meilleure façon, bien que plus compliquée. Il faut alors stocker chaque ligne (ou colonne) dans une liste préallouée et ensuite appeler rbind() (ou cbind()) sur tous les éléments d'un coup via la fonction do.call(). La fonction do.call() vous permet d'executer une fonction donnée sur une liste d'arguments. De cette façon, rbind() est appelée une seule fois, a la fin, ce qui élimine le problème de copier l'objet au fur et à mesure qu'il grossit.

growingdf <- function(n, row) {
  # predefinission notre dataframe
  df <- data.frame(numeric(n), character(n), stringsAsFactors=FALSE)
  for (i in 1:n) {
    # remplacons la ieme ligne par row
    df[i,] <- row 
  }
  return(df)
}
 
growingdf2 <- function(n, row) {
  # Voici la facon d'allouer une liste a n elements
  df <- vector("list", n)
  for (i in 1:n) {
    # on place row dans le ieme element
    df[[i]] <- row 
  }
  return(do.call(rbind, df))
}
 
# Stockons notre ligne dans une liste puisque nous avons des elements differents (un nombre et une chaine
# de caracteres)
row <- list(1, "Hello World")
microbenchmark(growingdf(5000, row),
               growingdf2(5000, row),
               times=10)

La famille apply

Afin d'éviter le problème des objets qui grossisent dans les boucles et pour faciliter l'application de fonctions sur des objets tels que des dataframes, R nous offre ce que nous appelerons les fonctions apply (parce qu'elles possèdent toutes apply dans leur nom…). Il s'agit d'un groupe de fonctions qui vont exécuter une autre fonction sur un objet d'une type particulier. Leur utilisation diffère seulement selon le type d'objet sur lequel on applique la fonction ou du type de la valeur de retour.

Les fonctions de la famille apply ne sont pas toujours le meilleur choix d'un point de vue performance car elles vont souvent cacher une boucle écrite en R dans leur code. Cependant, elles peuvent grandement réduire le temps de programmation par le confort d'utilisation qu'elles fournissent.

L'une des plus populaires est simplement apply(), qui execute une fonction sur les lignes ou les colonnes d'une dataframe ou d'une matrice. La fonction prend 3 arguments principaux:

  • l'objet sur lequel on veut appliquer notre fonction
  • le sous-ensemble sur lequel on veut appliquer la fonction. 1 est pour les lignes, 2 pour les colonnes
  • la fonction à appliquer
  • les arguments éventuels à passer à la fonction fournie
df <- data.frame(1:100, 101:200)
# Somme sur les lignes
apply(df, 1, sum)
# Moyenne sur les colonnes
apply(df, 2, mean)
# on peut egalement fournir des arguments supplementaire a la fonction
apply(df, 2, mean, na.rm=TRUE)
# On peut egalement definir directement une fonction. le premiere argument de cette 
# fonction sera obligatoirement ce sur quoi on veut iterer. Ici chaque ligne est consideree
# comme un vecteur de nombre comme montre par la fonction str()
apply(df, 1, function(x){str(x)})
# On peut egalement ajouter d'autres arguments
apply(df, 1, function(x, y){x[2] - x[1] + y}, y=5)

Toutes les fonctions apply fonctionnent sur le même modèle. D'un point de vue performance, les plus intéressantes sont sans doute lapply et vapply car ce sont des fontions primitives écrites en C. lapply retourne une liste de la même longueur que notre objet original. vapply vous autorise à spécifier le format de la valeur de retour de votre fonction. Ce peut être un vecteur ou un tableau.

a <- list(1:100, 101:200)
# appliquons mean a tous les elements
lapply(a, mean)  # on obtient une liste en retour
unlist(lapply(a, mean)) # utilisez unlist pour avoir un vecteur a la place
vapply(a, mean, 0) # on dit donc a vapply que notre resultat sera un simple nombre

4. Brève introduction à quelques paquets utiles dans R

Knitr est un paquet qui peut être utilisé pour générer des rapports de façon dynamique ou des pages web à partir de code en R. Le code est évalué au moment de créer le rapport.

Le code peut être écrit facilement en RStudion en utilisant le langage Markdown. :

Exemple de code Markdown

Voir la page web résultante.

Data table est un paquet très utile qui peut faciliter et améliorer l'efficacité de certaines opérations en R. Les tables de données (data tables) sont très similaires aux data frames. Vous pouvez même les construire à partir de data frames.

Introduction to Data table (PDF)

install.packages('data.table')
library(data.table)

Créez un très long jeu de données avec une colonne contenant des lettres et une colonne avec des nombres aléatoires.

mydf<-data.frame(a=rep(LETTERS,each=1e5),b=rnorm(26*1e5))

Convertir le data frame en table de données.

mydt<-data.table(mydf)

Une clé doit être attribuée à chaque table de données. Cette clé doit être une (ou plus) colonne provenant de la table. Cette clé est à la base de l'organisation de la table de données.

setkey(mydt,a)

Une fois que la clé est attribuée, nous pouvons facilement extraire, par exemple, toutes les lignes contentant la clé (colonne a) égale à F.

mydt['F']

Donne la valeur moyenne pour la colonne b, pour chaque lettre de la colonne a.

mydt[,mean(b),by=a]

Comparons maintenant la performance de Data table avec les autres méthodes nous permettant d'effectuer la même tâche.

system.time(t1<-mydt[,mean(b),by=a])

Avec tapply()

system.time(t2<-tapply(mydf$b,mydf$a,mean))

Avec reshape2

NOTE: plyr et reshape2 ont été traités dans l'atelier 3.

library(reshape2)
meltdf<-melt(mydf)
system.time(t3<-dcast(meltdf,a~variable,mean))

Avec plyr , un suite d'outils qui peuvent être utiliser pour séparer des données en blocs homogènes, appliquer une fonction sur chaque bloc, et remettre les blocs ensemble.

library(plyr)
system.time(t4<-ddply(mydf,.(a),summarize,mean(b)))

Avec dplyr , un nouvelle version de plyr qui est plus rapide et adaptée spécialement aux data frames.

library(dplyr)
ti1<-proc.time()
groups <- group_by(mydf, a)
t5 <- summarise(groups, total = mean(b))
eltime<-proc.time()-ti1

Avec sqldf. Ce paquet permet d'écrire des requêtes de types SQL (Structured Query Language) sur des data frames.

library(sqldf)
system.time(t6<-sqldf('SELECT a, avg(b) FROM mydf GROUP BY a'))

Avec une boucle FOR

ti1<-proc.time()
# Initialisation d'un data frame vide avec deux colonnes et 26 lignes. 
t7<-data.frame(letter=unique(mydf$a),mean=rep(0,26))
for (i in t6$letter ){
  t7[t7$letter==i,2]=mean(mydf[mydf$a==i,2])
}
eltime<-proc.time()-ti1
eltime

Avec une boucle FOR parallèlisée

On pour utiliser les paquets foreach et doMC pour executer des sections de code en parallèle sur des ordinateurs avec plusieurs coeurs. C'est particulièrement utile pour accélérer des calculs dans des boucles FOR dans lesquelles chaque itération roule indépendemment des autres. Notez que doMC peut ne pas fonctionne sous Windows. Ça devrait cependant fonctionner sous Linux ou Mac OSX.

library(foreach)
library(doMC)
registerDoMC(4) #Processeur quatre-coeurs
ti1<-proc.time()
t8<-data.frame(letter=unique(mydf$a),mean=rep(0,26))
t8[,2] <- foreach(i=t8$letter, .combine='c') %dopar% {
 mean(mydf[mydf$a==i,2])
}
eltime<-proc.time()-ti1
eltime

Le paquet RgoogleMaps vous permet d'afficher très simplement des images de Google Maps ou Google Satellite allows to very simply show Google maps or Google Satellite dans R, centrées sur la localisation de votre choix. Vous pouvez également superposer des données spatiales relativement aisément sur ces cates. La fonction getGeocode() permet de transformer une entrée de recherche pour un code postal ou un nom de lieu en coordonnées latitude, longitudes en utilisant les services Google.

library(RgoogleMaps)
myhome=getGeoCode('Olympic stadium, Montreal');
mymap<-GetMap(center=myhome, zoom=14)
PlotOnStaticMap(mymap,lat=myhome['lat'],lon=myhome['lon'],cex=5,pch=10,lwd=3,col=c('red'));

Le projet rOpenSci supporte le développement d'un nombre important de paquets R pour faciliter l'accès à des sources de données en ligne. Parmi celles-ci, figure le paquet Taxize, qui peut être utilisé pour extraire l'information taxonomique provenant de différents bases de données. On peut extraire, par exemple, des synonymes, des hiérarchies taxonomiques, les noms communs, et plus, de plus d'une dizaine de sources.

library(taxize)
spp<-tax_name(query=c("american beaver"),get="species")
fam<-tax_name(query=c("american beaver"),get="family")
correctname <- tnrs(c("fraxinus americanus"))
cla<-classification("acer rubrum", db = 'itis')

Un autre paquet de rOpenSci qui est très utile est Spocc. Il permet d'effectuer des recherches de données d'occurrence d'espèces provenant de plusieurs sources, dont Global Biodiversity Information Facility, une immense base de données mondiale contenant des centaines de millions d'occurrences venant de données de terrain ou de collections.

library(spocc)
occ_data <- occ(query = 'Acer nigrum', from = 'gbif')
mapggplot(occ_data)

Combinez spocc et RgoogleMaps

occ_data <- occ(query = 'Puma concolor', from = 'gbif')
occ_data_df=occ2df(occ_data)
occ_data_df<-subset(occ_data_df,!is.na(latitude) & latitude!=0)
mymap<-GetMap(center=c(mean(occ_data_df$latitude),mean(occ_data_df$longitude)), zoom=2)
PlotOnStaticMap(mymap,lat=occ_data_df$latitude,lon=occ_data_df$longitude,cex=1,pch=16,lwd=3,col=c('red'));

Geonames connecte R à Geonames.org, une base de données de noms de lieux et de toponymes.

library(geonames)
options(geonamesUsername="glaroc")
# Trouver les noms de lieux qui contiennent le terme "Mont Saint-Hilaire"
res<-GNsearch(q="Mont Saint-Hilaire")
res[,c('toponymName','fclName')]
#Extraire toutes les villes dans un rectangle définit par deux coins géographiques. 
dc<-GNcities(45.4, -73.55, 45.7, -73.6, lang = "en", maxRows = 10)
dc[,c('toponymName')]