Intégration Seurat : corriger l’effet de lot scRNA-seq (v5)

Divisez les échantillons en couches, intégrez-les avec Harmony via l’API IntegrateLayers de Seurat v5, et confirmez par une vérification quantitative que les lots se mélangent tandis que les types cellulaires restent distincts

Un guide pratique pour intégrer plusieurs échantillons single-cell RNA-seq et corriger les effets de lot en R avec Seurat v5. En partant de deux conditions dont les UMAP se séparent par échantillon plutôt que par type cellulaire, divisez l’assay RNA en couches par lot, exécutez Harmony via l’API IntegrateLayers() (avec RPCA et CCA comme alternatives interchangeables), regroupez sur l’embedding intégré, et vérifiez la correction quantitativement — les lots se mélangent, les types cellulaires partagés s’alignent, la biologie est préservée.

Date de publication

3 juillet 2026

Modifié

12 juillet 2026

AstucePoints clés
  • Lorsque vous profilez plusieurs échantillons — traité vs contrôle, ou plusieurs donneurs — chaque échantillon est un lot (batch), et les différences techniques entre lots peuvent dominer la biologie. Le symptôme est sans équivoque : l’UMAP se sépare par échantillon plutôt que par type cellulaire. C’est un effet de lot, et l’intégration le supprime.
  • Seurat v5 divise les échantillons en couches (obj[["RNA"]] <- split(obj[["RNA"]], f = obj$sample)), puis les corrige avec un seul appel : IntegrateLayers(obj, method = HarmonyIntegration, ...). C’est l’API moderne — elle remplace l’ancien schéma v4 FindIntegrationAnchors() / IntegrateData().
  • Harmony est le cheval de bataille : rapide, extensible, et ce que la plupart des pipelines multi-échantillons publiés exécutent. Remplacez par RPCAIntegration ou CCAIntegration en changeant un seul argument — même appel, méthode différente.
  • La figure signature est l’UMAP avant/après : des lots qui formaient deux nuages séparés deviennent une seule carte entremêlée, tandis que les îlots distincts de types cellulaires restent distincts. L’intégration corrige la variation parasite, pas la biologie.
  • Vérifiez toujours la correction avec un chiffre, pas seulement avec vos yeux. Une simple métrique de mélange des lots par regroupement (ou une silhouette de lot) confirme que les échantillons partagent désormais chaque regroupement ; une métrique scIB formelle (iLISI, kBET) est la version de qualité publication.

Introduction

Vous avez exécuté le workflow single-cell sur un échantillon et cela a fonctionné. Vous en avez maintenant deux : du sang périphérique profilé au repos (contrôle) et après stimulation à l’interféron-β (IFN-β). Vous les traitez ensemble, exécutez le même pipeline PCA → UMAP, colorez l’UMAP par échantillon — et les deux échantillons siègent dans deux nuages séparés qui se touchent à peine. C’est le moment que rencontre tout projet multi-échantillons, et ce n’est pas de la biologie : c’est un effet de lot, la variation technique entre des échantillons capturés séparément (jour de traitement, piste, lot de réactifs différents, ou — comme ici — un décalage d’expression à l’échelle du traitement) qui écarte sur la carte des types cellulaires par ailleurs identiques.

Laissé non corrigé, il ruine l’analyse. Le clustering découpe chaque type cellulaire en une « version contrôle » et une « version stimulée », de sorte que vous ne pouvez pas comparer la même population entre conditions — ce qui est tout l’intérêt d’ une expérience multi-échantillons. L’intégration corrige cela : elle aligne les types cellulaires partagés entre échantillons afin qu’un monocyte CD14+ de l’échantillon contrôle et un monocyte CD14+ de l’échantillon stimulé se retrouvent dans le même regroupement, tandis que des populations réellement différentes restent à part.

Cette leçon le fait à la manière Seurat v5. Vous diviserez les échantillons en couches, exécuterez Harmony via l’API en un appel IntegrateLayers(), tracerez l’UMAP avant/après qui montre la correction, et — l’étape que les gens sautent — vérifierez quantitativement que les lots se sont réellement mélangés et que les types cellulaires sont restés distincts. Nous travaillons sur un sous-ensemble du jeu de données canonique ifnb (PBMC stimulées à l’IFN-β vs contrôle), afin que le workflow se transfère directement à vos propres données multi-échantillons.

