Quantcast
Channel: Archives des R - Bioinfo-fr.net
Viewing all 33 articles
Browse latest View live

Représenter rapidement une ACP avec R et ggplot2

$
0
0

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 :

Screeplot et ACP

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...

 

Merci à Jnsll, Nico M., et Mat Blum pour leurs commentaires et relectures.

L’article Représenter rapidement une ACP avec R et ggplot2 est apparu en premier sur bioinfo-fr.net.


Les éléments répétés du génome humain : aperçu rapide avec R et le tidyverse

$
0
0

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

read.table()
  de R base :

repeats_dirty <- read_tsv("repeats_rmsk_hg38.txt.gz")

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)

Ce qui me donne cette table-ci :

head(repeats) %>%
    xtable() %>%
    print(type = "html", include.rownames = FALSE)

chr start end strand name family class
chr1 67108753 67109046 + L1P5 L1 LINE
chr1 8388315 8388618 - AluY Alu SINE
chr1 25165803 25166380 + L1MB5 L1 LINE
chr1 33554185 33554483 - AluSc Alu SINE
chr1 41942894 41943205 - AluY Alu SINE
chr1 50331336 50332274 + HAL1 L1 LINE

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.

repeats <- filter(
    repeats,
    !(grepl("?", repeats$class, fixed = TRUE) |
        grepl("?", repeats$family, fixed = TRUE) |
        grepl("?", repeats$name, fixed = TRUE)
    )
)

Les classes d'éléments répétés

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.

repeats$class <- factor(repeats$class) %>% fct_infreq %>% fct_rev

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 :

chr_length <- read_tsv("http://hgdownload-test.cse.ucsc.edu/goldenPath/hg38/bigZips/hg38.chrom.sizes", col_names = FALSE) %>%
    dplyr::rename(seqnames = X1, length = X2) %>%
    filter(seqnames %in% standard_chromosomes) %>%
    mutate(seqnames = factor(seqnames, levels = standard_chromosomes))
genome_length <- sum(as.numeric(chr_length$length)) # 3 088 269 832

Pour faciliter les prochains calculs, je rajoute une colonne contenant la largeur de chaque élément :

repeats <- mutate(repeats, width = end - start)

Et c'est parti pour un peu de magie dplyr ! Je groupe mon tableau par classe d'éléments répétés (avec

group_by()
), je calcule ensuite la longueur totale couverte par chaque classe (avec
summarise()
 ), et je lance le tout dans ggplot2 ! Je spécifie bien que je souhaite un axe secondaire, qui est une transformation linéaire de l'axe principal (
sec.axis = sec_axis(~100 * . / genome_length, name = "% du génome")
 ).

plot_genomeProp <- repeats %>%
    group_by(class) %>%
    summarise(total_length = sum(width)) %>%
    arrange(total_length) %>%
    ggplot(aes(x = class, y = total_length)) +
    geom_bar(stat = "identity", fill = "cornflowerblue") +
    geom_text(aes(label = paste0(round(100 * total_length / genome_length, digits = 1), "%")), y = 10000, hjust = 0) +
    labs(y = "Longueur cumulée (pb)") +
    scale_y_continuous(sec.axis = sec_axis(~100 * . / genome_length, name = "% du génome")) +
    coord_flip() +
    theme(axis.text.y = element_blank(), axis.title.y=element_blank()) +
    background_grid(major = "xy", minor = "none")

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 :

plot_sizeDistrib <- ggplot(repeats, aes(x = class, y = width)) +
    geom_boxplot(fill = "mediumorchid", outlier.shape = NA) +
    labs(x = "Classe", y = "Taille des éléments (pb)") +
    scale_y_continuous(sec.axis = dup_axis()) +
    coord_flip(ylim = c(0, 2500)) +
    theme(axis.text.y = element_blank(), axis.title.y=element_blank()) +
    background_grid(major = "xy", minor = "none")

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()

alt-text 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 :

arrange(repeats, desc(width)) %>%
    top_n(5) %>%
    xtable() %>%
    print(type = "html", include.rownames = FALSE)

chr start end strand name class family width
chr1 123500000 124000000 + ALR/Alpha Satellite centr 500000
chr1 123000000 123500000 + ALR/Alpha Satellite centr 500000
chr5 48000000 48500000 + ALR/Alpha Satellite centr 500000
chr7 59000000 59500000 + ALR/Alpha Satellite centr 500000
chr8 44500000 45000000 + ALR/Alpha Satellite centr 500000
chr12 35000000 35500000 + ALR/Alpha Satellite centr 500000

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 :

