Menu

Module 6 · Section 6.3

Normalisation

55 min · niveau intermediaire · dernière révision : 2026-11-01

Objectifs d'apprentissage

  • Distinguer les biais de profondeur, de longueur et de composition d'une librairie RNA-Seq
  • Calculer à la main des CPM et des TPM sur un petit tableau de comptages
  • Expliquer la construction et les hypothèses des size factors de DESeq2
  • Choisir une transformation adaptée à la visualisation ou au test différentiel
  • Implémenter et tester les normalisations usuelles en Python

Prérequis : 6.2 featureCounts

DESeq2 1.38+Python 3.11+

Prérequis

Cette section suppose que vous savez compter les lectures par gène et construire une matrice gènes × échantillons, comme l'a montré la section 6.2. Elle utilise les proportions, les moyennes géométriques et le logarithme en base 2. Le vocabulaire des formats de comptage est rappelé dans le module 6. Aucune connaissance préalable de DESeq2 n'est nécessaire : nous construisons d'abord les normalisations à la main, puis nous les relions aux modèles statistiques.

Introduction

Une expérience de séquençage d'ARN ne mesure pas directement la quantité de chaque transcrit. Elle produit des lectures, et le comptage associe à chaque gène le nombre de lectures alignées sur ses exons. Ce nombre dépend de l'abondance du transcrit, mais aussi de la profondeur, de la longueur du gène, de la composition de la librairie et de détails techniques. Comparer deux comptages bruts revient donc à comparer des nombres exprimés dans des unités différentes. Un gène deux fois plus long capte environ deux fois plus de fragments à abondance égale ; un échantillon séquencé deux fois plus profondément produit environ deux fois plus de lectures pour tous les gènes. Sans correction, une variation technique peut dépasser une variation biologique.

L'enjeu biologique est direct. Entre condition traitée et contrôle, on veut savoir quels transcrits changent réellement, et de combien. Une normalisation mal choisie peut créer des faux positifs, masquer des réponses modestes ou inverser le signe d'un effet. L'enjeu computationnel est de distinguer ce qui relève du modèle statistique de ce qui relève de la mise à l'échelle. Les CPM et les TPM décrivent et visualisent ; les size factors de DESeq2 servent à estimer des paramètres dans un modèle de comptage. Confondre les deux usages est une erreur fréquente.

Nous partons des trois biais, puis nous construisons chaque normalisation jusqu'aux tests différentiels.

Théorie

Trois biais à corriger

Le comptage cijc_{ij} du gène ii dans l'échantillon jj dépend de trois facteurs. La profondeur DjD_j est le nombre total de lectures. La longueur LiL_i, ou longueur effective des exons, détermine le nombre de fragments alignables. La composition de la librairie décrit la part occupée par les gènes très exprimés. On résume :

cij  ∝  Dj×Li×qij×bj,c_{ij} \;\propto\; D_j \times L_i \times q_{ij} \times b_j,

où qijq_{ij} est l'abondance relative et bjb_j un facteur de composition. Les normalisations cherchent à retirer DjD_j, LiL_i et bjb_j ; aucune ne le fait parfaitement, car chacune repose sur une hypothèse.

La profondeur est observable : diviser par le total suffit. La longueur se corrige avec l'annotation, mais sa définition est délicate : union des exons, transcrit le plus long, longueur effective après fragmentation. La composition est la plus difficile, car elle dépend de l'expression, précisément ce que l'on mesure. Les méthodes diffèrent donc surtout par leur estimation de bjb_j.

CPM : corriger la profondeur

La normalisation en CPM (counts per million) divise chaque comptage par le total de l'échantillon, puis multiplie par un million :

CPMij=cij∑k=1nckj×106.\mathrm{CPM}_{ij} = \frac{c_{ij}}{\sum_{k=1}^{n} c_{kj}} \times 10^6.

La somme des CPM d'un échantillon vaut 10610^6. Les rapports entre gènes d'un même échantillon sont préservés et les profondeurs deviennent comparables. Les CPM ne corrigent ni la longueur ni la composition : un gène long reste favorisé, et une hausse massive de quelques gènes déprime les autres. Ils restent utiles pour des contrôles rapides et des filtres, mais ne sont pas une entrée pour un test fondé sur un modèle de comptage.

