Skill Tracer
Skills › Knowledge Work Plugins › scvi-tools › scrna_integration.md

scrna_integration.md

429 lines · bio-research/skills/scvi-tools/references/scrna_integration.md at 2574e29

Open on GitHub
1# scRNA-seq Integration with scVI and scANVI
2
3This reference covers batch correction and dataset integration using scVI (unsupervised) and scANVI (semi-supervised with cell type labels).
4

scRNA-seq Integration with scVI and scANVI

This reference covers batch correction and dataset integration using scVI (unsupervised) and scANVI (semi-supervised with cell type labels).

Raw lines 1 to 4
5## Overview
6
7Single-cell datasets often have batch effects from:
8- Different donors/patients
9- Different experimental batches
10- Different technologies (10x v2 vs v3)
11- Different studies
13scVI and scANVI learn a shared latent space where batch effects are removed while biological variation is preserved.

Overview

Single-cell datasets often have batch effects from:

  • Different donors/patients
  • Different experimental batches
  • Different technologies (10x v2 vs v3)
  • Different studies

scVI and scANVI learn a shared latent space where batch effects are removed while biological variation is preserved.

Raw lines 5 to 14
15## When to Use Which Model
17| Model | Use When | Labels Needed |
18|-------|----------|---------------|
19| **scVI** | No labels available, exploratory analysis | No |
20| **scANVI** | Have partial/full labels, want better preservation | Yes (partial OK) |

When to Use Which Model

ModelUse WhenLabels Needed
scVINo labels available, exploratory analysisNo
scANVIHave partial/full labels, want better preservationYes (partial OK)
Raw lines 15 to 21
22## scVI Integration Workflow

scVI Integration Workflow

Raw lines 22 to 23
24### Step 1: Prepare Data
26```python
27import scvi
28import scanpy as sc
30# Load datasets
31adata1 = sc.read_h5ad("dataset1.h5ad")
32adata2 = sc.read_h5ad("dataset2.h5ad")
34# Add batch annotation
35adata1.obs["batch"] = "batch1"
36adata2.obs["batch"] = "batch2"
38# Concatenate
39adata = sc.concat([adata1, adata2], label="batch")
41# Ensure we have raw counts
42# If data is normalized, recover from .raw
43if hasattr(adata, 'raw') and adata.raw is not None:
44 adata = adata.raw.to_adata()
46# Store counts
47adata.layers["counts"] = adata.X.copy()
48```

Step 1: Prepare Data

import scvi
import scanpy as sc

# Load datasets
adata1 = sc.read_h5ad("dataset1.h5ad")
adata2 = sc.read_h5ad("dataset2.h5ad")

# Add batch annotation
adata1.obs["batch"] = "batch1"
adata2.obs["batch"] = "batch2"

# Concatenate
adata = sc.concat([adata1, adata2], label="batch")

# Ensure we have raw counts
# If data is normalized, recover from .raw
if hasattr(adata, 'raw') and adata.raw is not None:
    adata = adata.raw.to_adata()

# Store counts
adata.layers["counts"] = adata.X.copy()
Raw lines 24 to 49
50### Step 2: HVG Selection Across Batches
52```python
53# Select HVGs considering batch
54sc.pp.highly_variable_genes(
55 adata,
56 n_top_genes=2000,
57 flavor="seurat_v3",
58 batch_key="batch",
59 layer="counts"
62# Subset to HVGs
63adata = adata[:, adata.var["highly_variable"]].copy()
64```

Step 2: HVG Selection Across Batches

# Select HVGs considering batch
sc.pp.highly_variable_genes(
    adata,
    n_top_genes=2000,
    flavor="seurat_v3",
    batch_key="batch",
    layer="counts"
)

# Subset to HVGs
adata = adata[:, adata.var["highly_variable"]].copy()
Raw lines 50 to 65
66### Step 3: Setup and Train scVI
68```python
69# Register data with scVI
70scvi.model.SCVI.setup_anndata(
71 adata,
72 layer="counts",
73 batch_key="batch"
76# Create model
77model = scvi.model.SCVI(
78 adata,
79 n_latent=30, # Latent dimensions
80 n_layers=2, # Encoder/decoder depth
81 gene_likelihood="nb" # negative binomial (or "zinb")
84# Train
85model.train(
86 max_epochs=200,
87 early_stopping=True,
88 early_stopping_patience=10,
89 batch_size=128
92# Plot training history
93model.history["elbo_train"].plot()
94```

Step 3: Setup and Train scVI

# Register data with scVI
scvi.model.SCVI.setup_anndata(
    adata,
    layer="counts",
    batch_key="batch"
)

