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

label_transfer.md

373 lines · bio-research/skills/scvi-tools/references/label_transfer.md at 2574e29

Open on GitHub
1# Label Transfer and Reference Mapping with scANVI
2
3This reference covers using scANVI for transferring cell type annotations from a reference atlas to query data.
4

Label Transfer and Reference Mapping with scANVI

This reference covers using scANVI for transferring cell type annotations from a reference atlas to query data.

Raw lines 1 to 4
5## Overview
6
7Reference mapping (also called "label transfer") uses a pre-trained model on annotated reference data to predict cell types in new, unannotated query data. This is faster than re-clustering and more consistent across studies.
8
9scANVI excels at this because it:
10- Jointly embeds reference and query in shared space
11- Transfers labels probabilistically
12- Handles batch effects between reference and query

Overview

Reference mapping (also called "label transfer") uses a pre-trained model on annotated reference data to predict cell types in new, unannotated query data. This is faster than re-clustering and more consistent across studies.

scANVI excels at this because it:

  • Jointly embeds reference and query in shared space
  • Transfers labels probabilistically
  • Handles batch effects between reference and query
Raw lines 5 to 13
14## When to Use Reference Mapping
16- Annotating new dataset using existing atlas
17- Consistent annotation across multiple studies
18- Speed: no need to re-cluster and manually annotate
19- Quality: leverage expert-curated reference annotations

When to Use Reference Mapping

  • Annotating new dataset using existing atlas
  • Consistent annotation across multiple studies
  • Speed: no need to re-cluster and manually annotate
  • Quality: leverage expert-curated reference annotations
Raw lines 14 to 20
21## Workflow Options
231. **Train new model**: Train scANVI on reference, then map query
242. **Use pre-trained model**: Load existing model (e.g., from Model Hub)
253. **scArches**: Extend existing model with query data (preserves reference)

Workflow Options

  1. Train new model: Train scANVI on reference, then map query
  2. Use pre-trained model: Load existing model (e.g., from Model Hub)
  3. scArches: Extend existing model with query data (preserves reference)
Raw lines 21 to 26
27## Option 1: Train scANVI on Reference

Option 1: Train scANVI on Reference

Raw lines 27 to 28
29### Step 1: Prepare Reference Data
31```python
32import scvi
33import scanpy as sc
35# Load reference atlas
36adata_ref = sc.read_h5ad("reference_atlas.h5ad")
38# Check annotations
39print(f"Reference cells: {adata_ref.n_obs}")
40print(f"Cell types: {adata_ref.obs['cell_type'].nunique()}")
41print(adata_ref.obs['cell_type'].value_counts())
43# Ensure raw counts
44adata_ref.layers["counts"] = adata_ref.raw.X.copy() if adata_ref.raw else adata_ref.X.copy()
46# HVG selection
47sc.pp.highly_variable_genes(
48 adata_ref,
49 n_top_genes=3000,
50 flavor="seurat_v3",
51 batch_key="batch" if "batch" in adata_ref.obs else None,
52 layer="counts"
54adata_ref = adata_ref[:, adata_ref.var["highly_variable"]].copy()
55```

Step 1: Prepare Reference Data

import scvi
import scanpy as sc

# Load reference atlas
adata_ref = sc.read_h5ad("reference_atlas.h5ad")

# Check annotations
print(f"Reference cells: {adata_ref.n_obs}")
print(f"Cell types: {adata_ref.obs['cell_type'].nunique()}")
print(adata_ref.obs['cell_type'].value_counts())

# Ensure raw counts
adata_ref.layers["counts"] = adata_ref.raw.X.copy() if adata_ref.raw else adata_ref.X.copy()

