Skills › Knowledge Work Plugins › scvi-tools › batch_correction_sysvi.md
batch_correction_sysvi.md
417 lines · bio-research/skills/scvi-tools/references/batch_correction_sysvi.md at 2574e29
1# Advanced Batch Correction with sysVI
3This reference covers system-level batch correction using sysVI, designed for integrating data across major technological or study differences.
Advanced Batch Correction with sysVI
This reference covers system-level batch correction using sysVI, designed for integrating data across major technological or study differences.
Raw lines 1 to 45## Overview
7sysVI (System Variational Inference) extends scVI for scenarios where:
8- Batch effects are very strong (different technologies)
9- Standard scVI over-corrects biological signal
10- You need to separate "system" effects from biological variation
Overview
sysVI (System Variational Inference) extends scVI for scenarios where:
- Batch effects are very strong (different technologies)
- Standard scVI over-corrects biological signal
- You need to separate "system" effects from biological variation
12## When to Use sysVI vs scVI
14| Scenario | Recommended Model |
15|----------|-------------------|
16| Same technology, different samples | scVI |
17| 10x v2 vs 10x v3 | scVI (usually) |
18| 10x vs Smart-seq2 | sysVI |
19| Different sequencing depths | scVI with covariates |
20| Cross-study integration | sysVI |
21| Atlas-scale integration | sysVI |
When to Use sysVI vs scVI
| Scenario | Recommended Model |
|---|---|
| Same technology, different samples | scVI |
| 10x v2 vs 10x v3 | scVI (usually) |
| 10x vs Smart-seq2 | sysVI |
| Different sequencing depths | scVI with covariates |
| Cross-study integration | sysVI |
| Atlas-scale integration | sysVI |
23## Prerequisites
25```python
26import scvi
27import scanpy as sc
28import numpy as np
30print(f"scvi-tools version: {scvi.__version__}")
31```
Prerequisites
import scvi
import scanpy as sc
import numpy as np
print(f"scvi-tools version: {scvi.__version__}")Raw lines 23 to 3233## Understanding sysVI Architecture
35sysVI separates variation into:
361. **Biological variation**: Cell type, state, trajectory
372. **System variation**: Technology, study, lab effects
39```
40 ┌─────────────────┐
41Input counts ──────►│ Encoder │
42 │ │
43System info ───────►│ (conditioned) │
44 └────────┬────────┘
45 │
46 ┌────────▼────────┐
47 │ Latent z │
48 │ (biological) │
49 └────────┬────────┘
50 │
51 ┌────────▼────────┐
52System info ───────►│ Decoder │
53 │ (conditioned) │
54 └────────┬────────┘
55 │
56 Reconstructed counts
57```
Understanding sysVI Architecture
sysVI separates variation into:
- Biological variation: Cell type, state, trajectory
- System variation: Technology, study, lab effects
┌─────────────────┐
Input counts ──────►│ Encoder │
│ │
System info ───────►│ (conditioned) │
└────────┬────────┘
│
┌────────▼────────┐
│ Latent z │
│ (biological) │
└────────┬────────┘
│
┌────────▼────────┐
System info ───────►│ Decoder │
│ (conditioned) │
└────────┬────────┘
│
Reconstructed countsRaw lines 33 to 58Basic sysVI Workflow
Raw lines 59 to 6061### Step 1: Prepare Data
63```python
64# Load datasets from different systems
65adata1 = sc.read_h5ad("10x_data.h5ad")
66adata2 = sc.read_h5ad("smartseq_data.h5ad")
68# Add system labels
69adata1.obs["system"] = "10x"
70adata2.obs["system"] = "Smart-seq2"
72# Add batch labels (within system)
73# e.g., different samples within each technology
75# Concatenate
76adata = sc.concat([adata1, adata2])
78# Store raw counts
79adata.layers["counts"] = adata.X.copy()
80```
Step 1: Prepare Data
# Load datasets from different systems
adata1 = sc.read_h5ad("10x_data.h5ad")
adata2 = sc.read_h5ad("smartseq_data.h5ad")
# Add system labels
adata1.obs["system"] = "10x"
adata2.obs["system"] = "Smart-seq2"
# Add batch labels (within system)
# e.g., different samples within each technology
# Concatenate
adata = sc.concat([adata1, adata2])
# Store raw counts
adata.layers["counts"] = adata.X.copy()Raw lines 61 to 8182### Step 2: HVG Selection
84```python
85# Select HVGs considering both batch and system
86sc.pp.highly_variable_genes(
87 adata,
88 n_top_genes=4000, # More genes for cross-system
89 flavor="seurat_v3",
90 batch_key="system", # Consider system for HVG
91 layer="counts"
92)
94# Optionally: ensure overlap between systems
95# Check HVGs are expressed in both systems
96adata = adata[:, adata.var["highly_variable"]].copy()
97```
Step 2: HVG Selection
# Select HVGs considering both batch and system
sc.pp.highly_variable_genes(
adata,
n_top_genes=4000, # More genes for cross-system
flavor="seurat_v3",
batch_key="system", # Consider system for HVG
layer="counts"
)
# Optionally: ensure overlap between systems
# Check HVGs are expressed in both systems
adata = adata[:, adata.var["highly_variable"]].copy()Raw lines 82 to 9899### Step 3: Setup and Train sysVI
101```python
102# Setup AnnData
103# Note: sysVI may be accessed differently depending on version
104# Check scvi-tools documentation for current API
106scvi.model.SCVI.setup_anndata(
107 adata,
108 layer="counts",
109 batch_key="sample", # Within-system batches
110 categorical_covariate_keys=["system"] # System-level covariate
111)
113# For true sysVI (if available in your version)
114# scvi.model.SysVI.setup_anndata(...)
116# Create model with system awareness
117model = scvi.model.SCVI(
118 adata,
119 n_latent=30,
120 n_layers=2,
121 gene_likelihood="nb"
122)
124# Train
125model.train(max_epochs=300)
126```
Step 3: Setup and Train sysVI
# Setup AnnData
# Note: sysVI may be accessed differently depending on version
# Check scvi-tools documentation for current API
scvi.model.SCVI.setup_anndata(
adata,
layer="counts",
batch_key="sample", # Within-system batches
categorical_covariate_keys=["system"] # System-level covariate
)
# For true sysVI (if available in your version)
# scvi.model.SysVI.setup_anndata(...)
# Create model with system awareness
model = scvi.model.SCVI(
adata,
n_latent=30,
n_layers=2,
gene_likelihood="nb"
)
# Train
model.train(max_epochs=300)Raw lines 99 to 127128### Step 4: Extract Representations
130```python
131# Get latent representation
132adata.obsm["X_integrated"] = model.get_latent_representation()
134# Clustering and visualization
135sc.pp.neighbors(adata, use_rep="X_integrated")
136sc.tl.umap(adata)
137sc.tl.leiden(adata)
139# Check integration
140sc.pl.umap(adata, color=["system", "leiden", "cell_type"])
141```
Step 4: Extract Representations
# Get latent representation
adata.obsm["X_integrated"] = model.get_latent_representation()
# Clustering and visualization
sc.pp.neighbors(adata, use_rep="X_integrated")
sc.tl.umap(adata)
sc.tl.leiden(adata)
# Check integration
sc.pl.umap(adata, color=["system", "leiden", "cell_type"])Raw lines 128 to 142143## Alternative: Harmony + scVI
145For cross-system integration, combining methods can work well:
147```python
148import scanpy.external as sce
150# First run PCA
151sc.pp.pca(adata)
153# Apply Harmony for initial alignment
154sce.pp.harmony_integrate(adata, key="system")
156# Then train scVI on Harmony-corrected embedding
157# Or use Harmony representation directly
158```
Alternative: Harmony + scVI
For cross-system integration, combining methods can work well:
import scanpy.external as sce
# First run PCA
sc.pp.pca(adata)
# Apply Harmony for initial alignment
sce.pp.harmony_integrate(adata, key="system")
# Then train scVI on Harmony-corrected embedding
# Or use Harmony representation directlyRaw lines 143 to 159160## Alternative: Using Covariates in scVI
162For moderate system effects:
164```python
165# Include system as categorical covariate
166scvi.model.SCVI.setup_anndata(
167 adata,
168 layer="counts",
169 batch_key="sample",
170 categorical_covariate_keys=["system", "technology_version"]
171)
173model = scvi.model.SCVI(adata, n_latent=30)
174model.train()
175```
Alternative: Using Covariates in scVI
For moderate system effects:
# Include system as categorical covariate
scvi.model.SCVI.setup_anndata(
adata,
layer="counts",
batch_key="sample",
categorical_covariate_keys=["system", "technology_version"]
)
model = scvi.model.SCVI(adata, n_latent=30)
model.train()Raw lines 160 to 176177## Alternative: Separate Models + Integration
179For very different systems:
181```python
182# Train separate models
183scvi.model.SCVI.setup_anndata(adata1, layer="counts", batch_key="sample")
184model1 = scvi.model.SCVI(adata1)
185model1.train()
187scvi.model.SCVI.setup_anndata(adata2, layer="counts", batch_key="sample")
188model2 = scvi.model.SCVI(adata2)
189model2.train()
191# Get latent spaces
192adata1.obsm["X_scVI"] = model1.get_latent_representation()
193adata2.obsm["X_scVI"] = model2.get_latent_representation()
195# Align with CCA or Harmony
196# ... additional alignment step
197```
Alternative: Separate Models + Integration
For very different systems:
# Train separate models
scvi.model.SCVI.setup_anndata(adata1, layer="counts", batch_key="sample")
model1 = scvi.model.SCVI(adata1)
model1.train()
scvi.model.SCVI.setup_anndata(adata2, layer="counts", batch_key="sample")
model2 = scvi.model.SCVI(adata2)
model2.train()
# Get latent spaces
adata1.obsm["X_scVI"] = model1.get_latent_representation()
adata2.obsm["X_scVI"] = model2.get_latent_representation()
# Align with CCA or Harmony
# ... additional alignment stepRaw lines 177 to 198Evaluating Cross-System Integration
Raw lines 199 to 200201### Visual Assessment
203```python
204import matplotlib.pyplot as plt
206fig, axes = plt.subplots(1, 3, figsize=(15, 4))
208# Color by system
209sc.pl.umap(adata, color="system", ax=axes[0], show=False, title="By System")
211# Color by cell type
212sc.pl.umap(adata, color="cell_type", ax=axes[1], show=False, title="By Cell Type")
214# Color by expression of marker
215sc.pl.umap(adata, color="CD3D", ax=axes[2], show=False, title="CD3D Expression")
217plt.tight_layout()
218```
Visual Assessment
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Color by system
sc.pl.umap(adata, color="system", ax=axes[0], show=False, title="By System")
# Color by cell type
sc.pl.umap(adata, color="cell_type", ax=axes[1], show=False, title="By Cell Type")
# Color by expression of marker
sc.pl.umap(adata, color="CD3D", ax=axes[2], show=False, title="CD3D Expression")
plt.tight_layout()Raw lines 201 to 219220### Quantitative Metrics
222```python
223# Using scib-metrics
224from scib_metrics.benchmark import Benchmarker
226bm = Benchmarker(
227 adata,
228 batch_key="system",
229 label_key="cell_type",
230 embedding_obsm_keys=["X_integrated"]
231)
233bm.benchmark()
235# Key metrics:
236# - Batch mixing (ASW_batch, Graph connectivity)
237# - Bio conservation (NMI, ARI, ASW_label)
238```
Quantitative Metrics
# Using scib-metrics
from scib_metrics.benchmark import Benchmarker
bm = Benchmarker(
adata,
batch_key="system",
label_key="cell_type",
embedding_obsm_keys=["X_integrated"]
)
bm.benchmark()
# Key metrics:
# - Batch mixing (ASW_batch, Graph connectivity)
# - Bio conservation (NMI, ARI, ASW_label)Raw lines 220 to 239240### LISI Scores
242```python
243# Local Inverse Simpson's Index
244from scib_metrics import lisi
246# Batch LISI (higher = better mixing)
247batch_lisi = lisi.ilisi_graph(
248 adata,
249 batch_key="system",
250 use_rep="X_integrated"
251)
253# Cell type LISI (lower = better preservation)
254ct_lisi = lisi.clisi_graph(
255 adata,
256 label_key="cell_type",
257 use_rep="X_integrated"
258)
260print(f"Batch LISI: {batch_lisi.mean():.3f}")
261print(f"Cell type LISI: {ct_lisi.mean():.3f}")
262```
LISI Scores
# Local Inverse Simpson's Index
from scib_metrics import lisi
# Batch LISI (higher = better mixing)
batch_lisi = lisi.ilisi_graph(
adata,
batch_key="system",
use_rep="X_integrated"
)
# Cell type LISI (lower = better preservation)
ct_lisi = lisi.clisi_graph(
adata,
label_key="cell_type",
use_rep="X_integrated"
)
print(f"Batch LISI: {batch_lisi.mean():.3f}")
print(f"Cell type LISI: {ct_lisi.mean():.3f}")Raw lines 240 to 263Handling Specific Challenges
Raw lines 264 to 265266### Different Gene Sets
268```python
269# Find common genes
270common_genes = adata1.var_names.intersection(adata2.var_names)
271print(f"Common genes: {len(common_genes)}")
273# If too few, use gene mapping
274# Or impute missing genes
275```
Different Gene Sets
# Find common genes
common_genes = adata1.var_names.intersection(adata2.var_names)
print(f"Common genes: {len(common_genes)}")
# If too few, use gene mapping
# Or impute missing genesRaw lines 266 to 276277### Different Sequencing Depths
279```python
280# Add depth as continuous covariate
281adata.obs["log_counts"] = np.log1p(adata.obs["total_counts"])
283scvi.model.SCVI.setup_anndata(
284 adata,
285 layer="counts",
286 batch_key="sample",
287 continuous_covariate_keys=["log_counts"]
288)
289```
Different Sequencing Depths
# Add depth as continuous covariate
adata.obs["log_counts"] = np.log1p(adata.obs["total_counts"])
scvi.model.SCVI.setup_anndata(
adata,
layer="counts",
batch_key="sample",
continuous_covariate_keys=["log_counts"]
)Raw lines 277 to 290291### Unbalanced Cell Types
293```python
294# Check cell type distribution per system
295import pandas as pd
297ct_dist = pd.crosstab(adata.obs["system"], adata.obs["cell_type"], normalize="index")
298print(ct_dist)
300# If very unbalanced, consider:
301# 1. Subsample to balance
302# 2. Use scANVI with labels to preserve rare types
303```
Unbalanced Cell Types
# Check cell type distribution per system
import pandas as pd
ct_dist = pd.crosstab(adata.obs["system"], adata.obs["cell_type"], normalize="index")
print(ct_dist)
# If very unbalanced, consider:
# 1. Subsample to balance
# 2. Use scANVI with labels to preserve rare typesRaw lines 291 to 304305## Complete Pipeline
307```python
308def integrate_cross_system(
309 adatas: dict,
310 system_key: str = "system",
311 batch_key: str = "batch",
312 cell_type_key: str = "cell_type",
313 n_top_genes: int = 4000,
314 n_latent: int = 30
315):
316 """
317 Integrate datasets from different technological systems.
319 Parameters
320 ----------
321 adatas : dict
322 Dictionary of {system_name: AnnData}
323 system_key : str
324 Key for system annotation
325 batch_key : str
326 Key for within-system batch
327 cell_type_key : str
328 Key for cell type labels (optional)
329 n_top_genes : int
330 Number of HVGs
331 n_latent : int
332 Latent dimensions
334 Returns
335 -------
336 Integrated AnnData with model
337 """
338 import scvi
339 import scanpy as sc
341 # Add system labels and concatenate
342 for system_name, adata in adatas.items():
343 adata.obs[system_key] = system_name
345 adata = sc.concat(list(adatas.values()))
347 # Find common genes
348 for name, ad in adatas.items():
349 if name == list(adatas.keys())[0]:
350 common_genes = set(ad.var_names)
351 else:
352 common_genes = common_genes.intersection(ad.var_names)
354 adata = adata[:, list(common_genes)].copy()
355 print(f"Common genes: {len(common_genes)}")
357 # Store counts
358 adata.layers["counts"] = adata.X.copy()
360 # HVG selection
361 sc.pp.highly_variable_genes(
362 adata,
363 n_top_genes=n_top_genes,
364 flavor="seurat_v3",
365 batch_key=system_key,
366 layer="counts"
367 )
368 adata = adata[:, adata.var["highly_variable"]].copy()
370 # Setup with system as covariate
371 scvi.model.SCVI.setup_anndata(
372 adata,
373 layer="counts",
374 batch_key=batch_key if batch_key in adata.obs else None,
375 categorical_covariate_keys=[system_key]
376 )
378 # Train
379 model = scvi.model.SCVI(adata, n_latent=n_latent, n_layers=2)
380 model.train(max_epochs=300, early_stopping=True)
382 # Get representation
383 adata.obsm["X_integrated"] = model.get_latent_representation()
385 # Clustering
386 sc.pp.neighbors(adata, use_rep="X_integrated")
387 sc.tl.umap(adata)
388 sc.tl.leiden(adata)
390 return adata, model
392# Usage
393adatas = {
394 "10x_v3": sc.read_h5ad("10x_v3_data.h5ad"),
395 "Smart-seq2": sc.read_h5ad("smartseq_data.h5ad"),
396 "Drop-seq": sc.read_h5ad("dropseq_data.h5ad")
397}
399adata_integrated, model = integrate_cross_system(adatas)
401# Visualize
402sc.pl.umap(adata_integrated, color=["system", "leiden"])
403```
Complete Pipeline
def integrate_cross_system(
adatas: dict,
system_key: str = "system",
batch_key: str = "batch",
cell_type_key: str = "cell_type",
n_top_genes: int = 4000,
n_latent: int = 30
):
"""
Integrate datasets from different technological systems.
Parameters
----------
adatas : dict
Dictionary of {system_name: AnnData}
system_key : str
Key for system annotation
batch_key : str
Key for within-system batch
cell_type_key : str
Key for cell type labels (optional)
n_top_genes : int
Number of HVGs
n_latent : int
Latent dimensions
Returns
-------
Integrated AnnData with model
"""
import scvi
import scanpy as sc
# Add system labels and concatenate
for system_name, adata in adatas.items():
adata.obs[system_key] = system_name
adata = sc.concat(list(adatas.values()))
# Find common genes
for name, ad in adatas.items():
if name == list(adatas.keys())[0]:
common_genes = set(ad.var_names)
else:
common_genes = common_genes.intersection(ad.var_names)
adata = adata[:, list(common_genes)].copy()
print(f"Common genes: {len(common_genes)}")
# Store counts
adata.layers["counts"] = adata.X.copy()
# HVG selection
sc.pp.highly_variable_genes(
adata,
n_top_genes=n_top_genes,
flavor="seurat_v3",
batch_key=system_key,
layer="counts"
)
adata = adata[:, adata.var["highly_variable"]].copy()
# Setup with system as covariate
scvi.model.SCVI.setup_anndata(
adata,
layer="counts",
batch_key=batch_key if batch_key in adata.obs else None,
categorical_covariate_keys=[system_key]
)
# Train
model = scvi.model.SCVI(adata, n_latent=n_latent, n_layers=2)
model.train(max_epochs=300, early_stopping=True)
# Get representation
adata.obsm["X_integrated"] = model.get_latent_representation()
# Clustering
sc.pp.neighbors(adata, use_rep="X_integrated")
sc.tl.umap(adata)
sc.tl.leiden(adata)
return adata, model
# Usage
adatas = {
"10x_v3": sc.read_h5ad("10x_v3_data.h5ad"),
"Smart-seq2": sc.read_h5ad("smartseq_data.h5ad"),
"Drop-seq": sc.read_h5ad("dropseq_data.h5ad")
}
adata_integrated, model = integrate_cross_system(adatas)
# Visualize
sc.pl.umap(adata_integrated, color=["system", "leiden"])Raw lines 305 to 404405## Troubleshooting
407| Issue | Cause | Solution |
408|-------|-------|----------|
409| Systems don't mix | Effects too strong | Use more genes, increase n_latent |
410| Over-correction | Model too aggressive | Reduce n_layers, use scANVI |
411| Few common genes | Different platforms | Use gene name mapping |
412| One system dominates | Unbalanced sizes | Subsample larger dataset |
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Systems don't mix | Effects too strong | Use more genes, increase n_latent |
| Over-correction | Model too aggressive | Reduce n_layers, use scANVI |
| Few common genes | Different platforms | Use gene name mapping |
| One system dominates | Unbalanced sizes | Subsample larger dataset |
414## Key References
416- Lopez et al. (2018) "Deep generative modeling for single-cell transcriptomics"
417- Luecken et al. (2022) "Benchmarking atlas-level data integration in single-cell genomics"
Key References
- Lopez et al. (2018) "Deep generative modeling for single-cell transcriptomics"
- Luecken et al. (2022) "Benchmarking atlas-level data integration in single-cell genomics"