--- title: "Immune Repertoire Analysis - Detailed Code & Procedures" task: "" lineage_type: import upstream_source: https://github.com/mims-harvard/ToolUniverse/blob/e2520a96/skills/tooluniverse-immune-repertoire-analysis/ANALYSIS_DETAILS.md upstream_sha: e2520a96 imported_at: 2026-06-26 prompt_class: prompt upstream_changes: accepted author: upstream validated: false --- # Immune Repertoire Analysis - Detailed Code & Procedures ## Phase 1: Data Import & Clonotype Definition ### Load AIRR-seq Data ```python import pandas as pd import numpy as np from collections import Counter def load_airr_data(file_path, format='mixcr'): """ Load immune repertoire data from common formats. Supported formats: - 'mixcr': MiXCR output - 'immunoseq': Adaptive Biotechnologies ImmunoSEQ - 'airr': AIRR Community Standard - '10x': 10x Genomics VDJ output """ if format == 'mixcr': df = pd.read_csv(file_path, sep='\t') clonotype_df = pd.DataFrame({ 'cloneId': df.get('cloneId', range(len(df))), 'count': df.get('cloneCount', df.get('count', 0)), 'frequency': df.get('cloneFraction', df.get('frequency', 0)), 'cdr3aa': df.get('aaSeqCDR3', df.get('cdr3', '')), 'cdr3nt': df.get('nSeqCDR3', ''), 'v_gene': df.get('allVHitsWithScore', df.get('v_call', '')), 'j_gene': df.get('allJHitsWithScore', df.get('j_call', '')), 'chain': df.get('chain', 'TRB') }) elif format == '10x': df = pd.read_csv(file_path) clonotype_df = df.groupby('barcode').agg({ 'cdr3': lambda x: ','.join(x.dropna()), 'cdr3_nt': lambda x: ','.join(x.dropna()), 'v_gene': lambda x: ','.join(x.dropna()), 'j_gene': lambda x: ','.join(x.dropna()), 'chain': lambda x: ','.join(x.dropna()), 'umis': 'sum' }).reset_index() clonotype_df = clonotype_df.rename(columns={ 'barcode': 'cloneId', 'cdr3': 'cdr3aa', 'cdr3_nt': 'cdr3nt', 'umis': 'count' }) clonotype_df['frequency'] = clonotype_df['count'] / clonotype_df['count'].sum() elif format == 'airr': df = pd.read_csv(file_path, sep='\t') clonotype_df = pd.DataFrame({ 'cloneId': df.get('clone_id', range(len(df))), 'count': df.get('duplicate_count', 1), 'frequency': df.get('clone_frequency', df.get('duplicate_count', 1) / df.get('duplicate_count', 1).sum()), 'cdr3aa': df.get('junction_aa', ''), 'cdr3nt': df.get('junction', ''), 'v_gene': df.get('v_call', ''), 'j_gene': df.get('j_call', ''), 'chain': df.get('locus', 'TRB') }) clonotype_df['cdr3_length'] = clonotype_df['cdr3aa'].str.len() return clonotype_df ``` ### Define Clonotypes ```python def define_clonotypes(df, method='cdr3aa'): """ Define clonotypes based on various criteria. Methods: - 'cdr3aa': Amino acid CDR3 sequence only - 'cdr3nt': Nucleotide CDR3 sequence - 'vj_cdr3': V gene + J gene + CDR3aa (most common) """ if method == 'cdr3aa': df['clonotype'] = df['cdr3aa'] elif method == 'cdr3nt': df['clonotype'] = df['cdr3nt'] elif method == 'vj_cdr3': df['v_family'] = df['v_gene'].str.extract(r'(TRB[VDJ]\d+)', expand=False) df['j_family'] = df['j_gene'].str.extract(r'(TRB[VDJ]\d+)', expand=False) df['clonotype'] = df['v_family'] + '_' + df['j_family'] + '_' + df['cdr3aa'] clonotype_summary = df.groupby('clonotype').agg({ 'count': 'sum', 'frequency': 'sum' }).reset_index() clonotype_summary = clonotype_summary.sort_values('count', ascending=False) clonotype_summary['rank'] = range(1, len(clonotype_summary) + 1) return clonotype_summary ``` --- ## Phase 2: Diversity & Clonality Analysis ### Calculate Diversity Metrics ```python def calculate_diversity(clonotype_counts): """ Calculate diversity metrics for immune repertoire. Metrics: - Shannon entropy: Overall diversity - Simpson index: Probability two random clones are same - Inverse Simpson: Effective number of clonotypes - Gini coefficient: Inequality in clonotype distribution """ from scipy.stats import entropy if isinstance(clonotype_counts, pd.Series): counts = clonotype_counts.values else: counts = clonotype_counts freqs = counts / counts.sum() shannon = entropy(freqs, base=2) simpson = np.sum(freqs ** 2) inv_simpson = 1 / simpson if simpson > 0 else 0 sorted_freqs = np.sort(freqs) n = len(freqs) cumsum = np.cumsum(sorted_freqs) gini = (2 * np.sum((np.arange(1, n+1)) * sorted_freqs)) / (n * cumsum[-1]) - (n + 1) / n richness = len(counts) max_entropy = np.log2(richness) evenness = shannon / max_entropy if max_entropy > 0 else 0 clonality = 1 - evenness return { 'richness': richness, 'shannon_entropy': shannon, 'simpson_index': simpson, 'inverse_simpson': inv_simpson, 'gini_coefficient': gini, 'evenness': evenness, 'clonality': clonality } ``` ### Rarefaction Analysis ```python def rarefaction_curve(df, n_samples=100, n_boots=10): """ Generate rarefaction curve to assess sampling depth. Shows how clonotype richness increases with sequencing depth. """ total_reads = df['count'].sum() sample_sizes = np.linspace(1000, total_reads, n_samples) richness_curves = [] for _ in range(n_boots): richness_at_depth = [] for sample_size in sample_sizes: sampled = np.random.choice( df.index, size=int(sample_size), p=df['frequency'].values, replace=True ) unique_clonotypes = len(set(sampled)) richness_at_depth.append(unique_clonotypes) richness_curves.append(richness_at_depth) mean_richness = np.mean(richness_curves, axis=0) std_richness = np.std(richness_curves, axis=0) return sample_sizes, mean_richness, std_richness ``` --- ## Phase 3: V(D)J Gene Usage Analysis ### Analyze V and J Gene Usage ```python def analyze_vdj_usage(df): """Analyze V(D)J gene usage patterns.""" df['v_family'] = df['v_gene'].str.extract(r'(TRB[VDJ]\d+)', expand=False) df['j_family'] = df['j_gene'].str.extract(r'(TRB[VDJ]\d+)', expand=False) v_usage = df.groupby('v_family')['count'].sum().sort_values(ascending=False) v_usage_freq = v_usage / v_usage.sum() j_usage = df.groupby('j_family')['count'].sum().sort_values(ascending=False) j_usage_freq = j_usage / j_usage.sum() vj_pairs = df.groupby(['v_family', 'j_family'])['count'].sum().reset_index() vj_pairs['frequency'] = vj_pairs['count'] / vj_pairs['count'].sum() vj_pairs = vj_pairs.sort_values('count', ascending=False) return {'v_usage': v_usage_freq, 'j_usage': j_usage_freq, 'vj_pairs': vj_pairs} ``` ### Statistical Testing for Biased Usage ```python def test_vdj_bias(observed_usage, expected_frequencies=None): """Test whether V(D)J gene usage deviates from expected (uniform or reference).""" from scipy.stats import chisquare observed = observed_usage.values if expected_frequencies is None: expected = np.ones(len(observed)) / len(observed) * observed.sum() else: expected = expected_frequencies * observed.sum() chi2, pvalue = chisquare(observed, f_exp=expected) return {'chi2_statistic': chi2, 'p_value': pvalue, 'significant': pvalue < 0.05} ``` --- ## Phase 4: CDR3 Sequence Analysis ### CDR3 Length Distribution ```python def analyze_cdr3_length(df): """ Analyze CDR3 length distribution. Typical TCR CDR3 length: 12-18 amino acids Typical BCR CDR3 length: 10-20 amino acids """ length_dist = df.groupby('cdr3_length')['count'].sum().sort_index() length_freq = length_dist / length_dist.sum() mean_length = (df['cdr3_length'] * df['count']).sum() / df['count'].sum() median_length = df['cdr3_length'].median() return { 'length_distribution': length_freq, 'mean_length': mean_length, 'median_length': median_length } ``` ### Amino Acid Composition ```python def analyze_cdr3_composition(cdr3_sequences, weights=None): """Analyze amino acid composition in CDR3 regions.""" from collections import Counter if weights is None: weights = np.ones(len(cdr3_sequences)) aa_counts = Counter() total_aa = 0 for seq, weight in zip(cdr3_sequences, weights): for aa in seq: aa_counts[aa] += weight total_aa += weight aa_freq = {aa: count / total_aa for aa, count in aa_counts.items()} aa_freq_df = pd.DataFrame.from_dict(aa_freq, orient='index', columns=['frequency']) aa_freq_df = aa_freq_df.sort_values('frequency', ascending=False) return aa_freq_df ``` --- ## Phase 5: Clonal Expansion Detection ### Identify Expanded Clonotypes ```python def detect_expanded_clones(clonotypes, threshold_percentile=95): """ Identify clonally expanded T/B cell populations. Expanded clonotypes = clones above frequency threshold. """ threshold = np.percentile(clonotypes['frequency'], threshold_percentile) expanded = clonotypes[clonotypes['frequency'] >= threshold].copy() expanded = expanded.sort_values('frequency', ascending=False) total_expanded_freq = expanded['frequency'].sum() n_expanded = len(expanded) return { 'expanded_clonotypes': expanded, 'n_expanded': n_expanded, 'expanded_frequency': total_expanded_freq, 'threshold': threshold } ``` ### Longitudinal Clonotype Tracking ```python def track_clonotypes_longitudinal(timepoint_dataframes, clonotype_col='clonotype'): """ Track clonotype dynamics across multiple timepoints. Input: List of DataFrames, each representing one timepoint """ all_timepoints = [] for i, df in enumerate(timepoint_dataframes): df_copy = df.copy() df_copy['timepoint'] = i all_timepoints.append(df_copy[[clonotype_col, 'frequency', 'timepoint']]) merged = pd.concat(all_timepoints, ignore_index=True) tracking = merged.pivot(index=clonotype_col, columns='timepoint', values='frequency') tracking = tracking.fillna(0) tracking['persistence'] = (tracking > 0).sum(axis=1) tracking['mean_frequency'] = tracking.iloc[:, :-1].mean(axis=1) tracking['max_frequency'] = tracking.iloc[:, :-1].max(axis=1) tracking = tracking.sort_values(['persistence', 'max_frequency'], ascending=False) return tracking ``` --- ## Phase 6: Convergence & Public Clonotypes ### Detect Convergent Recombination ```python def detect_convergent_recombination(df): """ Identify cases where different nucleotide sequences encode same CDR3 amino acid. Convergent recombination = same CDR3aa from different CDR3nt sequences. """ convergence = df.groupby('cdr3aa').agg({ 'cdr3nt': lambda x: len(set(x)), 'count': 'sum', 'frequency': 'sum' }).reset_index() convergent = convergence[convergence['cdr3nt'] > 1].copy() convergent = convergent.rename(columns={'cdr3nt': 'n_nucleotide_variants'}) convergent = convergent.sort_values('n_nucleotide_variants', ascending=False) return convergent ``` ### Identify Public (Shared) Clonotypes ```python def identify_public_clonotypes(sample_dataframes, min_samples=2): """ Identify public (shared) clonotypes present in multiple samples. Input: List of DataFrames, each representing one sample """ all_samples = [] for i, df in enumerate(sample_dataframes): df_copy = df[['clonotype', 'frequency']].copy() df_copy['sample_id'] = f'Sample_{i+1}' all_samples.append(df_copy) merged = pd.concat(all_samples, ignore_index=True) public_counts = merged.groupby('clonotype').agg({ 'sample_id': lambda x: len(set(x)), 'frequency': 'mean' }).reset_index() public_counts = public_counts.rename(columns={'sample_id': 'n_samples'}) public = public_counts[public_counts['n_samples'] >= min_samples].copy() public = public.sort_values(['n_samples', 'frequency'], ascending=False) return public ``` --- ## Phase 7: Epitope Prediction & Specificity ### Query IEDB for Known Epitopes ```python def query_epitope_database(cdr3_sequences, organism='human', top_n=10): """Query IEDB for known T-cell epitopes matching CDR3 sequences.""" from tooluniverse import ToolUniverse tu = ToolUniverse() epitope_matches = {} for cdr3 in cdr3_sequences[:top_n]: result = tu.run_one_function({ "name": "IEDB_search_tcells", "arguments": {"receptor": cdr3, "organism": organism} }) if 'data' in result and 'epitopes' in result['data']: epitopes = result['data']['epitopes'] if len(epitopes) > 0: epitope_matches[cdr3] = epitopes return epitope_matches ``` ### Predict Epitope Specificity with VDJdb ```python def predict_specificity_vdjdb(cdr3_sequences, chain='TRB'): """ Predict antigen specificity using VDJdb (TCR database). VDJdb contains TCR sequences with known epitope specificity. """ # VDJdb: https://vdjdb.cdr3.net/search # Alternative: Use PubMed literature search from tooluniverse import ToolUniverse tu = ToolUniverse() specificity_results = {} for cdr3 in cdr3_sequences[:5]: result = tu.run_one_function({ "name": "PubMed_search_articles", "arguments": { "query": f'"{cdr3}" AND (epitope OR antigen OR specificity)', "max_results": 10 } }) if 'data' in result and 'papers' in result['data']: papers = result['data']['papers'] if len(papers) > 0: specificity_results[cdr3] = papers return specificity_results ``` --- ## Phase 8: Integration with Single-Cell Data ### Link Clonotypes to Cell Phenotypes ```python def integrate_with_single_cell(vdj_df, gex_adata, barcode_col='barcode'): """ Integrate TCR/BCR clonotypes with single-cell gene expression. Requires: vdj_df (DataFrame), gex_adata (AnnData object) """ import scanpy as sc clonotype_map = dict(zip(vdj_df[barcode_col], vdj_df['clonotype'])) gex_adata.obs['clonotype'] = gex_adata.obs.index.map(clonotype_map) gex_adata.obs['has_clonotype'] = ~gex_adata.obs['clonotype'].isna() clonotype_counts = gex_adata.obs['clonotype'].value_counts() expanded_clonotypes = clonotype_counts[clonotype_counts > 5].index.tolist() gex_adata.obs['is_expanded'] = gex_adata.obs['clonotype'].isin(expanded_clonotypes) return gex_adata ``` ### Clonotype-Phenotype Association ```python def analyze_clonotype_phenotype(adata, clonotype_col='clonotype', cluster_col='leiden'): """Analyze association between clonotypes and cell phenotypes/clusters.""" import scanpy as sc cells_with_tcr = adata[~adata.obs[clonotype_col].isna()].copy() clonotype_cluster = pd.crosstab( cells_with_tcr.obs[clonotype_col], cells_with_tcr.obs[cluster_col], normalize='index' ) cluster_specific = clonotype_cluster[clonotype_cluster.max(axis=1) > 0.8] top_per_cluster = {} for cluster in clonotype_cluster.columns: top_clonotypes = clonotype_cluster[cluster].sort_values(ascending=False).head(5) top_per_cluster[cluster] = top_clonotypes.index.tolist() return { 'clonotype_cluster_matrix': clonotype_cluster, 'cluster_specific_clonotypes': cluster_specific, 'top_clonotypes_per_cluster': top_per_cluster } ```