Certains bio-informaticiens ne jurent que par R (j'en fais partie). Je suis amoureux de sa simplicité (sic), son élégance (re-sic), sa documentation et ses innombrables packages tous plus utiles les uns que les autres. Et surtout c'est le seul langage que je maîtrise un peu convenablement, alors forcément je trouve tous les autres langages nuls, en toute objectivité.
Et pourtant R est universellement reconnu comme étant un langage de programmation ésotérique. Il a fallu tout le talent de Patrick Burns et de son livre pour que j'ouvre enfin les yeux et découvre qu'en fait R, c'est l'enfer.
Descendez donc avec moi dans des profondeurs abyssales au travers de quelques exemples librement inspirés de the R inferno.
R ne sait pas calculer
Définissons un joli vecteur :
1 |
> myVect <- c(0.1/1, 0.2/2, 0.3/3, 0.4/4, 0.5/5, 0.6/6, 0.7/7) |
Mais à quoi ressemble-t-il ?
1 2 |
> myVect [1] 0.1 0.1 0.1 0.1 0.1 0.1 0.1 |
Un bien beau vecteur. Vraiment. R saura-t-il trouver quelles valeurs sont égales à 0.1 ?
1 2 |
> myVect == 0.1 [1] TRUE TRUE FALSE TRUE TRUE FALSE FALSE |
Heu, bien joué R. Quatre bonnes réponses sur sept, c'est… encourageant. Surtout pour un programme de statistiques, très populaire, et très utilisé en bioinfo.
Reprenons :
1 2 3 4 5 6 |
> 0.1 == 0.1 [1] TRUE > 0.3/3 [1] 0.1 > 0.3/3 == 0.1 [1] FALSE |
Mais pourquoi donc ?
Le problème des nombres à virgule, c'est que la partie après la virgule peut être infinie, alors que la mémoire des ordinateurs ne l'est pas. Du coup, les ordinateurs décident plutôt de ne garder que quelques (dizaines de) nombres après la virgule, et d'oublier les autres. Ils font donc plein de petites erreurs de calcul débiles. Par défaut, R n'affiche pas tout ce qu'il sait d'un nombre, mais on peut lui demander d'en afficher plus (le nombre maximum de
peut dépendre de votre configuration) :
1 2 3 4 |
> print(0.1, digits = 22) [1] 0.10000000000000001 > print(0.3/3, digits = 22) [1] 0.099999999999999992 |
Et l'on voit bien en effet que pour notre langage préféré, 0.1 n'est pas égal a 0.3/3. Youpi. C'est génial.
Comme en fait c'était quand même gravos, les concepteurs de R, dans leur infinie sagesse, ont décidé de faire une fonction qui donne le résultat attendu, la mal nommée all.equal. Cette fonction compare (entre autre) si la différence entre deux nombres est moins importante qu'une petite tolerance.
1 2 |
> all.equal(0.3/3, 0.1) [1] TRUE |
Quelques détails :
- le paramètre tolerance vaut par défaut 1,5.10-8.
- Les concepteurs de R ont décidé que all.equal ne retournerait pas FALSE (parce que). Du coup, comme ils étaient quand même bien embêtés, ils ont créé la fonction isTRUE :
1> all.equal(1, 2)<br>[1] "Mean relative difference : 1"<br>> isTRUE(all.equal(1, 2))<br>[1] FALSE
- Pour rendre le tout vraiment infernal, all.equal n'est évidemment pas vectorisée, débrouillez-vous comme ça.
1> all.equal(myVect, 0.1)<br>[1] "Numeric : lengths (7, 1) differ"
R, le maître troll des langages de programmation…
R est en moyenne assez peu cohérent
Les fonctions min et max retournent respectivement le minimum et le maximum d'une série de nombres :
1 2 3 4 |
> min(-1, 5, 118) [1] -1 > max(-1, 5, 118) [1] 118 |
Jusque là, tout va bien. La fonction mean est utilisée pour trouver la moyenne :
1 2 |
> mean(-1, 5, 118) [1] -1 |
… Je pense que R devrait prendre quelques leçons de statistiques.
Sait-il calculer une médiane ?
1 2 |
> median(-1, 5, 118) "Error in median(-1, 5, 118) : unused argument (118)" |
Apparemment pas, mais au moins nous signale-t-il gentiment que quelque chose ne tourne pas rond.
Mais pourquoi donc ?
En fait, il vaut mieux utiliser ces fonctions sur des vecteurs, ce que tout le monde fait en pratique :
1 2 3 4 5 6 7 8 |
> min(c(-1, 5, 118)) [1] -1 > max(c(-1, 5, 118)) [1] 118 > mean(c(-1, 5, 118)) [1] 40.66667 > median(c(-1, 5, 118)) [1] 5 |
Notez tout de même que dans le premier cas, la fonction mean :
- ne retourne pas d’erreur.
- ne retourne pas de warning.
- retourne un résultat (faux) du type attendu (un nombre).
Quel machiavélisme ! Si l'utilisateur ne teste pas rigoureusement son code, il court droit à la catastrophe.
Le facteur numérique accidentel
Imaginons, par exemple, qu'un innocent vecteur numérique se retrouve par mégarde sous la forme d'un factor (par exemple, suite à une facétie des célèbres fonctions read.table ou data.frame, et de leur non moins célèbre paramètre stringsAsFactor dont la valeur par défaut est à l'origine de la majorité de mes bugs R). L'utilisateur averti pourrait se dire en toute confiance : "Pas de panique, je connais la fonction as.numeric".
Qui aurait pu imaginer qu'un tel désastre était imminent ?
1 2 3 4 5 6 7 8 9 10 11 |
> myVect <- factor(c(105 :100, 105, 104)) > myVect [1] 105 104 103 102 101 100 105 104 Levels : 100 101 102 103 104 105 > myVect >= 103 [1] NA NA NA NA NA NA NA NA "Warning message : In Ops.factor(myVect, 103) : ‘>=’ not meaningful for factors" # Haha, je connais la fonction as.numeric ! Tu ne m'auras pas comme ça, R ! > as.numeric(myVect) >= 103 [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE |
Mais pourquoi donc ?
1 |
as.numeric |
1 2 |
> as.numeric(myVect) [1] 6 5 4 3 2 1 6 5 |
Du coup, il faut passer par un p'tit coup de as.character :
1 2 |
> as.numeric(as.character(myVect)) [1] 105 104 103 102 101 100 105 104 |
Ou alors, si vous êtes pédant :
1 2 |
> as.numeric(levels(myVect))[myVect] [1] 105 104 103 102 101 100 105 104 |
L'arrondi du coin
Parfois, il est bien utile d’arrondir un peu tous ces nombres compliqués avec plein de virgules. La fonction round fait, en général, assez bien son travail :
1 2 3 4 |
> round(0.2) [1] 0 > round(7.86) [1] 8 |
Il y a toujours cette petite hésitation quant à savoir comment sera arrondi
1 |
0.5 |
, en
1 |
ou en
1 |
1 |
?
1 2 |
> round(0.5) [1] 0 |
OK. R fait donc partie de ces langages qui arrondissent
1 |
X.5 |
à l'entier inférieur. Pourquoi pas. Ce n'est pas ce qu'on m'a appris à l'école, mais bon, pourquoi pas ?
1 2 |
> round(1.5) [1] 2 |
Hein ?! Mais R enfin, qu'est-ce que tu fais ? C'est trop te demander d’être cohérent pendant deux lignes ?
1 2 3 4 |
> 0 :10 + 0.5 [1] 0.5 1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5 9.5 10.5 > round(0 :10 + 0.5) [1] 0 2 2 4 4 6 6 8 8 10 10 |
♪♫ Lalala ♫♪ Je suis R. Je fais ce que je veux. ♪♫ Lalala ♫♪ J’arrondis
1 X.5à l'entier pair le plus proche si je veux. ♪♫ Lalala ♫♪
R
Mais pourquoi donc ?
Si on arrondit un grand échantillonnage de…
Si on réfléchit bien, on s’aperçoit que…
…potentiel biais dans…
Et bien en fait…
Les auteurs de R ont…
Heu, bon, passons.
Une matrice dans un tableau
Il est possible d'inclure une
1 |
matrix |
comme colonne d'un
1 |
data.frame |
. Comparez donc :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
> myMat <- matrix(1 :6, ncol = 2) > myMat [,1] [,2] [1,] 1 4 [2,] 2 5 [3,] 3 6 > myDF1 <- data.frame(X = 101 :103, Y = myMat) > myDF2 <- data.frame(X = 101 :103) > myDF2$Y <- myMat > myDF1 X Y.1 Y.2 1 101 1 4 2 102 2 5 3 103 3 6 > myDF2 X Y.1 Y.2 1 101 1 4 2 102 2 5 3 103 3 6 > dim(myDF1) [1] 3 3 > dim(myDF2) [1] 3 2 > myDF1$Y NULL > myDF2$Y [,1] [,2] [1,] 1 4 [2,] 2 5 [3,] 3 6 |
Mais pourquoi ?
You will surely think that allowing a
1 data.frameto have components with more than one column is an abomination. That will be your thinking unless, of course, you’ve had occasion to see it being useful.
Patrick Burns
Vous penserez sûrement qu'autoriser des composants de plus d'une colonne dans un
1 data.frameest une abomination. Vous penserez cela, à moins bien sûr que vous n'ayez eu l'occasion de voir cela mis en pratique de façon utile.
L’échantillonnage approximatif
La fonction sample est bien pratique pour échantillonner au hasard un vecteur, avec ou sans remise :
1 2 3 |
> myVect <- c(2.71, 3.14, 42) > sample(myVect, size = 5, replace = TRUE) [1] 2.71 3.14 42.00 3.14 3.14 |
Le programmeur expérimenté se dira que ce bout de code a l'air dangereux dans le cas où
1 |
myVect |
est vide. En effet, mais R a la décence de nous prévenir :
1 2 3 4 |
> myVect <- numeric(0) > sample(myVect, size = 5, replace = TRUE) "Error in sample.int(length(x), size, replace, prob) : invalid first argument" |
Le cas où myVect ne contient qu'un unique élément semble bien moins problématique (hahaha ! Bande de naïfs) :
1 2 3 |
> myVect <- 3.14 > sample(myVect, size = 5, replace = TRUE) [1] 3 4 2 2 3 |
Notez bien :
- Pas de message d’erreur.
- Pas de warning.
- Un résultat sous la forme de 5 nombres, comme attendu.
Pourtant, ce n'était peux-être pas ce que vous désiriez. Je blêmis à l'idée du nombre de bugs non résolus causés par ce petit twist.
Mais pourquoi ?
Dans certains cas, c'est bien pratique de pouvoir faire
1 |
sample(100, 5) |
et d'obtenir :
1 |
[1] 59 70 30 23 58 |
R se montre compréhensif, et essaye de nous prévenir dans les petites lignes de bas de page du manuel :
If
1 xhas length 1, is numeric (in the sense of
1 is.numeric) and
1 x >= 1, sampling via
1 sampletakes place from
1 1 :x. Note that this convenience feature may lead to undesired behaviour when
1 xis of varying length in calls such as
1 sample(x).
1 help(sample)Si
1 xa une longueur de 1, est numérique (tel que défini par
1 is.numeric) et
1 x >= 1, l’échantillonnage via
1 samplea lieu sur
1 1 :x. Notez bien que cette caractéristique peut aboutir à un comportement indésiré lorsque la longueur de
1 xvarie lors d'appels tels que
1 sample(x).
Noitaréti à l'envers
Si vous essayez d'écrire en douce une petite boucle
1 |
for |
1 |
for |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
> myTable <- data.frame(x1 = 1 :5, x2 = letters[1 :5]) > myTable x1 x2 1 1 a 2 2 b 3 3 c 4 4 d 5 5 e > > for (i in 1 :nrow(myTable)) { + message(paste("row:", i)) + message(paste("x1:", myTable[i, "x1"], "x2:", myTable[i, "x2"])) + } row : 1 x1 : 1 x2 : a row : 2 x1 : 2 x2 : b row : 3 x1 : 3 x2 : c row : 4 x1 : 4 x2 : d row : 5 x1 : 5 x2 : e |
Que se passe-t-il si myTable est vide ?
1 2 3 4 5 6 7 8 9 10 11 12 |
myTable <- data.frame(x1 = NULL, x2 = NULL) > myTable data frame with 0 columns and 0 rows > > for (i in 1 :nrow(myTable)) { + message(paste("row:", i)) + message(paste("x1:", myTable[i, "x1"], "x2:", myTable[i, "x2"])) + } row : 1 x1 : x2 : row : 0 x1 : x2 : |
Mais pourquoi ?
Et oui,
1 |
nrow(myTable) |
vaut 0, et
1 |
1 :0 |
retourne
1 |
[1] 1 0 |
Ça alors, c'est pas de chance ! Préférez donc
1 |
seq_len(nrow(myTable)) |
.
Javascript, sort de ce coRps !
1 2 |
> 50 < "7" [1] TRUE |
Mais pourquoi ?
1 2 3 4 |
> 50 < as.numeric("7") [1] FALSE > "a" < "b" [1] TRUE |
Le message d’erreur le plus utile du monde
1 2 3 4 |
> i <- 1 > if (i == pi) message("pi!") > else message("papi!") Error : unexpected 'else' in "else" |
Mais pourquoi ?
If you aren’t expecting 'else' in "else", then where would you expect it ?
While you may think that R is ludicrous for giving you such an error message, R thinks you are even more ludicrous for expecting what you did to work.Patrick Burns
Si tu ne t’attends pas à un 'else' dans "else", où donc t'attends-tu à en voir un, R ?
Vous penserez peut-être que c'est ridicule pour R de retourner un tel message d'erreur. R pense que vous êtes encore plus ridicule d’espérer que ce que vous aviez fait fonctionnerait.
En vrac
- La fonction
1read.table1data.frame1table
- La fonction
1table
- La fonction sort.list ne sert pas à trier les listes.
-
1> seq(5 :10)
-
1[1] 1 2 3 4 5 6
(╯°□°)╯︵ ┻━┻
Bonus
Votre collègue s'est absenté en laissant sa session R ouverte ? Profitez-en, soyez infernal ! Voici quelques petites commandes amusantes à taper dans sa session :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
> F <- TRUE > T <- FALSE > c <- function(...) list(...) > return <- function(x) x + 1 > > # admirez le résultat : > c(1, 5) [[1]] [1] 1 [[2]] [1] 5 > double <- function(x) return(2 * x) > double(10) [1] 21 |
Le petit mot de fin
Tout ceci décrit le comportement de R 3.3.1. Il est possible que certaines incohérences soient résolues dans les prochaines versions. Cela me semble assez peu probable cependant, pour des raisons de rétro-compatibilité.
Vous ne l'aurez peut-être pas compris, mais ce n'est aucunement un article à charge. Il s'agit juste d'une mise en pratique de l’adage "qui aime bien châtie bien".
Si vous en redemandez, n'hésitez pas à remonter ma source : The R Inferno !
Merci à mes talentueux relecteurs Bebatut, Chopopope, Estel et Lroy !
Laisser un commentaire