User guide#

Worked, copy-paste-runnable examples for every analysis module. All functions take and return polars DataFrame s on the canonical clonotype schema (AIRR junction column names; see vdjtools.io.schema), so results chain together and drop straight into plotting or .write_csv.

A sample to play with#

Real workflows load a repertoire with io.read("sample.tsv") (see Loading data). So this page is runnable with no downloads, the snippet below draws a synthetic counted sample straight from a bundled model — every later example reuses demo_sample:

import polars as pl
from vdjtools import io as vio
from vdjtools.io import schema as S
from vdjtools.model import load_bundled
from vdjtools.model.generate import generate

def demo_sample(locus="TRB", n=4000, seed=0):
    """A canonical clonotype frame sampled from the bundled OLGA model."""
    seqs = generate(load_bundled(locus, "olga"), n, seed=seed, productive_only=True)
    counted = (seqs.group_by(["junction_nt", "junction_aa", "v_call", "d_call", "j_call"])
                   .len().rename({"len": S.COUNT}))
    return vio.normalize(counted, recompute_freq=True)

sample = demo_sample(seed=1)
sample.columns
# ['v_call', 'd_call', 'j_call', 'c_call', 'junction_aa', 'junction_nt',
#  'duplicate_count', 'frequency']

Loading data#

vdjtools.io reads native vdjtools, AIRR Rearrangement TSV, and Parquet, and auto-detects and converts the common third-party formats (MiXcr v1–4 incl. the C-gene / BCR isotype, MiGec, Adaptive immunoSEQ v1/v2, IMGT/HighV-QUEST, Vidjil, RTCR, TRUST4, and arda’s AIRR annotation output). Every reader returns the same canonical frame:

from vdjtools import io as vio

vio.sniff_format("clones.txt")        # -> 'mixcr' | 'immunoseq' | 'airr' | 'vdjtools' | ...
df = vio.read("clones.txt")           # auto-detect + convert to the canonical frame
df = vio.read_immunoseq("adaptive.tsv")   # or call a converter explicitly

Load a whole cohort from a metadata sheet (one row per sample, plus any phenotype columns), joining the metadata onto every clonotype:

meta = vio.read_metadata("metadata.txt")          # sample_name, disease_status, hla, ...
cohort = vio.read_samples(meta, base_dir="samples/")   # one long frame, metadata attached
by_id = vio.read_samples(meta, base_dir="samples/", as_dict=True)   # or {sample_id: frame}

For large cohorts, ingest_cohort writes a hive-partitioned Parquet store that scan_cohort reads back lazily (pl.LazyFrame), so you never hold every sample in memory at once.

Repertoire statistics#

vdjtools.stats covers diversity, rarefaction/extrapolation, spectratype, and segment usage. diversity_stats returns one row of estimators (observed richness, Chao1, ChaoE, Efron–Thisted, Shannon, normalized Shannon, inverse Simpson, d50):

from vdjtools import stats

stats.diversity_stats(sample)
# columns: reads, observed_diversity, chao1, chaoE, efron_thisted,
#          shannon_wiener, normalized_shannon_wiener, inverse_simpson, d50

# iNEXT-style Hill-number rarefaction + extrapolation with bootstrap CIs
stats.inext(sample, q=(0, 1, 2))         # order_q, m, method, sample_coverage, qD, qD_lo, qD_hi

# V / J / VJ segment usage, and the CDR3-length spectratype
stats.segment_usage(sample, "v")         # locus, v_call, weight
stats.spectratype(sample, kind="aa")     # length spectrum, reads-weighted

Pass weight="unique" to any usage/spectratype call to weight by clonotype instead of reads; inext_batch / rarefaction_batch run a whole cohort on the native parallel kernel.

CDR3 features#

vdjtools.features summarises CDR3 sequence content. physchem_profile gives the mean amino-acid physicochemical properties (Kidera factors, charge, hydropathy, …) per group and CDR3 region; kmer_profile counts k-mers:

from vdjtools import features

features.physchem_profile(sample, region="all", group_by=("v_call",))
# v_call, region, property, mean_value   (long format, one row per property)

features.kmer_profile(sample, k=3)       # locus, kmer, weight
features.v_kmer_c_profile(sample, k=3)   # V-anchored k-mer occurrences

Overlap and TCRnet#

vdjtools.overlap compares samples. Exact-match overlap is pure polars; fuzzy / similarity-aware overlap and TCRnet delegate to the vdjmatch + seqtree engine (pip install "vdjtools[overlap]"):

from vdjtools import overlap

a, b, c = demo_sample(seed=1), demo_sample(seed=2), demo_sample(seed=3)

overlap.overlap_metrics(a, b)            # {'D':.., 'F':.., 'F2':.., 'R':.., 'd1':.., 'd2':.., 'd12':..}
overlap.similarity_overlap(a, b)         # TINA / Leinster-Cobbold sequence-similarity overlap
overlap.tcrnet(a)                        # per-clonotype neighbourhood enrichment (E, p_enrichment)

