Valid XHTML     Valid CSS2    

Introduction à la programmation R (exercices)

Séance 3 : Conditions logiques et tests

                     gilles.hunault "at" univ-angers.fr

 

Table des matières cliquable

  1. Comparaison de valeurs (1)

  2. Comparaison de valeurs (2)

  3. Filtrage vectoriel

  4. Vérification de valeurs dans un fichier Excel

  5. Remplacements dans un vecteur

  6. Utiliser AND ou imbriquer ?

  7. Fonctions à résultats logiques de R

 
Il est possible d'afficher toutes les solutions via ?solutions=1 et de toutes les masquer avec via ?solutions=0.

 

1. Comparaison de valeurs (1)

On dispose de deux variables nommées valA et valB. Sans utiliser les fonctions de R, écrire des instructions qui mettent dans la variable petit la plus petite variable (la variable de plus petit contenu) et dans grand la plus grande variable. Donner ensuite la "bonne" solution avec les fonctions de R.

Solution :  

Un seul test logique suffit ici :


     #################################################
     #                                               #
     # à partir de valA et valB,                     #
     # on met dans petit la plus petite valeur       #
     #     et dans grand la plus grande valeur       #
     #                                               #
     #################################################
     
     #################################################
     # solution 1 sans fonction de R
     #################################################
     
     if (valA<valB) {
       petit <- valA
       grand <- valB
     } else {
       petit <- valB
       grand <- valA
     } # fin si
     
     #################################################
     # solution 2 toujours sans fonction de R
     #################################################
     
     petit <- valB
     grand <- valA
     if (valA<valB) {
       petit <- valA
       grand <- valB
     } # finsi
     
     #################################################
     # solution 3 avec fonctions de R
     #################################################
     
     petit <- min( valA, valB )
     grand <- max( valA, valB )
     

 

2. Comparaison de valeurs (2)

On dispose de trois variables nommées valA, valB et valC. Sans utiliser les fonctions de R, écrire des instructions qui mettent dans la variable tpetit la plus petite variable, dans milieu la variable intermédiaire et dans tgrand la plus grande variable. Donner ensuite la "bonne" solution avec les fonctions de R.

Et si on avait plus de trois valeurs à trier ?

Remarque : on supposera les valeurs distinctes.

Solution :  

Ici, les choses se compliquent car il y a 6 cas à envisager...


     #################################################
     #                                               #
     # à partir de valA, valB et valC,               #
     # on met dans tpetit la plus petite valeur      #
     #        dans milieu la valeur du milieu        #
     #     et dans tgrand la plus grande valeur      #
     #                                               #
     #################################################
     
     #################################################
     # solution 1 sans fonction de R (et sans else)
     #################################################
     # les cas sont triés par ordre alphabétique :
     #  valA valB valC puis valA valC valB etc.
     
     if ( (valA<valB) & (valB<valC) ) {
       tpetit <- valA
       milieu <- valB
       tgrand <- valC
     } # fin si cas 1
     
     if ( (valA<valC) & (valC<valB) ) {
       tpetit <- valA
       milieu <- valC
       tgrand <- valB
     } # fin si cas 2
     
     if ( (valB<valA) & (valA<valC) ) {
       tpetit <- valB
       milieu <- valA
       tgrand <- valC
     } # fin si cas 3
     
     [...] # il y a 6 cas en tout à écrire
     
     if ( (valC<valA) & (valA<valB) ) {
       tpetit <- valC
       milieu <- valA
       tgrand <- valB
     } # fin si cas 6
     
     ####################################################
     # solution 2 sans fonction de R avec des si emboités
     ####################################################
     
     if (valA<valB) {
        if (valB<valC) {
           tpetit <- valA
           milieu <- valB
           tgrand <- valC
        } else {
           tpetit <- valA
           milieu <- valC
           tgrand <- valB
        } # fin si valB<valC
     } else {
        if (valA<valC) {
        [...] # il y a 6 cas en tout à écrire
     
     } # fin si valA<valB
     
     #################################################
     # solution 3 sans fonction de R avec si en cascade
     #################################################
     
     if ( (valA < valB) & (valB < valC) ) {
        # ordre valA, valB, valC
        } else { if ((valA < valC) et (valC < valB) ) {
                 # ordre valA, valC, valB
                 } else { if ((valB < valA) et (valA < valC)) {
                          # ordre valB, valA, valC
                          } else { if ((valB < valC) et (valC < valA))
                                [...]
                                } # fINSI (valB < valC) et (valC < valA)
                          } # finsi (valB < valA) et (valA < valC)
                 } #  FINSI (valA < valC) et (valC < valB)
     } # FINSI (valA < valB) et (valB < valC)
     
     
     #################################################
     # solution 4 -- non évidente -- avec fonctions de R
     #################################################
     
     tpetit  <- min( valA, min(valB,valC) )
     milieu  <- min( max(valA,valB), min( max(valA,valC),max(valB,valC) ) )
     tgrand  <- max( valA, max(valB,valC) )
     
     #################################################
     # solution 5 avec fonctions de R
     #################################################
     
     tri     <- sort( c(valA,valB,valC) )
     tpetit  <- tri[ 1 ]
     milieu  <- tri[ 2 ]
     tgrand  <- tri[ 3 ]
     