RPKM et FPKM : ajouter la longueur

La normalisation RPKM (reads per kilobase per million mapped reads) corrige la profondeur et la longueur. Pour des lectures simples :

RPKMij=cij(Li/103)×(∑kckj/106)=cij×109Li∑kckj.\mathrm{RPKM}_{ij} = \frac{c_{ij}}{(L_i / 10^3) \times \left(\sum_{k} c_{kj} / 10^6\right)} = \frac{c_{ij} \times 10^9}{L_i \sum_{k} c_{kj}}.

La normalisation FPKM (fragments per kilobase per million mapped fragments) est l'équivalent pour des lectures appariées : chaque fragment compte une fois. La formule est identique, l'unité de comptage change. Le facteur 10910^9 combine kilobases et millions.

RPKM et FPKM corrigent deux biais sur trois. Leur défaut est que la somme des valeurs d'un échantillon n'est pas fixe : elle dépend de la profondeur, de la longueur moyenne et de la composition. Comparer des RPKM entre échantillons revient donc à comparer des unités dont l'échelle change avec la librairie.

TPM : normaliser par la longueur d'abord

La normalisation TPM (transcripts per million) inverse l'ordre des opérations. On calcule un taux de lectures par unité de longueur, puis on normalise ces taux à un million :

rij=cijLi,TPMij=rij∑k=1nrkj×106.r_{ij} = \frac{c_{ij}}{L_i}, \qquad \mathrm{TPM}_{ij} = \frac{r_{ij}}{\sum_{k=1}^{n} r_{kj}} \times 10^6.

Chaque échantillon a une somme de TPM égale à 10610^6. Un TPM de 50 00050\,000 signifie que le gène occupe 5 % du pool normalisé par la longueur. Les TPM corrigent la profondeur et la longueur, mais pas la composition : si quelques gènes monopolisent les lectures, leur part augmente et celle des autres diminue.

Différence entre TPM et FPKM

La différence entre TPM et FPKM n'est pas une simple convention. Les FPKM divisent par le total des comptages, puis par la longueur ; les TPM divisent par la longueur, puis par la somme des taux. Les FPKM normalisent d'abord par la profondeur observée, les TPM d'abord par la longueur.

Prenons deux gènes de 1 000 pb et 2 000 pb, avec 10 lectures chacun dans un échantillon de 20 lectures. Les FPKM valent 500 000500\,000 et 250 000250\,000, pour une somme de 750 000750\,000. Les taux valent 0,010{,}01 et 0,0050{,}005 ; les TPM valent 666 667666\,667 et 333 333333\,333, pour une somme de 10610^6. Le rapport entre gènes est le même, mais l'échelle globale diffère. Si la longueur moyenne varie d'un échantillon à l'autre, la somme des FPKM change, tandis que celle des TPM reste fixe : d'où la préférence pour les TPM dans les comparaisons de profils.

La médiane des rapports

La médiane des rapports (median of ratios), utilisée par DESeq2, estime un facteur d'échelle par échantillon à partir de tous les gènes. L'idée : un échantillon plus profond produit des comptages plus grands pour la plupart des gènes, et quelques gènes différentiels ne doivent pas faire dévier l'estimation.

Pour chaque gène ii, on calcule la moyenne géométrique sur les mm échantillons :

gi=(∏j=1mcij)1/m.g_i = \left(\prod_{j=1}^{m} c_{ij}\right)^{1/m}.

Cette valeur sert de pseudo-échantillon de référence. Pour chaque échantillon jj, on prend la médiane des rapports :

sj=mediane⁡i(cijgi).s_j = \operatorname{mediane}_{i} \left(\frac{c_{ij}}{g_i}\right).

Le facteur sjs_j est le size factor. Les comptages normalisés valent cij/sjc_{ij}/s_j. Un facteur de 1,2 signifie que les comptages médians sont 20 % plus élevés que dans la référence. Ce n'est pas la profondeur totale : deux échantillons de même profondeur peuvent avoir des size factors différents si leur composition diffère.