# all-pairs distance matrix -> 2-D embedding for a cohort
dist = overlap.pairwise_distances({"A": a, "B": b, "C": c}, metric="F")
overlap.cluster_samples(dist, method="mds")     # sample, mds1, mds2

# frequency trajectories of the top clonotypes across an ordered series
overlap.track_clonotypes({"t0": a, "t1": b, "t2": c}, top=50)

Preprocessing#

vdjtools.preprocess normalises samples before comparison — downsampling to a common depth, error-correction, filtering, and pooling/joining:

from vdjtools import preprocess

preprocess.downsample(sample, 1000)              # resample to 1000 reads (numpy multinomial)
preprocess.filter_functional(sample)             # drop out-of-frame / stop-codon clonotypes
preprocess.filter_frequency(sample, min_freq=1e-4)
preprocess.correct(sample, max_mismatches=2)     # collapse likely sequencing errors

# combine samples: pooled clonotype table, or an incidence/frequency join
preprocess.pool_samples([a, b, c])
preprocess.join_samples([a, b, c], min_samples=2)   # clonotypes seen in >=2 samples

correct_vj_usage applies a VJ-usage batch-effect correction across a cohort.

Biomarker association#

vdjtools.biomarker finds clonotypes whose incidence (presence across subjects) associates with a phenotype, by Fisher-exact test (the Emerson-2017 design):

import polars as pl
from vdjtools.biomarker import fisher_association, metaclonotypes

# cohort: long frame with a sample_id column; phenotype: one row per subject
cohort = pl.DataFrame({
    "sample_id":   ["p0","p1","p2","n0","n1","n2"],
    "v_call":      ["TRBV1"]*6, "j_call": ["TRBJ1"]*6,
    "junction_aa": ["CASSXF","CASSXF","CASSXF","CASSXF","CASSBG","CASSBG"],
    "duplicate_count": [10]*6,
})
pheno = pl.DataFrame({"sample_id": ["p0","p1","p2","n0","n1","n2"],
                      "cmv": [True, True, True, False, False, False]})

fisher_association(cohort, pheno, pheno_col="cmv")
# per feature: incidence, n_pos_present, n_neg_present, direction, log2_or, p_value

metaclonotypes(cohort)                 # group near-identical CDR3s (1-mismatch + V/J) -> meta_id

Use match="1mm" on fisher_association to pool single-mismatch neighbours of each clonotype (needs the [overlap] engine).

Single-cell#

vdjtools.sc ingests 10x / AIRR-Cell contigs into a flat cell_id-keyed frame, resolves and pairs chains with doublet / mispairing QC, and scores paired α/β generation probability:

from vdjtools import sc

cells = sc.read_10x("filtered_contig_annotations.csv")   # -> cell_id-keyed Rearrangement frame
cells = sc.resolve_chains(cells)                         # pick the productive chain per locus
paired = sc.pair_chains(cells, locus_pair="TRA_TRB")     # one row per α/β cell

sc.paired_pgen(paired)                 # adds pgen_alpha, pgen_beta, pgen_paired (= product)

write_airr_cell exports the AIRR Cell / Receptor format; to_anndata bridges to the scverse ecosystem.

Recombination model#

vdjtools.model is the native V(D)J engine — generation probability, sampling, and EM inference. Precomputed models for all 7 human loci ship in the wheel:

from vdjtools.model import load_bundled, native
from vdjtools.model.generate import generate

model = load_bundled("TRB", "olga")           # or "learned" (fit to real repertoires)
native.pgen_aa(model, "CASSLAPGATNEKLFF")      # amino-acid Pgen (matches OLGA to 1e-15)
native.pgen_aa(model, "CASSLAPGATNEKLFF", mismatches=1)     # + the Hamming-1 ball
native.pgen_aa_batch(model, seqs, threads=0)   # many CDR3s, thread-parallel (~11x)
generate(model, 1000)                          # sample a repertoire -> DataFrame

Learn a model from your own out-of-frame reads with vdjtools.model.infer.infer_native(), and explore any model’s recombination Bayes net interactively with marimo edit notebooks/model_explorer.py. See the API reference for the full surface.

Command line#

Every workflow above has a CLI counterpart; inputs are auto-detected and results are written as TSV to -o (or stdout):

vdjtools models                                # list the bundled models
vdjtools generate -m TRB -n 1000 -o gen.tsv    # (cf. olga-generate_sequences)
vdjtools pgen seqs.tsv -m TRB -o pgen.tsv      # (cf. olga-compute_pgen)
vdjtools diversity     sampleA.tsv sampleB.tsv -o diversity.tsv
vdjtools overlap       *.tsv -o overlap.tsv
vdjtools segment-usage *.tsv --segment v -o usage.tsv
vdjtools spectratype   *.tsv -o spectra.tsv

Run vdjtools <command> --help for options.