# Create model
model = scvi.model.SCVI(
    adata,
    n_latent=30,          # Latent dimensions
    n_layers=2,           # Encoder/decoder depth
    gene_likelihood="nb"  # negative binomial (or "zinb")
)

# Train
model.train(
    max_epochs=200,
    early_stopping=True,
    early_stopping_patience=10,
    batch_size=128
)

# Plot training history
model.history["elbo_train"].plot()
Raw lines 66 to 95
96### Step 4: Get Integrated Representation
98```python
99# Get latent representation
100adata.obsm["X_scVI"] = model.get_latent_representation()
102# Use for clustering and visualization
103sc.pp.neighbors(adata, use_rep="X_scVI", n_neighbors=15)
104sc.tl.umap(adata)
105sc.tl.leiden(adata, resolution=1.0)
107# Visualize integration
108sc.pl.umap(adata, color=["batch", "leiden"], ncols=2)
109```

Step 4: Get Integrated Representation

# Get latent representation
adata.obsm["X_scVI"] = model.get_latent_representation()

# Use for clustering and visualization
sc.pp.neighbors(adata, use_rep="X_scVI", n_neighbors=15)
sc.tl.umap(adata)
sc.tl.leiden(adata, resolution=1.0)

# Visualize integration
sc.pl.umap(adata, color=["batch", "leiden"], ncols=2)
Raw lines 96 to 110
111### Step 5: Save Model
113```python
114# Save model for later use
115model.save("scvi_model/")
117# Load model
118model = scvi.model.SCVI.load("scvi_model/", adata=adata)
119```

Step 5: Save Model

# Save model for later use
model.save("scvi_model/")

# Load model
model = scvi.model.SCVI.load("scvi_model/", adata=adata)
Raw lines 111 to 120
121## scANVI Integration Workflow
123scANVI extends scVI with cell type labels for better biological preservation.

scANVI Integration Workflow

scANVI extends scVI with cell type labels for better biological preservation.

Raw lines 121 to 124
125### Step 1: Prepare Data with Labels
127```python
128# Labels should be in adata.obs
129# Use "Unknown" for unlabeled cells
130print(adata.obs["cell_type"].value_counts())
132# For partially labeled data
133# Mark unlabeled cells
134adata.obs["cell_type_scanvi"] = adata.obs["cell_type"].copy()
135# adata.obs.loc[unlabeled_mask, "cell_type_scanvi"] = "Unknown"
136```

Step 1: Prepare Data with Labels

# Labels should be in adata.obs
# Use "Unknown" for unlabeled cells
print(adata.obs["cell_type"].value_counts())

# For partially labeled data
# Mark unlabeled cells
adata.obs["cell_type_scanvi"] = adata.obs["cell_type"].copy()
# adata.obs.loc[unlabeled_mask, "cell_type_scanvi"] = "Unknown"
Raw lines 125 to 137
138### Step 2: Option A - Train scANVI from Scratch
140```python
141# Setup for scANVI
142scvi.model.SCANVI.setup_anndata(
143 adata,
144 layer="counts",
145 batch_key="batch",
146 labels_key="cell_type"
149# Create model
150scanvi_model = scvi.model.SCANVI(
151 adata,
152 n_latent=30,
153 n_layers=2
156# Train
157scanvi_model.train(max_epochs=200)
158```

Step 2: Option A - Train scANVI from Scratch

# Setup for scANVI
scvi.model.SCANVI.setup_anndata(
    adata,
    layer="counts",
    batch_key="batch",
    labels_key="cell_type"
)

# Create model
scanvi_model = scvi.model.SCANVI(
    adata,
    n_latent=30,
    n_layers=2
)

# Train
scanvi_model.train(max_epochs=200)
Raw lines 138 to 159
160### Step 2: Option B - Initialize scANVI from scVI (Recommended)
162```python
163# First train scVI
164scvi.model.SCVI.setup_anndata(adata, layer="counts", batch_key="batch")
165scvi_model = scvi.model.SCVI(adata, n_latent=30)
166scvi_model.train(max_epochs=200)
168# Initialize scANVI from scVI
169scanvi_model = scvi.model.SCANVI.from_scvi_model(
170 scvi_model,
171 labels_key="cell_type",
172 unlabeled_category="Unknown" # For partially labeled data
175# Fine-tune scANVI (fewer epochs needed)
176scanvi_model.train(max_epochs=50)
177```

Step 2: Option B - Initialize scANVI from scVI (Recommended)

