Twitter icon


e-mail: enseignement(at)sodad.com

La fouille de données est un domaine peut-être mal connu des professeurs du secondaire, et pourtant il fait aujourd'hui son entrée dans les programmes du lycée. Nous pouvons aider les enseignants à s'approprier cette nouvelle matière et les outils qui s'y rapportent.

Le document Ressources pour la classe terminale générale et technologique, probabilités et statistique indique que le logiciel R, entre autres, sera préconisé par le ministère dans le cadre des TICE en rapport avec l'enseignement des statistiques à partir de la rentrée 2012.

R désigne autant un langage que le logiciel éponyme qui en assure la mise en œuvre. Il est principalement utilisé par les chercheurs et par les professionnels. Vouloir l'utiliser dans le cadre du lycée demandera certainement un effort de formation préalable en direction des enseignants ce qui pourra être aussi l'occasion d'une sensibilisation au domaine de la fouille de donnée (data mining), sujet potentiel de travaux pratiques.

Nous espérons ici mettre en lumière la nécessité d'un tel effort en nous appuyant dans une certaine mesure sur le contenu du document mentionné plus haut (plus précisément sur la partie comprise entre les pages 57 et 65). Dans ce document on mentionne les différentes modalités d'utilisation du logiciel, à la console (en saisissant des expressions du langage) ou via des interfaces graphiques disponibles sous forme de packages. On donne également quelques exemples de scripts R, malheureusement sans mettre suffisamment en évidence les particularités du langage qui peuvent rendre son utilisation par les lycéens difficile.

R n'échappe pas à la règle qui stipule qu'aucun outil n'est simple lorsque les conditions nécessaires à son apprentissage ne sont pas réunies. Ici la maîtrise de l'algorithmique de base n'est qu'une condition sine qua non. A cela il faut ajouter la maitrise de la manipulation des différents types disponibles (booléen, numérique, caractère, ...) et ainsi que des structures de données de R. Mais il faut surtout y adjoindre la connaissance du calcul matriciel, car le langage R repose précisément sur cela et l'écriture de code procédural y relève souvent de la maladresse, voire de la méconnaissance des principes qui le sous-tendent.

Avec R, si on est amené à écrire une boucle dont le nombre d'itérations est un peu élevé (disons quelques milliers), les performances du programme ainsi conçu peuvent ne pas être au rendez-vous. Si les boucles sont imbriquées, il se peut qu'un temps de calcul rédhibitoire mène à une impasse. Là, autant que faire se peut, il faut remplacer les itérations par des manipulations de matrices. Les élèves fraîchement confrontés à ces nouveaux objets mathématiques pourront-ils facilement procéder à ce changement de paradigme, de l'itératif vers le matriciel ?

En guise d'illustration des particularités du langage, voici un petit exemple tiré du document. En page 63 on tombe sur l'exemple de fonction suivant:



qui est reproduit ici pour plus de commodité:

pileface <- function (nbsim = 2000) {
    resultats <- rep (NA, 3)
    names (resultats) <- c ("deuxpiles", "deuxfaces", "autres")
    for (i in 1:nbsim) {
        pieceA <- sample(c("Pile", "Face"), 1)
        pieceB <- sample(c("Pile", "Face"), 1)
        if (pieceA == "Pile" & pieceB == "Pile") {
            resultats [1] <- resultats [1] + 1
        } else {
            if (pieceA == "Face" & pieceB == "Face") {
                resultats[2] <- resultats[2] + 1
            } else {
                resultats[3] <- resultats[3] + 1
            }
        }
    }
    print (resultats)
    print (resultats / nbsim)
    barplot (resultats / nbsim)
}


Il s'agit simplement d'effectuer deux tirages à pile ou face avec deux pièces un certain nombre de fois, et on compte les occurrences de combinaisons que l'on obtient.

Il faut noter que ce petit programme ne peut pas fonctionner, autrement dit il est bogué. Il suffit pour s'en convaincre de l'essayer:

> pileface ()

deuxpiles deuxfaces    autres
       NA       NA       NA
deuxpiles deuxfaces    autres
       NA       NA       NA
Error in plot.window(xlim, ylim, log = log, ...) :
  need finite 'ylim' values
In addition: Warning messages:
1: In min(x) : no non-missing arguments to min; returning Inf
2: In max(x) : no non-missing arguments to max; returning -Inf


L'erreur réside dans l'initialisation suivante:

resultats <- rep (NA, 3)

et quand on sait que NA + 1 = NA, on comprend ce qui se passe. Nous avons là une illustration de la nécessité d'appuyer l'utilisation du langage sur des bases fermes. En R il y a des types de données, et des valeurs particulières dont il faut bien saisir le comportement. NA ou NaN par exemple. L'erreur ici est simple et saute aux yeux dès lors que les concepts fondamentaux du langage ont été sérieusement acquis. Parfois ce type d'erreur est enfoui de manière beaucoup plus sournoise dans un programme plus long et plus compliqué, et sa résolution en sera d'autant plus coûteuse. Un truc au passage: il est possible d'utiliser la commande debug. Il serait navrant d'assister au naufrage de séances entières de travaux pratiques sous les assauts de bataillons de bugs irréductibles.

