{"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":14875579,"sourceType":"competition"},{"sourceId":13665986,"sourceType":"datasetVersion","datasetId":8688768},{"sourceId":14115963,"sourceType":"datasetVersion","datasetId":8992374}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install -q transformers obonet biopython torchmetrics","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-15T08:31:03.338722Z","iopub.execute_input":"2025-12-15T08:31:03.338913Z","iopub.status.idle":"2025-12-15T08:31:24.161344Z","shell.execute_reply.started":"2025-12-15T08:31:03.338896Z","shell.execute_reply":"2025-12-15T08:31:24.160689Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# CÀI ĐẶT THƯ VIỆN TRƯỚC (NẾU CHƯA CÓ)\n# ==========================================\n!pip install -q transformers obonet biopython torchmetrics\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import KFold\nfrom tqdm.auto import tqdm\nimport os\nimport gc\nimport obonet\nimport networkx as nx\nimport random\n\n# ==========================================\n# 0. REPRODUCIBILITY (SEED EVERYTHING)\n# ==========================================\ndef seed_everything(seed=42):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n    print(f\"Locked Random Seed: {seed}\")\n\nseed_everything(42)\n\n# ==========================================\n# 1. CONFIGURATION PATHS\n# ==========================================\nclass Config:\n    RAW_DATA_DIR = '/kaggle/input/cafa-6-protein-function-prediction'\n    EMBED_DIR = '/kaggle/input/cafa-6-t5-embeddings'\n    \n    TRAIN_DIR = os.path.join(RAW_DATA_DIR, 'Train')\n    TEST_DIR = os.path.join(RAW_DATA_DIR, 'Test')\n    \n    TRAIN_TERMS = os.path.join(TRAIN_DIR, 'train_terms.tsv')\n    TRAIN_TAXONOMY = os.path.join(TRAIN_DIR, 'train_taxonomy.tsv')\n    OBO_FILE = os.path.join(TRAIN_DIR, 'go-basic.obo')\n    TEST_TAXONOMY = os.path.join(TEST_DIR, 'testsuperset-taxon-list.tsv')\n    \n    TRAIN_EMBEDS = os.path.join(EMBED_DIR, 'train_embeds.npy')\n    TRAIN_IDS = os.path.join(EMBED_DIR, 'train_ids.npy')\n    TEST_EMBEDS = os.path.join(EMBED_DIR, 'test_embeds.npy')\n    TEST_IDS = os.path.join(EMBED_DIR, 'test_ids.npy')\n    \n    NUM_CLASSES = 1500\n    BATCH_SIZE = 256\n    EPOCHS = 12\n    LR = 1e-3\n    N_FOLDS = 5\n    TOP_TAXONS_COUNT = 50\n    DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\ncfg = Config()\nprint(f\"Running on: {cfg.DEVICE}\")\n\n# ==========================================\n# 2. DATA PROCESSING & TAXONOMY\n# ==========================================\ndef clean_ids(id_list):\n    cleaned = []\n    for x in id_list:\n        if isinstance(x, bytes):\n            x = x.decode('utf-8')\n        x = str(x).strip()\n        if '|' in x:\n            parts = x.split('|')\n            if len(parts) >= 2:\n                cleaned.append(parts[1])\n            else:\n                cleaned.append(x)\n        else:\n            cleaned.append(x)\n    return np.array(cleaned)\n\ndef load_and_align_data_fixed():\n    print(\"--- 1. Loading Embeddings & Fixing IDs ---\")\n    X_train = np.load(cfg.TRAIN_EMBEDS)\n    train_ids_raw = np.load(cfg.TRAIN_IDS)\n    \n    X_test = np.load(cfg.TEST_EMBEDS)\n    test_ids_raw = np.load(cfg.TEST_IDS)\n    \n    train_ids = clean_ids(train_ids_raw)\n    test_ids = clean_ids(test_ids_raw)\n    \n    print(\"--- 2. Processing Taxonomy Features ---\")\n    train_tax_df = pd.read_csv(cfg.TRAIN_TAXONOMY, sep='\\t', dtype=str)\n    train_tax_map = dict(zip(train_tax_df.iloc[:, 0], train_tax_df.iloc[:, 1]))\n    \n    test_tax_df = pd.read_csv(cfg.TEST_TAXONOMY, sep='\\t', dtype=str)\n    test_tax_map = dict(zip(test_tax_df.iloc[:, 0], test_tax_df.iloc[:, 1]))\n    \n    top_taxons = train_tax_df.iloc[:, 1].value_counts().head(cfg.TOP_TAXONS_COUNT).index.tolist()\n    tax2idx = {t: i for i, t in enumerate(top_taxons)}\n    \n    def create_tax_features(ids, tax_map):\n        feats = np.zeros((len(ids), cfg.TOP_TAXONS_COUNT), dtype=np.float32)\n        match_count = 0\n        for i, pid in enumerate(ids):\n            if pid in tax_map:\n                match_count += 1\n                tid = tax_map[pid]\n                if tid in tax2idx:\n                    feats[i, tax2idx[tid]] = 1.0\n        print(f\"   > Found taxonomy for {match_count}/{len(ids)} proteins\")\n        return feats\n\n    print(\"Generating Taxon Vectors...\")\n    train_tax_feats = create_tax_features(train_ids, train_tax_map)\n    test_tax_feats = create_tax_features(test_ids, test_tax_map)\n    \n    X_train_final = np.concatenate([X_train, train_tax_feats], axis=1)\n    X_test_final = np.concatenate([X_test, test_tax_feats], axis=1)\n    \n    return X_train_final, train_ids, X_test_final, test_ids\n\n# ==========================================\n# 3. MODEL: RESNET\n# ==========================================\nclass ResidualBlock(nn.Module):\n    def __init__(self, dim, dropout=0.3):\n        super().__init__()\n        self.net = nn.Sequential(\n            nn.Linear(dim, dim),\n            nn.BatchNorm1d(dim),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(dim, dim),\n            nn.BatchNorm1d(dim)\n        )\n        self.relu = nn.ReLU()\n    def forward(self, x):\n        return self.relu(x + self.net(x))\n\nclass AdvancedModel(nn.Module):\n    def __init__(self, input_dim, num_classes):\n        super().__init__()\n        self.entry = nn.Sequential(\n            nn.Linear(input_dim, 1024),\n            nn.BatchNorm1d(1024),\n            nn.ReLU(),\n            nn.Dropout(0.2)\n        )\n        self.blocks = nn.Sequential(\n            ResidualBlock(1024),\n            ResidualBlock(1024),\n            ResidualBlock(1024)\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)\n\n# ==========================================\n# 4. TRAINING UTILS\n# ==========================================\nclass ProteinDataset(Dataset):\n    def __init__(self, X, Y=None):\n        self.X = torch.from_numpy(X).float()\n        self.Y = torch.from_numpy(Y).float() if Y is not None else None\n    def __len__(self): return len(self.X)\n    def __getitem__(self, i): \n        return (self.X[i], self.Y[i]) if self.Y is not None else self.X[i]\n\ndef train_one_fold(fold, model, train_loader, val_loader):\n    criterion = nn.BCEWithLogitsLoss()\n    optimizer = optim.AdamW(model.parameters(), lr=cfg.LR, weight_decay=1e-4)\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=1)\n    \n    best_loss = float('inf')\n    early_stop_count = 0\n    patience = 3 \n    \n    for epoch in range(cfg.EPOCHS):\n        model.train()\n        t_loss = 0\n        for x, y in train_loader:\n            x, y = x.to(cfg.DEVICE), y.to(cfg.DEVICE)\n            optimizer.zero_grad()\n            logits = model(x)\n            loss = criterion(logits, y)\n            loss.backward()\n            optimizer.step()\n            t_loss += loss.item()\n            \n        model.eval()\n        v_loss = 0\n        with torch.no_grad():\n            for x, y in val_loader:\n                x, y = x.to(cfg.DEVICE), y.to(cfg.DEVICE)\n                v_loss += criterion(model(x), y).item()\n        \n        avg_v_loss = v_loss / len(val_loader)\n        scheduler.step(avg_v_loss)\n        \n        if avg_v_loss < best_loss:\n            best_loss = avg_v_loss\n            torch.save(model.state_dict(), f'model_fold_{fold}.pth')\n            early_stop_count = 0\n        else:\n            early_stop_count += 1\n            \n        if early_stop_count >= patience:\n            print(f\"  Fold {fold}: Early stopping at epoch {epoch}\")\n            break\n    \n    return best_loss\n\n# ==========================================\n# 5. MAIN PIPELINE\n# ==========================================\nX_train_full, train_ids_full, X_test_full, test_ids_full = load_and_align_data_fixed()\n\nprint(\"--- 3. Processing Labels ---\")\ndf = pd.read_csv(cfg.TRAIN_TERMS, sep='\\t', names=['EntryID', 'term', 'aspect'])\ntop_terms = df['term'].value_counts().head(cfg.NUM_CLASSES).index.tolist()\nterm2idx = {t: i for i, t in enumerate(top_terms)}\nid_to_terms = df[df['term'].isin(top_terms)].groupby('EntryID')['term'].apply(set).to_dict()\n\nY_full = np.zeros((len(train_ids_full), cfg.NUM_CLASSES), dtype=np.float32)\nvalid_mask = []\nfor i, pid in enumerate(train_ids_full):\n    if pid in id_to_terms:\n        valid_mask.append(True)\n        for term in id_to_terms[pid]:\n            Y_full[i, term2idx[term]] = 1.0\n    else:\n        valid_mask.append(False)\n\nvalid_mask = np.array(valid_mask)\nX_train_clean = X_train_full[valid_mask]\nY_clean = Y_full[valid_mask]\n\n# Training\nkf = KFold(n_splits=cfg.N_FOLDS, shuffle=True, random_state=42)\ninput_dim = X_train_clean.shape[1]\n\nprint(f\"\\n--- Starting Ensemble Training ({cfg.N_FOLDS} Folds) ---\")\nfor fold, (train_idx, val_idx) in enumerate(kf.split(X_train_clean)):\n    print(f\">> Fold {fold+1}/{cfg.N_FOLDS}\")\n    train_ds = ProteinDataset(X_train_clean[train_idx], Y_clean[train_idx])\n    val_ds = ProteinDataset(X_train_clean[val_idx], Y_clean[val_idx])\n    train_loader = DataLoader(train_ds, batch_size=cfg.BATCH_SIZE, shuffle=True)\n    val_loader = DataLoader(val_ds, batch_size=cfg.BATCH_SIZE)\n    \n    model = AdvancedModel(input_dim, cfg.NUM_CLASSES).to(cfg.DEVICE)\n    train_one_fold(fold, model, train_loader, val_loader)\n    \n    del model, train_loader, val_loader, train_ds, val_ds\n    torch.cuda.empty_cache()\n    gc.collect()\n\n# Inference\nprint(\"\\n--- Running Inference on Test Set ---\")\ntest_loader = DataLoader(ProteinDataset(X_test_full), batch_size=cfg.BATCH_SIZE)\nfinal_preds = np.zeros((len(test_ids_full), cfg.NUM_CLASSES), dtype=np.float32)\n\nfor fold in range(cfg.N_FOLDS):\n    model = AdvancedModel(input_dim, cfg.NUM_CLASSES).to(cfg.DEVICE)\n    model.load_state_dict(torch.load(f'model_fold_{fold}.pth'))\n    model.eval()\n    \n    fold_preds = []\n    with torch.no_grad():\n        for x in tqdm(test_loader, desc=f\"Predicting Fold {fold+1}\"):\n            x = x.to(cfg.DEVICE)\n            logits = model(x)\n            fold_preds.append(torch.sigmoid(logits).cpu().numpy())\n            \n    final_preds += np.vstack(fold_preds)\n\nfinal_preds /= cfg.N_FOLDS\n\n# GO Propagation\nprint(\"--- Applying GO Structure Propagation ---\")\ngo_graph = obonet.read_obo(cfg.OBO_FILE)\nterm_to_idx = {t: i for i, t in enumerate(top_terms)}\n\nparent_map = {}\nvalid_set = set(top_terms)\nfor term in top_terms:\n    if term in go_graph:\n        parents = list(go_graph.successors(term))\n        for p in parents:\n            if p in valid_set:\n                if p not in parent_map: parent_map[p] = []\n                parent_map[p].append(term)\n\nfor _ in range(2):\n    for parent, children in parent_map.items():\n        if children:\n            p_idx = term_to_idx[parent]\n            c_indices = [term_to_idx[c] for c in children]\n            max_child_score = final_preds[:, c_indices].max(axis=1)\n            final_preds[:, p_idx] = np.maximum(final_preds[:, p_idx], max_child_score)\n\n# E. SUBMISSION FILE (FIXED NAME FOR NEXT STEP)\nprint(\"--- Creating Submission File ---\")\nsubmission_rows = []\nthreshold = 0.005 \n\nfor i, pid in enumerate(tqdm(test_ids_full)):\n    idxs = np.where(final_preds[i] > threshold)[0]\n    for idx in idxs:\n        term = top_terms[idx]\n        score = final_preds[i, idx]\n        submission_rows.append(f\"{pid}\\t{term}\\t{score:.3f}\")\n\n# QUAN TRỌNG: Lưu file tên là submission_dl.tsv để Cell tiếp theo (Ensemble) đọc được\nwith open('submission_dl.tsv', 'w') as f:\n    f.write('\\n'.join(submission_rows))\n\nprint(\"DONE! Intermediate file submission_dl.tsv is ready. PLEASE RUN THE NEXT CELL TO GET submission.tsv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T08:31:24.163071Z","iopub.execute_input":"2025-12-15T08:31:24.163281Z","iopub.status.idle":"2025-12-15T08:31:28.650752Z","shell.execute_reply.started":"2025-12-15T08:31:24.163261Z","shell.execute_reply":"2025-12-15T08:31:28.649794Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport torch\nfrom Bio import SeqIO\nfrom tqdm.auto import tqdm\nimport gc\nimport os\n\n# ==========================================\n# CẤU HÌNH CHIẾN THẮNG & CHỐNG TIMEOUT\n# ==========================================\nclass Config:\n    RAW_DATA_DIR = '/kaggle/input/cafa-6-protein-function-prediction'\n    EMBED_DIR = '/kaggle/input/cafa-6-t5-embeddings'\n    \n    DL_SUBMISSION = 'submission_dl.tsv' \n    \n    TRAIN_SEQ = os.path.join(RAW_DATA_DIR, 'Train', 'train_sequences.fasta')\n    TRAIN_TERMS = os.path.join(RAW_DATA_DIR, 'Train', 'train_terms.tsv')\n    TEST_SEQ = os.path.join(RAW_DATA_DIR, 'Test', 'testsuperset.fasta')\n    \n    TRAIN_EMBEDS = os.path.join(EMBED_DIR, 'train_embeds.npy')\n    TEST_EMBEDS = os.path.join(EMBED_DIR, 'test_embeds.npy')\n    \n    # Trọng số Ensemble\n    W_DL = 0.4\n    W_KNN = 0.6\n    \n    # --- CẤU HÌNH CHỐNG TIMEOUT ---\n    ROUND_DECIMALS = 3   # Làm tròn 2 số thập phân -> Giảm số lượng ngưỡng tính toán\n    TOP_K = 65           # Chỉ lấy 65 nhãn tốt nhất mỗi protein (Thay vì 150)\n    MIN_SCORE = 0.01     # Bỏ qua các điểm quá thấp\n    \n    DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\ncfg = Config()\n\n# ==========================================\n# MODULE 1: EXACT MATCH (ĐÃ SỬA LỖI HEADER)\n# ==========================================\ndef run_exact_match():\n    print(\">>> 1. Running Exact Sequence Matching (Lookup)...\")\n    \n    # 1. Load Train Sequences\n    print(\"   Indexing Train Sequences...\")\n    train_seq_map = {}\n    for record in SeqIO.parse(cfg.TRAIN_SEQ, \"fasta\"):\n        seq_str = str(record.seq)\n        \n        # Trích xuất ID an toàn\n        if '|' in record.id:\n            pid = record.id.split('|')[1]\n        else:\n            pid = record.id\n            \n        if seq_str not in train_seq_map:\n            train_seq_map[seq_str] = []\n        train_seq_map[seq_str].append(pid)\n        \n    # 2. Load Train Terms\n    print(\"   Loading Train Terms...\")\n    df_terms = pd.read_csv(cfg.TRAIN_TERMS, sep='\\t', names=['id', 'term', 'aspect'])\n    df_terms['id'] = df_terms['id'].astype(str).str.strip()\n    \n    top_terms = set(df_terms['term'].value_counts().head(5000).index)\n    df_terms = df_terms[df_terms['term'].isin(top_terms)]\n    \n    id_to_terms = df_terms.groupby('id')['term'].apply(set).to_dict()\n    \n    # 3. Match Test Sequences\n    print(\"   Matching Test Sequences...\")\n    exact_matches = []\n    match_count = 0\n    \n    for record in tqdm(SeqIO.parse(cfg.TEST_SEQ, \"fasta\"), desc=\"Exact Matching\"):\n        test_seq = str(record.seq)\n        \n        # Xử lý header an toàn\n        if '|' in record.id:\n            test_id = record.id.split('|')[1]\n        else:\n            test_id = record.id\n        \n        if test_seq in train_seq_map:\n            train_ids = train_seq_map[test_seq]\n            \n            collected_terms = set()\n            for tid in train_ids:\n                if tid in id_to_terms:\n                    collected_terms.update(id_to_terms[tid])\n            \n            if collected_terms:\n                match_count += 1\n                for term in collected_terms:\n                    # Gán 0.99 để không bị round xuống thấp quá, nhưng vẫn dưới 1.0\n                    exact_matches.append({'id': test_id, 'term': term, 'exact_score': 0.99})\n                \n    print(f\"   >>> FOUND {match_count} EXACT MATCHES!\")\n    return pd.DataFrame(exact_matches)\n\n# ==========================================\n# MODULE 2: WEIGHTED KNN\n# ==========================================\ndef run_knn():\n    print(\"\\n>>> 2. Running Weighted KNN...\")\n    X_train = np.load(cfg.TRAIN_EMBEDS)\n    X_test = np.load(cfg.TEST_EMBEDS)\n    \n    X_train = torch.from_numpy(X_train).to(cfg.DEVICE)\n    X_train = torch.nn.functional.normalize(X_train, p=2, dim=1)\n    \n    X_test = torch.from_numpy(X_test)\n    X_test = torch.nn.functional.normalize(X_test, p=2, dim=1)\n    \n    df = pd.read_csv(cfg.TRAIN_TERMS, sep='\\t', names=['id', 'term', 'aspect'])\n    train_ids_npy = np.load(os.path.join(cfg.EMBED_DIR, 'train_ids.npy'))\n    train_ids_npy = [str(x).split('|')[1] if '|' in str(x) else str(x) for x in train_ids_npy]\n    \n    id_to_terms = df.groupby('id')['term'].apply(list).to_dict()\n    idx_to_terms = {}\n    for i, pid in enumerate(train_ids_npy):\n        if pid in id_to_terms:\n            idx_to_terms[i] = id_to_terms[pid]\n            \n    test_ids_npy = np.load(os.path.join(cfg.EMBED_DIR, 'test_ids.npy'))\n    test_ids_npy = [str(x).split('|')[1] if '|' in str(x) else str(x) for x in test_ids_npy]\n    \n    knn_preds = []\n    K = 10\n    BATCH_SIZE = 500\n    \n    num_test = len(test_ids_npy)\n    for i in tqdm(range(0, num_test, BATCH_SIZE), desc=\"KNN Computing\"):\n        batch_test = X_test[i : i + BATCH_SIZE].to(cfg.DEVICE)\n        sim_matrix = torch.mm(batch_test, X_train.t())\n        vals, inds = torch.topk(sim_matrix, k=K, dim=1)\n        vals = vals.cpu().numpy()\n        inds = inds.cpu().numpy()\n        \n        for j in range(len(batch_test)):\n            tid = test_ids_npy[i + j]\n            scores = {}\n            total_weight = 0\n            \n            for k in range(K):\n                idx = inds[j, k]\n                sim = vals[j, k]\n                \n                if sim > 0.3: \n                    weight = sim * sim\n                    total_weight += weight\n                    if idx in idx_to_terms:\n                        for term in idx_to_terms[idx]:\n                            scores[term] = scores.get(term, 0) + weight\n                            \n            if total_weight > 0:\n                for term in scores:\n                    scores[term] /= total_weight\n                    scores[term] *= min(1.0, total_weight) \n            \n            for term, score in scores.items():\n                if score > 0.01:\n                    knn_preds.append({'id': tid, 'term': term, 'knn_score': score})\n                    \n    del X_train, X_test\n    torch.cuda.empty_cache()\n    gc.collect()\n    return pd.DataFrame(knn_preds)\n\n# ==========================================\n# MODULE 3: MERGE & OPTIMIZE (ANTI-TIMEOUT)\n# ==========================================\ndef main():\n    # 1. Exact Match\n    df_exact = run_exact_match()\n    \n    # 2. KNN\n    df_knn = run_knn()\n    \n    # 3. Load DL\n    print(\"\\n>>> 3. Loading DL Submission...\")\n    try:\n        df_dl = pd.read_csv(cfg.DL_SUBMISSION, sep='\\t', header=None, names=['id', 'term', 'dl_score'])\n    except:\n        print(\"Error loading DL submission. Make sure file exists!\")\n        return\n\n    # 4. Merge\n    print(\">>> 4. Merging Strategies...\")\n    print(\"   Merging DL + KNN...\")\n    merged = pd.merge(df_dl, df_knn, on=['id', 'term'], how='outer')\n    merged['dl_score'] = merged['dl_score'].fillna(0)\n    merged['knn_score'] = merged['knn_score'].fillna(0)\n    \n    merged['ensemble_score'] = (cfg.W_DL * merged['dl_score']) + (cfg.W_KNN * merged['knn_score'])\n    \n    print(\"   Overriding with Exact Matches...\")\n    if not df_exact.empty:\n        merged = pd.merge(merged, df_exact, on=['id', 'term'], how='outer')\n        merged['exact_score'] = merged['exact_score'].fillna(0)\n        merged['final_score'] = merged[['ensemble_score', 'exact_score']].max(axis=1)\n    else:\n        merged['final_score'] = merged['ensemble_score']\n        \n    # ==========================================================\n    # 5. TỐI ƯU HÓA CUỐI CÙNG ĐỂ CHỐNG TIMEOUT SCORING\n    # ==========================================================\n    print(\">>> 5. Optimizing for Anti-Timeout...\")\n    \n    # Bước A: Lọc điểm thấp\n    print(f\"   Filtering scores < {cfg.MIN_SCORE}...\")\n    final_df = merged[merged['final_score'] > cfg.MIN_SCORE]\n    \n    # Bước B: Làm tròn điểm (Quantization)\n    # Đây là bước quan trọng nhất để giảm thời gian chấm\n    print(f\"   Rounding scores to {cfg.ROUND_DECIMALS} decimals...\")\n    final_df['final_score'] = final_df['final_score'].round(cfg.ROUND_DECIMALS)\n    \n    # Bước C: Cắt tỉa (Pruning) lấy Top K\n    print(f\"   Limiting to Top {cfg.TOP_K} terms per protein...\")\n    # Sort: ID tăng, Điểm giảm\n    final_df = final_df.sort_values(['id', 'final_score'], ascending=[True, False])\n    # Chỉ giữ lại Top K dòng cho mỗi ID\n    final_df = final_df.groupby('id').head(cfg.TOP_K)\n    \n    # Save\n    print(\">>> Saving submission.tsv...\")\n    final_df[['id', 'term', 'final_score']].to_csv('submission.tsv', sep='\\t', index=False, header=False)\n    \n    print(\"SUCCESS! File: submission.tsv\")\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T08:31:28.651129Z","iopub.status.idle":"2025-12-15T08:31:28.651333Z","shell.execute_reply.started":"2025-12-15T08:31:28.651233Z","shell.execute_reply":"2025-12-15T08:31:28.651242Z"}},"outputs":[],"execution_count":null}]}