# HVG selection
sc.pp.highly_variable_genes(
    adata_ref,
    n_top_genes=3000,
    flavor="seurat_v3",
    batch_key="batch" if "batch" in adata_ref.obs else None,
    layer="counts"
)
adata_ref = adata_ref[:, adata_ref.var["highly_variable"]].copy()
Raw lines 29 to 56
57### Step 2: Train scANVI on Reference
59```python
60# First train scVI (unlabeled)
61scvi.model.SCVI.setup_anndata(
62 adata_ref,
63 layer="counts",
64 batch_key="batch"
67scvi_ref = scvi.model.SCVI(adata_ref, n_latent=30)
68scvi_ref.train(max_epochs=200)
70# Initialize scANVI from scVI
71scanvi_ref = scvi.model.SCANVI.from_scvi_model(
72 scvi_ref,
73 labels_key="cell_type",
74 unlabeled_category="Unknown"
77# Train scANVI
78scanvi_ref.train(max_epochs=50)
80# Save for later use
81scanvi_ref.save("scanvi_reference_model/")
82```

Step 2: Train scANVI on Reference

# First train scVI (unlabeled)
scvi.model.SCVI.setup_anndata(
    adata_ref,
    layer="counts",
    batch_key="batch"
)

scvi_ref = scvi.model.SCVI(adata_ref, n_latent=30)
scvi_ref.train(max_epochs=200)

# Initialize scANVI from scVI
scanvi_ref = scvi.model.SCANVI.from_scvi_model(
    scvi_ref,
    labels_key="cell_type",
    unlabeled_category="Unknown"
)

# Train scANVI
scanvi_ref.train(max_epochs=50)

# Save for later use
scanvi_ref.save("scanvi_reference_model/")
Raw lines 57 to 83
84### Step 3: Prepare Query Data
86```python
87# Load query data
88adata_query = sc.read_h5ad("query_data.h5ad")
90# CRITICAL: Use same genes as reference
91common_genes = adata_ref.var_names.intersection(adata_query.var_names)
92print(f"Common genes: {len(common_genes)}")
94# Subset query to reference genes
95adata_query = adata_query[:, adata_ref.var_names].copy()
97# Handle missing genes (set to 0)
98missing_genes = set(adata_ref.var_names) - set(adata_query.var_names)
99if missing_genes:
100 # Add missing genes with zero expression
101 import numpy as np
102 from scipy.sparse import csr_matrix
104 zero_matrix = csr_matrix((adata_query.n_obs, len(missing_genes)))
105 # ... concat and reorder to match reference
107# Store counts
108adata_query.layers["counts"] = adata_query.X.copy()
109```

Step 3: Prepare Query Data

# Load query data
adata_query = sc.read_h5ad("query_data.h5ad")

# CRITICAL: Use same genes as reference
common_genes = adata_ref.var_names.intersection(adata_query.var_names)
print(f"Common genes: {len(common_genes)}")

# Subset query to reference genes
adata_query = adata_query[:, adata_ref.var_names].copy()

# Handle missing genes (set to 0)
missing_genes = set(adata_ref.var_names) - set(adata_query.var_names)
if missing_genes:
    # Add missing genes with zero expression
    import numpy as np
    from scipy.sparse import csr_matrix
    
    zero_matrix = csr_matrix((adata_query.n_obs, len(missing_genes)))
    # ... concat and reorder to match reference
    
# Store counts
adata_query.layers["counts"] = adata_query.X.copy()
Raw lines 84 to 110
111### Step 4: Map Query to Reference
113```python
114# Prepare query data for mapping
115scvi.model.SCANVI.prepare_query_anndata(adata_query, scanvi_ref)
117# Create query model from reference
118scanvi_query = scvi.model.SCANVI.load_query_data(
119 adata_query,
120 scanvi_ref
123# Fine-tune on query (optional but recommended)
124scanvi_query.train(
125 max_epochs=100,
126 plan_kwargs={"weight_decay": 0.0}
129# Get predictions
130adata_query.obs["predicted_cell_type"] = scanvi_query.predict()
132# Get prediction probabilities
133soft_predictions = scanvi_query.predict(soft=True)
134adata_query.obs["prediction_score"] = soft_predictions.max(axis=1)
135```

Step 4: Map Query to Reference