# First train scVI
scvi.model.SCVI.setup_anndata(adata, layer="counts", batch_key="batch")
scvi_model = scvi.model.SCVI(adata, n_latent=30)
scvi_model.train(max_epochs=200)

# Initialize scANVI from scVI
scanvi_model = scvi.model.SCANVI.from_scvi_model(
    scvi_model,
    labels_key="cell_type",
    unlabeled_category="Unknown"  # For partially labeled data
)

# Fine-tune scANVI (fewer epochs needed)
scanvi_model.train(max_epochs=50)
Raw lines 160 to 178
179### Step 3: Get Results
181```python
182# Latent representation
183adata.obsm["X_scANVI"] = scanvi_model.get_latent_representation()
185# Predicted labels for unlabeled cells
186predictions = scanvi_model.predict()
187adata.obs["predicted_cell_type"] = predictions
189# Prediction probabilities
190soft_predictions = scanvi_model.predict(soft=True)
192# Visualization
193sc.pp.neighbors(adata, use_rep="X_scANVI")
194sc.tl.umap(adata)
195sc.pl.umap(adata, color=["batch", "cell_type", "predicted_cell_type"])
196```

Step 3: Get Results

# Latent representation
adata.obsm["X_scANVI"] = scanvi_model.get_latent_representation()

# Predicted labels for unlabeled cells
predictions = scanvi_model.predict()
adata.obs["predicted_cell_type"] = predictions

# Prediction probabilities
soft_predictions = scanvi_model.predict(soft=True)

# Visualization
sc.pp.neighbors(adata, use_rep="X_scANVI")
sc.tl.umap(adata)
sc.pl.umap(adata, color=["batch", "cell_type", "predicted_cell_type"])
Raw lines 179 to 197
198## Comparing Integration Quality

Comparing Integration Quality

Raw lines 198 to 199
200### Visual Assessment
202```python
203import matplotlib.pyplot as plt
205fig, axes = plt.subplots(1, 3, figsize=(15, 4))
207# Before integration (on PCA)
208sc.pp.pca(adata)
209sc.pl.pca(adata, color="batch", ax=axes[0], title="Before (PCA)", show=False)
211# After scVI
212sc.pp.neighbors(adata, use_rep="X_scVI")
213sc.tl.umap(adata)
214sc.pl.umap(adata, color="batch", ax=axes[1], title="After scVI", show=False)
216# After scANVI
217sc.pp.neighbors(adata, use_rep="X_scANVI")
218sc.tl.umap(adata)
219sc.pl.umap(adata, color="batch", ax=axes[2], title="After scANVI", show=False)
221plt.tight_layout()
222```

Visual Assessment

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# Before integration (on PCA)
sc.pp.pca(adata)
sc.pl.pca(adata, color="batch", ax=axes[0], title="Before (PCA)", show=False)

# After scVI
sc.pp.neighbors(adata, use_rep="X_scVI")
sc.tl.umap(adata)
sc.pl.umap(adata, color="batch", ax=axes[1], title="After scVI", show=False)

# After scANVI
sc.pp.neighbors(adata, use_rep="X_scANVI")
sc.tl.umap(adata)
sc.pl.umap(adata, color="batch", ax=axes[2], title="After scANVI", show=False)

plt.tight_layout()
Raw lines 200 to 223
224### Quantitative Metrics (scib)
226```python
227# pip install scib-metrics
229from scib_metrics.benchmark import Benchmarker
231bm = Benchmarker(
232 adata,
233 batch_key="batch",
234 label_key="cell_type",
235 embedding_obsm_keys=["X_pca", "X_scVI", "X_scANVI"]
238bm.benchmark()
239bm.plot_results_table()
240```

Quantitative Metrics (scib)

# pip install scib-metrics

from scib_metrics.benchmark import Benchmarker

bm = Benchmarker(
    adata,
    batch_key="batch",
    label_key="cell_type",
    embedding_obsm_keys=["X_pca", "X_scVI", "X_scANVI"]
)

bm.benchmark()
bm.plot_results_table()
Raw lines 224 to 241
242## Differential Expression
244scVI provides differential expression that accounts for batch effects:
246```python
247# DE between groups
248de_results = model.differential_expression(
249 groupby="cell_type",
250 group1="T cells",
251 group2="B cells"
254# Filter significant
255de_sig = de_results[
256 (de_results["is_de_fdr_0.05"] == True) &
257 (abs(de_results["lfc_mean"]) > 1)
260print(de_sig.head(20))
261```

Differential Expression

scVI provides differential expression that accounts for batch effects:

