Install any skill in seconds. Free to start, no credit card required.
Get Started Free →RNA velocity analysis with scVelo. Estimate cell state transitions from unspliced/spliced mRNA dynamics, infer trajectory directions, compute latent time, and identify driver genes in single-cell RNA-seq data. Complements Scanpy/scVI-tools for trajectory inference.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-13 | ✗→✓ | ▲ Improved | — | — |
| case-15 | ✗→✓ | ▲ Improved | — | — |
| case-16 | ✗→✓ | ▲ Improved | — | — |
| case-19 | ✓→✓ | = Same ✓ | — | — |
| case-03 | ✗→✗ | = Same ✗ | — | — |
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: pip install scvelo
Key resources:
Use scVelo when:
scVelo requires count matrices for both unspliced and spliced RNA. These are generated by:
lamanno modevelocyto run10x / velocyto runData is stored in an AnnData object with layers["spliced"] and layers["unspliced"].
pythonimport 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)
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) # knn_connectivities must be computed first sc.pp.neighbors(adata, n_neighbors=30, n_pcs=30) scv.pp.moments( adata, n_pcs=30, n_neighbors=30 )
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)" )
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)
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 )
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))
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')
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 )
pythonimport 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
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 |
| 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 |
| 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 |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.