La méthode suppose que la majorité des gènes ne sont pas différentiellement exprimés, ou que les changements se compensent. La médiane résiste à une minorité de gènes fortement régulés. Les gènes avec un comptage nul sont écartés, car leur moyenne géométrique est nulle. Si aucun gène n'est positif partout, les facteurs ne peuvent pas être estimés.

Biais de composition

Le biais de composition est le piège le plus subtil. Imaginons un gène représentant 70 % des lectures. S'il double dans la condition traitée, la profondeur totale double presque, et les CPM des gènes inchangés diminuent d'environ moitié. Une analyse naïve conclurait à une répression globale, alors que seule une minorité de gènes a changé. La médiane des rapports corrige ce cas si la majorité des gènes reste stable : le size factor augmente et les comptages inchangés sont ramenés à leur niveau attendu.

Le biais devient problématique lorsque le changement est réellement global, par exemple lors d'une activation massive ou d'une infection virale. Aucune normalisation fondée sur les gènes endogènes ne peut alors distinguer une hausse générale d'une hausse de profondeur. Les spike-in, qui ajoutent une quantité connue d'ARN exogène, apportent une référence externe. Sans eux, les facteurs d'échelle restent relatifs.

Normalisation et gènes différentiellement exprimés

La normalisation fait partie du modèle statistique. DESeq2 ajuste un modèle binomial négatif sur les comptages bruts, avec un offset construit à partir des size factors. Les comptages ne sont pas remplacés par des valeurs normalisées : le modèle utilise leur variance et corrige l'échelle par l'offset. edgeR procède de façon analogue avec la normalisation TMM (trimmed mean of M-values), robuste aux gènes les plus variables.

La normalisation influence les log fold changes, surtout pour les gènes faiblement exprimés. Un gène à quelques lectures peut varier d'un facteur deux par simple bruit de comptage. Il faut donc filtrer les gènes très faiblement exprimés, avec un seuil indépendant de la condition, et conserver des réplicats biologiques. Une normalisation corrige une échelle, pas la variabilité biologique.

Transformation logarithmique

Les comptages et les CPM sont asymétriques : quelques gènes très exprimés dominent l'échelle, et la variance augmente avec la moyenne. La transformation log⁡2(CPM+1)\log_2(\mathrm{CPM}+1) comprime les grandes valeurs, stabilise approximativement la variance et rend les différences multiplicatives additives. Un rapport de 2 devient une différence de 1, ce qui facilite la lecture des heatmaps. Le pseudocomptage de 1 évite le logarithme de zéro, mais son choix n'est pas neutre : il modifie les gènes faiblement exprimés et peut créer des écarts artificiels.

Pour la visualisation, DESeq2 propose VST (variance stabilizing transformation) et rlog, qui stabilisent la variance sans pseudocomptage arbitraire. Pour les tests, on préfère le modèle de comptage.

Limite des TPM pour les tests différentiels

Les TPM sont des proportions : leur somme est contrainte à 10610^6, donc une hausse d'un gène implique mécaniquement une baisse des autres. Cette contrainte viole l'indépendance des observations et déforme la relation moyenne-variance. Les tests conçus pour des comptages, comme le test exact de edgeR ou le modèle binomial négatif de DESeq2, attendent des nombres de lectures et une variance qui dépend de la moyenne. Leur appliquer des TPM revient à ignorer l'incertitude de comptage.

Les TPM restent excellents pour décrire un échantillon, comparer des profils, construire une heatmap ou vérifier des réplicats. Pour tester un changement, on utilise les comptages bruts avec une normalisation adaptée : size factors de DESeq2, TMM de edgeR, ou log-CPM avec poids de précision dans limma-voom. Cette dernière méthode transforme les comptages mais conserve une modélisation de la variance, ce qui la distingue d'un test naïf sur des TPM.

Exemple travaillé

Prenons quatre gènes et deux échantillons.

GèneS1S2Longueur (pb)
g110201 000
g220202 000
g33010500
g440501 000

