Introduction à la programmation avec R
gilles.hunault "at" univ-angers.fr
Cours 4 - Boucles et itérations
Table des matières cliquable
4. Itérations et sorties de boucles
5. Imbrications, itérations et sorties de boucles
6. Exemples de boucles usuelles (mais potentiellement lentes) en R
Exercices : énoncés solutions [Retour à la page principale du cours]1. Boucles "TANT QUE"
La boucle TANT QUE (while en anglais) est un peu comme une structure si répétitive. Sa syntaxe algorithmique est la suivante :
# structure algorithmique TANT QUE # -------------------------------- TANT QUE (condition) [...] # bloc d'instructions exécutées # tant que la condition reste vraie FINTANT QUESon écriture en R est immédiate :
# traduction en R de la boucle TANT QUE # ------------------------------------- while (condition) { # bloc d'instructions }En voici un exemple :
# exemple de boucle TANT QUE : # recherche du premier symbole point dans un nom de fichier # ------------------------------------------------------------------- nomFic <- "essai.serie1.xls" posPoint <- 0 while (substr(nomFic,posPoint,posPoint)!=".") { posPoint <- posPoint + 1 } # fin tant que cat(" le point est vu en position ",posPoint,"dans le fichier ",nomFic,"\n")Il faut être très prudent(e) avec une boucle tant que car l'ordinateur peut boucler pendant un temps infini si la condition est mal écrite. Ainsi dans l'exemple précédent, nous avons malheureusement oublié que la machine ne doit pas aller après le dernier caractère. Supposer qu'un nom de fichier contient toujours un point serait une erreur de conception et le code précédent ne s'arrête jamais s'il n'y a pas de point dans le nom de fichier. Voici ce qu'il faut écrire pour avoir un code correct :
# recherche du premier symbole point dans un nom de fichier # avec une boucle TANT QUE qui se termine forcément nomFic <- "essai.serie1.xls" posPoint <- 0 nbcar <- nchar(nomFic) while ( (substr(nomFic,posPoint,posPoint)!=".") & (posPoint<nbcar)) { posPoint <- posPoint + 1 } # fin tant que if (posPoint>nbcar) { cat(" le point n'est pas dans le nom du fichier\n") } else { cat(" le point est vu en position ",posPoint,"dans le fichier ",nomFic,"\n") } # fin si # remarque : une "vraie" solution R serait, sans boucle tant que : # # posPoint <- which(strsplit(nomFic,split="")[[1]]==".") # # et which(unlist(strsplit(nomFic,split=""))==".") # # pour trouver toutes les positions du point dans nomFic2. Boucles "POUR"
Lorsque le nombre de répétitions est connu, que ce soit une valeur constante comme 10 ou le nombre des éléments d'un vecteur, on peut utiliser une autre structure répétitive, nommée boucle POUR dont voici la syntaxe algorithmique :
# structure algorithmique POUR # ---------------------------- POUR indice DE valeurInitiale A valeurFinale # bloc d'instructions exécutées # avec la valeur indice FINPOURLà encore, son écriture en R est immédiate :
# traduction en R de la boucle POUR # --------------------------------- for (indice in (valeurInitiale : valeurFinale) ) { # bloc d'instructions }Et un exemple peut être :
# exemple : correspondance entre lettres et chiffres # ------------------------------------------------------------------- for (indLet in (1:5) ) { lettre <- LETTERS[ indLet] cat("la lettre",lettre,"est en position",indLet,"\n") } ; # fin pour indLet # affichage : ------------- la lettre A est en position 1 la lettre B est en position 2 la lettre C est en position 3 la lettre D est en position 4 la lettre E est en position 5Dans les faits, R dispose d'une boucle POUR plus générale d'itération nommée aussi for, proche des boucles foreach des autres langages de programmation. En voici des exemples :
# boucle POUR générale en R # ---------------------------- for (ELEMENT in STRUCTURE) { # bloc d'instructions } ## boucles POUR sur les colonnes d'un dataframe # via les noms de colonne for (nom in names(df) ) { [...] } # fin pour nom # via les numéros de colonne pdvCol <- 1:ncol(df) # plage de variation for (indcol in pdvCol) { } # fin pour indcol3. Boucles "REPETER JUSQU'A"
Le troisième type de boucle disponible consiste à faire le test pour savoir si on recommence la boucle en fin de corps de boucle. La syntaxe algorithmique est la suivante :
# structure algorithmique REPETER # -------------------------------- REPETER [...] # bloc d'instructions exécutées JUSQU'A (condition)Sa traduction n'est pas immédiate en R parce qu'on écrit seulement
repeat { # bloc d'instructions exécutées }Il faut impérativement passer par break ou stop() pour sortir de la boucle REPETER JUSQU'A en R. Voici un exemple d'une telle boucle en R qui demande un nom de fichier et teste si le fichier est présent avant d'aller plus loin :
repeat { nomf <- readline(" donner le nom d'un fichier : ") if (!file.exists(nomf)) { cat(" fichier",nomf," nom vu dans le répertoire courant.\n") } else { break } # fin de si } # fin de répéter cat(" ok, traitement du fichier ",nomf,"\n")Attention : il y beaucoup de fonctions R qui dispensent d'écrire des boucles. Il est prudent de les apprendre parce que l'exécution des boucles en R est souvent lente à cause de la gestion en mémoire des variables. Ainsi la fonction cbind() fournit nativement la correspondance entre indice et élément. Voir les exercices pour se convaincre qu'il faut apprendre de nombreuses fonctions R. On notera au passage que la fonction cbind() renvoie une matrice ou un data.frame en fonction des paramètres passés.
4. Itérations et sorties de boucles
Il peut arriver que l'on veuille sortir d'une boucle ou du script. Quelque soit le type de boucle, la fonction stop() quitte le script en cours alors que l'instruction break permet de sortir de la boucle. On peut aussi utiliser next pour forcer la machine à passer à l'itération suivante. Voici des exemples :
# une boucle infinie d'interpréteur repeat { cat("Donner un entier dont vous voulez le carré ou 0 pour arrêter : ") rep <- as.numeric(readline()) if (rep==0) { break } cat(" le carre de ",rep," est ",rep**2,"\n") } # fin répéter_jusqu'àL'exécution de cette boucle aboutit aux résultats suivants :
Donner un entier dont vous voulez le carré ou 0 pour arrêter : 5 le carre de 5 est 25 Donner un entier dont vous voulez le carré ou 0 pour arrêter : 8 le carre de 8 est 64 Donner un entier dont vous voulez le carré ou 0 pour arrêter : 05. Imbrications, itérations et sorties de boucles
Comme pour les tests, on peut imbriquer les boucles, éventuellement de différents types. De plus, un corps de boucle contient des instructions, donc on peut avoir une boucle à l'intérieur de la partie "sinon" d'un test, qui contient elle-même un autre test avec une autre boucle, etc.
On veillera à ne pas trop multiplier l'imbrication des structures de façon à pouvoir s'y retrouver facilement. Trois ou quatre niveaux d'imbrication (une boucle dans un test dans une boucle dans...) parait être la limite supportable de la compréhension. Au-delà, il faut certainement recourir à un sous-programme (une fonction) pour que ce soit lisible... Cela peut se tester par programme, par exemple avec le package cyclocomp qui teste la complexité cyclomatique du code d'une fonction.
Voici des exemples avec chacun des types de boucles qui illustrent ces points.
Nous commençons avec deux boucles POUR imbriquées pour afficher les valeurs de x^y inférieures à 1 million pour les nombres x et y de 1 à 10. On affiche les puissances de 1, de 2, de 3... ligne par ligne. Pour éviter de calculer mathématiquement jusqu'à quel terme on doit aller, on calcule la puissance et on ne l'affiche que si elle est inférieure à un million.
Voici le code R correspondant :
# boucles imbriquées et BREAK cat("table des puissances inférieures ou égales à 1 million\n") for (indi in 1:10) { for (indj in 1:10) { puiss <- indi**indj if (puiss<=10**6) { cat(sprintf("%9d",puiss)) } else { break } # fin si } # fin pour indj cat("\n") } # fin pour indiet le résultat de son exécution
table des puissances inférieures ou égales à 1 million 1 1 1 1 1 1 1 1 1 1 2 4 8 16 32 64 128 256 512 1024 3 9 27 81 243 729 2187 6561 19683 59049 4 16 64 256 1024 4096 16384 65536 262144 5 25 125 625 3125 15625 78125 390625 6 36 216 1296 7776 46656 279936 7 49 343 2401 16807 117649 823543 8 64 512 4096 32768 262144 9 81 729 6561 59049 531441 10 100 1000 10000 100000 1000000Le deuxième exemple montre comment tester une fonction à l'aide d'une réponse utilisateur via une boucle REPETER . Ici, nous avons pris l'exemple de la fonction qui calcule le carré pour des raisons de simplicité.
L'idée est ici de demander une valeur à l'utilisateur, d'afficher le résultat de la fonction et de recommencer. Nous avons décidé arbitrairement qu'entrer la valeur zéro fait sortir de la boucle.
# une boucle infinie d'interpréteur repeat { cat("Donner un entier dont vous voulez le carré ou 0 pour arrêter : ") rep <- as.numeric(readline()) if (rep==0) { break } cat(" le carre de ",rep," est ",rep**2,"\n") } # fin répéter_jusqu'àL'exécution de cette boucle aboutit aux résultats suivants, sachant que l'utilisateur a saisi 5, puis 8 puis 0 (afin d'arrêter le calcul).
Donner un entier dont vous voulez le carré ou 0 pour arrêter : 5 le carre de 5 est 25 Donner un entier dont vous voulez le carré ou 0 pour arrêter : 8 le carre de 8 est 64 Donner un entier dont vous voulez le carré ou 0 pour arrêter : 0Comme troisième exemple, nous essayons de traiter avec une boucle TANT QUE. tous les fichiers-texte du répertoire avec une numérotation régulière lorsqu'ils ont plus de deux lignes :
numFic <- 1 nomFic <- paste("fic",sprintf("%03d",numFic),".txt",sep="") while (file.exists(nomFic)) { lignes <- readLines(nomFic) nbLign <- length(lignes) cat(" le fichier ",nomFic," comporte ",nbLign," lignes\n") if (nbLign<2) { stop("-- fin de parcours des fichiers.\n") } # fin si numFic <- numFic + 1 nomFic <- paste("fic",sprintf("%03d",numFic),".txt",sep="") } # fin tant que cat("tous les fichiers ont été traités.\n")Un exemple d'éxécution, avec quelques fichiers présents dans notre répertoire courant fournit l'affichage suivant. On remarquera que R écrit Erreur lorsqu'on fait appel à la fonction stop() .
le fichier fic001.txt comporte 3 lignes le fichier fic002.txt comporte 2 lignes le fichier fic003.txt comporte 3 lignes le fichier fic004.txt comporte 1 lignes Erreur : -- fin de parcours des fichiers.6. Exemples de boucles usuelles (mais potentiellement lentes) en R
Nous présentons ici quelques boucles faussement susceptibles d'être utiles. En effet, la plupart de ces boucles explicites peuvent être remplacées à moindre coût d'écriture par des fonctions de R prévues pour cela. Ces exemples ne sont donc fournis qu'à titre d'entrainement à la lecture de boucles et ne sont pas optimisés.
Nous aurons l'occasion de revoir un peu plus tard comment réaliser les traitements associés à ces boucles de façon beaucoup plus "propre et concise" et, en tous cas, dans l'esprit de R.
6.1 Traitement de toutes les colonnes d'un data frame
Il arrive assez fréquemment qu'on ait à traiter toutes les colonnes numériques quantitatives d'un data frame, par exemple pour en calculer systématiquement les moyennes, les médianes (au fait, vous vous souvenez de la différence statistique entre ces deux indicateurs de tendance centrale et pourquoi on calcule la moyenne des tailles, la médiane des poids pour les humains et pas l'inverse ?)...
R fournit avec la fonction ncol() le nombre de colonnes d'un data frame et avec la fonction names() le nom de ces colonnes. Il pourrait être tentant d'écrire une boucle comme
for (indCol in 1:ncol(df)) { nomCol <- names(df)[indCol] cat("traitement de la colonne numéro",indCol,"soit",nomCol,"\n") } # fin pour indColmais c'est certainement se fatiguer beaucoup, surtout si le numéro de colonne n'est pas utilisé. Il est possible en R de passer directement en revue les noms de colonne. Au passage, nous montrons comment construire un fichier graphique de type .png dont le nom reprend celui de la colonne en cours, après détection du type numérique de la colonne :
for (col in names(df)) { # pour chaque nom de colonne if (is.numeric(col)) { # s'il s'agit de données numériques ficPng <- paste(col,".png",sep="") # suite du traitement de la colonne } # fin si } # fin pour col6.2 Calculs par sous-groupe
Imaginons maintenant que nous voulions parcourir toutes les lignes d'un data frame pour compter (maladroitement) via le code-sexe (variable SX) le nombre d'hommes (SX=1) et de femmes (SX=2). De façon encore plus maladroite, nous allons mettre dans la variable agesH les ages des hommes et dans la variable agesF les ages des femmes. Voici ce qu'il ne faut pas faire, mais au moins, vous l'aurez vu et vous saurez l'éviter :
# exemple de calcul par sous-groupe # (non optimisé) nbInd <- nrow(df) nbHom <- 0 # comptage du nombre d'hommes nbFem <- 0 # mais alors "où sont les femmes ?", comme dit Patrick Juvet agesH <- c() # ages des hommes agesF <- c() # ages des femmes for (indLig in (1:nbInd)) { if (df[indLig,"SX"]==1) { nbHom <- nbHom + 1 agesH <- c(ageH,df[indLig,"AGE"]) } else { nbFem <- nbFem + 1 agesF <- c(ageF,df[indLig,"AGE"]) } # finsi } # fin pour indLing6.3 Traitement d'une liste de fichiers et moyenne dans un tableau
On veut traiter n fichiers, disons ficSerie01.txt, ficSerie02.txt, ficSerie03.txt... mais ce pourrait être bien sûr des fichiers Excel. Une fois le traitement de chaque fichier effectué, on veut obtenir un tableau et un fichier .CSV résumé des traitements.
Pour notre exemple, nous allons limiter le traitement, via la fonction traiteFichier() à la détermination du nombre de lignes du fichier. Voici cette fonction :
# exemple de traitement d'un fichier # réduit à la détermination de son nombre de lignes traiteFichier <- function( nomFic ) { if (!file.exists(nomFic)) { cat("fichier",nomFic,"non vu.\n") } else { dataFic <- read.table(nomFic,head=TRUE) nbl <- nrow(dataFic) cat("il y a",nbl,"lignes de données dans",nomFic,"\n") } # fin si } # fin de fonction traiteFichierUne fois la fonction traiteFichier() mise au point, on peut écrire -- même si ce n'est pas la seule et la meilleure solution -- une boucle pour passer en revue une série de fichiers :
# traitement d'une série de fichiers # à l'aide de la fonction traiteFichier() # désignation des fichiers (plusieurs exemples) nomsFichiers <- c("fichier1.txt","ficDeux.txt","fic_serie3.xtxt") nbfic <- 3 nomsFichiers <- paste("ficSerie",sprintf("%02d",1:nbfic),".txt",sep="") for (nomfic in nomsFichiers) { traiteFichier( nomfic ) } # fin pour nomficAfin d'obtenir un tableau résumé, il faut prévoir un data.frame pour accueillir les résultats et modifier la fonction traiteFichier() pour qu'elle renvoie un résultat par fichier. On remplit alors au fur et à mesure la structure d'accueil dans la boucle. Une fois la boucle terminée, on peut afficher les résultats et les sauvegarder.
Voici la fonction modifiée :
# exemple de traitement d'un fichier # réduit à la détermination de son nombre de lignes # on renvoie ce nombre de lignes # ou NA si le fichier n'est pas vu traiteFichier <- function( nomFic ) { cats(paste("Traitement du fichier",nomFic)) if (!file.exists(nomFic)) { cat("fichier",nomFic,"non vu.\n") nbl <- NA } else { dataFic <- read.table(nomFic,head=TRUE) nbl <- nrow(dataFic) cat("il y a",nbl,"lignes de données dans",nomFic,"\n") } # fin si return(nbl) } # fin de fonction traiteFichierLe script qui comporte la boucle et la gestion du data.frame est le suivant :
# traitement d'une série de fichiers # à l'aide de la fonction traiteFichier() # on sauvegarde dans un tableau l'information # renvoyée (le nombre de lignes), on l'affiche # et on l'exporte dans un fichier .CSV # désignation des fichiers nbfic <- 3 nomsFichiers <- paste("ficSerie",sprintf("%02d",1:nbfic),".txt",sep="") # structure d'accueil des résultats (data frame) tabRes <- data.frame(matrix(NA,nrow=nbfic+1,ncol=2)) names(tabRes) <- c("fichier","nombre de lignes") # boucle de traitement et remplissage de la structure d'accueil idf <- 0 # numéro courant de fichier for (nomfic in nomsFichiers) { idf <- idf + 1 nblc <- traiteFichier( nomfic ) tabRes[ idf, 1 ] <- nomfic tabRes[ idf, 2 ] <- nblc } # fin pour nomfic cats("Analyse globale des fichiers") # ajout de la moyenne sur l'ensemble des fichiers tabRes[ (nbfic+1), 1 ] <- "moyenne" tabRes[ (nbfic+1), 2 ] <- mean(as.numeric(tabRes[(1:nbfic),2])) # affichage et export cat("Voici le tableau résumé\n") print(tabRes) nomCsv <- "nblRes.csv" write.csv(x=tabRes,file=nomCsv) cat("\nrésultats écrits dans",nomCsv,"\n")Exemple d'exécution avec les fichiers cités :
Traitement du fichier ficSerie01.txt ==================================== il y a 100 lignes de données dans ficSerie01.txt Traitement du fichier ficSerie02.txt ==================================== il y a 30 lignes de données dans ficSerie02.txt Traitement du fichier ficSerie03.txt ==================================== il y a 40 lignes de données dans ficSerie03.txt Analyse globale des fichiers ============================ Voici le tableau résumé fichier nombre delignes 1 ficSerie01.txt 100.00000 2 ficSerie02.txt 30.00000 3 ficSerie03.txt 40.00000 4 moyenne 56.66667 résultats écrits dans nblRes.csvFichier CSV produit :
"","fichier","nombre delignes" "1","ficSerie01.txt",100 "2","ficSerie02.txt",30 "3","ficSerie03.txt",40 "4","moyenne",56.66666666666677. Comment et pourquoi éviter les boucles pour en R
Les boucles, et en particulier les boucles POUR, sont souvent le moyen de réaliser des calculs par itération dans les langages de programmation "classiques".
CE N'EST PAS LE CAS EN R parce que R est un langage vectoriel. De nombreuses fonctions s'appliquent directement aux vecteurs, comme sum() ; pour les matrices et les listes, il faut utiliser des fonctions comme apply(), lapply(), sapply() et tapply()... Et il y a beaucoup d'autres fonctions prévues pour couvrir de nombreux autres cas, comme table(), split(), sweep()... Nous verrons à la séance 6 comment on arrive à se passer des boucles et à la séance 8 comment optimiser des boucles si on doit vraiment les utiliser.
8. Spécificités du langage R
Une programmeuse, un programmeur C ou Java "classique" a l'habitude des boucles POUR dès qu'il s'agit de parcourir une structure. En R , il y a beaucoup d'autres solutions et il faut du temps pour les connaitre. Ainsi, et même si ce n'est pas "la" bonne solution, pour obtenir la variable précédente agesH qui contient les ages de hommes, on peut se contenter en R d'utiliser le filtrage vectoriel et écrire
agesH <- df$AGE[ df$SX==1 ]ou, de façon plus lisible :
agesH <- with(df, AGE[ SX==1 ] )Il est clair qu'on est loin de l'écriture d'une boucle explicite. Pour générer une liste avec les ages des hommes dans une composante et les ages des femmes dans une autre composante, on aurait pu utiliser la fonction split().
Cela permettrait ensuite d'utiliser lapply() pour effectuer des calculs composantes par composantes. Et si on était encore plus fort(e), on utiliserait tapply() qui fait tout en mémoire, comme par exemple :
# lecture des fonctions (gH) source("http://forge.info.univ-angers.fr/~gh/wstat/statgh.r",encoding="latin1") # lecture des données urld <- "http://forge.info.univ-angers.fr/~gh/wstat/Introduction_R/personnes.dar" pers <- read.table(urld,header=TRUE,row.names=1,encoding="latin1") # on crée une liste avec une composante pour les hommes # et une composante pour les femmes decoupe <- split(x=pers$AGE,f=pers$SEXE) # on peut alors utiliser lapply print( lapply(X=decoupe,FUN=mean) ) # mais sapply est mieux : print( sapply(X=decoupe,FUN=mean) ) # en fait, tapply fait tout cela en une ligne : cats("Résultats avec tapply") print( moys <- tapply( X=pers$AGE,INDEX=pers$SEXE, FUN=mean ) ) # écriture dangereuse # par croisement cats("Résultats par croisement") print( moys <- tapply( X=pers$AGE,INDEX=list(pers$SEXE,pers$ETUD),FUN=mean ) )$Femme [1] 32.6 $Homme [1] 37.05 Femme Homme 32.60 37.05 Résultats avec tapply ===================== Femme Homme 32.60 37.05 Résultats par croisement ======================== Niveau_bac Secondaire Supérieur Femme 29.75000 37.60000 19.00000 Homme 34.16667 43.36364 19.66667Même lorsqu'on veut utiliser des boucles POUR en R il faut se méfier. Ainsi l'expression for (i in 1:length(V)) n'est pas une "bonne" écriture. En effet, si le vecteur V est vide, sa longueur est nulle. Dans un langage classique, une boucle de 1 à 0 n'est pas exécutée. Pour R, la notation 1:0 signifie qu'on veut aller de 1 à 0 et il exécutera la boucle ! Ce qui est pire, c'est que V[i] ne renverra pas alors forcément d'erreur lorsque i vaut 0. Il est donc conseillé d'utiliser for (i in seq_along(V)) pour parcourir les indices de V car si V est vide, R ne rentrera pas dans la boucle avec cette écriture.
Exercices : énoncés solutions [Retour à la page principale du cours]
Retour à la page principale de (gH)