Skill Tracer
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

Open on GitHub
1# Advanced Batch Correction with sysVI
2
3This reference covers system-level batch correction using sysVI, designed for integrating data across major technological or study differences.
4

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 4
5## Overview
6
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
Raw lines 5 to 11
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

ScenarioRecommended Model
Same technology, different samplesscVI
10x v2 vs 10x v3scVI (usually)
10x vs Smart-seq2sysVI
Different sequencing depthsscVI with covariates
Cross-study integrationsysVI
Atlas-scale integrationsysVI
Raw lines 12 to 22
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 32
33## 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:

  1. Biological variation: Cell type, state, trajectory
  2. System variation: Technology, study, lab effects
                    ┌─────────────────┐
Input counts ──────►│    Encoder      │
                    │                 │
System info ───────►│  (conditioned)  │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │   Latent z      │
                    │  (biological)   │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
System info ───────►│    Decoder      │
                    │  (conditioned)  │
                    └────────┬────────┘
                             │
                    Reconstructed counts
Raw lines 33 to 58
59## Basic sysVI Workflow

Basic sysVI Workflow

Raw lines 59 to 60
61### 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 81
82### 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"
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 98
99### 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
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"
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 127
128### 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 142
143## 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 directly
Raw lines 143 to 159
160## 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"]
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 176
177## 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 step
Raw lines 177 to 198
199## Evaluating Cross-System Integration

Evaluating Cross-System Integration

Raw lines 199 to 200
201### 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 219
220### 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"]
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 239
240### 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"
253# Cell type LISI (lower = better preservation)
254ct_lisi = lisi.clisi_graph(
255 adata,
256 label_key="cell_type",
257 use_rep="X_integrated"
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 263
264## Handling Specific Challenges

Handling Specific Challenges

Raw lines 264 to 265
266### 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 genes
Raw lines 266 to 276
277### 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"]
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 290
291### 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 types
Raw lines 291 to 304
305## 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")
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 404
405## 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

IssueCauseSolution
Systems don't mixEffects too strongUse more genes, increase n_latent
Over-correctionModel too aggressiveReduce n_layers, use scANVI
Few common genesDifferent platformsUse gene name mapping
One system dominatesUnbalanced sizesSubsample larger dataset
Raw lines 405 to 413
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"
Raw lines 414 to 417