projets
Mini-projet 2 / 4 · Master Biologie

Analyse d’expression génique (RNA-seq)

D’une matrice brute de comptages RNA-seq à une liste classée de gènes différentiellement exprimés, en huit étapes — en Python standard, sans pandas, sans scipy.

Pré-requis

Maîtrise des dicts, listes, fichiers et modules (ateliers 1-12). Notions de moyenne et de logarithme.

À la fin du projet

Un script rnaseq.py qui transforme un TSV de comptages en un rapport de DEGs avec heatmap intégrée.

Étape 1 sur 8

Charger la matrice de comptages

TSV avec gènes en lignes, échantillons en colonnes

Étape 1 / 8

Charger la matrice de comptages

★★
Contexte

Le RNA-seq donne, après alignement et comptage, une matrice de comptages : combien de reads se sont alignés sur chaque gène, pour chaque échantillon. C’est la donnée primaire de toute étude transcriptomique.

Voici un mini-jeu de données pédagogique : 10 gènes oncologiques, mesurés dans 3 contrôles (ctrl_1 à ctrl_3) et 3 traités (treat_1 à treat_3). Sauvegardez-le dans comptages.tsv :

gene	ctrl_1	ctrl_2	ctrl_3	treat_1	treat_2	treat_3
ENSG_BRCA1	820	790	850	1240	1180	1310
ENSG_TP53	450	480	470	920	880	950
ENSG_GAPDH	2100	2150	2080	2080	2120	2110
ENSG_MYC	320	310	340	130	120	140
ENSG_KRAS	180	170	190	60	70	80
ENSG_BAX	500	520	490	790	810	780
ENSG_BCL2	600	620	590	240	260	220
ENSG_CDK2	380	360	400	380	390	410
ENSG_RB1	290	310	280	295	300	305
ENSG_VEGFA	150	160	140	450	470	440
Travail à réaliser

Écrivez charger_comptages(chemin) qui renvoie :

  • la liste des noms d’échantillons (la ligne d’en-tête sans la colonne « gene »),
  • un dict {gene: [counts_par_echantillon]}.

Vérifiez en affichant la dimension de la matrice :

6 échantillons : ['ctrl_1', 'ctrl_2', 'ctrl_3', 'treat_1', 'treat_2', 'treat_3']
10 gènes chargés.
BRCA1 -> [820, 790, 850, 1240, 1180, 1310]
Étape 2 sur 8

Statistiques par gène

Somme, moyenne, min, max

Étape 2 / 8

Statistiques par gène

★★
Contexte

Premier regard sur les données : pour chaque gène, quel est son niveau d’expression moyen ? Est-il très variable d’un échantillon à l’autre ? Cette première inspection révèle déjà les gènes très exprimés (housekeeping comme GAPDH) et les gènes faiblement détectés (à filtrer plus tard).

Travail à réaliser

Écrivez stats_gene(comptages_gene) qui renvoie un dict :

{"total": int, "moyenne": float, "min": int, "max": int}

Puis tableau_stats(comptages) qui imprime toutes les lignes alignées :

Gène                total    moyenne    min     max
ENSG_BRCA1          6190     1031.7    790     1310
ENSG_TP53           4150      691.7    450     950
ENSG_GAPDH         12640     2106.7   2080     2150
...
Étape 3 sur 8

Normalisation CPM

Counts Per Million — la base de toute comparaison

Étape 3 / 8

Normalisation CPM

★★
Contexte

On ne peut pas comparer directement les comptages bruts entre deux échantillons. Pourquoi ? Parce que l’échantillon A peut avoir été séquencé deux fois plus profondément que B — son gène GAPDH aura mécaniquement deux fois plus de reads, sans changement biologique réel.

Solution la plus simple : normaliser par la taille de librairie (total de reads par échantillon), exprimée en CPM (Counts Per Million) :

CPM(gene, échantillon) = comptage_gene / total_échantillon * 1_000_000

(Méthode rudimentaire mais didactique. En vrai, on utilise TPM ou DESeq2/TMM pour des analyses sérieuses.)

Travail à réaliser

Deux fonctions :

  • tailles_librairies(comptages, echantillons) → renvoie une liste de longueur len(echantillons) contenant le total par échantillon.
  • normaliser_cpm(comptages, echantillons) → renvoie une matrice CPM, même structure que comptages, mais avec des float.
Étape 4 sur 8

Filtrer les gènes peu exprimés

Réduire le bruit avant toute analyse

Étape 4 / 8

Filtrer les gènes peu exprimés

★★
Contexte

Un gène détecté à 2-3 reads dans seulement un échantillon n’apporte qu’un bruit statistique. Avant le calcul du fold-change, on élimine les gènes faibles.

Règle classique (edgeR et consorts) : un gène est gardé s’il atteint au moins N CPM dans au moins M échantillons.

Travail à réaliser

Écrivez filtrer(cpm, seuil_cpm=1.0, min_echantillons=3) qui renvoie un nouveau dict ne contenant que les gènes qui respectent la règle.