svglite("plots/ERparChrom.svg", width = 5, height = 3)
repeats %>%
    mutate(simple_class = if_else(
        !(class %in% c("SINE", "LINE", "LTR")),
        "other",
        as.character(class)
    )) %>%
    group_by(chr, simple_class) %>%
    summarize(cum_size = sum(width)) %>%
    left_join(chr_length, by = c("chr" = "seqnames")) %>%
    ungroup %>%
    mutate(
        frac_repeat = cum_size/length,
        simple_class = factor(simple_class, levels = c("other", "LTR","SINE", "LINE")),
        chr = factor(chr, levels = standard_chromosomes)
    ) %>%
    ggplot(aes(x = chr, y = frac_repeat, fill = simple_class)) +
    geom_bar(stat = "identity") +
    theme(axis.text.x = element_text(angle = 90, hjust = 0, vjust = 0.5)) +
    scale_y_continuous(labels = scales::percent) +
    scale_fill_manual(values = c("gray", viridis(3, begin = 0.25, end = 0.9))) +
    labs(x = NULL, y = "% du chromosome", title = "Contenu répété de chaque chromosome", fill = "Classe") +
    background_grid(major = "xy", minor = "none")
dev.off()

alt-text 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 :

group_by(repeats, class) %>%
    summarise(n_family = length(unique(family)), n_subfamily = length(unique(name)), n_element = n()) %>%
    arrange(desc(n_family), desc(n_subfamily)) %>%
    xtable() %>%
    print(type = "html", include.rownames = FALSE)

class n_family n_subfamily n_element
DNA 16 226 479941
LINE 7 171 1516226
LTR 6 567 709475
SINE 6 60 1779233
Satellite 4 22 7018
Simple_repeat 1 14162 678663
Unknown 1 71 5531
tRNA 1 62 1777
snRNA 1 12 4285
Retroposon 1 6 5397
scRNA 1 5 1334
Low_complexity 1 4 98618
rRNA 1 3 1751
RC 1 3 1754
RNA 1 1 666
srpRNA 1 1 1595

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,

coord_flip()
  et
annotation_logticks()
  sont mutuellement exclusif !

Je commence par préparer les données :

effectif_table <- group_by(repeats, class, family) %>%
    summarise(diff_name = length(unique(name)), size = n()) %>%
    ungroup()

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
~
  et
.x
 .

multiFamily <- c("Satellite", "LTR", "LINE", "SINE", "DNA")

myFamilyPlots <- map(
    multiFamily,
    ~filter(effectif_table, class == .x) %>%
        arrange(size) %>%
        mutate(family = factor(family, levels = family)) %>%
        mutate(text_dark = if_else(diff_name > 50, TRUE, FALSE)) %>%
        ggplot(aes(x = family, y = size, fill = diff_name)) +
        geom_bar(stat = "identity") +
        geom_text(aes(label = size, color = text_dark), y = 1, hjust = 0) +
        scale_fill_viridis(limits=c(0, 100),  oob = scales::squish) +
        scale_color_manual(values = c("grey80", "black"), guide = FALSE) +
        scale_y_log10() +
        coord_flip(ylim = c(10, 1e7)) +
        background_grid(major = "xy", minor = "none") +
        labs(x = NULL, y = NULL, title = .x) +
        theme(legend.position = "none")
)
names(myFamilyPlots) <- multiFamily
myFamilyPlots$DNA <- myFamilyPlots$DNA + labs(y = "Nombre d'éléments")

Générons ensuite le même type de panneau pour toutes les classes d'éléments ayant une seule famille :

plot_other <- filter(effectif_table, !(class %in% c("Satellite", "LTR", "LINE", "SINE", "DNA"))) %>%
    arrange(size) %>%
    mutate(family = factor(family, levels = family)) %>%
    ggplot(aes(x = family, y = size, fill = diff_name)) +
    geom_bar(stat = "identity") +
    geom_text(aes(label = size), y = 1, hjust = 0, stat = "count") +
    scale_fill_viridis(limits=c(0, 100),  oob = scales::squish) +
    scale_y_log10() +
    coord_flip(ylim = c(10, 1e7)) +
    background_grid(major = "xy", minor = "none") +
    labs(x = NULL, y = NULL, title = "Autres", fill = "Nombre de\nsous-familles") +
    theme(legend.position = "bottom")

Je récupère la légende pour l'afficher à part, à l'aide d'une fonction de cowplot :

myLegend <- get_legend(plot_other)
plot_other <- plot_other + theme(legend.position = "none")

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.

myFamilyPlots <- map(myFamilyPlots, ggplotGrob)
plot_other <- ggplotGrob(plot_other)

myFamilyPlots.widths <- map(myFamilyPlots, ~.x$widths[1:3])
plot_other.widths <- plot_other$widths[1:3]

max.widths <- grid::unit.pmax(
    plot_other.widths,
    do.call(grid::unit.pmax, myFamilyPlots.widths)
)

plot_other$widths[1:3] <- max.widths
myFamilyPlots <- map(myFamilyPlots, function(x) {
    x$widths[1:3] <- max.widths
    return(x)
})

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()

alt-text 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 SINE dé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.

L’article Les éléments répétés du génome humain : aperçu rapide avec R et le tidyverse est apparu en premier sur bioinfo-fr.net.

Maîtrisez le cache de Rmarkdown !

$
0
0

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 :
    ```{r setup, echo=FALSE, message=FALSE}
    library(knitr)
    opts_chunk$set(fig.align = "center",
                   fig.retina = 2,
                   fig.width = 10,
                   cache = TRUE,
                   cache.lazy = FALSE)
    ```

    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 :