Les données : deux échantillons, un objet

Le ifnb_subset.rds commité est une petite tranche du benchmark canonique ifnb : 3 000 PBMC (cellules mononucléées du sang périphérique), 1 500 de l’échantillon contrôle et 1 500 de l’échantillon stimulé à l’IFN-β, stockées comme une matrice de comptage brute plus une table de métadonnées. La colonne stim est le lot (CTRL / STIM) ; la colonne seurat_annotations contient des étiquettes de types cellulaires publiées que nous utilisons uniquement pour vérifier l’intégration, jamais pour la piloter.

library(Seurat)

# A 2-condition subset of the canonical ifnb benchmark: raw counts + metadata (stim, seurat_annotations)
d <- readRDS("_data/ifnb_subset.rds")
obj <- CreateSeuratObject(counts = d$counts, meta.data = d$meta)

table(obj$stim)                 # the two batches: 1,500 cells each

CTRL STIM 
1500 1500 

Deux échantillons, 1 500 cellules chacun, désormais dans un seul objet Seurat. Chaque bloc ci-dessous repart exactement de ce point, de sorte que vous pouvez copier n’importe lequel d’entre eux et l’exécuter seul.

Voir d’abord l’effet de lot

Avant de corriger quoi que ce soit, regardez le problème. Exécutez le pipeline single-cell standard sans intégration — normaliser, trouver les gènes hautement variables (HVG, les ~2 000 gènes qui varient le plus entre cellules et portent le signal biologique), mettre à l’échelle, réduire avec la PCA (analyse en composantes principales, qui compresse les cellules en quelques dizaines de composantes), et projeter avec l’UMAP (uniform manifold approximation and projection, la carte en 2-D). Puis colorez l’UMAP de deux façons : par échantillon et par type cellulaire.

library(Seurat)
d <- readRDS("_data/ifnb_subset.rds")
obj <- CreateSeuratObject(counts = d$counts, meta.data = d$meta)

# Standard pipeline, no integration
obj <- NormalizeData(obj)
obj <- FindVariableFeatures(obj)
obj <- ScaleData(obj)
obj <- RunPCA(obj, npcs = 30, verbose = FALSE)
obj <- RunUMAP(obj, dims = 1:30, reduction = "pca",
               reduction.name = "umap.unintegrated", verbose = FALSE)

DimPlot(obj, reduction = "umap.unintegrated", group.by = "stim") |
  DimPlot(obj, reduction = "umap.unintegrated", group.by = "seurat_annotations")

Two UMAP scatter plots of the same 3,000 unintegrated cells. Left, coloured by sample: the control cells (salmon-red) and the interferon-beta–stimulated cells (teal) occupy separate regions with little overlap. Right, coloured by published cell type: within each sample region the same cell types appear (monocytes, CD4 T, CD8 T, B, NK and others), so each cell type is drawn as two separate copies — one per sample. Together the panels show the split is driven by sample, not by biology.

Lisez les deux panneaux ensemble. À gauche, les cellules se séparent nettement en un nuage contrôle et un nuage stimulé — colorées entièrement selon l’échantillon dont elles proviennent. À droite, les étiquettes de types cellulaires publiées révèlent l’indice : les mêmes types cellulaires sont présents dans les deux nuages. Il y a un groupe de monocytes côté contrôle et un groupe de monocytes côté stimulé ; un groupe de lymphocytes T CD4 dans chacun ; et ainsi de suite. Chaque population a été dupliquée, une fois par échantillon. C’est l’empreinte d’un effet de lot — une biologie qui devrait se chevaucher a été écartée par un axe technique. Si vous regroupiez ceci maintenant, vous obtiendriez « monocytes contrôle » et « monocytes stimulés » comme regroupements séparés, et toute comparaison inter-conditions serait dénuée de sens.

Le modèle Seurat v5 : diviser les échantillons en couches

Seurat v5 représente une expérience multi-échantillons avec des couches à l’intérieur d’un seul assay. Vous divisez les comptages en une couche par lot, et l’intégration traite alors chaque couche comme un échantillon à aligner. C’est le changement structurel clé par rapport à v4 (qui jonglait avec une liste d’objets séparés et les refusionnait avec des ancres).

library(Seurat)
d <- readRDS("_data/ifnb_subset.rds")
obj <- CreateSeuratObject(counts = d$counts, meta.data = d$meta)

