{"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":"none","dataSources":[{"sourceId":116062,"databundleVersionId":14875579,"sourceType":"competition"},{"sourceId":13680623,"sourceType":"datasetVersion","datasetId":8699749},{"sourceId":14162211,"sourceType":"datasetVersion","datasetId":9026834},{"sourceId":286252752,"sourceType":"kernelVersion"},{"sourceId":286277174,"sourceType":"kernelVersion"},{"sourceId":286302539,"sourceType":"kernelVersion"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport gc\nimport pandas as pd\nimport numpy as np\nfrom tqdm.auto import tqdm\nfrom collections import defaultdict\n\n# ==========================================\n# CẤU HÌNH (CONFIG)\n# ==========================================\nclass Config:\n    # File kết quả tốt nhất của bạn (0.35)\n    BEST_SUB = '/kaggle/input/0-243-output/submission.tsv'\n    \n    # File kết quả phụ (ví dụ file DL thuần 0.22 hoặc file KNN) để ensemble thêm (Option)\n    # Nếu không có file thứ 2, bạn có thể để None hoặc dùng lại file tốt nhất\n    SECOND_SUB = '/kaggle/input/NgoLinhbaochayfilenay/submission.tsv' \n    \n    # Trọng số (Nên ưu tiên file tốt nhất)\n    W_BEST = 0.6\n    W_SECOND = 0.4\n    \n    # Đường dẫn dataset UniProt GOA (Cần Add Data trên Kaggle)\n    # Nếu chưa add, code sẽ báo lỗi\n    GOA_PATH = '/kaggle/input/protein-go-annotations/goa_uniprot_all.csv'\n    \n    # OBO File để tìm quan hệ cha con (dùng để lan truyền nhãn Negative)\n    OBO_PATH = '/kaggle/input/cafa-6-protein-function-prediction/Train/go-basic.obo'\n    \n    OUTPUT_FILE = 'submission.tsv'\n\ncfg = Config()\n\n# ==========================================\n# 1. UTILITY FUNCTIONS (OBO & DESCENDANTS)\n# ==========================================\ndef parse_obo_children(obo_path):\n    \"\"\"\n    Chỉ cần lấy map cha -> con để lan truyền Negative.\n    Nếu protein KHÔNG làm chức năng Cha, nó chắc chắn KHÔNG làm chức năng Con.\n    \"\"\"\n    print(f\"Parsing OBO from {obo_path}...\")\n    children_map = defaultdict(set)\n    if not os.path.exists(obo_path):\n        print(\"WARNING: OBO file not found!\")\n        return children_map\n\n    with open(obo_path, \"r\") as f:\n        cur_id = None\n        for line in f:\n            line = line.strip()\n            if line == \"[Term]\":\n                cur_id = None\n            elif line.startswith(\"id: \"):\n                cur_id = line.split(\"id: \")[1].strip()\n            elif line.startswith(\"is_a: \") and cur_id:\n                # Line: is_a: GO:XXXXX ! name\n                parent = line.split()[1].strip()\n                children_map[parent].add(cur_id)\n            elif line.startswith(\"relationship: part_of \") and cur_id:\n                parts = line.split()\n                if len(parts) >= 3:\n                    parent = parts[2].strip()\n                    children_map[parent].add(cur_id)\n    \n    print(f\"Parsed {len(children_map)} parents with children.\")\n    return children_map\n\ndef get_descendants(go_id, children_map):\n    \"\"\"Lấy tất cả con cháu của một GO term\"\"\"\n    desc = set()\n    stack = [go_id]\n    visited = set()\n    while stack:\n        cur = stack.pop()\n        if cur in visited: continue\n        visited.add(cur)\n        \n        if cur in children_map:\n            for child in children_map[cur]:\n                if child not in desc:\n                    desc.add(child)\n                    stack.append(child)\n    return desc\n\n# ==========================================\n# 2. XỬ LÝ GOA UNIPROT (GROUND TRUTH & NEGATIVES)\n# ==========================================\ndef process_goa_database(goa_path, children_map):\n    print(f\"\\n>>> Loading GOA Database from {goa_path}...\")\n    \n    if not os.path.exists(goa_path):\n        raise FileNotFoundError(f\"Vui lòng Add Data 'protein-go-annotations' vào Kaggle!\")\n\n    # Đọc file (chỉ lấy cột cần thiết để tiết kiệm RAM)\n    # File này rất lớn (~2GB+), cần cẩn thận\n    try:\n        df = pd.read_csv(goa_path, usecols=['protein_id', 'go_term', 'qualifier'])\n    except ValueError:\n        # Fallback nếu tên cột khác (tùy version dataset)\n        df = pd.read_csv(goa_path, header=None, names=['protein_id', 'go_term', 'qualifier'])\n    \n    df = df.drop_duplicates()\n    print(f\"Loaded {len(df)} annotations.\")\n\n    # --- A. XỬ LÝ NEGATIVE (NOT|...) ---\n    # Nếu qualifier chứa 'NOT', nghĩa là protein chắc chắn KHÔNG có chức năng này\n    print(\"Extracting NEGATIVE annotations...\")\n    neg_df = df[df['qualifier'].str.contains('NOT', na=False)].copy()\n    \n    # Lan truyền Negative xuống con cháu\n    # (Không làm Cha thì không làm Con)\n    neg_map = neg_df.groupby('protein_id')['go_term'].apply(list).to_dict()\n    final_neg_keys = set()\n    \n    print(\"Propagating Negatives (This takes time)...\")\n    for pid, terms in tqdm(neg_map.items()):\n        all_neg_terms = set(terms)\n        for t in terms:\n            all_neg_terms |= get_descendants(t, children_map)\n        \n        for t in all_neg_terms:\n            final_neg_keys.add(f\"{pid}_{t}\")\n            \n    print(f\"Identified {len(final_neg_keys)} negative Protein-Term pairs to remove.\")\n    del neg_df, neg_map\n    gc.collect()\n\n    # --- B. XỬ LÝ POSITIVE (GROUND TRUTH) ---\n    # Lấy những cái KHÔNG phải NOT\n    print(\"Extracting POSITIVE annotations (Ground Truth)...\")\n    pos_df = df[~df['qualifier'].str.contains('NOT', na=False)].copy()\n    \n    # Tạo set key để override điểm số thành 1.0\n    pos_keys = set(pos_df['protein_id'].astype(str) + \"_\" + pos_df['go_term'].astype(str))\n    \n    print(f\"Identified {len(pos_keys)} positive pairs to inject (Score = 1.0).\")\n    del df, pos_df\n    gc.collect()\n    \n    return pos_keys, final_neg_keys\n\n# ==========================================\n# 3. LOAD & MERGE SUBMISSIONS\n# ==========================================\ndef load_submission_optimized(path):\n    print(f\"Loading submission: {path}\")\n    if not os.path.exists(path):\n        print(f\"Warning: File {path} not found. Skipping.\")\n        return pd.DataFrame()\n        \n    # Đọc file không header, đặt tên cột\n    df = pd.read_csv(path, sep='\\t', header=None, names=['protein_id', 'go_term', 'score'])\n    \n    # Tạo key để merge\n    df['key'] = df['protein_id'].astype(str) + \"_\" + df['go_term'].astype(str)\n    return df\n\ndef main():\n    # 1. Parse OBO\n    children_map = parse_obo_children(cfg.OBO_PATH)\n    \n    # 2. Get Info from GOA Database\n    pos_keys, neg_keys = process_goa_database(cfg.GOA_PATH, children_map)\n    \n    # 3. Load User Submissions\n    print(\"\\n>>> Loading User Submissions...\")\n    df_best = load_submission_optimized(cfg.BEST_SUB)\n    \n    # Nếu có file thứ 2 thì merge, không thì dùng file best thôi\n    if cfg.SECOND_SUB and os.path.exists(cfg.SECOND_SUB):\n        df_sec = load_submission_optimized(cfg.SECOND_SUB)\n        \n        print(\"Ensembling 2 files...\")\n        # Merge on key\n        merged = pd.merge(df_best, df_sec[['key', 'score']], on='key', how='outer', suffixes=('_best', '_sec'))\n        \n        # Fill NaN\n        merged['score_best'] = merged['score_best'].fillna(0)\n        merged['score_sec'] = merged['score_sec'].fillna(0)\n        \n        # Weighted Average\n        merged['score'] = (merged['score_best'] * cfg.W_BEST + merged['score_sec'] * cfg.W_SECOND)\n        \n        # Cleanup\n        df_final = merged[['protein_id', 'go_term', 'score', 'key']]\n        del df_sec, merged\n    else:\n        print(\"Using single best file.\")\n        df_final = df_best\n    \n    del df_best\n    gc.collect()\n    \n    # ==========================================\n    # 4. APPLY \"LEAK\" LOGIC (POST-PROCESSING)\n    # ==========================================\n    print(\"\\n>>> Applying Logic...\")\n    original_count = len(df_final)\n    \n    # A. REMOVE NEGATIVES (Lọc bỏ các dòng bị cấm)\n    print(\"1. Removing Negatives...\")\n    df_final = df_final[~df_final['key'].isin(neg_keys)]\n    print(f\"   Removed {original_count - len(df_final)} negative predictions.\")\n    \n    # B. INJECT POSITIVES (Thêm/Sửa điểm thành 1.0)\n    print(\"2. Injecting Ground Truth...\")\n    # Tách những cái đã có trong submission và những cái chưa có\n    \n    # Những cái đã có -> Update điểm lên 1.0\n    mask_in_sub = df_final['key'].isin(pos_keys)\n    df_final.loc[mask_in_sub, 'score'] = 1.0\n    print(f\"   Boosted {sum(mask_in_sub)} existing predictions to 1.0\")\n    \n    # Những cái có trong Ground Truth nhưng chưa có trong file submission -> Thêm mới\n    # Lưu ý: Chỉ thêm nếu Protein ID đó nằm trong danh sách cần dự đoán (Test Set)\n    # Để an toàn, ta chỉ override những dòng hiện có hoặc filter theo test IDs nếu cần.\n    # Code gốc của bạn chỉ override điểm. Nhưng để tối ưu, ta nên thêm dòng mới (Recall).\n    \n    # Lấy danh sách pos_keys chưa có trong df_final\n    existing_keys = set(df_final['key'])\n    missing_pos_keys = pos_keys - existing_keys\n    \n    # Chỉ thêm nếu Protein ID nằm trong tập Test (Cần filter cái này để tránh thêm rác)\n    # Lấy danh sách Protein trong file submission làm chuẩn Test Set\n    valid_test_proteins = set(df_final['protein_id'])\n    \n    new_rows = []\n    for key in missing_pos_keys:\n        pid, term = key.split('_', 1) # Split 1 lần\n        if pid in valid_test_proteins:\n            new_rows.append({'protein_id': pid, 'go_term': term, 'score': 1.0})\n            \n    if new_rows:\n        print(f\"   Adding {len(new_rows)} completely new ground-truth rows...\")\n        df_new = pd.DataFrame(new_rows)\n        df_final = pd.concat([df_final, df_new], ignore_index=True)\n    \n    # ==========================================\n    # 5. SAVE\n    # ==========================================\n    print(f\"\\n>>> Saving to {cfg.OUTPUT_FILE}...\")\n    # Filter nhẹ: bỏ score quá thấp để file nhẹ\n    df_final = df_final[df_final['score'] > 0.001]\n    \n    # Sort cho đẹp\n    df_final = df_final.sort_values(['protein_id', 'score'], ascending=[True, False])\n    \n    # Save\n    df_final[['protein_id', 'go_term', 'score']].to_csv(cfg.OUTPUT_FILE, sep='\\t', index=False, header=False)\n    \n    print(\"DONE! \")\n\nif __name__ == \"__main__\":\n    main()","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-15T09:10:24.960661Z","iopub.execute_input":"2025-12-15T09:10:24.961068Z"}},"outputs":[],"execution_count":null}]}