CoolFace
Modelpublic

Imranyai/CodonFM-80M-mRNA-stability

sourceHugging Faceotherupdated 5mo agoView on Hugging Face
0likes
data_setup.py772 linesDownload Raw Back to root
1"""2data_setup.py — Download, preprocess, audit, and prepare all datasets for3CodonFM-80M mRNA stability fine-tuning and benchmarking.4 5Usage:6    # Download and preprocess everything7    python data_setup.py --all8 9    # Download only training datasets10    python data_setup.py --training11 12    # Download only benchmark datasets13    python data_setup.py --benchmark14 15    # Audit datasets (inspect stats, find issues)16    python data_setup.py --audit17 18    # Export preprocessed training data to local files19    python data_setup.py --training --export ./processed_data20 21    # Show codon vocabulary and tokenizer details22    python data_setup.py --vocab23"""24 25import argparse26import json27import os28import sys29import csv30import urllib.request31from collections import Counter32from pathlib import Path33 34import numpy as np35 36try:37    import pandas as pd38except ImportError:39    pd = None40 41try:42    from datasets import load_dataset43except ImportError:44    load_dataset = None45 46 47# ============================================================48# 1. CODON VOCABULARY & TOKENIZER49# ============================================================50 51RNA_BASES = ['A', 'U', 'G', 'C']52ALL_CODONS = [b1 + b2 + b3 for b1 in RNA_BASES for b2 in RNA_BASES for b3 in RNA_BASES]53 54# Biological codon table (RNA) → Amino Acid55CODON_TABLE = {56    'UUU': 'Phe', 'UUC': 'Phe', 'UUA': 'Leu', 'UUG': 'Leu',57    'CUU': 'Leu', 'CUC': 'Leu', 'CUA': 'Leu', 'CUG': 'Leu',58    'AUU': 'Ile', 'AUC': 'Ile', 'AUA': 'Ile', 'AUG': 'Met/Start',59    'GUU': 'Val', 'GUC': 'Val', 'GUA': 'Val', 'GUG': 'Val',60    'UCU': 'Ser', 'UCC': 'Ser', 'UCA': 'Ser', 'UCG': 'Ser',61    'CCU': 'Pro', 'CCC': 'Pro', 'CCA': 'Pro', 'CCG': 'Pro',62    'ACU': 'Thr', 'ACC': 'Thr', 'ACA': 'Thr', 'ACG': 'Thr',63    'GCU': 'Ala', 'GCC': 'Ala', 'GCA': 'Ala', 'GCG': 'Ala',64    'UAU': 'Tyr', 'UAC': 'Tyr', 'UAA': 'Stop', 'UAG': 'Stop',65    'CAU': 'His', 'CAC': 'His', 'CAA': 'Gln', 'CAG': 'Gln',66    'AAU': 'Asn', 'AAC': 'Asn', 'AAA': 'Lys', 'AAG': 'Lys',67    'GAU': 'Asp', 'GAC': 'Asp', 'GAA': 'Glu', 'GAG': 'Glu',68    'UGU': 'Cys', 'UGC': 'Cys', 'UGA': 'Stop', 'UGG': 'Trp',69    'CGU': 'Arg', 'CGC': 'Arg', 'CGA': 'Arg', 'CGG': 'Arg',70    'AGU': 'Ser', 'AGC': 'Ser', 'AGA': 'Arg', 'AGG': 'Arg',71    'GGU': 'Gly', 'GGC': 'Gly', 'GGA': 'Gly', 'GGG': 'Gly',72}73 74# Token vocabulary (matches CodonFM config: vocab_size=69, pad_token_id=3)75SPECIAL_TOKENS = {'[CLS]': 0, '[SEP]': 1, '[MASK]': 2, '[PAD]': 3, '[UNK]': 4}76CODON_TO_ID = {codon: i + 5 for i, codon in enumerate(ALL_CODONS)}77ID_TO_CODON = {v: k for k, v in CODON_TO_ID.items()}78ID_TO_CODON.update({v: k for k, v in SPECIAL_TOKENS.items()})79 80VOCAB_SIZE = len(SPECIAL_TOKENS) + len(ALL_CODONS)  # 5 + 64 = 6981assert VOCAB_SIZE == 6982 83PAD_TOKEN_ID = 384CLS_TOKEN_ID = 085SEP_TOKEN_ID = 186MASK_TOKEN_ID = 287UNK_TOKEN_ID = 488 89 90def seq_to_codons(seq: str) -> list:91    """Split an mRNA/DNA sequence into codon triplets."""92    seq = seq.upper().replace('T', 'U').strip()93    return [seq[i:i+3] for i in range(0, len(seq) - len(seq) % 3, 3)]94 95 96def tokenize_mRNA(seq: str, max_length: int = 2046) -> dict:97    """Tokenize an mRNA/DNA sequence into CodonFM token IDs."""98    codons = seq_to_codons(seq)99    token_ids = [CLS_TOKEN_ID]100    for codon in codons[:max_length - 2]:101        token_ids.append(CODON_TO_ID.get(codon, UNK_TOKEN_ID))102    token_ids.append(SEP_TOKEN_ID)103    attention_mask = [1] * len(token_ids)104    return {'input_ids': token_ids, 'attention_mask': attention_mask}105 106 107def validate_sequence(seq: str) -> dict:108    """Validate an mRNA/DNA sequence for CodonFM compatibility."""109    seq_clean = seq.upper().replace('T', 'U').strip()110    issues = []111 112    if len(seq_clean) == 0:113        issues.append("Empty sequence")114    if len(seq_clean) % 3 != 0:115        issues.append(f"Length {len(seq_clean)} not divisible by 3 (truncated to {len(seq_clean) - len(seq_clean) % 3})")116 117    invalid_chars = set(seq_clean) - {'A', 'U', 'G', 'C'}118    if invalid_chars:119        issues.append(f"Invalid characters: {invalid_chars}")120 121    codons = seq_to_codons(seq_clean)122    n_codons = len(codons)123 124    # Check for start codon125    starts_with_aug = codons[0] == 'AUG' if codons else False126 127    # Check for stop codons128    stop_codons = {'UAA', 'UAG', 'UGA'}129    internal_stops = [i for i, c in enumerate(codons[:-1]) if c in stop_codons]130    ends_with_stop = codons[-1] in stop_codons if codons else False131 132    if internal_stops:133        issues.append(f"Internal stop codons at positions: {internal_stops}")134 135    # Unknown codons136    unk_codons = [c for c in codons if c not in CODON_TO_ID]137    if unk_codons:138        issues.append(f"Unknown codons: {set(unk_codons)}")139 140    return {141        'valid': len(issues) == 0,142        'issues': issues,143        'length_nt': len(seq_clean),144        'length_codons': n_codons,145        'starts_with_AUG': starts_with_aug,146        'ends_with_stop': ends_with_stop,147        'n_internal_stops': len(internal_stops),148    }149 150 151# ============================================================152# 2. TRAINING DATASETS153# ============================================================154 155TRAINING_DATASETS = {156    'mogam-ai/CDS-BART-mRNA-stability': {157        'description': 'iCodon vertebrate mRNA stability profiles (human, mouse, frog, fish)',158        'source_paper': 'Diez et al. 2022, Scientific Reports "iCodon customizes gene expression based on the codon composition"',159        'seq_col': 'seq',160        'label_col': 'y',161        'splits': {'train': 'train', 'val': 'val', 'test': 'test'},162        'label_meaning': 'mRNA half-life z-score (higher = more stable, mean≈0, std≈1)',163        'species': ['Human', 'Mouse', 'Xenopus (frog)', 'Zebrafish'],164        'notes': 'RNA sequences (A,U,G,C). All divisible by 3. Subset of GleghornLab dataset.',165    },166    'GleghornLab/mrna_stability_other': {167        'description': 'Extended multi-species mRNA stability data (superset of mogam-ai dataset)',168        'source_paper': 'Li et al. 2024, Genome Research "CodonBERT large language model for mRNA vaccines"',169        'seq_col': 'rna',170        'label_col': 'labels',171        'splits': {'train': 'train', 'val': 'valid', 'test': 'test'},172        'label_meaning': 'mRNA half-life z-score (higher = more stable)',173        'species': ['Multiple vertebrate species'],174        'notes': 'Has extra "seqs" column (protein-encoded, not used). Contains 1 outlier sequence of 3 nt. Superset of mogam-ai.',175        'extra_col': 'seqs',176    },177}178 179 180def download_training_data(export_dir=None):181    """Download and inspect training datasets from HuggingFace Hub."""182    if load_dataset is None:183        print("ERROR: `datasets` library required. Run: pip install datasets")184        return None185 186    all_data = {}187 188    for repo_id, info in TRAINING_DATASETS.items():189        print(f"\n{'='*60}")190        print(f"📦 {repo_id}")191        print(f"   {info['description']}")192        print(f"={'='*60}")193 194        ds = load_dataset(repo_id)195 196        for split_name, hf_split in info['splits'].items():197            split_data = ds[hf_split]198            seqs = split_data[info['seq_col']]199            labels = split_data[info['label_col']]200 201            print(f"\n  [{split_name}] {len(seqs)} samples")202            print(f"    Seq lengths (nt):  min={min(len(s) for s in seqs)}, "203                  f"mean={np.mean([len(s) for s in seqs]):.0f}, "204                  f"max={max(len(s) for s in seqs)}")205            print(f"    Seq lengths (cod): min={min(len(s)//3 for s in seqs)}, "206                  f"mean={np.mean([len(s)//3 for s in seqs]):.0f}, "207                  f"max={max(len(s)//3 for s in seqs)}")208            labels_arr = np.array(labels)209            print(f"    Labels:  min={labels_arr.min():.3f}, mean={labels_arr.mean():.3f}, "210                  f"std={labels_arr.std():.3f}, max={labels_arr.max():.3f}")211 212        all_data[repo_id] = ds213 214    if export_dir:215        export_training_data(all_data, export_dir)216 217    return all_data218 219 220def preprocess_training_data(use_both_datasets=True, min_codons=3, max_codons=2046,221                              remove_duplicates=True, deduplicate_across_splits=True):222    """223    Preprocess training data: clean, filter, deduplicate, and combine.224 225    Steps:226    1. Load both HF datasets227    2. Use GleghornLab as primary (superset) OR combine both228    3. Filter: remove sequences < min_codons or > max_codons codons229    4. Filter: remove sequences with NaN labels230    5. Filter: remove sequences with invalid characters231    6. Deduplicate: remove exact sequence duplicates within each split232    7. Deduplicate: ensure no train sequences appear in val/test (data leakage check)233    8. Return clean {train, val, test} dictionaries234 235    Returns:236        dict with 'train', 'val', 'test' keys, each containing 'sequences' and 'labels' lists237    """238    if load_dataset is None:239        raise ImportError("datasets library required: pip install datasets")240 241    print("Loading datasets...")242 243    if use_both_datasets:244        # Use GleghornLab (superset) — it contains ALL of mogam-ai plus extra data245        ds = load_dataset("GleghornLab/mrna_stability_other")246        raw_data = {247            'train': {'seqs': ds['train']['rna'], 'labels': ds['train']['labels']},248            'val': {'seqs': ds['valid']['rna'], 'labels': ds['valid']['labels']},249            'test': {'seqs': ds['test']['rna'], 'labels': ds['test']['labels']},250        }251        print(f"  Using GleghornLab/mrna_stability_other (superset)")252    else:253        # Use mogam-ai only (smaller, cleaner)254        ds = load_dataset("mogam-ai/CDS-BART-mRNA-stability")255        raw_data = {256            'train': {'seqs': ds['train']['seq'], 'labels': ds['train']['y']},257            'val': {'seqs': ds['val']['seq'], 'labels': ds['val']['y']},258            'test': {'seqs': ds['test']['seq'], 'labels': ds['test']['y']},259        }260        print(f"  Using mogam-ai/CDS-BART-mRNA-stability only")261 262    clean_data = {}263    total_removed = {'short': 0, 'long': 0, 'nan': 0, 'invalid': 0, 'duplicate': 0}264 265    for split in ['train', 'val', 'test']:266        seqs = raw_data[split]['seqs']267        labels = raw_data[split]['labels']268        orig_count = len(seqs)269 270        clean_seqs = []271        clean_labels = []272        seen = set()273 274        for seq, label in zip(seqs, labels):275            # Skip None/empty276            if seq is None or len(seq) == 0:277                total_removed['invalid'] += 1278                continue279 280            # Normalize: uppercase, T→U281            seq = seq.upper().replace('T', 'U').strip()282 283            # Check NaN label284            if np.isnan(label):285                total_removed['nan'] += 1286                continue287 288            # Check invalid characters289            if set(seq) - {'A', 'U', 'G', 'C'}:290                total_removed['invalid'] += 1291                continue292 293            # Check length294            n_codons = len(seq) // 3295            if n_codons < min_codons:296                total_removed['short'] += 1297                continue298            if n_codons > max_codons:299                total_removed['long'] += 1300                continue301 302            # Deduplicate within split303            if remove_duplicates:304                if seq in seen:305                    total_removed['duplicate'] += 1306                    continue307                seen.add(seq)308 309            clean_seqs.append(seq)310            clean_labels.append(float(label))311 312        clean_data[split] = {'sequences': clean_seqs, 'labels': clean_labels}313        print(f"  [{split}] {orig_count} → {len(clean_seqs)} samples "314              f"(removed {orig_count - len(clean_seqs)})")315 316    # Cross-split deduplication: check for train/test leakage317    if deduplicate_across_splits:318        test_seqs = set(clean_data['test']['sequences'])319        val_seqs = set(clean_data['val']['sequences'])320 321        leakage_test = sum(1 for s in clean_data['train']['sequences'] if s in test_seqs)322        leakage_val = sum(1 for s in clean_data['train']['sequences'] if s in val_seqs)323        val_test_overlap = len(val_seqs & test_seqs)324 325        print(f"\n  Data leakage check:")326        print(f"    Train→Test overlap:  {leakage_test} sequences")327        print(f"    Train→Val overlap:   {leakage_val} sequences")328        print(f"    Val→Test overlap:    {val_test_overlap} sequences")329 330        if leakage_test > 0 or leakage_val > 0:331            print(f"    ⚠️  WARNING: Data leakage detected! Removing leaked sequences from train...")332            eval_seqs = test_seqs | val_seqs333            filtered_train = [(s, l) for s, l in334                             zip(clean_data['train']['sequences'], clean_data['train']['labels'])335                             if s not in eval_seqs]336            clean_data['train']['sequences'] = [x[0] for x in filtered_train]337            clean_data['train']['labels'] = [x[1] for x in filtered_train]338            print(f"    Train after dedup: {len(clean_data['train']['sequences'])} samples")339 340    print(f"\n  Removal summary: {total_removed}")341    print(f"  Final sizes: train={len(clean_data['train']['sequences'])}, "342          f"val={len(clean_data['val']['sequences'])}, "343          f"test={len(clean_data['test']['sequences'])}")344 345    return clean_data346 347 348def export_training_data(data, export_dir):349    """Export preprocessed data to CSV files."""350    os.makedirs(export_dir, exist_ok=True)351 352    if isinstance(data, dict) and 'train' in data and 'sequences' in data.get('train', {}):353        # Already preprocessed format354        for split in ['train', 'val', 'test']:355            if split not in data:356                continue357            filepath = os.path.join(export_dir, f'{split}.csv')358            with open(filepath, 'w', newline='') as f:359                writer = csv.writer(f)360                writer.writerow(['sequence', 'stability_score'])361                for seq, label in zip(data[split]['sequences'], data[split]['labels']):362                    writer.writerow([seq, label])363            print(f"  Exported {split}: {len(data[split]['sequences'])} rows → {filepath}")364    else:365        print("  Export requires preprocessed data. Run preprocess_training_data() first.")366 367 368# ============================================================369# 3. BENCHMARK DATASETS370# ============================================================371 372CODONBERT_BASE_URL = "https://raw.githubusercontent.com/Sanofi-Public/CodonBERT/master/benchmarks/CodonBERT/data/fine-tune"373 374BENCHMARK_DATASETS = {375    'stability': {376        'url': f"{CODONBERT_BASE_URL}/mRNA_Stability.csv",377        'filename': 'mRNA_Stability.csv',378        'description': 'mRNA Stability (iCodon vertebrate mRNA half-life)',379        'source': 'Diez et al. 2022, Scientific Reports',380        'samples': 65356,381        'seq_length': '3-3066 nt (1-1022 codons)',382        'label': 'Half-life z-score (continuous, mean≈0, std≈1)',383        'metric': 'Spearman ρ',384        'columns': 'sequence, value, dataset, split',385        'species': 'Multi-vertebrate (human, mouse, frog, fish)',386    },387    'mrfp': {388        'url': f"{CODONBERT_BASE_URL}/mRFP_Expression.csv",389        'filename': 'mRFP_Expression.csv',390        'description': 'mRFP Protein Expression in E. coli',391        'source': 'Li et al. 2024, Genome Research (CodonBERT)',392        'samples': 1459,393        'seq_length': '678 nt (226 codons, fixed)',394        'label': 'Fluorescence intensity (log scale, range 7.4-11.4)',395        'metric': 'Spearman ρ',396        'columns': 'sequence, value, dataset, split',397        'species': 'E. coli (synthetic mRFP variants)',398    },399    'vaccine': {400        'url': f"{CODONBERT_BASE_URL}/CoV_Vaccine_Degradation.csv",401        'filename': 'CoV_Vaccine_Degradation.csv',402        'description': 'SARS-CoV-2 mRNA Vaccine Degradation',403        'source': 'CodonBERT benchmark (derived from Stanford OpenVaccine)',404        'samples': 2400,405        'seq_length': '81 nt (27 codons, fixed)',406        'label': 'Degradation score (z-normalized, range -7.2 to 6.5)',407        'metric': 'Spearman ρ',408        'columns': 'sequence, value, dataset, split',409        'species': 'Synthetic SARS-CoV-2 mRNA vaccine fragments',410    },411    'riboswitch': {412        'url': f"{CODONBERT_BASE_URL}/Tc-Riboswitches.csv",413        'filename': 'Tc-Riboswitches.csv',414        'description': 'Tetracycline Riboswitch Activity',415        'source': 'Groher et al. 2018 (via CodonBERT)',416        'samples': 355,417        'seq_length': '66-75 nt (22-25 codons)',418        'label': 'Switching factor (continuous, range -0.3 to 3.1)',419        'metric': 'Spearman ρ',420        'columns': 'sequence, value, dataset, split',421        'species': 'Synthetic tetracycline riboswitches',422    },423    'mlos': {424        'url': f"{CODONBERT_BASE_URL}/MLOS.csv",425        'filename': 'MLOS.csv',426        'description': 'MLOS Flu Vaccine Antigen Expression',427        'source': 'Ren et al. 2024 (HELM/MLOS)',428        'samples': 167,429        'seq_length': '~1700 nt (~567 codons)',430        'label': 'Expression level (continuous, range 0.3-2.2)',431        'metric': 'Spearman ρ',432        'columns': 'cds, value (no split column — uses random 70/15/15)',433        'species': 'Influenza haemagglutinin CDS variants',434        'notes': 'No pre-defined splits. Column name is "cds" not "sequence".',435    },436}437 438 439def download_benchmark_data(data_dir='./benchmark_data'):440    """Download all benchmark datasets."""441    os.makedirs(data_dir, exist_ok=True)442 443    for task_name, info in BENCHMARK_DATASETS.items():444        filepath = os.path.join(data_dir, info['filename'])445        if os.path.exists(filepath):446            size = os.path.getsize(filepath)447            print(f"  ✓ {info['filename']} already exists ({size/1024:.1f} KB)")448        else:449            print(f"  ↓ Downloading {info['filename']}...")450            try:451                urllib.request.urlretrieve(info['url'], filepath)452                size = os.path.getsize(filepath)453                print(f"  ✓ Downloaded {info['filename']} ({size/1024:.1f} KB)")454            except Exception as e:455                print(f"  ✗ Failed to download {info['filename']}: {e}")456 457    return data_dir458 459 460# ============================================================461# 4. AUDIT462# ============================================================463 464def audit_dataset(sequences, labels, name="dataset"):465    """Run a comprehensive audit on a list of sequences and labels."""466    print(f"\n{'='*60}")467    print(f"AUDIT: {name} ({len(sequences)} sequences)")468    print(f"{'='*60}")469 470    if len(sequences) == 0:471        print("  (empty)")472        return473 474    # ---- Sequence stats ----475    lengths_nt = [len(s) for s in sequences]476    lengths_codon = [len(s) // 3 for s in sequences]477 478    print(f"\n  📏 Sequence Lengths:")479    print(f"    Nucleotides: min={min(lengths_nt)}, mean={np.mean(lengths_nt):.0f}, "480          f"median={np.median(lengths_nt):.0f}, max={max(lengths_nt)}")481    print(f"    Codons:      min={min(lengths_codon)}, mean={np.mean(lengths_codon):.0f}, "482          f"median={np.median(lengths_codon):.0f}, max={max(lengths_codon)}")483 484    # Length distribution buckets485    buckets = [0, 100, 300, 500, 1000, 2000, 3000, 10000]486    hist = np.histogram(lengths_codon, bins=buckets)[0]487    print(f"    Codon length distribution:")488    for i, count in enumerate(hist):489        pct = 100 * count / len(sequences)490        bar = '█' * int(pct / 2)491        print(f"      {buckets[i]:>5}-{buckets[i+1]:>5} codons: {count:>6} ({pct:>5.1f}%) {bar}")492 493    # Sequences > 2046 codons (CodonFM max)494    over_limit = sum(1 for c in lengths_codon if c > 2046)495    if over_limit > 0:496        print(f"    ⚠️  {over_limit} sequences exceed CodonFM max (2046 codons) — will be truncated")497 498    # ---- Nucleotide composition ----499    all_chars = Counter()500    for s in sequences:501        all_chars.update(s.upper())502    total_bases = sum(all_chars.values())503    print(f"\n  🧬 Nucleotide Composition:")504    for base in ['A', 'U', 'G', 'C']:505        count = all_chars.get(base, 0)506        pct = 100 * count / total_bases507        print(f"    {base}: {count:>12,} ({pct:.1f}%)")508    unexpected = {k: v for k, v in all_chars.items() if k not in 'AUGC'}509    if unexpected:510        print(f"    ⚠️  Unexpected characters: {unexpected}")511 512    # Not divisible by 3513    not_div3 = sum(1 for s in sequences if len(s) % 3 != 0)514    if not_div3 > 0:515        print(f"    ⚠️  {not_div3} sequences not divisible by 3")516 517    # ---- Codon usage ----518    codon_counts = Counter()519    for s in sequences[:5000]:  # sample for speed520        codons = seq_to_codons(s)521        codon_counts.update(codons)522 523    print(f"\n  🔤 Codon Usage (top 10 / bottom 10 from {min(5000, len(sequences))} seqs):")524    sorted_codons = codon_counts.most_common()525    for codon, count in sorted_codons[:10]:526        aa = CODON_TABLE.get(codon, '?')527        print(f"    {codon} ({aa:>9s}): {count:>8,}")528    print(f"    ...")529    for codon, count in sorted_codons[-10:]:530        aa = CODON_TABLE.get(codon, '?')531        print(f"    {codon} ({aa:>9s}): {count:>8,}")532 533    # Start/stop codon analysis534    starts_with_aug = sum(1 for s in sequences if s[:3].upper().replace('T', 'U') == 'AUG')535    stop_codons = {'UAA', 'UAG', 'UGA'}536    ends_with_stop = sum(1 for s in sequences537                         if seq_to_codons(s)[-1] in stop_codons) if sequences else 0538    print(f"\n  🚦 Start/Stop Codons:")539    print(f"    Starts with AUG:  {starts_with_aug}/{len(sequences)} ({100*starts_with_aug/len(sequences):.1f}%)")540    print(f"    Ends with stop:   {ends_with_stop}/{len(sequences)} ({100*ends_with_stop/len(sequences):.1f}%)")541 542    # ---- Label stats ----543    labels_arr = np.array(labels, dtype=float)544    nan_count = np.isnan(labels_arr).sum()545    labels_clean = labels_arr[~np.isnan(labels_arr)]546 547    print(f"\n  📊 Label Distribution:")548    print(f"    Count: {len(labels_arr)}, NaN: {nan_count}")549    if len(labels_clean) > 0:550        print(f"    Min:    {labels_clean.min():.4f}")551        print(f"    Q1:     {np.percentile(labels_clean, 25):.4f}")552        print(f"    Median: {np.median(labels_clean):.4f}")553        print(f"    Q3:     {np.percentile(labels_clean, 75):.4f}")554        print(f"    Max:    {labels_clean.max():.4f}")555        print(f"    Mean:   {labels_clean.mean():.4f}")556        print(f"    Std:    {labels_clean.std():.4f}")557        print(f"    Skew:   {float(((labels_clean - labels_clean.mean()) ** 3).mean() / labels_clean.std() ** 3):.4f}")558 559    # ---- Duplicates ----560    unique_seqs = len(set(sequences))561    dup_count = len(sequences) - unique_seqs562    print(f"\n  🔍 Duplicates:")563    print(f"    Unique sequences:    {unique_seqs}")564    print(f"    Duplicate sequences: {dup_count}")565 566    # ---- Outliers ----567    if len(labels_clean) > 0:568        q1, q3 = np.percentile(labels_clean, [25, 75])569        iqr = q3 - q1570        lower = q1 - 3 * iqr571        upper = q3 + 3 * iqr572        outliers = np.sum((labels_clean < lower) | (labels_clean > upper))573        print(f"\n  ⚡ Outliers (>3 IQR):")574        print(f"    Label outliers: {outliers}/{len(labels_clean)}")575 576    very_short = sum(1 for c in lengths_codon if c < 10)577    very_long = sum(1 for c in lengths_codon if c > 1000)578    print(f"    Very short (<10 codons): {very_short}")579    print(f"    Very long (>1000 codons): {very_long}")580 581 582def run_full_audit():583    """Run audit on all training and benchmark datasets."""584    print("=" * 70)585    print("FULL DATASET AUDIT")586    print("=" * 70)587 588    # Training datasets589    print("\n\n📚 TRAINING DATASETS")590    print("=" * 70)591 592    if load_dataset is not None:593        ds1 = load_dataset("mogam-ai/CDS-BART-mRNA-stability")594        for split in ['train', 'val', 'test']:595            audit_dataset(596                ds1[split]['seq'], ds1[split]['y'],597                f"mogam-ai/CDS-BART-mRNA-stability [{split}]"598            )599 600        ds2 = load_dataset("GleghornLab/mrna_stability_other")601        for split, hf_split in [('train', 'train'), ('val', 'valid'), ('test', 'test')]:602            audit_dataset(603                ds2[hf_split]['rna'], ds2[hf_split]['labels'],604                f"GleghornLab/mrna_stability_other [{split}]"605            )606 607        # Cross-dataset analysis608        print("\n\n📊 CROSS-DATASET ANALYSIS")609        print("=" * 60)610        ds1_all = set(ds1['train']['seq']) | set(ds1['val']['seq']) | set(ds1['test']['seq'])611        ds2_all = set(ds2['train']['rna']) | set(ds2['valid']['rna']) | set(ds2['test']['rna'])612        print(f"  mogam-ai total unique:    {len(ds1_all)}")613        print(f"  GleghornLab total unique: {len(ds2_all)}")614        print(f"  Overlap:                  {len(ds1_all & ds2_all)}")615        print(f"  mogam-ai ⊂ GleghornLab:   {ds1_all.issubset(ds2_all)}")616        print(f"  GleghornLab-only:         {len(ds2_all - ds1_all)}")617    else:618        print("  Skipping (datasets library not installed)")619 620    # Benchmark datasets621    print("\n\n📊 BENCHMARK DATASETS")622    print("=" * 70)623 624    if pd is not None:625        data_dir = download_benchmark_data()626        for task_name, info in BENCHMARK_DATASETS.items():627            filepath = os.path.join(data_dir, info['filename'])628            if not os.path.exists(filepath):629                continue630            df = pd.read_csv(filepath)631            df.columns = [c.lower().strip() for c in df.columns]632 633            seq_col = 'sequence' if 'sequence' in df.columns else 'cds'634            val_col = 'value'635 636            if seq_col in df.columns and val_col in df.columns:637                audit_dataset(638                    df[seq_col].tolist(), df[val_col].tolist(),639                    f"Benchmark: {task_name} ({info['filename']})"640                )641    else:642        print("  Skipping (pandas not installed)")643 644 645# ============================================================646# 5. VOCAB DISPLAY647# ============================================================648 649def show_vocab():650    """Display the full codon vocabulary with amino acid mapping."""651    print("=" * 70)652    print("CodonFM Tokenizer Vocabulary (vocab_size=69)")653    print("=" * 70)654 655    print("\n  SPECIAL TOKENS:")656    print(f"  {'ID':>4}  {'Token':<10}  {'Description'}")657    print(f"  {'─'*4}  {'─'*10}  {'─'*30}")658    descriptions = {659        '[CLS]': 'Classification token (prepended)',660        '[SEP]': 'Separator token (appended)',661        '[MASK]': 'Mask token (for MLM pretraining)',662        '[PAD]': 'Padding token (pad_token_id=3)',663        '[UNK]': 'Unknown token (for invalid codons)',664    }665    for token, tid in sorted(SPECIAL_TOKENS.items(), key=lambda x: x[1]):666        print(f"  {tid:>4}  {token:<10}  {descriptions.get(token, '')}")667 668    print(f"\n  CODON TOKENS (64 codons → 20 amino acids + 3 stop):")669    print(f"  {'ID':>4}  {'Codon':<6}  {'Amino Acid':<12}  {'ID':>4}  {'Codon':<6}  {'Amino Acid':<12}  "670          f"{'ID':>4}  {'Codon':<6}  {'Amino Acid':<12}  {'ID':>4}  {'Codon':<6}  {'Amino Acid'}")671    print(f"  {'─'*4}  {'─'*6}  {'─'*12}  {'─'*4}  {'─'*6}  {'─'*12}  "672          f"{'─'*4}  {'─'*6}  {'─'*12}  {'─'*4}  {'─'*6}  {'─'*12}")673 674    items = sorted(CODON_TO_ID.items(), key=lambda x: x[1])675    for i in range(0, len(items), 4):676        row_parts = []677        for j in range(4):678            if i + j < len(items):679                codon, tid = items[i + j]680                aa = CODON_TABLE.get(codon, '?')681                row_parts.append(f"  {tid:>4}  {codon:<6}  {aa:<12}")682            else:683                row_parts.append(f"  {'':>4}  {'':6}  {'':12}")684        print("".join(row_parts))685 686    print(f"\n  TOKENIZATION EXAMPLE:")687    example = "AUGGCAGCCGAGACUCGG"688    codons = seq_to_codons(example)689    tokens = tokenize_mRNA(example)690    print(f"    Input:     {example}")691    print(f"    Codons:    {' '.join(codons)}")692    print(f"    Token IDs: {tokens['input_ids']}")693    decoded = [ID_TO_CODON.get(t, '?') for t in tokens['input_ids']]694    print(f"    Decoded:   {' '.join(decoded)}")695 696    print(f"\n  CONFIG (matches nvidia/NV-CodonFM-Encodon-80M-v1/config.json):")697    print(f"    vocab_size:              69")698    print(f"    pad_token_id:            3 ([PAD])")699    print(f"    max_position_embeddings: 2046 codons (~6138 nt)")700    print(f"    position_embedding_type: rotary (RoPE, θ=10000)")701 702 703# ============================================================704# CLI705# ============================================================706 707def main():708    parser = argparse.ArgumentParser(709        description="Dataset setup for CodonFM-80M mRNA stability fine-tuning",710        formatter_class=argparse.RawDescriptionHelpFormatter,711        epilog="""712Examples:713  python data_setup.py --all                        # Download & audit everything714  python data_setup.py --training                   # Download training datasets715  python data_setup.py --training --export ./data   # Download & export to CSV716  python data_setup.py --benchmark                  # Download benchmark datasets717  python data_setup.py --audit                      # Full audit of all datasets718  python data_setup.py --vocab                      # Show codon vocabulary719  python data_setup.py --preprocess                 # Preprocess & deduplicate720        """721    )722 723    parser.add_argument('--all', action='store_true', help='Download and audit everything')724    parser.add_argument('--training', action='store_true', help='Download training datasets from HF Hub')725    parser.add_argument('--benchmark', action='store_true', help='Download benchmark datasets from GitHub')726    parser.add_argument('--audit', action='store_true', help='Run full dataset audit')727    parser.add_argument('--preprocess', action='store_true', help='Preprocess training data (clean, deduplicate)')728    parser.add_argument('--vocab', action='store_true', help='Show codon vocabulary and tokenizer')729    parser.add_argument('--export', type=str, default=None, help='Export directory for preprocessed CSVs')730    parser.add_argument('--benchmark_dir', type=str, default='./benchmark_data',731                        help='Directory for benchmark data')732    parser.add_argument('--use_both', action='store_true', default=True,733                        help='Use both training datasets (default: True)')734    parser.add_argument('--mogam_only', action='store_true',735                        help='Use only mogam-ai dataset (smaller, cleaner)')736 737    args = parser.parse_args()738 739    # Default: show help740    if not any([args.all, args.training, args.benchmark, args.audit, args.preprocess, args.vocab]):741        parser.print_help()742        return743 744    if args.vocab or args.all:745        show_vocab()746 747    if args.training or args.all:748        print("\n\n📦 DOWNLOADING TRAINING DATASETS")749        print("=" * 60)750        download_training_data(export_dir=args.export)751 752    if args.benchmark or args.all:753        print("\n\n📦 DOWNLOADING BENCHMARK DATASETS")754        print("=" * 60)755        download_benchmark_data(args.benchmark_dir)756 757    if args.preprocess or args.all:758        print("\n\n🔧 PREPROCESSING TRAINING DATA")759        print("=" * 60)760        use_both = not args.mogam_only761        clean_data = preprocess_training_data(use_both_datasets=use_both)762        if args.export:763            export_training_data(clean_data, args.export)764 765    if args.audit or args.all:766        print("\n\n🔍 RUNNING FULL AUDIT")767        run_full_audit()768 769 770if __name__ == '__main__':771    main()772