# Split the RNA assay into one layer per sample
obj[["RNA"]] <- split(obj[["RNA"]], f = obj$stim)

Layers(obj)                     # counts.CTRL and counts.STIM — one layer per batch
[1] "counts.CTRL" "counts.STIM"

Après split(), l’assay contient counts.CTRL et counts.STIM au lieu d’une seule couche counts. NormalizeData(), FindVariableFeatures() et ScaleData() opèrent maintenant par couche, de sorte que les statistiques de chaque échantillon sont calculées sur ses propres cellules — exactement ce que vous voulez avant de les aligner. Vous exécutez la PCA sur les gènes variables combinés comme d’habitude ; la division ne change que la manière dont les étapes en amont traitent les échantillons.

Intégrer avec Harmony — un seul appel

Avec les échantillons en couches, l’intégration est une seule fonction. IntegrateLayers() prend la PCA non corrigée et produit une nouvelle représentation corrigée pour l’effet de lot (un embedding). La méthode ici est Harmony (Korsunsky et al., 2019) : elle déplace itérativement les cellules dans l’espace des PCA de sorte que les regroupements contiennent un mélange bien équilibré de chaque lot, sans faire s’effondrer ensemble des types cellulaires distincts. Elle est rapide, s’adapte à des millions de cellules, et est ce que la majorité des pipelines multi-échantillons publiés exécutent — y compris l’atlas CAR-T dont le code ancre cette leçon.

library(Seurat)
d <- readRDS("_data/ifnb_subset.rds")
obj <- CreateSeuratObject(counts = d$counts, meta.data = d$meta)
obj[["RNA"]] <- split(obj[["RNA"]], f = obj$stim)
obj <- NormalizeData(obj)
obj <- FindVariableFeatures(obj)
obj <- ScaleData(obj)
obj <- RunPCA(obj, npcs = 30, verbose = FALSE)

# One call: correct the batch effect, writing a new "harmony" reduction
obj <- IntegrateLayers(obj, method = HarmonyIntegration,
                       orig.reduction = "pca", new.reduction = "harmony",
                       verbose = FALSE)

# Project the CORRECTED embedding and colour by cell type
obj <- RunUMAP(obj, dims = 1:30, reduction = "harmony",
               reduction.name = "umap.harmony", verbose = FALSE)
DimPlot(obj, reduction = "umap.harmony", group.by = "seurat_annotations")

A UMAP of the 3,000 cells after Harmony integration, coloured by published cell type. Unlike the unintegrated version, each cell type now forms a single coherent island that contains cells from both samples: one monocyte territory (CD14 and CD16 monocytes), one large T-cell region (CD4 naive, CD4 memory, CD8 T and activated T), an NK group beside the CD8 T cells, a separate B-cell island, and small dendritic-cell, plasmacytoid-dendritic-cell and megakaryocyte groups. The cell-type structure is preserved while the sample duplication is gone.

IntegrateLayers() lit la réduction pca et en écrit une version corrigée appelée harmony ; tout ce qui vient ensuite — le graphe de voisinage, le clustering, l’UMAP — s’exécute maintenant sur reduction = "harmony" au lieu de "pca". Coloré par type cellulaire, l’UMAP corrigé montre à quoi sert l’intégration : chaque type cellulaire redevient un unique îlot cohérent, n’étant plus scindé en une copie contrôle et une copie stimulée. La biologie a survécu ; l’axe de lot a disparu.

L’UMAP avant/après

Mettez les deux côte à côte, colorés par échantillon, et la correction est indéniable — c’est la figure qui mérite de figurer en tête d’une analyse d’intégration.

library(Seurat)
library(ggplot2)
d <- readRDS("_data/ifnb_subset.rds")
obj <- CreateSeuratObject(counts = d$counts, meta.data = d$meta)
obj[["RNA"]] <- split(obj[["RNA"]], f = obj$stim)
obj <- NormalizeData(obj)
obj <- FindVariableFeatures(obj)
obj <- ScaleData(obj)
obj <- RunPCA(obj, npcs = 30, verbose = FALSE)
obj <- RunUMAP(obj, dims = 1:30, reduction = "pca",
               reduction.name = "umap.unintegrated", verbose = FALSE)
obj <- IntegrateLayers(obj, method = HarmonyIntegration,
                       orig.reduction = "pca", new.reduction = "harmony",
                       verbose = FALSE)