library(devtools)
source_url("https://gist.github.com/achateigner/e3f905d9fc98d34c8ecee93275c80a07/raw/loadAllChunksCache.R")

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 :
    library(devtools)
    source_url("https://gist.github.com/achateigner/f7948d43f34c1d1bcd83097036338601/raw/ipak.R")
    packagesNeeded <- c("captioner", "apercu", "viridis")
    ipak(packagesNeeded)
  • 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 !

L’article Maîtrisez le cache de Rmarkdown ! est apparu en premier sur bioinfo-fr.net.

Qu'est-ce qu'un site d'initiation de la transcription ?

$
0
0

Comment je me suis posé la question.

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 :

Exemple de pile de profil epigénétiques centrés sur les sites d'initiations de la transcription.

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 eukaryotes

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 laborieuse méthode d'extension d'amorces, à faible débit.
  • 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.

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 !

Sites d'initiations de la transcription du gènes BRCA1, annotés à l'aide de gracieuses flèches oranges dedans et marron autour.

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 annotation GENCODE.

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.

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 :

mutate(
    midTranscript,
    start = if_else(strand == "+", start, end - 1L),
    end   = if_else(strand == "+", start + 1L, end)
)

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.

Remerciements

Un grand merci à mes relectrices et relecteurs : Clémence, Nisaea, Yoann M. et Gwenaelle !

Références :

[1] A promoter-level mammalian expression atlas. FANTOM Consortium and the RIKEN PMI and CLST (DGT). DOI: 10.1038/nature13182

[2] Gene-level differential analysis at transcript-level resolution. Lynn Yi, Harold Pimentel, Nicolas L. Bray, Lior Pachter. DOI: 10.1101/190199

L’article Qu'est-ce qu'un site d'initiation de la transcription ? est apparu en premier sur bioinfo-fr.net.

Rendre ses projets R plus accessibles grâce à Shiny

$
0
0

Bonjour à tous !

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).
    • Un réalisé avec la librairie ggplot2
    • Un dynamique réalisé avec plotly
    • Un dynamique réalisé avec Google

L’ensemble du code permettant de réaliser l’application est disponible sur github : https://github.com/bioinfo-fr/bioinfo-fr_Shiny

Pré-requis

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) :

  1. installer R : https://cran.r-project.org/
  2. Installer RStudio : https://www.rstudio.com/products/rstudio/download/

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 :

  • la longueur des sépales ;
  • la largeur des sépales ;
  • la longueur des pétales ;
  • la largeur des pétales ;
  • l’espèce de fleurs.

Un fichier au format txt est disponible ici :
https://github.com/bioinfo-fr/bioinfo-fr_Shiny/blob/master/datasetIris.txt .

Les packages R

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!

install.packages("anyLib")
anyLib::anyLib(c("shiny", "shinydashboard", "shinyWidgets", "DT", "plotly", "ggplot2", "googleVis", "colourpicker"))

Création de l’architecture

Mise en place d’un dashboard

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).

library(shiny)
library(shinydashboard)

ui <- dashboardPage(
  dashboardHeader(),
  dashboardSidebar(),
  dashboardBody()
)

server <- function(input, output) { }

shinyApp(ui, server)
Visualisation de l’application

Test de l’application

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.

ui <- dashboardPage(
  dashboardHeader(title = "bioinfo-fr"),
  dashboardSidebar( ),
  dashboardBody( )
)
Visualisation de l’application

Ajouter des pages

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 .

ui <- dashboardPage(
  dashboardHeader(title = "bioinfo-fr"),
  dashboardSidebar(
    sidebarMenu(
      menuItem("Lecture des données", tabName = "readData", icon = icon("readme")),
      menuItem("Visualisation des données", tabName = "visualization", icon = icon("poll"))
    )
  ),
  dashboardBody(
    tabItems(
      # Read data
      tabItem(tabName = "readData",
              h1("Lecture des données")
      ),
      
      # visualization
      tabItem(tabName = "visualization",
              h1("Visualisation des données")
      )
    )
  )
)
Visualisation de l'application

Création d’un lecteur de fichier

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.

ui <- dashboardPage(
  dashboardHeader(title = "bioinfo-fr"),
  dashboardSidebar(
    sidebarMenu(
      menuItem("Lecture des données", tabName = "readData", icon = icon("readme")),
      menuItem("Visualisation des données", tabName = "visualization", icon = icon("poll"))
    )
  ),
  dashboardBody(
    tabItems(
      # Read data
      tabItem(tabName = "readData",
              h1("Lecture des données"),
              fileInput("dataFile",label = NULL,
                        buttonLabel = "Browse...",
                        placeholder = "No file selected")
      ),
      
      # visualization
      tabItem(tabName = "visualization",
              h1("Visualisation des données")
      )
    )
  )
)
Visualisation de l’application

Zone de paramétrage

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
  • inline = T : pour avoir les radio boutons alignés
