alterlab-scvelo
Run RNA velocity analysis with scVelo on single-cell RNA-seq data — estimate cell-state transitions from spliced/unspliced mRNA dynamics, infer trajectory direction, compute latent time, and identify driver genes. Use when adding directionality to trajectories or studying differe
Install
npx skills add https://github.com/AlterLab-IEU/AlterLab-Academic-Skills/tree/main/skills/bioinformatics/alterlab-scvelo
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install alterlab-ieu-alterlab-academic-skills@llmmart
git clone https://github.com/AlterLab-IEU/AlterLab-Academic-Skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole alterlab-ieu/alterlab-academic-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
scVelo — RNA Velocity Analysis
Overview
scVelo is the leading Python package for RNA velocity analysis in single-cell RNA-seq data. It infers cell state transitions by modeling the kinetics of mRNA splicing — using the ratio of unspliced (pre-mRNA) to spliced (mature mRNA) abundances to determine whether a gene is being upregulated or downregulated in each cell. This allows reconstruction of developmental trajectories and identification of cell fate decisions without requiring time-course data.
Installation: uv pip install "scvelo==0.3.4" — 0.3.4 (uploaded 2026-02-24) is the
current release and the package is now only minimally maintained: its published release
notes stop at 0.2.5, upstream CI still targets Python 3.9/3.10, and its dependency
declarations (numpy>=1.17, scanpy>=1.5, anndata>=0.7.5) have no upper bounds, so pip
will happily build an environment scVelo was never tested against.
Practical consequence: install it in its own environment with a pinned NumPy rather than
into a current scverse stack. If you hit dtype or scalar-assignment errors under NumPy 2 —
most often in mode="stochastic", whose second-moment regression assigns a one-element
least-squares result to a scalar — pin numpy<2 (e.g. numpy==1.26.4) and re-run. Check
velocity results for silent breakage rather than assuming they are fine: compare the
stochastic and dynamical modes, and confirm the streamlines agree with known biology.
Key resources:
- Documentation: https://scvelo.readthedocs.io/
- GitHub: https://github.com/theislab/scvelo
- Paper: Bergen et al. (2020) Nature Biotechnology. PMID: 32747759
When to Use This Skill
Use scVelo when:
- Trajectory inference from snapshot data: Determine which direction cells are differentiating
- Cell fate prediction: Identify progenitor cells and their downstream fates
- Driver gene identification: Find genes whose dynamics best explain observed trajectories
- Developmental biology: Model hematopoiesis, neurogenesis, epithelial-to-mesenchymal transitions
- Latent time estimation: Order cells along a pseudotime derived from splicing dynamics
- Complement to Scanpy: Add directional information to UMAP embeddings
Does NOT Trigger
| Scenario | Use Instead |
|---|---|
| QC, normalization, PCA/UMAP, Leiden clustering, marker genes (no velocity) | alterlab-scanpy |
Reading/wrangling the .h5ad object, layers, and concatenation |
alterlab-anndata |
| Probabilistic integration / batch correction / model-based DE (scVI, scANVI, veloVI) | alterlab-scvi-tools |
| Producing the spliced/unspliced count layers from FASTQ in the first place | alterlab-rnaseq-quant |
| Inferring a gene-regulatory network from expression | alterlab-arboreto |
Prerequisites
scVelo requires count matrices for both unspliced and spliced RNA. These are generated by:
- STARsolo or kallisto|bustools with
lamannomode - velocyto CLI:
velocyto run10x/velocyto run - alevin-fry / simpleaf with spliced/unspliced output
Data is stored in an AnnData object with layers["spliced"] and layers["unspliced"].
Standard RNA Velocity Workflow
1. Setup and Data Loading
import scvelo as scv
import scanpy as sc
import numpy as np
import matplotlib.pyplot as plt
# Configure settings
scv.settings.verbosity = 3 # Show computation steps
scv.settings.presenter_view = True
scv.settings.set_figure_params('scvelo')
# Load data (AnnData with spliced/unspliced layers)
# Option A: Load from loom (velocyto output)
adata = scv.read("cellranger_output.loom", cache=True)
# Option B: Merge velocyto loom with Scanpy-processed AnnData
adata_processed = sc.read_h5ad("processed.h5ad") # Has UMAP, clusters
adata_velocity = scv.read("velocyto.loom")
adata = scv.utils.merge(adata_processed, adata_velocity)
# Verify layers
print(adata)
# obs × var: N × G
# layers: 'spliced', 'unspliced' (required)
# obsm['X_umap'] (required for visualization)
2. Preprocessing
# Filter and normalize (follows Scanpy conventions)
scv.pp.filter_and_normalize(
adata,
min_shared_counts=20, # Minimum counts in spliced+unspliced
n_top_genes=2000 # Top highly variable genes
)
# Compute first and second order moments (means and variances).
# scv.pp.moments runs PCA + a kNN graph internally if they're absent, so
# you do NOT need a separate sc.pp.neighbors call here. Calling moments with
# n_pcs/n_neighbors and ALSO running sc.pp.neighbors first just recomputes the
# graph with possibly mismatched params — let moments own it on a fresh object.
scv.pp.moments(
adata,
n_pcs=30,
n_neighbors=30
)
3. Velocity Estimation — Stochastic Model
The stochastic model is fast and suitable for exploratory analysis:
# Stochastic velocity (faster, less accurate)
scv.tl.velocity(adata, mode='stochastic')
scv.tl.velocity_graph(adata)
# Visualize
scv.pl.velocity_embedding_stream(
adata,
basis='umap',
color='leiden',
title="RNA Velocity (Stochastic)"
)
4. Velocity Estimation — Dynamical Model (Recommended)
The dynamical model fits the full splicing kinetics and is more accurate:
# Recover dynamics (computationally intensive; ~10-30 min for 10K cells)
scv.tl.recover_dynamics(adata, n_jobs=4)
# Compute velocity from dynamical model
scv.tl.velocity(adata, mode='dynamical')
scv.tl.velocity_graph(adata)
5. Latent Time
The dynamical model enables computation of a shared latent time (pseudotime):
# Compute latent time
scv.tl.latent_time(adata)
# Visualize latent time on UMAP
scv.pl.scatter(
adata,
color='latent_time',
color_map='gnuplot',
size=80,
title='Latent time'
)
# Identify top genes ordered by latent time
top_genes = adata.var['fit_likelihood'].sort_values(ascending=False).index[:300]
scv.pl.heatmap(
adata,
var_names=top_genes,
sortby='latent_time',
col_color='leiden',
n_convolve=100
)
6. Driver Gene Analysis
# Identify genes with highest velocity fit
scv.tl.rank_velocity_genes(adata, groupby='leiden', min_corr=0.3)
df = scv.DataFrame(adata.uns['rank_velocity_genes']['names'])
print(df.head(10))
# Speed and coherence
scv.tl.velocity_confidence(adata)
scv.pl.scatter(
adata,
c=['velocity_length', 'velocity_confidence'],
cmap='coolwarm',
perc=[5, 95]
)
# Phase portraits for specific genes
scv.pl.velocity(adata, ['Cpe', 'Gnao1', 'Ins2'],
ncols=3, figsize=(16, 4))
7. Velocity Arrows and Pseudotime
# Arrow plot on UMAP
scv.pl.velocity_embedding(
adata,
arrow_length=3,
arrow_size=2,
color='leiden',
basis='umap'
)
# Stream plot (cleaner visualization)
scv.pl.velocity_embedding_stream(
adata,
basis='umap',
color='leiden',
smooth=0.8,
min_mass=4
)
# Velocity pseudotime (alternative to latent time)
scv.tl.velocity_pseudotime(adata)
scv.pl.scatter(adata, color='velocity_pseudotime', cmap='gnuplot')
8. PAGA Trajectory Graph
# PAGA graph with velocity-informed transitions
scv.tl.paga(adata, groups='leiden')
df = scv.get_df(adata, 'paga/transitions_confidence', precision=2).T
df.style.background_gradient(cmap='Blues').format('{:.2g}')
# Plot PAGA with velocity
scv.pl.paga(
adata,
basis='umap',
size=50,
alpha=0.1,
min_edge_width=2,
node_size_scale=1.5
)
Complete Workflow Script
import scvelo as scv
import scanpy as sc
def run_rna_velocity(adata, n_top_genes=2000, mode='dynamical', n_jobs=4):
"""
Complete RNA velocity workflow.
Args:
adata: AnnData with 'spliced' and 'unspliced' layers, UMAP in obsm
n_top_genes: Number of top HVGs for velocity
mode: 'stochastic' (fast) or 'dynamical' (accurate)
n_jobs: Parallel jobs for dynamical model
Returns:
Processed AnnData with velocity information
"""
scv.settings.verbosity = 2
# 1. Preprocessing
scv.pp.filter_and_normalize(adata, min_shared_counts=20, n_top_genes=n_top_genes)
if 'neighbors' not in adata.uns:
sc.pp.neighbors(adata, n_neighbors=30)
scv.pp.moments(adata, n_pcs=30, n_neighbors=30)
# 2. Velocity estimation
if mode == 'dynamical':
scv.tl.recover_dynamics(adata, n_jobs=n_jobs)
scv.tl.velocity(adata, mode=mode)
scv.tl.velocity_graph(adata)
# 3. Downstream analyses
if mode == 'dynamical':
scv.tl.latent_time(adata)
scv.tl.rank_velocity_genes(adata, groupby='leiden', min_corr=0.3)
scv.tl.velocity_confidence(adata)
scv.tl.velocity_pseudotime(adata)
return adata
Key Output Fields in AnnData
After running the workflow, the following fields are added:
| Location | Key | Description |
|---|---|---|
adata.layers |
velocity |
RNA velocity per gene per cell |
adata.layers |
fit_t |
Fitted latent time per gene per cell |
adata.obsm |
velocity_umap |
2D velocity vectors on UMAP |
adata.obs |
velocity_pseudotime |
Pseudotime from velocity |
adata.obs |
latent_time |
Latent time from dynamical model |
adata.obs |
velocity_length |
Speed of each cell |
adata.obs |
velocity_confidence |
Confidence score per cell |
adata.var |
fit_likelihood |
Gene-level model fit quality |
adata.var |
fit_alpha |
Transcription rate |
adata.var |
fit_beta |
Splicing rate |
adata.var |
fit_gamma |
Degradation rate |
adata.uns |
velocity_graph |
Cell-cell transition probability matrix |
Velocity Models Comparison
| Model | Speed | Accuracy | When to Use |
|---|---|---|---|
stochastic |
Fast | Moderate | Exploratory; large datasets |
deterministic |
Medium | Moderate | Simple linear kinetics |
dynamical |
Slow | High | Publication-quality; identifies driver genes |
Best Practices
- Start with stochastic mode for exploration; switch to dynamical for final analysis
- Need good coverage of unspliced reads: Short reads (< 100 bp) may miss intron coverage
- Minimum 2,000 cells: RNA velocity is noisy with fewer cells
- Velocity should be coherent: Arrows should follow known biology; randomness indicates issues
- k-NN bandwidth matters: Too few neighbors → noisy velocity; too many → oversmoothed
- Sanity check: Root cells (progenitors) should have high unspliced/spliced ratios for marker genes
- Dynamical model requires distinct kinetic states: Works best for clear differentiation processes
Troubleshooting
| Problem | Solution |
|---|---|
| Missing unspliced layer | Re-run velocyto or use STARsolo with --soloFeatures Gene Velocyto |
| Very few velocity genes | Lower min_shared_counts; check sequencing depth |
| Random-looking arrows | Try different n_neighbors or velocity model |
| Memory error with dynamical | Set n_jobs=1; reduce n_top_genes |
| Negative velocity everywhere | Check that spliced/unspliced layers are not swapped |
Additional Resources
- scVelo documentation: https://scvelo.readthedocs.io/
- Tutorial notebooks: https://scvelo.readthedocs.io/en/stable/VelocityBasics.html
- GitHub: https://github.com/theislab/scvelo
- Paper: Bergen V et al. (2020) Nature Biotechnology. PMID: 32747759
- velocyto (preprocessing): http://velocyto.org/
- CellRank 2 (fate prediction; consumes a velocity kernel but also works without RNA velocity, via pseudotime / real-time / metabolic-labelling kernels — the actively maintained route for fate probabilities): https://cellrank.readthedocs.io/
- dynamo (metabolic labeling alternative): https://dynamo-release.readthedocs.io/
Files (alterlab-academic-skills)
-
evals
-
evals.json 4.3 KB
{ "skill": "alterlab-scvelo", "evals": [ { "id": "dynamical-velocity-pipeline", "prompt": "I have an AnnData with spliced and unspliced layers and a UMAP from Scanpy. I want a publication-quality RNA velocity analysis. Walk me through preprocessing and running the dynamical model in scVelo.", "expected_output": "Triggers the scVelo skill. Should use scv.pp.filter_and_normalize and scv.pp.moments for preprocessing, then scv.tl.recover_dynamics followed by scv.tl.velocity(mode='dynamical') and scv.tl.velocity_graph. Should note dynamical is the accurate/publication choice and is computationally intensive.", "assertions": [ {"type": "should_trigger", "value": true}, {"type": "output_contains", "value": "recover_dynamics"}, {"type": "behavior", "value": "Runs the dynamical model (recover_dynamics + velocity mode='dynamical' + velocity_graph) after filter_and_normalize and moments."} ] }, { "id": "latent-time-driver-genes", "prompt": "After fitting the dynamical model on my scVelo object, how do I compute latent time and pull out the top driver genes that best explain the trajectory?", "expected_output": "Triggers the scVelo skill. Should use scv.tl.latent_time for the shared latent time, and rank driver genes via scv.tl.rank_velocity_genes (groupby cluster) and/or the var['fit_likelihood'] ranking. May show scv.pl.heatmap sorted by latent_time.", "assertions": [ {"type": "should_trigger", "value": true}, {"type": "output_contains", "value": "latent_time"}, {"type": "behavior", "value": "Computes latent_time and identifies driver genes via rank_velocity_genes or fit_likelihood ranking."} ] }, { "id": "velocity-stream-embedding", "prompt": "I just want a quick exploratory velocity field overlaid on my UMAP, colored by my leiden clusters, as a clean stream plot. Which scVelo functions and what's the fast mode?", "expected_output": "Triggers the scVelo skill. Should use the stochastic mode (scv.tl.velocity(mode='stochastic') + velocity_graph) for fast exploration, then scv.pl.velocity_embedding_stream(basis='umap', color='leiden') for the stream plot. May contrast with velocity_embedding arrow plots.", "assertions": [ {"type": "should_trigger", "value": true}, {"type": "output_contains", "value": "velocity_embedding_stream"}, {"type": "behavior", "value": "Uses the stochastic mode for fast exploration and plots with velocity_embedding_stream on the UMAP basis."} ] }, { "id": "missing-unspliced-troubleshoot", "prompt": "My scVelo run errors out because there's no unspliced layer in my AnnData. The data came from a standard CellRanger run. How do I actually get the spliced/unspliced counts I need?", "expected_output": "Triggers the scVelo skill. Should explain that RNA velocity needs both spliced and unspliced count layers, which are NOT in a plain CellRanger filtered matrix — they must be generated by velocyto (run10x), STARsolo with --soloFeatures Gene Velocyto, or kallisto|bustools / alevin-fry in lamanno mode. Then merge with the processed AnnData via scv.utils.merge.", "assertions": [ {"type": "should_trigger", "value": true}, {"type": "output_contains", "value": "velocyto"}, {"type": "behavior", "value": "Identifies that spliced/unspliced layers must be regenerated (velocyto/STARsolo Velocyto/kallisto|bustools lamanno) and merged into the AnnData."} ] }, { "id": "near-miss-scanpy", "prompt": "I have a raw single-cell gene-expression matrix. I want to do standard QC, normalize, find highly variable genes, run PCA and neighbors, then Leiden cluster and embed in UMAP. No trajectory stuff yet.", "expected_output": "Should NOT trigger the scVelo skill. This is the standard Scanpy clustering/embedding pipeline (QC, normalize, HVG, PCA, neighbors, leiden, umap) with no spliced/unspliced velocity component. It belongs to the scanpy skill. The response should defer to scanpy rather than scVelo, since scVelo only adds directionality on top of an existing embedding.", "assertions": [ {"type": "should_not_trigger", "value": true}, {"type": "output_contains", "value": "scanpy"} ] } ] }
-
-
references
-
velocity_models.md 4.9 KB
# scVelo Velocity Models Reference ## Mathematical Framework RNA velocity is based on the kinetic model of transcription: ``` dx_s/dt = β·x_u - γ·x_s (spliced dynamics) dx_u/dt = α(t) - β·x_u (unspliced dynamics) ``` Where: - `x_s`: spliced mRNA abundance - `x_u`: unspliced (pre-mRNA) abundance - `α(t)`: transcription rate (varies over time) - `β`: splicing rate - `γ`: degradation rate **Velocity** is defined as: `v = dx_s/dt = β·x_u - γ·x_s` - **v > 0**: Gene is being upregulated (more unspliced than expected at steady state) - **v < 0**: Gene is being downregulated (less unspliced than expected) ## Model Comparison ### Steady-State (Velocyto, original) - Assumes constant α (transcription rate) - Fits γ using linear regression on steady-state cells - **Limitation**: Requires identifiable steady states; assumes constant transcription ```python # Use with scVelo for backward compatibility scv.tl.velocity(adata, mode='steady_state') ``` ### Stochastic Model (scVelo v1) - Extends steady-state with variance/covariance terms - Models cell-to-cell variability in mRNA counts - More robust to noise than steady-state ```python scv.tl.velocity(adata, mode='stochastic') ``` ### Dynamical Model (scVelo v2, recommended) - Jointly estimates all kinetic rates (α, β, γ) and cell-specific latent time - Does not assume steady state - Identifies induction vs. repression phases - Computes fit_likelihood per gene (quality measure) ```python scv.tl.recover_dynamics(adata, n_jobs=4) scv.tl.velocity(adata, mode='dynamical') ``` **Kinetic states identified by dynamical model:** | State | Description | |-------|-------------| | Induction | α > 0, x_u increasing | | Steady-state on | α > 0, constant high expression | | Repression | α = 0, x_u decreasing | | Steady-state off | α = 0, constant low expression | ## Velocity Graph The velocity graph connects cells based on their velocity similarity to neighboring cells' states: ```python scv.tl.velocity_graph(adata) # Stored in adata.uns['velocity_graph'] # Entry [i,j] = probability that cell i transitions to cell j ``` **Parameters:** - `n_neighbors`: Number of neighbors considered - `sqrt_transform`: Apply sqrt transform to data (default: False for spliced) - `approx`: Use approximate nearest neighbor search (faster for large datasets) ## Latent Time Interpretation Latent time τ ∈ [0, 1] for each gene represents: - τ = 0: Gene is at onset of induction - τ = 0.5: Gene is at peak of induction (for a complete cycle) - τ = 1: Gene has returned to steady-state off **Shared latent time** is computed by taking the average over all velocity genes, weighted by fit_likelihood. ## Quality Metrics ### Gene-level - `fit_likelihood`: Goodness-of-fit of dynamical model (0-1; higher = better) - Use for filtering driver genes: `adata.var[adata.var['fit_likelihood'] > 0.1]` - `fit_alpha`: Transcription rate during induction - `fit_gamma`: mRNA degradation rate - `fit_r2`: R² of kinetic fit ### Cell-level - `velocity_length`: Magnitude of velocity vector (cell speed) - `velocity_confidence`: Coherence of velocity with neighboring cells (0-1) ### Dataset-level ```python # Check overall velocity quality scv.pl.proportions(adata) # Ratio of spliced/unspliced per cell scv.pl.velocity_confidence(adata, groupby='leiden') ``` ## Parameter Tuning Guide | Parameter | Function | Default | When to Change | |-----------|----------|---------|----------------| | `min_shared_counts` | Filter genes | 20 | Increase for deep sequencing; decrease for shallow | | `n_top_genes` | HVG selection | 2000 | Increase for complex datasets | | `n_neighbors` | kNN graph | 30 | Decrease for small datasets; increase for noisy | | `n_pcs` | PCA dimensions | 30 | Match to elbow in scree plot | | `t_max_rank` | Latent time constraint | None | Set if known developmental direction | ## Integration with Other Tools ### CellRank (Fate Prediction) ```python import cellrank as cr from cellrank.kernels import VelocityKernel, ConnectivityKernel # Combine velocity and connectivity kernels vk = VelocityKernel(adata).compute_transition_matrix() ck = ConnectivityKernel(adata).compute_transition_matrix() combined = 0.8 * vk + 0.2 * ck # Compute macrostates (terminal and initial states) g = cr.estimators.GPCCA(combined) g.compute_macrostates(n_states=4, cluster_key='leiden') g.plot_macrostates(which="all") # Compute fate probabilities g.compute_fate_probabilities() g.plot_fate_probabilities() ``` ### Scanpy Integration scVelo works natively with Scanpy's AnnData: ```python import scanpy as sc import scvelo as scv # Run standard Scanpy pipeline first sc.pp.normalize_total(adata) sc.pp.log1p(adata) sc.pp.highly_variable_genes(adata) sc.pp.pca(adata) sc.pp.neighbors(adata) sc.tl.umap(adata) sc.tl.leiden(adata) # Then add velocity on top scv.pp.moments(adata) scv.tl.recover_dynamics(adata) scv.tl.velocity(adata, mode='dynamical') scv.tl.velocity_graph(adata) scv.tl.latent_time(adata) ```
-
-
scripts
-
rna_velocity_workflow.py 8 KB
""" RNA Velocity Analysis Workflow using scVelo =========================================== Complete pipeline from raw data to velocity visualization. Usage: python rna_velocity_workflow.py Or import and use run_velocity_analysis() with your AnnData object. """ import scvelo as scv import scanpy as sc import matplotlib matplotlib.use('Agg') # Non-interactive backend import os def run_velocity_analysis( adata, groupby="leiden", n_top_genes=2000, n_neighbors=30, mode="dynamical", n_jobs=4, output_dir="velocity_results", ): """ Complete RNA velocity analysis workflow. Parameters ---------- adata : AnnData AnnData object with 'spliced' and 'unspliced' layers. Should already have UMAP and cluster annotations. groupby : str Column in adata.obs for cell type labels. n_top_genes : int Number of top highly variable genes. n_neighbors : int Number of neighbors for moment computation. mode : str Velocity model: 'stochastic' (fast) or 'dynamical' (accurate). n_jobs : int Parallel jobs for dynamical model fitting. output_dir : str Directory for saving output figures. Returns ------- AnnData with velocity annotations. """ os.makedirs(output_dir, exist_ok=True) # ── Settings ────────────────────────────────────────────────────────────── scv.settings.verbosity = 2 # NOTE: scVelo prepends settings.figdir (and a plot-type prefix) to whatever # you pass in save=. So pass BARE filenames to save= below — do NOT prefix # them with output_dir again, or you get a doubly-nested path. scv.settings.figdir = output_dir # ── Step 1: Check layers ─────────────────────────────────────────────────── assert "spliced" in adata.layers, "Missing 'spliced' layer. Run velocyto first." assert "unspliced" in adata.layers, "Missing 'unspliced' layer. Run velocyto first." print(f"Input: {adata.n_obs} cells × {adata.n_vars} genes") # ── Step 2: Preprocessing ───────────────────────────────────────────────── print("Step 1/5: Preprocessing...") scv.pp.filter_and_normalize(adata, min_shared_counts=20, n_top_genes=n_top_genes) if "neighbors" not in adata.uns: sc.pp.neighbors(adata, n_neighbors=n_neighbors, n_pcs=30) scv.pp.moments(adata, n_pcs=30, n_neighbors=n_neighbors) print(f" {adata.n_vars} velocity genes selected") # ── Step 3: Velocity estimation ──────────────────────────────────────────── print(f"Step 2/5: Fitting velocity model ({mode})...") if mode == "dynamical": scv.tl.recover_dynamics(adata, n_jobs=n_jobs) scv.tl.velocity(adata, mode=mode) scv.tl.velocity_graph(adata) print(" Velocity graph computed") # ── Step 4: Downstream analyses ──────────────────────────────────────────── print("Step 3/5: Computing latent time and confidence...") scv.tl.velocity_confidence(adata) scv.tl.velocity_pseudotime(adata) if mode == "dynamical": scv.tl.latent_time(adata) if groupby in adata.obs.columns: scv.tl.rank_velocity_genes(adata, groupby=groupby, min_corr=0.3) # ── Step 5: Visualization ───────────────────────────────────────────────── print("Step 4/5: Generating figures...") # Stream plot scv.pl.velocity_embedding_stream( adata, basis="umap", color=groupby, title="RNA Velocity", save="velocity_stream.png", ) # Arrow plot scv.pl.velocity_embedding( adata, arrow_length=3, arrow_size=2, color=groupby, basis="umap", save="velocity_arrows.png", ) # Pseudotime scv.pl.scatter( adata, color="velocity_pseudotime", cmap="gnuplot", title="Velocity Pseudotime", save="pseudotime.png", ) if mode == "dynamical" and "latent_time" in adata.obs: scv.pl.scatter( adata, color="latent_time", color_map="gnuplot", title="Latent Time", save="latent_time.png", ) # Speed and coherence scv.pl.scatter( adata, c=["velocity_length", "velocity_confidence"], cmap="coolwarm", perc=[5, 95], save="velocity_quality.png", ) # Top driver genes heatmap (dynamical only) if mode == "dynamical" and "fit_likelihood" in adata.var: top_genes = adata.var["fit_likelihood"].sort_values(ascending=False).index[:50] scv.pl.heatmap( adata, var_names=top_genes, sortby="latent_time", col_color=groupby, n_convolve=50, save="driver_gene_heatmap.png", ) # ── Step 6: Save results ─────────────────────────────────────────────────── print("Step 5/5: Saving results...") output_h5ad = os.path.join(output_dir, "adata_velocity.h5ad") adata.write_h5ad(output_h5ad) print(f" Saved to {output_h5ad}") # Summary statistics confidence = adata.obs["velocity_confidence"].dropna() print("\nSummary:") print(f" Velocity model: {mode}") print(f" Cells: {adata.n_obs}") print(f" Velocity genes: {adata.n_vars}") print(f" Mean velocity confidence: {confidence.mean():.3f}") print(f" High-confidence cells (>0.7): {(confidence > 0.7).sum()} ({(confidence > 0.7).mean():.1%})") if mode == "dynamical" and "fit_likelihood" in adata.var: good_genes = (adata.var["fit_likelihood"] > 0.1).sum() print(f" Well-fit genes (likelihood>0.1): {good_genes}") print(f"\nOutput files saved to: {output_dir}/") return adata def load_from_loom(loom_path, processed_h5ad=None): """ Load velocity data from velocyto loom file. Args: loom_path: Path to velocyto output loom file processed_h5ad: Optional path to pre-processed Scanpy h5ad file """ adata_loom = scv.read(loom_path, cache=True) if processed_h5ad: adata_processed = sc.read_h5ad(processed_h5ad) # Merge: keep processed metadata and add velocity layers adata = scv.utils.merge(adata_processed, adata_loom) else: adata = adata_loom # Run basic Scanpy pipeline sc.pp.normalize_total(adata, target_sum=1e4) sc.pp.log1p(adata) sc.pp.highly_variable_genes(adata, n_top_genes=3000) sc.pp.pca(adata) sc.pp.neighbors(adata) sc.tl.umap(adata) sc.tl.leiden(adata, resolution=0.5) return adata if __name__ == "__main__": # Example usage with simulated data (for testing) print("scVelo RNA Velocity Workflow - Demo Mode") print("=" * 50) # Load example dataset adata = scv.datasets.pancreas() print(f"Loaded pancreas dataset: {adata}") # Run analysis adata = run_velocity_analysis( adata, groupby="clusters", n_top_genes=2000, mode="dynamical", n_jobs=2, output_dir="pancreas_velocity", ) print("\nAnalysis complete!") print(f"Key results:") print(f" adata.layers['velocity']: velocity per gene per cell") print(f" adata.obs['latent_time']: pseudotime from dynamics") print(f" adata.obs['velocity_confidence']: per-cell confidence") if "rank_velocity_genes" in adata.uns: print(f" adata.uns['rank_velocity_genes']: driver genes per cluster")
-
-
SKILL.md 12.6 KB
--- name: alterlab-scvelo description: Run RNA velocity analysis with scVelo on single-cell RNA-seq data — estimate cell-state transitions from spliced/unspliced mRNA dynamics, infer trajectory direction, compute latent time, and identify driver genes. Use when adding directionality to trajectories or studying differentiation dynamics from spliced/unspliced layers (velocyto/STARsolo output); for the general QC, clustering, UMAP, and differential-expression analysis pipeline prefer alterlab-scanpy instead, and for .h5ad data-structure I/O and layer wrangling prefer alterlab-anndata instead. Part of the AlterLab Academic Skills suite. license: MIT allowed-tools: Read Write Edit Bash(python:*) Bash(uv:*) compatibility: "Self-contained — runs under `uv run python` with the skill's Python package installed; no API key or account required. scVelo 0.3.4 (released 2026-02) is the current and effectively final upstream release; it is minimally maintained, so pin it and validate results rather than expecting fixes." metadata: skill-author: AlterLab version: "1.1.0" last_updated: "2026-09-23" --- # scVelo — RNA Velocity Analysis ## Overview scVelo is the leading Python package for RNA velocity analysis in single-cell RNA-seq data. It infers cell state transitions by modeling the kinetics of mRNA splicing — using the ratio of unspliced (pre-mRNA) to spliced (mature mRNA) abundances to determine whether a gene is being upregulated or downregulated in each cell. This allows reconstruction of developmental trajectories and identification of cell fate decisions without requiring time-course data. **Installation:** `uv pip install "scvelo==0.3.4"` — 0.3.4 (uploaded 2026-02-24) is the current release and the package is now only minimally maintained: its published release notes stop at 0.2.5, upstream CI still targets Python 3.9/3.10, and its dependency declarations (`numpy>=1.17`, `scanpy>=1.5`, `anndata>=0.7.5`) have no upper bounds, so pip will happily build an environment scVelo was never tested against. Practical consequence: install it in its own environment with a pinned NumPy rather than into a current scverse stack. If you hit dtype or scalar-assignment errors under NumPy 2 — most often in `mode="stochastic"`, whose second-moment regression assigns a one-element least-squares result to a scalar — pin `numpy<2` (e.g. `numpy==1.26.4`) and re-run. Check velocity results for silent breakage rather than assuming they are fine: compare the stochastic and dynamical modes, and confirm the streamlines agree with known biology. **Key resources:** - Documentation: https://scvelo.readthedocs.io/ - GitHub: https://github.com/theislab/scvelo - Paper: Bergen et al. (2020) Nature Biotechnology. PMID: 32747759 ## When to Use This Skill Use scVelo when: - **Trajectory inference from snapshot data**: Determine which direction cells are differentiating - **Cell fate prediction**: Identify progenitor cells and their downstream fates - **Driver gene identification**: Find genes whose dynamics best explain observed trajectories - **Developmental biology**: Model hematopoiesis, neurogenesis, epithelial-to-mesenchymal transitions - **Latent time estimation**: Order cells along a pseudotime derived from splicing dynamics - **Complement to Scanpy**: Add directional information to UMAP embeddings ### Does NOT Trigger | Scenario | Use Instead | |----------|-------------| | QC, normalization, PCA/UMAP, Leiden clustering, marker genes (no velocity) | `alterlab-scanpy` | | Reading/wrangling the `.h5ad` object, layers, and concatenation | `alterlab-anndata` | | Probabilistic integration / batch correction / model-based DE (scVI, scANVI, veloVI) | `alterlab-scvi-tools` | | Producing the spliced/unspliced count layers from FASTQ in the first place | `alterlab-rnaseq-quant` | | Inferring a gene-regulatory network from expression | `alterlab-arboreto` | ## Prerequisites scVelo requires count matrices for both **unspliced** and **spliced** RNA. These are generated by: 1. **STARsolo** or **kallisto|bustools** with `lamanno` mode 2. **velocyto** CLI: `velocyto run10x` / `velocyto run` 3. **alevin-fry** / **simpleaf** with spliced/unspliced output Data is stored in an `AnnData` object with `layers["spliced"]` and `layers["unspliced"]`. ## Standard RNA Velocity Workflow ### 1. Setup and Data Loading ```python import scvelo as scv import scanpy as sc import numpy as np import matplotlib.pyplot as plt # Configure settings scv.settings.verbosity = 3 # Show computation steps scv.settings.presenter_view = True scv.settings.set_figure_params('scvelo') # Load data (AnnData with spliced/unspliced layers) # Option A: Load from loom (velocyto output) adata = scv.read("cellranger_output.loom", cache=True) # Option B: Merge velocyto loom with Scanpy-processed AnnData adata_processed = sc.read_h5ad("processed.h5ad") # Has UMAP, clusters adata_velocity = scv.read("velocyto.loom") adata = scv.utils.merge(adata_processed, adata_velocity) # Verify layers print(adata) # obs × var: N × G # layers: 'spliced', 'unspliced' (required) # obsm['X_umap'] (required for visualization) ``` ### 2. Preprocessing ```python # Filter and normalize (follows Scanpy conventions) scv.pp.filter_and_normalize( adata, min_shared_counts=20, # Minimum counts in spliced+unspliced n_top_genes=2000 # Top highly variable genes ) # Compute first and second order moments (means and variances). # scv.pp.moments runs PCA + a kNN graph internally if they're absent, so # you do NOT need a separate sc.pp.neighbors call here. Calling moments with # n_pcs/n_neighbors and ALSO running sc.pp.neighbors first just recomputes the # graph with possibly mismatched params — let moments own it on a fresh object. scv.pp.moments( adata, n_pcs=30, n_neighbors=30 ) ``` ### 3. Velocity Estimation — Stochastic Model The stochastic model is fast and suitable for exploratory analysis: ```python # Stochastic velocity (faster, less accurate) scv.tl.velocity(adata, mode='stochastic') scv.tl.velocity_graph(adata) # Visualize scv.pl.velocity_embedding_stream( adata, basis='umap', color='leiden', title="RNA Velocity (Stochastic)" ) ``` ### 4. Velocity Estimation — Dynamical Model (Recommended) The dynamical model fits the full splicing kinetics and is more accurate: ```python # Recover dynamics (computationally intensive; ~10-30 min for 10K cells) scv.tl.recover_dynamics(adata, n_jobs=4) # Compute velocity from dynamical model scv.tl.velocity(adata, mode='dynamical') scv.tl.velocity_graph(adata) ``` ### 5. Latent Time The dynamical model enables computation of a shared latent time (pseudotime): ```python # Compute latent time scv.tl.latent_time(adata) # Visualize latent time on UMAP scv.pl.scatter( adata, color='latent_time', color_map='gnuplot', size=80, title='Latent time' ) # Identify top genes ordered by latent time top_genes = adata.var['fit_likelihood'].sort_values(ascending=False).index[:300] scv.pl.heatmap( adata, var_names=top_genes, sortby='latent_time', col_color='leiden', n_convolve=100 ) ``` ### 6. Driver Gene Analysis ```python # Identify genes with highest velocity fit scv.tl.rank_velocity_genes(adata, groupby='leiden', min_corr=0.3) df = scv.DataFrame(adata.uns['rank_velocity_genes']['names']) print(df.head(10)) # Speed and coherence scv.tl.velocity_confidence(adata) scv.pl.scatter( adata, c=['velocity_length', 'velocity_confidence'], cmap='coolwarm', perc=[5, 95] ) # Phase portraits for specific genes scv.pl.velocity(adata, ['Cpe', 'Gnao1', 'Ins2'], ncols=3, figsize=(16, 4)) ``` ### 7. Velocity Arrows and Pseudotime ```python # Arrow plot on UMAP scv.pl.velocity_embedding( adata, arrow_length=3, arrow_size=2, color='leiden', basis='umap' ) # Stream plot (cleaner visualization) scv.pl.velocity_embedding_stream( adata, basis='umap', color='leiden', smooth=0.8, min_mass=4 ) # Velocity pseudotime (alternative to latent time) scv.tl.velocity_pseudotime(adata) scv.pl.scatter(adata, color='velocity_pseudotime', cmap='gnuplot') ``` ### 8. PAGA Trajectory Graph ```python # PAGA graph with velocity-informed transitions scv.tl.paga(adata, groups='leiden') df = scv.get_df(adata, 'paga/transitions_confidence', precision=2).T df.style.background_gradient(cmap='Blues').format('{:.2g}') # Plot PAGA with velocity scv.pl.paga( adata, basis='umap', size=50, alpha=0.1, min_edge_width=2, node_size_scale=1.5 ) ``` ## Complete Workflow Script ```python import scvelo as scv import scanpy as sc def run_rna_velocity(adata, n_top_genes=2000, mode='dynamical', n_jobs=4): """ Complete RNA velocity workflow. Args: adata: AnnData with 'spliced' and 'unspliced' layers, UMAP in obsm n_top_genes: Number of top HVGs for velocity mode: 'stochastic' (fast) or 'dynamical' (accurate) n_jobs: Parallel jobs for dynamical model Returns: Processed AnnData with velocity information """ scv.settings.verbosity = 2 # 1. Preprocessing scv.pp.filter_and_normalize(adata, min_shared_counts=20, n_top_genes=n_top_genes) if 'neighbors' not in adata.uns: sc.pp.neighbors(adata, n_neighbors=30) scv.pp.moments(adata, n_pcs=30, n_neighbors=30) # 2. Velocity estimation if mode == 'dynamical': scv.tl.recover_dynamics(adata, n_jobs=n_jobs) scv.tl.velocity(adata, mode=mode) scv.tl.velocity_graph(adata) # 3. Downstream analyses if mode == 'dynamical': scv.tl.latent_time(adata) scv.tl.rank_velocity_genes(adata, groupby='leiden', min_corr=0.3) scv.tl.velocity_confidence(adata) scv.tl.velocity_pseudotime(adata) return adata ``` ## Key Output Fields in AnnData After running the workflow, the following fields are added: | Location | Key | Description | |----------|-----|-------------| | `adata.layers` | `velocity` | RNA velocity per gene per cell | | `adata.layers` | `fit_t` | Fitted latent time per gene per cell | | `adata.obsm` | `velocity_umap` | 2D velocity vectors on UMAP | | `adata.obs` | `velocity_pseudotime` | Pseudotime from velocity | | `adata.obs` | `latent_time` | Latent time from dynamical model | | `adata.obs` | `velocity_length` | Speed of each cell | | `adata.obs` | `velocity_confidence` | Confidence score per cell | | `adata.var` | `fit_likelihood` | Gene-level model fit quality | | `adata.var` | `fit_alpha` | Transcription rate | | `adata.var` | `fit_beta` | Splicing rate | | `adata.var` | `fit_gamma` | Degradation rate | | `adata.uns` | `velocity_graph` | Cell-cell transition probability matrix | ## Velocity Models Comparison | Model | Speed | Accuracy | When to Use | |-------|-------|----------|-------------| | `stochastic` | Fast | Moderate | Exploratory; large datasets | | `deterministic` | Medium | Moderate | Simple linear kinetics | | `dynamical` | Slow | High | Publication-quality; identifies driver genes | ## Best Practices - **Start with stochastic mode** for exploration; switch to dynamical for final analysis - **Need good coverage of unspliced reads**: Short reads (< 100 bp) may miss intron coverage - **Minimum 2,000 cells**: RNA velocity is noisy with fewer cells - **Velocity should be coherent**: Arrows should follow known biology; randomness indicates issues - **k-NN bandwidth matters**: Too few neighbors → noisy velocity; too many → oversmoothed - **Sanity check**: Root cells (progenitors) should have high unspliced/spliced ratios for marker genes - **Dynamical model requires distinct kinetic states**: Works best for clear differentiation processes ## Troubleshooting | Problem | Solution | |---------|---------| | Missing unspliced layer | Re-run velocyto or use STARsolo with `--soloFeatures Gene Velocyto` | | Very few velocity genes | Lower `min_shared_counts`; check sequencing depth | | Random-looking arrows | Try different `n_neighbors` or velocity model | | Memory error with dynamical | Set `n_jobs=1`; reduce `n_top_genes` | | Negative velocity everywhere | Check that spliced/unspliced layers are not swapped | ## Additional Resources - **scVelo documentation**: https://scvelo.readthedocs.io/ - **Tutorial notebooks**: https://scvelo.readthedocs.io/en/stable/VelocityBasics.html - **GitHub**: https://github.com/theislab/scvelo - **Paper**: Bergen V et al. (2020) Nature Biotechnology. PMID: 32747759 - **velocyto** (preprocessing): http://velocyto.org/ - **CellRank 2** (fate prediction; consumes a velocity kernel but also works without RNA velocity, via pseudotime / real-time / metabolic-labelling kernels — the actively maintained route for fate probabilities): https://cellrank.readthedocs.io/ - **dynamo** (metabolic labeling alternative): https://dynamo-release.readthedocs.io/
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.