scientific-skills/scvi-tools/references/models-multimodal.md
This document covers models for joint analysis of multiple data modalities in scvi-tools.
Purpose: Joint analysis of CITE-seq data (simultaneous RNA and protein measurements from same cells).
Key Features:
When to Use:
Data Requirements:
.X or a layer.obsm["protein_expression"]Basic Usage:
import scvi
# Setup data - specify both RNA and protein layers
scvi.model.TOTALVI.setup_anndata(
adata,
layer="counts", # RNA counts
protein_expression_obsm_key="protein_expression", # Protein counts
batch_key="batch"
)
# Train model
model = scvi.model.TOTALVI(adata)
model.train()
# Get joint latent representation
latent = model.get_latent_representation()
# Get normalized values for both modalities
rna_normalized = model.get_normalized_expression()
protein_normalized = model.get_normalized_expression(
transform_batch="batch1",
protein_expression=True
)
# Differential expression (works for both RNA and protein)
rna_de = model.differential_expression(groupby="cell_type")
protein_de = model.differential_expression(
groupby="cell_type",
protein_expression=True
)
Key Parameters:
n_latent: Latent space dimensionality (default: 20)n_layers_encoder: Number of encoder layers (default: 1)n_layers_decoder: Number of decoder layers (default: 1)protein_dispersion: Protein dispersion handling ("protein" or "protein-batch")empirical_protein_background_prior: Use empirical background for proteinsAdvanced Features:
Protein Imputation:
# Impute missing proteins for RNA-only cells
# (useful for mapping RNA-seq to CITE-seq reference)
protein_foreground = model.get_protein_foreground_probability()
imputed_proteins = model.get_normalized_expression(
protein_expression=True,
n_samples=25
)
Denoising:
# Get denoised counts for both modalities
denoised_rna = model.get_normalized_expression(n_samples=25)
denoised_protein = model.get_normalized_expression(
protein_expression=True,
n_samples=25
)
Best Practices:
Purpose: Integration of paired and unpaired multi-omic data (e.g., RNA + ATAC, paired and unpaired cells).
Key Features:
When to Use:
Data Requirements:
Basic Usage:
# Prepare data with modality information
# adata.X should contain all features (genes + peaks)
# adata.var["modality"] indicates "Gene" or "Peak"
# adata.obs["modality"] indicates which modality each cell has
scvi.model.MULTIVI.setup_anndata(
adata,
batch_key="batch",
modality_key="modality" # Column indicating cell modality
)
model = scvi.model.MULTIVI(adata)
model.train()
# Get joint latent representation
latent = model.get_latent_representation()
# Impute missing modalities
# E.g., predict ATAC for RNA-only cells
imputed_accessibility = model.get_accessibility_estimates(
indices=rna_only_indices
)
# Get normalized expression/accessibility
rna_normalized = model.get_normalized_expression()
atac_normalized = model.get_accessibility_estimates()
Key Parameters:
n_genes: Number of gene featuresn_regions: Number of accessibility regionsn_latent: Latent dimensionality (default: 20)Integration Scenarios:
Scenario 1: Fully Paired (10x Multiome):
# All cells have both RNA and ATAC
# Single modality key: "paired"
adata.obs["modality"] = "paired"
Scenario 2: Partially Paired:
# Some cells have both, some RNA-only, some ATAC-only
adata.obs["modality"] = ["RNA+ATAC", "RNA", "ATAC", ...]
Scenario 3: Completely Unpaired:
# Separate RNA and ATAC experiments
adata.obs["modality"] = ["RNA"] * n_rna + ["ATAC"] * n_atac
Advanced Use Cases:
Cross-Modality Prediction:
# Predict peaks from gene expression
accessibility_from_rna = model.get_accessibility_estimates(
indices=rna_only_cells
)
# Predict genes from accessibility
expression_from_atac = model.get_normalized_expression(
indices=atac_only_cells
)
Modality-Specific Analysis:
# Separate analysis per modality
rna_subset = adata[adata.obs["modality"].str.contains("RNA")]
atac_subset = adata[adata.obs["modality"].str.contains("ATAC")]
Purpose: Multi-sample analysis accounting for sample-specific and shared variation.
Key Features:
When to Use:
Basic Usage:
scvi.model.MRVI.setup_anndata(
adata,
layer="counts",
batch_key="batch",
sample_key="sample" # Critical: defines biological samples
)
model = scvi.model.MRVI(adata, n_latent=10, n_latent_sample=5)
model.train()
# Get representations
shared_latent = model.get_latent_representation() # Shared across samples
sample_specific = model.get_sample_specific_representation()
# Sample distance matrix
sample_distances = model.get_sample_distances()
Key Parameters:
n_latent: Dimensionality of shared latent spacen_latent_sample: Dimensionality of sample-specific spacesample_key: Column defining biological samplesAnalysis Workflow:
# 1. Identify shared cell types across samples
sc.pp.neighbors(adata, use_rep="X_MrVI_shared")
sc.tl.umap(adata)
sc.tl.leiden(adata, key_added="shared_clusters")
# 2. Analyze sample-specific variation
sample_repr = model.get_sample_specific_representation()
# 3. Compare samples
distances = model.get_sample_distances()
# 4. Find sample-enriched genes
de_results = model.differential_expression(
groupby="sample",
group1="Disease",
group2="Healthy"
)
Use Cases:
Use for: CITE-seq (RNA + protein, same cells)
Use for: Multiple modalities (RNA + ATAC, etc.)
Use for: Multi-sample RNA-seq
import scvi
import scanpy as sc
# 1. Load CITE-seq data
adata = sc.read_h5ad("cite_seq.h5ad")
# 2. QC and filtering
sc.pp.filter_genes(adata, min_cells=3)
sc.pp.highly_variable_genes(adata, n_top_genes=4000)
# Protein QC
protein_counts = adata.obsm["protein_expression"]
# Remove low-quality proteins
# 3. Setup totalVI
scvi.model.TOTALVI.setup_anndata(
adata,
layer="counts",
protein_expression_obsm_key="protein_expression",
batch_key="batch"
)
# 4. Train
model = scvi.model.TOTALVI(adata, n_latent=20)
model.train(max_epochs=400)
# 5. Extract joint representation
latent = model.get_latent_representation()
adata.obsm["X_totalVI"] = latent
# 6. Clustering on joint space
sc.pp.neighbors(adata, use_rep="X_totalVI")
sc.tl.umap(adata)
sc.tl.leiden(adata, resolution=0.5)
# 7. Differential expression for both modalities
rna_de = model.differential_expression(
groupby="leiden",
group1="0",
group2="1"
)
protein_de = model.differential_expression(
groupby="leiden",
group1="0",
group2="1",
protein_expression=True
)
# 8. Save model
model.save("totalvi_model")