[…]
tabItem(tabName = "readData",
              h1("Lecture des données"),
              fileInput("dataFile",label = NULL,
                        buttonLabel = "Browse...",
                        placeholder = "No file selected"),
              
              h3("Parameters"),
              
              # Input: Checkbox if file has header
              radioButtons(id = "header", 
                           label = "Header",
                           choices = c("Yes" = TRUE,
                                       "No" = FALSE),
                           selected = TRUE, inline=T),
              
              # Input: Select separator ----
              radioButtons(id = "sep", 
                           label = "Separator",
                           choices = c(Comma = ",",
                                       Semicolon = ";",
                                       Tab = "\t"),
                           selected = "\t", inline=T),
              
              # Input: Select quotes ----
              radioButtons(id = "quote", 
                           label= "Quote",
                           choices = c(None = "",
                                       "Double Quote" = '"',
                                       "Single Quote" = "'"),
                           selected = "", inline=T)
              
      ),

[…]
Visualisation de l'application

Zone de prévisualisation

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.

tabItems(
      # Read data
      tabItem(tabName = "readData",
              h1("Lecture des données"),
              fileInput("dataFile",label = NULL,
                        buttonLabel = "Browse...",
                        placeholder = "No file selected"),
              
              h3("Parameters"),
              
              # Input: Checkbox if file has header
              radioButtons(inputId = "header", 
                           label = "Header",
                           choices = c("Yes" = TRUE,
                                       "No" = FALSE),
                           selected = TRUE, inline=T),
              
              # Input: Select separator ----
              radioButtons(inputId = "sep", 
                           label = "Separator",
                           choices = c(Comma = ",",
                                       Semicolon = ";",
                                       Tab = "t"),
                           selected = "t", inline=T),
              
              # Input: Select quotes ----
              radioButtons(inputId = "quote", 
                           label= "Quote",
                           choices = c(None = "",
                                       "Double Quote" = '"',
                                       "Single Quote" = "'"),
                           selected = "", inline=T),
              h3("File preview"),
              dataTableOutput(outputId = "preview")
              
      ),

Côté Server

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.

output$preview <-  renderDataTable({
    
    req(input$dataFile)
    
    df <- read.csv(input$dataFile$datapath,
                   header = as.logical(input$header),
                   sep = input$sep,
                   quote = input$quote,
                   nrows=10
    )
  },  options = list(scrollX = TRUE , dom = 't'))

Si nous détaillons le code :

  • 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,...) ).

tabItem(tabName = "readData",
              h1("Lecture des données"),
              fileInput("dataFile",label = NULL,
                        buttonLabel = "Browse...",
                        placeholder = "No file selected"),
              
              fluidRow(
                column(3,
                       h3("Parameters"),
                       
                       # Input: Checkbox if file has header
                       radioButtons(inputId = "header", 
                                    label = "Header",
                                    choices = c("Yes" = TRUE,
                                                "No" = FALSE),
                                    selected = TRUE, inline=T),
                       
                       # Input: Select separator ----
                       radioButtons(inputId = "sep", 
                                    label = "Separator",
                                    choices = c(Comma = ",",
                                                Semicolon = ";",
                                                Tab = "t"),
                                    selected = "t", inline=T),
                       
                       # Input: Select quotes ----
                       radioButtons(inputId = "quote", 
                                    label= "Quote",
                                    choices = c(None = "",
                                                "Double Quote" = '"',
                                                "Single Quote" = "'"),
                                    selected = "", inline=T)
                ),
                column(9,
                       h3("File preview"),
                       dataTableOutput(outputId = "preview")
                )
              )
Visualisation de l'application

Bouton de lecture

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".

[...]
actionButton(inputId = "actBtnVisualisation", label = "Visualisation",icon = icon("play") )
[...]

Pour une question esthétique, nous ajoutons un saut de ligne avant le bouton et nous mettons le bouton dans une division pour pouvoir le centrer :

[...]
tags$br(),
div(actionButton(inputId = "actBtnVisualisation", label = "Visualisation",icon = icon("play") ), align = "center")
[...]

Côté serveur

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)
data = reactiveValues()

  observeEvent(input$actBtnVisualisation, {
    data$table = read.csv(input$dataFile$datapath,
                          header = as.logical(input$header),
                          sep = input$sep,
                          quote = input$quote,
                          nrows=10)
  })

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.

updateTabItems(session, "tabs", selected = "visualization")

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.

output$dataTable = DT::renderDataTable(data$table)
Visualisation de l'application

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.
output$dataTable = DT::renderDataTable({
    datatable(data$table, filter = 'top') %>% 
      formatStyle('Sepal.Length', 
                  background = styleColorBar(data$table$Sepal.Length, 'lightcoral'),
                  backgroundSize = '100% 90%',
                  backgroundRepeat = 'no-repeat',
                  backgroundPosition = 'center'
      ) %>%
      formatStyle(
        'Sepal.Width',
        backgroundColor = styleInterval(c(3,4), c('white', 'red', "firebrick")),
        color = styleInterval(c(3,4), c('black', 'white', "white"))
      ) %>%
      formatStyle(
        'Petal.Length',
        background = styleColorBar(data$table$Petal.Length, 'lightcoral'),
        backgroundSize = '100% 90%',
        backgroundRepeat = 'no-repeat',
        backgroundPosition = 'center'
      ) %>%
      formatStyle(
        'Petal.Width',
        backgroundColor = styleInterval(c(1,2), c('white', 'red', "firebrick")),
        color = styleInterval(c(1,2), c('black', 'white', "white"))
      ) %>%
      formatStyle(
        'Species',
        backgroundColor = styleEqual(
          unique(data$table$Species), c('lightblue', 'lightgreen', 'lavender')
        )
      )
  })