obj <- RunUMAP(obj, dims = 1:30, reduction = "harmony",
               reduction.name = "umap.harmony", verbose = FALSE)

p1 <- DimPlot(obj, reduction = "umap.unintegrated", group.by = "stim") +
  ggtitle("Before: split by sample")
p2 <- DimPlot(obj, reduction = "umap.harmony", group.by = "stim") +
  ggtitle("After Harmony: mixed")
p1 | p2

Two UMAP scatter plots of the same 3,000 cells coloured by sample (salmon-red = control, teal = interferon-beta stimulated), titled 'Before: split by sample' and 'After Harmony: mixed'. Left, before integration: within each cell-group the salmon-red control cells and teal stimulated cells sit in separate halves — cells split by sample. Right, after Harmony integration: the salmon-red and teal points are thoroughly intermixed throughout every group, so control and stimulated cells of the same type now occupy the same territory. The contrast between the separated left panel and the blended right panel is the visual proof of batch correction.

À gauche, les deux échantillons sont deux nuages. À droite, ils forment une seule carte où les cellules contrôle et stimulées sont entremêlées partout. Notez ce qui ne s’est pas produit : la carte corrigée ne s’est pas effondrée en une masse unique. La structure des types cellulaires est intacte (vous l’avez vue par type cellulaire ci-dessus) — seul le décalage échantillon-à-échantillon a été supprimé. C’est tout l’objectif de l’intégration : mélanger les lots, préserver la biologie.

Vérifiez-le avec un chiffre, pas seulement avec vos yeux

Un UMAP est persuasif mais c’est une projection, et « ça a l’air mélangé » n’est pas un résultat. Confirmez la correction avec une statistique. La vérification la plus intuitive : regrouper sur l’embedding intégré, croiser regroupement × échantillon, et se demander si chaque regroupement contient désormais les deux échantillons. Un regroupement bien intégré est proche de la répartition 50/50 globale ; un regroupement piloté par le lot est presque entièrement un seul échantillon.

library(Seurat)
d <- readRDS("_data/ifnb_subset.rds")
obj <- CreateSeuratObject(counts = d$counts, meta.data = d$meta)
obj[["RNA"]] <- split(obj[["RNA"]], f = obj$stim)
obj <- NormalizeData(obj)
obj <- FindVariableFeatures(obj)
obj <- ScaleData(obj)
obj <- RunPCA(obj, npcs = 30, verbose = FALSE)

# Cluster on the UNINTEGRATED PCA (the "before")
obj <- FindNeighbors(obj, reduction = "pca", dims = 1:30, verbose = FALSE)
obj <- FindClusters(obj, resolution = 0.4, cluster.name = "clusters_pca", verbose = FALSE)

# Cluster on the INTEGRATED Harmony embedding (the "after")
obj <- IntegrateLayers(obj, method = HarmonyIntegration,
                       orig.reduction = "pca", new.reduction = "harmony",
                       verbose = FALSE)
obj <- FindNeighbors(obj, reduction = "harmony", dims = 1:30, verbose = FALSE)
obj <- FindClusters(obj, resolution = 0.4, cluster.name = "clusters_harmony", verbose = FALSE)

# Mean per-cluster minority-sample fraction: 0 = every cluster is one sample; 0.5 = perfectly mixed
minority_fraction <- function(clusters, batch) {
  ct <- table(clusters, batch)
  mean(apply(ct, 1, function(row) min(row) / sum(row)))
}
c(unintegrated = minority_fraction(obj$clusters_pca, obj$stim),
  harmony      = minority_fraction(obj$clusters_harmony, obj$stim))
unintegrated      harmony 
   0.1757848    0.4698855 

Le chiffre raconte l’histoire que l’œil avait devinée. Avant l’intégration, la fraction moyenne du lot minoritaire est d’environ 0.18 — la plupart des regroupements sont fortement un seul échantillon (plusieurs sont > 99 % une seule condition), l’effet de lot rendu concret. Après Harmony, elle monte à environ 0.47, près de l’idéal 0.5 : chaque regroupement contient désormais un mélange presque équilibré de cellules contrôle et stimulées. Les échantillons partagent leurs regroupements.

Une alternative à un seul chiffre est la largeur de silhouette de lot — à quel point les échantillons sont séparables dans l’embedding. Plus bas est meilleur ici (vous voulez que les lots soient inséparables) :