La dernière solution, qui met tous les éléments dans un vecteur et utilise la fonction sort() pour trier les éléments est la plus générale et couvre aussi le cas de variables égales. Ouf, pas besoin de faire 3, 4, 5... structures SI emboitées !

On notera que pour vérifier ces algorithmes on ne peut pas se contenter de vérifier que cela "marche" sur un exemple. Il faut bien tester les 6 cas avant d'être sûr de la validité de l'algorithme (ou du programme). Ainsi l'instruction "tentante"


        milieu  <- min( max(valA,valB), max(valB,valC) )
     

met bien "ce qu'il faut" dans milieu dans 4 cas sur 6. Si on ne vérifie pas tout, ce qui est fastidieux car il faut être exhaustif, on risque de s'en rendre compte après avoir exécuté le programme plusieurs fois et avoir utilisé de faux résultats.

Pour les plus courageux et les plus courageuses, on pourra essayer de refaire cet exercice si on ajoute un ou plusieurs NA dans le vecteur...

 

3. Filtrage vectoriel

Soit v le vecteur défini par  

 v  <- c( 1, 8, 2, -5, 15, 6, 9, -1, 4) 

Donner les expressions R qui fournissent :

  • le dernier élément de v ;

  • les trois derniers éléments de v ;

  • les éléments de v de rang pair.

Solution :  

Puisque length(v) est à la fois la longueur et l'indice du dernier élément de v, v[ length(v) ] est le dernier élément de v.

Pour les trois derniers éléments de v, deux solutions au moins sont faciles à trouver :


     # les trois derniers éléments de v, solution 1
     
     lng <- length(v)
     v[ (lng-2) : lng ]
     
     # les trois derniers éléments de v, solution 2
     
     lng <- length(v)
     v[ - (1:(lng-3)) ]
     

Dans la mesure où R dispose de l'opérateur %% qui renvoie le modulo, c'est-à-dire le reste de la division entière, les éléments de v de rang pair sont a priori donnés par l'expression


     v[ 1:length(v) %% 2 == 0 ]
     

car "pair" équivaut à "dont le reste de la division par 2 est zéro". Toutefois cette expression est peu lisible et certainement peu compréhensible, sauf à bien connaitre la priorité entre opérateurs. Ajouter des parenthèses ne simplifie en rien l'expression :


     v[ ((1:length(v)) %% 2) == 0 ]
     

Il est au final beaucoup plus lisible d'écrire cela en plusieurs affectations :


     ind <- 1:length(v)     # indices de v
     rem <- ind %% 2        # restes de la division par 2
     print( v[ rem == 0 ] ) # affichage pour les rangs pairs
     

Attention à la notation 1:length(v) car elle réserve des suprises si le vecteur est de longueur nulle. On lui préfère en général la fonction seq_along() :


     # définissons V
     
     >  v  <- c( 1, 8, 2, -5, 15, 6, 9, -1, 4)
     
     # voici ses indices
     
     > 1:length(v)
       1 2 3 4 5 6 7 8 9
     
     # et avec seq_along() ?
     
     > seq_along(v)
       1 2 3 4 5 6 7 8 9
     
     ## c'est la catastrophe si length(v) renvoie zéro :
     
     # un nouveau v
     
     > v <- NULL
     
     # seq_along() est alors correct
     
     > seq_along(v)
       integer(0)
     
     # mais pas la séquence avec length(v)
     
     > 1:length(v)
       1 0
     

 

4. Vérification de valeurs dans un fichier Excel

On veut lire le fichier Excel nommé elfNA.xls et savoir combien de cases sont non vides pour la colonne AGE. Comment réaliser cela avec R ?

Solution :  

