Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Integrate multiple scRNA-seq samples/batches using Harmony, scVI, Seurat anchors, and fastMNN. Remove technical variation while preserving biological differences. Use when integrating multiple scRNA-seq batches or datasets.
.claude/skills/bio-single-cell-batch-integration/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✓→✓ | = Same ✓ | — | — |
| case-01 | ✗→✗ | = Same ✗ | — | — |
| case-03 | ✗→✗ | = Same ✗ | — | — |
| case-18 | ✗→✗ | = Same ✗ | — | — |
| case-07 | ✗→✗ | = Same ✗ | — | — |
Reference examples tested with: anndata 0.10+, scanpy 1.10+, scikit-learn 1.4+, scvi-tools 1.1+
Before using code patterns, verify installed versions match. If versions differ:
pip show <package> then help(module.function) to check signaturespackageVersion('<pkg>') then ?function_name to verify parametersIf code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
Integrate multiple scRNA-seq datasets to remove batch effects while preserving biological variation.
| Tool | Speed | Scalability | Best For | |------|-------|-------------|----------| | Harmony | Fast | Good | Quick integration, most use cases | | scVI | Moderate | Excellent | Large datasets, deep learning | | Seurat CCA/RPCA | Moderate | Good | Conserved biology across batches | | fastMNN | Fast | Good | MNN-based correction |
Goal: Remove batch effects from merged scRNA-seq datasets using Harmony's iterative correction of PCA embeddings.
Approach: Run PCA on merged data, iteratively adjust embeddings to mix batches while preserving biological variation, and use corrected embeddings for downstream analysis.
"Integrate my batches" → Merge samples, preprocess jointly, correct technical variation in the embedding space, and cluster on corrected coordinates.
rlibrary(Seurat) library(harmony) # Merge datasets first merged <- merge(sample1, y = list(sample2, sample3), add.cell.ids = c('S1', 'S2', 'S3')) # Standard preprocessing merged <- NormalizeData(merged) merged <- FindVariableFeatures(merged) merged <- ScaleData(merged) merged <- RunPCA(merged) # Run Harmony on PCA embeddings merged <- RunHarmony(merged, group.by.vars = 'orig.ident', dims.use = 1:30) # Use harmony embeddings for downstream merged <- RunUMAP(merged, reduction = 'harmony', dims = 1:30) merged <- FindNeighbors(merged, reduction = 'harmony', dims = 1:30) merged <- FindClusters(merged, resolution = 0.5)
r# Correct for both sample and technology merged <- RunHarmony(merged, group.by.vars = c('sample', 'technology'), dims.use = 1:30, max.iter.harmony = 20)
pythonimport scanpy as sc import scanpy.external as sce adata = sc.read_h5ad('merged.h5ad') # Standard preprocessing sc.pp.normalize_total(adata, target_sum=1e4) sc.pp.log1p(adata) sc.pp.highly_variable_genes(adata, batch_key='batch') adata = adata[:, adata.var.highly_variable] sc.pp.scale(adata) sc.tl.pca(adata) # Run Harmony sce.pp.harmony_integrate(adata, key='batch') # Use corrected embedding sc.pp.neighbors(adata, use_rep='X_pca_harmony') sc.tl.umap(adata) sc.tl.leiden(adata)
Goal: Integrate batches using a deep generative model that learns a shared latent space.
Approach: Train a variational autoencoder (scVI) conditioned on batch to learn batch-invariant latent representations, then use the latent space for clustering and visualization.
pythonimport scvi import scanpy as sc adata = sc.read_h5ad('merged.h5ad') # Setup for scVI scvi.model.SCVI.setup_anndata(adata, batch_key='batch') # Train model model = scvi.model.SCVI(adata, n_latent=30, n_layers=2) model.train(max_epochs=100, early_stopping=True) # Get latent representation adata.obsm['X_scVI'] = model.get_latent_representation() # Use for downstream sc.pp.neighbors(adata, use_rep='X_scVI') sc.tl.umap(adata) sc.tl.leiden(adata)
python# Include continuous covariates scvi.model.SCVI.setup_anndata(adata, batch_key='batch', continuous_covariate_keys=['percent_mito']) model = scvi.model.SCVI(adata, n_latent=30) model.train()
python# If you have reference labels for some cells scvi.model.SCANVI.setup_anndata(adata, batch_key='batch', labels_key='cell_type', unlabeled_category='Unknown') model = scvi.model.SCANVI(adata, n_latent=30) model.train(max_epochs=100) # Predict labels for unlabeled cells adata.obs['predicted_type'] = model.predict()
Goal: Integrate batches using Seurat's anchor-based framework (CCA or RPCA).
Approach: Find shared biological anchors between datasets via canonical correlation analysis, then use anchors to correct expression values into a unified space.
rlibrary(Seurat) # Split by batch obj_list <- SplitObject(merged, split.by = 'batch') # Normalize each obj_list <- lapply(obj_list, function(x) { x <- NormalizeData(x) x <- FindVariableFeatures(x, selection.method = 'vst', nfeatures = 2000) return(x) }) # Find integration anchors anchors <- FindIntegrationAnchors(object.list = obj_list, dims = 1:30) # Integrate integrated <- IntegrateData(anchorset = anchors, dims = 1:30) # Switch to integrated assay for downstream DefaultAssay(integrated) <- 'integrated' integrated <- ScaleData(integrated) integrated <- RunPCA(integrated) integrated <- RunUMAP(integrated, dims = 1:30)
r# Use reciprocal PCA for faster integration anchors <- FindIntegrationAnchors(object.list = obj_list, dims = 1:30, reduction = 'rpca') integrated <- IntegrateData(anchorset = anchors, dims = 1:30)
r# Seurat v5 uses layers merged[['RNA']] <- split(merged[['RNA']], f = merged$batch) merged <- IntegrateLayers(merged, method = CCAIntegration, orig.reduction = 'pca', new.reduction = 'integrated.cca') merged <- JoinLayers(merged)
rlibrary(batchelor) library(SingleCellExperiment) # Convert Seurat to SCE sce <- as.SingleCellExperiment(merged) # Run fastMNN corrected <- fastMNN(sce, batch = sce$batch, d = 30, k = 20) # Extract corrected values reducedDim(sce, 'MNN') <- reducedDim(corrected, 'corrected')
Goal: Assess whether integration successfully removed batch effects while preserving biological variation.
Approach: Compute mixing metrics (LISI, silhouette scores) and visualize batch versus cell-type separation before and after integration.
r# LISI score (lower = more mixed) library(lisi) lisi_scores <- compute_lisi(Embeddings(merged, 'harmony'), merged@meta.data, c('batch', 'cell_type')) # Batch mixing should be high, cell type separation preserved mean(lisi_scores$batch) # Want high mean(lisi_scores$cell_type) # Want low (preserved)
r# Before integration DimPlot(merged, reduction = 'pca', group.by = 'batch') DimPlot(merged, reduction = 'pca', group.by = 'cell_type') # After integration DimPlot(merged, reduction = 'harmony', group.by = 'batch') DimPlot(merged, reduction = 'harmony', group.by = 'cell_type')
pythonfrom sklearn.metrics import silhouette_score # Batch silhouette (want low - batches mixed) batch_sil = silhouette_score(adata.obsm['X_scVI'], adata.obs['batch']) # Cell type silhouette (want high - types separated) celltype_sil = silhouette_score(adata.obsm['X_scVI'], adata.obs['cell_type'])
Goal: Run end-to-end multi-sample integration from raw 10X files to clustered, integrated UMAP.
Approach: Load and merge samples, preprocess jointly, integrate with Harmony, and perform downstream clustering on corrected embeddings.
rlibrary(Seurat) library(harmony) # Load and merge samples samples <- list.files('data/', pattern = '*.h5', full.names = TRUE) obj_list <- lapply(samples, Read10X_h5) names(obj_list) <- gsub('.h5', '', basename(samples)) merged <- merge(CreateSeuratObject(obj_list[[1]], project = names(obj_list)[1]), y = lapply(2:length(obj_list), function(i) CreateSeuratObject(obj_list[[i]], project = names(obj_list)[i]))) # QC merged[['percent.mt']] <- PercentageFeatureSet(merged, pattern = '^MT-') merged <- subset(merged, nFeature_RNA > 200 & nFeature_RNA < 5000 & percent.mt < 20) # Preprocess merged <- NormalizeData(merged) merged <- FindVariableFeatures(merged, nfeatures = 2000) merged <- ScaleData(merged, vars.to.regress = 'percent.mt') merged <- RunPCA(merged, npcs = 50) # Integrate with Harmony merged <- RunHarmony(merged, group.by.vars = 'orig.ident') # Downstream analysis on integrated data merged <- RunUMAP(merged, reduction = 'harmony', dims = 1:30) merged <- FindNeighbors(merged, reduction = 'harmony', dims = 1:30) merged <- FindClusters(merged, resolution = 0.5) DimPlot(merged, group.by = c('orig.ident', 'seurat_clusters'), ncol = 2)
| Scenario | Recommended | |----------|-------------| | Quick integration, most cases | Harmony | | Large datasets (>500k cells) | scVI or Harmony | | Strong batch effects | scVI | | Reference mapping | Seurat anchors or scANVI | | Preserving rare populations | fastMNN |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of -100 percentage points is the difference between those two pass rates over the 23 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.