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.
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é.
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”).
Les distributions de comptages comptent généralement un très grand nombre de valeurs nulles (“zero-inflated distributions”).
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.
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.
… A ECRIRE: DESCRIPTION DU CAS D’ETUDE
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 |
## 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. ")
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")
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 |
Avant toute autre chose, il convient de mener une exploration préliminaire des données, afin de se familiariser avec leur distribution.
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")
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")
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 |
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()
## 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)\).
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")
par(par.ori)
# 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)
par(par.ori)
## 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
## 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)
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:
\[x_{max,i} = \text{max}_{j = 0}^{s} x_{i,j}\]
où \(i\) est l’indice de lignes (gènes), \(j\) l’indice de colonnes (échantillons), et \(s\) le nombre d’échantillons.
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)
# 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. ")
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")
## 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)
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"])
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)
Dessinez un nuage de points (plot()
) comparant les P-valeurs ajustées et non-ajustées.
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 ?
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.
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 ?
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 ?
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 ?
# 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" )))
## 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")
# 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")
par(mfrow = c(1,1))
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.
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")
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")
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")
Var1 | Freq |
---|---|
FALSE | 4138 |
TRUE | 2136 |
## Export the results
outfiles <- vector()
## 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))
## 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 |
Dessinez un nuage (plot()
) qui compare lles p-valeurs nominales (non-ajustées) retournées respectivement par edgeR et DEseq2.
Dessinez un nuage (plot()
) qui compare lles p-valeurs ajustées retournées respectivement par edgeR et DEseq2.
Dessinez un diagramme de Venn qui compare les ensemble de gènes déclarés positifs par edgeR et DESeq2.
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.
## 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")
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 |