Visualisation de l'application

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.
output$dataTable = DT::renderDataTable({
    datatable(data$table, filter = 'top') %>% 
     [...]
Visualisation de l'application

Visualisation graphique

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.

output$plotAvecR <- renderPlot({
    plot(data$table$Petal.Length,data$table$Sepal.Length, 
         main = "Sepal length vs Petal length (R)",
         ylab = "Sepal length",
         xlab = "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.

fluidRow(
                column(3,plotOutput("plotAvecR")),
                column(3, plotOutput("plotAvecGgplot2"))
              )

Côté serveur

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.

output$plotAvecGgplot2 <- renderPlot({
    ggplot(data=data$table, aes(x = Sepal.Length, y = Sepal.Width)) + 
      geom_point(aes(color=Species, shape=Species)) +
      xlab("Sepal Length") +  ylab("Sepal Width") +
      ggtitle("Sepal Length-Width (ggplot2")
  })
Visualisation de l'application

Graphique Plotly

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.

fluidRow(
                column(3, plotOutput("plotAvecR")),
                column(3, plotOutput("plotAvecGgplot2")),
                column(3, plotlyOutput("plotAvecPlotly"))
              )

Côté serveur

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.

plot_ly(data = data$table, x = ~ Petal.Length, y = ~ Petal.Width, color = ~ Species) %>%
        layout(title = 'Petal Length-Width (plotly)',
               yaxis = list(title = "Petal width"),
               xaxis = list(title = "Petal length"))

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>”) ) .

fluidRow(
                column(3, plotOutput("plotAvecR")),
                column(3, plotOutput("plotAvecGgplot2")),
                column(3, plotlyOutput("plotAvecPlotly")),
                column(3, htmlOutput("plotAvecGoogle"))
              )

Côté serveur

Pour les graphiques Google, nous utilisons les fonctions graphiques commençant par gvis et le rendu est fait avec la fonction renderGvis. Elles sont détaillées à la page suivante https://cran.r-project.org/web/packages/googleVis/vignettes/googleVis_examples.html .

output$plotAvecGoogle <- renderGvis({
      gvisHistogram(as.data.frame(data$table$Petal.Width),
                    options=list(title ="Petal width (Google)",
                                 height=400)
      )
  })

Visualisation de l’application

Visualisation du l'application

Gérer le tableau vide

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.

plot(data$table$Petal.Length[input$dataTable_rows_all],
           data$table$Sepal.Length[input$dataTable_rows_all], 
           main = input$title,
           ylab = "Sepal length",
           xlab = "Petal length",
           pch = as.numeric(input$pch),
           col = input$colR, 
           cex = input$cex)

Visualisation dans l’application

Visualisation de l'application

Conclusion

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).

Merci à mes relecteurs Aurélien C. et Ismaël P. pour leur aide !

Versions des outils utilisés

R version 3.5.1 (2018-07-02)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Debian GNU/Linux 9 (stretch)

Matrix products: default
BLAS: /usr/lib/openblas-base/libblas.so.3
LAPACK: /usr/lib/libopenblasp-r0.2.19.so

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C               LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8     LC_MONETARY=en_US.UTF-8    LC_MESSAGES=C             
 [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                  LC_ADDRESS=C               LC_TELEPHONE=C             LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

attached base packages:
[1] parallel  stats4    stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] bindrcpp_0.2.2              shinycssloaders_0.2.0       shinyjs_1.0                 colourpicker_1.0            shinyWidgets_0.4.4          reshape2_1.4.3             
 [7] plotly_4.8.0                ggplot2_3.1.0               FactoMineR_1.41             DT_0.5                      DESeq2_1.22.2               SummarizedExperiment_1.12.0
[13] DelayedArray_0.8.0          BiocParallel_1.16.5         matrixStats_0.54.0          Biobase_2.42.0              GenomicRanges_1.34.0        GenomeInfoDb_1.18.1        
[19] IRanges_2.16.0              S4Vectors_0.20.1            BiocGenerics_0.28.0         shinydashboard_0.7.1        shiny_1.2.0                

loaded via a namespace (and not attached):
 [1] bitops_1.0-6           bit64_0.9-7            RColorBrewer_1.1-2     httr_1.4.0             tools_3.5.1            backports_1.1.3        R6_2.3.0              
 [8] rpart_4.1-13           Hmisc_4.1-1            DBI_1.0.0              lazyeval_0.2.1         colorspace_1.3-2       nnet_7.3-12            withr_2.1.2           
[15] tidyselect_0.2.5       gridExtra_2.3          bit_1.1-14             compiler_3.5.1         htmlTable_1.13.1       flashClust_1.01-2      scales_1.0.0          
[22] checkmate_1.9.0        genefilter_1.64.0      stringr_1.3.1          digest_0.6.18          foreign_0.8-70         XVector_0.22.0         base64enc_0.1-3       
[29] pkgconfig_2.0.2        htmltools_0.3.6        htmlwidgets_1.3        rlang_0.3.0.1          rstudioapi_0.8         RSQLite_2.1.1          bindr_0.1.1           
[36] jsonlite_1.6           acepack_1.4.1          dplyr_0.7.8            RCurl_1.95-4.11        magrittr_1.5           GenomeInfoDbData_1.2.0 Formula_1.2-3         
[43] leaps_3.0              Matrix_1.2-14          Rcpp_1.0.0             munsell_0.5.0          yaml_2.2.0             scatterplot3d_0.3-41   stringi_1.2.4         
[50] MASS_7.3-50            zlibbioc_1.28.0        plyr_1.8.4             grid_3.5.1             blob_1.1.1             promises_1.0.1         crayon_1.3.4          
[57] miniUI_0.1.1.1         lattice_0.20-35        splines_3.5.1          annotate_1.60.0        locfit_1.5-9.1         knitr_1.21             pillar_1.3.1          
[64] geneplotter_1.60.0     XML_3.98-1.16          glue_1.3.0             latticeExtra_0.6-28    data.table_1.11.8      httpuv_1.4.5           gtable_0.2.0          
[71] purrr_0.2.5            tidyr_0.8.2            assertthat_0.2.0       xfun_0.4               mime_0.6               xtable_1.8-3           later_0.7.5           
[78] viridisLite_0.3.0      survival_2.42-3        tibble_1.4.2           AnnotationDbi_1.44.0   memoise_1.1.0          cluster_2.0.7-1

Bibliographie

[1]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
[2]Winston Chang and Barbara Borges Ribeiro (2018). shinydashboard: Create Dashboards with 'Shiny'. R package version 0.7.1. https://CRAN.R-project.org/package=shinydashboard
[3]Victor Perrier, Fanny Meyer and David Granjon (2018). shinyWidgets: Custom Inputs Widgets for Shiny. R package version 0.4.4. https://CRAN.R-project.org/package=shinyWidgets
[4]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
[5]Carson Sievert (2018) plotly for R. https://plotly-book.cpsievert.me
[6]H. Wickham. ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York, 2016.
[7]Markus Gesmann and Diego de Castillo. Using the Google Visualisation API with R. The R Journal, 3(2):40-44, December 2011.
[8]Dean Attali (2017). colourpicker: A Colour Picker Tool for Shiny and for Selecting Colours in Plots. R package version 1.0. https://CRAN.R-project.org/package=colourpicker
[9]Aurelien Chateigner (2018). anyLib: Install and Load Any Package from CRAN, Bioconductor or Github. R package version 1.0.5.
https://CRAN.R-project.org/package=anyLib

L’article Rendre ses projets R plus accessibles grâce à Shiny est apparu en premier sur bioinfo-fr.net.

Créer des Heatmaps à partir de grosses matrices en R

$
0
0

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.

library(dplyr)
library(purrr)

Ncol <- 50
genmat <- map(
    1:20000,
    function(i) {
        runif(Ncol) + c(sort(abs(rnorm(50)))[1:25], rev(sort(abs(rnorm(50)))[1:25]))  * i/5000
    }
) %>% do.call(rbind, .)

genmat[1:5, 1:5]
##            [,1]      [,2]       [,3]      [,4]      [,5]
## [1,] 0.24750229 0.8144309 0.31405005 0.2787540 0.8435071
## [2,] 0.44266149 0.1147394 0.28464511 0.6437944 0.7597911
## [3,] 0.11495737 0.6750608 0.04393633 0.5712240 0.2088942
## [4,] 0.16660166 0.5508895 0.75274403 0.7340737 0.9325773
## [5,] 0.07285492 0.4573314 0.09437322 0.1534962 0.4939674

dim(genmat)
## [1] 20000    50

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).

library(ggplot2)
library(patchwork)

# Wide to long transformation
data_for_ggplot <- as.data.frame(genmat) %>% 
    mutate(row = rownames(.)) %>% 
    tidyr::pivot_longer(-row, names_to = "col") %>%
    mutate(row = as.numeric(row), col = readr::parse_number(col))
    
# with geom_tile()
p1 <- ggplot(data_for_ggplot, aes(x = col, y = row, fill = value)) +
  geom_tile() +
  scale_fill_gradient2(
      low = "white", mid = "darkorange", high = "black",
      limits = c(0, 3), midpoint = 1.5, oob = scales::squish
  ) +
  labs(title = "geom_tile") +
  theme_void() +
  theme(legend.position = "none")

# with geom_raster()
p2 <- ggplot(data_for_ggplot, aes(x = col, y = row, fill = value)) +
  geom_raster() +
  scale_fill_gradient2(
      low = "white", mid = "darkorange", high = "black",
      limits = c(0, 3), midpoint = 1.5, oob = scales::squish
  ) +
  labs(title = "geom_raster") +
  theme_void()
  theme(legend.position = "none")

p1 + p2
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 top pour 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 :

Heatmap(
  genmat[nrow(genmat):1, ],
  col = circlize::colorRamp2(c(0, 1.5, 3), c("white", "darkorange", "black")),
  cluster_rows = FALSE, cluster_columns = FALSE,
  show_heatmap_legend = FALSE,
  use_raster = TRUE,
  raster_resize = TRUE, raster_device = "png",
  column_title = "With rasterisation"
)
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.

L’article Créer des Heatmaps à partir de grosses matrices en R est apparu en premier sur blog bioinformatique communautaire scientifique.

Pourquoi et comment déposer un package R sur Bioconductor ?

$
0
0

Ç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.

Note:

  • Si vous avez déjà soumis un package sur le CRAN, sautez directement à la section Les prérequis spécifiques à Bioconductor 😉
  • 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.

Le tronc commun avec le CRAN

Pour vous assurer que votre package soit partageable à la communauté, il est nécessaire de respecter certaines bonnes pratiques, ainsi que de s'assurer qu'il soit exécutable par d'autres. Et en concret, comment on teste ça ?

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 classes de 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 devez être inscrits au site de support de Bioconductor et abonné à la liste bioc-devel

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

L’article Pourquoi et comment déposer un package R sur Bioconductor ? est apparu en premier sur blog bioinformatique communautaire scientifique.

Choisir entre R et Python en bioinformatique ? Regards croisés entre collègues enseignants-chercheurs

$
0
0

Gaëlle Lelandais et Pierre Poulain

Illustration de Gwenaelle Lemoine.

Qui sommes-nous ?

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 !

Pour nous retrouver :

Gaëlle Lelandais : https://twitter.com/glelandais

Pierre Poulain : https://twitter.com/pierrepo


  1. ​*​
    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 :).
  2. ​†​
    Le principe était de mettre en application une méthode de lissage d’une signal par une stratégie de moyenne mobile.
  3. ​‡​
    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 !
  4. ​§​
    Je décide de garder ma source de l’époque anonyme 🙂
  5. ​¶​
    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 !).
  6. ​#​
    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...
  7. ​**​
    Sur ce point une ressource intéressante nous a été pointée par nos relecteurs : https://insights.stackoverflow.com/survey/2020#most-popular-technologies.
  8. ​††​
    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 🙂

