Valid XHTML     Valid CSS2    

Introduction à la programmation avec R

                gilles.hunault "at" univ-angers.fr

Cours 4 - Boucles et itérations

 

Table des matières cliquable

  1. Boucles "TANT QUE"

  2. Boucles "POUR"

  3. Boucles "REPETER JUSQU'A"

  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

  7. Comment et pourquoi éviter les boucles pour en R

  8. Spécificités du langage 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 QUE
     

Son é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 nomFic
     
     

2. 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
     
     FINPOUR
     

Là 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 5
     

Dans 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 indcol
     

3. 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 :
          0
     

5. 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 indi
     

et 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  1000000
     

Le 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 :
          0
     

Comme 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 indCol
     

mais 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 col
     

6.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 indLing
     

6.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 traiteFichier
     

Une 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 nomfic
     
     

Afin 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 traiteFichier
     

Le 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.csv
     

Fichier CSV produit :


     "","fichier","nombre delignes"
     "1","ficSerie01.txt",100
     "2","ficSerie02.txt",30
     "3","ficSerie03.txt",40
     "4","moyenne",56.6666666666667
     

7. 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.66667
     

Mê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 gH    Retour à la page principale de   (gH)