Les deux échantillons totalisent 100 lectures. Les CPM valent donc les comptages multipliés par 10410^4 : pour S1, 100 000, 200 000, 300 000 et 400 000 ; pour S2, 200 000, 200 000, 100 000 et 500 000.

Pour les TPM, les taux cij/Lic_{ij}/L_i valent dans S1 0,0100{,}010, 0,0100{,}010, 0,0600{,}060 et 0,0400{,}040, de somme 0,1200{,}120, soit 83 33383\,333, 83 33383\,333, 500 000500\,000 et 333 333333\,333 TPM. Dans S2, ils valent 0,0200{,}020, 0,0100{,}010, 0,0200{,}020 et 0,0500{,}050, de somme 0,1000{,}100, soit 200 000200\,000, 100 000100\,000, 200 000200\,000 et 500 000500\,000. Chaque échantillon somme à 10610^6.

Les size factors utilisent les moyennes géométriques : g1=14,14g_1 = 14{,}14, g2=20g_2 = 20, g3=17,32g_3 = 17{,}32, g4=44,72g_4 = 44{,}72. Les rapports de S1 valent 0,7070{,}707, 1,0001{,}000, 1,7321{,}732 et 0,8940{,}894, de médiane 0,9470{,}947. Pour S2, ils valent 1,4141{,}414, 1,0001{,}000, 0,5770{,}577 et 1,1181{,}118, de médiane 1,0591{,}059. S2 est donc environ 12 % plus « profond » que S1 au sens de la médiane, alors que les totaux sont identiques. Après division, g1 passe de 10 à 10,56 dans S1 et de 20 à 18,89 dans S2 : la hausse reste réelle mais réduite. g2, inchangé en comptages, passe de 21,12 à 18,89 et paraît légèrement réprimé. Les trois normalisations ne répondent pas à la même question.

Implémentation

Le code ci-dessous reprend les fonctions de l'exercice et les applique au jeu de l'étude de cas. Il n'utilise que la bibliothèque standard, ce qui permet de l'exécuter dans le navigateur et de vérifier chaque étape.

CPM, TPM et médiane des rapports
SortieCtrl+Entrée pour exécuter · 10 s max
Aucune sortie. Exécutez le code pour voir le résultat.

Le code s'exécute dans votre navigateur (Web Worker isolé). Il n'a pas accès au serveur ; l'accès réseau est désactivé pendant l'exécution. Le premier clic sur « Exécuter » télécharge Pyodide depuis jsDelivr (plusieurs mégaoctets, version 0.26.4 épinglée) ; rien n'est chargé avant ce clic.

La sortie vérifiée le 2026-11-01 est :

[0.9771, 1.0827, 0.999, 0.9502]
[261937.2, 130968.6, 61391.5, 545702.6]
[167832.2, 111888.1, 20979.0, 699300.7]

Trois points méritent attention. L'orientation de la matrice, d'abord : median_of_ratios attend les gènes en lignes et les échantillons en colonnes ; une transposition silencieuse produit des facteurs plausibles mais faux. Les zéros, ensuite : un gène sans lecture est exclu, car sa moyenne géométrique est nulle ; si trop de gènes sont concernés, le facteur devient instable. La validation, enfin : les fonctions lèvent ValueError plutôt que de renvoyer NaN ou l'infini, ce qui évite de propager des erreurs silencieuses.

La complexité est linéaire en nombre de cellules pour les CPM et les TPM, soit O(nm)O(nm) pour nn gènes et mm échantillons. La médiane des rapports trie nn rapports par échantillon, soit O(nmlog⁡n)O(nm \log n). Ces coûts sont négligeables devant l'alignement et le comptage.

Étude de cas

Nous appliquons ces calculs au fichier datasets/rnaseq/counts.tsv, un jeu synthétique créé pour le cours. Il contient quatre gènes et quatre échantillons. Les longueurs ne figurent pas dans le fichier ; nous utilisons les valeurs synthétiques du tableau, comme on utiliserait les longueurs d'exons issues d'une annotation.

GèneLongueur (pb)S1S2S3S4
geneA1 5001201304045
geneB2 0008075220210
geneC80015181614
geneD3 000500510490505