L’article Choisir entre R et Python en bioinformatique ? Regards croisés entre collègues enseignants-chercheurs est apparu en premier sur blog bioinformatique communautaire scientifique.


Introduction à la manipulation d'intervalles dans R

$
0
0

Introduction

"Quelle est la profondeur de ce séquençage ?"

"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.

ir <- IRanges(
         start = c(4, 5, 9, 13, 19, 20, 20),
         end = c(15, 10, 14, 16, 25, 23, 24)
         )

Voyons à quoi ressemble notre objet:

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.

integer-Rle of length 25 with 13 runs
Lengths: 3 1 4 2 2 2 1 1 2 1 4 1 1
Values:  0 1 2 3 2 3 2 1 0 1 3 2 1

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

Pour aller plus loin ...

______________________________________________________________________________

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

L’article Introduction à la manipulation d'intervalles dans R est apparu en premier sur blog bioinformatique communautaire scientifique.

Manipulation d'intervalles génomiques dans R

$
0
0

Introduction

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.

Nous allons pour cela utiliser une bibliothèque de la suite Bioconductor, GenomicRanges.

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 "-").

gr <- GRanges(
  seqnames = c(rep("chr1", 4), rep("chr2", 4)),
  ranges = IRanges(start = c(1, 8, 3, 6, 1, 2, 1, 8),
                   end = c(5, 9, 6, 8, 2, 8, 4, 10)),
  strand = c(rep("+", 2), rep("-", 2), rep("+", 2), rep("-", 2)))

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().