Une case vide correspond à la valeur NA. Il suffit donc d'écrire :


     # lecture du fichier Excel
     
     library(gdata)
     elf <- read.xls("http://forge.info.univ-angers.fr/~gh/wstat/Programmation_R/Programmation_introduction/elfNA.xls")
     
     ## nombre de cases non vides (solution 1)
     ## --------------------------------------
     
     #  comptage de cases vides pour la colonne AGE
     
     nbcv <- sum( is.na(elf$AGE) )
     
     # on en déduit le nombre de cases non vides
     
     nv <- length(elf$AGE) - nbcv
     
     ## nombre de cases non vides (solution 2)
     ## --------------------------------------
     
     nv <- length( na.omit(elf$AGE) )
     

On remarquera que la deuxième solution n'utilise pas de test logique en apparence.

 

5. Remplacements dans un vecteur

On voudrait convertir les cases non vides de la colonne AGE et remplacer par "jeune" tout AGE<20 et par "vieux" sinon. Quel code R faut-il écrire ?

Reprendre ensuite avec "jeune" pour AGE<20, "adulte" pour AGE entre 20 et 60 (bornes incluses) et "vieux" pour AGE>60.

On pourra simuler 50 nombres entiers entre 1 et 99 pour AGE à l'aide de l'expression :


     AGE <- round( runif(50,min = 1,max=99) )
     

Solution :  

R est riche en fonctions. Si une solution avec ifelse() convient pour deux valeurs, il faut avoir recours à cut() pour plus de deux modalités, sachant que le remplacement par filtre vectoriel est sans doute plus simple à lire :


     #############################################
     #                                           #
     #  conversions d'age en classes d'age       #
     #                                           #
     #############################################
     
     
     # solution A1 pour jeune<20 et vieux sinon
     # ---------------------------------------
     
     ageCL1 <- ifelse(age<20,"jeune","vieux")
     
     # solution A2 pour jeune<20 et vieux sinon
     # ---------------------------------------
     
     ageCL2 <- rep("vieux",length(age))
     ageCL2[ is.na(age) ] <- NA
     ageCL2[ age<20     ] <- "jeune"
     
     identical(ageCL2, ageCL2) # renvoie TRUE
     
     ## =======================================================
     
     # solution B1 pour jeune<20, adulte entre 20 et 60, vieux>60
     # ---------------------------------------------------------
     
     ageCL3 <- ifelse(age<20,"jeune",ifelse(age>60,"vieux","adulte"))
     
     # solution B2 pour jeune<20, adulte entre 20 et 60, vieux>60
     # ---------------------------------------------------------
     
     ageCL4 <- cut(x=age,
                   breaks=c(0,20,60,100),
                   labels=c("jeune","adulte","vieux")
                  ) # fin de cut
     
     identical(ageCL3, ageCL4) # renvoie FALSE : ageCL4 est un facteur
     identical(factor(ageCL3,levels=c("jeune","adulte","vieux")),ageCL4) # TRUE
     
     # solution B3 pour jeune<20, adulte entre 20 et 60, vieux>60
     # ---------------------------------------------------------
     
     ageCL5 <- rep("vieux",length(age))
     ageCL5[ is.na(age) ] <- NA
     ageCL5[ age<=60    ] <- "adulte"
     ageCL5[ age<20     ] <- "jeune"
     
     # solution B4 pour jeune<20, adulte entre 20 et 60, vieux>60
     # ---------------------------------------------------------
     
     ageCL6 <- rep("adulte",length(age))
     ageCL6[ age<20     ] <- "jeune"
     ageCL6[ age>60     ] <- "vieux"
     ageCL6[ is.na(age) ] <- NA
     
     # solution B5 pour jeune<20, adulte entre 20 et 60, vieux>60
     # ---------------------------------------------------------
     
     ageCL7 <- rep(NA,length(age))
     ageCL7[ age<20                    ] <- "jeune"
     ageCL7[ (age>=20) & (age<=60)     ] <- "adulte"
     ageCL7[ age>60                    ] <- "vieux"
     
     

Remarques : lorsque la fonction ifelse() rencontre NA, elle renvoie NA. Si on ne fournit pas de labels à la fonction cut(), elle utilise les bornes de classes comme labels où les parenthèses signifient borne non comprise alors que les crochets signifient borne comprise comme dans :


     > print(levels(ageCL4))
     
     "(0,20]"   "(20,60]"  "(60,100]"
     
     

 

6. Utiliser AND ou imbriquer ?

En logique mathématique, on apprend que a ET b est équivalent à b ET a . Est-ce que pour R les codes a & b et b & a sont équivalents ? Comment le prouver ou au contraire l'infirmer, sachant que a et b sont des expressions R ? Quelle est la différence entre & et && en R  ?

Solution :  