La question est simple : les variations observées traduisent-elles un changement d'expression, ou sont-elles produites par la profondeur, la longueur et la composition ? Les totaux valent 715, 733, 766 et 774 lectures. La profondeur est presque constante, ce qui permet d'isoler les autres effets. Pourtant, la composition change : geneA domine dans S1 et S2, geneB domine dans S3 et S4, et geneD représente à lui seul 65 à 70 % de chaque librairie.

Les CPM de geneA passent de 167 832 à 58 140 entre S1 et S4, soit une baisse d'un facteur 2,9, contre 2,7 pour les comptages bruts. Pour geneB, les CPM passent de 111 888 à 287 206, soit une hausse d'un facteur 2,6, contre 2,75 pour les comptages. Les CPM corrigent donc la profondeur, mais ne changent pas l'interprétation de fond : geneA baisse et geneB monte. Ils ne corrigent pas la longueur, et geneD, long, reste surreprésenté.

Les TPM modifient la lecture. Avec les longueurs indiquées, geneD passe de 545 703 à 524 675 TPM entre S1 et S4, soit environ 52 à 55 % du pool, au lieu de 65 à 70 % des comptages. geneB atteint 343 750 TPM dans S3, et geneA tombe à 83 333 TPM dans le même échantillon. La correction de longueur pénalise geneD, long, et favorise geneC, court, dont les TPM restent autour de 55 000 à 71 000 malgré des comptages modestes. Chaque échantillon somme à 10610^6 TPM.

Les size factors calculés par la médiane des rapports valent 0,977, 1,083, 0,999 et 0,950. S2 possède le facteur le plus élevé, alors que son total (733) n'est que le deuxième. S3, dont le total est le plus élevé (766), a un facteur proche de 1 : geneD y est stable et geneB a augmenté, mais les gènes médians n'ont pas changé d'échelle. Ces facteurs ne mesurent donc pas la profondeur ; ils mesurent le décalage médian des comptages. Après division, geneA vaut 122,8, 120,1, 40,0 et 47,4 : la hausse apparente de S2 par rapport à S1 disparaît, car S2 est légèrement plus profond au sens de la médiane. Ces valeurs sont des sorties vérifiées le 2026-11-01 sur le fichier synthétique.

L'interprétation biologique doit rester prudente. Le jeu est synthétique et ne contient que quatre gènes ; les size factors y sont instables par construction, car la médiane porte sur quatre valeurs. Dans une vraie analyse, la médiane porte sur des milliers de gènes et devient robuste. La leçon reste valable : les CPM décrivent la part de lectures, les TPM la part du pool corrigé par la longueur, et les size factors l'échelle relative d'un modèle de comptage.

Erreurs fréquentes

Quiz

  1. Question 1.Quelle normalisation corrige la profondeur et la longueur des gènes, tout en garantissant une somme de 1 000 000 par échantillon ?
  2. Question 2.Quelle est la différence essentielle entre TPM et FPKM ?
  3. Question 3.Sur quelle hypothèse repose la médiane des rapports de DESeq2 ?
  4. Question 4.Quelle entrée la documentation de DESeq2 recommande-t-elle pour l'analyse différentielle ?
  5. Question 5.Pourquoi les TPM sont-ils problématiques pour un test différentiel ?
Répondez à toutes les questions pour valider.

Exercices

Exercice guidé6.3-e1

Énoncé

Implémenter cpm(counts), tpm(counts, lengths) et log2_cpm(counts) en Python pur. cpm et tpm renvoient des listes de flottants dont la somme vaut 10610^6, et lèvent ValueError si les comptages sont négatifs, si la somme est nulle, si les longueurs n'ont pas la bonne taille ou si une longueur est nulle ou négative. log2_cpm renvoie log⁡2(CPM+1)\log_2(\mathrm{CPM}+1). Vérifier les valeurs sur l'exemple du cours et sur counts.tsv.

Comment vérifier votre travail

Les tests se trouvent dans exercises/module-6/6.3-normalisation/test_6_3.py. Depuis la racine du dépôt :

python -m pytest exercises/module-6/6.3-normalisation -q

