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
1# Label Transfer and Reference Mapping with scANVI
3This reference covers using scANVI for transferring cell type annotations from a reference atlas to query data.
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 45## Overview
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.
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
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
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
- Train new model: Train scANVI on reference, then map query
- Use pre-trained model: Load existing model (e.g., from Model Hub)
- scArches: Extend existing model with query data (preserves reference)
Option 1: Train scANVI on Reference
Raw lines 27 to 2829### 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"
53)
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 5657### 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"
65)
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"
75)
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 8384### 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 110111### 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
121)
123# Fine-tune on query (optional but recommended)
124scanvi_query.train(
125 max_epochs=100,
126 plan_kwargs={"weight_decay": 0.0}
127)
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 136137### 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 152Option 2: Use Pre-Trained Models
Raw lines 153 to 154155### 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"
167)
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 172173### 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 182183## 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}
202)
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 204205## 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
228)
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 230Quality Control for Predictions
Raw lines 231 to 232233### 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 245246### 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']
254}
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 264265## 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"
353)
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 360361## 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
| Issue | Cause | Solution |
|---|---|---|
| Many low-confidence predictions | Query has novel cell types | Manually annotate low-confidence cells |
| Wrong predictions | Reference doesn't match tissue | Use tissue-appropriate reference |
| Gene mismatch | Different gene naming | Convert gene IDs |
| All same prediction | Query too different | Check data quality, try different reference |
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"