Je ne sais pas pour vous, mais moi, à chaque fois que j'assiste à une réunion de labo, il y a quasi systématiquement un graphique d'ACP pour montrer les données. Et à chaque fois, il s'agit d'un graphique de base, généré avec R, avec la fonction plot(), des couleurs qui piquent les yeux et des axes et légendes illisibles. La critique est facile me direz-vous, j'avoue avoir moi aussi présenté ce genre de graphique assez souvent. Mais au bout d'un moment, vu que les ACP ça devient très très routinier dans le travail d'un "data analyste", la nécessité de produire rapidement et facilement un graphique ACP se fait sentir.
À cette occasion, j'ai retroussé mes manches et j'ai écrit un petit bout de code maison qui génère tout seul les graphiques pour un nombre donné de composantes principales, et ce en une seule fonction. J'en ai profité pour utiliser ggplot2, dont je vous invite à lire la présentation, pour rendre ces graphiques un peu plus esthétiques. Je partage donc avec vous mon secret pour faire des graphiques sexy. Ce n'est pas très compliqué en soit, c'est juste des graphiques, mais je vous donne mon code si jamais ça peut aussi vous servir et vous épargner du temps.
Aperçu du type de graphique que produit le code qui va suivre
Première étape : chargement de la librairie
Comme je l'ai dit plus haut, nous allons utiliser la librairie ggplot2. Cette fonction permet d'installer automatiquement le paquet s'il n'est pas déjà installé.
if (!require("ggplot2")) install.packages("ggplot2")
Deuxième étape : le screeplot
Lorsque l'on fait une ACP, on aime bien savoir quelles composantes principales (PC) accumulent le plus de variance (pour les détails sur ce qu'est une ACP, c'est par là). Pour aller vite, on peut simplement lancer la commande screeplot(), mais le rendu n'est pas hyper sexy. En plus il n'y a pas de légende sur l'axe des abscisses, ce qui est fâcheux quand on a plus de 20 PC. D'ailleurs, quand on lance une ACP sur un set de données avec plus de 100 échantillons (si si, ça arrive), on se retrouve avec un barplot tout serré où en réalité l'information qui nous intéresse est compressée sur la gauche du graphique.
Pour résoudre cela, la fonction suivante prend en entrée le résultat de l'ACP (calculée avec prcomp), et le nombre de PC à afficher. C'est pas le plus "hot" des graphiques mais ça peut être pratique.
plot_percent_var <- function(pca, pc){
# Calcule du pourcentage de variance
percent_var_explained <- (pca$sdev^2 / sum(pca$sdev^2))*100
# Préparation d'un tableau avec le numéro des composantes principales
# et le pourcentage de variance qui lui est associé
percent_var_explained <- data.frame(
PC=1:length(percent_var_explained),
percent_Var=percent_var_explained
)
# Récupérer uniquement le nombre de PC indiqué en argument
sub_percent_var_explained <- percent_var_explained[1:pc,]
# Génère le graphique
p <- ggplot(sub_percent_var_explained, aes(x=PC, y=percent_Var)) +
# Génère un barplot
geom_col()+
# Utilise le thème "black and white"
theme_bw() +
# Renomme l'axe des abscisses
xlab("PCs") +
# Renomme l'axe des ordonnées
ylab("% Variance") +
# Titre du graphique
ggtitle("Screeplot")+
# Option de taille des éléments textuels
theme(
axis.text=element_text(size=16),
axis.title=element_text(size=16),
legend.text = element_text(size =16),
legend.title = element_text(size =16 ,face="bold"),
plot.title = element_text(size=18, face="bold", hjust = 0.5),
# Astuce pour garder un graphique carré
aspect.ratio=1
)
# Affiche le graphique
print(p)
}
Troisième étape : l'ACP
Passons au nerf de la guerre : l'ACP en elle même. Pour apprécier la complexité d'un set de données, il est souvent nécessaire de regarder un peu plus loin que juste les 2 premières composantes. Ce qui me fatigue le plus (il ne me faut pas grand chose), c'est de relancer la commande de graphique pour chaque combinaison. Pour satisfaire ma fainéantise, il me faut donc une fonction avec le nombre de PC voulues et hop, ça me sort toutes les combinaisons possibles.
La fonction suivante prend en entrée le résultat d'une ACP (calculée avec prcomp), le nombre de PC à regarder, les conditions des échantillons (une liste qui fait la même taille que le nombre d'échantillons), et une palette de couleur en hexadécimale (par exemple: "#fb7072"), avec autant de couleurs que de conditions différentes (e.g. deux conditions, cas et contrôle, donc 2 couleurs).
plot_pca <- function(pca=pca, pc=pc, conditions=conditions, colours=colours){
# Transforme le nombre de PC en argument en nom de PC
PCs <- paste("PC",1:pc, sep="")
# Calcule le pourcentage de variance par PC
percent_var_explained <- (pca$sdev^2 / sum(pca$sdev^2))*100
# Transforme le vecteur de conditions en un facteur
cond <- factor(conditions)
# Crée un autre facteur avec les conditions
col <- factor(conditions)
# Change les niveaux du facteur avec la palette de couleur pour attribuer
# à chaque condition une couleur
levels(col) <- colours
# Re-transforme le facteur en vecteur
col <- as.vector(col)
# Récupère les scores pour le graphique
scores <- as.data.frame(pca$x)
# Génère toutes les combinaisons possibles de PC
PCs.combinations <- combn(PCs,2)
# Génère un graphique pour chaque combinaison
# avec une boucle apply
g <- apply(
PCs.combinations,
2,
function(combination)
{
p1 <- ggplot(scores, aes_string(x=combination[1], y=combination[2])) +
# Dessine des points avec une bordure de 0.5 remplis avec une couleur
geom_point(shape = 21, size = 2.5, stroke=0.5, aes(fill=cond)) +
# Utilise le thème "black and white"
theme_bw() +
# Spécifie la palette de couleur et donne un titre vide à la légende
scale_fill_manual(
values=colours,
name=""
) +
# Renomme le titre des axes des abscisses et des ordonnées en "PCx (pourcentage de variance)" avec 3 chiffres après la virgule
xlab(paste(combination[1], " (",round(percent_var_explained[as.numeric(gsub("PC", "", combination[1]))], digit=3),"%)", sep=""))+
ylab(paste(combination[2], " (",round(percent_var_explained[as.numeric(gsub("PC", "", combination[2]))], digit=3),"%)", sep=""))+
# Titre du graphique
ggtitle("PCA")+
# Option de taille des éléments texte
theme(
axis.text=element_text(size=16),
axis.title=element_text(size=16),
legend.text = element_text(size =16),
legend.title = element_text(size =16 ,face="bold"),
plot.title = element_text(size=18, face="bold", hjust = 0.5),
# Astuce pour garder un graphique carré
aspect.ratio=1
)
# Affiche le graphique
print(p1)
}
)
}
Et enfin : la démo
# Génération d'un set de données aléatoires avec 3 groupes
set.seed(12345)
x <- c(rnorm(200, mean = -1), rnorm(200, mean = 1.5), rnorm(200, mean = 0.8))
y <- c(rnorm(200, mean = 1), rnorm(200, mean = 1.7), rnorm(200, mean = -0.8))
z <- c(rnorm(200, mean = 0.5), rnorm(200, mean = 7), rnorm(200, mean = 0))
data <- data.frame(x, y, z)
# Définition des groupes
group <- as.factor(rep(c(1,2,3), each=200))
# Définition de la palette de couleur (on peut aussi utiliser RColorBrewer ou tout autre palette déjà faite)
palette <- c("#77b0f3", "#8dcf38", "#fb7072")
# On lance le calcule de l'ACP
pca <- prcomp(data, center=TRUE, scale=TRUE)
# On affiche le graphique "Screeplot" (pourcentage de variance par composante principale)
plot_percent_var(pca, 3)
# On génère le graphique de l'ACP pour les 2 premières composantes principales
plot_pca(
pca=pca,
pc=2,
conditions=group,
colours=palette
)
Et voilà le résultat :
Résultat des commandes précédentes. À gauche le screeplot, à droite l'ACP avec les deux premières composantes et le pourcentage de variance entre parenthèse, le tout coloré en fonction des conditions (1 à 3).
Juste une dernière remarque : si vous spécifiez plus de 2 PC à afficher, ça va générer les graphiques chacun leur tour, donc je vous conseille d'appeler la fonction plot_pca() dans un pdf pour les sauvegarder dans un seul fichier. Peut-être un jour j'essayerai le paquet gridExtra pour afficher plusieurs graphiques sur une même page... Un jour...
Dans un précédent article, nous avions regardé le fichier d'annotation des gènes du génome humain d’après Gencode. J'avais utilisé pour cela la puissante combinaison dplyr + ggplot2 (packages centraux du tidyverse), particulièrement adaptée à tout ce qui est manipulation et visualisation de données tabulaires.
Mais notre génome n'est pas constitué que de gènes, loin s'en faut ! Les éléments répétés sont en fait bien plus majoritaires. Je ne vais pas me risquer à donner ici une définition précise de ce qu'est un élément répété, je me contenterai de rappeler que si les éléments transposables sont des éléments répétés, tout les éléments répétés ne sont pas transposables ! Comme souvent en bio-informatique, je vais me contenter de la définition pragmatique d'élément répété : un élément répété est un élément décrit dans ma table d'annotation des éléments répétés. :-p
Les sources d'annotation des éléments répétés du génome humain sont bien plus rares que pour ce qui concerne les gènes. Je vous propose d'utiliser le temps de cet article une table disponible sur le UCSC table browser. Alors oui, l'interface a mal vieilli, mais le UCSC table browser reste une formidable collection de fichiers d'annotation du génome. Pour obtenir la table en question, il suffit normalement de changer le champ group sur Repeats et de laisser le reste par défaut.
Comment obtenir une table d'annotation des éléments répétés du génome humain. Vous pouvez cliquer sur le bouton describe table schema pour une description des colonnes de la table.
J'ai personnellement téléchargé cette table le 4 avril 2017. Peut-être la vôtre sera-t-elle plus récente, et donc légèrement différente ? En effet, les annotations du génome humain, gènes comme éléments répétés, ne sont pas encore parfaites et sont toujours activement améliorées. Cette table a été générée à l'aide de l'outil RepeatMasker, outil qui permet de masquer (en remplaçant par des N) les nucléotides d'un fichier fasta qui sont inclus dans des éléments répétés. Je trouve assez ironique qu'une des meilleures sources d'annotation des éléments répétés soit issue d'un logiciel visant à s'en débarrasser. ^^ Ce logiciel de plus de 20 ans sert notamment à faciliter l'annotation des gènes des génomes en masquant les séquences répétées.
Si vous souhaitez reproduire les analyses ci-dessous, je vous laisse donc télécharger la table, la mettre dans un répertoire de travail, et lancer R. Si vous n'en avez rien à faire de R, vous pouvez tout à fait sauter les blocs de code et autres explications pour vous contenter de regarder les jolies images. Je détaille cependant ma démarche, en espérant qu'au moins l'une ou l'un d'entre vous puisse en retirer une astuce utile, au prix d'un alourdissement assez conséquent de ce billet.
Import et toilettage des données
Après avoir lancé R et défini un répertoire de travail approprié (via la commande
setwd()
), je commence par charger quelques packages que j'aime bien :
library(purrr) # programmation fonctionnelle
library(dplyr) # manipulation de tableaux de données
library(readr) # import de fichiers txt
library(ggplot2) # des jolis plots
library(cowplot) # thème ggplot2 + outils pour figures multi-panneaux
library(forcats) # manipulations de factor
library(svglite) # export en svg
library(viridis) # une palette de couleur sympa
library(xtable) # export des tableaux en HTML
J'importe la table dans R à l'aide d'une fonction du package readr, fonction qui est plus rapide et qui a des valeurs par défaut de ses paramètres plus adaptées que la fonction
Je vais ensuite ne garder que les colonnes qui m'intéressent, que je renomme. J'en profite aussi pour ne garder que les lignes concernant les chromosomes standards, en filtrant les haplotypes alternatifs qui ne feraient qu'alourdir certaines figures par la suite.
standard_chromosomes <- c(paste0("chr", 1:22), "chrX", "chrY")
repeats <- select(repeats_dirty, genoName, genoStart, genoEnd, strand, repName, repFamily, repClass) %>%
dplyr::rename(chr = genoName, start = genoStart, end = genoEnd, name = repName, family = repFamily, class = repClass) %>%
filter(chr %in% standard_chromosomes)
Nous avons donc une classifications des éléments répétés en trois niveaux hiérarchiques, dans l'ordre : class > family > name.
Avant de regarder plus en détail cette classification, j'en profite pour filtrer les quelques lignes contenant un "?", qui correspondent à des classifications incertaines. Je pourrais les garder, mais il y en a relativement peu, et elles complexifieraient l'analyse et alourdiraient les figures.
La première figure que nous allons générer s’intéresse au 1er niveau hiérarchique de la classification : les classes d'éléments répétés. Combien y en a-t-il ? (divulgâchis : 16) Quel est l’effectif de chacune des classes ? Quelle fraction du génome chaque classe couvre-t-elle ? Quelle est la distribution des longueurs des éléments au sein de chaque classe ?
Je vais trier les classes par effectif décroissant pour rendre la figure plus jolie. Pour cela, comme j'utiliserai ggplot2, il me faut modifier l'ordre des levels de la colonne class après l'avoir transformée en factor. J'utilise quelques fonctions du package forcats.
Pour le premier panneau, un diagramme en barres, j'utilise des astuces vues dans le précédent billet. Si vous découvrez ggplot2, pourquoi ne pas jeter un coup d’œil sur cet article star du blog ? Comme toujours, ce qui prend le plus de lignes, ce n'est pas la figure en elle-même, mais tous les petits ajustages nécessaires pour la rendre plus jolie.
plot_Class <- ggplot(repeats, aes(x = class)) +
geom_bar(stat = "count", fill = "indianred1") +
geom_text(aes(label = ..count..), y = 10000, hjust = 0, stat = "count") +
labs(x = "Classe", y = "Nombre d'éléments") +
scale_y_continuous(sec.axis = dup_axis()) +
coord_flip() +
background_grid(major = "xy", minor = "none")
Pour le deuxième panneau, j'ai envie de voir la longueur totale couverte par chaque classe d'éléments répétés. Pour aider à la lecture, ce ne serait pas mal d'indiquer aussi la fraction du génome couverte par chaque classe. Voilà une excellente occasion d'utiliser une feature récemment ajoutée à ggplot2 : le second axe ! Avant toute chose, et comme la dernière fois, je récupère auprès de UCSC la longueur totale de chaque chromosome :
Le troisième et dernier panneau sera un aperçu de la distribution des largeurs pour chaque classe d'éléments répétés. Des boites à moustaches générées avec ggplot2 suffisent ici :
Enfin, j'arrange laborieusement les panneaux à l'aide du package cowplot et de quelques nombres magiques qui vont bien pour rendre la figure plaisante à l’œil :
myoffset <- 0.008
firstplotsize <- 0.44
svglite("plots/classeER.svg", width = 10, height = 5)
ggdraw() +
draw_plot(plot_Class , x = 0.0, y = myoffset, w = firstplotsize, h = 0.96 - 2*myoffset) +
draw_plot(plot_genomeProp , x = firstplotsize, y = 0.0, w = (1-firstplotsize)/2, h = 0.96) +
draw_plot(plot_sizeDistrib, x = firstplotsize + (1-firstplotsize)/2, y = 0.0, w = (1-firstplotsize)/2, h = 0.96) +
draw_plot_label(LETTERS[1:3], x = c(0.14, firstplotsize - 0.01, firstplotsize + (1-firstplotsize)/2 - 0.01), y = 0.92) +
draw_label("Classes d'éléments répétés", size = 15, x = 0.5, y = 0.97)
dev.off()
Figure 1: Les classes d'éléments répétés du génome humain. A. Nombre d'éléments répétés pour chaque classe. B. Fraction du génome couvert par chaque classe. C. Distribution des tailles d'éléments répétés pour chaque classe.
Les plus observateurs d'entre vous auront peut être réalisé, avec stupeur, qu'en effet RepeatMasker catégorise les gènes d'ARN ribosomaux (rRNA) et de transferts (tRNA) comme étant des éléments répétés ! Ce qui est techniquement exact, mais m'a un peu surpris au début (ça va mieux maintenant, merci). Je me suis amusé à comparer le nombre de copies de gènes d'ARN ribosomaux recensé par GENCODE, vu la dernière fois (544) avec ceux repérés par RepeatMasker (1 751). Peut-être la différence est-elle due aux copies non fonctionnelles, incluses dans la liste RepeatMasker mais pas dans celle de GENCODE ? Une telle différence se retrouve pour d'autres catégories de gènes ARN. Par exemple GENCODE recense 1 900 snRNA et RepeatMasker 4 285.
Si les SINE (short interspersed nuclear elements) sont plus nombreux que les LINE (Long interspersed nuclear elements), ils sont en général plus courts, et donc constituent une fraction moindre de notre génome. La troisième classe la plus abondante, à la fois en effectif et en fraction du génome, est celle des éléments à LTR (long terminal repeat). Il s'agit donc d'éléments issus de rétrovirus endogènes.
Notez que la figure 1C ne montre pas les points oustiders. En effet, les plus longs éléments répétés le sont tellement que les montrer écraseraient le reste de la figure. Voyez plutôt :
D’après la table d'annotation, les éléments répétés les plus longs sont donc les centromères, faisant tous exactement 500 000 paires de base. Quelle coïncidence ! En fait, à l'heure d’écriture de cet article, les centromères du génome humain ne sont toujours pas assemblés... Parce que figurez-vous qu'assembler 23 ou 24 régions d'environ 500 kb très hautement répétées, ce n'est pas de la tarte ! En attendant, les centromères sont donc annotés avec une longueur estimée arbitraire. Mais avec le rapide développement des technologies de séquençage de fragments longs, il est possible que les centromères humains soient assemblés prochainement. Les plus longs reads séquencés par la technologie Nanopore se rapprochent de la méga-base !
Notre génome est en tout cas constitué par environ :
sum(repeats$width) / genome_length
49,4% d'éléments répétés ! Sont-ils homogènement répartis entre les chromosomes ? C'est ce que je vous propose de découvrir ensuite.
Distributions des éléments répétés entre chromosomes
Tout d'abord, souhaitant mettre en évidence les trois plus grandes catégories d'éléments répétés (LINE, SINE et LTR), je crée une nouvelle colonne via un
mutate()
et un
if_else()
. Je regroupe ensuite le tableau par chromosome (
group_by()
) et par classe et somme les largeurs d'éléments répétés (
mutate(sum(width))
). Je joins le tableau à celui contenant la longueur des chromosomes (
left_join()
) pour pouvoir calculer la fraction de chaque chromosome contenant des éléments répétés (le second
mutate()
). J'en profite pour réorienter les levels de factors pour ordonner les différentes colonnes dans la figure. Et enfin j'envoie les données dans ggplot2, en ajustant tout un tas de micro-détails pour avoir une figure exactement comme j'aime :
Figure 2 : Contenu en éléments répétés de chaque chromosome.
En première approximation, il semble que chaque autosome ait un contenu en éléments répétés à peu près équivalent, oscillant entre environ 42% pour le chromosome 22, et 59% pour le chromosome 19. Il est amusant de comparer cette figure avec celle du contenu en gènes de chaque chromosome générée la dernière fois. Ainsi le chromosome 19 est à la fois l'autosome le plus riche en gènes protéiques et le plus riche en éléments répétés ! L'énigmatique chromosome 13 est relativement pauvre en éléments répétés, et en même temps pauvre en gènes. Les chromosomes sexuels font ici leur malin, avec le chromosome X ayant le plus fort taux en éléments répétés (62%), et le chromosome Y le plus faible (28%). Étonnamment (en tout cas pour moi), notre chromosome Y est donc ni riche en pseudogènes, ni riche en éléments répétés, il est juste... petit.
Les familles d'éléments répétés
Après avoir détaillé les classes d'éléments répétés, jetons un œil aux niveaux de classifications suivants, familles et sous-familles :
Nous allons essayer de représenter graphiquement cette diversité, en affichant des diagrammes en barres d'effectif de chaque famille de répétés. Je vais colorier les barres par le nombre de sous-familles pour chaque famille. Les effectifs variant énormément, je suis contraint d'utiliser une échelle logarithmique. J'ai alors été surpris de découvrir que pour l'instant,
Générons ensuite un panneau de figure par classe d'éléments répétés possédant de multiples sous-familles (classes Satellite, LTR, LINE, SINE et DNA). J'utilise pour cela la fonction
map()
du package purrr, une variante de
lapply()
, en définissant une fonction anonyme via les notations quelque peu ésotériques
J'utilise un peu de magie noire pour homogénéiser les marges de mes différents panneaux et gérer l'alignement vertical. Ne me demandez pas d'expliquer, j'ai juste copié-collé un bout de code depuis internet.
Enfin, j'arrange les différents panneaux, et j'exporte la figure dans un .SVG :
svglite("plots/familleER.svg", width = 10, height = 7)
ggdraw() +
draw_text("Les familles\nd'éléments répétés", x = 1/6, y = 0.95, size = 18) +
draw_plot(myLegend , x = 0.0, y = 0.75, w = 1/3, h = 0.2 ) +
draw_plot(myFamilyPlots$DNA , x = 0.0, y = 0.0 , w = 1/3, h = 0.8 ) +
draw_plot(myFamilyPlots$LINE , x = 1/3, y = 0.62, w = 1/3, h = 0.38) +
draw_plot(myFamilyPlots$SINE , x = 1/3, y = 0.31, w = 1/3, h = 0.31) +
draw_plot(myFamilyPlots$LTR , x = 1/3, y = 0.0 , w = 1/3, h = 0.31) +
draw_plot(myFamilyPlots$LTR , x = 1/3, y = 0.0 , w = 1/3, h = 0.31) +
draw_plot(myFamilyPlots$Satellite, x = 2/3, y = 0.7 , w = 1/3, h = 0.3 ) +
draw_plot(plot_other , x = 2/3, y = 0.0 , w = 1/3, h = 0.7 )
dev.off()
Figure 3 : Les familles d'éléments répétés.
Reconnaissez-vous des noms familiers ? Par exemple, nous avons 2 118 insertions d'éléments PiggyBac dans notre génome. Ce transposon est à l'origine d'une méthode de clonage de gènes dans des plasmides assez populaire.
Ce que je remarque surtout, c'est que des ARN de transferts (tRNA) se baladent dans la catégorie des SINE. MAIS POURQUOI ! POURQUOI ON NE PEUT PAS AVOIR DES CLASSIFICATIONS COHÉRENTES EN BIOINFORMATIQUE !
Hum hum, pardon.
En fait tout va bien : la classification est strictement non chevauchante au niveau des sous-familles : les tRNA de classe SINE ne contiennent pas les même sous-familles de tRNA que les tRNA de classe tRNA. Oui, je sais, ce n'est pas très clair. Mais il se trouve qu'un certain nombre de SINEdérivent de séquences d'ARN de transferts. Je pense donc que cette classification est tout à fait justifiée.
Je pourrais me perdre ensuite dans les détails des différentes sous-familles d'élément répétés, mais je préfère laisser les plus curieux d'entre vous se perdre dans ce fascinant tableau, et nous raconter leurs trouvailles en commentaires. Et c'est donc sur cette abrupte conclusion que je conclus.
Un grand merci aux super relecteurs et relectrice : Clémence, eorn, Mathurin et Max, sans qui cet article serait beaucoup moins bien.
Pour des raisons de reproduction de la science, il est important de conserver une trace de tout ce que l'on fait sur son ordinateur. Pour cela, faire des rapports est la meilleure manière que je connaisse qui permette d'inclure le code et les résultats d'une analyse. Pour faire ça bien avec R, on a déjà vu dans un article précédant que les rapports Rmarkdown étaient une très bonne solution.
License: CC0 Public Domain
Le but de cet article va être de vous permettre de gagner du temps avec le cache de Rmarkdown et quelques autres petites astuces. Pour cela, vous aurez besoin de 2 choses : avoir les bases du Rmarkdown et avoir des bases générales en informatiques. J'utilise RStudio pour faire mes codes Rmarkdown, je vous encourage à en faire de même.
Allez, c'est parti !
Alors si vous avez suivi l'article de notre cher ami jéro, vous avez un rapport Rmarkdown tout fait, qui date du 22 mars 2005 et qui produit un PDF. Je vais commencer par le commencement de votre fichier et vous donner 3 astuces du début de chacun de vos rapports :
Personnellement, je fais très rarement des rapports PDF, pour des raisons de compatibilité du code entre Windows et Mac OS/Linux. En effet, je suis obligé d'utiliser Windows au boulot et il y a beaucoup de problèmes pour compiler les PDF. Je ne vais pas rentrer dans les détails, mais retenez donc que je ne fais quasiment que des fichiers HTML. Donc déjà, dans mon entête en YAML, je remplace
pdf_document
par html_document. J'ajoute d'ailleurs l'option
number_sections: true
en dessous et au même niveau d'indentation que
toc: true
, ce qui va automatiquement mettre un numéro à mes sections.
Deuxième astuce, on peut mettre du code R dans l'entête YAML. L'intérêt est qu'on peut donc utiliser des fonctions, comme la fonction Sys.time(), qui nous donne la date et l'heure. J'ai donc dans chacune de mes entêtes à la place de
date: une date entrée manuellement
le code suivant :
date: "`r substr(Sys.time(), 1, 10)`"
, qui vous mettra automatiquement à chaque fois que vous allez compiler votre rapport, la date de cette compilation.
Dernière astuce avant de vraiment parler de cache, on peut définir en début de rapport des options qui vont s'appliquer à tous les chunks par la suite, par défaut. Donc je vous donne mes paramètres par défaut et je vous les commente ensuite :
Comme vous pouvez le voir, je les mets dans leur propre chunk que je nomme "setup". Le nom d'un chunk est très important pour se repérer lors de la compilation. On s'en servira en plus à la suite de cet article. Le nom est automatiquement compris par RStudio comme les caractères qui se trouvent après
` ` `{r
et avant la virgule. Après la virgule se trouvent les options de ce chunk, qui le rendent silencieux (echo=FALSE) et qui sortent les messages vers la console de compilation et pas dans le rapport.
Ensuite on a donc le code R, je charge la library knitr sur la première ligne et je définis les options de mes chunks suivants sur les lignes suivantes. Les options sont passées par la fonction
opts_chunk$set
. Je choisis l'alignement de mes figures au centre de la page, une taille de figures doublée quand elle s'affiche sur un écran à très haute résolution, une largeur de figure de 10, et enfin la création d'un cache pour tous mes chunks, qui n'utilisera pas le lazy loading (je ne vais pas expliquer ce que c'est, mais utilisez cette option ou vous aurez des problèmes). On aura ainsi tous nos chunks suivant celui-ci qui utiliseront ces options.
J'en profite pour ouvrir une parenthèse pour les plus avancés, si vous voulez avoir du code de suivi de l'avancement de votre compilation qui s'affiche dans votre console de compilation, il suffit de faire imprimer à votre code R un message ou un warning, et de mettre les options du chunk correspondantes en FALSE, ce qui fera que l'impression du message ou du warning ne se feront pas dans le rapport mais dans la console.
Voilà, maintenant que c'est fait pour les astuces, je vais vous en dire plus sur le cache.
Le cache c'est une copie sur votre disque dur de ce qui a été fait dans un chunk. C'est particulièrement utile lorsque l'on fait des analyses qui prennent beaucoup de temps à tourner. En effet, quand vous travaillez sur votre rapport, si l'un de vos chunks prend 2 jours à tourner, vous serez bien contents de ne pas avoir à le refaire à chaque fois que vous modifiez l'un des autres chunks. Ainsi, lorsqu'un chunk crée un cache, dans le dossier où vous avez stocké votre rapport, un sous-dossier se crée portant le nom de votre rapport + _cache. Dans ce dernier, un dossier html/pdf/autre se crée en fonction du type de rapport que vous produisez, puis enfin à l'intérieur vous trouverez les fichiers qui portent comme nom
nom de votre chunk_unCodeBienCompliqué.RData
. Le code bien compliqué est un hash, en gros un code, qui garantie que votre chunk n'a pas changé depuis qu'il a été sauvé en cache. Si le chunk a changé, alors automatiquement un nouveau cache sera produit, remplaçant celui qui est devenu inutile. Un petit point de détail, on peut avoir des longues analyses qui produisent des petits caches et des courtes qui en produisent des gros (et vice-versa). Plus un fichier de cache est gros plus il prendra de temps à être chargé. il peut donc être utile d'enlever le cache d'un chunk (donc mettre cache=FALSE dans les options du chunk) qui ferait une analyse rapide mais produirait un gros cache, si votre disque dur n'est pas très performant. Je ne mets pas en cache non plus mon chunk qui charge mes librairies et mes fichiers, ça peut causer des soucis. Enfin, je ne mets pas non plus en cache les chunks qui initialisent un sous programme, ou une méthode de multithreading. Dans tous les autres cas, gardez le cache.
J'en entends déjà me dire "Bon ok, mais tu dois avoir une autre idée derrière la tête avec ces caches". Ils me connaissent bien, en effet. Alors tout d'abord, sachez que les caches s'exportent très bien. Un cache produit sur un cluster peut tout à fait être rapatrié sur votre ordinateur. Donc vous pouvez faire le gros d'un rapport sur un cluster, puis fignoler sur votre machine locale. Ensuite, et c'est là pour moi la plus grande beauté de la chose, on peut les charger directement comme n'importe quel fichier RData (avec la fonction load). L'intérêt étant que si on veut reprendre toute une analyse et bricoler dans la console R, c'est possible. Il suffit de charger chacun des chunks dans l'ordre !
"Mais pourquoi on ne charge pas que le dernier ???" Ah, bonne question ! (Et oui j'aime me faire des dialogues dans ma tête...)
Chaque chunk ne contient QUE ce qu'il a produit. Il ne contient donc aucun objet créé avant. Il faut donc bien tous les charger pour tout avoir. Et c'est là qu'on va pointer le plus gros problème de cette méthode : Si on change un chunk qui a des répercussions sur la suite, ce n'est pas automatiquement propagé. Dans ce cas, je vous conseille soit de supprimer le fichier cache des chunks qui doivent être modifiés, soit de supprimer tout le cache.
"Ouhla mais c'est galère de charger chaque chunk dans le bon ordre !"
Bon, vu que je suis sympa avec vous, j'ai écrit une petite fonction R qui va le faire pour vous. Pour qu'elle fonctionne, vous aurez besoin d'avoir déjà installé la librairie devtools, puis de lire mon script en fichier source :
Ensuite, il vous suffira d'appeler ma fonction et de lui donner en argument le nom du rapport pour lequel vous voulez charger le cache :
loadAllChunksCache("Rapport.Rmd")
Il chargera automatiquement et dans l'ordre de votre rapport le cache qu'il trouvera dans le premier dossier existant qui correspond à "Rapport_cache/html/" ou "Rapport_cache/pdf/" ou "Rapport_cache/word/". Vous pouvez aussi lui spécifier en deuxième argument le dossier où vous voulez qu'il prennent le cache. C'est utile par exemple si vous avez du cache dans le dossier "Rapport_cache/html/" mais que vous voulez celui qui est dans "Rapport_cache/pdf/". Il faudra aussi pour qu'il fonctionne que vous ayez nommé vos chunks, il ne fonctionnera pas avec les noms automatiques. Je pourrais mettre ça en place, mais je n'encourage pas cette mauvaise pratique. Flemmard oui, mais flemmard avec classe !
Voilà pour mes astuces sur le cache des rapports Rmarkdown. Je pense que le cache est le plus gros intérêt de faire des rapports dès le début du développement de n'importe quel projet. Je vous conseille une fois que vous avez fini de développer votre rapport de supprimer tout le cache et de refaire tourner votre analyse entièrement. On n'est jamais à l'abris d'une erreur qui se propage.
Enfin, et pour finir, je vais faire un peu de pub pour mes librairies et autres scripts qui pourraient vous être utiles :
J'ai amélioré la fonction ipak que j'ai trouvé sur le github de Steven Worthington qui permet maintenant en une seule fonction de charger une liste de librairies, et de les installer du CRAN ou de bioconductor directement si elles ne sont pas installées. Elles seront chargées après leur installation. Pour l'utiliser, encore une fois il vous faut devtools d'installé, puis charger cette fonction :
J'ai créé le package apercu, dont la fonction principale, ap(), vous permet d'afficher... roulement de tambour... un aperçu de vos objets. Je m'explique : de la même manière que head() affiche les 6 premieres lignes d'une matrice par défaut, ou les 6 premiers éléments d'un vecteur ou d'une liste, ap() en affiche 5, à la différence que pour une matrice ou un data.frame, il n'affiche aussi que les 5 premières colonnes. Cette fonction vous sera particulièrement utile en cours de développement, pour voir rapidement vos grosses matrices, data frames, listes, vecteurs et même des objets plus compliqués et imbriqués. Ce package est disponible sur le CRAN, il s'installe donc normalement avec install.packages("apercu") ou avec ipak("apercu").
Voilà ! Sur ce je vous laisse en remerciant mes relecteurs, Yoann M. et Kumquatum pour leurs commentaires !
Chez les eucaryotes, l'ADN est organisé en domaines plus ou moins compactés, avec des taux de transcription plus ou moins élevés, et qui sont marqués différentiellement par un certain nombre de marques épigénétiques (méthylation de l'ADN, modifications post-traductionnelles des histones, variants d'histones, etc.). Il est fréquent d'essayer de corréler le niveau d'expression des gènes avec la présence ou l'absence d'une marque épigénétique à proximité des sites d’initiations de la transcription (raccourcis en TSS, pour transcription start sites). Par exemple, pour reprendre la carte de chaleur de la figure 1C de l'article présentant la suite d'outils deepTools2 :
Figure 1C issue de l'article présentant la suite d'outils deepTools2. Piles de profils épigénétiques H3K4me3, H3K27me3 et H3K36me3 (tri-méthylations des lysines 4, 27 et 36 des histones 3) centrés sur les TSS, réparti par clustering en un groupe actif et un groupe inactif.
Pour générer ce type de figures, il est nécessaire d'être en possession d'une liste de sites d’initiation de la transcription. Et donc, de se poser cette question fondamentale :
Qu'est-ce qu'un site d’initiation de la transcription ?
La définition est triviale : un site d'initiation de la transcription désigne une position génomique où le processus de transcription (synthèse d'un ARN depuis une matrice d'ADN) s'initie. En général, il est surtout fait référence aux sites d'initiations de la transcription des gènes, et/ou d'éléments transposables. Souvent l'ARN produit est coiffé au niveau de la base correspondante au site d'initiation de la transcription. Il ne faut pas confondre le site d’initiation de la transcription avec le site d'initiation de la traduction : les deux étant séparés sur les ARN messagers par la région 5' non traduite (ou 5' untranslated region, 5'UTR).
Structure des gènes eucaryotes (traduit d'après une image Wikimedia Commons CC BY 4.0). TSS: site d'initiation de la transcription (Transcription Start Site).
Petite subtilité : si la première base transcrite se désigne par le "+1" de transcription, la base la précédant sur le brin d'ADN est par convention appelée le "-1" de transcription, donc sans base "0" de transcription.
Comment identifier les sites d'initiations de la transcription ?
Je connais principalement deux méthodes expérimentales d'identification de ces sites. Sans rentrer dans les détails :
la plus récente méthode CAGE, pour Cap Analysis Gene Expression, un acronyme désignant un processus de capture des extrémités coiffées des ARN, suivi de leur séquençage haut débit. C'est cette méthode qui a été privilégiée par le formidable consortium Fantom5.
Les deux approches ont leurs limites. Outre la différence évidente de débit, citons le fort taux de décrochage des transcriptases inverses, facteur limitant de la méthode d'extension d'amorce, mais aussi la complexité du protocole expérimental du CAGE, ainsi que son incapacité à détecter les sites d'initiations non coiffés au niveau des ARNs.
Oui, ça OK, mais on la télécharge où la liste des TSS ?
Je reconnais bien là votre pragmatisme de bioinformaticiens. Pour nous, nul besoin de réflexions bio-philosophiques poussées : un gène, c'est ce qui est présent dans notre fichier d'annotation des gènes, et cette approche nous convient la plupart du temps ! Et les sites d'initiations de la transcription se trouvent au début des gènes.
(Ouvrons une parenthèse tout de même, pour rappeler qu'il est indispensable de bien connaître les limites actuelles de ces annotations. Ainsi, si l'humain a environ 20 000 ARN longs non-codants (lncRNA) et l’opossum seulement 8 000, ce n'est sans doute pas que nos 12 000 lncRNA de plus nous rendent supérieurement intelligents, mais plutôt que de nombreux lncRNA ne sont pas encore identifiés chez l'opossum !)
Pour les espèces sur lesquelles j'ai le plus travaillé (humain, souris), la source de référence des annotations des gènes est l'organisation GENCODE, dont les annotations sont utilisées par Ensembl.
Mais, et c'est là où je voulais en venir en commençant cet article : avec l'amélioration des annotations, les modèles de gènes comportent de plus en plus de transcrits différents. Prenons par exemple le cas du gène BRCA1 (pour BReast CAncer 1, un gène connu pour son implication dans les cancers du sein et de l'ovaire).
Où est le TSS de BRCA1 ?
Regardons à quoi ressemble les modèles de transcrits du gène BRCA1 dans la version actuelle des annotations Gencode (la 27). C'est un gène situé sur le brin moins, son début est donc sur la droite, et il est transcrit vers la gauche. D'après l'image Ensembl :
Vu Ensembl du gène BRCA1 dans le génome humain.
J'ai dénombré à l’œil 30 transcrits différents, auxquels correspondent 7 sites d'initiation de la transcription différents !
Les sept sites d'initiations de la transcription du gène BRCA1, annotés à l'aide de gracieuses flèches orange dedans et marron autour.
Une approche possible pourrait être de garder chaque site d'initiation de chaque transcrit pour nos figures, mais cela aboutit à une explosion du nombre de TSS : 200 401 dans la version 27 humaine de GENCODE ! Une autre approche serait de garder uniquement les sites d'initiation uniques, soit environ 7 pour BRCA1. Mais c'est alors faire peser autant un TSS utilisé par des dizaines de modèles de transcrits avec des TSS alternatifs utilisés par de rares transcrits alternatifs. L'approche favorite des bioinformaticiens est donc assez souvent de ne garder qu'un seul site d’initiation de la transcription par gène.
Par facilité, certains s'en sortent en ne gardant que les lignes de type "gene" dans le fichier d'annotation GENCODE (pour plus de détails, voir par exemple cet article). Une ligne par gène, un seul TSS, problème résolu !
Or la ligne gene de GENCODE part du nucléotide le plus en amont parmi tous les transcrit jusqu'au nucléotide le plus en aval parmi tout les transcrits (pas nécessairement dans le même transcrit, d'ailleurs), ce qui nous fera un TSS de BRCA1 ici :
Position du TSS de BRCA1 d'après la ligne "gene" des annotations GENCODE.
Alors qu'intuitivement, en suivant une règle majoritaire, j'aurais plutôt tendance à privilégier ce site d'initiation là :
Le principal TSS de BRCA1, d'après votre serviteur.
Utiliser les lignes "gene" de fichier d'annotation GENCODE pour obtenir une liste d'un unique site d'initiation de la transcription par gène me semble donc assez maladroit pour un nombre important de gènes.
Pour ma part, je prends plutôt le site d'initiation médian de tout les transcrits d'un gène, excluant ainsi les quelques transcrits suspicieux commençant soit très tôt, soit très tard. Dans les cas où il y a un nombre pair de transcrit, je ne prends pas la moyenne des coordonnées des deux TSS du milieu, mais bien l'un des deux (en l’occurrence, le TSS du premier des deux "transcrits du milieu").
Voici par exemple le petit script R que je me suis fait, qui part du fichier GFF3 fournit par GENCODE (pour plus de détails, voir cet article), et retourne un fichier .bed d'un unique TSS par gène. Le script n'est pas des plus rapides malheureusement, mais je n'ai à le faire tourner qu'une seule fois par fichier d'annotation.
library(rtracklayer)
library(tidyverse)
# l'import peu prendre 2 minutes en fonction de votre configuration
gencode <- import("gencode.v27.annotation.gff3.gz", format = "GFF") %>%
# import() retourne un objet GRanges, nous le convertissons en tibble
as_tibble
transcript <- filter(gencode, type == "transcript")
# fonction retournant le transcrit du milieu parmi tous les transcrits du gène 'gene'
getMiddleLineFor <- function(gene) {
tempt <- dplyr::filter(transcript, gene_id == gene)
if(tempt$strand[1] == "+") {
# si le gène est '+', le TSS est au 'start'
tempt <- dplyr::arrange(tempt, start)
} else if(tempt$strand[1] == "-") {
# si le gène est '-', le TSS est au 'end'
tempt <- dplyr::arrange(tempt, end)
}
# retourne la ligne du mileur
dplyr::slice(tempt, ceiling(nrow(tempt)/2))
}
# puis j'applique la fonction à tous les gènes
# environ 20 minutes...
midTranscript <- map_dfr(
unique(transcript$gene_id),
getMiddleLineFor
)
write.table(
select(midTranscript, seqnames, start, end, gene_id,level, strand, gene_type, gene_name),
file = "gencode.v27.annotation.middleTSStranscript.bed",
quote = FALSE, row.names = FALSE, col.names = FALSE, sep = "\t"
)
Si je ne veux que la position des TSS et pas le start et le end du transcrit du milieu, je rajoute cette étape :
La fonction est facilement adaptable lorsqu'on en souhaite non pas le site d'initiation de la transcription du milieu, mais le site de terminaison de la transcription du milieu !
Après intense réflexion, au lieu de prendre le TSS médian, il pourrait être judicieux de retenir le mode des TSS des transcrits d'un gène ? Il faut cependant une implémentation maline qui traite judicieusement les cas où il y a autant de TSS différents que de transcrits...
Pour aller plus loin
Je vous ai donc proposé une méthode simple pour sélectionner un seul site d'initiation de la transcription par gène. Il est fort probable que pour un certain nombre de gènes, le site d'initiation "majoritaire" soit variable en fonction du tissu : tel TSS sera favorisé dans un type cellulaire, mais un autre TSS le sera dans un autre tissu. Une autre méthode, beaucoup plus lourde, mais plus maline, pourrait donc consister à identifier le TSS majoritaire de vos types cellulaires d’intérêts d'après les données CAGE du consortium Fantom5 (du moins, pour l'humain et la souris).
Une des découvertes de ce consortium a d'ailleurs été que l'idée d'un site d'initiation de la transcription bien défini n'était sans doute pas valide pour tous les gènes. Les promoteurs riches en CpG auraient plutôt une "zone" diffuse d'initiation de la transcription [1].
Enfin, si l'on s’embête tant avec ces histoires de TSS unique par gène, c'est que bien souvent nous ne possédons une mesure d'expression qu'au niveau du gène. Mais les progrès techniques et algorithmiques en RNA-seq font que la quantification au niveau des transcrits directement est de plus en plus fiable [2]. Raisonner avec des transcrits permet d'éviter tous ces détours méthodologiques à la recherche de la "bonne liste" des TSS.
Vous avez un script que vous souhaitez partager avec une équipe expérimentale? Vous ne voulez pas que les utilisateurs modifient le code pour paramétrer votre programme? Vous codez avec R ? Alors cet article est fait pour vous ! Nous allons voir comment créer une application web avec R et permettre à votre utilisateur d’exécuter votre code sans le voir.
Shiny
Le package que nous utiliserons est shiny. Il est proposé par Rstudio (https://shiny.rstudio.com/) et disponible sur le CRAN. Ce package permet de construire des applications web très simplement sans connaissances particulières en HTML et CSS. Les fonctions que nous appellerons dans R vont être traduites en HTML. Par exemple, h1(‘Un titre’) sera transformé en <h1>Un titre</h1>. Il n’est donc pas indispensable de savoir coder en HTML, mais des connaissances dans les langages web pourront vous être utiles dans des cas particuliers, puisqu'il est possible d’intégrer dans l’application shiny du code HTML brut.
Une application shiny se divise en 2 parties :
l’UI : Il s’agit de l’interface utilisateur visible dans une page web. Nous pourrons y retrouver des graphes, des tableaux, du texte, etc. L’utilisateur pourra interagir avec cette interface par le biais de boutons, de sliders, de cases, etc.
le serveur : Il s’agit de la « zone de travail ». Tous les calculs, préparations de données ou analyses que R réalisera seront faits côté serveur.
Nous allons voir dans cet article toutes les étapes pour créer une application complète. Elle sera capable de lire un fichier en fonction de paramètres enregistrés par l’utilisateur puis d'afficher :
Un tableau avec de la coloration conditionnelle
4 graphiques obtenus par des approches différentes
Un classique réalisé avec R. Ce graphique sera paramétrable par l’utilisateur (couleur ou titre par exemple).
Toutes les étapes pour créer une application Shiny seront détaillées dans ce post. Connaître la syntaxe de R simplifiera grandement la lecture de l'article mais n’est pas indispensable.
Pour réaliser cette application, il vous faudra une version à jour de RStudio (plus simple que la console R). Pour l’installer, suivez les étapes suivantes (l’ordre est important) :
Note : pour les utilisateurs de R les plus avancés, l’application peut être développée dans un environnement virtuel comme docker (sujet de mon prochain post).
Les données
Les données utilisées pour cette application proviennent du tableau IRIS regroupant des mesures sur des fleurs (disponible dans Rdataset et décrit ici https://archive.ics.uci.edu/ml/datasets/iris ). Ce jeu de données est très utilisé pour illustrer les fonctions dans R et pour le machine learning. Le tableau est composé de 5 colonnes :
Les packages utilisés pour réaliser l’application sont disponibles sur le CRAN. Ils s’installent avec la commande : install.packages(). Les packages que nous utiliserons sont :
shiny [1] : Il permettra de construire l’application web
shinydashboard [2]: Il permettra de créer une architecture dynamique à la page web avec une zone de titre, une menu rabattable et une zone principale
shinyWidgets [3] : Il permettra de mettre un message d’alerte pour confirmer la lecture correcte du tableau
DT [4] : Il permettra de créer un tableau dynamique avec de la coloration conditionnelle
plotly [5] , ggplot2 [6] et googleVis [7] : Ils nous permettront de réaliser des graphiques
colourpicker [8] : Il permettra à l’utilisateur de sélectionner une couleur.
Nous utiliserons pour les installer et les charger un autre package : anylib [9]. Ce package est très pratique car il permet d'installer (si besoin) et de charger une liste de package. En plus, il a été créé par un des auteurs de Bioinfo-fr : Aurelien Chateigner. Que demander de plus!
Pour mettre en forme notre application web (la partie UI visible par l’utilisateur), nous allons utiliser le package shinydashboard. La documentation est présente ici : https://rstudio.github.io/shinydashboard/index.html .
L'architecture minimale avec shinydasbord est zone de titre (bleue), une barre latérale (noir) et une zone principale (grise).
Pour tester l’application, il faut sauvegarder le code puis appuyer sur le bouton Run App au dessus à droite de l’éditeur de texte de Rstudio. Un point important, Rstudio reconnaît par défaut les applications qui se nomme app.R. Je vous conseille vivement de nommer votre fichier app.R.
Si vous travaillez avec la console R, vous pouvez lancer la commande suivante :
runApp()
Ajouter un titre
Dans la fonction dashboardHeader, nous ajoutons un titre à l’application (ici bioinfo-fr). Ce titre sera affiché en haut à gauche.
La première étape est d’ajouter des éléments (item) dans la barre de menu latérale (partie noire). Nous utilisons pour cela la fonction dashboardSidebar. Nous y ajoutons la fonction sidebarMenuqui contient les items du menu.
Ensuite, il faut indiquer que la partie body aura plusieurs pages (des tabItems). Chaque tabItem correspond à une page accessible par le menu. Le menuItem doit avoir le même nom que l’argument tabName de la fonction tabItem pour y accéder (exemple : readData). Dans chaque page, nous ajoutons un titre de niveau 1 (h1). Vous pouvez remarquer l'utilisation de la fonction icon (icon = icon(...)). L'argument de la fonction est un nom d'icône que nous pouvons trouver sur ces deux sites : https://fontawesome.com/ et https://getbootstrap.com/docs/4.3/components/alerts/ . En utilisant cette fonction, vous aurez une petite image (icônes) à gauche du nom de l'élément (par exemple un livre pour la lecture des données). Il est aussi possible de l'utiliser pour des boutons .
L’objectif est de proposer une interface simple pour lire un fichier dans l’application et qui permette à l’utilisateur de paramétrer la lecture et d’avoir une prévisualisation du fichier lu.
Importer un fichier
Pour importer un fichier, shiny propose la fonction fileInput. Il est possible de faire du “drag and drop” dans la zone de l’import ou de sélectionner un fichier dans l’explorateur de fichiers. Le type de fichier visible est paramétrable dans les arguments. Ici, nous utiliserons le paramétrage par défaut.
Nous souhaitons maintenant paramétrer 3 points lors de la lecture du fichier : type de séparateur (virgule, tabulation, espace), type de quote (simple, double, aucune) et la présence/absence des noms de colonnes (header). Nous utilisons pour cela des radio boutons. La fonction utilisée est radioButtons. 5 arguments sont utilisés :
id : identifiant du groupe de radio boutons (ici nous avons 3 groupes de radio boutons pour nos 3 paramètres),
label : le titre présent au dessus du groupe de radio boutons,
choices : les choix possibles dans le groupe de radio boutons. A noter, la zone située à gauche du "=" contient les informations qui seront affichées dans l'application alors que la partie droite indique ce que comprend R côté serveur. Pour le header par exemple, il sera affiché “Yes” côté UI et nous récupérerons côté serveur TRUE (“Yes” = TRUE) lorsque que nous récupérerons la valeur du radio bouton côté serveur.
Selected : Nom du radio bouton sélectionné au lancement de l’application
Dans cette zone, nous allons visualiser les premières lignes du fichier que nous souhaitons lire. Il faut donc :
Créer une zone d’affichage dans l’UI
Lire les données côté serveur et envoyer les données dans la zone d’affichage
Côté UI
Pour afficher le tableau, nous utilisons la fonction dataTableOutput. Une zone va être créée pour afficher un tableau. Nous donnons à cette zone un identifiant en utilisant l’argument outputId . Cet identifiant est indispensable pour retrouver la zone côté serveur.
Nous souhaitons à présent afficher de l’information. Du côté serveur, pour envoyer de l’information, la syntaxe commence quasiment toujours par output$ puis l’ID de la zone de sortie (ici notre tableau de prévisualisation avec l’id "preview"). Ce que nous souhaitons lui envoyer est un tableau. Nous utilisons donc la fonction <- renderDataTable({ }). Dans cette dernière fonction, nous allons lire le tableau qui va être renvoyé. Pour récupérer de l’information du côté UI, il faut utiliser la syntaxe suivante : input$ID. Par exemple, nous souhaitons récupérer le choix de l'utilisateur concernant le header : input$header.
req(input$dataFile) : bloque la suite du code si la zone d’import de fichier est vide
df <- read.csv() : on stocke dans df la lecture du fichier
input$dataFile$datapath : chemin d’accès au fichier importé
header = as.logical(input$header) : récupération de la réponse de l’utilisateur pour savoir si présence ou absence d’un header. Le as.logical permet de convertir un TRUE ou FALSE en booléen.
sep = input$sep, quote = input$quote : récupération du paramétrage de l’utilisateur pour le séparateur et les quotes. Ces informations sont données aux arguments de la fonction read.csv()
nrows=10 : Nous ne souhaitons pas lire tout le fichier. Seules les premières lignes sont nécessaires pour savoir si le tableau est lu correctement ou non. Nous lisons donc les 10 premières lignes.
options = list(scrollX = TRUE , dom = 't') : Si le tableau a de nombreuses colonnes, cette option permet d’avoir un scroll horizontal
Vous pouvez maintenant tester sur un fichier texte contenant un tableau. Le changement de paramétrage a un effet direct sur la visualisation.
Visualisation de l'application
Organisation des éléments
Pour les connaisseurs de bootstrap, Shiny intègre son code. Pour les autres, il est possible d’organiser le contenu d’une page à l’aide d’une grille. La grille est composée de lignes ( fluidRow()) elles-mêmes composées de 12 blocs. Nous allons placer les paramètres et la prévisualisation sur une même ligne. Nous souhaitons stocker les paramètres dans 3 blocs (column(3,...) : la colonne aura une taille de 3 blocs) et 9 blocs pour la prévisualisation ( column(9,...) ).
Pour finir avec cette page, nous allons créer un bouton pour valider le paramétrage de la lecture du tableau. En cliquant sur ce bouton, l’ensemble du fichier sera lu. Nous ne réalisons pas une lecture dynamique comme précédemment. En effet, à chaque changement de paramètre, l’ensemble du fichier est relu. Si le fichier est gros, le temps de lecture sera long.
Côté UI
Nous ajoutons un actionButton. L’identifiant de notre bouton est "actBtnVisualisation".
Lorsque que le bouton est cliqué, nous souhaitons à présent que le contenu du fichier soit stocké dans une variable. Il s’agit d’une variable particulière. Elle doit être visible par toutes les fonctions côté serveur et relancer toutes les fonctions qui l’utilisent si elle change. Il s’agit d’une variable réactive (reactiveValues). Si nous détaillons le code :
Nous déclarons une reactiveValue avec comme nom data.
Nous allons utiliser une fonction qui permet d’attendre une action particulière. Ici nous attendons que l’utilisateur clique sur le bouton. Une fois que le bouton a été cliqué, le code entre les { } sera exécuté. Ici, l’objectif sera de stocker le contenu du fichier importé dans la reactiveValue sous le nom table (data$table)
Ainsi, à chaque clic du bouton, data$table sera mis à jour ainsi que toutes les fonctions qui l’utilise (ex : des graphiques). Nous pouvons aussi ajouter un message pour confirmer la lecture du fichier. Nous utiliserons sendSweetAlert proposé dans le package shinyWidgets. La documentation est disponible ici : https://github.com/dreamRs/shinyWidgets .
observeEvent(input$actBtnVisualisation, {
data$table = read.csv(input$dataFile$datapath,
header = as.logical(input$header),
sep = input$sep,
quote = input$quote,
nrows=10)
sendSweetAlert(
session = session,
title = "Done !",
text = "Le fichier a bien été lu !",
type = "success"
)
})
Visualisation du message
Changement de page
Enfin, notre application étant composée de 2 pages, nous souhaitons changer de page une fois que le fichier est lu pour arriver sur la page de visualisation.
Pour rappel, “tabs” est l’identifiant de notre sidebarMenu. Nous allons avec cette commande chercher dans la sidebarMenu la page qui a comme identifiant “visualization” et changer de page.
Visualisation
Exploration du tableau
Nous allons à présent afficher le tableau complet. Nous utilisons pour cela le package DT (https://rstudio.github.io/DT/ ). Il permet de rechercher, sélectionner ou trier les informations d'un tableau de données. Il faut pour cela créer une zone où sera affiché le tableau dans l’UI.
Côté UI
tabItem(tabName = "visualization",
h1("Visualisation des données"),
h2("Exploration du tableau"),
dataTableOutput('dataTable')
)
Puis du côté serveur, il ne reste plus qu’à envoyer le contenu de notre fichier dans ce tableau par le biais de la reactiveValue. Ainsi, le tableau sera automatiquement mis à jour si un nouveau fichier est lu.
Il est possible de faire de la mise en forme conditionnelle comme dans excel. Le code proposé par la suite est dépendant du tableau utilisé. En effet, nous allons cibler les colonnes d’intérêt par leur nom pour une question de lisibilité.
Voici une proposition de mise en forme conditionnelle de notre tableau (inspiré de l’exemple proposé dans la documentation du package DT).
Histogramme des valeurs pour les colonnes Sepal.length et Petal.length
Coloration par seuils multiples pour les colonnes Sepal.width et Petal.width (fond blanc écriture noire, fond rouge écriture blanche et fond rouge foncé écriture blanche)
Coloration du fond en fonction de l’espèce pour la colonne espèce.
Enfin, pour améliorer l’exploration, il est possible d’ajouter des filtres par colonnes. Pour les valeurs numériques, les données sont filtrées par un slider. Pour les colonnes contenant du texte, il y a deux possibilités :
Peu de variabilité entre les éléments. Par exemple, la colonne Species ne contient que 3 éléments différents : setosa, versicolor et virginica. Dans ce cas, le filtre sera composé des éléments uniques de cette colonne qui seront cliquables. En les cliquant, toutes les lignes avec cet élément seront sélectionnées.
Grande variabilité entre les éléments. Dans ce cas, une zone pour entrer du texte sera proposée. Le texte saisi sera recherché dans la colonne.
Nous allons créer de 4 façons différentes des graphiques et les afficher dans l’application shiny :
des graphiques statiques
un plot de base avec R
un graphique avec ggplot2
des graphiques dynamiques
Avec plotly
Avec google
Les graphiques seront représentés sur la même ligne avec une fluidRow et 4 colonnes (comme nous avons fait précédemment).
Graphique R
R propose une grande palette de graphiques de base. Cependant, il s'agit uniquement de graphiques statiques.
Côté UI
Il faut comme précédemment créer une zone pour indiquer où va être affiché le graphique. La fonction utilisée est plotOutput.
tabItem(tabName = "visualization",
h1("Visualisation des données"),
h2("Exploration du tableau"),
dataTableOutput('dataTable'),
h2("Graphiques"),
fluidRow(
column(3,plotOutput("plotAvecR") )
)
)
Côté serveur
Nous allons pour ce graphique comparer la corrélation entre la taille des sépales et des pétales. Comme pour le tableau, la syntaxe est la suivante pour envoyer de l’information du côté UI : output$ID_de_la_zone. Pour envoyer un plot, nous utilisons la fonction renderPlot. Dans cette fonction, vous pouvez mettre n’importe quel graphique de R. Afin de mettre à jour automatiquement les graphiques, nous utilisons notre reactiveValue : data. Chaque fois que data changera, le plot sera généré de nouveau. Pour accéder au contenu du fichier lu qui est stocké dans la reactiveValue data sous le nom de table, nous utilisons de nouveau la syntaxe suivante : data$table. Il s’agit d’un dataframe (la lecture par read.csv2 renvoie un dataframe). Les colonnes sont donc accessibles par un $ puis le nom. Au final, pour obtenir le vecteur contenant les valeurs de longueur des pétales, nous utiliserons la syntaxe suivante : data$table$Petal.Length.
Le paramétrage du plot est libre et n’est pas contraint par shiny.
Visualisation de l'application
Graphique par ggplot2
Ggplot2 est une librairie graphique de plus en plus utilisée. Elle propose des graphiques plus évolués que ceux de base dans R. Vous trouverez une documentation très bien faite ici : https://ggplot2.tidyverse.org/. Nous allons comparer les largeurs et les longueurs des sépales. Une coloration en fonction de l’espèce est proposée.
Côté UI
Comme toujours, nous allons créer une zone où sera affiché le graphique. La fonction utilisée est encore plotOutput.
Nous allons procéder de la même façon que précédemment. La différence est liée au contenu de la fonction renderPlot. Nous allons cette fois-ci utiliser les fonctions de ggplot2.
Plotly est un package de j’affectionne particulièrement. Il propose énormément d’outils préprogrammés (enregistrement de l’image, zoom, informations supplémentaires). De plus, il n’est pas exclusivement réservé à R. Il est possible de l’utiliser aussi dans des projets en JS et en python (aussi simple d’utilisation).
Côté UI
De nouveau, nous allons créer une zone pour afficher le graphique. Attention, nous changeons de fonction. Nous utiliserons cette fois plotlyOutput.
Je n’expliquerai pas ici la syntaxe pour réaliser un graphique avec Plotly. La documentation sur le site est extrêmement bien faite avec de très nombreux exemples (https://plot.ly/r/). Vous pouvez mettre n’importe quel graphique plotly dans la fonction. Ici, nous comparons la largeur et la longueur des pétales.
Je vous invite lorsque vous lancerez l’application à survoler ce graphique. Il y a énormément d’informations disponibles et d’outils d’exploration.
Graphique Google
Pour finir, les graphiques de Google sont de plus en plus populaires et offrent un plus large choix de représentations que Plotly (calendrier, etc.). Ici, nous allons réaliser un histogramme de la largeur des pétales.
Côté UI
Nous créons de nouveau une zone pour afficher le graphique. La fonction utilisée est htmlOutput. Cette fonction est capable d’interpréter du code HTML venant du serveur. Si vous souhaitez écrire du HTML directement dans la partie UI, il vous suffit d’utiliser la fonction HTML (ex : HTML(“<h1>Titre 1</h1>”) ) .
En lançant l’application, si vous vous rendez sur la partie visualisation, vous trouverez plein d’erreurs. Ces erreurs sont la cause de l’utilisation d’une reactiveValue. En effet, lorsque rien n’a encore été lu, data$table est NULL (vide). Or toutes les fonctions que nous utilisons ne gèrent pas les NULL. Nous ajouterons pour le tableau et les graphiques un peu de code pour lui dire de renvoyer NULL si le tableau est vide.
if (!is.null(data$table)) {
[représentation graphique ou le tableau]
} else {
NULL
}
Interagir avec les graphique
Nous allons voir deux types d'interactions avec les graphiques pour illustrer la simplicité pour l’utilisateur d'interagir avec les données et les représentations :
Sélectionner les données à afficher à l’aide du tableau
Changer des paramètres graphiques sur le plot de base proposé par R (le premier graphique). Tous ces changements peuvent bien sûr être appliqués sur tous les graphiques.
Sélectionner les données à afficher à l’aide du tableau
Grâce à Shiny, il est possible de faire communiquer le tableau avec les graphiques. Nous profitons pour cela de la puissance du package DT qui génère le tableau. Les modifications que nous allons réaliser seront uniquement côté serveur. L'objectif est de récupérer les lignes qui sont affichées dans le tableau et de n'utiliser que ces lignes dans les graphiques. Comme précédemment, pour récupérer de l’information dans l’UI, il faut utiliser input$ID_ZONE. Nous souhaitons récupérer de l'information de notre tableau qui a comme identifiant dataTable. Ensuite, nous ajoutons _rows_all à la fin de l’ID pour obtenir les lignes. Ainsi, avec input$dataTable_rows_all, nous avons les lignes affichées dans le tableau. Il ne reste plus qu’à les sélectionner dans le vecteur de données. Le graphique est à présent dynamique.
output$plotAvecR <- renderPlot({
if (!is.null(data$table)) {
plot(data$table$Petal.Length[input$dataTable_rows_all],
data$table$Sepal.Length[input$dataTable_rows_all],
main = "Sepal length vs Petal length (R)",
ylab = "Sepal length",
xlab = "Petal length")
} else {
NULL
}
})
La même démarche est ensuite appliquée aux autres graphiques. Grâce aux filtres du tableau, nous avons ainsi la possibilité de sélectionner par les données numériques (longueur et largeur) et par l’espèce.
Changement de couleur pour le graphique de base R
L’objectif est de vous montrer une autre façon d'interagir avec les graphiques. En effet, il se peut que vous n’utilisiez pas de tableau dans votre application. De très nombreux exemples sont disponibles en ligne (ici par exemple : https://shiny.rstudio.com/gallery/). Nous allons implémenter 4 changements sur ce graphique pour vous donner des exemples d’utilisation d’inputs :
Changement de la couleur des points (avec l’utilisation d’un colour picker capable de gérer la transparence)
Changement du type de point
Changement de la taille des points
Changement du titre
Côté UI
Pour plus de lisibilité lors de l’utilisation, nous avons changé la disposition des graphiques pour avoir sur une ligne le graphique R avec ses paramètres et sur une seconde les trois autres graphiques. Vous pouvez ainsi voir la simplicité de la réorganisation d’une page à l’aide du système de Grid.
tabItem(tabName = "visualization",
h1("Visualisation des données"),
h2("Exploration du tableau"),
dataTableOutput('dataTable'),
h2("Graphiques"),
fluidRow(
column(4, plotOutput("plotAvecR")),
column(4, colourpicker::colourInput("colR", "Couleur graphique R", "black",allowTransparent = T),
sliderInput("cex", "Taille",
min = 0.5, max = 3,
value = 1,step = 0.2
)),
column(4, selectInput(inputId = "pch", choices = 1:20, label = "Type de points",selected = 1),
textInput("title", "Titre", "Sepal length vs Petal length (R)") )
),
tags$br(),
fluidRow(
column(4, plotOutput("plotAvecGgplot2")),
column(4, plotlyOutput("plotAvecPlotly")),
column(4, htmlOutput("plotAvecGoogle"))
)
)
Pour faire entrer de l’information, nous avons besoin de 4 fonctions input : colourInput pour la couleur (du package colourpcicker), sliderInput pour la taille des points, selectInput pour le type de points et textInput pour le titre du graphique.
Côté serveur
Nous allons récupérer les entrées et les intégrer dans notre plot.
Et voilà ! Vous avez réalisé une application complète capable de lire un fichier en fonction de paramètres et d'explorer ses données. Vous trouverez l’ensemble du code sur github ici : https://github.com/bioinfo-fr/bioinfo-fr_Shiny . A travers ce post, nous avons vu comment rendre interactive l’exploration d’un tableau de données à l’aide de Shiny. Vos utilisateurs n’auront plus à voir votre code. Ils auront simplement à appuyer sur Run App. Il existe de nombreuses solutions de partage (https://shiny.rstudio.com/tutorial/written-tutorial/lesson7/). De nombreuses autres possibilités sont disponibles et pourront être détaillées dans d’autres articles (concatémérisation et intégration continue d’une application Shiny, par exemple).
Winston Chang, Joe Cheng, JJ Allaire, Yihui Xie and Jonathan McPherson (2018). shiny: Web Application Framework for R. R package version 1.2.0.https://CRAN.R-project.org/package=shiny
Yihui Xie, Joe Cheng and Xianying Tan (2018). DT: A Wrapper of the JavaScript Library 'DataTables'. R package version 0.5. https://CRAN.R-project.org/package=DT
En génomique, et sans doute dans tout un tas d'autres domaines omiques ou big data, nous essayons souvent de tracer des grosses matrices sous forme d'heatmap. Par grosse matrice, j'entends une matrice dont le nombre de lignes et/ou de colonnes est plus grand que le nombre de pixels sur l'écran que vous utilisez. Par exemples, si vous avez une matrice de 50 colonnes et de 20 000 lignes (cas assez fréquent quand il y a une ligne par gène), il y a de forte chances que cette matrice aura plus de lignes qu'il n'y a de pixels sur votre écran -- 1080 pixels verticaux sur un écran HD (à moins bien sûr que vous lisiez ceci dans un futur lointain d'hyper haute définition).
Le problème lorsqu'on affiche des matrices qui ont plus de lignes que de pixel à l'écran, c'est justement que chaque pixel va devoir représenter plusieurs cellules de la matrice, et que le comportement par défaut de R sur ce point-là n'est pas forcément optimal.
Un exemple de données numériques
Commençons par générer un faux jeu de données, imitant ce qu'on peut obtenir en épigénomique. J'essaye de produire un signal centré au milieu des rangs, de plus en plus fort en fonction des colonnes, tout en gardant une part d'aléatoire. Je laisse le code pour que vous puissiez jouer chez vous à reproduire les figures de cet article, mais vous n'avez pas besoin de comprendre cette section pour comprendre la suite.
Il existe de nombreuses fonctions en R pour afficher cette matrice, par exemple heatmap(), gplots::heatmap.2(), ggplot2::geom_raster(), ou ComplexHeatmap::Heatmap(). La plupart de ces fonctions font appel à la fonction image() de plus bas niveau, qui est celle qui trace la matrice colorée. C'est cette fonction que nous allons utiliser dans ce billet:
oldpar <- par(mar = rep(0.2, 4)) # reducing plot margins
image(
t(genmat), # image() has some weird opinions about how your matrix will be plotted
axes = FALSE,
col = colorRampPalette(c("white", "darkorange", "black"))(30), # our colour palette
breaks = c(seq(0, 3, length.out = 30), 100) # colour-to-value mapping
)
box() # adding a box arround the heatmap
Figure 1: Une bien belle heatmap ?
On pourrait penser que tout va bien ici. On arrive bien à voir un signal, et on en tire la conclusion qu'il est au centre, plus fort en haut qu'en bas. Le souci c'est qu'avec 20 000 lignes dans notre matrice on devrait avoir une image beaucoup moins bruitée. Comme il n'y a que quelques centaines de pixels de hauteur dans le png (par défaut), R doit décider d'une manière ou d'une autre comment résumer l'information de plusieurs cellules en un seul pixel. Il semble que R choisisse plus ou moins au hasard une seule cellule à afficher par pixel (probablement la première ou la dernière dans la pile). Il y a donc un sous-échantillonnage important.
On pourrait imaginer générer un png de plus de 20 000 pixels de haut pour compenser, mais ça fait des fichiers lourds à manipuler, et il faut penser à agrandir d'autant la taille du texte et l’épaisseur des traits pour un résultat potable.
Autre idée, certaines devices graphiques (par exemple pdf(), mais pas png()) permettent jouer avec le paramètre useRaster = TRUE de la fonction image(), ce qui peut aider dans quelques situations. La rasterisation, d'après wikipedia, "est un procédé qui consiste à convertir une image vectorielle en une image matricielle". L’algorithme de rasterisation va donc essayer de convertir plusieurs lignes de données en un seul pixel.
pdf("big_hm_1.pdf")
layout(matrix(c(1, 2), nrow = 1)) # side by side plot
# Left plot, no rasterisation
image(
t(genmat),
axes = FALSE,
col = colorRampPalette(c("white", "darkorange", "black"))(30),
breaks = c(seq(0, 3, length.out = 30), 100),
main = "Original matrix"
)
# Right plot, with rasterisation
image(
t(genmat),
axes = FALSE,
col = colorRampPalette(c("white", "darkorange", "black"))(30),
breaks = c(seq(0, 3, length.out = 30), 100),
useRaster = TRUE,
main = "With rasterization"
)
dev.off()
Le fichier pdf généré est disponible ici. Mais les différents lecteurs pdf n'affichent pas le même rendu des plots en questions :
Figure 2 : useRaster = TRUE, une solution loin d'être idéale
Acrobat, Edge et Okular donnent le rendu attendu: une représentation bien plus fine des données originales lorsque la rasterisation est activée. Evince et SumatraPDF inversent les rendus, et voilent la version "non rasterisée" ! Le lecteur de pdf de Firefox abandonne carrément (en tout cas sous Windows 10, GNU/Linux il affiche le même résultat qu'Acrobat, Edge et Okular). Si votre lecteur de pdf préféré n'est pas parmi ceux que j'ai testé, je serai curieux d'avoir le résultat que vous obtenez en commentaire.
Pour info, alors que le pdf fait 5 Mo, le même code exportant du svg génère un fichier de 200 Mo ! Je n'ai lâchement pas eu le courage de l'ouvrir pour voir le rendu obtenu...
Au final, la rasterisation essaie de résumer les informations contenues derrières chaque pixel en en faisant une moyenne. Mais c'est un processus qu'on peut essayer de faire nous même, ce qui a deux avantages : on s'affranchit des différences de rendus entre lecteurs de pdf, et ça marchera même sur les devices non vectoriels, du genre png, ce qui évite de générer des images trop lourdes.
L'idée est donc de redimensionner la matrice avant le plot, en la rendant plus petite et en appliquant une fonction qui "résumera" les cellules correspondantes à chaque pixel (par exemple la fonction mean()). Je vous propose cette petite fonction (aussi disponible sur canSnippet) :
# reduce matrix size, using a summarizing function (default, mean)
redim_matrix <- function(
mat,
target_height = 100,
target_width = 100,
summary_func = function(x) mean(x, na.rm = TRUE),
output_type = 0.0, #vapply style
n_core = 1 # parallel processing
) {
if(target_height > nrow(mat) | target_width > ncol(mat)) {
stop("Input matrix must be bigger than target width and height.")
}
seq_height <- round(seq(1, nrow(mat), length.out = target_height + 1))
seq_width <- round(seq(1, ncol(mat), length.out = target_width + 1))
# complicated way to write a double for loop
do.call(rbind, parallel::mclapply(seq_len(target_height), function(i) { # i is row
vapply(seq_len(target_width), function(j) { # j is column
summary_func(
mat[
seq(seq_height[i], seq_height[i + 1]),
seq(seq_width[j] , seq_width[j + 1] )
]
)
}, output_type)
}, mc.cores = n_core))
}
genmatred <- redim_matrix(genmat, target_height = 600, target_width = 50) # 600 is very rougthly the pixel height of the image.
genmatred[1:5, 1:5]
## [,1] [,2] [,3] [,4] [,5]
## [1,] 0.4530226 0.4911097 0.4927123 0.5302643 0.5561331
## [2,] 0.5263392 0.5138786 0.5324716 0.5354325 0.5050932
## [3,] 0.4196155 0.4887105 0.5238630 0.5183627 0.5296764
## [4,] 0.5024431 0.5015508 0.5155568 0.5537814 0.5318501
## [5,] 0.5121447 0.5533040 0.4882006 0.4877140 0.5222805
dim(genmatred)
## [1] 600 50
Comparons le rendu Avant / Après :
layout(matrix(c(1, 2), nrow = 1))
# left plot, original matrix
image(
t(genmat),
axes = FALSE,
col = colorRampPalette(c("white", "darkorange", "black"))(30),
breaks = c(seq(0, 3, length.out = 30), 100),
main = "Original matrix"
)
box()
# Right plot, reduced matrix
image(
t(genmatred),
axes = FALSE,
col = colorRampPalette(c("white", "darkorange", "black"))(30),
breaks = c(seq(0, 3, length.out = 30), 100),
main = "Reduced matrix"
)
box()
# restoring margin size to default values
Figure 3 : Un rendu bien plus fin lorsqu'on réduit la taille de la matrice nous même.
Paradoxalement, on "discerne" bien mieux les détails des 20 000 lignes de la matrice en réduisant la taille de la matrice nous même, plutôt qu'en laissant R (mal) afficher les 20 000 lignes.
Matrices creuses (sparse)
Dans certaines situations, faire la moyenne des cellules par pixel n'est pas la manière la plus maline de résumer les données. Par exemple, dans le cas de matrices creuses, on ne souhaite pas moyenner nos quelques valeurs isolées par tous les zéros les entourant. Dans ces cas-là, prendre la valeur maximale correspondra mieux à ce qu'on cherche à montrer.
J'ai rencontré ce cas dans une étude d'eQTL (analyse QTL en utilisant les niveaux d'expressions des gènes comme phénotypes). L'idée et d'identifier des Single Nucleotide Polymorphisms (SNP, des variants / mutations) qui sont associé à des changements d'expression des gènes. Pour cela fait un test statistique d'association entre chaque SNP et chaque niveau d'expression de gènes, se qui nous donne autant de p-valeurs.
Nous avions l'expression d'environ 20 000 gènes, et environ 45 000 SNP, résultant en une matrice de 20 000 x 45 000 p-valeurs. La plupart des p-valeurs sont non significatives, et seule une minorité était très petite (ou très grande après transformation en -log10(p-valeur)). Or, ce qu'on souhaite c'est afficher les p-valeur des SNP principaux (lead SNP). On va donc plutôt prendre le maximum des -log10(p-valeur) plutôt que leur moyenne :
# left matrix, we take the mean of the -log10 of the p-values
redim_matrix(
eqtls,
target_height = 600, target_width = 600,
summary_func = function(x) mean(x, na.rm = TRUE),
n_core = 14
)
# right matrix we take the maximum of the -log10 of the p-values
redim_matrix(
eqtls,
target_height = 600, target_width = 600,
summary_func = function(x) max(x, na.rm = TRUE),
n_core = 14
)
Figure 4: À gauche : On résume la grosse matrice en calculant la moyenne des p-valeur par pixel. À droite : On prend la plus petite p-valeur (la plus grande après transformation -log10) pour mieux voir la significativité des lead SNP. Les p-valeurs réelles sont bien mieux représentés.
Données catégorielles
Dans le cas de données catégorielles, on ne peut pas vraiment prendre une moyenne des valeurs. Il faut plutôt faire les moyennes des couleurs associées à chaque catégorie (Ici, je le fais dans l'espace colorimétrique RGB, mais ça fonctionne peut-être encore mieux si la moyenne est faite en espace HCL ?). Pour afficher une matrice de couleurs, il faut utiliser rasterImage() au lieu d'image().
# some fake data
mycolors <- matrix(c(
sample(c("#0000FFFF", "#FFFFFFFF", "#FF0000FF"), size = 5000, replace = TRUE, prob = c(2, 1, 1)),
sample(c("#0000FFFF", "#FFFFFFFF", "#FF0000FF"), size = 5000, replace = TRUE, prob = c(1, 2, 1)),
sample(c("#0000FFFF", "#FFFFFFFF", "#FF0000FF"), size = 5000, replace = TRUE, prob = c(1, 1, 2))
), ncol = 1)
color_mat <- t(as.matrix(mycolors))
# custom funtion to average HTML colors
mean_color <- function(mycolors) {
R <- strtoi(x = substr(mycolors,2,3), base = 16)
G <- strtoi(x = substr(mycolors,4,5), base = 16)
B <- strtoi(x = substr(mycolors,6,7), base = 16)
alpha <- strtoi(x = substr(mycolors,8,9), base = 16)
return(
rgb(
red = round(mean(R)),
green = round(mean(G)),
blue = round(mean(B)),
alpha = round(mean(alpha)),
maxColorValue = 255
)
)
}
# Let's apply the redim_matrix() function using ou newly defined mean_color() function:
color_mat_red <- redim_matrix(
color_mat,
target_height = 1,
target_width = 500,
summary_func = mean_color,
output_type = "string"
)
# And do the ploting
layout(matrix(c(1, 2), nrow = 2))
# left plot, original matrix
plot(c(0,1), c(0,1), axes = FALSE, type = "n", xlab = "", ylab = "", xlim = c(0, 1), ylim = c(0,1), xaxs="i", yaxs="i", main = "Full matrix")
rasterImage(
color_mat,
xleft = 0,
xright = 1,
ybottom = 0,
ytop = 1
)
box()
# right plot, summarised matrix
plot(c(0,1), c(0,1), axes = FALSE, type = "n", xlab = "", ylab = "", xlim = c(0, 1), ylim = c(0,1), xaxs="i", yaxs="i", main = "Reduced matrix")
rasterImage(
color_mat_red,
xleft = 0,
xright = 1,
ybottom = 0,
ytop = 1
)
box()
Figure 5: Réduire la taille d'une matrice catégorielle avant le plot, en faisant la moyenne des couleurs par pixel, permet de représenter plus fidèlement les données.
ggplot2
D’après mes tests, ggplot2 est aussi affecté par ce souci d'overplotting, que ce soit geom_tile() ou geom_raster() (qui est une version optimisée de geom_tile() quand les cases sont régulières).
Figure 6 : ggplot2 victime de l'overplotting. Notez de subtiles différences entre geom_tile() et geom_raster().
ComplexHeatmap
Le package Bioconductor ComplexHeatmap est vraiment toppour générer des Heatmaps un peu complexe, avec des annotations dans tous les sens.
Cela dit, mes quelques tests suggèrent qu'il souffre du même problème d'overplotting que les autres fonctions. Il réalise un sous-échantillonnage des cellules à afficher, au lieu de moyenner les données par pixel :
library(ComplexHeatmap)
Heatmap(
genmat[nrow(genmat):1, ], # putting the top on top
col = circlize::colorRamp2(c(0, 1.5, 3), c("white", "darkorange", "black")),
cluster_rows = FALSE, cluster_columns = FALSE,
show_heatmap_legend = FALSE,
column_title = "No rasterisation"
)
Figure 7 : ComplexHeatmap nous déçoit.
La fonction Heatmap() a bien des paramètres qui permettent une rasterization dans le cas de grosses matrices, mais ils semblent plus utiles pour réduire le poids des fichiers vectoriels que pour résoudre le problème de sous-échantillonnage :
Figure 8: ComplexHeatmap nous déçoit même avec use_raster = TRUE
Conclusion
En R, réduisez vos grosses matrices avant de les afficher, vous verrez mieux les petits détails. Sinon vous obtiendrez des heatmaps un peu approximatives.
Le principal souci de cette solution, c'est qu'il faut faire les décorations (axes, barres de couleurs sur les côtés, dendrogrammes, etc.) à la main, ce qui est un peu laborieux.
Merci a mes talentueux relecteurs Mathurin, Gwenaëlle, et lhtd. Une version de cet article traduite en anglais sera publiée prochainement sur le blog de l'auteur.
Ça y est, votre code R un poil brut commence à avoir de la substance et vous envisagez d'en faire un outil à part entière. Comme tout bioinformaticien qui se respecte, vous envisagez donc de packager (ou paqueter en français) proprement cet ensemble de scripts R.
Non on ne largue pas une nuée de scripts non commentés, non documentés, avec juste un mail disant "Non mais tu changes tel et tel paramètres et ça fonctionne"...
Quand j'essaie d'expliquer le concept de reproductibilité à un mauvais bioinformaticien
Votre package R étant évidemment d'une grande utilité à la communauté bioinformatique, vous envisagez de le partager. La question du répertoire de packages où vous le déposerez une fois fini va donc se poser, et avec, les conditions de développement du package qui s'appliqueront.
Pourquoi déposer son package sur Bioconductor alors qu'il y a le CRAN ?
Le CRAN (pour Comprehensive R Archive Network) est évidemment LE répertoire de packages R. Il centralise bon nombre d'entre eux, touchant à des domaines variés : statistiques, physique, mathématiques, psychologie, écologie, visualisations, machine learning, cartographie, ..., et bioinformatique. Cependant à avoir autant de variété, on s'y perd. C'est pourquoi Bioconductor a en partie été créé.
Bioconductor c'est un répertoire alternatif de packages R spécifiquement dédié à la bioinformatique créé en 2002. Akira vous le présentait en 2012 sur le blog, et huit ans plus tard, force est de constater que le projet tient toujours, et même prend de l'ampleur (cf. Figure 1). La vocation est restée la même : regrouper sous une même bannière des packages spécifiques à la bioinformatique, et notamment à l'analyse et la compréhension de données de séquençage haut débit.
Figure 1 : évolution du nombre de packages déposés sur Bioconductor
L'avantage, c'est que la visibilité de votre package est augmentée au sein de la communauté bioinfo. Et au-delà du simple répertoire, c'est également une communauté particulièrement active qui s'y rattache, avec un système à la StackOverflow.
Mais est-ce intéressant pour autant dans votre cas ? Car tout le monde n'a pas intérêt et ne peut pas déposer dans Bioconductor.
Comment savoir si on doit déposer son package dans le CRAN ou Biocondutor ?
Alors pourquoi avoir fait ce répertoire plutôt que d'avoir tout mis dans le CRAN ? Et comment choisir où votre package doit être déposé ? Quelques éléments pour vous aider :
Comme dit plus haut, Bioconductor est spécialisé en package pour la bioinformatique. Donc si vous développez un package avec des méthodes généralistes comme dplyr ou spécialisées dans d'autres domaines comme ecodist en écologie, visez plutôt le CRAN.
À l'inverse, si vous importez dans votre package d'autres package qui proviennent de Bioconductor, visez plutôt un dépôt sur celui-ci.
Si vous êtes prêts à assurer un support à long terme, notamment en répondant aux questions sur votre package, dirigez-vous vers Bioconductor. Sinon visez le CRAN.
Si vous souhaitez une publication "en production" rapide de votre package, visez le CRAN. En effet Bioconductor fonctionne avec un système de release (publication) bisannuelle (une en avril, et une en octobre). Vous avez cependant accès à une branche dites de développement en attendant la release, la bioc-devel. Sachez tout de même qu'on a déjà vu des articles sortir avec un lien vers cette branche. La branche de production ne semble donc pas être une obligation pour publier son article même si c'est recommandé (question de stabilité).
Le CRAN est en droit de déprécier un package car il ne convient pas avec la nouvelle release de R (en vous mettant au courant quelques semaines au plus avant de le faire). Bioconductor possède une plus grande souplesse sur ce point : si vous vous assurez qu'il fonctionne pour chaque release, il est peu probable qu'il soit déprécié.
Si vous avez envie de ne pas passer les tests qui cassent les gonades de Bioconductor, allez sur le CRAN
Évidemment, vous trouverez des packages de bioinformatique sur le CRAN également (ex: WGCNA), mais c'est là une occasion de manquer de visibilité au sein de notre communauté. À savoir également que rien n'est gravé dans le marbre. La preuve avec le package mixOmics qui était initialement sur le CRAN mais a été déplacé sur Bioconductor il y a un peu plus d'un an !
J'ai choisi, ce sera Bioconductor ! Mais comment ça se passe ?
Félicitations ! Mais attendez-vous à un chemin pavé d’embûches et assez éprouvant, je ne vais pas vous mentir. Une fois fini, vous verrez que le jeu en vaut la chandelle.
Si vous n'avez jamais développé de package R, laissez-moi un commentaire comme quoi ça vous intéresse. J'y consacrerai alors un autre billet de blog. En attendant, je vous laisse suivre ce très bon tutoriel pour apprendre (ou bien lire le manuel du CRAN si vous avez un côté maso... puriste).
Imaginons à présent que vous avez (ou pensez avoir) terminé votre package appelé, de façon très originale, mypkg. Il va falloir passer par plusieurs phases de pré-soumission.
Commençons par la compilation de votre package, ou build. Si vos fonctions tournent sans problème lorsque vous les exécutez à la main hors de votre package, il n'est pas assuré que celui-ci fonctionne en tant que tel pour autant !
Deux choix s'offrent alors à vous quant à la façon de procéder :
Vous êtes un accro du shell et de R sans IDE ? Il vous faudra faire rouler dans votre shell la commande R CMD install. Exemple : R CMD install /home/bioinfofr/dev/R/mypkg
Vous aimez bien Rstudio et avoir un coup de pouce ? Le package devtools est fait pour vous. C'est lui qui est utilisé derrière les boutons de build de Rstudio. Rendez-vous dans cet onglet comme en Figure 2 et cliquez sur Build Source Package.
Figure 2 : Interface de Rstudio avec la localisation de la fenêtre de build et le bouton de compilation du package source.
Si tout se passe bien, vous devriez avoir ce genre de rapport :
✓ checking for file ‘/home/bioinfofr/dev/R/mypkg/DESCRIPTION’ ...
─ preparing ‘mypkg’: (1s)
✓ checking DESCRIPTION meta-information ...
─ installing the package to build vignettes
✓ creating vignettes (2m 8s)
─ checking for LF line-endings in source and make files and shell scripts
─ checking for empty or unneeded directories
Removed empty directory ‘mypkg/tests’
─ building ‘mypkg_0.99.99.tar.gz’
[1] "/home/bioinfofr/dev/R/mypkg_0.99.99.tar.gz"
Source package written to ~/dev/R
Et sinon... Eh bien les messages d'erreurs sont plutôt explicites sur la source du problème. Le plus souvent vous aurez simplement mal respecté l'architecture d'un package R (un dossier mal nommé, des fichiers qui traînent, etc.) ou des informations manquantes dans le fichier DESCRIPTION.
Une fois que votre package est compilé, passons aux checks (vérifications) automatiques. À nouveau, deux choix s'offrent à vous :
En shell : Il vous faudra faire rouler la commande R CMD check. Exemple : R CMD check /home/bioinfofr/dev/R/mypkg
Avec Rstudio/devtools : rendez-vous dans l'onglet build comme en Figure 3 et cliquez sur Check. Cela lance la fonction devtools::check().
Figure 3 : Interface de Rstudio avec la localisation de la fenêtre de build et le bouton de check
Une fois terminé, voici le genre de rapport que vous devriez obtenir :
── Building ──────────────────────────────────── mypkg ──
Setting env vars:
● CFLAGS : -Wall -pedantic -fdiagnostics-color=always
● CXXFLAGS : -Wall -pedantic -fdiagnostics-color=always
● CXX11FLAGS: -Wall -pedantic -fdiagnostics-color=always
─────────────────────────────────────────────────────────
✓ checking for file ‘/home/bioinfofr/dev/R/mypkg/DESCRIPTION’ (372ms)
─ preparing ‘mypkg’: (1.5s)
✓ checking DESCRIPTION meta-information ...
─ installing the package to build vignettes
✓ creating vignettes (2m 17.3s)
─ checking for LF line-endings in source and make files and shell scripts (351ms)
─ checking for empty or unneeded directories
Removed empty directory ‘mypkg/tests’
─ building ‘mypkg_0.99.99.tar.gz’
── Checking ──────────────────────────────────── mypkg ──
Setting env vars:
● _R_CHECK_CRAN_INCOMING_USE_ASPELL_: TRUE
● _R_CHECK_CRAN_INCOMING_REMOTE_ : FALSE
● _R_CHECK_CRAN_INCOMING_ : FALSE
● _R_CHECK_FORCE_SUGGESTS_ : FALSE
● NOT_CRAN : true
── R CMD check ─────────────────────────────────────────────────────────────────
─ using log directory ‘/home/bioinfofr/dev/R/mypkg/mypkg.Rcheck’ (517ms)
─ using R version 4.0.0 (2020-04-24)
─ using platform: x86_64-pc-linux-gnu (64-bit)
─ using session charset: UTF-8
─ using options ‘--no-manual --as-cran’ (692ms)
✓ checking for file ‘mypkg/DESCRIPTION’
─ this is package ‘mypkg’ version ‘0.99.99’
─ package encoding: UTF-8
✓ checking package namespace information ...
✓ checking package dependencies (6.5s)
✓ checking if this is a source package
✓ checking if there is a namespace
✓ checking for executable files (510ms)
✓ checking for hidden files and directories
✓ checking for portable file names ...
✓ checking for sufficient/correct file permissions
✓ checking whether package ‘mypkg’ can be installed (24.9s)
N checking installed package size ...
installed size is 10.2Mb
sub-directories of 1Mb or more:
data 5.6Mb
doc 4.1Mb
✓ checking package directory ...
✓ checking for future file timestamps (1.1s)
✓ checking ‘build’ directory
✓ checking DESCRIPTION meta-information ...
✓ checking top-level files
✓ checking for left-over files
✓ checking index information ...
✓ checking package subdirectories ...
✓ checking R files for non-ASCII characters ...
✓ checking R files for syntax errors ...
✓ checking whether the package can be loaded (8s)
✓ checking whether the package can be loaded with stated dependencies (7.9s)
✓ checking whether the package can be unloaded cleanly (7.3s)
✓ checking whether the namespace can be loaded with stated dependencies (7.4s)
✓ checking whether the namespace can be unloaded cleanly (7.6s)
✓ checking loading without being on the library search path (7.6s)
✓ checking dependencies in R code (7.1s)
✓ checking S3 generic/method consistency (10.2s)
✓ checking replacement functions (7.2s)
✓ checking foreign function calls (7.4s)
✓ checking R code for possible problems (26s)
✓ checking Rd files (339ms)
✓ checking Rd metadata ...
✓ checking Rd line widths ...
✓ checking Rd cross-references ...
✓ checking for missing documentation entries (7.9s)
✓ checking for code/documentation mismatches (23.6s)
✓ checking Rd usage sections (9.6s)
✓ checking Rd contents ...
✓ checking for unstated dependencies in examples ...
✓ checking contents of ‘data’ directory (380ms)
✓ checking data for non-ASCII characters (657ms)
✓ checking data for ASCII and uncompressed saves ...
✓ checking installed files from ‘inst/doc’ ...
✓ checking files in ‘vignettes’ ...
✓ checking examples (1m 21.8s)
Examples with CPU (user + system) or elapsed time > 5s
user system elapsed
my_foo_one 17.875 0.191 11.064
my_foo_two 1.468 0.032 10.086
my_foo_de_vous 0.152 0.004 24.143
my_foo_foo_ne 0.022 0.004 7.865
✓ checking for unstated dependencies in vignettes ...
✓ checking package vignettes in ‘inst/doc’ ...
✓ checking re-building of vignette outputs (1m 47.3s)
✓ checking for non-standard things in the check directory
✓ checking for detritus in the temp directory
See
‘/home/bioinfofr/dev/R/mypkg/mypkg.Rcheck/00check.log’
for details.
── R CMD check results ────────────────────────────────────── mypkg 0.99.99 ────
Duration: 6m 14.2s
> checking installed package size ... NOTE
installed size is 10.2Mb
sub-directories of 1Mb or more:
data 5.6Mb
doc 4.1Mb
0 errors ✓ | 0 warnings ✓ | 1 notes x
R CMD check succeeded
Comme vous le voyez, le risque d'erreurs/avertissements est assez grand (trop grand pour que je couvre ici tous les cas !). Mais rassurez-vous, personne ne pense à tout dès la première fois qu'il développe son package. Vous finirez donc irrémédiablement avec des corrections à faire, car pour pouvoir soumettre votre package, vous ne devez avoir aucune ERROR ou WARNING. La meilleure pratique pour éviter d'avoir des dizaines de corrections à faire, c'est de tester le build et exécuter les checks régulièrement dans votre processus de développement (ça vous évitera de devoir recoder des pans entiers de votre outil car l'erreur vient d'un bout de code sur lequel s'appuient d'autres). Pensez également à mettre à jour les packages sur votre poste local car ce sont les dernières version de ceux-ci qui seront utilisées par le CRAN pour rouler les checks de leur côté (et pareil pour Bioconductor).
Les NOTEs sont quant à elles tolérées dans une certaine mesure, et cette mesure est à la discrétion de la personne qui fera la relecture critique de votre package (mais comme nous allons viser Bioconductor dans cet article, ce sont d'autres reviewers encore que ceux du CRAN).
Les prérequis spécifiques à Bioconductor
Vous avez tous vos checks qui passent ? Bien joué. Mais il va encore vous falloir passer ceux de Bioconductor ! Et c'est pas gagné.
Bioconductor est encore plus restrictif que le CRAN sur ses tests appelés BiocCheck. En effet, ils ont défini tout un ensemble de bonnes pratiques supplémentaires liées à la fois aux spécificités bioinformatiques, à la lisibilité du code, à des recommandations sur l'utilisation de telle ou telle façon de coder. Si certains de leurs partis pris sont discutables, il n'en reste pas moins que vous allez devoir vous y soumettre.
À défaut de vous les lister tous, je vais vous indiquer le minimum à respecter pour vous éviter masse de changement. L'idéal reste tout de même de les lire avant de commencer à développer votre package, car certains vont fortement impacter votre package.
Il va falloir définir à quelle catégorie votre package appartient : Software, Experiment Data, ou Annotation. La première est celle regroupant les outils d'analyse à proprement dit. Les deux dernières sont des packages de données aux noms assez explicites quant à leur contenu.
Si votre package utilise des données classiques en bioinfo (matrices gènes X échantillons, FASTA, VCF, spectro de masse, etc.) sachez que vous devrez au moins rendre compatible votre package avec les common classesde Bioconductor. Ces formats sont une tentative de standardisation de l'input dans les packages de ce dépôt.
Si c'est un Software, votre package final devra faire moins de 5Mo. Attention donc aux jeux de données d'exemple que vous insérez dedans. Si jamais vous souhaitez inclure des données plus grosses, on vous conseille de soumettre un autre package de type Experiment Data, ou Annotation et de l'appeler dans votre package.
Il devra être compatible Linux ET Windows ! Mauvaise idée donc de se baser sur des spécificités de votre OS. Ou alors préparez-vous à devoir développer un équivalent dans l'autre OS.
Toutes vos fonctions/données doivent être documentées ! Et pas seulement avec une rapide description. On parle d'une explication des paramètres, d'une valeur retour, et surtout d'un exemple exécutable fonctionnel.
Il faudra écrire une vignette, ce document faisant office d'introduction à votre package et de tutoriel. Il est nécessaire qu'on passe à travers un maximum des fonctionnalités de votre package, l'idéal étant d'y réaliser une analyse type, sur de véritables données ou des données simulées.
Les tests unitaires ne sont pas obligatoires d'après leur guidelines, mais soyez assurés qu'ils vous les demanderont tout de même pour la majorité de vos fonctions au moment de la revue. Mais en tant que bioinformaticien consciencieux vous les auriez déjà faits n'est-ce pas ?
Vous pensez avoir respecté ces recommandations ainsi que la liste complète sur leur site ? Passons à la vérification ! Tout d'abord, installez le package BiocCheck car les checks de Bioconductor ne se trouvent pas nativement dans l'install de R. Ensuite, lancez-le depuis votre session R en vous assurant d'avoir votre working
if (!requireNamespace("BiocManager", quietly = TRUE))
install.packages("BiocManager")
BiocManager::install("BiocCheck")
BiocCheck::BiocCheck()
Des erreurs/warnings vont à nouveau s'afficher, et recommence le jeu de la correction en fonction des messages donnés.
À savoir que même si les NOTEs sont en théorie acceptées, il suffit que vous tombiez sur un reviewer un peu zélé pour que vous deviez également les corriger entièrement. Voici un exemple de rapport BiocCheck :
This is BiocCheck version 1.24.0. BiocCheck is a work in progress. Output and severity of issues
may change. Installing package...
* Checking Package Dependencies...
* Checking if other packages can import this one...
* Checking to see if we understand object initialization...
* NOTE: Consider clarifying how 4 object(s) are initialized. Maybe they are part of a dataset loaded with data(), or perhaps part of an object referenced in with() or within().
function (object)
my_foo_one (.)
my_foo_two (sides)
my_foo_de_vous (ouuuh)
my_foo_foo_ne (.)
* Checking for deprecated package usage...
* Checking for remote package usage...
* Checking version number...
* Checking version number validity...
Package version 0.99.99; pre-release
* Checking R Version dependency...
* Checking package size...
Skipped... only checked on source tarball
* Checking individual file sizes...
* Checking biocViews...
* Checking that biocViews are present...
* Checking package type based on biocViews...
Software
* Checking for non-trivial biocViews...
* Checking that biocViews come from the same category...
* Checking biocViews validity...
* Checking for recommended biocViews...
* Checking build system compatibility...
* Checking for blank lines in DESCRIPTION...
* Checking if DESCRIPTION is well formatted...
* Checking for whitespace in DESCRIPTION field names...
* Checking that Package field matches directory/tarball name...
* Checking for Version field...
* Checking for valid maintainer...
* Checking DESCRIPTION/NAMESPACE consistency...
* Checking vignette directory...
This is a software package
* Checking library calls...
* Checking for library/require of mypkg...
* Checking coding practice...
* NOTE: Avoid sapply(); use vapply()
Found in files:
foo_fighters.R (line 145, column 17)
* NOTE: Avoid 1:...; use seq_len() or seq_along()
Found in files:
foo_tre.R (line 83, column 67)
* Checking parsed R code in R directory, examples, vignettes...
* Checking function lengths..................................
* NOTE: Recommended function length <= 50 lines.
There are 2 functions > 50 lines.
foo_taj_2_gueule() (R/foo_rage.R, line 59): 194 lines
foo_l() (R/foo_fly.R, line 301): 113 lines
* Checking man page documentation...
* NOTE: Consider adding runnable examples to the following man pages which document exported
objects:
foo_lure.Rd
* Checking package NEWS...
* NOTE: Consider adding a NEWS file, so your package news will be included in Bioconductor
release announcements.
* Checking unit tests...
* Checking skip_on_bioc() in tests...
* Checking formatting of DESCRIPTION, NAMESPACE, man pages, R source, and vignette source...
* Checking if package already exists in CRAN...
* Checking for bioc-devel mailing list subscription...
* NOTE: Cannot determine whether maintainer is subscribed to the bioc-devel mailing list (requires admin credentials). Subscribe here:
https://stat.ethz.ch/mailman/listinfo/bioc-devel
* Checking for support site registration...
Maintainer is registered at support site.
Summary:
ERROR count: 0
WARNING count: 0
NOTE count: 7
For detailed information about these checks, see the BiocCheck vignette, available at
https://bioconductor.org/packages/3.11/bioc/vignettes/BiocCheck/inst/doc/BiocCheck.html#interpreting-bioccheck-output
$error
character(0)
$warning
character(0)
$note
[1] "Consider clarifying how 4 object(s) are initialized. Maybe they are part of a data set loaded with data(), or perhaps part of an object referenced in with() or within()."
[2] " Avoid sapply(); use vapply()"
[3] " Avoid 1:...; use seq_len() or seq_along()"
[4] "Recommended function length <= 50 lines."
[5] "Consider adding runnable examples to the following man pages which document exported objects:"
[6] "Consider adding a NEWS file, so your package news will be included in Bioconductor release announcements."
[7] "Cannot determine whether maintainer is subscribed to the bioc-devel mailing list (requires adminncredentials). Subscribe here: https://stat.ethz.ch/mailman/listinfo/bioc-devel"
Comme vous le voyez il reste des NOTEs, libre à vous de choisir de les corriger à présent ou attendre de voir si le reviewer vous l'impose (personnellement j'ai perdu à ce jeu comme vous pourrez le voir dans la section Retour d'expérience personnelle).
Le processus de soumission à Bioconductor
Tout semble bon ? Ça compile ? Vous passez tous les checks ? Bien, passons à la soumission de votre package.
Si ce n'est pas déjà fait, poussez votre package sur GitHub (et si vous n'y connaissez rien à git, lisez cet article ). C'est en effet l'hebergeur qu'a choisi Bioconductor pour gérer la soumission et le suivi du développement des packages (mais rien ne vous empêche d'avoir votre repo sur un GitLab, Bitbucket, etc. à côté bien évidemment).
Créez une issue sur le repo Contributions de Bioconductor en suivant le guide fournit. Rajoutez un petit bonjour et une phrase rapide de description, ça ne mange pas de pain non plus.
Créez un webhook pour votre repo. Cela va permettre au bot qui gère l'automatisation des soumissions d'être informé qu'il y a un nouveau package à aller chercher, et par la suite ça permettra qu'il soit informé quand vous effectuez des modifications sur votre repo (correction de code, ajout de nouvelle fonctionnalité...).
Ajoutez une clef SSH publique. Elle sera utilisée pour copier votre repo une fois votre package accepté.
Attendez... Votre package va être tagué comme "1. awaiting moderation'" en attendant, comme c'est dit dans le tag, qu'un reviewer soit assigné et vienne estimer si votre package vaut le coup d'être ajouté à Bioconductor.
Si sa pertinance est jugée bonne, le tag changera pour "2. review in progress" et le bot commencera alors à re-exécuter tous les checks que vous avez en théorie déjà passés haut la main.
Et là… Peu importe si vous avez mis tout votre cœur, sueur et temps dans vos checks, peu importe que ça roule sur votre PC et votre VM pour l'autre OS, vous allez avoir des erreurs ! Murphy, si tu nous entends…
Le bot va taguer votre issue avec ERROR ou WARNINGS ainsi que vous donner un lien vers un rapport détaillé des tests et erreurs obtenues pour chacun des OS (voir Figure 4, attention, ça pique les yeux). Et si Murphy vous a épargné, sautez directement trois points plus loin (ne passez pas par la case départ, ne touchez pas 20K).
Commence alors un nouveau cycle d'essai/erreur pour la correction des derniers soucis qui sont apparus. Cependant, pour que le bot prenne en compte les modifications, il vous faudra incrémenter la version de votre package. Vous commencez normalement à 0.99.0 (format imposé par Bioconductor) et à chaque modification que vous poussez et où vous souhaiterez une évaluation, il faudra passer à 0.99.1, 0.99.2, etc.
Notez que si le bot détecte des modifications poussées mais pas d'incrément, il taguera votre issue avec "VERSION BUMP REQUIRED". Il est donc de bonne pratique d'effectuer vos modifications sur une autre branche que le master, puis de merger avec le master quand vous avez fini votre modification et fait repasser tous les tests. Je ne saurais que trop vous recommander un modèle de branches git de ce type. Attention, n'incrémentez la version qu'après le merge, sinon le bot ne déclenche pas un nouveau build.
Figure 4 : Exemple d'un rapport de build que vous donne le bot Bioconductor en lien dans votre issue sur GitHub
Une fois toutes les modifications faites pour n'avoir aucunes erreurs et le précieux tag "OK", votre reviewer va évaluer votre package. C'est notamment à ce moment-là que vous verrez s'il vous demande d'adresser les NOTEs laissées, si vous ne suivez pas des guidelines non testées automatiquement comme l'utilisation des common classes. Il vous demandera probablement des modifications, et vous repartirez pour un nouveau cycle d'essai/erreur.
Enfin, une fois tout cela fini, l'encore plus précieux tag "3a. accepted" vous sera normalement attribué et votre package sera ajouté au repo de Bioconductor. Il sera alors publié avec la prochaine release à venir.
Si pour une raison ou une autre votre package était tagué "3b. declined", n'hésitez pas à communiquer avec les reviewers pour en connaitre la raison.
Vous pouvez souffler, c'est fait ! À présent, il ne vous reste plus qu'à savourer votre victoire, et vous assurer à chaque release de Bioconductor (2 fois par an donc) que votre package compile toujours et passe les checks.
Retour d'expérience personnelle et astuces
Avant de vous laisser avec quelques conclusions, je voudrais vous apporter rapidement quelques points basés sur un ressenti personnel de cette "expérience" :
Le processus est LONG, et c'est un euphémisme. Entre les checks de base qui ne passent pas, puis ceux de BiocChecks, il y a déja pas mal de temps passé à corriger le code, et je les passais pourtant régulièrement au cours de mon développement ainsi que mes tests unitaires + la même chose sur ma VM Windows. Quand une fois fini j'ai vu de nouvelles erreurs s'afficher dans le rapport du bot de Bioconductor, j'ai ragé assez fort... Je maintiens que la fonction BiocCheck devrait intégrer ces tests supplémentaires effectués sur leurs serveurs...
Si vous visez une release en particulier, prenez-vous-y à l'avance de facilement 1 à 2 mois. Les reviewers sont surchargés sur la fin et mettent du temps à faire les reviews. Le mien n'a d'ailleurs donné de signe de vie que deux semaines après la date buttoir de dépôt, me laissant moins de 5 jours pour corriger les nombreuses remarques qu'il m'a faites. J'ai finalement loupé la release…
J'ai aussi eu la malchance de tomber en plein changement de version majeure de R (3.6 -> 4.0) sur laquelle vous devez tester votre package, alors que la release officielle n'est pas encore disponible. À vous les joies de la compilation de la R-alpha ! Ou sinon vous faites les bourrins et vous vous servez du rapport d'erreur du bot Bioconductor mais c'est pas mal plus lent de faire les allers-retours, et je préfère avec un master (un peu) propre.
Mettez vos packages à jour avant de pousser sur Bioconductor, vraiment. Ça évite de ronchonner sur votre reviewer en disant que vous n'arrivez pas à reproduire ce bug magique qui n'existe que sur leurs serveurs et sur aucune de vos VM (et donc éviter de passer pour une andouille).
Pour ceux qui développent sous Linux et ne veulent pas se prendre la tête avec une VM, il existe win-builder. Vous pouvez d'ailleurs l'utiliser directement dans devtools avec la fonction check_win_release(). Je ne l'ai découvert qu'après avoir monté ma VM et compilé la R-alpha dessus...
À la relecture, Guillaume m'a parlé de R-hub. C'est une version plus poussée de win-builder qui vous permet de faire les checks sur sur une grande variété d'OS et de versions de chacun. Très utile aussi donc
Certains reviewers sont mieux que d'autres : le mien était particulièrement désagréable dans sa façon de parler et assez zélé. Il m'a obligé à corriger l'intégralité des NOTEs. Mais d'autres sont charmants et comprennent que certaines NOTEs ne justifient pas un refus du package.
Conclusion
La soumission d'un package de Bioconductor est une épreuve en soi. Il y a de quoi se décourager à de maintes reprises, mais le résultat est là : un package visible par toute la communauté bioinfo. Il sera donc plus facilement partageable et pourra potentiellement devenir incontournable dans votre domaine.
Bon courage à vous si vous vous lancez dedans !
Merci à mes relecteurs Guillaume, lhtd, et neolem pour la relecture de cet article assez dense !
Références
Le site de bioconductor : http://www.bioconductor.org/
Les superbes tutoriels (en français) de mes collègues Charles Joly-Beauparlant et Éric Fournier. Ils sont écrits de façon décontractée avec un condensé du minimum à savoir, des outils pour vous faire gagner du temps, et de quelques bonnes pratiques. L'un porte sur la création d'un package R, l'autre sur le processus de soumission à Bioconductor, et un dernier que je n'ai pas encore appliqué qui porte sur l'intégration continue via Travis.
Tous les deux passionnés par l’enseignement, les problématiques de big data et d’analyse de données en biologie, nous nous côtoyons professionnellement depuis 15 ans, avec écoute et bienveillance. Si l’étiquette de « bioinformaticien » nous est souvent attribuée, nous sommes pourtant très différents.
Je (Gaëlle) travaille sur des problématiques de génomique fonctionnelle des champignons. Je me considère comme une analyste de données « omiques » (transcriptomique essentiellement). J’aime les statistiques et (un peu) l’informatique. Sans surprise, mon langage de programmation préféré est R.
Je (Pierre) travaille sur des problématiques d’analyse et de recyclage des données en biologie (en protéomique notamment). Je développe des outils logiciels facilitant le travail aux interfaces entre les scientifiques. J’aime l’informatique et (un peu) les statistiques. Sans surprise, mon langage de programmation préféré est Python.
Dans ce contexte, notre association, que ce soit en enseignement ou en recherche, a toujours été très pertinente. En un mot, nous sommes complémentaires.
Pourquoi cet article ?
Pour ceux qui débutent une formation en bioinformatique, la question du choix d’un langage de programmation est à la fois récurrente et importante. Nous avons développé chacun(e) des ressources pédagogiques pour accompagner nos étudiants dans leur apprentissage du langage de programmation R (Gaëlle) ou Python (Pierre). Celles-ci sont disponibles sur internet et ont été utilisées par des centaines d’apprenants, en formation initiale ou continue. Si idéalement la maîtrise des deux langages de programmation, R et Python, nous semble pertinente, dans la pratique, cela est compliqué. Ainsi, s’il fallait former les futurs scientifiques en bioinformatique / analyse de données / data science à un seul langage de programmation, lequel faudrait-il privilégier ? Dans cet article, nous partageons nos réflexions sur ce sujet. Nous sollicitons la participation du lecteur pour compléter cette discussion, et nous faire découvrir des points de vue qui nous auraient échappés*.
Découvertes respectives des langages R et Python
Je (Gaëlle) me souviens parfaitement de ma découverte du langage R. J’étais alors étudiante dans le DEA « Analyse des Génomes et Modélisation Moléculaire » (année 2001 - 2002, Université Paris Diderot). Nous suivions une formation intensive en programmation et après une première série de cours en C, l’enseignant avait changé ce matin-là. Il nous a salué et a immédiatement demandé « d’ouvrir un terminal, et de taper air ». Mes collègues de promotion et moi étions perplexes, mais l’une d’entre nous a dit « c’est la lettre R qu’il faut taper ». Elle connaissait déjà, en effet, ce logiciel, grâce à sa formation d’ingénieure agronome. Me voilà donc face à mon premier terminal R, pour la matinée. Le formateur nous a parlé de calculs vectoriels et matriciels, de fonctions d’optimisation numérique, etc. Son cours avait une coloration très mathématiques. J’étais perdue. Pour conclure la séance, il nous a dit, « voici une matrice bruitée, et pour vous entraîner, je vous demande d’écrire une procédure de lissage† ». Facile (ou pas !). « C’est obligatoire comme travail ? », avons-nous posé comme question. « Non » a répondu l’enseignant qui se voulait gentil à notre égard. « Ouf » ai-je pensé, « je vais pouvoir vite passer à autre chose ». Mais 20 ans après cette journée, je ne suis toujours pas passée à autre chose...
Je (Pierre) ai débuté en programmation avec du Windev, puis du Fortran, du Java, du C++ et du Perl en master. Pendant les 3 ans de ma thèse (2003 - 2006), j’ai simulé le comportement de peptides par des algorithmes Monte Carlo. J’ai développé un logiciel en C++ pour réaliser ces simulations. Je créais aussi de petits programmes pour analyser mes résultats, toujours en C++. Ce n’était pas très optimal d’utiliser ce langage de programmation pour calculer une moyenne ou formater des tableaux de données. Un jour, un collègue enseignant-chercheur qui travaillait dans mon bureau m’a parlé de Python. Il utilisait ce langage pour analyser ses données. Il s'agissait de Python 2.6 à l’époque (nous étions en 2005), mais quelle découverte ! Je n’avais plus besoin de compiler mes programmes et les étapes laborieuses de transformation de données étaient facilitées par les ancêtres du module NumPy (Numeric et numarray). Étant dans un labo de physique, je réalisais mes figures avec des outils comme gnuplot (en ligne de commande) ou xmgrace (avec une interface graphique, mais aussi scriptable). Lors de mon arrivée en post-doc dans un laboratoire de bioinformatique (2006), à ma grande surprise, tout le monde me parlait de R pour faire des figures. « C’est quoi R ? », ai-je demandé.
Principaux usages des langages R et Python
Au quotidien, mon travail (Gaëlle) se résume essentiellement à exploiter des tableaux de données. Au plus, ces tableaux sont composés de quelques milliers de lignes et quelques dizaines de colonnes. Je suis bien loin des volumes du big data, tels que les data scientists peuvent les rencontrer aujourd’hui. Je travaille le plus souvent en local sur mon ordinateur. J’utilise R via l’interface RStudio et je crée des rapports d’analyses de données avec R Mardown. Mes librairies les plus utilisées sont FactoMineR, igraph, marray, limma, DESeq2, bPeaks et plus récemment dplyr, ggplot2, mixOmics ou ade4. Les seules limites que je ressens parfois dans mon utilisation de R concernent le traitement de fichiers de données non structurées, en particulier ceux qui comportent des informations textuelles‡. Je me dis alors qu’il faudrait que je puisse utiliser Python. C’est d’ailleurs pour cette raison que j'ai suivi en 2012, une formation Python. À l’époque, je commençais à travailler avec des fichiers de données FASTQ, et il se disait que « R ne passerait pas le cap du traitement des données de séquençage haut débit»§. Mais finalement, ma formation Python a été bien vite oubliée, car une fois de plus, R a gardé toute sa pertinence dans mes activités de recherche. J’ai également pu compter sur la présence au laboratoire de mes premiers étudiants en thèse, très bien formés en Python.
Au quotidien, mon travail (Pierre) consiste à développer des algorithmes et des outils pour l’analyse de données, depuis la bioinformatique structurale jusqu’à la protéomique. J’utilise des modules comme NumPy et pandas pour le traitement de données et les statistiques descriptives, Matplotlib, Seaborn et Plotly pour la visualisation, Scipy pour les tests statistiques, scikit-learn pour faire un peu de machine learning, pytest pour les tests et l’intégration continue, flask pour le développement d’applications web, Voilà et Streamlit pour les tableaux de bord (dashboards) et les applications web très simples. Au-delà de Python à proprement dit, j’utilise beaucoup Jupyter Lab pour l’exploration de données et git et GitHub pour conserver la trace des différentes modifications apportées au code, et surtout, pour le travail collaboratif et la gestion de projet. Je package aussi la plupart des outils que je développe sous forme de paquets Python (dans PyPI), conda (dans Bioconda) ou Docker (dans Biocontainers). Pour autant, R ne m’est pas étranger. Certains de mes collègues y étant attachés, j’ai appris ce langage et je l’ai utilisé pour générer les figures d’un article publié en 2010. Je me suis aussi formé en participant à un MOOC R en 2014 et j’ai ainsi pu accompagner le travail d’un biostatisticien de 2014 à 2016 (lui travaillant avec STATA, moi avec R).
De nouvelles perspectives
En 2021, je (Gaëlle) décide de m’inscrire au diplôme universitaire de Bioinformatique Intégrative (DU DUBii), formation continue proposée par l’Université Paris et l’IFB. Après une année bouleversée par la crise COVID-19, mes motivations sont multiples. L’une d’elle est mon souhait de me (re)former à la programmation Python. En effet, dans mon université, la décision a été prise d’initier les étudiants de licence à la programmation informatique en utilisant Python (et non plus R comme cela était le cas depuis quelques années). J’avais soutenu cette volonté de changement, car je n’ai pas oublié mes débuts difficiles avec R et je sais à quel point ce langage peut être déroutant. Je sais également que je vais retrouver Pierre pour ces apprentissages. Je suis curieuse de vivre ses enseignements comme apprenante et j’en profite aussi (quand même...) pour le mettre au défi de me montrer que « Python vaut bien R » en analyse de données (Note de Pierre : « Challenge accepted »). J’aborde donc cette nouvelle formation en Python avec enthousiasme et application, soutenue par une équipe pédagogique dont l’encadrement est impeccable. Je retrouve assez facilement mes acquis de 2012 et je me dis que je vais pouvoir sereinement encadrer des étudiants de licence l’année prochaine, en TP de Python. Objectif atteint donc. Mais voilà que les deux derniers jours de la formation arrivent, avec au programme : « NumPy » et « pandas ». Des noms de modules familiers, mais que je n’avais pas expérimentés lors de ma précédente formation. Je découvre alors (et je comprends enfin) que la communauté des développeurs Python, par le biais de la data science, développe des solutions qui se rapprochent beaucoup de mes usages de R. Il est possible de manipuler des vecteurs et des tableaux, de façon très simple. Les représentations de données sont facilitées, ainsi que la mise en application des méthodes statistiques telle que l’ACP (un comble !). Bref, je constate que depuis ma dernière formation, Python a bien changé. R aussi a également beaucoup évolué. L’ensemble des librairies « tidyverse » est devenu incontournable, modifiant en profondeur la syntaxe classiquement utilisée dans le « R-base » et permettant l’obtention de graphiques élégants et interactifs. Alors R ou Python, Python ou R ? Que choisir en 2021 ? Les discussions ont été nombreuses et parfois vives entre les participants de la formation¶…
Utilisateur et formateur Python depuis de nombreuses années, je (Pierre) ai été témoin de l’évolution de Python depuis son utilisation dans des disciplines scientifiques comme la physique et la bioinformatique jusqu’à son adoption massive en data science. L'essor de la bibliothèque pandas au début des années 2010 a révolutionné la manière dont un tableau de données peut être analysé, aussi simplement qu’avec R. Des évolutions de pandas, comme Vaex supportent la manipulation de tableaux de données de plusieurs millions de lignes, quasiment instantanément. Les bibliothèques graphiques comme Bokeh et Plotly produisent des graphiques interactifs. L’écosystème Jupyter, depuis les notebooks (ou bloc-code) jusqu’à l’environnement d’analyse Jupyter Lab, ont aussi grandement contribué à la diffusion de ce langage de programmation. Plus récemment, les bibliothèques Voilà et surtout Streamlit, sont devenus de sérieux compétiteurs de Shiny en R#.
Bilan : nécessité de différencier formation initiale et formation continue, importance de la communauté de recherche
Finalement, notre analyse commune (Gaëlle et Pierre) est que derrière les langages de programmation R et Python, deux communautés de scientifiques différentes se regroupent. Les mathématiques / statistiques pour R et l’informatique / physique pour Python. Assez naturellement, chacun rejoint sa communauté comme nous l’avons fait l’un et l’autre. Les échanges sont facilités si vous avez (comme nous) un(e) bon(ne) ami(e) dans l’autre communauté, les limitations ponctuelles sont alors vite réglées.
Ainsi, je (Gaëlle) vais continuer à travailler avec R en recherche et utiliser ponctuellement Python (surtout en enseignement). Je pense que mes 20 années d’expérience avec R ont encore une valeur supérieure aux améliorations récentes de Python. Mais, je sais maintenant qu’il est possible de faire la plupart de mes analyses avec Python, c’est une évolution de pensée importante. Je choisis donc de conserver R pour mes activités de recherche, car pour moi, c’est plus simple ainsi. Mais, que faut-il proposer aux nouveaux apprenants ? Ceux qui n’ont pas 20 ans d’expériences avec l’un ou l’autre de ces langages de programmation.
Pour répondre à cette question, il est nécessaire de différencier les contextes de formation. En formation continue (professionnelle), nos apprenants appartiennent à une communauté de chercheurs « omiques », qui utilisent pour la très grande majorité d’entre eux R. Cette situation a certainement vocation à évoluer (voir ci-dessous), mais pour le moment, elle est ainsi. Nous pensons donc que dans ce contexte et si « un seul langage » doit être choisi, il est utile de privilégier R (avec une attention particulière portée sur les solutions récentes comme « tidyverse »). Bien sûr, si le temps de formation le permet, Python est alors un bon complément. Par contre, en formation initiale, la situation est différente. Python est aujourd’hui le langage de programmation abordé au lycée et nos étudiants, particulièrement ceux de licence, ne rentreront dans le monde professionnel que dans 3 à 5 ans. Il est nécessaire d’anticiper ce que seront alors R et Python**. Il nous semble que les évolutions de la biologie vers la data science sont en faveur de Python††. Aussi, nous recommandons de former tous les étudiants de licence à Python. Ceux qui deviendront les data scientists de demain verront ensuite s’ils ont besoin de compétences complémentaires en R. Quels que soient leurs choix, la formation continue devra s’adapter en conséquence. Nous y serons prêts !
Nous remercions Olivier Dameron, Guillaume Devailly et Yoann Mouscaz pour leurs premiers retours (très constructifs) sur ce texte. Certaines de leurs remarques ont été prises en compte, les autres sont évoquées en note de bas de page (pour ne pas perdre une miette de réflexions sur le sujet :).
†
Le principe était de mettre en application une méthode de lissage d’une signal par une stratégie de moyenne mobile.
‡
Sur ce point, nos relecteurs nous indiquent ne pas comprendre les limites de R dans le traitement des fichiers non structurés. Je (Gaëlle) leurs réponds que les limites ne viennent pas de R, mais de moi (!). En clair, je n'ai jamais pris le temps de bien regarder les solutions existantes. Merci de me donner l'opportunité de cette clarification !
§
Je décide de garder ma source de l’époque anonyme
¶
Nos relecteurs nous font remarquer que la question "s'il fallait n'en choisir qu'un" pourrait être modifier en "par lequel commencer ?". En effet, un élément important à prendre en compte est qu'une fois qu'un premier langage de programmation est maitrisé, le coût d'apprentissage d'un deuxième est fortement diminué (un peu comme l'apprentissage des langues étrangères - enfin, il paraît !).
#
Nos relecteurs nous font remarquer qu'apprendre un langage de programmation, c'est à la fois en apprendre une syntaxe et des principes. Dans ce contexte, Python et R font partie de la même famille des langages de scripts en programmation impérative. Les principes généraux sont globalement les mêmes et c'est une des raisons pour lesquelles finalement, leurs usages convergent...
**
Sur ce point une ressource intéressante nous a été pointée par nos relecteurs : https://insights.stackoverflow.com/survey/2020#most-popular-technologies.
††
Cette affirmation a fait bondir un de nos relecteurs. Je (Gaëlle) reconnais être sans doute sous l'influence de Pierre sur ce point ! Pierre est beaucoup plus avancé et au courant des innovations en Python que je ne le suis avec R (souvenez-vous que j'aime seulement "un peu" l'informatique). Nous aurions une grande curiosité à lire une réponse documentée sur ce sujet. A vos claviers donc les "vrais" codeurs en R
"Quelle proportion de SNPs se situent dans des exons ?"
"Y a-t-il des pics dans ces données de ChIP-seq ?"
"Quelle proportion de promoteurs chevauchent des îlots CpG ?"
Voilà le genre de questions rencontrées fréquemment en bioinformatique. Nous pouvons y répondre à l'aide de la manipulation d'intervalles. Un intervalle est défini par ses positions de début et de fin. Nous allons voir comment travailler avec ce genre de données en utilisant des packages de la suite Bioconductor pour R. Il existe l'équivalent dans d'autres langages, notamment bedtools et PyRanges.
Au niveau le plus élémentaire, un intervalle est ainsi constitué de deux nombres, indiquant son début et sa fin. Le package IRanges permet par exemple de manipuler ce type de données très basiques.
Nous ne travaillons cependant pas avec des données dénuées de contexte. Il est en effet important de savoir sur quel chromosome et sur quel brin on se situe, quelle est la taille du génome, ... Le package GenomicRanges, que nous étudierons dans le second article de cette série, présente ainsi tout son intérêt, en permettant de rajouter ces informations.
Le package plyranges, qui clôturera cette série, permet quant à lui de manipuler des intervalles à l'aide de la syntaxe dplyr (noms de fonctions simples et enchaînement d'opérations à l'aide de pipes).
Installer et charger le package IRanges
Dans un premier temps, nous allons installer et charger le package dans R:
if (!require("BiocManager"))
install.packages("BiocManager")
BiocManager::install("IRanges")
library(IRanges)
Créer un objet IRanges
Pour créer un objet IRanges, au moins deux éléments sur les trois suivants sont nécessaires :
la position de début des intervalles
la position de fin des intervalles
la largeur des intervalles
La fonction IRanges() permet la création de ce type d'objet, en indiquant le début et la fin des intervalles. Nous aurions également pu créer le même objet en indiquant le début et la largeur des intervalles.
Nous pouvons en extraire le contenu à l'aide des fonctions suivantes:
# extraire les positions de début des intervalles
start(ir)
# extraire les positions de fin des intervalles
end(ir)
# extraire les largeurs des intervalles
width(ir)
# extraire les deux premières lignes
ir[1:2]
# extraire les intervalles d'au moins 7 unités
ir[width(ir) >= 7]
Manipulations de base
Nous allons maintenant passer en revue les principales fonctions de manipulation d'intervalles du package.
# ordonner les intervalles de gauche à droite et fusionner les intervalles
# chevauchants ou adjacents
reduce(ir)
# modifier la taille des intervalles à la largeur souhaitée
# possibilité d'ancrer l'intervalle (fix = "start", "end", ou "center")
# par défaut, fix = "start"
resize(ir, width = 2, fix = "start")
# extraire les régions non couvertes par les intervalles
# ne prend pas en compte les régions non couvertes par les intervalles
gaps(ir)
# décaler les intervalles de n unités dans un sens ou dans l'autre
shift(ir, -3)
Voici une illustration des résultats obtenus à l'aide des commandes ci-dessus:
Attention : gaps() est exclusif et ne contient donc pas les frontières
Calcul de profondeur
Le calcul de profondeur fait partie des opérations les plus courantes. Il s'agit de savoir, pour chaque position de l'espace défini par l'ensemble des intervalles, combien d'intervalles couvrent cette position.
coverage(ir)
Cette commande renvoie un objet de classe Rle (Run-length encoding, Codage par plages en français), qui permet la compression de données sans perte. Plutôt que d'avoir par exemple une chaîne de caractères "abbcccddddeeeee", ce codage indique combien de fois une donnée est répétée. Notre chaîne de caractères devient ainsi "a1b2c3d4e5". Ce type de codage est donc particulièrement intéressant pour des données contenant une grande quantité de valeurs répétées.
Le résultat se lit de la manière suivante : les 3 premières positions ne sont pas couvertes, puis la 4e position est couverte 1 fois, ..., l'avant-dernière position est couverte 2 fois et enfin la dernière position est couverte 1 fois.
Comme mentionné dans l'introduction, IRanges atteint ses limites dès lors que l'on travaille avec des intervalles génomiques, pour lesquels le package GenomicRanges est bien plus adapté. Ce sera l'objet de notre prochain article !
Métadonnées
Notre objet ir se compose de 7 intervalles et 0 colonne de métadonnées. Ces métadonnées peuvent être des noms de gènes, des scores, des pourcentages de GC, ...
Voyons comment ajouter ce genre de données à notre objet.
# Ajouter des noms de gènes
mcols(ir)$gene <- c(paste0("gene", LETTERS[1:7]))
# Ajouter des scores
mcols(ir)$score <- seq(from = 0, to = 10, length = 7)
Nous pouvons accéder à ces différents éléments de la manière suivante :
Conclusion
Dans les prochains articles, à paraître prochainement, nous verrons comment utiliser ces outils de façon plus concrète, en particulier pour la manipulation et les opérations sur des intervalles génomiques. N’hésitez pas à poser vos questions en commentaires, j’essayerai d’y répondre dans ces prochains articles ou directement dans le fil des commentaires
Un grand merci à Camille, Aurélien, azerin, Guillaume et Yoann pour leurs commentaires, conseils et autres remarques constructives dans le cadre ce cette première contribution au blog !
Les logos des packages ont été téléchargés sur BiocStickers
Nous avons abordé, dans le précédent article de cette série, les bases de la manipulation d'intervalles dans R.
Ce deuxième article a pour objectif de montrer comment manipuler des intervalles génomiques. Au niveau le plus basique, un intervalle est défini par deux nombres entiers positifs délimitant son début et sa fin. Nous allons pouvoir ajouter des couches supplémentaires d'informations, telles que les chromosomes et leurs tailles ainsi que le brin.
Cette bibliothèque est en quelque sorte l'équivalent des fameux bedtools, très utilisés pour les analyses en génomique.
Installer et charger la bibliothèque GenomicRanges
Commençons par installer et charger la bibliothèque.
# Installer BiocManager
if (!require("BiocManager", quietly = TRUE))
install.packages("BiocManager")
# Installer la bibliothèque GenomicRanges
BiocManager::install("GenomicRanges")
# Charger la bibliothèque GenomicRanges
library(GenomicRanges)
Créer un objet GRanges
Coordonnées des intervalles
Nous allons créer un objet contenant des intervalles, ainsi que des informations sur les chromosomes et les brins de chromosomes qui portent ces intervalles. Un objet granges est l'équivalent d'un fichier au format .bed (Browser Extensible Data) contenant au minimum trois colonnes : le nom de la séquence, un intervalle (un objet qui comprend une position de début et une position de fin) et le brin (positif "+" ou négatif "-").
Dans le code ci-dessus, la fonction c() permet de combiner des éléments dans un vecteur (ou une colonne). La fonction rep(x, n) permet de répéter 'n' fois l'objet x. Ainsi, avec le code seqnames = c(rep("chr1", 4), rep("chr2", 4)), nous assignons les quatre premiers intervalles au chromosome 1 (chr1), et les quatre intervalles suivants au chromosome 2 (chr2).
On indique aussi que la moitié de nos intervalles sont sur le brin '+' et le reste sur le brin '-'. Si le brin n'a pas d'importance, on utilise alors strand = "*" lors de la création de l'objet granges. L'argument ignore.strand = TRUE permet d'ignorer le brin dans la plupart des fonctions de la bibliothèque GenomicRanges.
Voyons à quoi ressemble notre nouvel objet :
Métadonnées
Notre objet gr ne contient pour le moment que des informations nécessaire pour la création des intervalles ("GRanges object with 8 ranges and 0 metadata columns"). Nous pouvons ajouter des informations supplémentaires comme des noms de gènes, le taux en GC, etc. à l'aide du paramètre mcols().
Pour l'instant, les intervalles de notre objet sont situés sur des brins de chromosomes, mais nous n'avons pas précisé leurs tailles, ni le génome ("2 sequences from an unspecified genome; no seqlengths"). Sans ces informations, nous ne pouvons faire des opérations que sur les régions de chromosomes couvertes par ces intervalles.
La commande seqinfo() permet d'afficher et modifier les informations liées aux séquences :
Les informations concernant la taille des chromosomes de l'espèce ou du génome de référence peuvent être trouvées sur le site de l'UCSC ou celui d'Ensembl. La bibliothèque BSgenomes, permet aussi de récupérer ces informations pour certains génomes.
Nous pouvons aussi ajouter ces informations de la façon suivante :
# Longueurs des chromosomes
seqlengths(gr) <- c("chr1" = 10, "chr2" = 10)
# Chromosomes circulaires ou non
isCircular(gr) <- c(FALSE, FALSE)
# Nom du génome
genome(gr) <- "mon.genome"
Affichons à nouveau les informations à l'aide de seqinfo() :
Extraire des données
Nous pouvons accéder aux informations à l'aide des commandes de base suivantes :
# extraire les chromosomes
seqnames(gr)
# extraire les intervalles génomiques
granges(gr)
# extraire les brins
strand(gr)
Nous pouvons également utiliser des opérateurs logiques :
# Extraire les intervalles sur le chr1
gr[seqnames(gr) == "chr1", ]
# Extraire les intervalles sur les brins positifs
gr[strand(gr) == "+", ]
# Extraire les intervalles sur le brin négatif du chr2
gr[seqnames(gr) == "chr2" & strand(gr) == "-", ]
# Extraire l'intervalle n°3
gr[mcols(gr)$id == "interval3"]
Opérations sur des intervalles génomiques
Un seul objet granges
Nous pouvons effectuer des opérations sur notre objet :
reduce(gr) : fusionner les intervalles chevauchants ou adjacents
gaps(gr) : extraire les positions non couvertes par des intervalles
coverage(gr) : calculer la couverture (nombre de fois que chaque position est couverte par un intervalle)
Résultats des fonctions détaillées ci-dessus
D'autres fonctions existent :
flank(gr, width = n, start = TRUE) : extraire une région de longueur 'n' en amont. Pour extraire une région de longueur 'n' en aval, utiliser start = FALSE.
shift(gr, shift = n) : déplacer les intervalle de 'n' positions vers la droite (ne tient pas compte du brin). Pour décaler les intervalles vers la gauche, utiliser shift = -n.
resize(gr, width = n, fix = "start") : modifier la taille des intervalles de longueur 'n', en déplaçant la fin de l'intervalle (fix = "start") ou le début de l'intervalle (fix = "end").
promoters(gr, upstream = x, downstream = y) : extraire les régions promotrices, entre x positions en amont (par défaut 2000) et y positions en aval (par défaut 200)
Plusieurs objets granges
Après avoir vu différentes opérations sur un seul objet granges, voyons comment comparer plusieurs objets.
Commençons par créer deux objets : gr1 et gr2.
# gr1 : intervalles du chr1 sur le brin positif
gr1 <- gr[seqnames(gr) == "chr1" & strand(gr) == "+", ]
# créer l'objet gr2
gr2 <- GRanges(
seqnames = "chr1",
ranges = IRanges(start = c(2, 6),
end = c(3, 10)),
strand = "+")
Les fonctions suivantes permettent d'extraire l'union et l'intersection de deux objets granges :
union(gr1, gr2) : union des deux objets.
intersect(gr1, gr2) : intersection des deux objets.
Nous pouvons également rechercher et compter les régions où les intervalles se recouvrent :
findOverlaps(gr1, gr2) : détecter les régions où les intervalles se recouvrent. Cette fonction retourne un objet de type Hits qui indique pour chaque intervalle du premier objet (query) les intervalles du deuxième objet qui le recouvrent (subject).
countOverlaps(gr1, gr2) pour chaque intervalle du premier objet, indique le nombre d'intervalles du deuxième objet qui le recouvrent.
Pour finir, voyons une dernière fonction qui permet de calculer la distance entre deux objets. Créons tout d'abord deux nouveaux objets :
gr3 <- gr2[1, ]
gr4 <- gr1[2, ]
distanceToNearest(gr3, gr4) : calcule la distance entre chaque intervalle du premier objet et les intervalles du deuxième objet.
Résultats des fonctions détaillées ci-dessus
Importer et exporter des fichiers
La bibliothèque rtracklayer permet d'importer et d'exporter des données sous forme d'objet granges.
Attention ! Si vous souhaitez que la colonne de métadonnées soit exportée, elle doit porter l'en-tête name. En effet, les colonnes nommées name et score seront exportées. Pensez bien à vérifier que c'est le cas avant d'exporter votre objet !
# Installer la bibliothèque GenomicRanges
BiocManager::install("GenomicRanges")
# Charger la bibliothèque
library(rtracklayer)
# Ajouter une colonne "name"
mcols(gr)$name <- mcols(gr)$id
# Exporter l'objet granges au format bed
export(gr, "gr.bed")
# Importer un fichier bed
fichier_bed <- rtracklayer::import("gr.bed")
Conclusion
Nous avons vu, dans les deux premiers articles de cette série, comment créer et manipuler des intervalles génomiques avec R, à l'aide des bibliothèques IRanges et GenomicRanges.
Dans le prochain article, nous verrons comment effectuer le même genre d'opérations tout en utilisant une syntaxe "tidyverse" à l'aide de la bibliothèque plyranges.
L'analyse de séquences est au cœur de nombreux domaines de la bio-informatique. Le billet du jour s'intéressera aux séquences ADN, en se proposant de compter la fréquence en dinucléotides dans quelques génomes d'organismes modèles (avec une petite arrière-pensée derrière la tête).
Qu'est-ce qu'un dinucléotide ?
L'ADN double brins est classiquement structuré sous forme de double hélice, avec deux brins de direction opposée. À chaque position il y a deux acides nucléiques formant une paire, les appariements classiques étant A en face de T et C en face de G. Ce qu'on appelle dinucléotide est une séquence de deux nucléotides successifs, à ne pas confondre avec une paire de nucléotide. Un petit schéma sera peut-être plus clair :
Figure 1: Séquence double brin GATTACA. En bleu : un dinucléotide. En rouge : une paire de base.
On peut aussi dire qu'un dinucléotide est un k-mer de longueur (k) égale à 2.
Qu'est-ce que la fréquence en dinucléotide d'une séquence ?
Prenons par exemple cette séquence :
Figure 2: Séquence TGTG
Quels dinucléotides dénombrez vous ? Je vous laisse deux phrases pour réfléchir. C'est bon pour vous ?
On trouve deux fois le nucléotide TG (facile), et une fois le nucléotide GT (au milieu, peut-être l'avez-vous oublié ?). Donc pour une séquence de longueur n, il y aura n-1 dinucléotides. Ce qui donne une fréquence de 2/3 pour le dinucléotide TG et une fréquence de 1/3 pour le dinucléotide GT.
Mais est-ce bien tout ?
Et bien ça dépend. Si c'est un ADN simple brin, oui. Mais s'il s'agit d'un ADN double brin, surprise! il y a d'autres dinucléotides cachés en dessous !
Figure 3: Il y a un ACAC caché sous le TGTG
Sous la séquence du haut, on trouvera donc deux dinucléotides CA et un dinucléotide AC (les dinucléotides sont lus et écrits dans le sens 5' vers 3' par convention). Une séquence double brin de longueur n aura donc 2n-2 dinucléotides.
Cela nous donnerait donc une fréquence de 2/6 pour les dinucléotides TG et CA, et un fréquence de 1/6 pour les dinucléotides GT et CA.
Pour une petite séquence, compter à la main c'est faisable, mais dès que la séquence s’agrandit, autant le faire en utilisant la *bio-informatique* !
Calcul bio-informatique de la fréquence en dinucléotides avec R / Bioconductor
Je vous propose d'utiliser des fonctions existantes du package R / Bioconductor Biostrings. La séquence nucléique doit être présentée sous la forme d'un objet DNAString qui se crée via la fonction DNAString(). On peut ensuite appliquer la fonction dinucleotideFrequency() avec ou sans le paramètre as.prob = TRUE, en fonction de si l'on souhaite des comptages ou des fréquences.
dinucleotideFrequency(DNAString("GTGT"))
#> AA AC AG AT CA CC CG CT GA GC GG GT TA TC TG TT
#> 0 0 0 0 0 0 0 0 0 0 0 2 0 0 1 0
print(dinucleotideFrequency(DNAString("GTGT"), as.prob = TRUE), digits = 3)
#> AA AC AG AT CA CC CG CT GA GC GG GT TA TC TG TT
#> 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.667 0.000 0.000 0.333 0.000
La fonction dinucleotideFrequency() ne calcule que la fréquence du brin rentré, et non la fréquence du brin opposé. Pour la calculer, deux méthodes.
1) La méthode peu efficace mais simple : calculer le contenu en dinucléotide du complément inverse de la séquence et faire la moyenne des deux fréquences :
my_seq <- DNAString("GTGT")
(dinucleotideFrequency(my_seq, as.prob = TRUE) +
dinucleotideFrequency(reverseComplement(my_seq), as.prob = TRUE)) / 2
#> AA AC AG AT CA CC CG CT GA GC GG GT TA TC TG TT
#> 0.000 0.333 0.000 0.000 0.167 0.000 0.000 0.000 0.000 0.000 0.000 0.333 0.000 0.000 0.167 0.000
2) La méthode efficace, mais demandant de coder un peu plus : réassigner les fréquences du brin + aux dinucléotides complémentaires inverses, ce qui évite de tout recalculer.
Voici une petite fonction pour calculer la fréquence en dinucléotide d'une séquence ADN double brins. J'en profite pour faire un format de sortie sous forme de tableau tidy pour nous simplifier la vie un peu plus tard.
dsdinfreq <- function(dnastring, ...) {
top_strand_freq <- dinucleotideFrequency(dnastring, ...)
bottom_strand_freq <- top_strand_freq[c("TT", "GT", "CT", "AT", "TG", "GG", "CG", "AG", "TC", "GC", "CC", "AC", "TA", "GA", "CA", "AA")]
freq <- (top_strand_freq + bottom_strand_freq) / 2
data.frame(
dinucleotide = names(freq),
freq = freq
)
}
dsdinfreq(DNAString("GTGT"), as.prob = TRUE)
#> dinucleotide freq
#> AA AA 0.0000000
#> AC AC 0.3333333
#> AG AG 0.0000000
#> AT AT 0.0000000
#> CA CA 0.1666667
#> CC CC 0.0000000
#> CG CG 0.0000000
#> CT CT 0.0000000
#> GA GA 0.0000000
#> GC GC 0.0000000
#> GG GG 0.0000000
#> GT GT 0.3333333
#> TA TA 0.0000000
#> TC TC 0.0000000
#> TG TG 0.1666667
#> TT TT 0.0000000
Fréquence en dinucléotides du génome de référence de la drosophile
Maintenant que nous savons calculer à la fois pratiquement et théoriquement la fréquence en dinucléotides d'une séquence, attaquons nous à un organisme modèle, au hasard le génome de la dropsophile.
Le génome de la plupart des espèces modèles est déjà disponible sous forme d'objets BSgenome dans Bioconductor, mais pour être plus générique ici je fais un import directement depuis le fichier fasta du génome de référence Ensembl.
On tombe là sur un premier os. Il n'y a pas une séquence dans le génome de la drosophile, mais 1870 séquences ! Cinq autosomes, 2 chromosomes sexuels, un chromosome mitochondrial, et tout un tas de contigs non assemblés. Il nous faut donc modifier légèrement notre fonction d'origine pour qu'elle fonctionne bien avec ce genre d'objets (des DNAStringSet et non de simple DNAString) :
dsdinfreq <- function(dnastring) {
top_strand_freq <- dinucleotideFrequency(dnastring) |> colSums()
bottom_strand_freq <- top_strand_freq[c("TT", "GT", "CT", "AT", "TG", "GG", "CG", "AG", "TC", "GC", "CC", "AC", "TA", "GA", "CA", "AA")]
freq <- proportions(top_strand_freq + bottom_strand_freq)
data.frame(
dinucleotide = names(freq),
freq = freq
)
}
dsdinfreq(droso_genome)
#> dinucleotide freq
#> AA AA 0.10168661
#> AC AC 0.05262196
#> AG AG 0.05440317
#> AT AT 0.08125731
#> CA CA 0.06794174
#> CC CC 0.04634818
#> CG CG 0.04133863
#> CT CT 0.05440317
#> GA GA 0.05570537
#> GC GC 0.05535490
#> GG GG 0.04634818
#> GT GT 0.05262196
#> TA TA 0.06463509
#> TC TC 0.05570537
#> TG TG 0.06794174
#> TT TT 0.10168661
Figure 4 : Fréquences dinucléotidiques du génome de référence de la Drosophile
Comme on peut le voir, tout les dinucléotides n'ont pas la même fréquence. Mais est-ce un résultat attendu ? Ou pour reformuler la question, quel est la fréquence attendue pour chaque dinucléotide ?
Fréquence observée, fréquence théorique
Comme il y a 16 dinucléotides possibles, on pourrait s'attendre à ce que chaque dinucléotide soit observé avec une fréquence de 1/16 (soit 6,25%). Ce serait le cas si les nucléotides simples avaient une fréquence d'un quart. Hors ce n'est pas le cas en pratique ! S'il y a bien autant de A que de T et autant de C que de G, le contenu en G+C d'un génome peut varier énormément d'une espèce à l'autre (C'est d'ailleurs utilisé pour vérifier les puretés d'assemblages, voir par exemple les BlobPlots) !
On peut donc calculer de manière un peu plus fine la fréquence théorique d'un dinucléotide (TA par exemple, noté p(TA) ) comme la probabilité d'avoir un T en premier ( p(T) ) multiplié par la probabilité d'avoir un A en second ( p(A) ), où p(T) et p(A) correspondent à la fréquence en nucléotide A et T de la séquence étudiée :
p(AT) = p(A) x p(T)
Ou de manière plus générique :
p(N1N2) = p(N1) x p(N2)
On peut utiliser la fonction alphabetFrequency() de Biostrings pour obtenir les fréquences de chaque nucléotide, et ensuite il s'agit de tout faire fonctionner ensemble. Par exemple, et sans détailler :
double_strand_dinucleotide_frequency(droso_genome)
# A tibble: 32 × 6
#> dinucleotide nucleotide_1 nucleotide_2 type n freq
#> <chr> <chr> <chr> <chr> <dbl> <dbl>
#> 1 AA A A observed 28995037 0.102
#> 2 AC A C observed 15004688 0.0526
#> 3 AG A G observed 15512583 0.0544
#> 4 AT A T observed 23169804 0.0813
#> 5 CA C A observed 19372988 0.0679
#> 6 CC C C observed 13215774 0.0463
#> 7 CG C G observed 11787346 0.0413
#> 8 CT C T observed 15512583 0.0544
#> 9 GA G A observed 15883894 0.0557
#>10 GC G C observed 15783960 0.0554
# … with 22 more rows
Figure 5 : Fréquences dinucléotidiques observées et attendues du génome de référence de la Drosophile
Il y a donc plus de AA et TT qu'attendu dans le génome de la drosophile ! On pourrait tester statistiquement cette différence, avec par exemple un test du chi2, mais il y a tellement de nucléotides que la p-value retournée est scandaleusement petite.
Fréquence en dinucléotides du génome de plusieurs espèces modèles
Il ne reste plus qu'à faire tourner ces fonctions sur les génomes de quelques espèces modèles populaires (en étant raisonnable avec l'API d'Ensembl lors de la récupération des génomes de références), et voilà :
Figure 6 : Fréquences en dinucléotides des génomes d’espèces modèles en biologie.
Les plus observateurs et observatrices d’entre vous remarqueront peut-être la disparition des dinucléotides CG dans les génomes des vertébrés. Ce mystère pourra faire l'objet d'un prochain billet si la demande se fait pressante.
Remerciements
Mille mercis à mes relecteurs et relectrices Sébastien Gradit, Sarah Guinchard, Virginie J, et à nos admins chéries !
Si vous analysez des données d'épigénomique telles que de l'ATAC-seq ou des ChIP-seq, vous souhaitez sûrement pouvoir représenter des exemples de pics sous forme de genomic tracks comme on en voit souvent dans les publications.
Lorsque l'on inspecte ses données de coverage (ou couverture), on charge généralement le fichier Bam ou le fichier BigWig dans notre navigateur de génome préféré (pour ma part IGV, mais aussi IGB ou UCSC). Après quelques paramétrages, on peut visualiser et comparer plusieurs échantillons ensemble comme dans l'exemple ci-dessous :
Screenshot d'IGV avec des données d'épigénomique dont les réplicats ont été supperposés ("Overlay").
IGV, c'est super pratique pour inspecter ses données, se balader le long du génome et regarder ses signaux, mais c'est moins pratique si l'on veut en faire une figure pour un papier. Il y a bien la possibilité de sauvegarder sa vue en PNG ou en SVG, mais ensuite il faut retoucher le résultat pour pouvoir le monter en figure qualité publication, et ça quand on est bioinformaticien, on aime pas trop…
J'ai longtemps cherché un moyen d'automatiser la génération de genomic tracks pas trop moches avec R. Il existe bien des paquets pour faire cela, mais chacun a ses propres limitations et/ou son esthétique bien à lui. Je ne vais pas vous en faire une liste exhaustive, mais sachez que malgré mes recherches, je n'ai toujours pas trouvé l'outil parfait qui répond à tous mes besoins sans devoir faire des compromis.
Voici une liste des paquets que j'ai regardé (sans forcément les tester) :
Vous l'aurez compris, mon choix s'est porté sur ce dernier, Gviz, pour construire mes figures, et je vais vous partager mes paramètres pour arriver à ce résultat :
Tout d'abord, on va commencer par préparer la track d'annotation du génome de référence, celle où l'on voit les gènes.
Pour cela, vous pouvez soit utiliser les génomes déjà tous prêts présent dans bioConductor, ou bien charger le même génome que vous avez utilisé pour votre analyse (option que je préfère, pour une question d'exactitude de la présentation de résultats).
Comme je cherche à vous partager les moments où j'ai eu des difficultés afin de vous simplifier la vie (et aussi pour mon moi du futur), je vais vous montrer un cas un peu compliqué avec un GTF de chez Gencode.
Pour dessiner la track du gène Tram1 chez la souris (mm10), voici le code minimal par défaut :
# Load libraries
library("GenomicFeatures")
library("Gviz")
# Path to the GTF file
my_genome <- "path/to/mouse/gencode.vM25.annotation.gtf.gz"
# Tram1 gene coordinates
my_gene <- GRanges(
seqnames="chr1",
IRanges(
start=13552693,
end=13601910
)
)
# Gviz gene track
geneTrack <- GeneRegionTrack(
my_genome,
name = "Genes"
)
# Plot the region containing the Tram1 gene
plotTracks(
geneTrack,
from = start(my_gene),
to = end(my_gene),
chromosome = as.character(seqnames(my_gene))
)
Voici ce que l'on obtient :
Région du gène Tram1 à partir d'un fichier GTF Gencode
Pas très probant… Le gène est représenté par de gros rectangles, alors qu'on s'attend à voir des exons et des introns.
Les fichiers GTF de chez Gencode contiennent des entrées pour les gènes (et pas que exon et CDS par exemple), ce qui génère de gros rectangles qui masquent la structure exon/intron des gènes.
Voici une des lignes en question dans mon fichier GTF :
Pour s'en débarrasser, l'astuce consiste à charger le génome comme un objet TxDb, lisible par Gviz. Cela va enlever les gros rectangles qui masquent la structure des gènes :
# Load GTF file as a TxDb object
TxDb <- GenomicFeatures::makeTxDbFromGFF(my_genome)
# Gviz gene track
geneTrack <- GeneRegionTrack(
TxDb,
name = "Genes"
)
# Plot the region containing the Tram1 gene
plotTracks(
geneTrack,
from = start(my_gene),
to = end(my_gene),
chromosome = as.character(seqnames(my_gene))
)
Région du gène Tram1 à partir d'un fichier GTF Gencode transformé en TxDb
C'est beaucoup mieux, mais il manque encore quelque chose, le nom des gènes ! En temps normal, voici à quoi devrait ressembler le code pour afficher les noms des gènes :
# Gviz gene track
geneTrack <- GeneRegionTrack(
myGTF,
# Show gene names
transcriptAnnotation="symbol",
name = "Genes"
)
Mais voilà ce que ça donne avec un objet TxDb, on se retrouve avec les ID Ensembl à la place des noms.
Région du gène Tram1 avec les ID des genes à la place des noms des gènes
Là encore, il va falloir avoir recours à une astuce. Une fois transformé en TxDb, nous avons perdu l'information du nom des gènes car les symboles ne font pas parti du format TxDb. Il va alors falloir aller les récupérer depuis le fichier GTF :
# Load the GTF file with rtracklayer
genome_gtf <- rtracklayer::import(my_genome)
# Extract gene_id and gene_symbol and remove duplicates
gene2symbol <- unique(mcols(genome_gtf)[,c("gene_id","gene_name")])
# Define gene_id as rownames
rownames(gene2symbol) <- gene2symbol$gene_id
Nous obtenons alors une table de correspondance des ID des gènes et de leurs noms respectifs:
Pour afficher les noms des gènes sur la track, il va falloir remplacer les ID par les symboles, comme ceci:
geneTrack <- GeneRegionTrack(
TxDb,
# Show gene names
transcriptAnnotation="symbol",
name = "Genes"
)
ranges(geneTrack)$symbol <- gene2symbol[ranges(geneTrack)$gene, "gene_name"]
Région du gène Tram1 avec les noms des gènes
Enfin, ici nous voyons les transcrits alternatifs des gènes. Ce n'est pas trop gênant quand il y en a peu, mais ça le devient quand il y en a vraiment plein. Nous allons alors réduire l'affichage pour ne montrer qu'un seul modèle de gène avec tous les exons alternatifs :
# Gviz gene track
geneTrack <- GeneRegionTrack(
TxDb,
# Show gene names
transcriptAnnotation="symbol",
# Collapse all alternative transcripts
collapseTranscripts = "meta",
name = "Genes"
)
ranges(geneTrack)$symbol <- gene2symbol[ranges(geneTrack)$gene, "gene_name"]
Région du gène Tram1 dont les transcrits alternatifs sont réduit en un modèle de gène
Vous noterez au passage que l'on a perdu les boîtes plus fines qui montrent les UTR des gènes. Comme il existe plusieurs UTR possibles, Gviz fait le choix de ne pas les distinguer et de dessiner les UTR aussi épais que des exons.
Travaillons maintenant un peu l'esthétique de la track, parce que le diable se cache dans les détails. Premièrement, je n'aime pas le jaune pour les boîtes des gènes, je n'aime pas non plus qu'il y ait un contours autours des boîtes, parce qu'ils font apparaître les exons plus gros qu'ils ne le sont réellement, je n'aime pas le fond grisé du titre de la track qui manque cruellement de contraste, et enfin chez la souris les gènes doivent être notés en italique. Voilà à quoi ressemble ma version "pimpée" :
# Gviz gene track
geneTrack <- GeneRegionTrack(
TxDb,
# Collapse all alternative transcripts
collapseTranscripts = "meta",
# Print gene symbols
transcriptAnnotation="symbol",
# Name of the track
name = "Genes",
# Gene name in italic
fontface.group="italic",
# Remove borders around exons
col = 0,
# Color of the exons
fill = "#585858",
# Apply the exon color to the thin line in introns
col.line = NULL,
# Color of the gene names
fontcolor.group= "#333333",
# Font size
fontsize.group=18
)
ranges(geneTrack)$symbol <- gene2symbol[ranges(geneTrack)$gene, "gene_name"]
# Plot the region containing the Tram1 gene
plotTracks(
geneTrack,
from = start(my_gene),
to = end(my_gene),
chromosome = as.character(seqnames(my_gene)),
# Remove the grey background and the white borders of the track name
background.title = "transparent",
col.border.title="transparent",
# Track name color
col.title = "#333333"
)
Région du gène Tram1 avec mes paramètres esthétiques
Le résultat est plus sobre, et c'est ce que l'on veut pour une figure de papier, parce que l'on veut que l'attention du lecteur se fixe sur les données que l'on va présenter avec.
Les fichiers Bed
Les analyses d'ATAC ou ChIP-seq consistent à appeler des pics, c'est à dire à détecter les signaux enrichis d'ouverture de la chromatine ou de site de fixation de protéines sur l'ADN. Le résultats de cette analyse se concrétise en un ficher Bed contenant les coordonnées des pics. Nous pouvons représenter ces régions génomique autours de nos gènes préféré dans Gviz comme ceci :
my_peaks <- rtracklayer::import("example.bed")
# Get peaks in the Tram1 gene area
my_peak_locus <- subsetByOverlaps(my_peaks, my_gene)
peak_track <- AnnotationTrack(
my_peak_locus,
fill = "#5f4780",
col.line = NULL,
col = 0,
name = "Peaks"
)
# Plot the region containing the Tram1 gene
plotTracks(
# Plot peak track on top of the gene track
c(peak_track, geneTrack),
from = start(my_gene),
to = end(my_gene),
chromosome = as.character(seqnames(my_gene)),
# Remove the grey background and the white borders of the track name
background.title = "transparent",
col.border.title="transparent",
# Track name color
col.title = "#333333"
)
Pics dans la région autours du gène Tram1
Les bigWig
Charger les bigWig dans R
Les fichiers bigWig sont souvent lourds (autours de 100 Mb, voire beaucoup plus selon le type d'expérience), alors charger ces fichiers en entier dans R peut vite devenir limitant en terme de RAM. Pour ne pas exploser les ressources de votre machine, il est possible de n'importer que les données d'un locus précis grâce à rtracklayer. Voyons comment charger de façon relativement automatisée des fichiers bigWig.
Dans un premier temps, on va lister les fichiers à notre disposition qui sont rangés ensemble dans un seul dossier. Les fichiers bigWig sont tous nommés comme suit: "Condition_Replicat.bw" afin de faciliter l'extraction des informations depuis leur nom (c'est une habitude que j'ai prise, vous faites peut-être différemment) :
# path to the bigWig files
bw_folder <- "path/to/my_bg"
# List all the bigWig files that have a ".bw" extention
bw_files <- list.files(path=bw_folder, pattern = ".bw")
Nous allons ensuite extraire les conditions des noms de fichiers, définir une couleur pour chacune d'entre elles, et importer les données relative à notre gène d'intérêt, ici Tram1 :
# Extract the condition from the bw file name , i.e. the string before the first "_"
conditions <- sapply(strsplit(bw_files, "_"), `[`, 1)
colors <- c("#fdb049", "#94d574")
names(colors) <- unique(conditions)
# Import the signal around the gene of interest from the bigwig files
import_bw_file <- function(folder, file, locus){
gr <- rtracklayer::import(
paste(folder, file, sep="/"),
# Specify the region you want to import as a GRanges object
which=locus
)
return(gr)
}
# For each bigWig file, make a GRanges object of the region around our gene of interest
gr_list <- lapply(bw_files, function(file){
gr <- import_bw_file(bw_folder, file, my_gene)
})
names(gr_list) <- conditions
Visualiser les bigWig avec Gviz
Nous avons maintenant une liste d'objets GRanges avec les données de couvertures autours de notre gène préféré. Nous pouvons maintenant les représenter avec Gviz :
# Generate the bigWig signal track
bigwig_tracks <- function(gr, locus, colors){
# My annotation contains non-regular chromosome names
# The following option avoids an error because of the chr names
options(ucscChromosomeNames=FALSE)
# Get the condition of the sample
condition <- unique(gr$condition)
# Get the corresponding color
color <- colors[condition]
# Prepare the track
gTrack <- DataTrack(
# Granges object
range = gr,
# Type of graph: histogram
type = "hist",
# No borders around histogram bars
col.histogram=0,
# Histogram color
fill.histogram=color,
# Draw a line at 0
baseline=0,
# Baseline color
col.baseline=color,
# Size of the baseline
lwd.baseline=1,
# Number of bins for the histogram
window=1000,
chromosome = as.character(seqnames(locus)),
name = unique(gr$condition),
# The scale starts at 0 and finish at the maximum of the bw values
ylim=c(0, trunc(max(gr$score), digit=4)),
# Show a tick only for the maximum value
yTicksAt=c(0,trunc(max(gr$score), digit=4)),
# Height of the track
size=1
)
return(gTrack)
}
bw_track_list <- lapply(gr_list, function(gr){
bw_track <- bigwig_tracks(gr, my_gene, colors)
})
# Plot the region containing the Tram1 gene
plotTracks(
# Plot bigWig tracks, with the peak and the gene track
c(bw_track_list, peak_track, geneTrack),
from = start(my_gene),
to = end(my_gene),
chromosome = as.character(seqnames(my_gene)),
# Remove the grey background and the white borders of the track name
background.title = "transparent",
col.border.title="transparent",
# Track name color
col.title = "#333333",
# Axis color
col.axis = "#333333"
)
Et voilà le résultat :
Couverture dans la région autours du gène Tram1
Superposer les réplicats
Parce que je suis toujours plus exigeante, j'aimerais superposer mes réplicats, pour n'avoir que 2 tracks à montrer, ce qui fera gagner beaucoup de place. Je vais reproduire la fonction "Overlay" de IGV, avec mes réplicats qui consiste à superposer les tracks, et je vais appliquer une légère transparence pour voir les différences entre les réplicats :
# For each condition, make a list of GRanges object for the replicates
gr_cond_list <- lapply(unique(conditions), function(cond){
# Get the file names corresponding to the current condition
cond_bw_files <- grep(cond, bw_files, value=TRUE)
# For each bigWig file, make a GRanges object of the region around our gene of interest
lapply(cond_bw_files, function(file){
condition <- sapply(strsplit(file, "_"), `[`, 1)
gr <- import_bw_file(bw_folder, file, condition, my_gene)
})
})
# The result looks like this:
# gr_cond_list:
# |-Cond1:
# |-Rep1
# |-Rep2
# |-Rep3
# |-Cond2:
# |-Rep1
# |-Rep2
# |-Rep3
# Generate the bigWig signal track
bigwig_tracks_overlay <- function(gr_list, locus, colors){
options(ucscChromosomeNames=FALSE)
track_list <- lapply(gr_list, function(gr){
condition <- unique(gr$condition)
color <- colors[condition]
gTrack <- DataTrack(
# Granges object
range = gr,
# Type of graph: histogram
type = "hist",
# No borders around histogram bars
col.histogram=0,
# Histogram color
fill.histogram=color,
# Make the histogram transparent
alpha=0.8,
# Prevent the track title to also be transparent
alpha.title = 1,
# Draw a line at 0
baseline=0,
# Baseline color
col.baseline=color,
# Size of the baseline
lwd.baseline=1,
# Number of bins for the histogram
window=1000,
chromosome = as.character(seqnames(locus)),
name = unique(gr$condition),
# The scale starts at 0 and finish at the maximum of the bw values
ylim=c(0, trunc(max(gr$score), digit=4)),
# Show a tick only for the maximum value
yTicksAt=c(0,trunc(max(gr$score), digit=4)),
# Height of the track
size=2
)
})
# Overlay the tracks from the same condition
gTrack <- OverlayTrack(track_list)
return(gTrack)
}
# Run the bigwig_tracks_overlay function
bw_track_list <- lapply(gr_cond_list, function(cond_gr){
bw_track <- bigwig_tracks_overlay(cond_gr, my_gene, colors)
})
# Plot the region containing the Tram1 gene
plotTracks(
# Plot peak track on top of the gene track
c(bw_track_list, peak_track, geneTrack),
from = start(my_gene),
to = end(my_gene),
chromosome = as.character(seqnames(my_gene)),
# Remove the grey background and the white borders of the track name
background.title = "transparent",
col.border.title="transparent",
# Track name color
col.title = "#333333",
# Axis color
col.axis = "#333333"
)
Couverture dans la région autours du gène Tram1 avec les réplicat supperposés
Normaliser les valeurs des tracks
Enfin, pour aller encore plus dans les détails, vous noterez que la hauteur des tracks est réglée sur les valeurs maximales des données de couvertures, mais que ces valeurs ne sont pas les mêmes d'une condition à l'autre. Pour faciliter la comparaison des deux conditions, on va fixer la hauteur des tracks à la valeur maximale toutes conditions confondues, pour pouvoir apprécier visuellement les différences entre les deux :
# Merge all the GRanges objects as one
max_score <- do.call("c", do.call("c", gr_cond_list))
# Get the max score value
max_score <- max(max_score$score)
On va maintenant pouvoir passer cette valeur à notre fonction bigwig_tracks_overlay :
# Generate the bigWig signal track
bigwig_tracks_overlay <- function(gr_list, locus, max_score, colors){
[...]
# The scale starts at 0 and finish at the maximum of the bw values
ylim=c(0, trunc(max_score, digit=4)),
# Show a tick only for the maximum value
yTicksAt=c(0,trunc(max_score, digit=4)),
[...]
)
})
# Overlay the tracks from the same condition
gTrack <- OverlayTrack(track_list)
return(gTrack)
}
Couverture dans la région autours du gène Tram1 avec les réplicat supperposés, cette fois avec les valeurs en Y normalisées entre les deux conditions
Mettre en évidence des régions ou des pics
On y est presque, réglons les derniers détails. Pour mettre en valeur les régions où il y a des pics, on peut dessiner des rectangles en arrière plan qui traversent les tracks afin de mieux voir leur délimitations. Pour cela, on utilise la fonction HighlightTrack qui prend une liste de positions start et end pour délimiter les régions à mettre en évidence, et qui englobe les tracks sous lesquelles on veut faire passer les rectangles :
ht <- HighlightTrack(
# The highlights will be dranw across all the tracks
trackList = c(bw_track_list, peak_track, geneTrack),
# Start position of the peaks
start = start(my_peak_locus),
# End position of the peaks
end = end(my_peak_locus),
chromosome = as.character(seqnames(my_peak_locus)),
# No borders
col = 0,
# Highlight in grey
fill = "#666666",
# Make it very transparent
alpha=0.1
)
# Plot the region containing the Tram1 gene
plotTracks(
# Plot highlight track that contains all the tracks
ht,
from = start(my_gene),
to = end(my_gene),
chromosome = as.character(seqnames(my_gene)),
# Remove the grey background and the white borders of the track name
background.title = "transparent",
col.border.title="transparent",
# Track name color
col.title = "#333333",
# Axis color
col.axis = "#333333"
)
Et voilà ce que ça donne :
Pics dans la région autours du gène Tram1 mis en évidence par des rectangles grisés
On voit maintenant bien où sont les pics par rapport aux signaux des bigWigs et aussi par rapport au gène. Notez toutefois qu'il y a une limitation à l'usage des highlights si vous exportez vos figures en PNG avec des unités autres que des pixels. Voici ce que ça donne quand on l'exporte en donnant des tailles en cm :
Export de la figure en PNG avec des unitées en cm. Les highlights ne sont plus alignées correctement…
Les rectangles gris vers la gauche ne sont plus alignés correctement avec les pics ! Je ne compred pas d'où vient ce problème, alors il faut trouver un autre moyen d'exporter ses figures. Je n'ai pas testé toutes les options, mais l’export en PDF fonctionne bien.
Limitations et conclusion
Comme vous l'avez vu, pour générer des figures de genomic tracks de bonne qualité visuelle, il faut en suer un petit peu, et c'est pour ça que j'ai voulu partager mon code avec vous, afin de vous épargner du temps et des tâtonnements. Bien que très modulable, Gviz réclame pas mal de petits bidouillages à gauche à droite pour obtenir le résultat que l'on souhaite, surtout quand on est un poil maniaque des graphiques comme moi… Je n'ai pas présenté toutes mes astuces ici mais si vous voulez par exemple avoir les titres des tracks à l'horizontale, sachez que l'option rotation.title=0 existe bien, mais n'est que partiellement implémentée et il faudra bidouiller avec la largeur de la colonne de titre et rajouter des espaces dans les noms des titres pour que ça passe…
Néanmoins, je trouve quand même que l'effort en vaut la peine, le résultat est visuellement satisfaisant, et je ne pense pas que les autres paquets R puissent faire mieux à l'heure de l'écriture de cet article.
Lorsque l'on traite des données de RNA-seq, il arrive très souvent de se retrouver avec une matrice de quantification de l'expression des gènes (un tableau avec le nombre de reads par gène) dont le nom des gènes est représenté par leur identifiant (ou ID) de chez Ensembl (ex. : "ENSG00000128573") et non par leurs symboles […]