Corrigé verrouillé

Le corrigé est chargé uniquement après confirmation. Il n'est pas présent dans la page avant cette action.

Exercice approfondi6.3-e2

Énoncé

Implémenter median_of_ratios(counts_matrix) pour une matrice gènes × échantillons. La fonction valide la rectangularité et les valeurs, calcule la moyenne géométrique de chaque gène, puis la médiane des rapports pour chaque échantillon. Les gènes dont un comptage est nul sont exclus. Lever ValueError si aucun gène n'a de comptage positif partout. Comparer ensuite, sur counts.tsv, les facteurs obtenus à une normalisation naïve par le total, et expliquer pourquoi les deux classements des échantillons diffèrent.

Comment vérifier votre travail

Les tests se trouvent dans exercises/module-6/6.3-normalisation/test_6_3.py. Depuis la racine du dépôt :

python -m pytest exercises/module-6/6.3-normalisation -q

Corrigé verrouillé

Le corrigé est chargé uniquement après confirmation. Il n'est pas présent dans la page avant cette action.

Mini-projet6.3-projet

Énoncé

Mini-projet : rapport de normalisation. Implémenter normalization_report(counts, lengths) qui reçoit une matrice gènes × échantillons et les longueurs associées. La fonction valide les entrées, calcule les tailles de librairie, les matrices CPM et TPM, les size factors, la somme des TPM par échantillon, le gène dominant selon chaque normalisation et le nombre de changements de rang. Elle renvoie un dictionnaire documenté dans le README du projet. Tester les cas limites : matrice vide, non rectangulaire, longueurs incohérentes, comptages négatifs, échantillon de somme nulle et absence de gène utilisable pour la médiane des rapports.

Comment vérifier votre travail

Les tests se trouvent dans exercises/module-6/6.3-normalisation/projet/test_projet_6_3.py. Depuis la racine du dépôt :

python -m pytest exercises/module-6/6.3-normalisation/projet -q

Corrigé verrouillé

Le corrigé est chargé uniquement après confirmation. Il n'est pas présent dans la page avant cette action.

Résumé

  • Les comptages bruts dépendent de la profondeur, de la longueur des gènes et de la composition de la librairie.
  • Les CPM corrigent la profondeur ; les RPKM et FPKM ajoutent la longueur, mais leur somme n'est pas fixe.
  • Les TPM normalisent par la longueur puis par la somme des taux, et totalisent 10610^6 par échantillon.
  • La médiane des rapports estime un size factor par échantillon en supposant que la majorité des gènes ne changent pas.
  • Le biais de composition peut faire apparaître une répression globale lorsqu'un petit nombre de gènes domine la librairie.
  • Les tests différentiels utilisent les comptages bruts avec un modèle adapté, pas les TPM.
  • La transformation log⁡2(CPM+1)\log_2(\mathrm{CPM}+1) sert à visualiser ; VST et rlog sont préférés pour stabiliser la variance.
  • Une normalisation corrige une échelle, pas la variabilité biologique : les réplicats restent indispensables.

Références

  • Mortazavi, A., Williams, B. A., McCue, K., Schaeffer, L. et Wold, B. (2008). Mapping and quantifying mammalian transcriptomes by RNA-Seq. Nature Methods, 5(7), 621–628.
  • Robinson, M. D. et Oshlack, A. (2010). A scaling normalization method for differential expression analysis of RNA-seq data. Genome Biology, 11(3), R25.
  • Love, M. I., Huber, W. et Anders, S. (2014). Moderated estimation of fold change and dispersion for RNA-seq data with DESeq2. Genome Biology, 15(12), 550.
  • Liao, Y., Smyth, G. K. et Shi, W. (2014). featureCounts: an efficient general purpose program for assigning sequence reads to genomic features. Bioinformatics, 30(7), 923–930.
  • DESeq2. Analyzing RNA-seq data with DESeq2, vignette et documentation officielle. https://bioconductor.org/packages/DESeq2
  • Bioconductor. DESeq2 reference manual. https://bioconductor.org/packages/release/bioc/manuals/DESeq2/man/DESeq2.pdf