Introduction

Depuis l’avénement du du séquençage massivement parallèle (NGS, Next Generation Sequencing) en 2007, la détection de gènes différentiellement exprimés (DEG) à partir de données transcriptomiques RNA-seq constitue l’une de ses application les plus populaires.

Le principe est de mesurer, dans différentes conditions, la concentration d’ARN correspondant à chaque gène, et de comparer ces concentrations entre les échantillons de deux (comparaison à 2 groupes ou binaire) ou plusieurs conditions (comparaison multi-groupes).

Dès les premières analyses, les chercheurs se sont rendus compte que les méthodes classiques d’analyse différentielles (tests de Student, ANOVA) ne se prêtaient pas du tout à l’analyse de telles données, car leur nature diffère fondamentalement des hypothèss de travail sous-jacentes, pour différentes raisons.

  1. Le niveau d’expression d’un gène est quantifié sur une échelle discrète (comptage du nombre de lectures alignées qui chevauchent le gène), alors que les tests paramétriques reposent sur une hypoothèse de normalité.

  2. Les ordres de grandeur des comptages varient fortement d’un gène à l’autre: certains gènes ont une poignée de comptages par échantillons, d’autres des centaines de milliers. Les gènes très très fortement représentés correspondent généralement à des gènes non-codants (par exemple ARN ribosomique) ou à des fonctions cellulaires particulières liées au métabolisme de l’ARN, et on comprend les raisons biologiques de leur sur-représentation. Ils n’en constituent pas moins ce qu’on appelle en statistique des “valeurs aberrantes” (“outliers”).

  3. Les distributions de comptages comptent généralement un très grand nombre de valeurs nulles (“zero-inflated distributions”).

  4. Ces particularités posent des problèmes particuliers pour la normalisation des librairies de comptages :

    • la présence d’outliers affecte fortement la moyenne, et de façon très instable, ce qui biaise fortement l’estimation de la tendance centrale.

    • la stratégie de repli sur des estimateurs robustes, comme la médiane est contestable du fait du très grand nombre de valeurs nulles (dans certains cas, plusn d’un quart voire la moitié des gènes ont une valeur nulle pour un échantillon).

Des méthodes spécifiques ont donc été développées dès 2010 pour affronter ces difficultés particulières de la normalisation et de l’analyse différentielle des données de RNA-seq.

But de ce TP

Le but de ce TP est d’effectuer une première exploration de l’analyse différentielle des données d’expression, sur base d’un petit cas d’étude simple: l’analyse transcriptionnelle de mutants de sporulation chez la levure Saccharomyces cerevisiae.

Cas d’étude

… A ECRIRE: DESCRIPTION DU CAS D’ETUDE

Sources des données

Paramètres de l’analyse

Nous définissons dans une variable R (de type liste) les paramètres de l’analyse. Ceci nous permettra de reproduire ultérieurement exactement la même succession d’étapes soit en utilisant des données différentes, soit en modifiant les paramètres particuliers (design, seuils, …).

## Load libraries
message("Loading libraries")
library(knitr)
#library(kableExtra) ## Note: kableExtra has some side effect on kable: column padding is null, so all numbers seem to be mixed up
#library(FactoMineR)
# library(clues)
#library(RColorBrewer)
# library(ComplexHeatmap)
library(vioplot)
library(DESeq2)
library(edgeR)

# library(corrplot)
# library(ClassDiscovery)
# library(formattable)

# message("getwd()\t", getwd())
parameters <- list(
  # data.folder = "data/GSE89530", # dossier des données locales
  data.folder = "/shared/projects/du_bii_2019/data/module3/seance5/GSE89530", # on the IFB-cluster-core
  workdir = "~/DU-Bii/m3s5/", # Directory to export the results
  counts = "GSE89530_counts.tsv.gz",
  sample.descr = "GSE89530_samples.tsv.gz",
  alpha = 0.05, # seuil de significativité
  epsilon = 0.1, # pseudo-comptage pour la transformation log2
  gene.filter.percent.zeros = 90, # Discard genes having zero values in more than this percentage of samples
  gene.filter.min.count = 10, # Minimal counts per gene
  
  ## Test and control groups for differential analysis
  test.group = "bdf1_Y187F_Y354F_mutant_8",
  control.group = "Wild_type_8"
)

## Create output directory
dir.create(parameters$workdir, showWarnings = FALSE, recursive = TRUE)

## Print the parameter values in the report
kable(t(data.frame(parameters)), col.names = "Parameter Value")
Parameter Value
data.folder /shared/projects/du_bii_2019/data/module3/seance5/GSE89530
workdir ~/DU-Bii/m3s5/
counts GSE89530_counts.tsv.gz
sample.descr GSE89530_samples.tsv.gz
alpha 0.05
epsilon 0.1
gene.filter.percent.zeros 90
gene.filter.min.count 10
test.group bdf1_Y187F_Y354F_mutant_8
control.group Wild_type_8

Téléchargement des données

## List files in the data folder
# list.files(parameters$data.folder)

## Load sample descriptions
message("Loading sample descriptions from file\t", parameters$sample.descr)
samples <- read.delim(file.path(parameters$data.folder, parameters$sample.descr), row.names = 1, header = TRUE)
# View(samples) # Check visually the content of the samples data frame

## Load counts of reads per gene
message("Loading counts from file\t", parameters$counts)
counts <- read.delim(file.path(parameters$data.folder, parameters$counts), row.names = 1, header = TRUE)

## Check that the count table contains the same sample IDs as the sample description table. If not, stop everything here.
if (!setequal(rownames(samples), colnames(counts))) {
  stop("The sample IDs differ between count table (column names) and sample descriptions (row names). ")
}