# Prepare query data for mapping
scvi.model.SCANVI.prepare_query_anndata(adata_query, scanvi_ref)

# Create query model from reference
scanvi_query = scvi.model.SCANVI.load_query_data(
    adata_query,
    scanvi_ref
)

# Fine-tune on query (optional but recommended)
scanvi_query.train(
    max_epochs=100,
    plan_kwargs={"weight_decay": 0.0}
)

# Get predictions
adata_query.obs["predicted_cell_type"] = scanvi_query.predict()

# Get prediction probabilities
soft_predictions = scanvi_query.predict(soft=True)
adata_query.obs["prediction_score"] = soft_predictions.max(axis=1)
Raw lines 111 to 136
137### Step 5: Evaluate Predictions
139```python
140# Confidence scores
141print(f"Mean prediction confidence: {adata_query.obs['prediction_score'].mean():.3f}")
143# Low confidence predictions
144low_conf = adata_query.obs['prediction_score'] < 0.5
145print(f"Low confidence cells: {low_conf.sum()} ({low_conf.mean()*100:.1f}%)")
147# Visualize
148sc.pp.neighbors(adata_query, use_rep="X_scANVI")
149sc.tl.umap(adata_query)
150sc.pl.umap(adata_query, color=['predicted_cell_type', 'prediction_score'])
151```

Step 5: Evaluate Predictions

# Confidence scores
print(f"Mean prediction confidence: {adata_query.obs['prediction_score'].mean():.3f}")

# Low confidence predictions
low_conf = adata_query.obs['prediction_score'] < 0.5
print(f"Low confidence cells: {low_conf.sum()} ({low_conf.mean()*100:.1f}%)")

# Visualize
sc.pp.neighbors(adata_query, use_rep="X_scANVI")
sc.tl.umap(adata_query)
sc.pl.umap(adata_query, color=['predicted_cell_type', 'prediction_score'])
Raw lines 137 to 152
153## Option 2: Use Pre-Trained Models

Option 2: Use Pre-Trained Models

Raw lines 153 to 154
155### From Model Hub
157```python
158# scvi-tools maintains models on HuggingFace
159# Check: https://huggingface.co/scvi-tools
161# Example: Load pre-trained model
162from huggingface_hub import hf_hub_download
164model_path = hf_hub_download(
165 repo_id="scvi-tools/example-model",
166 filename="model.pt"
169# Load model
170model = scvi.model.SCANVI.load(model_path, adata=adata_query)
171```

From Model Hub

# scvi-tools maintains models on HuggingFace
# Check: https://huggingface.co/scvi-tools

# Example: Load pre-trained model
from huggingface_hub import hf_hub_download

model_path = hf_hub_download(
    repo_id="scvi-tools/example-model",
    filename="model.pt"
)

# Load model
model = scvi.model.SCANVI.load(model_path, adata=adata_query)
Raw lines 155 to 172
173### From Published Atlas
175```python
176# Many atlases provide pre-trained models
177# Example workflow with CellTypist-style model
179# Download reference model
180# model = scvi.model.SCANVI.load("atlas_model/", adata=adata_query)
181```

From Published Atlas

# Many atlases provide pre-trained models
# Example workflow with CellTypist-style model

# Download reference model
# model = scvi.model.SCANVI.load("atlas_model/", adata=adata_query)
Raw lines 173 to 182
183## Option 3: scArches for Incremental Updates
185scArches extends a reference model without retraining from scratch:
187```python
188# Load existing reference model
189scanvi_ref = scvi.model.SCANVI.load("reference_model/")
191# Surgery: prepare for query integration
192scanvi_ref.freeze_layers()
194# Map query data
195scvi.model.SCANVI.prepare_query_anndata(adata_query, scanvi_ref)
196scanvi_query = scvi.model.SCANVI.load_query_data(adata_query, scanvi_ref)
198# Train only query-specific parameters
199scanvi_query.train(
200 max_epochs=200,
201 plan_kwargs={"weight_decay": 0.0}
203```

Option 3: scArches for Incremental Updates