library(Seurat)
library(cluster)
d <- readRDS("_data/ifnb_subset.rds")
obj <- CreateSeuratObject(counts = d$counts, meta.data = d$meta)
obj[["RNA"]] <- split(obj[["RNA"]], f = obj$stim)
obj <- NormalizeData(obj)
obj <- FindVariableFeatures(obj)
obj <- ScaleData(obj)
obj <- RunPCA(obj, npcs = 30, verbose = FALSE)
obj <- IntegrateLayers(obj, method = HarmonyIntegration,
                       orig.reduction = "pca", new.reduction = "harmony",
                       verbose = FALSE)

batch <- as.integer(factor(obj$stim))
sil <- function(reduction) {
  emb <- Embeddings(obj, reduction)[, 1:30]
  summary(silhouette(batch, dist(emb)))$avg.width
}
c(unintegrated = sil("pca"), harmony = sil("harmony"))
unintegrated      harmony 
 0.092633806  0.006008717 

La silhouette de lot chute d’environ 0.09 sur la PCA brute à environ 0.01 sur l’embedding Harmony — essentiellement zéro, ce qui signifie qu’une cellule n’est plus plus proche de son propre échantillon que de l’autre. Les deux métriques disent la même chose : le lot est corrigé.

Ces deux vérifications sont le plancher pratique. La version de qualité publication est le panneau de benchmark scIB (Luecken et al., 2022), qui note l’intégration sur deux axes à la fois — le mélange des lots (iLISI, kBET, batch ASW) versus la conservation biologique (cell-type ASW, ARI, NMI) — parce qu’une méthode peut toujours mélanger parfaitement les lots en détruisant la biologie, et un bon score doit résister à cela. L’idée bilatérale est ce qui compte : ne jamais rapporter le mélange sans confirmer aussi que les types cellulaires ont survécu.

Confirmer que la biologie a survécu

L’autre moitié de l’idée scIB, rendue concrète : croiser les regroupements intégrés avec les étiquettes de types cellulaires publiées. Une bonne intégration produit des regroupements qui sont des types cellulaires cohérents, pas des fragments pilotés par le lot.

library(Seurat)
d <- readRDS("_data/ifnb_subset.rds")
obj <- CreateSeuratObject(counts = d$counts, meta.data = d$meta)
obj[["RNA"]] <- split(obj[["RNA"]], f = obj$stim)
obj <- NormalizeData(obj)
obj <- FindVariableFeatures(obj)
obj <- ScaleData(obj)
obj <- RunPCA(obj, npcs = 30, verbose = FALSE)
obj <- IntegrateLayers(obj, method = HarmonyIntegration,
                       orig.reduction = "pca", new.reduction = "harmony",
                       verbose = FALSE)
obj <- FindNeighbors(obj, reduction = "harmony", dims = 1:30, verbose = FALSE)
obj <- FindClusters(obj, resolution = 0.4, verbose = FALSE)

# Each integrated cluster should be one coherent cell type
table(cluster = Idents(obj), obj$seurat_annotations)[, 1:8]
       
cluster CD14 Mono CD4 Naive T CD4 Memory T CD16 Mono   B CD8 T T activated  NK
      0         3         530          366         2   0    53          26   0
      1       919           0            0         5   0     0           0   0
      2         0           1            0         0 194     1           4   0
      3         0           0            0         0   0   126           0 143
      4         3           0            0       248   0     0           0   0
      5         0           3            2         0   0     2          95   0
      6         0           0            0         0   0     0           0   0
      7         0           0            2         0   0     0           0   1

Chaque ligne (regroupement) se concentre sur un ou deux types cellulaires apparentés : un regroupement est celui des lymphocytes T CD4, un autre celui des monocytes CD14+, un autre celui des lymphocytes B, un autre celui des lymphocytes T CD8 et NK, et ainsi de suite — et chacun de ces regroupements tire des deux échantillons. C’est la condition de succès énoncée précisément : des regroupements qui sont des types cellulaires partagés entre conditions, exactement ce qui vous permet maintenant de demander « comment les monocytes ont-ils répondu à l’IFN-β ? » — l’ expression différentielle inter-conditions que l’intégration débloque.

Pour finir : joindre les couches