## Check that sample IDs are ordered in the same way between sample descriptions (rows) and count table (columns). 
differing.IDs <- colnames(counts) != rownames(samples)
if (sum(differing.IDs) > 0) {
  ## If not, re-order columnts of the count table to match the rows of the sample description.  
  message("Reordering columns of the count table in order to match the order of sample descrition row names")
  counts <- counts[, rownames(samples)]
  
}

La table de comptages comporte 7478 lignes (correspondant aux gènes) et 18 colonnes (correspondant aux échantillons). Nous pouvons afficher un petit morceau de cette table, ens électionnant au hasard quelques lignes et colonnes.

some.genes <- sample(1:nrow(counts), size = 10, replace = FALSE)
some.samples <- sample(1:ncol(counts), size = 4, replace = FALSE)

kable(counts[some.genes, some.samples], caption = "Random selection of some genes and some columns of the raw count table. ")
Random selection of some genes and some columns of the raw count table.
GSM2375285 GSM2375295 GSM2375282 GSM2375288
YLR221C 312 486 307 156
YJL037W 706 3270 15 12
YOR252W 263 477 233 148
YKL166C 129 156 109 68
YJL052W 666 465 856 830
YLL018C-A 597 450 724 482
YOR337W 500 1194 729 489
YJL122W 274 634 323 117
YIR007W 443 169 465 350
YPL144W 75 63 100 55

Le tableau GSE89530_samples.tsv.gz fournit une description de chaque échantillon.

# print(samples[some.samples, ])
kable(samples[some.samples, ], 
      caption = "**Table de comptage de lectures (short reads) par gène.** Sélection arbitraire de quelques gènes (lignes) et échantillons (colonnes)", 
      align = "c")
Table de comptage de lectures (short reads) par gène. Sélection arbitraire de quelques gènes (lignes) et échantillons (colonnes)
Condition Replicate Genotype Time.point Label
GSM2375285 Wild_type_4 2 Wild-type 4 Sc_WT_4h_2
GSM2375295 bdf1_Y187F_Y354F_mutant_8 3 bdf1-Y187F-Y354F mutant 8 bd1bd2mut_8h_3
GSM2375282 bdf1_Y187F_Y354F_mutant_4 1 bdf1-Y187F-Y354F mutant 4 bd1bd2mut_4h_1
GSM2375288 bdf1_Y187F_Y354F_mutant_4 2 bdf1-Y187F-Y354F mutant 4 bd1bd2mut_4h_2

Exploration des données

Avant toute autre chose, il convient de mener une exploration préliminaire des données, afin de se familiariser avec leur distribution.

Statistiques descriptives

La fonction R summary() calcule des statistiques de base pour chaque colonne d’une matrice ou data frame. Nous imprimons ici un sous-ensemble de ces échantillons.

summary(counts[, some.samples])
   GSM2375285         GSM2375295         GSM2375282       GSM2375288      
 Min.   :     0.0   Min.   :     0.0   Min.   :     0   Min.   :     0.0  
 1st Qu.:    30.2   1st Qu.:    35.2   1st Qu.:    32   1st Qu.:    20.0  
 Median :   254.0   Median :   239.0   Median :   262   Median :   173.0  
 Mean   :   809.9   Mean   :   919.0   Mean   :   903   Mean   :   638.4  
 3rd Qu.:   595.0   3rd Qu.:   643.0   3rd Qu.:   647   3rd Qu.:   435.8  
 Max.   :472819.0   Max.   :511934.0   Max.   :542630   Max.   :425896.0  

Nous pouvons calculer quelques paramètres additionnels, qui nous éclaireront sur les données.

all.counts.vector <- unlist(counts) # Regroup all counts in a vevtor to compute statistics

count.stats <- data.frame(
  mean = mean(all.counts.vector),
  sd = sd(all.counts.vector),
  iqr = IQR(all.counts.vector),
  min = min(all.counts.vector),
  P05 = quantile(all.counts.vector, probs = 0.05),
  Q1 = quantile(all.counts.vector, probs = 0.25),
  median = quantile(all.counts.vector, probs = 0.5),
  Q3 = quantile(all.counts.vector, probs = 0.75),
  P95 = quantile(all.counts.vector, probs = 0.95),
  P99 = quantile(all.counts.vector, probs = 0.99),
  max = max(all.counts.vector)
  )

# View(count.stat) # for quick check in R interface
kable(t(count.stats), 
      caption = "Statistics on raw count values (all samples together)", 
      format.args = list(decimal.mark = '.', big.mark = ","), digits = 2,
      col.names = "Parameter value")
Statistics on raw count values (all samples together)
Parameter value
mean 827.44
sd 8,891.07
iqr 524.00
min 0.00
P05 0.00
Q1 24.00
median 206.00
Q3 548.00
P95 2,321.70
P99 7,759.73
max 722,346.00
rm(all.counts.vector) ## Free memory space

Nous pouvons également appliquer ces méthodes à chaque échantillon séparément, au moyen de la fonction apply()

## Compute sample means using the apply() function
sample.means <- apply(counts, 2, mean)

## Compute different stats per sample and store them as columns of a data frame
sample.stats <- data.frame(
  label = samples$Label,
  mean = apply(counts, 2, mean),
  sd = apply(counts, 2, sd),
  zeros = apply(counts == 0, 2, sum),
  Q1 = apply(counts, 2, quantile, probs = 0.75),
  median = apply(counts, 2, median),
  Q3 = apply(counts, 2, quantile, probs = 0.75),
  P95 = apply(counts, 2, quantile, probs = 0.95),
  lib.size.Mb = apply(counts, 2, sum)/1e6
)

kable(sample.stats, 
      caption = "Sample-wise statistics", 
      digits = c(0,0,0,0,0,0,0,0,1), 
      format.args = list(decimal.mark = '.', big.mark = ","),
      align = "l")