Sur notre jeu, avec les valeurs par défaut, tous les gènes devraient passer (ils sont tous exprimés). Pour montrer le filtre, testez aussi avec seuil_cpm=10000 — quasiment plus rien ne passe.

Avant filtre  : 10 gènes
Après filtre  : 10 gènes (seuil_cpm=1.0)
Après filtre  :  1 gènes (seuil_cpm=10000)  -> housekeeping
Étape 5 sur 8

Calcul du log2 fold-change

Combien de fois plus exprimé en condition traitée ?

Étape 5 / 8

Calcul du log2 fold-change

★★
Contexte

Le fold-change mesure de combien un gène est surexprimé (ou sous-exprimé) entre deux conditions :

FC = moyenne(traité) / moyenne(contrôle)

On prend ensuite le log2 : log2(2) = 1 (2× plus), log2(4) = 2 (4× plus), log2(0.5) = −1 (2× moins). Cette transformation rend la distribution symétrique autour de 0.

Astuce numérique : on ajoute un pseudo-comptage (+1) pour éviter log2(0).

Travail à réaliser

Écrivez log2_fc(cpm, echantillons, groupe_ctrl, groupe_treat) qui renvoie un dict {gene: log2FC}.

groupe_ctrl et groupe_treat sont des listes de noms d’échantillons (sous-ensemble de echantillons).

log2FC pour quelques gènes :
  BRCA1   :  +0.59  (un peu surexprimé)
  MYC     :  -1.36  (réprimé ~ 2.6x)
  GAPDH   :  -0.02  (inchangé)
  VEGFA   :  +1.59  (surexprimé ~ 3x)
Étape 6 sur 8

Identifier les DEGs

Gènes différentiellement exprimés (|log2FC| ≥ 1)

Étape 6 / 8

Identifier les DEGs

★★
Contexte

Un DEG (Differentially Expressed Gene) est, dans notre version simplifiée, un gène dont |log2FC| ≥ 1 — soit au moins 2× de différence entre les deux conditions.

Une vraie analyse ajouterait un test statistique (t-test, Wald via DESeq2) puis une correction multiple (Benjamini-Hochberg FDR). On reste sur le critère du fold-change pour rester en Python pur.

Travail à réaliser

Écrivez trouver_degs(fc, seuil=1.0) qui renvoie deux listes triées (décroissant en magnitude) : (up_regules, down_regules). Chaque entrée est un tuple (gene, log2fc).

Affichez un compte-rendu :

DEGs surexprimés (log2FC >= +1) :
  VEGFA   :  +1.59  (3.0x)
  TP53    :  +1.00  (2.0x)
DEGs sous-exprimés (log2FC <= -1) :
  KRAS    :  -1.45  (0.37x)
  MYC     :  -1.36  (0.39x)
  BCL2    :  -1.31  (0.40x)
Étape 7 sur 8

Heatmap en ASCII

Visualiser sans matplotlib

Étape 7 / 8

Heatmap en ASCII

★★
Contexte

Une heatmap est l’outil de visualisation classique d’une analyse d’expression : chaque ligne = un gène, chaque colonne = un échantillon, intensité de couleur = niveau d’expression. On peut faire une version terminal-only avec des blocs Unicode — utile sur un serveur sans interface graphique.

Avant l’affichage, on z-scale par ligne : chaque gène est centré-réduit, ce qui rend visible le motif de variation indépendamment du niveau absolu d’expression.

Travail à réaliser

Écrivez heatmap_ascii(cpm, echantillons, genes=None, largeur=20) qui imprime une heatmap : pour chaque gène, normaliser entre 0 et 1 (sur la ligne), puis afficher int(v * largeur) blocs.

Si genes est None, tracer tous les gènes ; sinon seulement la sous-liste.

Heatmap (largeur 20, normalisée par gène)

              ctrl_1   ctrl_2   ctrl_3   treat_1  treat_2  treat_3
BRCA1         █▁       ▁        ███      █████████████████   ████████
MYC           ████████ ███████  ████████ ▁        ▁        ▁
...

(la sortie réelle utilisera des espaces et selon la valeur normalisée).

Étape 8 sur 8

Pipeline CLI complet

rnaseq.py : matrice → rapport

Étape 8 / 8

Pipeline CLI complet

★★★
Contexte

On a sept briques. Reste à les assembler en un outil en ligne de commande qui prend une matrice de comptages, deux listes d’échantillons (contrôle et traité), et qui produit un rapport complet.

Travail à réaliser

Rassemblez vos fonctions dans rnaseq.py. main() doit :

  1. lire les arguments : chemin TSV + groupes (par ex. ctrl_1,ctrl_2,ctrl_3 et treat_1,treat_2,treat_3),
  2. charger, normaliser, filtrer,
  3. calculer log2FC + DEGs,
  4. afficher le rapport : taille de librairie, top up/down, heatmap des DEGs.

Lancement attendu :

$ python rnaseq.py comptages.tsv ctrl_1,ctrl_2,ctrl_3 treat_1,treat_2,treat_3