La logique mathématique et le traitement des expressions logiques en informatique sont très proches mais différents. Mathématiquement, pour évaluer a ET b, on évalue a puis on évalue b et enfin on effectue le ET logique sur les deux résultats précédents d'évaluation.

C'est exactement ce que fait R quand il rencontre a & b. Par contre, l'évaluation du code a && b se fait sur le mode coupe-circuit : si a renvoie FAUX, ce n'est pas la peine d'évaluer b parce que FAUX et n'importe quoi renvoie toujours FAUX. Donc les expressions a && b et b && a ne sont pas strictement équivalentes alors que a & b et b & a sont "assez" équivalentes pour des expressions simples.

Il faut donc plutot interpréter le code


     if (a && b)
         traitement()
     } # fin si sur a et b
     

comme le code suivant, avec une imbrication, où on voit bien qu'il n'y a pas symétrie d'utilisation entre a et b :


     if (a) {
       if (b) {
         traitement()
       } # fin si sur b
     } # fin si sur a
     

C'est pourquoi vous ne devez jamais utiliser && avec des vecteurs, comme le montre le code suivant, sous peine d'obtenir non pas un vecteur de résultats logiques calculés terme à terme mais juste une seule valeur logique  :


     # comparaison incorrecte de vecteurs
     # à cause du mode "court-circuit"
     
     > print( c(TRUE,FALSE) && c(TRUE,TRUE) )
       TRUE
     
     # comparaison correcte de vecteurs
     
     > print( c(TRUE,FALSE) & c(TRUE,TRUE) )
       TRUE FALSE
     