Sample-wise statistics
label mean sd zeros Q1 median Q3 P95 lib.size.Mb
GSM2375281 bd1bd2mut_0h_1 1,007 10,212 957 684 253 684 2,952 7.5
GSM2375287 bd1bd2mut_0h_2 632 6,591 1,083 412 147 412 1,897 4.7
GSM2375293 bd1bd2mut_0h_3 636 6,584 1,078 423 151 423 1,898 4.8
GSM2375278 Sc_WT_0h_1 1,127 11,725 957 775 297 775 3,385 8.4
GSM2375284 Sc_WT_0h_2 521 5,592 1,101 362 135 362 1,567 3.9
GSM2375290 Sc_WT_0h_3 779 7,821 1,026 548 194 548 2,187 5.8
GSM2375282 bd1bd2mut_4h_1 903 9,814 811 647 262 647 2,569 6.8
GSM2375288 bd1bd2mut_4h_2 638 7,149 942 436 173 436 1,818 4.8
GSM2375294 bd1bd2mut_4h_3 732 8,118 850 515 208 515 2,102 5.5
GSM2375279 Sc_WT_4h_1 980 9,135 878 735 310 735 2,830 7.3
GSM2375285 Sc_WT_4h_2 810 8,039 898 595 254 595 2,264 6.1
GSM2375291 Sc_WT_4h_3 632 5,556 1,013 470 194 470 1,853 4.7
GSM2375283 bd1bd2mut_8h_1 804 9,438 835 500 190 500 2,187 6.0
GSM2375289 bd1bd2mut_8h_2 956 10,819 808 626 248 626 2,608 7.1
GSM2375295 bd1bd2mut_8h_3 919 9,672 862 643 239 643 2,689 6.9
GSM2375280 Sc_WT_8h_1 1,078 10,956 879 607 230 607 2,672 8.1
GSM2375286 Sc_WT_8h_2 756 8,022 893 476 189 476 1,942 5.7
GSM2375292 Sc_WT_8h_3 983 11,193 973 462 166 462 2,219 7.3

Comparaison moyenne - variance par gène

gene.stats <- data.frame(
  mean = apply(counts, 1, mean),
  var = apply(counts, 1, var),
  sd = apply(counts, 1, sd)
)


plot(x = gene.stats$mean, y = gene.stats$var,
     main = "Mean / variance plot",
     xlab = "Mean per gene",
     ylab = "Var per gene", log="xy",
     col = densCols(x = gene.stats$mean, y = gene.stats$var))
abline()
Mean-Variance plot

Mean-Variance plot

Distribution des comptages

## Compute some basic statistics

## Display histogram of the raw counts
hist(unlist(counts), 
     breaks = 100, 
     col = "#FFDDBB",
     xlab = "Reads per gene", 
     ylab = "Number of observations", 
     main = "Distribution of raw counts")

Cet histogramme n’est pas très informatif, car toutes les valeurs sont concentrées dans la première tranche (à l’extrême gauche). Ceci résulte du fiat que les intervalles de classe ont été définis sur base de l’étendue totale, et qu’il existe apparemment une observation qui a une valeur énorme par rapport aux autres (valeur aberrante, “outlier”). De fait, la valeur la plus élevée (722346) dépasse de très loin la moyenne (827.4361757)

Afin de visualiser toute l’étendue des observations tout en mettant plus de détail sur les valeurs faibles, nous pouvons effectuer une transformation logarithmique. Nous devons cependant prêter attention à certains détails.

  • Conventionnellement, on utlise la transformation log2 pour les données RNA-seq, car elle fournit un découpage plus fin des valeurs couvertes.

  • Les données RnA-seq contiennent généralement un bon nombre de valeurs nulles (gènes non détectés), qui posent un problème pour la conversion logarithmique (\(log(0) = -\infty\)). On contourne ce problème en ajoutant aux comptages un petit nombre arbitraire, qu’on dénomme pseudo-comptage et qu’on symbolise par la lettre grècque epsilon (\(\epsilon\)).

Il est cournant d’ajouter la valeur \(1\), mais nous préférons ajouter une valeur inférieure à 1, pour bien distinguer les comptages réels (qui peuvent de fait prendre une valeur 1) des pseudo-comptages.

Pour cette analyse nous choisissons \(\epsilon = 0.1\).

La conversion donne donc.

\[\text{log2count}= log2(n + \epsilon) = log2(n + 0.1) \] L’histogramme des log2counts est plus informatif que celui des comptages bruts.

counts.log2 <- log2(counts + parameters$epsilon)

hist(unlist(counts.log2), 
     xlab = "log2(counts + epsilon)", 
     ylab = "Number of observations", 
     main = "Distribution of log2(counts)", 
     breaks = 100, col = "#BBDDEE")

Notons d’emblée le pic très élevé à gauche, qui correspond à toutes les observations nulles. Sur l’axe \(X\), il apparait à la valeur \(-3.3\), qui correspond à \(log_2(\epsilon) = log_2(0.1)\).

Boîtes à moustache

Les boîtes à moustache sont très utilisées pour obtenir une vision d’ensemble de plusieurs distribution.

## Associate a specific color to each condition
conditions <- unique(samples$Condition)
condition.colors <- rainbow(n = length(conditions))
names(condition.colors) <- conditions

## Associate a color to each sample according to its condition
samples$color <- condition.colors[samples$Condition]

par.ori <- par(no.readonly = TRUE) # Store the original value of the graphical parameters before modifying them

# Box plot of counts and log2-transformed counts
par(mfrow = c(1,2))
par(mar = c(5.1, 8.1, 4.1, 1.1)) # Increase leftmargin for sample labels

boxplot(counts, horizontal = TRUE, las = 1, names = samples$Label, col = samples$color, xlab = "Raw counts")

boxplot(counts.log2, horizontal = TRUE, las = 1, names = samples$Label, col = samples$color, xlab = "log2counts")
**Boxplot of the counts per gene in each sample.** Left: raw counts. Right: log2_transformed counts.

