| name | kallisto-bustools |
| description | scRNA-seq quantification with kallisto + bustools via kb-python. Pseudoalignment-based — orders of magnitude faster than full alignment (STAR / cellranger) while producing comparable count matrices. Covers index generation (kb ref), per-sample quantification (kb count), supported chemistries (10X v1/v2/v3, Drop-seq, CEL-Seq, inDrops, SMART-Seq), the nac workflow for nascent/mature RNA (snRNA-seq + RNA velocity), and the handoff to scanpy / Seurat / SCE for downstream analysis. |
| license | BSD-2-Clause |
| metadata | null |
kallisto | bustools: Pseudoalignment-Based scRNA-seq Quantification
Overview
kallisto + bustools is a pseudoalignment-based quantification pipeline for single-cell RNA-seq. The standard tools (cellranger, STARsolo, alevin-fry) do full alignment of reads to the genome; kallisto skips that and only determines which transcripts are compatible with each read. The result: 5–20× faster than cellranger, comparable accuracy, much lower memory footprint.
The toolkit has three components:
| Tool | What it does |
|---|
kallisto | The pseudoaligner (also handles non-single-cell quantification) |
bustools | Processes BUS files — barcode correction, UMI dedup, count matrix generation |
kb-python | Thin wrapper that runs kallisto + bustools end-to-end with one command |
For most users, kb-python is the only entry point you need. This protocol uses kb throughout.
When to Use This Skill
- Generating count matrices from raw FASTQ files for any droplet-based chemistry (10X, Drop-seq, inDrops) or plate-based (SMART-Seq)
- Processing many samples where cellranger would take days — kb does it in hours
- snRNA-seq / RNA velocity workflows that need both nascent (pre-mRNA) and mature counts
- Custom seqspec-defined chemistries
- Pipelines where you want maximum reproducibility — kb is deterministic, while cellranger has subtle non-determinism
Not for:
- Cell calling / empty-drop detection — kb produces unfiltered matrices; use
DropletUtils::emptyDrops or CellBender downstream
- Cell-type annotation, clustering, DE — handoff to scanpy / Seurat / SCE
- Spatial transcriptomics — use Space Ranger or the spatial-transcriptomics protocol
Prerequisites
- Python 3.8+
- ~32 GB RAM for human/mouse index build; ~16 GB for
kb count per sample
- Linux/macOS preferred (Windows works via WSL)
- Reference: a transcriptome FASTA + GTF (Ensembl, GENCODE, RefSeq)
Installation
pip install kb_python
conda install -c bioconda kb-python
kb --version
kb --list
kb-python bundles the kallisto + bustools binaries — no separate install needed for them.
For very large indexes (whole genome with introns, atlas-scale references), you may need to build kallisto from source for the v0.50+ features:
git clone https://github.com/pachterlab/kallisto && cd kallisto && mkdir build && cd build
cmake .. -DUSE_HDF5=ON && make && make install
Pipeline 1 — Build the Reference Index
You only do this once per genome. Reuse for all your samples.
Standard transcriptome (most users)
kb ref \
-i index.idx \
-g t2g.txt \
-f1 cdna.fasta \
GRCh38.primary_assembly.genome.fa.gz \
gencode.v44.annotation.gtf.gz
Output files:
index.idx — the kallisto pseudoalignment index (~3-5 GB for human)
t2g.txt — transcript-to-gene mapping
cdna.fasta — extracted transcript sequences (intermediate, keep if rebuilding)
This takes ~30-60 minutes for human/mouse on a typical workstation. Index files are reusable across all your samples.
Pre-built indexes (skip the build)
For common references, kb-python can download pre-built indexes:
kb ref -d human -i index.idx -g t2g.txt
kb ref -d mouse -i index.idx -g t2g.txt
kb ref -d zebrafish -i index.idx -g t2g.txt
Faster than building, identical result. Recommended unless you have a custom genome.
nac workflow — nascent + mature RNA (for snRNA-seq and RNA velocity)
Single-nucleus RNA-seq captures predominantly intronic reads (from pre-mRNA in the nucleus). Standard transcriptome indexes only handle exonic/mature mRNA, so you lose those reads. The nac workflow indexes both.
kb ref \
--workflow nac \
-i index.idx \
-g t2g.txt \
-c1 cdna.txt \
-c2 nascent.txt \
-f1 cdna.fasta \
-f2 nascent.fasta \
GRCh38.primary_assembly.genome.fa.gz \
gencode.v44.annotation.gtf.gz
Additional outputs vs standard:
cdna.txt — transcript IDs for mature (spliced) sequences
nascent.txt — transcript IDs for nascent (intron-containing) sequences
nascent.fasta — pre-mRNA sequences for the index
Required when:
- Running snRNA-seq (most nuclei reads land in introns)
- Doing RNA velocity (need separate spliced + unspliced counts)
For RNA velocity specifically, also consider the older lamanno workflow which produces the spliced/unspliced layout scVelo expects:
kb ref --workflow lamanno -i index.idx -g t2g.txt \
-f1 cdna.fasta -f2 intron.fasta \
-c1 cdna_t2c.txt -c2 intron_t2c.txt \
GRCh38.primary_assembly.genome.fa.gz gencode.v44.annotation.gtf.gz
Convenience: bash scripts/kb_ref.sh --genome GRCh38.fa --gtf annotation.gtf --out-dir reference/. See references/advanced.md for nac vs lamanno trade-offs and how to choose.
Source: Index generation docs.
Pipeline 2 — Quantify a Sample
kb count \
-i index.idx \
-g t2g.txt \
-x 10xv3 \
-o output_dir \
--h5ad \
sample_R1.fastq.gz sample_R2.fastq.gz
Key arguments
| Argument | Purpose | Notes |
|---|
-i index.idx | Path to the index | From kb ref |
-g t2g.txt | Transcript-to-gene mapping | From kb ref |
-x <tech> | Chemistry / sequencing technology | See kb --list |
-o output_dir | Output directory | Created if missing |
--workflow nac | Use nascent+mature index | Match the kb ref workflow |
--h5ad | Write .h5ad directly (scanpy-ready) | Recommended |
--filter bustools | Apply knee-plot cell filtering | Optional — usually do this in scanpy |
-t N | Threads | Default 8; bump on big machines |
-m 8G | Max RAM | Default 4G; bump for big indexes |
Supported chemistries (-x values)
| Chemistry | -x value | FASTQ layout |
|---|
| 10X Chromium v2 (3') | 10xv2 | R1 = barcode (16 bp) + UMI (10 bp); R2 = cDNA |
| 10X Chromium v3 (3' / multiome) | 10xv3 | R1 = barcode (16 bp) + UMI (12 bp); R2 = cDNA |
| 10X Chromium v1 (legacy) | 10xv1 | I1, R1, R2 — index file is the barcode |
| Drop-seq | DROPSEQ | R1 = barcode (12 bp) + UMI (8 bp); R2 = cDNA |
| inDrops v1 | INDROPSV1 | (custom — see kb --list) |
| CEL-Seq | CELSEQ | UMI + barcode in R1 |
| SMART-Seq v2 / v3 | SMARTSEQ2 / SMARTSEQ3 | full-length, --parity paired |
| BD Rhapsody | BDWTA | barcode + UMI |
| Slide-seq, Visium HD, etc. | various | run kb --list |
Full list: kb --list after installation. For unsupported chemistries, define one in seqspec and pass via -x <seqspec.yaml>.
Input FASTQ layout
For paired-end droplet chemistries, file order is R1 first, R2 second, repeated per lane:
kb count -i index.idx -g t2g.txt -x 10xv3 -o out --h5ad \
L001_R1.fastq.gz L001_R2.fastq.gz \
L002_R1.fastq.gz L002_R2.fastq.gz \
L003_R1.fastq.gz L003_R2.fastq.gz
For 10X v1 (legacy), also include the I1 file: I1 R1 R2 I1 R1 R2 ....
For SMART-Seq2 / full-length protocols where both reads are biological, use --parity paired:
kb count -i index.idx -g t2g.txt -x SMARTSEQ2 -o out --parity paired \
SAMPLE1_1.fastq.gz SAMPLE1_2.fastq.gz \
SAMPLE2_1.fastq.gz SAMPLE2_2.fastq.gz
Output structure
output_dir/
├── counts_unfiltered/ # cell × gene matrix BEFORE cell calling
│ ├── cells_x_genes.mtx # sparse matrix (Matrix Market format)
│ ├── cells_x_genes.barcodes.txt # cell barcodes (one per row)
│ ├── cells_x_genes.genes.txt # gene IDs (Ensembl)
│ ├── cells_x_genes.genes.names.txt # gene symbols
│ └── adata.h5ad # AnnData (if --h5ad was passed)
├── inspect.json # QC metrics from bustools
├── run_info.json # kallisto run info (alignment rate, etc.)
├── output.bus # raw BUS file
├── output.unfiltered.bus # barcode-corrected
└── matrix.ec # equivalence classes
The counts_unfiltered/ directory is your handoff point. Empty droplets are NOT filtered out at this stage — call cells with DropletUtils::emptyDrops or CellBender downstream.
Convenience: bash scripts/kb_count.sh --index index.idx --t2g t2g.txt --tech 10xv3 --out-dir sample1_out fastq/R1.fastq.gz fastq/R2.fastq.gz.
Source: pseudoalignment + output files docs.
Pipeline 3 — Handoff to Downstream (scanpy / Seurat / SCE)
scanpy / Python
import scanpy as sc
import anndata
adata = anndata.read_h5ad('output_dir/counts_unfiltered/adata.h5ad')
adata = sc.read_mtx('output_dir/counts_unfiltered/cells_x_genes.mtx')
barcodes = open('output_dir/counts_unfiltered/cells_x_genes.barcodes.txt').read().splitlines()
genes = open('output_dir/counts_unfiltered/cells_x_genes.genes.txt').read().splitlines()
gene_names = open('output_dir/counts_unfiltered/cells_x_genes.genes.names.txt').read().splitlines()
adata.obs_names = barcodes
adata.var_names = gene_names
adata.var['gene_id'] = genes
sc.pp.filter_cells(adata, min_genes=200)
sc.pp.filter_genes(adata, min_cells=3)
sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
Seurat / R
library(Seurat)
library(Matrix)
mtx <- readMM("output_dir/counts_unfiltered/cells_x_genes.mtx")
barcodes <- readLines("output_dir/counts_unfiltered/cells_x_genes.barcodes.txt")
genes <- readLines("output_dir/counts_unfiltered/cells_x_genes.genes.names.txt")
mtx <- t(mtx)
rownames(mtx) <- genes
colnames(mtx) <- barcodes
mtx <- as(mtx, "CsparseMatrix")
obj <- CreateSeuratObject(counts = mtx, min.cells = 3, min.features = 200)
SingleCellExperiment / R
library(SingleCellExperiment)
library(Matrix)
mtx <- t(readMM("output_dir/counts_unfiltered/cells_x_genes.mtx"))
rownames(mtx) <- readLines("output_dir/counts_unfiltered/cells_x_genes.genes.names.txt")
colnames(mtx) <- readLines("output_dir/counts_unfiltered/cells_x_genes.barcodes.txt")
sce <- SingleCellExperiment(assays = list(counts = mtx))
Source: kb → scanpy notebook.
nac Workflow — Loading Nascent + Mature Counts
When you ran kb count --workflow nac, the output has BOTH a mature (cdna) and nascent matrix:
import scanpy as sc
mature = sc.read_mtx('output_dir/counts_unfiltered/cells_x_genes.mature.mtx')
nascent = sc.read_mtx('output_dir/counts_unfiltered/cells_x_genes.nascent.mtx')
import anndata
adata = anndata.AnnData(
X = mature.X,
layers = {'spliced': mature.X, 'unspliced': nascent.X},
obs = mature.obs,
var = mature.var,
)
For snRNA-seq (no velocity, just better gene counts), sum spliced + unspliced before downstream — nuclei reads come from both.
Multi-Sample Batch Processing
cat > samples.txt <<EOF
sample1 /data/s1_R1.fastq.gz /data/s1_R2.fastq.gz
sample2 /data/s2_R1.fastq.gz /data/s2_R2.fastq.gz
sample3 /data/s3_R1.fastq.gz /data/s3_R2.fastq.gz
EOF
kb count -i index.idx -g t2g.txt -x 10xv3 -o multi_out --h5ad \
--batch-barcodes \
--tcc \
batch.txt
Each cell's barcode gets prefixed with its sample ID, so you can split later.
Key Parameters to Adjust
kb ref
--workflow — standard (default), nac (nascent+mature for snRNA-seq + velocity), lamanno (legacy velocity layout)
- Memory: index build for human/mouse needs ~16 GB; use a smaller
k-mer (default 31) only for tiny references
kb count
-x <tech> — must match your library chemistry exactly; use kb --list to check
--workflow — must match the kb ref workflow
--h5ad — recommended; writes a ready-to-load AnnData
--filter bustools — applies bustools knee-plot cell calling (optional)
-t — threads; one per core, kb scales near-linearly to ~8 threads
-m 8G — RAM cap; bump if you see OOM errors
Memory considerations
- Index files for human/mouse: ~3-5 GB. Keep them on local SSD for
kb count.
kb count peak RAM is roughly 1.5× the index size.
- For atlas-scale runs (1000+ samples), build the index once and reuse across all
kb count jobs.
Best Practices
- Build the index once, reuse forever. It's deterministic — same FASTA + GTF + workflow gives the same index. Stash it on shared storage.
- Use
--h5ad. Saves writing the awkward MTX → AnnData glue code. Works for scanpy and converts cleanly to Seurat / SCE.
- Don't filter cells in
kb count. Skip --filter bustools for most analyses — use scanpy min_genes=200 + min_cells=3 downstream, or CellBender for ambient-aware filtering.
- For snRNA-seq, ALWAYS use
--workflow nac. Without nascent indexing, you throw away 30-70% of reads (the introns).
- Check
run_info.json after every run. Pseudoalignment rate < 70% means your reference doesn't match the data well — check species, gene model version.
- Multi-sample: use
--batch-barcodes + a samples file. One kb count call across many samples is much faster than parallel jobs because the index loads once.
- Reproducibility: pin kb-python + kallisto + bustools versions with
kb --version. Index files can subtly change across versions.
Common Pitfalls
- Mismatched
kb ref and kb count workflows. kb ref --workflow nac + kb count without --workflow nac silently produces wrong results.
- Forgetting
-x chemistry. Default is 10x v3; using it on v2 / Drop-seq data fails silently (most barcodes won't correct).
- Confusing R1 (barcode + UMI) with R2 (cDNA). Most chemistries are R1=barcode, R2=cDNA. The order in the command matters.
- Using filtered_feature_bc_matrix output style as input to
kb. kb takes raw FASTQs; it's the upstream step. cellranger's matrix outputs aren't compatible.
- Index built on wrong gene model. If counts look 30% lower than expected from cellranger, the GTF probably annotates fewer transcripts. Use the same GTF cellranger uses (typically GENCODE or Ensembl primary assembly) for fair comparison.
End-to-End Template
assets/kallisto_template.sh — single parameterized script. Edit FASTQ paths, technology, and reference paths, then run end-to-end ref → count → handoff message.
Convenience Scripts
scripts/kb_ref.sh — index builder (standard or nac workflow)
scripts/kb_count.sh — per-sample quantifier
For downstream analysis, switch to one of:
References
- kallisto docs — Pachter Lab
- Introduction
- Installation
- Index generation
- Single-cell quantification
- scanpy notebook tutorial
- Bray et al. (2016), Near-optimal probabilistic RNA-seq quantification, Nature Biotechnology (original kallisto paper)
- Melsted et al. (2021), Modular, efficient and constant-memory single-cell RNA-seq preprocessing, Nature Biotechnology (kb-python paper)
- Sullivan et al. (2024), kallisto, bustools, and kb-python for quantifying bulk, single-cell, and single-nucleus RNA-seq, Nature Protocols