Une fois que vous intégrez en aval sur la réduction harmony, rejoignez les couches par échantillon en une seule couche counts/data afin que la détection des marqueurs et l’expression différentielle voient toutes les cellules ensemble.

obj <- JoinLayers(obj)          # collapse counts.CTRL / counts.STIM back to one layer

JoinLayers() est le pendant du split() que vous avez fait au début : diviser pour intégrer, joindre pour analyser. À partir d’ ici, l’objet se comporte comme n’importe quel objet Seurat mono-échantillon, si ce n’est que son embedding est corrigé pour l’effet de lot.

Autres méthodes : RPCA, CCA, STACAS

Harmony est la recommandation par défaut, mais IntegrateLayers() est agnostique quant à la méthode — vous changez un seul argument pour échanger l’algorithme, et le reste du workflow est identique :

# Reciprocal PCA — more conservative; the choice when samples share few cells or are very different
obj <- IntegrateLayers(obj, method = RPCAIntegration,
                       orig.reduction = "pca", new.reduction = "integrated.rpca")

# Canonical correlation analysis — more aggressive alignment; good for subtle shared signal
obj <- IntegrateLayers(obj, method = CCAIntegration,
                       orig.reduction = "pca", new.reduction = "integrated.cca")

RPCA (PCA réciproque) est la méthode basée sur les ancres que l’équipe Seurat recommande lorsque les lots sont grands ou ne se chevauchent que partiellement en types cellulaires — elle est plus rapide et plus conservatrice que CCA, moins susceptible de forcer ensemble des populations non correspondantes. CCA (analyse de corrélation canonique) aligne plus agressivement, ce qui aide lorsque le signal partagé est subtil mais risque la sur-correction. Une règle empirique utile : essayez d’abord Harmony (rapide, valeur par défaut robuste) ; recourez à RPCA lorsque vous craignez la sur-intégration (des types distincts fusionnés), et validez celle que vous choisissez avec les vérifications mélange-versus-biologie ci-dessus. Une quatrième option, STACAS, est une méthode d’ancres réglée pour protéger l’hétérogénéité biologique et populaire pour les atlas immunitaires/tumoraux ; elle s’intègre dans le même cadre Seurat.

Pour les atlas très grands et complexes, les leaders actuels du benchmark sont les modèles d’apprentissage profond scVI / scANVI (le package Python scvi-tools ; Lopez et al., 2018), qui dominent le benchmark d’intégration scIB (Luecken et al., 2022). Ils sont uniquement en Python. Cette leçon enseigne la voie native R robuste et largement utilisée — Harmony, RPCA, CCA, STACAS — qui est compétitive pour la grande majorité des workflows et vous garde dans un seul langage. Si vous avez plus tard besoin de scVI à l’échelle d’un atlas, zellkonverter/sceasy font le pont d’un objet Seurat vers l’écosystème Python ; pour la plupart des projets multi-échantillons, la voie R présentée ici est tout ce dont vous avez besoin.

Interpréter honnêtement : la correction a deux modes d’échec

L’intégration est un équilibre, et les deux extrêmes sont mauvais :

  • La sous-correction laisse un effet de lot résiduel — les regroupements penchent encore vers un seul échantillon, l’UMAP montre encore une structure d’échantillon. Correctif : utilisez plus de gènes d’intégration, plus de dimensions, ou une méthode plus forte (Harmony → CCA).
  • La sur-correction fusionne des populations réellement différentes parce que l’algorithme a reçu, en somme, l’instruction de rendre les échantillons identiques. Le signe de danger est un type cellulaire réel, attendu, qui disparaît — deux types distincts fondus en un seul regroupement. C’est pourquoi vous ne faites jamais confiance au seul score de mélange : une méthode peut maximiser le mélange des lots en effaçant la biologie.

Le workflow honnête est celui, bilatéral, que vous avez exécuté : confirmer que les lots se sont mélangés et confirmer que les types cellulaires ont survécu. Lorsqu’un traitement remodèle réellement l’expression — comme le fait l’IFN-β ici — rappelez-vous que l’intégration vise à aligner les types cellulaires partagés pour la comparaison, non à effacer l’effet du traitement ; la réponse biologique vit dans l’expression différentielle par type cellulaire que vous réalisez après l’intégration, pas dans l’axe de lot que vous avez supprimé.

Problèmes fréquents