Boxplot of the counts per gene in each sample. Left: raw counts. Right: log2_transformed counts.

par(par.ori)

Violin plots

# Violin Plots
par(mfrow = c(1,2))
par(mar = c(5.1, 8.1, 4.1, 1.1)) # Increase left margin for sample labels
par(las = 1)
vioplot(as.list(counts), horizontal = TRUE, 
        col = samples$color, xlab = "Raw counts",
        main = "Violin plot\nRaw counts",
        names = samples$Label)

vioplot(as.list(counts.log2), horizontal = TRUE, 
        col = samples$color, xlab = "Log2(counts + epsilon)",
        main = "Violin plot\nlog-transformed counts",
        names = samples$Label)
**Violin plots**. Left: raw counts. Right: log2-transformed counts.

Violin plots. Left: raw counts. Right: log2-transformed counts.

par(par.ori)

Filtrage des gènes

Gènes à variance nulle (valeur constante)

## Zero-variance filter
gene.var <- apply(counts, 1, var)

## Define a Boolean vector (TRUE/FALSE) 
## indicating whether each gene has a null variance.
zero.var.genes <- gene.var == 0

kable(table(zero.var.genes))
zero.var.genes Freq
FALSE 6918
TRUE 560
## Count the number of genes having a zero variance
# sum(gene.var == 0)

Nous écartons les gènes qui ont une variance nulle (autrement dit, qui ont les mêmes valeurs pour tous les échantillons), puisque leurs moyennes seront forcément identiques entre conditions. Dans notre jeu de données, ceci nous amène à écarter 560

Pourcentage de valeurs nulles

## Discard genes having zeros in at least 95% of samples
message("Applying threshold on the percent of non-zero counts per gene: ", parameters$gene.filter.percent.zeros, "%")

## A Boolean table indicating whether each count is null (TRUE) or not (FALSE)
zero.counts <- counts == 0 

## Count number of zero values per gene (=row of the expression table)
percent.zeros <- 100*apply(zero.counts, 1, sum) / ncol(counts)

# Note: we store the histogram values in a variable named "h" for further use
h <- hist(percent.zeros, 
          breaks = 20, col = "#DDDDDD", 
          main = "Filter on the percentage of zero counts", 
          xlab = "Percent of samples", ylab = "Number of genes", las = 1)

# View(h)

## Get histogram max counts (y value) to position the arrow
h.max <- max(h$counts) 
arrows(x0 = parameters$gene.filter.percent.zeros, 
       y0 = h.max * 0.5, 
       x1 = parameters$gene.filter.percent.zeros, 
       y1 = h.max * 0.3, 
       col = "red", lwd = 2,
       angle = 30, length = 0.1)
arrows(x0 = parameters$gene.filter.percent.zeros, 
       y0 = h.max * 0.5, 
       x1 = 100, 
       y1 = h.max * 0.5, code = 0,
       col = "red", lwd = 4,
       angle = 30, length = 0.1)
text(x = parameters$gene.filter.percent.zeros, 
     y = h.max*0.5, 
     labels = paste(sep = "", parameters$gene.filter.percent.zeros, "%"),
     col = "red", pos = 3)
Frequency of samples with zero counts per gene. Genes exceeding the thresold (red arrow) were filtered out.

Frequency of samples with zero counts per gene. Genes exceeding the thresold (red arrow) were filtered out.

Comptages minimaux par gène

Un autre critère possible est de supprimer les gènes faiblement détectés, en écartant les gènes pour lesquels les comptages sont inférieurs à un seuil donné pour tous les échantillons. Nous fixons ici (arbitrairement) le seuil à 10.

Nous procédont comme suit:

  • Calculer pour chaque gène \(i\) le comptage maximal observé parmi tous les échantillons.

\[x_{max,i} = \text{max}_{j = 0}^{s} x_{i,j}\]

\(i\) est l’indice de lignes (gènes), \(j\) l’indice de colonnes (échantillons), et \(s\) le nombre d’échantillons.

  • Tester si la valeur maximale observée est inférieure au seuil minimum.
max.per.gene <- apply(counts, 1, max)

# View(summary(min.count))
h <- hist(log2(max.per.gene + parameters$epsilon), breaks = 100, col = "#BBBBFF",
          xlab = "log2(max count per gene)",
          ylab = "Number of genes", las = 1,
          main = "Filtering on min count per gene")
h.max <- max(h$counts)
arrows(x0 = log2(parameters$gene.filter.min.count), 
       y0 = h.max * 0.5, 
       x1 = log2(parameters$gene.filter.min.count), 
       y1 = h.max * 0.3, 
       col = "red", lwd = 2,
       angle = 30, length = 0.1)
arrows(x0 = log2(parameters$epsilon), 
       y0 = h.max * 0.5, 
       x1 = log2(parameters$gene.filter.min.count), 
       y1 = h.max * 0.5, 
       col = "red", lwd = 4, code = 0,
       angle = 30, length = 0.1)
text(x = log2(parameters$gene.filter.min.count), 
     y = h.max * 0.5, 
     labels = paste(sep = "", "log2(", 
                    parameters$gene.filter.min.count,
                    ") = ", signif(digits = 3, log2(parameters$gene.filter.min.count))),
     col = "red", pos = 3)
Distribution of max counts per gene. Genes below the thresold (red arrow) are filtered out.

Distribution of max counts per gene. Genes below the thresold (red arrow) are filtered out.

# Build a data frame with one row per gene and one column per filtering criterion
discarded.genes <- data.frame(
  zero.var = zero.var.genes,
  too.many.zeros = (percent.zeros > parameters$gene.filter.percent.zeros),
  too.small.counts = max.per.gene < parameters$gene.filter.min.count
)