scArches extends a reference model without retraining from scratch:

# Load existing reference model
scanvi_ref = scvi.model.SCANVI.load("reference_model/")

# Surgery: prepare for query integration
scanvi_ref.freeze_layers()

# Map query data
scvi.model.SCANVI.prepare_query_anndata(adata_query, scanvi_ref)
scanvi_query = scvi.model.SCANVI.load_query_data(adata_query, scanvi_ref)

# Train only query-specific parameters
scanvi_query.train(
    max_epochs=200,
    plan_kwargs={"weight_decay": 0.0}
)
Raw lines 183 to 204
205## Visualize Reference and Query Together
207```python
208# Concatenate for joint visualization
209adata_ref.obs["dataset"] = "reference"
210adata_query.obs["dataset"] = "query"
212# Get latent representations
213adata_ref.obsm["X_scANVI"] = scanvi_ref.get_latent_representation()
214adata_query.obsm["X_scANVI"] = scanvi_query.get_latent_representation()
216# Combine
217adata_combined = sc.concat([adata_ref, adata_query])
219# Compute combined UMAP
220sc.pp.neighbors(adata_combined, use_rep="X_scANVI")
221sc.tl.umap(adata_combined)
223# Plot
224sc.pl.umap(
225 adata_combined,
226 color=["dataset", "cell_type", "predicted_cell_type"],
227 ncols=2
229```

Visualize Reference and Query Together

# Concatenate for joint visualization
adata_ref.obs["dataset"] = "reference"
adata_query.obs["dataset"] = "query"

# Get latent representations
adata_ref.obsm["X_scANVI"] = scanvi_ref.get_latent_representation()
adata_query.obsm["X_scANVI"] = scanvi_query.get_latent_representation()

# Combine
adata_combined = sc.concat([adata_ref, adata_query])

# Compute combined UMAP
sc.pp.neighbors(adata_combined, use_rep="X_scANVI")
sc.tl.umap(adata_combined)

# Plot
sc.pl.umap(
    adata_combined,
    color=["dataset", "cell_type", "predicted_cell_type"],
    ncols=2
)
Raw lines 205 to 230
231## Quality Control for Predictions

Quality Control for Predictions

Raw lines 231 to 232
233### Confidence Filtering
235```python
236# Filter predictions by confidence
237confidence_threshold = 0.7
239high_conf = adata_query[adata_query.obs['prediction_score'] >= confidence_threshold].copy()
240low_conf = adata_query[adata_query.obs['prediction_score'] < confidence_threshold].copy()
242print(f"High confidence: {len(high_conf)} ({len(high_conf)/len(adata_query)*100:.1f}%)")
243print(f"Low confidence: {len(low_conf)} ({len(low_conf)/len(adata_query)*100:.1f}%)")
244```

Confidence Filtering

# Filter predictions by confidence
confidence_threshold = 0.7

high_conf = adata_query[adata_query.obs['prediction_score'] >= confidence_threshold].copy()
low_conf = adata_query[adata_query.obs['prediction_score'] < confidence_threshold].copy()

print(f"High confidence: {len(high_conf)} ({len(high_conf)/len(adata_query)*100:.1f}%)")
print(f"Low confidence: {len(low_conf)} ({len(low_conf)/len(adata_query)*100:.1f}%)")
Raw lines 233 to 245
246### Marker Validation
248```python
249# Validate predictions with known markers
250markers = {
251 'T cells': ['CD3D', 'CD3E'],
252 'B cells': ['CD19', 'MS4A1'],
253 'Monocytes': ['CD14', 'LYZ']
256for ct, genes in markers.items():
257 ct_cells = adata_query[adata_query.obs['predicted_cell_type'] == ct]
258 if len(ct_cells) > 0:
259 for gene in genes:
260 if gene in adata_query.var_names:
261 expr = ct_cells[:, gene].X.mean()
262 print(f"{ct} - {gene}: {expr:.3f}")
263```

Marker Validation