On repère facilement encore une autre erreur: l'emploi inapproprié de l'opérateur '&' (qui est un opérateur vectoriel) à la place de l'opérateur '&&'. Ici cela ne prête pas à conséquence, les opérandes étant de longueur 1. Il faut quand même corriger cela.

Voici une version modifiée:

pileface.debuguee <- function (nbsim = 2000) {
    resultats <- rep (0, 3)
    names (resultats) <- c ("deuxpiles", "deuxfaces", "autres")
    for (i in 1:nbsim) {
        pieceA <- sample(c("Pile", "Face"), 1)
        pieceB <- sample(c("Pile", "Face"), 1)
        if (pieceA == "Pile" && pieceB == "Pile") {
            resultats [1] <- resultats [1] + 1
        } else {
            if (pieceA == "Face" && pieceB == "Face") {
                resultats[2] <- resultats[2] + 1
            } else {
                resultats[3] <- resultats[3] + 1
            }
        }
    }
    print (resultats)
    print (resultats / nbsim)
    barplot (resultats / nbsim)
}

et qui marche:

> pileface.debuguee ()

deuxpiles deuxfaces    autres
      504       511       985
deuxpiles deuxfaces    autres
   0.2520    0.2555    0.4925


Le programme proposé est bien classique, sous une forme à laquelle les élèves sont habitués. On y voit une boucle : for (i in 1:nbsim). Autrement dit, on va répéter ce qui suit nbsim fois. Faisons un test, en donnant à nbsim la valeur aberrante ici de 2 millions:

> system.time (pileface.debuguee (2000000))

deuxpiles deuxfaces    autres
   500290    499604   1000106
deuxpiles deuxfaces    autres
0.250145  0.249802  0.500053
   user  system elapsed
49.475   0.055  49.701


L'exécution d'une boucle de deux millions d'itérations prend donc une cinquantaine de secondes (49.475) sur une machine standard (hp dv 7 sous linux Centos 6). Ce qui peut être acceptable ici ne l'est pas forcément dans d'autres situations: si cette itération est elle-même enfouie dans d'autres itérations, comme cela peut se rencontrer dans des procédures d'estimations distributionnelles que les statisticiens appellent bootstrap ou dans des simulations par la méthode de Monte Carlo. Si la boucle ci-dessus est embarquée au sein d'une autre boucle de 500 itérations par exemple le temps d'exécution avoisinera 7 heures.

Bien sûr pour l'exemple qui est proposé 2 000 000 itérations est une quantité extravagante. Mais ce cas est simplissime et certaines simulations un peu plus intéressantes peuvent engager des nombres d'itérations, éventuellement imbriquées, importants.

Voici la forme que prendrait cette même fonction, pour peu que l'on veuille bien se plier à la philosophie du langage:

pileface.plusrapide <- function (nbsim = 2000) {
    U <- c ("Pile", "Face") ## juste pour mémoire; on aurait bien pu écrire U <- 1:2
    tirageA <- sample (U, nbsim, replace = TRUE)
    tirageB <- sample (U, nbsim, replace = TRUE)
    tbl <- table (tirageA, tirageB)
    resultats <- c (tbl [1,1], tbl [2,2], tbl [1,2] + tbl [2,1])
    names (resultats) <- c ("deuxpiles", "deuxfaces", "autres")
    print (resultats)
    print (resultats / nbsim)
    barplot (resultats / nbsim)
}


Cette fois-ci le temps d'exécution est plus acceptable: 1 seconde.

> system.time (pileface.plusrapide (2000000))

deuxpiles deuxfaces    autres
   499370    500125   1000505
deuxpiles deuxfaces    autres
0.2496850 0.2500625 0.5002525
   user  system elapsed
  1.088   0.063   1.155


En utilisant le langage dans son mode naturel le temps d'exécution a été divisé par 50. Embarquée dans une boucle de 500 itérations conformément au scénario précédent, le temps d'exécution est réduit à un peu plus de 8 minutes.

Une chose encore. On peut tenter la modification suivante:

pileface.anim <- function (nbsim = 2000) {
    resultats <- rep (0, 3)
    names (resultats) <- c ("deuxpiles", "deuxfaces", "autres")
    for (i in 1:nbsim) {
        pieceA <- sample(c("Pile", "Face"), 1)
        pieceB <- sample(c("Pile", "Face"), 1)
        if (pieceA == "Pile" && pieceB == "Pile") {
            resultats [1] <- resultats [1] + 1
        } else {
            if (pieceA == "Face" && pieceB == "Face") {
                resultats[2] <- resultats[2] + 1
            } else {
                resultats[3] <- resultats[3] + 1
            }
        }
        barplot (resultats / nbsim)
    }
}