## Print the discarding criteria for some genes 
kable(discarded.genes[some.genes, ], caption = "Gene filtering criteria. TRUE indicates that a gene is discarded. ")
Gene filtering criteria. TRUE indicates that a gene is discarded.
zero.var too.many.zeros too.small.counts
YLR221C FALSE FALSE FALSE
YJL037W FALSE FALSE FALSE
YOR252W FALSE FALSE FALSE
YKL166C FALSE FALSE FALSE
YJL052W FALSE FALSE FALSE
YLL018C-A FALSE FALSE FALSE
YOR337W FALSE FALSE FALSE
YJL122W FALSE FALSE FALSE
YIR007W FALSE FALSE FALSE
YPL144W FALSE FALSE FALSE
## Draw a venn diagram indicating the number of genes 
## discarded by the different criteria. 
venn.counts <- vennCounts(discarded.genes)
vennDiagram(venn.counts, cex = 0.8,
            main = "Genes discarded by different criteria")
Number of genes discarded during the filtering step.

Number of genes discarded during the filtering step.

## Genes passing the filters are those for which 
## all the discarding criteria are FALSE, 
## i.e. the sum of the row is 0
filtered.genes <- apply(discarded.genes, 1, sum) == 0

## Select a matrix with the filtered genes, 
## i.e. those not discarded by any criterion
filtered.counts <- counts[filtered.genes, ]

## Update the set of test genes to avoir NA values
some.genes <- sample(1:nrow(filtered.counts), size = 10, replace = FALSE)

Reproductibilité des réplicats

Nous pouvons comparer les réplicats pour une même condition au moyen d’un nuage de points, avec la fonction plot().

## Sélectionner les échantillons de la condition "test"
message("Comparing counts between replicates for condition", parameters$test.group)
selected.samples <- samples$Condition  == parameters$test.group
plot(counts.log2[, selected.samples], 
     col = samples[selected.samples, "color"])
Between-replicate comparison of counts per gene for the test condition.

Between-replicate comparison of counts per gene for the test condition.

Détection de gènes différentiellement exprimés avec edgeR

Comparaison entre 2 groupes

Dans un premier temps, nous allons effectuer une simple comparaison de 2 groupes: sauvage versus mutant au temps 0.

## Detection of Differentially Expressed Genes (DEG) with edgeR
message("Detecting Differentially Expressed Genes (DEG) with edgeR")


## Select the subsets of counts (columns) and samples (rows) corresponding to the control and test groups
control.samples <- which(samples$Condition == parameters$control.group)
test.samples <- which(samples$Condition == parameters$test.group)
selected.samples <- c(test.samples, control.samples)

## Select samples for a 2-group

## Build a "model matrix" from the class labels
## This matrix contains one row per sample and one column per class
designMat <- model.matrix(~ as.vector(samples[selected.samples, "Condition"]))
# View(designMat)

## Build an edgeR::DGEList object which is required to run edgeR DE analysis
dgList <- DGEList(counts = filtered.counts[, selected.samples])
# class(dgList)
# is(dgList)

## Estimate the dispersion parameters. 
message("\tedgeR\tEstimating dispersion")
dgList <- estimateDisp(dgList, design = designMat)
dgList <- estimateTagwiseDisp(dgList, design = designMat)

## Fit edgeR model for differential expression analysis.
## We chose glmQLFit because it is claimed to offer a more accurate control of type I error. 
message("\tedgeR\tmodel fitting with glmQLFit()")
fit <- glmQLFit(dgList, design = designMat)

## Run test to detect differentially expressed genes
message("\tedgeR\tdetecting differentially expressed genes with glmQLFTest()")
qlf <- glmQLFTest(fit, coef = 2:ncol(designMat))
qlf.TT <- topTags(qlf, n = nrow(qlf$table), sort.by = "none",
                  adjust.method = "fdr")


## Select differentially expressed genes
edgeR.DEG.table <- as.data.frame(qlf.TT$table)

## Compute some additional statistics
edgeR.DEG.table$evalue <- edgeR.DEG.table$PValue * nrow(edgeR.DEG.table)
edgeR.DEG.table$rank <- rank(edgeR.DEG.table$FDR, ties.method = "average")
edgeR.DEG.table$positive <- edgeR.DEG.table$FDR < parameters$alpha
# table(edgeR.DEG.table$positive)
# names(edgeR.DEG.table)
# View(edgeR.DEG.table)
# sum(egeR.DEG)

Exercices (solutions ci-dessous)

  1. Dessinez un nuage de points (plot()) comparant les P-valeurs ajustées et non-ajustées.

  2. Quel serait le nombre de faux-positifs attendus sous hypothès nulle si l’on appliquait le seuil \(\alpha = 0.05\) à la p-valeur non-ajustée ?

  3. Comptez le nombre de gènes déclarés positifs selon qu’on applique le seuil \(\alpha = 0.05\) à la p-valeur ajustée ou non-ajustée.

  4. Dessinez un histogramme (hist()) des P-valeurs non ajustées, avec 20 intervalles de classe (breaks = 20). Observez la répartition des gènes dans la moitié inférieure (p-valeur < 0.5) et supérieure (p-valeur > 0.5) de l’histogramme. Interprétez le résultat en 2 ou 3 phrases. A quoi s’attendrait-on si tous les gènes étaient sous hypothèse nulle ?

  5. Dessinez un nuage de points (plot()) comparant le log2(fold-change) en abscisse et la P-valeur en ordonnée. Interprétez le résultat en 2 ou 3 phrases. Où se trouvent les gènes signicatifs ?

  6. Dessinez un volcano plot, c’est-à-dire un nuage de points (plot()) présentant en abscisse le log2(fold-change), et en ordonnée la significativité, définie comme suit.

\[\text{sig} = -log_{10}(\text{FDR})\]

Interprétez le résultat en 2 ou 3 phrases. Commentez la relation entre le logFC et la significativité. Où se trouvent les gènes signicatifs ?