# Ajouter des identifiants
mcols(gr)$id <- paste0(seqnames(gr), "_",
                       c(rep("plus", 2), rep("minus", 2),
                         rep("plus", 2), rep("minus", 2)), "_",
                         start(gr), "_", end(gr))

Notre objet ressemble maintenant à ceci :

Informations sur les chromosomes

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" &amp; 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" &amp; 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.

Pour aller plus loin ...


Un grand merci à Guillaume, Matthias et Pierre pour leurs commentaires, conseils et autres remarques constructives !

Les logos des bibliothèques ont été téléchargés sur BiocStickers.

L’article Manipulation d'intervalles génomiques dans R est apparu en premier sur blog bioinformatique communautaire scientifique.

Fréquences des dinucléotides dans le génome d'organismes modèles

$
0
0

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
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
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.

droso_genome_path <- "http://ftp.ensemblgenomes.org/pub/metazoa/release-52/fasta/drosophila_melanogaster/dna/Drosophila_melanogaster.BDGP6.32.dna.toplevel.fa.gz"

droso_genome <- readDNAStringSet(droso_genome_path)

droso_genome
DNAStringSet object of length 1870:
#>           width seq                               names               
#>    [1] 23513712 CGACAATGCACGACA...GAGAACAGAGAAGAG 2L dna:primary_as...
#>    [2] 25286936 CTCAAGATACCTTCT...AAACAGAATGAATTC 2R dna:primary_as...
#>    [3] 28110227 TAGGGAGAAATATGA...TCCATGTTATTCTAT 3L dna:primary_as...
#>    [4] 32079331 ACGGGACCGAGTATA...GCATTCTAGGAATTC 3R dna:primary_as...
#>    [5]  1348131 TTATTATATTATTAT...TTTGAGATATATGAA 4 dna:primary_ass...
#>    ...      ... ...
#> [1866]     1001 TATACATATATACAT...ATACGGATATACATA 211000022278576 d...
#> [1867]     1001 AGCACGACGCGACTG...AACTAACAAAGACCG 211000022279016 d...
#> [1868]     1001 TAATACGATTCACCT...CAGGACAAGGCACAC 211000022278495 d...
#> [1869]      564 AAGAGAAGAGAAGAG...GTCGACCTGCAGGCA 211000022280427 d...
#> [1870]      544 GTGTGCTGGAAAGGT...CGACCTACTCACAGC 211000022279089 d...

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
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_nucleotide_frequency <- function(dnastring) {
    top_strand_n <- alphabetFrequency(dnastring, baseOnly = TRUE)[, 1:4] |> colSums()
    bottom_strand_n <- top_strand_n[c("T", "G", "C", "A")]
    proportions(top_strand_n + bottom_strand_n)
}