Pour l'essayer:

pileface.anim ()

Il s'agit là typiquement d'une situation où l'emploi de la boucle est justifié, et cette manière de faire est fréquemment utilisée pour créer des graphiques animés avec R.

Le bestiaire des différents types d'objets manipulés par R est foisonnant. Même sans parler des types environment, call, language ou encore des mécanismes par lesquels un modèle orienté objet est proposé, à la sauce S3 ou S4, toutes choses qui peuvent et doivent ne concerner que des utilisateurs réguliers de R. Mais même à un niveau très élémentaire, on ne peut pas faire l'impasse sur ce qu'est une liste. Un data.frame, par exemple, est une liste, tout comme la plupart des résultats produits par de nombreuses fonctions. On peut encore moins faire l'impasse sur le fonctionnement des vecteurs; sur leurs modes d'indexation, par filtre ou par indices; sur la règle du recyclage, qui permet de composer des vecteurs de dimensions différentes; sur les attributs, qui dotent les vecteurs de propriétés qui en font des matrices ou des tableaux multidimensionnels.

Par exemple, soit un vecteur de dimension 100 dont les composantes sont tirées aléatoirement selon une loi uniforme parmi les entiers naturels inférieurs à 11 :

X <- sample (1:10, 100, replace=TRUE)

Si l'on veut annuler toutes les composantes d'indice impair, on peut écrire :

X <- X * (0:1)

ou alors si l'on veut annuler toutes les composantes dont la valeur excède 5 :

X [ X > 5 ] <- 0

Tout cela est très simple pourvu que l'on ait eu la possibilité de bien enseigner les bases du langage et de s'assurer qu'elle étaient bien acquises.

Il existe toute une panoplie d'outils qui permettent de s'affranchir des boucles. Les fonctions ifelse, apply, tapply ou les opérateurs vectoriels n'en sont que quelques exemples. On trouve en page 64 le fragment de code suivant :

serieS3de <- array(data = NA, dim = c(6, 6, 6))
for(i in 1:6){
    for(j in 1:6){
        for(k in 1:6){
            serieS3de[i, j, k] <- i + j + k
        }
    }
}


Par l'usage de la fonction appropriée, ce bout de code pourra être remplacé par l'unique ligne suivante, beaucoup plus performante en terme de temps d'exécution :

serieS3de <- outer (1:6, outer (1:6, 1:6, FUN="+"), FUN="+")

Elle est objectivement moins lisible, et ici ce procédé paraît à juste titre artificiel au regard des petits nombres. Cependant c'est le genre de code qu'un élève ou un professeur pourrait trouver lors de la recherche d'exemples sur le net ou dans les sources des packages, et on peut être pénalisé si on ne sait pas l'interpréter.

Par curiosité, on peut exécuter ce code dans sa version boucle en remplaçant la valeur 6 par la valeur 300. Sur la même configuration que précédemment on observe un temps d'exécution de 104 secondes. Par comparaison la version avec outer consomme 1 seconde, soit un rapport supérieur à 100.

Pour le gain obtenu en renonçant aux boucles on peut être amené à payer le prix fort en terme de consommation de mémoire. De plus, ce ne sont pas tant les opérateurs vectoriels qui sont performants que l'interpréteur du langage qui est inefficace, et si des progrès sont accomplis de ce coté-là, beaucoup de ces observations deviennent caduques. Il est également possible de tester une version commerciale et en principe optimisée de R, REvolution.

Tous ces points abordés ici, il n'est pas déplacé de les soulever dans le cadre du lycée. Des logiciels comme R ou Scilab ouvrent les portes du monde de la simulation numérique, et quelques belles expériences peuvent être montées pour illustrer par exemple des phénomènes de convergence ou pour faire une initiation aux systèmes dynamiques. Mais pour que ces manipulations se déroulent dans de bonnes conditions il faut préalablement s'assurer d'une maîtrise suffisante des outils.

La fouille de données est un domaine d'avenir, riche d'applications et apte à satisfaire l'intelligence des individus les plus curieux. Par nature c'est un lieu où se rencontrent les sciences dures (mathématiques, biologie, ...) et humaines (histoire, sociologie, ...). Nous sommes entièrement disposés à assister les enseignants pour une bonne prise en main de R ou pour mettre en place des ateliers de statistiques, de fouille de données ou de simulation.



Sur ce sujet, n'hésitez-pas à nous contacter par mail via enseignement(at)sodad.com



 

SoDAD INSIGHT - Agropole - Deltagro 3, BP 350 - F-47931 AGEN Cedex 9 - FRANCE Email : contact@sodad.com