Adjusted versus non-adjusted P-values

# class(qlf.TT)
plot(edgeR.DEG.table$PValue, edgeR.DEG.table$FDR, 
     main = "Multiple testing correction",
     xlab = "Nominal p-value",
     ylab = "FDR",
     log = "xy", 
     col = densCols(x = edgeR.DEG.table$PValue,
                    y = edgeR.DEG.table$FDR),
     panel.first = grid(), 
     las = 1)
abline(h = parameters$alpha, col = "#BB0000")
abline(v = parameters$alpha, col = "#FF0000", lty = "dashed")
legend("topleft",
       legend = c(
         paste(sep = "", "p-value < ", parameters$alpha, ": ", 
               sum(edgeR.DEG.table$PValue < parameters$alpha), " genes"),
         paste(sep = "", "FDR < ", parameters$alpha, ": ", sum(edgeR.DEG.table$FDR < parameters$alpha), " genes" )))
**Ajusted (ordinate) versus non-adjusted (abscissa) p-values returned by edger**. Strong colors indicate a higher density of overlapping points. Note the strong impact of the multiple testing correction.

Ajusted (ordinate) versus non-adjusted (abscissa) p-values returned by edger. Strong colors indicate a higher density of overlapping points. Note the strong impact of the multiple testing correction.

Histogram of the nominal P-values

## P-value histogram
hist(edgeR.DEG.table$PValue, 
     breaks = 20, col = "#DDEEFF", las = 1,
     main = "edgeR P-value histogram",
     xlab = "Nominal P-value", 
     ylab = "Number of genes")
**Histogram of edgeR nominal P-values.**

Histogram of edgeR nominal P-values.

Volcano plot

# Quick test: plot the different symbols
# plot(1:30, pch = 1:30)


# Assign a graphical symbol to each gene according to its positive/negative status
edgeR.DEG.table$pch <- 1 # circle symbol
edgeR.DEG.table$pch[edgeR.DEG.table$positive] <- 3 # + symbol

par(mfrow = c(1,2))
plot(x = edgeR.DEG.table$logFC, 
     y = -log10(edgeR.DEG.table$PValue),
     las = 1, 
     xlab = "log2(fold-change)",
     main = "Volcano plot of edgeR unadjusted p-values",
     ylab = "-log10(P-value)",
     pch = edgeR.DEG.table$pch, 
     col = densCols(x = edgeR.DEG.table$logFC, 
                    y = -log10(edgeR.DEG.table$PValue)),
     panel.first = grid())
abline(v = 0)
abline(h = 0)

plot(x = edgeR.DEG.table$logFC, 
     y = -log10(edgeR.DEG.table$FDR),
     las = 1, 
     xlab = "log2(fold-change)",
     main = "Volcano plot of edgeR FDR",
     ylab = "-log10(P-value)",
     pch = edgeR.DEG.table$pch, 
     col = densCols(x = edgeR.DEG.table$logFC, 
                    y = -log10(edgeR.DEG.table$FDR)),
     panel.first = grid())
abline(v = 0)
abline(h = 0)
abline(h = -log10(parameters$alpha), col = "#BB0000")
**Volcano plots of edgeR unadjusted (left) and adjusted (right) P-values**. Multple testing correction method: FDR.

Volcano plots of edgeR unadjusted (left) and adjusted (right) P-values. Multple testing correction method: FDR.

par(mfrow = c(1,1))

Détection de gènes différentiellement exprimés avec DESeq2

Nous allons maintenant utiliser DESeq2 pour détecter des gènes différentiellement exprimés sur le même jeu de données. Nous comparerons ensuite les résultats obtenus avec edgeR et DESeq2, respectivement.

Comparaison de 2 groupes

Dans un premier temps, nous allons effectuer une simple comparaison de 2 groupes: sauvage versus mutant au temps 0.

## Use the DESeqDataSetFromMatrix to create a DESeqDataSet object
message("Creating DESeq2 dataset")
dds.2groups <- DESeqDataSetFromMatrix(
  countData = filtered.counts[, selected.samples], 
  colData = samples[selected.samples, ], 
  design = ~ Condition)
# View(dds.all.conditions)

## Run differential expression analysis with DESeq2
dds.2groups <- DESeq(dds.2groups)
# View(dds.all.conditions)

resultsNames(dds.2groups)
[1] "Intercept"                                          "Condition_Wild_type_8_vs_bdf1_Y187F_Y354F_mutant_8"
## Get the result table
deseq2.res.dds.2groups <- as.data.frame(results(dds.2groups))
kable(deseq2.res.dds.2groups[some.genes, ])
baseMean log2FoldChange lfcSE stat pvalue padj
YBR225W 244.689195 0.4296799 0.2509285 1.7123596 0.0868304 0.1775735
YGR010W 160.653261 -0.7817281 0.2900742 -2.6949250 0.0070404 0.0244651
YKL001C 293.381189 -1.5336306 0.4681641 -3.2758399 0.0010535 0.0051962
YPR145W 3999.507230 -1.9770166 0.4876586 -4.0540996 0.0000503 0.0003952
YFL001W 138.282126 -0.4780824 0.4012022 -1.1916247 0.2334084 0.3700795
YPR059C 6.445059 -0.2402650 0.7820804 -0.3072127 0.7586815 0.8422931
YLL039C 9189.263978 -1.1966941 0.1632193 -7.3318193 0.0000000 0.0000000
YMR291W 476.968986 -0.4244133 0.1941120 -2.1864356 0.0287838 0.0758575
YMR022W 540.335918 -0.1484145 0.2328350 -0.6374236 0.5238489 0.6579550
YDR327W 182.555445 0.2791229 0.2313968 1.2062525 0.2277202 0.3633561
## Sort genes by significance (increasing adjusted pvalue)
deseq2.res.dds.2groups <- deseq2.res.dds.2groups[order(deseq2.res.dds.2groups$padj, decreasing = FALSE), ]

