Analyse d’expression différentielle avec DESeq2 dans R : un tutoriel pas à pas
D’une matrice de comptage à une liste classée de gènes différentiellement exprimés — ajuster, interpréter, rétrécir et visualiser avec un volcano plot
Un guide pratique et complet de l’analyse d’expression différentielle des gènes avec DESeq2 dans R. Partez d’une matrice de comptage de bulk RNA-seq, construisez le DESeqDataSet avec le bon design, lancez le test binomial négatif entre deux conditions, et lisez chaque colonne du résultat — la moyenne de base, le log2 fold change et sa direction, l’erreur standard, la statistique de Wald, la p-value et la padj ajustée par FDR. Rétrécissez les fold changes pour le classement et le tracé, faites correspondre les identifiants Ensembl aux symboles de gènes, et tracez les deux figures que tout article RNA-seq utilise : le MA plot et le volcano plot. Sur le jeu de données airway intégré.
Date de publication
29 juin 2026
Modifié
8 juillet 2026
AstucePoints clés
DESeq2 repère les gènes qui changent entre deux conditions en bulk RNA-seq. Il modélise les comptages bruts avec une loi binomiale négative et emprunte de l’information entre les gènes — bien plus fiable qu’un t-test gène par gène sur une poignée de réplicats.
Donnez-lui la matrice de comptage brute (jamais des valeurs pré-normalisées), une table d’échantillons et une formule de design. DESeq2 estime lui-même ses facteurs de taille, vous n’avez donc rien à normaliser à la main.
Le résultat comporte une ligne par gène : log2FoldChange (taille et direction de l’effet), pvalue et padj — la p-value corrigée par FDR à laquelle vous vous fiez réellement. Le seuil est padj < 0.05, pas pvalue.
log2FoldChange est le rapport en log₂ des deux conditions : +1 = 2× plus élevé, -1 = 2× plus faible. Le signe dépend du niveau choisi comme référence — fixez-le avec relevel() avant de tester.
Rétrécissez les fold changes (lfcShrink, apeglm) avant de classer ou de tracer, puis lisez le résultat avec un MA plot (effet vs expression — une vue de CQ) et un volcano plot (effet vs significativité — les hits).
Introduction
Vous disposez d’une expérience de bulk RNA-seq : des cellules traitées d’une façon ou d’une autre, chaque condition séquencée quelques fois, et une matrice de comptage où chaque ligne est un gène et chaque nombre indique combien de reads se sont alignés sur ce gène dans cet échantillon. La question est toujours la même — quels gènes ont changé, et de combien ?
C’est l’analyse d’expression différentielle, et DESeq2 en est l’outil standard. C’est le bon choix dès que vous avez des comptages bruts issus d’une comparaison planifiée (traité vs contrôle, mutant vs sauvage, tumeur vs normal) avec au moins deux réplicats par groupe. Ce guide vous mène de la matrice de comptage à une liste de gènes classée et annotée et au volcano plot que vous mettrez dans l’article — et, tout aussi important, vous montre comment lire chaque nombre que le test renvoie.
Nous utilisons le jeu de données airway : des cellules musculaires lisses des voies respiratoires provenant de quatre donneurs, chacune avec et sans dexamethasone (un anti-inflammatoire glucocorticoïde). Il est fourni sous forme de package Bioconductor, de sorte que toute l’analyse est reproductible sur votre machine.
Les données : une matrice de comptage et ses échantillons
airway se présente comme un RangedSummarizedExperiment — un conteneur qui réunit la matrice de comptage (assay) et la table par échantillon (colData). Chargez-le et examinez les deux.
library(airway)library(DESeq2)data("airway")airway # the container: genes x samples
# The sample table: `cell` = the four donor cell lines, `dex` = treated / untreated.as.data.frame(colData(airway)[, c("cell", "dex")])
cell dex
SRR1039508 N61311 untrt
SRR1039509 N61311 trt
SRR1039512 N052611 untrt
SRR1039513 N052611 trt
SRR1039516 N080611 untrt
SRR1039517 N080611 trt
SRR1039520 N061011 untrt
SRR1039521 N061011 trt
Huit échantillons : quatre lignées cellulaires, chacune apparaissant une fois traitée (trt) et une fois non traitée (untrt). Cette structure appariée a son importance — nous contrôlerons la lignée cellulaire dans le design pour que le test voie l’effet de la dexamethasone, et non les différences d’un donneur à l’autre.
# Raw counts — whole numbers, one row per gene. The top-left corner:head(assay(airway))[, 1:4]
DESeq2 travaille sur son propre objet, le DESeqDataSet, qui regroupe les comptages, la table d’échantillons et la formule de design. La formule énumère ce qui explique l’expression : ici, ~ cell + dex signifie « tenir compte de la lignée cellulaire, puis tester le traitement ». La variable d’intérêt se place en dernier.
dds <-DESeqDataSet(airway, design =~ cell + dex)# Set the reference level explicitly: compare treated AGAINST untreated.dds$dex <-relevel(dds$dex, ref ="untrt")
Définir la référence avec relevel() n’est pas une formalité optionnelle — cela décide de la direction de chaque fold change. Avec untrt comme référence, un log2FoldChange positif signifie « plus élevé chez les traités ».
Un filtre rapide élimine les gènes presque dépourvus de reads. Ils n’apportent aucune information et ne font qu’alourdir la charge des tests multiples.
# Keep genes with at least 10 reads in total across the 8 samples.keep <-rowSums(counts(dds)) >=10dds <- dds[keep, ]nrow(dds) # genes retained
[1] 22369
NotePourquoi DESeq2 et pas un t-test ?
Un t-test suppose des mesures à peu près normales à variance égale. Les comptages RNA-seq ne sont ni l’un ni l’autre : ce sont des nombres entiers, jamais négatifs, asymétriques, et plus variables pour les gènes fortement exprimés. DESeq2 ajuste un modèle de loi binomiale négative par gène — l’équivalent de la courbe en cloche pour les données de comptage — et laisse chaque gène avoir sa propre variabilité supplémentaire (dispersion), qu’il stabilise en empruntant de l’information à l’ensemble des gènes. C’est ce qui le rend fiable avec seulement quelques réplicats. Il normalise aussi en interne via un facteur de taille par échantillon (médiane des rapports), de sorte que des échantillons séquencés à des profondeurs différentes sont comparables sans aucune étape manuelle. Proches parents : edgeR (également binomiale négative, performant avec très peu de réplicats) et limma-voom (rapide sur de très grandes études) — voir la FAQ.
Lancer le test d’expression différentielle
DESeq() réalise tout l’ajustement en un seul appel — facteurs de taille, dispersions et test de Wald sur la loi binomiale négative. Ensuite, results() extrait la table du contraste qui vous intéresse.
out of 22369 with nonzero total read count
adjusted p-value < 0.1
LFC > 0 (up) : 2610, 12%
LFC < 0 (down) : 2224, 9.9%
outliers [1] : 0, 0%
low counts [2] : 4337, 19%
(mean count < 5)
[1] see 'cooksCutoff' argument of ?results
[2] see 'independentFiltering' argument of ?results
summary() donne l’essentiel : combien de gènes montent (up) et descendent (down) au seuil par défaut padj < 0.1, plus combien ont été écartés par le filtrage indépendant et la détection de valeurs aberrantes. Pour travailler au FDR conventionnel de 5 %, triez la table et conservez padj < 0.05.
res <- res[order(res$padj), ] # most significant firsthead(res, 5)
log2 fold change (MLE): dex trt vs untrt
Wald test p-value: dex trt vs untrt
DataFrame with 5 rows and 6 columns
baseMean log2FoldChange lfcSE stat pvalue
<numeric> <numeric> <numeric> <numeric> <numeric>
ENSG00000152583 997.445 4.57497 0.184241 24.8314 4.11067e-136
ENSG00000165995 495.096 3.29110 0.133053 24.7353 4.46338e-135
ENSG00000120129 3409.038 2.94785 0.121876 24.1872 3.03384e-129
ENSG00000101347 12703.413 3.76702 0.155992 24.1488 7.68266e-129
ENSG00000189221 2341.781 3.35365 0.142181 23.5872 5.21271e-123
padj
<numeric>
ENSG00000152583 7.41235e-132
ENSG00000165995 4.02419e-131
ENSG00000120129 1.82354e-125
ENSG00000101347 3.46334e-125
ENSG00000189221 1.87991e-119
Interpréter la sortie — chaque colonne
Cette table est tout l’objet de l’analyse : lisez-la colonne par colonne plutôt que de sauter directement à la p-value :
baseMean — le comptage normalisé moyen du gène sur l’ensemble des échantillons. Un niveau d’expression approximatif ; des valeurs très faibles signifient que l’estimation repose sur peu de données.
log2FoldChange — la taille de l’effet et la direction sur une échelle log₂. +1 = 2× plus élevé chez les traités, +2 = 4× plus élevé, -1 = 2× plus faible. Comme nous avons fixé untrt comme référence, positif = à la hausse avec la dexamethasone.
lfcSE — l’erreur standard de ce log2 fold change : la précision de l’estimation.
stat — la statistique de Wald, log2FoldChange / lfcSE. Forte amplitude → preuve solide.
pvalue — la p-value brute pour « ce gène ne change pas ».
padj — la p-value après correction pour les tests multiples (FDR de Benjamini–Hochberg). Avec ~18 000 gènes testés, la p-value brute produit forcément des milliers de faux positifs ; padj contrôle cela. C’est la colonne sur laquelle vous filtrez.
# Genes at 5% FDR, and how the change splits up vs down.sig <-subset(res, padj <0.05)nrow(sig)
[1] 4000
table(direction =ifelse(sig$log2FoldChange >0, "up in treated", "down in treated"))
direction
down in treated up in treated
1807 2193
Un signal nettement bilatéral — environ 2 200 gènes à la hausse et 1 800 à la baisse. Cet équilibre est ce à quoi ressemble une vraie réponse biologique ; un résultat tout dans une seule direction signale en général un problème de normalisation ou de design.
ImportantFiltrez sur padj, jamais sur la pvalue brute
Avec des milliers de gènes testés simultanément, la colonne pvalue brute listera de nombreux gènes comme « significatifs » par le seul effet du hasard. Filtrez toujours sur padj < 0.05 (FDR). Un gène avec pvalue = 0.001 mais padj = 0.4 n’est pas un hit — parmi des milliers de tests, un p de 0.001 n’a rien de remarquable.
Rétrécir les fold changes
Pour les gènes à faible comptage, le log2FoldChange brut est extrêmement incertain — un gène avec 3 reads dans un groupe et 1 dans l’autre affiche un énorme fold change qui ne signifie rien. lfcShrink() tire ces estimations bruitées vers zéro proportionnellement à leur incertitude, en laissant presque intacts les gènes bien mesurés. Rétrécissez toujours avant de classer les gènes ou de tracer un MA plot / volcano plot.
resultsNames(dds) # the coefficient name to shrink
resLFC <-lfcShrink(dds, coef ="dex_trt_vs_untrt", type ="apeglm")
coef = "dex_trt_vs_untrt" nomme l’effet traité-vs-non-traité (visible dans resultsNames()) ; l’estimateur de rétrécissement (shrinkage) apeglm est l’option par défaut actuelle de DESeq2 et celle à citer.
Faire correspondre les identifiants Ensembl aux symboles de gènes
airway étiquette les gènes par identifiant Ensembl (ENSG…), que personne ne lit d’un coup d’œil. Faites-les correspondre aux symboles de gènes avec le package d’annotation humaine, pour rendre les graphiques et les tables interprétables.
library(org.Hs.eg.db)resLFC$symbol <-mapIds(org.Hs.eg.db,keys =rownames(resLFC),column ="SYMBOL",keytype ="ENSEMBL",multiVals ="first")# The strongest hits, now readable:head(as.data.frame(resLFC[order(resLFC$padj), c("symbol", "log2FoldChange", "padj")]), 6)
Les hits les plus forts — menés par SPARCL1 et DUSP1, deux gènes répondant aux glucocorticoïdes bien documentés — constituent une vérification rassurante : le pipeline a trouvé de la vraie biologie, pas du bruit.
Visualiser : MA plot et volcano plot
Une table de 22 000 lignes est impossible à lire à l’œil nu ; les analyses RNA-seq la transforment donc en deux images standard.
MA plot — une vue de contrôle qualité
Le MA plot montre, pour chaque gène, son expression moyenne (x, échelle logarithmique) face à son log2 fold change rétréci (y), les gènes significatifs étant colorés et les plus forts étiquetés. C’est le premier graphique à tracer, car il révèle si le changement se comporte raisonnablement entre gènes à faible et à fort comptage — le nuage doit se resserrer étroitement vers zéro à gauche. Nous traçons la version prête à publier avec ggpubr::ggmaplot.
Chaque point est un gène, et le nuage se resserre vers zéro à faible expression — le signe d’un résultat sain, sans biais systématique. Les gènes colorés passent à la fois FDR < 5 % et un changement d’un facteur deux, une barre plus stricte que la seule significativité : sur les ~4 000 gènes significatifs au FDR, 454 sont à la hausse et 369 à la baisse à ce seuil de fold change, les plus forts (SPARCL1, CACNB2, DUSP1…) étant étiquetés. Ajouter un seuil de taille d’effet à la significativité est une pratique standard quand on veut une liste restreinte fiable.
Volcano plot — les hits
Le volcano plot est la figure que tout le monde reconnaît : le log2 fold change (x) face à la significativité statistique, -log10(padj) (y). Les gènes intéressants se trouvent dans les coins supérieurs — un grand changement et une petite p-value ajustée. Plutôt que de recourir à un package boîte noire, nous le construisons directement dans ggplot2 — une recette courte, prête à publier, qui vous appartient et que vous pouvez restyler pour n’importe quel article. Classez d’abord chaque gène selon les deux mêmes seuils que ci-dessus (FDR < 5 % et |log2FC| > 1) :
vol <-as.data.frame(resLFC)vol <- vol[!is.na(vol$padj), ]vol$status <-"Not sig."vol$status[vol$padj <0.05& vol$log2FoldChange >1] <-"Up"vol$status[vol$padj <0.05& vol$log2FoldChange <-1] <-"Down"# label the top 10 hits in EACH direction (so up and down are both shown)up <- vol[vol$status =="Up", ]down <- vol[vol$status =="Down", ]top <-rbind(head(up[order(up$padj), ], 10),head(down[order(down$padj), ], 10))table(vol$status)
Down Not sig. Up
369 17209 454
Traçons maintenant : des points colorés par statut, des lignes pointillées aux seuils, et les 10 gènes du sommet dans chaque direction étiquetés avec ggrepel pour que les noms ne se chevauchent jamais.
La plupart des gènes se situent près du centre (faible changement, non significatifs) ; les hits s’étalent dans les deux coins supérieurs — une réponse réelle, bilatérale. Les gènes en haut à droite (rouge) sont induits par la dexamethasone, ceux en haut à gauche (bleu azur) sont réprimés — les mêmes 454 à la hausse / 369 à la baisse signalés par le MA plot, désormais placés par significativité. Comme il s’agit de pur ggplot2, vous pouvez modifier les seuils, les couleurs ou les gènes étiquetés pour les adapter à votre propre figure.
NoteMA plot ou volcano plot — lequel utiliser ?
Tous deux placent le même fold change sur un axe mais l’associent à un second axe différent : ils répondent donc à des questions différentes — tracez les deux.
MA plot
Volcano plot
Axes
fold change vs expression moyenne
fold change vs significativité (−log10 padj)
Idéal pour
un coup d’œil CQ — le changement est-il cohérent entre gènes à faible et à fort comptage ?
repérer les top hits — grand changement et significatif — d’un coup d’œil
Angle mort
pas d’axe de significativité — vous ne voyez pas padj
masque le niveau d’expression — un grand fold change dans un gène à faible comptage peut sembler impressionnant mais peu fiable
Enregistrer les résultats pour les partager
Exportez la table annotée pour que vos collaborateurs puissent l’ouvrir dans Excel ou la transmettre à l’étape suivante (enrichissement de voies).
out <-as.data.frame(resLFC)out$ensembl <-rownames(out)write.csv(out, "deseq2_results.csv", row.names =FALSE)
Problèmes fréquents
Les fold changes pointent dans le mauvais sens. Le signe est fixé par le niveau de référence. Si des valeurs positives signifient « à la baisse chez les traités », c’est que vous avez oublié de relevel() le facteur (ou nommé le contraste à l’envers). Définissez relevel(dds$dex, ref = "untrt") avant DESeq() et relancez.
Beaucoup de NA dans padj. C’est attendu et intentionnel. DESeq2 fixe padj = NA pour les gènes qu’il écarte — ceux à très faible comptage (sans puissance) ou portant une valeur aberrante signalée. Ils sont retirés de la correction pour tests multiples afin de ne pas la diluer ; ce ne sont pas des erreurs.
lfcShrink échoue avec un coef inconnu. Le nom du coefficient doit correspondre exactement à resultsNames(dds). Après le relevel, l’effet traité-vs-non-traité est dex_trt_vs_untrt. Lancez resultsNames(dds) et copiez la chaîne.
Questions fréquentes
NoteQue signifie le log2 fold change dans DESeq2 ?
C’est le log₂ du rapport entre vos deux conditions, il se lit donc en puissances de deux : +1 = 2× plus élevé, +2 = 4× plus élevé, -1 = 2× plus faible. La direction du signe dépend du niveau de référence que vous fixez avec relevel() — avec untrt comme référence, une valeur positive signifie plus élevé dans le groupe traité.
NoteDois-je filtrer sur pvalue ou padj ?
Sur padj. Avec des milliers de gènes testés simultanément, la p-value brute produit de nombreux faux positifs par hasard. padj est la correction FDR de Benjamini–Hochberg ; padj < 0.05 maintient le taux de fausses découvertes attendu à 5 %. Une petite pvalue avec un grand padj n’est pas un vrai hit.
NoteDESeq2 ou edgeR — lequel choisir ?
Tous deux ajustent un modèle de loi binomiale négative aux comptages et s’accordent généralement sur les gènes forts. DESeq2 a des réglages par défaut prudents (filtrage indépendant, rétrécissement des fold changes) et est tolérant pour les designs typiques ; edgeR est souvent préféré avec très peu de réplicats et offre le test de quasi-vraisemblance flexible glmQLFit. Choisissez-en un, déclarez-le, et ne sélectionnez pas opportunément l’outil qui donne le résultat souhaité. limma-voom est une troisième option qui passe à l’échelle sur de très grandes études.
NoteDois-je donner à DESeq2 des comptages bruts ou des valeurs normalisées ?
Des comptages entiers bruts, toujours. DESeq2 estime lui-même sa normalisation (facteurs de taille) en interne et le modèle binomial négatif suppose des comptages. Ne lui donnez jamais de TPM, FPKM, RPKM, ni de valeurs log-transformées — cela casse les hypothèses du modèle.
NotePourquoi rétrécir les log2 fold changes ?
Les gènes à faible comptage produisent d’énormes estimations de fold change peu fiables. lfcShrink() tire les estimations bruitées vers zéro proportionnellement à leur incertitude tout en laissant tranquilles les gènes bien mesurés, de sorte que le classement et les volcano/MA plots reflètent des gènes auxquels vous pouvez réellement vous fier. Utilisez l’estimateur apeglm (l’option par défaut actuelle).
Testez vos connaissances
ImportantÀ votre tour : relancez la comparaison dans l’autre sens
À partir de l’objet dds de cette leçon, produisez une table de résultats où un log2FoldChangepositif signifie qu’un gène est plus élevé chez les non-traités (l’inverse de ce que nous avons fait). Vérifiez que le fold change du gène de tête a simplement changé de signe.
AstuceIndice
Vous n’avez pas besoin de réajuster le modèle. Soit vous changez l’ordre du contrast dans results(), soit vous appliquez relevel() au facteur pour faire de trt la référence et relancez DESeq().
AstuceSolution
Inversez simplement le contraste — pas besoin de réajuster :
Chaque log2FoldChange est l’opposé de l’original, et padj est inchangé — la preuve est la même, seule la direction choisie diffère.
NoteVérification rapide : que contrôle padj < 0.05 ?
A. La probabilité qu’un gène isolé soit un faux positif. B. La proportion attendue de faux positifs parmi les gènes que vous déclarez significatifs. C. La taille du fold change.
AstuceAfficher la réponse
B.padj est le taux de fausses découvertes (Benjamini–Hochberg). À padj < 0.05, environ 5 % des gènes que vous déclarez significatifs sont attendus comme faux positifs. Cela ne dit rien sur la taille de l’effet — c’est le log2FoldChange, distinct.
Conclusion
Vous avez mené une analyse d’expression différentielle de bout en bout : de la matrice de comptage airway à un DESeqDataSet avec un design ~ cell + dex, en passant par le test de Wald sur la loi binomiale négative, jusqu’à une table classée que vous pouvez lire colonne par colonne — log2FoldChange pour l’effet et la direction, padj pour le seuil. Vous avez rétréci les fold changes pour un classement honnête, fait correspondre les identifiants Ensembl aux symboles de gènes, et tracé les MA plot et volcano plot qui résument le résultat. Les règles pratiques : donnez-lui des comptages bruts, placez la variable d’intérêt en dernier dans le design et fixez son niveau de référence, et filtrez sur padj, pas pvalue.
L’étape naturelle suivante est de se demander ce que font ces gènes — en regroupant les hits en voies biologiques par analyse de sur-représentation et d’enrichissement d’ensembles de gènes.
Leçons connexes
Analyse RNA-seq bulk — la série complète, de la matrice de comptage à l’enrichissement de voies. · Bioinformatique — le pilier : RNA-seq, single-cell et workflows omiques reproductibles.
À suivre dans cette série : l’enrichissement de voies — transformer votre liste de gènes différentiellement exprimés en processus biologiques qu’ils affectent (ensembles de gènes GO, KEGG et Hallmark).
Cette page vous a-t-elle été utile ?
Merci pour votre retour !
Recevez les nouvelles leçons R & Python par e-mail
Pratique, reproductible, sans spam. Désinscription à tout moment.
Double opt-in. Nous ne partageons jamais votre e-mail.
Cette leçon est reproductible : chaque table, chaque statistique et chaque figure ont été produites par le code montré — copiez n’importe quel bloc et exécutez-le pour les reproduire. The runtime is the judge.
Références
Love, M. I., Huber, W., & Anders, S. (2014). Moderated estimation of fold change and dispersion for RNA-seq data with DESeq2. Genome Biology, 15(12), 550.
Zhu, A., Ibrahim, J. G., & Love, M. I. (2019). Heavy-tailed prior distributions for sequence count data: removing the noise and preserving large differences (apeglm). Bioinformatics, 35(12), 2084–2092.
Himes, B. E., et al. (2014). RNA-Seq transcriptome profiling identifies CRISPLD2 as a glucocorticoid responsive gene in airway smooth muscle cells (le jeu de données airway). PLoS ONE, 9(6), e99625.
Love, M. I., Anders, S., & Huber, W. Analyzing RNA-seq data with DESeq2 — la vignette Bioconductor.
@online{2026,
author = {},
title = {Analyse d’expression différentielle avec DESeq2 dans R : un
tutoriel pas à pas},
date = {2026-06-29},
url = {https://www.datanovia.com/learn/bioinformatics/bulk-rna-seq/differential-expression-deseq2},
langid = {fr}
}