# DE between groups
de_results = model.differential_expression(
    groupby="cell_type",
    group1="T cells",
    group2="B cells"
)

# Filter significant
de_sig = de_results[
    (de_results["is_de_fdr_0.05"] == True) &
    (abs(de_results["lfc_mean"]) > 1)
]

print(de_sig.head(20))
Raw lines 242 to 262
263## Advanced: Multiple Categorical Covariates
265```python
266# Include additional covariates beyond batch
267scvi.model.SCVI.setup_anndata(
268 adata,
269 layer="counts",
270 batch_key="batch",
271 categorical_covariate_keys=["donor", "technology"]
274model = scvi.model.SCVI(adata, n_latent=30)
275model.train()
276```

Advanced: Multiple Categorical Covariates

# Include additional covariates beyond batch
scvi.model.SCVI.setup_anndata(
    adata,
    layer="counts",
    batch_key="batch",
    categorical_covariate_keys=["donor", "technology"]
)

model = scvi.model.SCVI(adata, n_latent=30)
model.train()
Raw lines 263 to 277
278## Training Tips
280### For Large Datasets (>100k cells)
282```python
283model.train(
284 max_epochs=100, # Fewer epochs needed
285 batch_size=256, # Larger batches
286 train_size=0.9, # Less validation
287 early_stopping=True
289```

For Large Datasets (>100k cells)

model.train(
    max_epochs=100,      # Fewer epochs needed
    batch_size=256,      # Larger batches
    train_size=0.9,      # Less validation
    early_stopping=True
)
Raw lines 280 to 290
291### For Small Datasets (<10k cells)
293```python
294model = scvi.model.SCVI(
295 adata,
296 n_latent=10, # Smaller latent space
297 n_layers=1, # Simpler model
298 dropout_rate=0.2 # More regularization
301model.train(
302 max_epochs=400,
303 batch_size=64
305```

For Small Datasets (<10k cells)

model = scvi.model.SCVI(
    adata,
    n_latent=10,         # Smaller latent space
    n_layers=1,          # Simpler model
    dropout_rate=0.2     # More regularization
)

model.train(
    max_epochs=400,
    batch_size=64
)
Raw lines 291 to 306
307### Monitoring Training
309```python
310# Check training curves
311import matplotlib.pyplot as plt
313fig, ax = plt.subplots()
314ax.plot(model.history["elbo_train"], label="Train")
315ax.plot(model.history["elbo_validation"], label="Validation")
316ax.set_xlabel("Epoch")
317ax.set_ylabel("ELBO")
318ax.legend()
320# Should see convergence without overfitting
321```

Monitoring Training

# Check training curves
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot(model.history["elbo_train"], label="Train")
ax.plot(model.history["elbo_validation"], label="Validation")
ax.set_xlabel("Epoch")
ax.set_ylabel("ELBO")
ax.legend()

# Should see convergence without overfitting
Raw lines 307 to 322
323## Complete Pipeline
325```python
326def integrate_datasets(
327 adatas,
328 batch_key="batch",
329 labels_key=None,
330 n_top_genes=2000,
331 n_latent=30
332):
333 """
334 Integrate multiple scRNA-seq datasets.
336 Parameters
337 ----------
338 adatas : dict
339 Dictionary of {batch_name: AnnData}
340 batch_key : str
341 Key for batch annotation
342 labels_key : str, optional
343 Key for cell type labels (uses scANVI if provided)
344 n_top_genes : int
345 Number of HVGs
346 n_latent : int
347 Latent dimensions
349 Returns
350 -------
351 AnnData with integrated representation
352 """
353 import scvi
354 import scanpy as sc
356 # Add batch labels and concatenate
357 for batch_name, adata in adatas.items():
358 adata.obs[batch_key] = batch_name
360 adata = sc.concat(list(adatas.values()), label=batch_key)
362 # Store counts
363 adata.layers["counts"] = adata.X.copy()
365 # HVG selection
366 sc.pp.highly_variable_genes(
367 adata,
368 n_top_genes=n_top_genes,
369 flavor="seurat_v3",
370 batch_key=batch_key,
371 layer="counts"
372 )
373 adata = adata[:, adata.var["highly_variable"]].copy()
375 # Train model
376 if labels_key and labels_key in adata.obs.columns:
377 # Use scANVI
378 scvi.model.SCVI.setup_anndata(adata, layer="counts", batch_key=batch_key)
379 scvi_model = scvi.model.SCVI(adata, n_latent=n_latent)
380 scvi_model.train(max_epochs=200)
382 model = scvi.model.SCANVI.from_scvi_model(
383 scvi_model,
384 labels_key=labels_key,
385 unlabeled_category="Unknown"
386 )
387 model.train(max_epochs=50)
388 rep_key = "X_scANVI"
389 else:
390 # Use scVI
391 scvi.model.SCVI.setup_anndata(adata, layer="counts", batch_key=batch_key)
392 model = scvi.model.SCVI(adata, n_latent=n_latent)
393 model.train(max_epochs=200)
394 rep_key = "X_scVI"
396 # Add representation
397 adata.obsm[rep_key] = model.get_latent_representation()
399 # Compute neighbors and UMAP
400 sc.pp.neighbors(adata, use_rep=rep_key)
401 sc.tl.umap(adata)
402 sc.tl.leiden(adata)
404 return adata, model
406# Usage
407adatas = {
408 "study1": sc.read_h5ad("study1.h5ad"),
409 "study2": sc.read_h5ad("study2.h5ad"),
410 "study3": sc.read_h5ad("study3.h5ad")
413adata_integrated, model = integrate_datasets(
414 adatas,
415 labels_key="cell_type"
418sc.pl.umap(adata_integrated, color=["batch", "leiden", "cell_type"])
419```