## Compute the E-value, i.e. the expected number of false positives
deseq2.res.dds.2groups$evalue <- deseq2.res.dds.2groups$pvalue * nrow(deseq2.res.dds.2groups)

## Flag the genes declared positive 
## (i.e. differentially expressed genes)
deseq2.res.dds.2groups$positive <- deseq2.res.dds.2groups$padj < parameters$alpha

## Replace NA values by FALSE for the flag
deseq2.res.dds.2groups$positive[is.na(deseq2.res.dds.2groups$positive)] <- FALSE


kable(head(deseq2.res.dds.2groups, n=20), caption = "Most significant genes")
Most significant genes
baseMean log2FoldChange lfcSE stat pvalue padj evalue positive
YDR403W 5959.4264 6.802686 0.4680482 14.534158 0 0 0 TRUE
YDL218W 1330.4326 5.967320 0.4179094 14.278979 0 0 0 TRUE
YDR402C 478.8717 3.829931 0.3565490 10.741668 0 0 0 TRUE
YLR012C 283.6450 3.701907 0.3546005 10.439656 0 0 0 TRUE
YDR019C 1656.5499 -1.666358 0.1606152 -10.374847 0 0 0 TRUE
YHR136C 364.6893 -3.235702 0.3132024 -10.331024 0 0 0 TRUE
YCL048W-A 1166.8588 3.494313 0.3499982 9.983804 0 0 0 TRUE
YBR054W 465.8305 1.959048 0.1967399 9.957556 0 0 0 TRUE
YDR481C 1776.0346 -1.571571 0.1632137 -9.628918 0 0 0 TRUE
YHL028W 1033.7650 2.896800 0.3057668 9.473886 0 0 0 TRUE
YKR093W 219.6354 2.245243 0.2438249 9.208421 0 0 0 TRUE
YPL017C 468.1963 -1.658115 0.1807319 -9.174449 0 0 0 TRUE
YPR140W 4221.3821 2.969006 0.3266519 9.089204 0 0 0 TRUE
YKL051W 293.1184 -1.701202 0.1890227 -8.999988 0 0 0 TRUE
YIL045W 5220.8484 2.885226 0.3221962 8.954872 0 0 0 TRUE
YJL153C 5320.2053 -1.556167 0.1743461 -8.925733 0 0 0 TRUE
YLR265C 290.9602 2.397209 0.2684635 8.929364 0 0 0 TRUE
YEL057C 353.0099 3.669170 0.4146033 8.849834 0 0 0 TRUE
YKL163W 762.7615 -2.117467 0.2400691 -8.820242 0 0 0 TRUE
YGR088W 552.0450 3.103978 0.3588942 8.648729 0 0 0 TRUE
## BEWARE, DESeq2 produces NA values for some genes
kable(tail(deseq2.res.dds.2groups, n=10), caption = "Less significant genes")
Less significant genes
baseMean log2FoldChange lfcSE stat pvalue padj evalue positive
YGR280C 407.252191 -0.0025888 0.4236952 -0.0061100 0.9951249 0.9966600 6243.414 FALSE
snR36 6.144165 -0.0038069 0.7130335 -0.0053390 0.9957401 0.9970114 6247.273 FALSE
YLR004C 431.919635 0.0011678 0.2387877 0.0048905 0.9960980 0.9972106 6249.519 FALSE
YKL069W 224.639084 0.0007356 0.2364025 0.0031116 0.9975173 0.9983264 6258.424 FALSE
YNL113W 74.851169 -0.0014150 0.4572433 -0.0030947 0.9975308 0.9983264 6258.508 FALSE
YLR185W_intron 21.555742 0.0016993 0.6165551 0.0027562 0.9978009 0.9984374 6260.203 FALSE
YCL026C-B 35.385132 -0.0006591 0.4163923 -0.0015828 0.9987371 0.9992149 6266.077 FALSE
YOR004W 175.276696 0.0004707 0.4393938 0.0010713 0.9991453 0.9994639 6268.637 FALSE
YDL166C 98.693288 0.0001988 0.3229337 0.0006157 0.9995088 0.9996681 6270.918 FALSE
YBR293W 330.930747 -0.0000905 0.2949593 -0.0003069 0.9997551 0.9997551 6272.464 FALSE
kable(table(deseq2.res.dds.2groups$positive), 
      caption = "Number of genes declared positive")
Number of genes declared positive
Var1 Freq
FALSE 4138
TRUE 2136
## Export the results
outfiles <- vector()

Diagnostic plots

## Assign a symbol to genes according to the test result
deseq2.res.dds.2groups$pch <- 1 # circle
deseq2.res.dds.2groups$pch[deseq2.res.dds.2groups$positive] <- 3 # + symbol

## Set one color per gene depending on whether it is positive or not
gene.color <- rep(x = "#BBBBBB", times = nrow(deseq2.res.dds.2groups))
gene.color[deseq2.res.dds.2groups$positive] <- "#FF8844"
gene.pch <- rep(x = 1, times = nrow(deseq2.res.dds.2groups))
gene.pch[deseq2.res.dds.2groups$positive] <- "#+"

par(mfrow = c(2,2))
## MA plot? 
## Note: we explicitly indicate the package name to avoid confusion with limma::plotMA()
DESeq2::plotMA(object = results(dds.2groups), las = 1, 
               panel.first = grid(),
               main = "DESDeq2 : MA plot")