# Validate predictions with known markers
markers = {
    'T cells': ['CD3D', 'CD3E'],
    'B cells': ['CD19', 'MS4A1'],
    'Monocytes': ['CD14', 'LYZ']
}

for ct, genes in markers.items():
    ct_cells = adata_query[adata_query.obs['predicted_cell_type'] == ct]
    if len(ct_cells) > 0:
        for gene in genes:
            if gene in adata_query.var_names:
                expr = ct_cells[:, gene].X.mean()
                print(f"{ct} - {gene}: {expr:.3f}")
Raw lines 246 to 264
265## Complete Pipeline
267```python
268def transfer_labels(
269 adata_ref,
270 adata_query,
271 cell_type_key="cell_type",
272 batch_key=None,
273 n_top_genes=3000,
274 confidence_threshold=0.5
275):
276 """
277 Transfer cell type labels from reference to query.
279 Parameters
280 ----------
281 adata_ref : AnnData
282 Annotated reference data
283 adata_query : AnnData
284 Unannotated query data
285 cell_type_key : str
286 Column with cell type annotations in reference
287 batch_key : str, optional
288 Batch column
289 n_top_genes : int
290 Number of HVGs
291 confidence_threshold : float
292 Minimum confidence for predictions
294 Returns
295 -------
296 AnnData with predictions
297 """
298 import scvi
299 import scanpy as sc
301 # Prepare reference
302 adata_ref = adata_ref.copy()
303 adata_ref.layers["counts"] = adata_ref.X.copy()
305 sc.pp.highly_variable_genes(
306 adata_ref,
307 n_top_genes=n_top_genes,
308 flavor="seurat_v3",
309 batch_key=batch_key,
310 layer="counts"
311 )
312 adata_ref = adata_ref[:, adata_ref.var["highly_variable"]].copy()
314 # Train reference model
315 scvi.model.SCVI.setup_anndata(adata_ref, layer="counts", batch_key=batch_key)
316 scvi_ref = scvi.model.SCVI(adata_ref, n_latent=30)
317 scvi_ref.train(max_epochs=200)
319 scanvi_ref = scvi.model.SCANVI.from_scvi_model(
320 scvi_ref,
321 labels_key=cell_type_key,
322 unlabeled_category="Unknown"
323 )
324 scanvi_ref.train(max_epochs=50)
326 # Prepare query
327 adata_query = adata_query[:, adata_ref.var_names].copy()
328 adata_query.layers["counts"] = adata_query.X.copy()
330 # Map query
331 scvi.model.SCANVI.prepare_query_anndata(adata_query, scanvi_ref)
332 scanvi_query = scvi.model.SCANVI.load_query_data(adata_query, scanvi_ref)
333 scanvi_query.train(max_epochs=100, plan_kwargs={"weight_decay": 0.0})
335 # Get predictions
336 adata_query.obs["predicted_cell_type"] = scanvi_query.predict()
337 soft = scanvi_query.predict(soft=True)
338 adata_query.obs["prediction_score"] = soft.max(axis=1)
340 # Mark low confidence
341 adata_query.obs["confident_prediction"] = adata_query.obs["prediction_score"] >= confidence_threshold
343 # Add latent representation
344 adata_query.obsm["X_scANVI"] = scanvi_query.get_latent_representation()
346 return adata_query, scanvi_ref, scanvi_query
348# Usage
349adata_annotated, ref_model, query_model = transfer_labels(
350 adata_ref,
351 adata_query,
352 cell_type_key="cell_type"
355# Visualize
356sc.pp.neighbors(adata_annotated, use_rep="X_scANVI")
357sc.tl.umap(adata_annotated)
358sc.pl.umap(adata_annotated, color=['predicted_cell_type', 'prediction_score'])
359```

Complete Pipeline