L’UMAP se sépare encore par échantillon après l’intégration. Presque toujours, vous avez projeté la mauvaise réduction. L’UMAP et le graphe de voisinage doivent s’exécuter sur l’embedding intégré : RunUMAP(obj, reduction = "harmony", ...) et FindNeighbors(obj, reduction = "harmony", ...). Si vous laissez reduction = "pca" (la valeur par défaut), vous visualisez l’espace non corrigé et rien n’a changé.

IntegrateLayers() renvoie une erreur indiquant qu’il ne trouve pas les couches. Vous devez split() l’assay en couches par échantillon avant d’intégrer — obj[["RNA"]] <- split(obj[["RNA"]], f = obj$sample). Sur un objet non divisé, il y a une seule couche et rien à aligner. Vérifiez que Layers(obj) affiche un counts.* par échantillon.

Suivre un ancien tutoriel v4 avec FindIntegrationAnchors() / IntegrateData(). C’est le schéma Seurat v4 (une liste d’objets, un assay integrated). En v5, il est supplanté par le workflow de division en couches + IntegrateLayers() présenté ici — un objet, un appel, la réduction corrigée vivant aux côtés de la PCA originale. Mélanger les deux API est la source de confusion la plus courante ; choisissez v5 et restez-y.

Questions fréquentes

Divisez l’assay RNA en une couche par échantillon (obj[["RNA"]] <- split(obj[["RNA"]], f = obj$sample)), exécutez le pipeline standard NormalizeDataFindVariableFeaturesScaleDataRunPCA, puis appelez IntegrateLayers(obj, method = HarmonyIntegration, orig.reduction = "pca", new.reduction = "harmony"). Exécutez FindNeighbors, FindClusters et RunUMAP sur reduction = "harmony", puis JoinLayers(obj) pour terminer.

Seurat v4 gardait chaque échantillon comme un objet séparé, trouvait des ancres avec FindIntegrationAnchors(), et construisait un nouvel assay integrated avec IntegrateData(). Seurat v5 garde tout dans un seul objet avec les échantillons comme couches et les corrige avec un unique appel IntegrateLayers() qui écrit une nouvelle réduction (par ex. harmony) aux côtés de la PCA originale. L’API v5 est la recommandation actuelle ; les fonctions v4 existent encore mais sont supplantées.

Commencez par Harmony : elle est rapide, s’adapte à des millions de cellules, et est la valeur par défaut robuste que la plupart des pipelines exécutent. Utilisez RPCA (PCA réciproque) lorsque les échantillons sont grands ou partagent peu de types cellulaires et que vous voulez un alignement conservateur, résistant à la sur-correction. Utilisez CCA lorsque le signal partagé est subtil et que vous avez besoin d’un alignement plus agressif (au prix d’un risque de sur-correction plus élevé). Quelle que soit celle que vous choisissez, vérifiez le mélange des lots et la conservation biologique avant de lui faire confiance.

Regroupez sur l’embedding intégré et vérifiez que chaque regroupement contient les deux échantillons dans à peu près la proportion globale (une fraction du lot minoritaire par regroupement proche de 0.5), et que chaque regroupement est un type cellulaire cohérent plutôt qu’un fragment de lot. Une silhouette de lot proche de zéro confirme que les échantillons ne sont plus séparables. La version rigoureuse est le panneau de métriques scIB (iLISI/kBET pour le mélange, ASW/ARI pour la biologie) — vérifiez toujours le mélange et la biologie ensemble, jamais un seul.

Avant. L’intégration produit un embedding corrigé pour l’effet de lot, et vous regroupez sur celui-ci — sinon vous regroupez sur la PCA non corrigée et obtenez un regroupement par type cellulaire par échantillon. L’ordre est : normaliser → gènes variables → mettre à l’échelle → PCA → intégrer → graphe de voisinage + regroupements + UMAP sur la réduction intégrée.

Testez vos connaissances

À partir de ifnb_subset.rds, divisez l’assay RNA par stim, exécutez le pipeline standard jusqu’à la PCA, intégrez avec Harmony, puis regroupez sur l’embedding Harmony à la résolution 0.4 et confirmez avec un table() qu’au moins un regroupement contient des cellules des deux CTRL et STIM.

Après IntegrateLayers(..., new.reduction = "harmony"), appelez FindNeighbors(obj, reduction = "harmony", dims = 1:30) puis FindClusters(obj, resolution = 0.4). table(Idents(obj), obj$stim) montre la répartition de chaque regroupement par échantillon — un regroupement bien intégré a des effectifs substantiels dans les deux colonnes.

