{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":116062,"databundleVersionId":14084779,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================================================================\n# CELL 1: IMPORT VÀ CẤU HÌNH\n# ============================================================================\n\nimport numpy as np\nimport pandas as pd\nimport os\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom collections import Counter\nfrom sklearn.model_selection import train_test_split\nimport gc\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# Đường dẫn\nTRAIN_SEQ_PATH = '/kaggle/input/cafa-6-protein-function-prediction/Train/train_sequences.fasta' \nTRAIN_TERMS_PATH = '/kaggle/input/cafa-6-protein-function-prediction/Train/train_terms.tsv'\nTEST_SEQ_PATH = '/kaggle/input/cafa-6-protein-function-prediction/Test/testsuperset.fasta'\nOBO_PATH = '/kaggle/input/cafa-6-protein-function-prediction/Train/go-basic.obo'\n\n# Hyperparameters\nBATCH_SIZE = 64\nEPOCHS = 20\nLEARNING_RATE = 0.001\nNUM_LABELS = 1500\n\n# Ensemble config\nSEEDS = [42, 123, 456, 789]  # 4 seeds khác nhau\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"🔧 Device: {device}\")\nprint(f\"🎲 Ensemble với {len(SEEDS)} seeds: {SEEDS}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T14:44:16.802688Z","iopub.execute_input":"2025-12-09T14:44:16.803325Z","iopub.status.idle":"2025-12-09T14:44:16.809638Z","shell.execute_reply.started":"2025-12-09T14:44:16.803296Z","shell.execute_reply":"2025-12-09T14:44:16.809040Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CELL 2: HELPER FUNCTIONS - LOAD DATA\n# ============================================================================\n\ndef load_fasta(path):\n    \"\"\"Đọc file FASTA và trả về dict {protein_id: sequence}\"\"\"\n    sequences = {}\n    current_id = None\n    current_seq = []\n    \n    with open(path, 'r') as f:\n        for line in f:\n            line = line.strip()\n            if line.startswith('>'):\n                if current_id:\n                    sequences[current_id] = ''.join(current_seq)\n                \n                header = line[1:]\n                # Parse ID từ header (xử lý cả format sp|ID|Name và >ID)\n                if '|' in header:\n                    parts = header.split('|')\n                    current_id = parts[1] if len(parts) > 1 else header.split()[0]\n                else:\n                    current_id = header.split()[0]\n                    \n                current_seq = []\n            else:\n                current_seq.append(line)\n                \n        if current_id:\n            sequences[current_id] = ''.join(current_seq)\n            \n    return sequences\n\ndef clean_sequence(seq):\n    \"\"\"\n    Làm sạch chuỗi protein:\n    - Chỉ giữ 20 amino acids chuẩn\n    - Thay thế các ký tự đặc biệt\n    \"\"\"\n    valid_aa = set('ACDEFGHIKLMNPQRSTVWY')\n    replacements = {'X': 'A', 'U': 'C', 'B': 'D', 'Z': 'E'}\n    \n    cleaned = []\n    for aa in seq.upper():\n        if aa in valid_aa:\n            cleaned.append(aa)\n        elif aa in replacements:\n            cleaned.append(replacements[aa])\n    \n    return ''.join(cleaned)\n\ndef is_valid_sequence(seq, min_len=50, max_len=5000):\n    \"\"\"Kiểm tra sequence có hợp lệ không\"\"\"\n    length = len(seq)\n    if length < min_len or length > max_len:\n        return False\n    \n    valid_aa = set('ACDEFGHIKLMNPQRSTVWY')\n    valid_count = sum(1 for aa in seq if aa in valid_aa)\n    \n    # Yêu cầu ít nhất 90% là amino acid chuẩn\n    if valid_count / length < 0.9:\n        return False\n    \n    return True","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T14:44:16.810786Z","iopub.execute_input":"2025-12-09T14:44:16.811080Z","iopub.status.idle":"2025-12-09T14:44:16.830248Z","shell.execute_reply.started":"2025-12-09T14:44:16.811061Z","shell.execute_reply":"2025-12-09T14:44:16.829637Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CELL 3: FEATURE EXTRACTION\n# ============================================================================\n\ndef get_dipeptide_composition(sequence):\n    \"\"\"\n    Tạo vector 400 chiều từ tần suất di-peptide\n    Input: protein sequence\n    Output: numpy array (400,)\n    \"\"\"\n    aa_list = 'ACDEFGHIKLMNPQRSTVWY'\n    aa_map = {aa: i for i, aa in enumerate(aa_list)}\n    \n    dipeptide_counts = np.zeros((20, 20), dtype=np.float32)\n    length = len(sequence)\n    \n    if length < 2:\n        return dipeptide_counts.flatten()\n    \n    # Đếm tần suất từng cặp amino acid\n    for i in range(length - 1):\n        a1 = sequence[i]\n        a2 = sequence[i + 1]\n        if a1 in aa_map and a2 in aa_map:\n            dipeptide_counts[aa_map[a1], aa_map[a2]] += 1\n    \n    # Normalize bằng số cặp\n    return dipeptide_counts.flatten() / (length - 1)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T14:44:16.831159Z","iopub.execute_input":"2025-12-09T14:44:16.831356Z","iopub.status.idle":"2025-12-09T14:44:16.848469Z","shell.execute_reply.started":"2025-12-09T14:44:16.831341Z","shell.execute_reply":"2025-12-09T14:44:16.847754Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CELL 4: PREPROCESSING - LOAD VÀ CLEAN DATA\n# ============================================================================\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"📂 BƯỚC 1: LOAD VÀ CLEAN DỮ LIỆU\")\nprint(\"=\"*70)\n\n# Load train sequences\nprint(\"\\n1️⃣ Đang load train sequences...\")\ntrain_seqs_raw = load_fasta(TRAIN_SEQ_PATH)\nprint(f\"   ✓ Loaded: {len(train_seqs_raw)} sequences\")\n\n# Clean sequences\nprint(\"\\n2️⃣ Đang clean sequences...\")\ntrain_seqs = {}\ninvalid_count = 0\n\nfor pid, seq in train_seqs_raw.items():\n    cleaned = clean_sequence(seq)\n    if is_valid_sequence(cleaned):\n        train_seqs[pid] = cleaned\n    else:\n        invalid_count += 1\n\nprint(f\"   ✓ Valid sequences: {len(train_seqs)}\")\nprint(f\"   ✗ Invalid sequences: {invalid_count}\")\nprint(f\"   ✓ Keep ratio: {len(train_seqs)/len(train_seqs_raw)*100:.1f}%\")\n\n# Load train labels\nprint(\"\\n3️⃣ Đang load train labels...\")\ntrain_terms = pd.read_csv(TRAIN_TERMS_PATH, sep='\\t')\nprint(f\"   ✓ Total annotations: {len(train_terms)}\")\nprint(f\"   ✓ Unique GO terms: {train_terms['term'].nunique()}\")\n\n# Filter top terms\nprint(f\"\\n4️⃣ Đang filter top {NUM_LABELS} GO terms...\")\ntop_terms = train_terms['term'].value_counts().head(NUM_LABELS).index.tolist()\nterm_to_idx = {term: i for i, term in enumerate(top_terms)}\nidx_to_term = {i: term for term, i in term_to_idx.items()}\n\ntrain_terms_filtered = train_terms[\n    (train_terms['term'].isin(top_terms)) &\n    (train_terms['EntryID'].isin(train_seqs.keys()))\n]\n\nprotein_to_labels = train_terms_filtered.groupby('EntryID')['term'].apply(list).to_dict()\n\n# Get valid protein IDs (có cả sequence và labels)\nvalid_ids = [pid for pid in train_seqs.keys() if pid in protein_to_labels]\nprint(f\"   ✓ Proteins với labels: {len(valid_ids)}\")\n\n# Statistics\nseq_lengths = [len(train_seqs[pid]) for pid in valid_ids]\nlabel_counts = [len(protein_to_labels[pid]) for pid in valid_ids]\n\nprint(f\"\\n📊 THỐNG KÊ:\")\nprint(f\"   Sequence length: min={min(seq_lengths)}, max={max(seq_lengths)}, mean={np.mean(seq_lengths):.0f}\")\nprint(f\"   Labels per protein: min={min(label_counts)}, max={max(label_counts)}, mean={np.mean(label_counts):.1f}\")\n\n# Free memory\ndel train_seqs_raw, train_terms, train_terms_filtered\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T14:44:16.849164Z","iopub.execute_input":"2025-12-09T14:44:16.849328Z","iopub.status.idle":"2025-12-09T14:44:23.947157Z","shell.execute_reply.started":"2025-12-09T14:44:16.849314Z","shell.execute_reply":"2025-12-09T14:44:23.946539Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CELL 5: FEATURE EXTRACTION CHO TOÀN BỘ DATASET\n# ============================================================================\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"🔧 BƯỚC 2: FEATURE EXTRACTION\")\nprint(\"=\"*70)\n\nprint(\"\\n🔨 Đang extract dipeptide features cho tất cả proteins...\")\nall_features = {}\n\nfor i, pid in enumerate(valid_ids):\n    seq = train_seqs[pid]\n    features = get_dipeptide_composition(seq)\n    all_features[pid] = features\n    \n    if (i + 1) % 5000 == 0:\n        print(f\"   Progress: {i+1}/{len(valid_ids)} ({(i+1)/len(valid_ids)*100:.1f}%)\")\n\nprint(f\"\\n✓ Completed: {len(all_features)} feature vectors (400 dims each)\")\n\n# Verify\nsample_pid = valid_ids[0]\nprint(f\"\\n📋 Sample check:\")\nprint(f\"   Protein ID: {sample_pid}\")\nprint(f\"   Sequence length: {len(train_seqs[sample_pid])}\")\nprint(f\"   Feature shape: {all_features[sample_pid].shape}\")\nprint(f\"   Feature range: [{all_features[sample_pid].min():.4f}, {all_features[sample_pid].max():.4f}]\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T14:44:23.948788Z","iopub.execute_input":"2025-12-09T14:44:23.949142Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CELL 6: PYTORCH DATASET CLASS\n# ============================================================================\n\nclass CAFA6Dataset(Dataset):\n    \"\"\"PyTorch Dataset cho CAFA-6\"\"\"\n    def __init__(self, protein_ids, features_dict, label_dict, term_map, num_classes):\n        self.protein_ids = protein_ids\n        self.features_dict = features_dict\n        self.label_dict = label_dict\n        self.term_map = term_map\n        self.num_classes = num_classes\n    \n    def __len__(self):\n        return len(self.protein_ids)\n    \n    def __getitem__(self, idx):\n        pid = self.protein_ids[idx]\n        \n        # Features\n        features = self.features_dict[pid]\n        \n        # Labels (multi-hot encoding)\n        labels = np.zeros(self.num_classes, dtype=np.float32)\n        if pid in self.label_dict:\n            for term in self.label_dict[pid]:\n                if term in self.term_map:\n                    labels[self.term_map[term]] = 1.0\n        \n        return (\n            torch.tensor(features, dtype=torch.float32),\n            torch.tensor(labels, dtype=torch.float32)\n        )","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CELL 7: MODEL ARCHITECTURE\n# ============================================================================\n\nclass ResidualBlock(nn.Module):\n    \"\"\"Residual block với skip connection\"\"\"\n    def __init__(self, dim, dropout_rate=0.4):\n        super(ResidualBlock, self).__init__()\n        self.layer = nn.Sequential(\n            nn.Linear(dim, dim),\n            nn.BatchNorm1d(dim),\n            nn.ReLU(),\n            nn.Dropout(dropout_rate)\n        )\n    \n    def forward(self, x):\n        return x + self.layer(x)\n\nclass ResMLP(nn.Module):\n    \"\"\"Residual MLP cho multi-label classification\"\"\"\n    def __init__(self, input_dim=400, num_classes=1500):\n        super(ResMLP, self).__init__()\n        \n        self.entry = nn.Sequential(\n            nn.Linear(input_dim, 1024),\n            nn.BatchNorm1d(1024),\n            nn.ReLU(),\n            nn.Dropout(0.3)\n        )\n        \n        self.blocks = nn.Sequential(\n            ResidualBlock(1024, dropout_rate=0.4),\n            ResidualBlock(1024, dropout_rate=0.4)\n        )\n        \n        self.head = nn.Linear(1024, num_classes)\n    \n    def forward(self, x):\n        x = self.entry(x)\n        x = self.blocks(x)\n        return self.head(x)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CELL 8: TRAINING FUNCTION\n# ============================================================================\n\ndef train_single_seed(seed, train_ids, val_ids, all_features, protein_to_labels, \n                      term_to_idx, save_path):\n    \"\"\"\n    Train model với 1 seed cụ thể\n    \n    Args:\n        seed: random seed\n        train_ids: list protein IDs cho training\n        val_ids: list protein IDs cho validation\n        all_features: dict features\n        protein_to_labels: dict labels\n        term_to_idx: dict term mapping\n        save_path: path lưu model\n    \n    Returns:\n        best_val_loss: validation loss tốt nhất\n    \"\"\"\n    print(f\"\\n{'='*70}\")\n    print(f\"🎲 TRAINING SEED: {seed}\")\n    print(f\"{'='*70}\")\n    \n    # Set seed\n    torch.manual_seed(seed)\n    np.random.seed(seed)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed(seed)\n    \n    # Create datasets\n    train_dataset = CAFA6Dataset(\n        train_ids, all_features, protein_to_labels, term_to_idx, NUM_LABELS\n    )\n    val_dataset = CAFA6Dataset(\n        val_ids, all_features, protein_to_labels, term_to_idx, NUM_LABELS\n    )\n    \n    train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)\n    val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)\n    \n    # Create model\n    model = ResMLP(input_dim=400, num_classes=NUM_LABELS).to(device)\n    criterion = nn.BCEWithLogitsLoss()\n    optimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE, weight_decay=1e-4)\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(\n        optimizer, mode='min', factor=0.5, patience=3, verbose=False\n    )\n    \n    best_val_loss = float('inf')\n    patience_counter = 0\n    \n    # Training loop\n    for epoch in range(EPOCHS):\n        # Train\n        model.train()\n        train_loss = 0.0\n        \n        for features, labels in train_loader:\n            features, labels = features.to(device), labels.to(device)\n            \n            optimizer.zero_grad()\n            outputs = model(features)\n            loss = criterion(outputs, labels)\n            loss.backward()\n            optimizer.step()\n            \n            train_loss += loss.item()\n        \n        avg_train_loss = train_loss / len(train_loader)\n        \n        # Validation\n        model.eval()\n        val_loss = 0.0\n        \n        with torch.no_grad():\n            for features, labels in val_loader:\n                features, labels = features.to(device), labels.to(device)\n                outputs = model(features)\n                loss = criterion(outputs, labels)\n                val_loss += loss.item()\n        \n        avg_val_loss = val_loss / len(val_loader)\n        \n        # Scheduler step\n        scheduler.step(avg_val_loss)\n        \n        # Save best model\n        if avg_val_loss < best_val_loss:\n            best_val_loss = avg_val_loss\n            torch.save(model.state_dict(), save_path)\n            patience_counter = 0\n        else:\n            patience_counter += 1\n        \n        # Print progress\n        if (epoch + 1) % 5 == 0 or epoch == 0:\n            print(f\"   Epoch {epoch+1:2d}/{EPOCHS} | \"\n                  f\"Train: {avg_train_loss:.4f} | \"\n                  f\"Val: {avg_val_loss:.4f} | \"\n                  f\"Best: {best_val_loss:.4f}\")\n    \n    print(f\"\\n✓ Seed {seed} completed. Best val loss: {best_val_loss:.4f}\")\n    print(f\"✓ Model saved to: {save_path}\")\n    \n    return best_val_loss","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CELL 9: TRAIN ENSEMBLE (4 SEEDS)\n# ============================================================================\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"🚀 BƯỚC 3: TRAINING ENSEMBLE (4 SEEDS)\")\nprint(\"=\"*70)\n\n# Tạo thư mục lưu models\nos.makedirs('models', exist_ok=True)\n\nmodel_paths = []\nval_losses = []\n\nfor seed in SEEDS:\n    # Split data với seed này\n    train_ids, val_ids = train_test_split(\n        valid_ids, test_size=0.1, random_state=seed\n    )\n    \n    print(f\"\\n📊 Seed {seed}: Train={len(train_ids)}, Val={len(val_ids)}\")\n    \n    # Train\n    save_path = f'models/model_seed_{seed}.pth'\n    val_loss = train_single_seed(\n        seed, train_ids, val_ids, all_features, \n        protein_to_labels, term_to_idx, save_path\n    )\n    \n    model_paths.append(save_path)\n    val_losses.append(val_loss)\n    \n    # Free memory\n    torch.cuda.empty_cache()\n    gc.collect()\n\n# Summary\nprint(\"\\n\" + \"=\"*70)\nprint(\"📈 ENSEMBLE TRAINING SUMMARY\")\nprint(\"=\"*70)\nfor seed, loss in zip(SEEDS, val_losses):\n    print(f\"   Seed {seed}: Val Loss = {loss:.4f}\")\nprint(f\"\\n   Mean Val Loss: {np.mean(val_losses):.4f}\")\nprint(f\"   Std Val Loss:  {np.std(val_losses):.4f}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CELL 10: LOAD TEST DATA VÀ EXTRACT FEATURES\n# ============================================================================\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"📂 BƯỚC 4: LOAD VÀ PREPROCESS TEST DATA\")\nprint(\"=\"*70)\n\nprint(\"\\n1️⃣ Đang load test sequences...\")\ntest_seqs_raw = load_fasta(TEST_SEQ_PATH)\nprint(f\"   ✓ Loaded: {len(test_seqs_raw)} sequences\")\n\n# Clean test sequences\nprint(\"\\n2️⃣ Đang clean test sequences...\")\ntest_seqs = {}\nfor pid, seq in test_seqs_raw.items():\n    cleaned = clean_sequence(seq)\n    if is_valid_sequence(cleaned, min_len=10):  # Lỏng hơn cho test\n        test_seqs[pid] = cleaned\n\nprint(f\"   ✓ Valid test sequences: {len(test_seqs)}\")\n\n# Extract features\nprint(\"\\n3️⃣ Đang extract features cho test set...\")\ntest_features = {}\n\nfor i, (pid, seq) in enumerate(test_seqs.items()):\n    features = get_dipeptide_composition(seq)\n    test_features[pid] = features\n    \n    if (i + 1) % 5000 == 0:\n        print(f\"   Progress: {i+1}/{len(test_seqs)} ({(i+1)/len(test_seqs)*100:.1f}%)\")\n\nprint(f\"\\n✓ Test features ready: {len(test_features)} proteins\")\n\n# Free memory\ndel test_seqs_raw\ngc.collect()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CELL 11: ENSEMBLE PREDICTION\n# ============================================================================\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"🔮 BƯỚC 5: ENSEMBLE PREDICTION\")\nprint(\"=\"*70)\n\n# Load all models\nmodels = []\nprint(\"\\n📥 Loading models...\")\nfor i, path in enumerate(model_paths):\n    model = ResMLP(input_dim=400, num_classes=NUM_LABELS).to(device)\n    model.load_state_dict(torch.load(path))\n    model.eval()\n    models.append(model)\n    print(f\"   ✓ Model {i+1}/{len(models)} loaded from {path}\")\n\n# Predict với ensemble\nprint(\"\\n🔮 Đang dự đoán với ensemble...\")\nTEMP_SUBMISSION_FILE = 'submission_temp.tsv'\n\nwith open(TEMP_SUBMISSION_FILE, 'w') as f:\n    for count, (pid, seq) in enumerate(test_seqs.items(), 1):\n        features = test_features[pid]\n        features_tensor = torch.tensor([features], dtype=torch.float32).to(device)\n        \n        # Ensemble: Average predictions từ 4 models\n        ensemble_probs = np.zeros(NUM_LABELS, dtype=np.float32)\n        \n        with torch.no_grad():\n            for model in models:\n                logits = model(features_tensor)\n                probs = torch.sigmoid(logits).cpu().numpy()[0]\n                ensemble_probs += probs\n        \n        # Average\n        ensemble_probs /= len(models)\n        \n        # Lấy top 50 predictions\n        top_indices = np.argsort(ensemble_probs)[::-1][:50]\n        \n        for idx in top_indices:\n            score = ensemble_probs[idx]\n            if score > 0.005:\n                term_id = idx_to_term[idx]\n                f.write(f\"{pid}\\t{term_id}\\t{score:.3f}\\n\")\n        \n        if count % 2000 == 0:\n            print(f\"   Progress: {count}/{len(test_seqs)} proteins\")\n\nprint(f\"\\n✓ Predictions saved to: {TEMP_SUBMISSION_FILE}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CELL 12: POST-PROCESSING (GO HIERARCHY PROPAGATION)\n# ============================================================================\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"🌳 BƯỚC 6: POST-PROCESSING (GO Hierarchy)\")\nprint(\"=\"*70)\n\nFINAL_OUTPUT = 'submission.tsv'\n\n# Install goatools nếu cần\ntry:\n    from goatools.obo_parser import GODag\nexcept ImportError:\n    print(\"📦 Installing goatools...\")\n    import subprocess\n    import sys\n    subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"goatools\", \"-q\"])\n    from goatools.obo_parser import GODag\n\nif not os.path.exists(OBO_PATH):\n    print(\"⚠️  OBO file not found. Using raw predictions.\")\n    os.rename(TEMP_SUBMISSION_FILE, FINAL_OUTPUT)\nelse:\n    print(\"\\n1️⃣ Loading GO hierarchy...\")\n    godag = GODag(OBO_PATH)\n    print(f\"   ✓ Loaded {len(godag)} GO terms\")\n    \n    def propagate_scores(df_group, godag):\n        \"\"\"Propagate scores lên ancestor terms\"\"\"\n        current_scores = dict(zip(df_group['GO_Term'], df_group['Score']))\n        new_scores = current_scores.copy()\n        \n        for go_id, score in current_scores.items():\n            if go_id not in godag:\n                continue\n            \n            term_obj = godag[go_id]\n            ancestors = term_obj.get_all_parents()\n            \n            # Parent phải có score >= child\n            for ancestor in ancestors:\n                anc_score_old = new_scores.get(ancestor, 0.0)\n                new_scores[ancestor] = max(anc_score_old, score)\n        \n        return [[pid, term, score] for term, score in new_scores.items() if score >= 0.01]\n    \n    # Load temp submission\n    print(\"\\n2️⃣ Loading predictions...\")\n    sub_df = pd.read_csv(TEMP_SUBMISSION_FILE, sep='\\t', \n                         names=['ProteinID', 'GO_Term', 'Score'])\n    print(f\"   ✓ Loaded {len(sub_df)} predictions\")\n    \n    # Propagate\n    print(\"\\n3️⃣ Propagating scores...\")\n    final_data = []\n    \n    try:\n        from tqdm import tqdm\n        iterator = tqdm(sub_df.groupby('ProteinID'), desc=\"   Proteins\")\n    except ImportError:\n        iterator = sub_df.groupby('ProteinID')\n        print(\"   (Processing without progress bar)\")\n    \n    for pid, group in iterator:\n        refined_rows = propagate_scores(group, godag)\n        final_data.extend(refined_rows)\n    \n    # Save final submission\n    print(\"\\n4️⃣ Saving final submission...\")\n    result_df = pd.DataFrame(final_data, columns=['ProteinID', 'GO_Term', 'Score'])\n    result_df['Score'] = result_df['Score'].map(lambda x: f'{x:.3f}')\n    result_df.to_csv(FINAL_OUTPUT, sep='\\t', index=False, header=False)\n    \n    print(f\"   ✓ Total predictions: {len(result_df)}\")\n    print(f\"   ✓ Unique proteins: {result_df['ProteinID'].nunique()}\")\n    print(f\"   ✓ Unique GO terms: {result_df['GO_Term'].nunique()}\")\n\nprint(\"\\n\" + \"=\"*70)\nprint(f\"✅ HOÀN TẤT! File submission: {FINAL_OUTPUT}\")\nprint(\"=\"*70)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}