## Volcano plot of adjusted p-values
plot(x = deseq2.res.dds.2groups$log2FoldChange, 
     y = -log10(deseq2.res.dds.2groups$padj),
     main = "Volcano plot\nAdjusted P-values",
     las = 1, panel.first = grid(),
     xlab = "Log2 fold change",
     ylab  = "-log10(padj)",
     col = densCols(
       x = deseq2.res.dds.2groups$log2FoldChange, 
       y = -log10(deseq2.res.dds.2groups$padj)),
     pch = deseq2.res.dds.2groups$pch
     )
abline(v = 0)
abline(h = 0)


## P-value histogram
hist(deseq2.res.dds.2groups$pvalue, 
     breaks = 20, col = "#DDEEFF", 
     xlab = "Nominal P-value", 
     ylab = "Number of genes")


par(mfrow = c(1,1))

Toutes conditions confondues

## Use the DESeqDataSetFromMatrix to create a DESeqDataSet object
message("Creating DESeq2 dataset")
dds.all.conditions <- DESeqDataSetFromMatrix(
  countData = filtered.counts, 
  colData = samples, 
  design = ~ Condition)
# View(dds.all.conditions)

## Run differential expression analysis with DESeq2
dds.all.conditions <- DESeq(dds.all.conditions)
# View(dds.all.conditions)

resultsNames(dds.all.conditions)
[1] "Intercept"                                                        "Condition_bdf1_Y187F_Y354F_mutant_4_vs_bdf1_Y187F_Y354F_mutant_0" "Condition_bdf1_Y187F_Y354F_mutant_8_vs_bdf1_Y187F_Y354F_mutant_0" "Condition_Wild_type_0_vs_bdf1_Y187F_Y354F_mutant_0"              
[5] "Condition_Wild_type_4_vs_bdf1_Y187F_Y354F_mutant_0"               "Condition_Wild_type_8_vs_bdf1_Y187F_Y354F_mutant_0"              
## Get the result table
deseq2.res.all.conditions <- results(dds.all.conditions)
deseq2.res.all.conditions.df <- as.data.frame(deseq2.res.all.conditions)
kable(deseq2.res.all.conditions.df[some.genes, ])
baseMean log2FoldChange lfcSE stat pvalue padj
YBR225W 302.641570 0.2096397 0.2393356 0.8759233 0.3810717 0.4412991
YGR010W 247.073433 -0.8058750 0.1897339 -4.2473972 0.0000216 0.0000458
YKL001C 235.681556 0.3805740 0.4589381 0.8292492 0.4069634 0.4671219
YPR145W 2772.090602 -0.3093528 0.3590851 -0.8615028 0.3889612 0.4489224
YFL001W 185.724353 -0.9781276 0.3134956 -3.1200676 0.0018081 0.0031285
YPR059C 7.076586 1.5086643 0.8368546 1.8027794 0.0714229 0.0972877
YLL039C 8112.265124 0.0082526 0.2028185 0.0406894 0.9675435 0.9731273
YMR291W 527.551116 -0.8971674 0.1819015 -4.9321615 0.0000008 0.0000020
YMR022W 564.104525 0.4024555 0.1988592 2.0238220 0.0429885 0.0608002
YDR327W 193.958497 0.4798121 0.1960650 2.4472091 0.0143967 0.0221060

Exercices

  1. Dessinez un nuage (plot()) qui compare lles p-valeurs nominales (non-ajustées) retournées respectivement par edgeR et DEseq2.

  2. Dessinez un nuage (plot()) qui compare lles p-valeurs ajustées retournées respectivement par edgeR et DEseq2.

  3. Dessinez un diagramme de Venn qui compare les ensemble de gènes déclarés positifs par edgeR et DESeq2.

Contrôles négatifs

Exercice

Générez un tableau ayant les mêmes dimensions (lignes, colonnes) que votre tableau d’expresion, en le remplissant de nombres aléatoires tirés selon une distribution de Poisson d’espérance 200 (fonction rpois()).

Effectuez une analyse différentielle sur ces données.

Fichiers de résultats

## Build a file prefix by concatenating the two compared groups
file.prefix <- paste(sep = "__", parameters$test.group, 
                     "vs", parameters$control.group)


## DESeq2 result table
outfiles["DESeq2 all genes"] <- file.path(
  parameters$workdir, 
  paste(sep = "", file.prefix, "_DESeq2_all_genes.tsv"))
write.table(
  x = deseq2.res.dds.2groups, 
  quote = FALSE, row.names = TRUE, col.names = NA,
  file = outfiles["DESeq2 all genes"])


## Differentially expressed genes
outfiles["DESeq2 positive genes"] <- file.path(
  parameters$workdir, 
  paste(sep = "", file.prefix, "_DESeq2_positive.tsv"))
write.table(
  x = deseq2.res.dds.2groups[deseq2.res.dds.2groups$positive, ], 
  quote = FALSE, row.names = TRUE, col.names = NA,
  file = outfiles["DESeq2 positive genes"] )

## Differentially expressed genes
outfiles["DESeq2 DEG iDs"] <- file.path(
  parameters$workdir, paste(sep = "", file.prefix, "_DESeq2_positive_geneIDs.txt"))
write.table(
  x = row.names(deseq2.res.dds.2groups[deseq2.res.dds.2groups$positive, ]), 
  quote = FALSE, row.names = FALSE, col.names = FALSE,
  file = outfiles["DESeq2 DEG iDs"] )

kable(outfiles, caption = "fichiers de résultats", col.names = "Result file")
fichiers de résultats
Result file
DESeq2 all genes ~/DU-Bii/m3s5//bdf1_Y187F_Y354F_mutant_8__vs__Wild_type_8_DESeq2_all_genes.tsv
DESeq2 positive genes ~/DU-Bii/m3s5//bdf1_Y187F_Y354F_mutant_8__vs__Wild_type_8_DESeq2_positive.tsv
DESeq2 DEG iDs ~/DU-Bii/m3s5//bdf1_Y187F_Y354F_mutant_8__vs__Wild_type_8_DESeq2_positive_geneIDs.txt