V(D)J Recombination and Adaptive Immune Receptor Biology
When to Use
- Explaining V(D)J recombination mechanics, IMGT gene nomenclature (
TRBV20-1*01), or CDR3 junction structure to interpret TCR/BCR sequencing results
- Deciding how to define a clonotype (nucleotide-identical, amino-acid-identical, CDR3-only, or distance-based) before comparing repertoires
- Parsing an AIRR-format rearrangement TSV or a 10x Genomics
filtered_contig_annotations.csv VDJ output into a clonotype table
- Computing repertoire diversity/clonality (Shannon entropy, Simpson index, Chao1, D50) to compare conditions (tumor vs blood, pre- vs post-treatment)
- Detecting clonal expansion, spectratyping CDR3 length, or flagging public clonotypes against a reference (e.g. VDJdb)
Version Compatibility
- Python ≥3.10, pandas ≥2.0, numpy ≥1.24, scipy ≥1.11, matplotlib ≥3.7
- AIRR Community Rearrangement schema ≥1.4 (https://docs.airr-community.org/)
- 10x Genomics Cell Ranger
vdj ≥7.0 output format (filtered_contig_annotations.csv)
- For scRNA-seq-paired VDJ objects use scirpy ≥0.13 or Dandelion ≥0.3 (see
bio-tcr-bcr-analysis-scirpy-analysis)
Prerequisites
pip install pandas numpy scipy matplotlib
- Basic immunology: T-cell/B-cell receptor structure, MHC/peptide presentation
- A clonotype table (AIRR TSV or 10x CSV) with
v_call/j_call/junction_aa/duplicate_count columns, or raw contigs to build one
Core biology reference
| Locus prefix |
Chain |
Receptor |
Segments |
TRAV, TRAJ |
α |
TCR αβ |
V, J (no D) |
TRBV, TRBD, TRBJ |
β |
TCR αβ |
V, D, J |
TRGV, TRGJ |
γ |
TCR γδ |
V, J (no D) |
TRDV, TRDD, TRDJ |
δ |
TCR γδ |
V, D, J |
IGHV, IGHD, IGHJ |
heavy |
BCR/Ab |
V, D, J |
IGKV, IGKJ / IGLV, IGLJ |
κ/λ light |
BCR/Ab |
V, J (no D) |
CDR3 (IMGT positions 105–117) spans V-end Cys → junction (N-nucleotides + D + N-nucleotides) → J-start Phe/Trp — it is the most variable part of the receptor and the basis of clonotype identity. B cells additionally undergo AID-mediated somatic hypermutation after antigen activation (~10⁻³ mutations/bp/generation), further diversifying CDRs — this does not apply to T cells.
Clonotype definition strategies (pick one and state it explicitly before comparing samples):
| Strategy |
Definition |
Use case |
| Strict |
Identical V + J + CDR3 nucleotide |
Tracking exact clonal lineages |
| AA |
Same V + J + CDR3 amino acid |
Convergent recombination |
| CDR3-only |
Identical CDR3 amino acid |
Cross-individual "public" clones |
| Distance-based |
CDR3 Hamming/Levenshtein distance ≤ 1 |
Clonal family clustering (SHM) |
Building an AIRR Clonotype Table
Goal: turn raw V(D)J calls (as would come from IgBlast/MiXCR/10x Cell Ranger) into an AIRR-style clonotype table with per-clone frequency.
Approach: one row per unique clone with v_call/d_call/j_call/junction_aa/duplicate_count; frequency is duplicate_count normalized by total reads/UMIs in the sample.
import numpy as np
import pandas as pd
def build_airr_clonotype_table(n_clones=200, seed=0):
"""Construct an AIRR-format clonotype table (stand-in for parsed
10x `filtered_contig_annotations.csv` or MiXCR/IgBlast output).
Returns a DataFrame with clone_id/v_call/d_call/j_call/junction_aa/
duplicate_count/cdr3_length/frequency, sorted by clone size.
"""
rng = np.random.default_rng(seed)
trbv_genes = ["TRBV2", "TRBV5-1", "TRBV6-5", "TRBV9", "TRBV12-3",
"TRBV20-1", "TRBV28", "TRBV29-1", "TRBV7-2", "TRBV30"]
trbj_genes = ["TRBJ1-1", "TRBJ1-2", "TRBJ2-1", "TRBJ2-3", "TRBJ2-7"]
amino_acids = list("ACDEFGHIKLMNPQRSTVWY")
def random_cdr3_aa(min_len=8, max_len=18):
"""CDR3 amino acid sequence: starts with conserved Cys, ends with Phe."""
length = rng.integers(min_len, max_len + 1)
middle = "".join(rng.choice(amino_acids, length - 2))
return "C" + middle + "F"
clone_sizes = np.sort(rng.zipf(1.8, n_clones))[::-1] # power-law clone sizes
clone_sizes = np.minimum(clone_sizes, 5000)
table = pd.DataFrame({
"clone_id": [f"clonotype{i + 1}" for i in range(n_clones)],
"v_call": rng.choice(trbv_genes, n_clones),
"d_call": rng.choice(["TRBD1", "TRBD2"], n_clones),
"j_call": rng.choice(trbj_genes, n_clones),
"junction_aa": [random_cdr3_aa() for _ in range(n_clones)],
"duplicate_count": clone_sizes,
})
table["cdr3_length"] = table["junction_aa"].str.len()
table["frequency"] = table["duplicate_count"] / table["duplicate_count"].sum()
return table.sort_values("duplicate_count", ascending=False).reset_index(drop=True)
clonotype_table = build_airr_clonotype_table()
print(f"Total clones: {len(clonotype_table)}")
print(f"Top clone frequency: {clonotype_table['frequency'].max():.1%}")
print(clonotype_table.head(10).to_string(index=False))
Repertoire Diversity Metrics
Goal: quantify how dominated a repertoire is by expanded clones (tumor infiltration, acute infection, leukemia) vs how diverse/polyclonal it is.
Approach: compute Shannon entropy, normalized Shannon, Simpson index, clonality (1 − normalized Shannon), D50 (clones needed to reach 50% of reads), and Chao1 (estimated true richness from singletons/doubletons) directly from clone frequencies/counts.
def shannon_entropy(freqs):
"""Shannon entropy H = -sum(p_i * ln(p_i)) over nonzero clone frequencies."""
freqs = np.asarray(freqs)
freqs = freqs[freqs > 0]
return -np.sum(freqs * np.log(freqs))
def normalized_shannon(freqs):
"""H' = H / ln(N); 0-1 scale, 1.0 = perfectly uniform repertoire."""
n = int(np.sum(np.asarray(freqs) > 0))
h = shannon_entropy(freqs)
return h / np.log(n) if n > 1 else 0.0
def simpson_d(freqs):
"""Simpson index D = sum(p_i^2); D -> 1 as a single clone dominates."""
return float(np.sum(np.asarray(freqs) ** 2))
def clonality(freqs):
"""Clonality = 1 - H'; approaches 1 for monoclonal expansions (e.g. leukemia)."""
return 1 - normalized_shannon(freqs)
def d50_index(freqs):
"""Number of top clones needed to reach 50% of total reads."""
sorted_freqs = np.sort(np.asarray(freqs))[::-1]
cum = np.cumsum(sorted_freqs)
return int(np.searchsorted(cum, 0.5) + 1)
def chao1_estimator(duplicate_counts):
"""Chao1 richness estimator: true clone count inferred from singletons (f1)
and doubletons (f2) among observed clones. duplicate_counts: per-clone
read/UMI counts (S_obs = number of observed clones).
"""
counts = np.asarray(duplicate_counts)
s_obs = len(counts)
f1 = np.sum(counts == 1)
f2 = np.sum(counts == 2)
if f2 == 0:
return s_obs + f1 * (f1 - 1) / 2 # bias-corrected form when f2 = 0
return s_obs + (f1 ** 2) / (2 * f2)
freqs = clonotype_table["frequency"].values
print(f"Shannon H: {shannon_entropy(freqs):.3f}")
print(f"Normalized H': {normalized_shannon(freqs):.3f}")
print(f"Simpson D: {simpson_d(freqs):.4f}")
print(f"Clonality: {clonality(freqs):.3f}")
print(f"D50: {d50_index(freqs)}")
print(f"Chao1 richness: {chao1_estimator(clonotype_table['duplicate_count']):.1f}")
Comparing V-Gene Usage and CDR3 Length Between Conditions
Goal: compare TRBV gene usage, CDR3 length distribution (spectratype), and clonal expansion between two samples (e.g. healthy blood vs tumor-infiltrating lymphocytes).
Approach: build a per-sample V-gene usage histogram, overlay CDR3-length distributions, and plot rank-abundance curves on a log scale to visualize clonal skewing.
import matplotlib.pyplot as plt
def compare_vgene_usage(sample_a_counts, sample_b_counts, gene_names,
label_a="Sample A", label_b="Sample B"):
"""Bar-plot V-gene usage (%) side by side for two repertoires.
sample_a_counts/sample_b_counts: arrays of per-gene clone/read counts,
aligned to gene_names.
"""
x = np.arange(len(gene_names))
w = 0.35
fig, ax = plt.subplots(figsize=(9, 4))
ax.bar(x - w / 2, np.asarray(sample_a_counts) / np.sum(sample_a_counts) * 100,
w, label=label_a, color="steelblue", alpha=0.8)
ax.bar(x + w / 2, np.asarray(sample_b_counts) / np.sum(sample_b_counts) * 100,
w, label=label_b, color="firebrick", alpha=0.8)
ax.set_xticks(x)
ax.set_xticklabels(gene_names, rotation=45, ha="right", fontsize=8)
ax.set_ylabel("Usage (%)")
ax.set_title("V-Gene Usage Comparison")
ax.legend()
plt.tight_layout()
return fig
rng = np.random.default_rng(42)
trbv_all = clonotype_table["v_call"].unique().tolist()
healthy_counts = rng.multinomial(2000, np.ones(len(trbv_all)) / len(trbv_all))
til_probs = np.ones(len(trbv_all))
til_probs[trbv_all.index("TRBV20-1")] = 8.0 # simulate tumor-reactive expansion
til_probs /= til_probs.sum()
til_counts = rng.multinomial(1200, til_probs)
compare_vgene_usage(healthy_counts, til_counts, trbv_all,
label_a="Healthy Blood", label_b="Tumor TIL")
plt.savefig("vgene_usage_comparison.png", dpi=120, bbox_inches="tight")
Pitfalls
- Clonotype definition matters: nucleotide-identical, CDR3-amino-acid-identical, and distance-based (Hamming ≤ 1) definitions give different clone counts — pick one and apply it consistently across all samples being compared.
- Sequencing depth confounds diversity metrics: Shannon/Simpson/Chao1 are not comparable across samples with different total read/UMI counts without rarefaction (subsample to equal depth first).
- Use UMI-collapsed counts, not raw reads: bulk TCR/BCR-seq without UMIs inflates apparent clone size via PCR duplication;
duplicate_count should reflect unique molecules.
- Not all chains have a D segment: TRA/TRG/IGK/IGL rearrange V+J only — don't apply TRB/IGH-style junctional-diversity or CDR3-length assumptions to light chains.
- 10x
filtered_contig_annotations.csv is one row per contig/chain, not per cell: aggregate by barcode and resolve multi-chain/multiplet cells before building clonotypes.
- Multiple testing: apply FDR correction (Benjamini-Hochberg) when testing diversity or clonotype frequency across many samples/timepoints simultaneously.
See Also
bio-tcr-bcr-analysis-scirpy-analysis — scRNA-seq-paired VDJ analysis with scirpy/AnnData
bio-tcr-bcr-analysis-vdjtools-analysis — command-line repertoire diversity and overlap statistics
bio-tcr-bcr-analysis-immcantation-analysis — BCR lineage reconstruction and somatic hypermutation
bio-tcr-bcr-analysis-repertoire-visualization — clonotype network and repertoire plotting
1---2name: bio-applied-vdj-biology3description: Compute TCR/BCR clonotype diversity (Shannon, Simpson, clonality, Chao1, D50) from AIRR/10x VDJ tables; explains IMGT V/D/J nomenclature and CDR3 junctions. Use for repertoire diversity, clonal expansion, or scTCR-seq analysis.4---56# V(D)J Recombination and Adaptive Immune Receptor Biology78## When to Use910- Explaining V(D)J recombination mechanics, IMGT gene nomenclature (`TRBV20-1*01`), or CDR3 junction structure to interpret TCR/BCR sequencing results11- Deciding how to define a clonotype (nucleotide-identical, amino-acid-identical, CDR3-only, or distance-based) before comparing repertoires12- Parsing an AIRR-format rearrangement TSV or a 10x Genomics `filtered_contig_annotations.csv` VDJ output into a clonotype table13- Computing repertoire diversity/clonality (Shannon entropy, Simpson index, Chao1, D50) to compare conditions (tumor vs blood, pre- vs post-treatment)14- Detecting clonal expansion, spectratyping CDR3 length, or flagging public clonotypes against a reference (e.g. VDJdb)1516## Version Compatibility1718- Python ≥3.10, pandas ≥2.0, numpy ≥1.24, scipy ≥1.11, matplotlib ≥3.719- AIRR Community Rearrangement schema ≥1.4 (https://docs.airr-community.org/)20- 10x Genomics Cell Ranger `vdj` ≥7.0 output format (`filtered_contig_annotations.csv`)21- For scRNA-seq-paired VDJ objects use scirpy ≥0.13 or Dandelion ≥0.3 (see `bio-tcr-bcr-analysis-scirpy-analysis`)2223## Prerequisites2425- `pip install pandas numpy scipy matplotlib`26- Basic immunology: T-cell/B-cell receptor structure, MHC/peptide presentation27- A clonotype table (AIRR TSV or 10x CSV) with `v_call`/`j_call`/`junction_aa`/`duplicate_count` columns, or raw contigs to build one2829### Core biology reference3031| Locus prefix | Chain | Receptor | Segments |32|---|---|---|---|33| `TRAV`, `TRAJ` | α | TCR αβ | V, J (no D) |34| `TRBV`, `TRBD`, `TRBJ` | β | TCR αβ | V, D, J |35| `TRGV`, `TRGJ` | γ | TCR γδ | V, J (no D) |36| `TRDV`, `TRDD`, `TRDJ` | δ | TCR γδ | V, D, J |37| `IGHV`, `IGHD`, `IGHJ` | heavy | BCR/Ab | V, D, J |38| `IGKV`, `IGKJ` / `IGLV`, `IGLJ` | κ/λ light | BCR/Ab | V, J (no D) |3940CDR3 (IMGT positions 105–117) spans V-end Cys → junction (N-nucleotides + D + N-nucleotides) → J-start Phe/Trp — it is the most variable part of the receptor and the basis of clonotype identity. B cells additionally undergo AID-mediated somatic hypermutation after antigen activation (~10⁻³ mutations/bp/generation), further diversifying CDRs — this does not apply to T cells.4142**Clonotype definition strategies** (pick one and state it explicitly before comparing samples):4344| Strategy | Definition | Use case |45|---|---|---|46| Strict | Identical V + J + CDR3 nucleotide | Tracking exact clonal lineages |47| AA | Same V + J + CDR3 amino acid | Convergent recombination |48| CDR3-only | Identical CDR3 amino acid | Cross-individual "public" clones |49| Distance-based | CDR3 Hamming/Levenshtein distance ≤ 1 | Clonal family clustering (SHM) |5051## Building an AIRR Clonotype Table5253**Goal:** turn raw V(D)J calls (as would come from IgBlast/MiXCR/10x Cell Ranger) into an AIRR-style clonotype table with per-clone frequency.54**Approach:** one row per unique clone with `v_call`/`d_call`/`j_call`/`junction_aa`/`duplicate_count`; frequency is duplicate_count normalized by total reads/UMIs in the sample.5556```python57import numpy as np58import pandas as pd596061def build_airr_clonotype_table(n_clones=200, seed=0):62 """Construct an AIRR-format clonotype table (stand-in for parsed63 10x `filtered_contig_annotations.csv` or MiXCR/IgBlast output).6465 Returns a DataFrame with clone_id/v_call/d_call/j_call/junction_aa/66 duplicate_count/cdr3_length/frequency, sorted by clone size.67 """68 rng = np.random.default_rng(seed)69 trbv_genes = ["TRBV2", "TRBV5-1", "TRBV6-5", "TRBV9", "TRBV12-3",70 "TRBV20-1", "TRBV28", "TRBV29-1", "TRBV7-2", "TRBV30"]71 trbj_genes = ["TRBJ1-1", "TRBJ1-2", "TRBJ2-1", "TRBJ2-3", "TRBJ2-7"]72 amino_acids = list("ACDEFGHIKLMNPQRSTVWY")7374 def random_cdr3_aa(min_len=8, max_len=18):75 """CDR3 amino acid sequence: starts with conserved Cys, ends with Phe."""76 length = rng.integers(min_len, max_len + 1)77 middle = "".join(rng.choice(amino_acids, length - 2))78 return "C" + middle + "F"7980 clone_sizes = np.sort(rng.zipf(1.8, n_clones))[::-1] # power-law clone sizes81 clone_sizes = np.minimum(clone_sizes, 5000)8283 table = pd.DataFrame({84 "clone_id": [f"clonotype{i + 1}" for i in range(n_clones)],85 "v_call": rng.choice(trbv_genes, n_clones),86 "d_call": rng.choice(["TRBD1", "TRBD2"], n_clones),87 "j_call": rng.choice(trbj_genes, n_clones),88 "junction_aa": [random_cdr3_aa() for _ in range(n_clones)],89 "duplicate_count": clone_sizes,90 })91 table["cdr3_length"] = table["junction_aa"].str.len()92 table["frequency"] = table["duplicate_count"] / table["duplicate_count"].sum()93 return table.sort_values("duplicate_count", ascending=False).reset_index(drop=True)949596clonotype_table = build_airr_clonotype_table()97print(f"Total clones: {len(clonotype_table)}")98print(f"Top clone frequency: {clonotype_table['frequency'].max():.1%}")99print(clonotype_table.head(10).to_string(index=False))100```101102## Repertoire Diversity Metrics103104**Goal:** quantify how dominated a repertoire is by expanded clones (tumor infiltration, acute infection, leukemia) vs how diverse/polyclonal it is.105**Approach:** compute Shannon entropy, normalized Shannon, Simpson index, clonality (1 − normalized Shannon), D50 (clones needed to reach 50% of reads), and Chao1 (estimated true richness from singletons/doubletons) directly from clone frequencies/counts.106107```python108def shannon_entropy(freqs):109 """Shannon entropy H = -sum(p_i * ln(p_i)) over nonzero clone frequencies."""110 freqs = np.asarray(freqs)111 freqs = freqs[freqs > 0]112 return -np.sum(freqs * np.log(freqs))113114115def normalized_shannon(freqs):116 """H' = H / ln(N); 0-1 scale, 1.0 = perfectly uniform repertoire."""117 n = int(np.sum(np.asarray(freqs) > 0))118 h = shannon_entropy(freqs)119 return h / np.log(n) if n > 1 else 0.0120121122def simpson_d(freqs):123 """Simpson index D = sum(p_i^2); D -> 1 as a single clone dominates."""124 return float(np.sum(np.asarray(freqs) ** 2))125126127def clonality(freqs):128 """Clonality = 1 - H'; approaches 1 for monoclonal expansions (e.g. leukemia)."""129 return 1 - normalized_shannon(freqs)130131132def d50_index(freqs):133 """Number of top clones needed to reach 50% of total reads."""134 sorted_freqs = np.sort(np.asarray(freqs))[::-1]135 cum = np.cumsum(sorted_freqs)136 return int(np.searchsorted(cum, 0.5) + 1)137138139def chao1_estimator(duplicate_counts):140 """Chao1 richness estimator: true clone count inferred from singletons (f1)141 and doubletons (f2) among observed clones. duplicate_counts: per-clone142 read/UMI counts (S_obs = number of observed clones).143 """144 counts = np.asarray(duplicate_counts)145 s_obs = len(counts)146 f1 = np.sum(counts == 1)147 f2 = np.sum(counts == 2)148 if f2 == 0:149 return s_obs + f1 * (f1 - 1) / 2 # bias-corrected form when f2 = 0150 return s_obs + (f1 ** 2) / (2 * f2)151152153freqs = clonotype_table["frequency"].values154print(f"Shannon H: {shannon_entropy(freqs):.3f}")155print(f"Normalized H': {normalized_shannon(freqs):.3f}")156print(f"Simpson D: {simpson_d(freqs):.4f}")157print(f"Clonality: {clonality(freqs):.3f}")158print(f"D50: {d50_index(freqs)}")159print(f"Chao1 richness: {chao1_estimator(clonotype_table['duplicate_count']):.1f}")160```161162## Comparing V-Gene Usage and CDR3 Length Between Conditions163164**Goal:** compare TRBV gene usage, CDR3 length distribution (spectratype), and clonal expansion between two samples (e.g. healthy blood vs tumor-infiltrating lymphocytes).165**Approach:** build a per-sample V-gene usage histogram, overlay CDR3-length distributions, and plot rank-abundance curves on a log scale to visualize clonal skewing.166167```python168import matplotlib.pyplot as plt169170171def compare_vgene_usage(sample_a_counts, sample_b_counts, gene_names,172 label_a="Sample A", label_b="Sample B"):173 """Bar-plot V-gene usage (%) side by side for two repertoires.174175 sample_a_counts/sample_b_counts: arrays of per-gene clone/read counts,176 aligned to gene_names.177 """178 x = np.arange(len(gene_names))179 w = 0.35180 fig, ax = plt.subplots(figsize=(9, 4))181 ax.bar(x - w / 2, np.asarray(sample_a_counts) / np.sum(sample_a_counts) * 100,182 w, label=label_a, color="steelblue", alpha=0.8)183 ax.bar(x + w / 2, np.asarray(sample_b_counts) / np.sum(sample_b_counts) * 100,184 w, label=label_b, color="firebrick", alpha=0.8)185 ax.set_xticks(x)186 ax.set_xticklabels(gene_names, rotation=45, ha="right", fontsize=8)187 ax.set_ylabel("Usage (%)")188 ax.set_title("V-Gene Usage Comparison")189 ax.legend()190 plt.tight_layout()191 return fig192193194rng = np.random.default_rng(42)195trbv_all = clonotype_table["v_call"].unique().tolist()196healthy_counts = rng.multinomial(2000, np.ones(len(trbv_all)) / len(trbv_all))197til_probs = np.ones(len(trbv_all))198til_probs[trbv_all.index("TRBV20-1")] = 8.0 # simulate tumor-reactive expansion199til_probs /= til_probs.sum()200til_counts = rng.multinomial(1200, til_probs)201202compare_vgene_usage(healthy_counts, til_counts, trbv_all,203 label_a="Healthy Blood", label_b="Tumor TIL")204plt.savefig("vgene_usage_comparison.png", dpi=120, bbox_inches="tight")205```206207## Pitfalls208209- **Clonotype definition matters**: nucleotide-identical, CDR3-amino-acid-identical, and distance-based (Hamming ≤ 1) definitions give different clone counts — pick one and apply it consistently across all samples being compared.210- **Sequencing depth confounds diversity metrics**: Shannon/Simpson/Chao1 are not comparable across samples with different total read/UMI counts without rarefaction (subsample to equal depth first).211- **Use UMI-collapsed counts, not raw reads**: bulk TCR/BCR-seq without UMIs inflates apparent clone size via PCR duplication; `duplicate_count` should reflect unique molecules.212- **Not all chains have a D segment**: TRA/TRG/IGK/IGL rearrange V+J only — don't apply TRB/IGH-style junctional-diversity or CDR3-length assumptions to light chains.213- **10x `filtered_contig_annotations.csv` is one row per contig/chain, not per cell**: aggregate by `barcode` and resolve multi-chain/multiplet cells before building clonotypes.214- **Multiple testing**: apply FDR correction (Benjamini-Hochberg) when testing diversity or clonotype frequency across many samples/timepoints simultaneously.215216## See Also217218- `bio-tcr-bcr-analysis-scirpy-analysis` — scRNA-seq-paired VDJ analysis with scirpy/AnnData219- `bio-tcr-bcr-analysis-vdjtools-analysis` — command-line repertoire diversity and overlap statistics220- `bio-tcr-bcr-analysis-immcantation-analysis` — BCR lineage reconstruction and somatic hypermutation221- `bio-tcr-bcr-analysis-repertoire-visualization` — clonotype network and repertoire plotting