double_strand_dinucleotide_frequency <- function(dnastring) {

    nucleotide_frequency <- double_strand_nucleotide_frequency(dnastring)

    top_strand_n <- dinucleotideFrequency(dnastring) |> colSums()
    bottom_strand_n <- top_strand_n[c("TT", "GT", "CT", "AT", "TG", "GG", "CG", "AG", "TC", "GC", "CC", "AC", "TA", "GA", "CA", "AA")]

    observed <- tibble(
        dinucleotide = names(top_strand_n),
        nucleotide_1 = substr(dinucleotide, 1, 1),
        nucleotide_2 = substr(dinucleotide, 2, 2),
        type = "observed",
        n = top_strand_n + bottom_strand_n,
        freq = proportions(n),
    )

    theoric <- tibble(
        dinucleotide = names(top_strand_n),
        nucleotide_1 = substr(dinucleotide, 1, 1),
        nucleotide_2 = substr(dinucleotide, 2, 2),
        type = "theoric",
        freq = nucleotide_frequency[nucleotide_1] * nucleotide_frequency[nucleotide_2],
        n = freq * sum(observed$n)
    )

    bind_rows(observed, theoric)
}

Ce qui donne :

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 !

L’article Fréquences des dinucléotides dans le génome d'organismes modèles est apparu en premier sur blog bioinformatique communautaire scientifique.

R : représenter des genomic tracks avec Gviz

$
0
0

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.

Exemple de genomic tracks tiré d'un article scientifique (CC-BY Davie et al. 2015)

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 :

Le but de cet article n'étant pas de vous faire un tour exhaustif de Gviz, je vous conseille fortement de lire, voire même de tester l'excellente vignette du paquet qui est disponible depuis bioConductor: https://bioconductor.org/packages/release/bioc/vignettes/Gviz/inst/doc/Gviz.html

Le génome de référence

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 :

"ENSMUSG00000025935.10"; gene_type "protein_coding"; gene_name "Tram1"; level 2; mgi_id "MGI:1919515"; havana_gene "OTTMUSG00000049113.1";

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:

> head(gene2symbol)
DataFrame with 6 rows and 2 columns
                                  gene_id     gene_name
                              <character>   <character>
ENSMUSG00000102693.1 ENSMUSG00000102693.1 4933401J01Rik
ENSMUSG00000064842.1 ENSMUSG00000064842.1       Gm26206
ENSMUSG00000051951.5 ENSMUSG00000051951.5          Xkr4
ENSMUSG00000102851.1 ENSMUSG00000102851.1       Gm18956
ENSMUSG00000103377.1 ENSMUSG00000103377.1       Gm37180
ENSMUSG00000104017.1 ENSMUSG00000104017.1       Gm37363

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")
> bw_files

[1] "Cond-1_REP1.bw" "Cond-1_REP2.bw" "Cond-1_REP3.bw" "Cond-2_REP1.bw"
[5] "Cond-2_REP2.bw" "Cond-2_REP3.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 :

png("test.png", width=14, height=8, unit="cm", res=200)
	plotTracks(
		ht,
		from = start(my_gene),
		to = end(my_gene),
		chromosome = as.character(seqnames(my_gene)),
		background.title = "transparent",
		col.border.title="transparent",
		col.title = "#333333",
		col.axis = "#333333"
	)
dev.off()
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.

Merci à Léopold, Ista, et Azerin pour la relecture.

L’article R : représenter des genomic tracks avec Gviz est apparu en premier sur Bioinfo-fr.net.

R : Convertir des Ensembl IDs en symboles de gènes

$
0
0

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 […]

L’article R : Convertir des Ensembl IDs en symboles de gènes est apparu en premier sur Bioinfo-fr.net.

Viewing all 33 articles
Browse latest View live