def transfer_labels(
    adata_ref,
    adata_query,
    cell_type_key="cell_type",
    batch_key=None,
    n_top_genes=3000,
    confidence_threshold=0.5
):
    """
    Transfer cell type labels from reference to query.
    
    Parameters
    ----------
    adata_ref : AnnData
        Annotated reference data
    adata_query : AnnData
        Unannotated query data
    cell_type_key : str
        Column with cell type annotations in reference
    batch_key : str, optional
        Batch column
    n_top_genes : int
        Number of HVGs
    confidence_threshold : float
        Minimum confidence for predictions
        
    Returns
    -------
    AnnData with predictions
    """
    import scvi
    import scanpy as sc
    
    # Prepare reference
    adata_ref = adata_ref.copy()
    adata_ref.layers["counts"] = adata_ref.X.copy()
    
    sc.pp.highly_variable_genes(
        adata_ref,
        n_top_genes=n_top_genes,
        flavor="seurat_v3",
        batch_key=batch_key,
        layer="counts"
    )
    adata_ref = adata_ref[:, adata_ref.var["highly_variable"]].copy()
    
    # Train reference model
    scvi.model.SCVI.setup_anndata(adata_ref, layer="counts", batch_key=batch_key)
    scvi_ref = scvi.model.SCVI(adata_ref, n_latent=30)
    scvi_ref.train(max_epochs=200)
    
    scanvi_ref = scvi.model.SCANVI.from_scvi_model(
        scvi_ref,
        labels_key=cell_type_key,
        unlabeled_category="Unknown"
    )
    scanvi_ref.train(max_epochs=50)
    
    # Prepare query
    adata_query = adata_query[:, adata_ref.var_names].copy()
    adata_query.layers["counts"] = adata_query.X.copy()
    
    # Map query
    scvi.model.SCANVI.prepare_query_anndata(adata_query, scanvi_ref)
    scanvi_query = scvi.model.SCANVI.load_query_data(adata_query, scanvi_ref)
    scanvi_query.train(max_epochs=100, plan_kwargs={"weight_decay": 0.0})
    
    # Get predictions
    adata_query.obs["predicted_cell_type"] = scanvi_query.predict()
    soft = scanvi_query.predict(soft=True)
    adata_query.obs["prediction_score"] = soft.max(axis=1)
    
    # Mark low confidence
    adata_query.obs["confident_prediction"] = adata_query.obs["prediction_score"] >= confidence_threshold
    
    # Add latent representation
    adata_query.obsm["X_scANVI"] = scanvi_query.get_latent_representation()
    
    return adata_query, scanvi_ref, scanvi_query

# Usage
adata_annotated, ref_model, query_model = transfer_labels(
    adata_ref,
    adata_query,
    cell_type_key="cell_type"
)

# Visualize
sc.pp.neighbors(adata_annotated, use_rep="X_scANVI")
sc.tl.umap(adata_annotated)
sc.pl.umap(adata_annotated, color=['predicted_cell_type', 'prediction_score'])
Raw lines 265 to 360
361## Troubleshooting
363| Issue | Cause | Solution |
364|-------|-------|----------|
365| Many low-confidence predictions | Query has novel cell types | Manually annotate low-confidence cells |
366| Wrong predictions | Reference doesn't match tissue | Use tissue-appropriate reference |
367| Gene mismatch | Different gene naming | Convert gene IDs |
368| All same prediction | Query too different | Check data quality, try different reference |

Troubleshooting

IssueCauseSolution
Many low-confidence predictionsQuery has novel cell typesManually annotate low-confidence cells
Wrong predictionsReference doesn't match tissueUse tissue-appropriate reference
Gene mismatchDifferent gene namingConvert gene IDs
All same predictionQuery too differentCheck data quality, try different reference
Raw lines 361 to 369
370## Key References
372- Xu et al. (2021) "Probabilistic harmonization and annotation of single-cell transcriptomics data with deep generative models"
373- Lotfollahi et al. (2022) "Mapping single-cell data to reference atlases by transfer learning"

Key References

  • Xu et al. (2021) "Probabilistic harmonization and annotation of single-cell transcriptomics data with deep generative models"
  • Lotfollahi et al. (2022) "Mapping single-cell data to reference atlases by transfer learning"
Raw lines 370 to 373