A comprehensive guide to computational tools for reconstructing cellular developmental trajectories
ใๆดๅบๆฏ่ต๏ผๅชๆ่ฎกๆถๅก็ๅพ่งใใ
In the red garage, a grand prix is never one movie โ it is thousands of sector times and telemetry messages streaming past the pit wall. Reconstructing the race from that data is the exact shape of trajectory inference: one snapshot of thousands of cells, no timelapse of any single one. Six reads from the Scuderia's pit wall, re-read as the concepts this module's methods formalize.
The tower never shows the race; it posts sector times, car by car. Order the field by sector and the running order appears โ nobody had to watch a single lap.
Pseudotime. Rank cells by progression, not by timestamps: a position along the course, inferred from state, not read off a clock.
Every car streams speed, gear, and throttle at each meter. Current velocity is a free prediction of the next sector โ if the channel is honest.
RNA velocity. Unspliced and spliced counts are a cell's telemetry channel: where it is headed next, read off the current stream.
Through any corner there is one fast line, and every car approximates it. Stray off it and you are slow, not creative.
Trajectory as a manifold. Cells move along low-dimensional lines through state space; the methods below differ in how they draw that line โ trees, curves, graphs, or optimal-transport flows.
Some circuits offer two lines through the same complex โ and both can win. But a careful steward asks whether the fork is real asphalt or a mirage in the spray.
Branching, verified. Bifurcations are real biology โ yet a smoother will happily draw a fork through discrete populations. Check the split exists before publishing it.
Hand the wrong car pole position and the whole result sheet reads backwards. The grid is declared before the race, by the rules โ not inferred from it.
Root selection. The algorithm does not know where the trajectory starts; biology declares it (progenitors, marker genes, velocity). A wrong root silently runs time in reverse.
Before the sheet is official, the FIA audits the telemetry โ sensor by sensor, sector by sector. A beautiful reconstruction with no audit is a rumor, not a result.
Benchmark before belief. Validate reconstructions against held-out timepoints and known biology (the Saelens et al. TI benchmark is the canonical audit) โ then publish the line.
ใ็ไธ่งๆดๅบๆฏ่ต๏ผๅฐฑไป่ฎกๆถๆฐๆฎ้่ฏปๅบ่ต่ฝฆ็บฟใใ โ you never watch the race; you read it.
Trajectory inference (TI) methods reconstruct the dynamic processes of cellular differentiation, development, and state transitions from single-cell data. These computational approaches allow researchers to understand how cells progress through different states over time, identify key transition points, and discover the genes that drive these changes.
Selecting the right trajectory inference method depends on several factors:
These methods infer future cell states by modeling the relationship between unspliced and spliced mRNA:
Nature
The pioneering RNA velocity method that introduced the concept of using splicing dynamics to predict future cell states.
Nature Biotechnology
An improved RNA velocity framework with dynamical modeling that accounts for transcriptional induction, repression, and steady-state. Most widely used velocity method.
Nature Methods
Probabilistic RNA velocity inference using variational inference with uncertainty quantification.
Nature Communications
Unified RNA velocity framework using unified latent time modeling across the transcriptome via Radial Basis Functions.
Cell
The most systematic extension of the RNA velocity framework: upgrades per-gene velocity estimates into a single continuous, transcriptome-wide vector field, and extends beyond the steady-state/dynamical splicing model to metabolically labeled data (SLAM-seq, sci-fate, NASC-seq).
Nature Biotechnology
Extends RNA velocity to multi-omic (RNA + ATAC) data, modeling the temporal relationship between chromatin accessibility and transcription rather than inferring velocity from splicing alone.
These methods use optimal transport theory to match cells across conditions or timepoints:
Cell
Uses optimal transport to infer developmental trajectories and fate probabilities across timepoints.
Nature (published online Jan 2025; bioRxiv preprint 2023)
Multi-Omics Single-Cell Optimal Transport - the most scalable OT framework, handling over 1.7 million cells with linear time complexity.
NeurIPS 2024
Gene-regulated neural optimal transport with uncertainty quantification for trajectory inference using flow matching.
Traditional approaches that construct trajectories using dimensionality reduction and graph structures:
Nature
Advanced trajectory inference framework using UMAP and principal graphs, designed to scale to millions of cells and handle discontinuous trajectories. Used to analyze the Mouse Organogenesis Cell Atlas (2 million cells).
Nature Methods
Earlier version using reversed graph embedding for trajectory reconstruction. Still widely used for smaller datasets.
BMC Genomics
Flexible trajectory inference using cluster-based minimum spanning trees and principal curves.
Genome Biology
Partition-based graph abstraction that creates coarse-grained trajectory representations. Exceptionally scalable: 1.3M cells in 90 seconds.
Nature Biotechnology
Models differentiation as a multi-step diffusion-like process over a Markov chain, outputting a probabilistic fate โ not a single deterministic branch assignment โ for every cell. One of the most widely cited single-cell trajectory methods.
Methods that combine multiple signals or integrate different trajectory inference approaches:
Nature Methods
Unified framework combining RNA velocity, pseudotime, gene expression, and experimental time for robust fate predictions. Scales to millions of cells.
Nature Communications
Feature selection for trajectory analysis that identifies genes driving dynamic processes.
Purpose-built tools for specific biological questions or data types:
Scientific Reports
Discovers novel cell types and subpopulations along developmental trajectories using cross-timepoint analysis.
Nature Methods (online Nov 2023)
Specialized method for B cell development using class-switch recombination as molecular clock.
Nature Machine Intelligence
Models growth dynamics explicitly during trajectory inference to account for proliferation using dynamic unbalanced optimal transport.
Nature Communications
Learns potential landscapes from temporal single-cell data to predict differentiation trajectories. Developed by Gifford lab at MIT CSAIL.
Nature Protocols
Trajectory visualization tool optimized for flow and mass cytometry data using force-directed graph layouts.
Nature Methods
Hierarchical lineage tree inference with probabilistic modeling for complex multi-branching developmental systems using Hodge Laplacian decomposition.
Nature Methods
Predicts absolute developmental potential (a "how progenitor-like is this cell" score, from 1 = totipotent to 0 = fully differentiated) directly from a single-snapshot transcriptome โ no unspliced/spliced reads, no defined root, no trajectory algorithm required.
This table provides a comprehensive comparison of key features across different trajectory inference methods:
| Method | Year | Max Scale | Multiple Timepoints | Single Snapshot | Spatial Data | Multimodal | Growth/Death | Key Strength |
|---|---|---|---|---|---|---|---|---|
| Monocle 3 | 2019 | 2M+ cells | โ | โ | โ | โ | โ | Ultra-scalable, discontinuous trajectories, convergent fates |
| Monocle 2 | 2017 | ~50K cells | โ | โ | โ | โ | โ | DDRTree algorithm, well-established |
| MOSCOT | 2025 | 1.7M+ cells | โ | โ | โ | โ | โ | Most scalable OT, spatial, multimodal |
| scVelo | 2020 | ~100K cells | โ | โ | โ | โ | โ | Dynamic velocity, most popular |
| GENOT | 2024 | Variable | โ | โ | โ | โ | โ | Stochastic OT, uncertainty quantification |
| CellRank 2 | 2024 | 1.3M+ cells | โ | โ | โ | โ | โ | Multi-view integration |
| veloVI | 2024 | ~50K cells | โ | โ | โ | โ | โ | Uncertainty quantification |
| UniTVelo | 2022 | ~100K cells | โ | โ | โ | โ | โ | Batch integration |
| CASi | 2024 | ~100K cells | โ | โ | โ | โ | โ | Novel cell type discovery |
| sciCSR | 2024 | ~50K cells | โ | โ | โ | โ | โ | B cell specialization |
| Slingshot | 2018 | ~50K cells | โ | โ | โ | โ | โ | Flexible, Bioconductor integration |
| PAGA | 2019 | ~200K cells | โ | โ | โ | โ | โ | Graph abstraction, exploratory |
| Waddington-OT | 2019 | Variable (temporal timecourse) | โ | โ | โ | โ | โ | Foundational temporal OT, fate probabilities |
| DELVE | 2024 | Variable | โ | โ | โ | โ | โ | Feature selection layer, cross-modality |
| TIGON | 2023 | Variable | โ | โ | โ | โ | โ | Growth-aware unbalanced OT + GRN inference |
| PRESCIENT | 2021 | Variable | โ | โ | โ | โ | โ | Waddington potential landscape, in silico perturbation |
| FLOW-MAP | 2020 | Variable (cytometry-scale) | โ | โ | โ | โ | โ | Flow/CyTOF-optimized force-directed visualization |
| PHLOWER | 2025 | Variable | โ | โ | โ | โ | โ | Up to 26 branches, uncertainty quantification |
| Palantir | 2019 | Variable | โ | โ | โ | โ | โ | Probabilistic multi-fate absorption, not hard branches |
| dynamo | 2022 | Variable | โ | โ | โ | Metabolic labeling optional | โ | Continuous vector field, acceleration/curvature |
| MultiVelo | 2023 | Variable (10x Multiome scale) | โ | โ | โ | โ | โ | RNA + ATAC joint velocity |
| CytoTRACE 2 | 2025 | Variable (broad platform validation) | โ | โ | โ | โ | โ | No-velocity, no-root developmental potential score |
If yes โ go to Q2. If no (a single snapshot) โ skip to Q3.
Yes, large-scale: MOSCOT (multimodal/spatial, atlas-scale) or Monocle 3 (complex topologies, discontinuous trajectories).
No, smaller-scale: Waddington-OT (foundational OT) | PRESCIENT (Waddington landscape) | TIGON (proliferation-aware, unbalanced OT) | GENOT (stochastic/cross-modality) | PRESCIENT and TIGON if you need growth/death modeled explicitly.
If no (low depth, or you just want a fast pre-check) โ CytoTRACE 2 (no-velocity developmental potential score) or PAGA (graph abstraction, exploratory). If yes โ go to Q4.
Yes: veloVI (variational, posterior-sampled uncertainty).
No: scVelo (standard, most widely used) | dynamo (continuous vector field, metabolic-labeling-aware) | UniTVelo (unified latent time) | MultiVelo (if you have paired RNA+ATAC) | Palantir (if you want probabilistic multi-fate absorption instead of a velocity vector) | CellRank 2 (to combine velocity + pseudotime + real time into one fate model).
velocity_confidence does not mean the biological signal is absent โ cells with low sequencing depth simply produce a weak velocity signal, but their developmental-potential score can still be meaningful. Used together: velocity tells you direction, CytoTRACE 2 tells you who is plausibly a progenitor.RNA Velocity (scVelo, veloVI):
Optimal Transport (MOSCOT, GENOT):
Graph-Based (Monocle 3, PAGA, Slingshot):
The walkthrough below takes a QC'd AnnData object end to end through a standard Python trajectory stack: neighbor graph and UMAP, Leiden clusters, PAGA for coarse topology, diffusion pseudotime for a continuous ordering, RNA velocity with scVelo, and finally CellRank to turn velocity plus connectivity into terminal states and fate probabilities. Every step is retrospective and computational โ the point is to interrogate an existing dataset, not to generate new material.
Scanpy, scVelo, and CellRank evolve quickly and pin overlapping dependencies, so install them into one dedicated environment rather than adding them to a general analysis env. CellRank 2 expects scvelo >= 0.3 and scanpy >= 1.9; installing all three in a single pip call lets the resolver pick a mutually consistent set.
# conda / mamba recommended
conda create -n traj python=3.10 -y
conda activate traj
# install together so the resolver reconciles shared pins
pip install "scanpy>=1.9" "scvelo>=0.3" "cellrank>=2.0" leidenalg igraph
# only needed if you must GENERATE spliced/unspliced counts yourself
# pip install velocyto scvelo[louvain] loompy
# (alternative: STARsolo --soloFeatures Gene Velocyto, or kb-python with --workflow lamanno)
Hardware. A laptop with 16 GB RAM comfortably handles ~50k cells through PAGA, DPT, and the stochastic velocity model. The scVelo dynamical model is the bottleneck: it fits a full splicing ODE per gene by EM and can take hours on 100k cells, so run it on a compute node with n_jobs set to the available cores. CellRank's kernels and GPCCA decomposition are sparse-matrix operations and stay cheap by comparison. No GPU is required anywhere in this pipeline.
AnnData โ the container everything reads and writes: adata.X (expression), adata.obs (per-cell annotation, where pseudotime and cluster labels land), adata.obsm (embeddings such as X_pca, X_umap, X_diffmap), adata.uns (graph and PAGA results)adata.layers["spliced"] / adata.layers["unspliced"] โ hard prerequisite for RNA velocity. These are separate count matrices produced by a velocity-aware quantifier (velocyto on the CellRanger BAM, STARsolo Velocyto mode, or kb-python's La Manno workflow). They cannot be derived from a standard filtered count matrix: the spliced/unspliced split requires read-to-intron alignment, and that information is gone once you have only a gene-by-cell matrix..loom), loaded with scv.read() and merged onto your QC'd object with scv.utils.merge(); barcode suffixes rarely match out of the box and usually need cleaning firstadata.obsp["connectivities"] and adata.obsp["distances"]; PAGA, DPT, and CellRank's connectivity kernel are all functions of this single graph, so its parameters propagate everywhereadata.layers["velocity"] (per-gene rates), adata.uns["velocity_graph"] (cell-cell transition scores); the dynamical model additionally writes adata.obs["latent_time"]adata.obsm["lineages_fwd"] holding per-cell absorption probabilities toward each terminal stateStart from the QC-filtered object, build the graph, run PAGA for topology, anchor a root cell using biology and compute diffusion pseudotime, then bring in the spliced/unspliced layers for velocity and hand both signals to CellRank.
import scanpy as sc
import scvelo as scv
import cellrank as cr
import numpy as np
# 1. Start from a QC'd object (doublets removed, ambient corrected).
# Trajectory methods are extremely sensitive to junk cells: a stray
# doublet sits "between" two clusters and looks exactly like a
# transitional state to any graph-based method.
adata = sc.read_h5ad("adata_qc.h5ad")
adata.layers["counts"] = adata.X.copy() # keep raw counts for later
# 2. Standard normalisation -> HVGs -> PCA
sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
adata.raw = adata # log-norm values for plots/markers
sc.pp.highly_variable_genes(adata, n_top_genes=2000)
adata = adata[:, adata.var.highly_variable].copy()
sc.pp.scale(adata, max_value=10)
sc.tl.pca(adata, n_comps=50, svd_solver="arpack")
# 3. Neighbour graph -> UMAP -> Leiden.
# n_neighbors is the single most consequential knob downstream: PAGA,
# DPT and CellRank's connectivity kernel all read this same graph.
sc.pp.neighbors(adata, n_neighbors=15, n_pcs=30)
sc.tl.umap(adata)
sc.tl.leiden(adata, resolution=1.0, key_added="leiden")
# 4. PAGA: coarse-grained topology between clusters. Read this BEFORE
# committing to a pseudotime -- it tells you whether the data is one
# connected continuum or several disconnected islands.
sc.tl.paga(adata, groups="leiden")
sc.pl.paga(adata, threshold=0.05, show=False) # prune weak edges
sc.tl.umap(adata, init_pos="paga") # PAGA-initialised embedding
# 5. Diffusion pseudotime. The root must come from BIOLOGY, not convenience:
# pick the cluster expressing your known progenitor/stem markers, then
# take an extreme cell within it along the first diffusion component.
sc.tl.diffmap(adata)
root_cluster = "3" # e.g. HSC/progenitor cluster
mask = (adata.obs["leiden"] == root_cluster).values
dc1 = adata.obsm["X_diffmap"][:, 1]
adata.uns["iroot"] = np.flatnonzero(mask)[np.argmin(dc1[mask])]
sc.tl.dpt(adata, n_dcs=15) # writes adata.obs["dpt_pseudotime"]
# Sanity check: pseudotime should INCREASE away from the progenitors.
# If your known mature markers sit at low pseudotime, the root is wrong
# and the whole trajectory is silently reversed.
print(adata.obs.groupby("leiden")["dpt_pseudotime"].median())
# 6. RNA velocity. This step REQUIRES spliced/unspliced layers -- they come
# from velocyto/STARsolo/kb-python, and cannot be recovered from a plain
# filtered count matrix. Merge the loom onto the QC'd object by barcode.
ldata = scv.read("sample.loom", cache=True)
ldata.var_names_make_unique()
adata = scv.utils.merge(adata, ldata) # intersects on barcodes
assert "spliced" in adata.layers and "unspliced" in adata.layers
scv.pp.filter_and_normalize(adata, min_shared_counts=20, n_top_genes=2000)
scv.pp.moments(adata, n_pcs=30, n_neighbors=30) # kNN-smoothed first/second moments
# stochastic: seconds-to-minutes, assumes a common steady state
scv.tl.velocity(adata, mode="stochastic")
# dynamical: fits the full splicing ODE per gene by EM. Much slower
# (hours at 100k cells) but relaxes the steady-state assumption and is the
# only mode that yields latent time.
# scv.tl.recover_dynamics(adata, n_jobs=8)
# scv.tl.velocity(adata, mode="dynamical")
# scv.tl.latent_time(adata) # adata.obs["latent_time"]
scv.tl.velocity_graph(adata, n_jobs=8)
scv.pl.velocity_embedding_stream(adata, basis="umap", color="leiden")
# Confidence diagnostics -- low values mean the arrows are not trustworthy.
scv.tl.velocity_confidence(adata)
scv.pl.scatter(adata, c=["velocity_length", "velocity_confidence"], cmap="coolwarm")
# 7. CellRank: combine a directional signal (velocity) with a similarity
# signal (connectivity). The connectivity kernel regularises velocity,
# which is noisy at the single-cell level.
vk = cr.kernels.VelocityKernel(adata).compute_transition_matrix()
ck = cr.kernels.ConnectivityKernel(adata).compute_transition_matrix()
combined = 0.8 * vk + 0.2 * ck # weights are a real choice
g = cr.estimators.GPCCA(combined)
g.compute_schur(n_components=20)
g.compute_macrostates(n_states=6, cluster_key="leiden")
g.predict_terminal_states() # inspect against known endpoints
g.compute_fate_probabilities() # adata.obsm["lineages_fwd"]
g.plot_fate_probabilities(same_plot=False)
# 8. Genes correlated with commitment to one fate
drivers = g.compute_lineage_drivers(lineages=["Erythroid"])
print(drivers.head(20))
adata.write_h5ad("adata_trajectory.h5ad")
sc.tl.dpt will happily run from any iroot and return a smooth, plausible-looking ordering. Choose the root from progenitor markers, then verify that known mature populations land at high pseudotime โ nothing in the output warns you when the direction is flipped.velocity_confidence and check a few phase portraits with scv.pl.velocity() instead of trusting the streamline plot alone.n_neighbors in sc.pp.neighbors and scv.pp.moments, the number of PCs, and the PAGA edge threshold all change which branches appear. Small neighborhoods fragment a real continuum; large ones bridge unrelated populations. Re-run the pipeline across a parameter grid and only report branches that survive.batch or orig.ident โ if a branch is one sample, it is technical.