Complete Pipeline

def integrate_datasets(
    adatas,
    batch_key="batch",
    labels_key=None,
    n_top_genes=2000,
    n_latent=30
):
    """
    Integrate multiple scRNA-seq datasets.
    
    Parameters
    ----------
    adatas : dict
        Dictionary of {batch_name: AnnData}
    batch_key : str
        Key for batch annotation
    labels_key : str, optional
        Key for cell type labels (uses scANVI if provided)
    n_top_genes : int
        Number of HVGs
    n_latent : int
        Latent dimensions
        
    Returns
    -------
    AnnData with integrated representation
    """
    import scvi
    import scanpy as sc
    
    # Add batch labels and concatenate
    for batch_name, adata in adatas.items():
        adata.obs[batch_key] = batch_name
    
    adata = sc.concat(list(adatas.values()), label=batch_key)
    
    # 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=batch_key,
        layer="counts"
    )
    adata = adata[:, adata.var["highly_variable"]].copy()
    
    # Train model
    if labels_key and labels_key in adata.obs.columns:
        # Use scANVI
        scvi.model.SCVI.setup_anndata(adata, layer="counts", batch_key=batch_key)
        scvi_model = scvi.model.SCVI(adata, n_latent=n_latent)
        scvi_model.train(max_epochs=200)
        
        model = scvi.model.SCANVI.from_scvi_model(
            scvi_model,
            labels_key=labels_key,
            unlabeled_category="Unknown"
        )
        model.train(max_epochs=50)
        rep_key = "X_scANVI"
    else:
        # Use scVI
        scvi.model.SCVI.setup_anndata(adata, layer="counts", batch_key=batch_key)
        model = scvi.model.SCVI(adata, n_latent=n_latent)
        model.train(max_epochs=200)
        rep_key = "X_scVI"
    
    # Add representation
    adata.obsm[rep_key] = model.get_latent_representation()
    
    # Compute neighbors and UMAP
    sc.pp.neighbors(adata, use_rep=rep_key)
    sc.tl.umap(adata)
    sc.tl.leiden(adata)
    
    return adata, model

# Usage
adatas = {
    "study1": sc.read_h5ad("study1.h5ad"),
    "study2": sc.read_h5ad("study2.h5ad"),
    "study3": sc.read_h5ad("study3.h5ad")
}

adata_integrated, model = integrate_datasets(
    adatas,
    labels_key="cell_type"
)

sc.pl.umap(adata_integrated, color=["batch", "leiden", "cell_type"])
Raw lines 323 to 420
421## Troubleshooting
423| Issue | Cause | Solution |
424|-------|-------|----------|
425| Batches not mixing | Too few shared genes | Use more HVGs, check gene overlap |
426| Over-correction | Biological variation removed | Use scANVI with labels |
427| Training diverges | Learning rate too high | Reduce lr, increase batch_size |
428| NaN loss | Bad data | Check for all-zero cells/genes |
429| Memory error | Too many cells | Reduce batch_size, use GPU |

Troubleshooting

IssueCauseSolution
Batches not mixingToo few shared genesUse more HVGs, check gene overlap
Over-correctionBiological variation removedUse scANVI with labels
Training divergesLearning rate too highReduce lr, increase batch_size
NaN lossBad dataCheck for all-zero cells/genes
Memory errorToo many cellsReduce batch_size, use GPU
Raw lines 421 to 429