library(Seurat)
d <- readRDS("_data/ifnb_subset.rds")
obj <- CreateSeuratObject(counts = d$counts, meta.data = d$meta)
obj[["RNA"]] <- split(obj[["RNA"]], f = obj$stim)
obj <- NormalizeData(obj)
obj <- FindVariableFeatures(obj)
obj <- ScaleData(obj)
obj <- RunPCA(obj, npcs = 30, verbose = FALSE)
obj <- IntegrateLayers(obj, method = HarmonyIntegration,
                       orig.reduction = "pca", new.reduction = "harmony",
                       verbose = FALSE)
obj <- FindNeighbors(obj, reduction = "harmony", dims = 1:30, verbose = FALSE)
obj <- FindClusters(obj, resolution = 0.4, verbose = FALSE)

table(Idents(obj), obj$stim)     # every cluster carries both CTRL and STIM

À la résolution 0.4, l’embedding Harmony donne 8 regroupements, et chaque ligne du tableau a des effectifs substantiels dans les deux colonnes CTRL et STIM — les échantillons partagent tous leurs regroupements, ce que l’intégration cherchait à accomplir.

A. Harmony a échoué — vous avez besoin d’une méthode plus forte comme CCA. B. Vous avez exécuté RunUMAP / FindNeighbors sur reduction = "pca" au lieu de reduction = "harmony". C. Les deux échantillons n’ont aucun type cellulaire en commun, ils ne peuvent donc pas être intégrés.

B. De loin la cause la plus courante. IntegrateLayers() écrit une nouvelle réduction (harmony) ; si vos FindNeighbors/RunUMAP en aval pointent encore vers la pca par défaut, vous visualisez l’espace non corrigé et l’effet de lot est intact. Mettez reduction = "harmony" dans les deux appels et relancez.

Conclusion

Intégrer des données single-cell multi-échantillons dans Seurat v5 est une séquence courte et bien définie. Vous avez divisé l’assay RNA en couches par échantillon, exécuté le pipeline standard normaliser → gènes variables → mettre à l’échelle → PCA, corrigé l’effet de lot avec un seul appel IntegrateLayers() (Harmony par défaut ; RPCA ou CCA en changeant un argument), et regroupé et visualisé sur l’embedding intégré — puis joint les couches pour analyser. La logique durable : un effet de lot se manifeste par un UMAP qui se sépare par échantillon, l’intégration supprime l’axe technique tout en préservant l’axe biologique, et vous le prouvez avec une vérification bilatérale — les lots mélangés (fraction minoritaire proche de 0.5, silhouette de lot proche de 0) et les types cellulaires conservés (des regroupements qui sont des types cohérents tirés de chaque échantillon). Une fois les échantillons alignés, les questions auxquelles une expérience multi-échantillons existe pour répondre — comment chaque type cellulaire répond selon les conditions — deviennent enfin posables.

Leçons connexes

🟢 Avec un agent IA

Demandez à Prova « comment intégrer mon propre objet Seurat multi-échantillons et vérifier que l’effet de lot est corrigé ? » — elle répond avec du code R que vous pouvez exécuter sur vos propres données, divise vos échantillons en couches, exécute IntegrateLayers() avec Harmony (ou RPCA/CCA), et vous aide à vérifier que les lots se sont mélangés tandis que les types cellulaires sont restés distincts. The runtime is the judge. Demander à Prova →

Cette page vous a-t-elle été utile ?

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.

Partager cette pageXLinkedInRedditHN
Note

Cette leçon est reproductible : chaque figure et chaque nombre ont été produits par le code montré — copiez n’importe quel bloc et exécutez-le pour les reproduire. The runtime is the judge.

Références

Réutilisation

Citation

BibTeX
@online{2026,
  author = {},
  title = {Intégration Seurat : corriger l’effet de lot scRNA-seq (v5)},
  date = {2026-07-03},
  url = {https://www.datanovia.com/learn/bioinformatics/single-cell/scrnaseq-integration-batch.html},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“Intégration Seurat : corriger l’effet de lot scRNA-seq (v5).” 2026. July 3. https://www.datanovia.com/learn/bioinformatics/single-cell/scrnaseq-integration-batch.html.