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
digits
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 :
1234> all.equal(1, 2)[1] "Mean relative difference : 1"> isTRUE(all.equal(1, 2))[1] FALSE - Pour rendre le tout vraiment infernal,
all.equal n'est évidemment pas vectorisée, débrouillez-vous comme ça.
12> all.equal(myVect, 0.1)[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 ?
as.numeric
ne fonctionne pas comme vous le pensez sur les factors :
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
0.5
, en
0
ou en
1
?
1 2 |
> round(0.5) [1] 0 |
OK. R fait donc partie de ces langages qui arrondissent
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 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
matrix
comme colonne d'un
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 data frame to 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 data.frame est 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ù
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
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 x has length 1, is numeric (in the sense of is.numeric ) and x >= 1 , sampling via sample takes place from 1 :x . Note that this convenience feature may lead to undesired behaviour when x is of varying length in calls such as sample(x) .
help(sample)
Si x a une longueur de 1, est numérique (tel que défini par is.numeric) et x >= 1, l’échantillonnage via sample a lieu sur 1 :x. Notez bien que cette caractéristique peut aboutir à un comportement indésiré lorsque la longueur de x varie lors d'appels tels que sample(x).
Noitaréti à l'envers
Si vous essayez d'écrire en douce une petite boucle
for
, comme ça, ni vu ni connu (en général, quand on est pédant et qu'on fait du R, on préfère faire de la programmation fonctionnelle) (mais sinon, les boucles
for
ont la réputation d'être lentes en R. C'est assez faux. Elles ne sont pas plus lentes que le reste, du moment que vous ne faîtes pas grandir des objets dedans. Pensez à pré-allouer le résultat) (mais reprenons) :
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,
nrow(myTable)
vaut
0
, et
1 :0
retourne
[1] 1 0
! Ça alors, c'est pas de chance ! Préférez donc
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 read.table retourne un data.frame, et pas une table . La fonction table retourne une table.
- La fonction sort.list ne sert pas à trier les listes.
-
12> seq(5 :10)[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