Si maintenant a et b sont des appels de fonctions qui modifient des variables globales (il s'agit bien sûr ici d'une hérésie qui mérite le bûcher, mais cela risque peut-être un jour de vous arriver, donc autant le savoir pour mieux l'éviter), alors il y a encore moins équivalence entre a & b et b & a comme le montre le code suivant où a correspond à l'appel de f() et b à celui de g() :


     x <- 0
     
     f <- function() { cat("f ") ; x <<-  100   ; return( FALSE) }
     g <- function() { cat("g ") ; x <<- "oui"  ; return( FALSE) }
     
     if ( f() & g() ) { NULL }
     cat("1. a et b, x vaut ",x,"\n")
     
     if ( g() & f() ) { NULL }
     cat("2. a et b, x vaut ",x,"\n")
     
     if ( f() && g() ) { NULL }
     cat("3. a et b, x vaut ",x,"\n")
     
     if ( g() && f() ) { NULL }
     cat("4. a et b, x vaut ",x,"\n")
     

Voici le résultat de son exécution -- si vous ne le saviez pas, l'expression <<- utilise l'environnement parent, donc ici x est une variable globale  :


     f g  1. a et b, x vaut  oui
     g f  2. a et b, x vaut  100
     
     f 3. a et b, x vaut  100
     g 4. a et b, x vaut  oui
     

On voit clairement qu'avec & les deux fonctions sont appelées alors qu'avec && seule la première fonction est utilisée. De plus à chaque fois on n'obtient pas le même résultat suivant qu'on commence par a ou par b.

 

7. Fonctions à résultats logiques de R

Comment fait-on en R pour tester si un mot est présent dans une liste de mots ou dans un vecteur de mots avec la fonction grep() ? Et avec l'opérateur %in% ?

On veut savoir si au moins un des éléments du vecteur V numérique est positif. Trouver une solution basée sur V>0 et sum. Pourquoi la fonction any() est-elle plus adaptée ?

On veut être sûr que tous les éléments du vecteur V numérique sont positifs. Trouver une solution basée sur V>0, sum et length(V). Pourquoi la fonction all() est-elle plus adaptée ?

A quoi sert la fonction all.equal() ?

Solution :  

La fonction grep() permet de connaitre la ou les positions d'un mot dans une liste ou dans un vecteur de mots comme on peut le voir avec le résultat d'exécution ci-dessous :


     # recherche d'un mot dans une liste ou un vecteur de mots
     
     > mot <- "le"
     > ldm <- strsplit("le chat et le chien courent.", " ")
     
     > print(ldm)
     [[1]]
     [1] "le"       "chat"     "et"       "le"       "chien"    "courent."
     
     > grep(mot,unlist(ldm))    # liste
       1 4
     
     > grep("che",ldm[[1]])     # vecteur
       integer(0)
     
     > grep("t$",ldm[[1]])      # expression régulière
       2 3
     
     > grep("bateau",ldm[[1]])  # pas vu !
       integer(0)
     

Le premier résultat renvoyé est 1 4 parce que "le" est trouvé comme premier et quatrième mot dans la liste ldm. Le deuxième résultat renvoyé montre que grep() sait aussi rechercher des sous-chaines : le vecteur résultat 2 5 signifie que "ch" est trouvé dans les deuxième et cinquième mots du vecteur ldm[[1]]. grep() sait utiliser les expressions régulières : le troisième résultat indique que les mots 2 et 3 finissent par "t", ce qui se décrit par l'expression régulière t$.

Lorsque la fonction grep() ne trouve pas ce qu'on cherche, elle renvoie integer(0) c'est-à-dire un vecteur de longueur 0. Il suffit donc d'écrire if (length(grep(...)>0) pour savoir si la recherche a été fructueuse.

Si on veut juste vérifier que le mot est exactement présent dans un vecteur, on peut utiliser l'opérateur %in% :


     # teste de la présence d'un mot dans un VECTEUR de mots
     
     > mot <- "le"
     > vec <- strsplit("le chat et le chien courent.", " ")[[1]]
     
     > if ( mot %in% vec ) { print("vu !") }
       vu !
     
     > print( "ch" %in% vec )
       FALSE
     
     > print( "bateau" %in% vec )
       FALSE
     

Dans les exercices de la séance 8, on montrera que %in% est vraiment beaucoup plus rapide que grep().

Pour savoir si au moins un des éléments du vecteur V numérique est positif, on peut se contenter, pour des vecteurs de petite longueur, de l'expression if (sum(V>0)>0). En effet, V>0 renvoie un ensemble de valeurs logiques TRUE et FALSE remplacés automatiquement par 1 et 0 à cause de l'appel de sum(). S'il y a au moins un élément positif, il y a au moins un TRUE donc au moins un 1, et la somme est forcément positive.

Cette solution n'est pas optimisée parce qu'on teste tous les éléments de V. Avec un vecteur d'un million d'éléments tous négatifs sauf le premier, on effectue un million de tests. C'est pourquoi la fonction any() est plus adaptée et sans doute plus rapide : elle renvoie VRAI dès qu'elle trouve un élément correspondant (mais sans dire où ; c'est l'expression which(V>0) qui en donne les positions). Dans les exercices de la séance 8, on calculera à quel point any(v>0) est plus rapide que sum(v>0)>0.

De la même façon, tous les éléments de V sont positifs si sum(v>0) est égal à la longueur de V. On peut donc le tester via if (sum(V>0)==length(V)) pour des vecteurs de petite longueur ou prendre l'habitude d'utiliser dès le départ if (all(V>0)).

Comme son nom l'indique, la fonction all.equal() permet de tester si deux objets sont égaux. Si vous vous souvenez bien, au chapitre précédent, nous avions, dans le calcul des contributions relatives pour la comparaison d'effectifs observés et d'effectifs théoriques, deux variables numériquement égales, mais l'une était nommée pas l'autre. La fonction all.equal() est capable de l'indiquer :


     > print( identical( tdk2$statistic, sum(df$Cntr)) )
       FALSE
     
     > print( identical( as.numeric(tdk2$statistic), sum(df$Cntr)) )
       TRUE
     
     > all.equal( tdk2$statistic, sum(df$Cntr) )
       "names for target but not for current"
     

De même qu'elle peut détecter des différences de longueur, de structure... :


     > all.equal( 1:10, 1:11 )
       "Numeric: lengths (10, 11) differ"
     
     > all.equal( 1:10, list(1:10) )
       "Modes: numeric, list"  "Lengths: 10, 1"  "target is numeric, current is list"
     

Si vous pensez avoir construit deux variables complétement équivalentes, surtout s'il s'agit de structures, all.equal() peut vous le dire ou expliquer ce qu'elle trouve de différent... une fois qu'identical() est passé par là, bien sûr.

Voici donc quelques autres détections obtenues grâce à all.equal() :


     
     > all.equal( 1:10, c(0,1:9) )
       "Mean relative difference: 0.1818182"
     
     > all.equal( 1, "1" )
       "Modes: numeric, character"
       "target is numeric, current is character"
     
     > all.equal( list(1:5), list(x=1:5) )
       "names for current but not for target"
     
     > all.equal( list(x=1:5), list(y=1:5) )
       "Names: 1 string mismatch"
     
     > all.equal( 1:2, c(1L,2L) )
       TRUE
     
     > all.equal( 1:2, c(1.0,2) )
       TRUE
     
     > all.equal( 1:2, c(1,2) )
       TRUE
     
     # allez, une petite surprise pour la route :
     
     > identical(1:2,c(1,2))
       FALSE
     

 

 

Code-source php de cette page. Retour à la page principale du cours.

 

 

retour gH    Retour à la page principale de   (gH)