{"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":14084779,"sourceType":"competition"},{"sourceId":282364487,"sourceType":"kernelVersion"},{"sourceId":282897090,"sourceType":"kernelVersion"},{"sourceId":283250975,"sourceType":"kernelVersion"},{"sourceId":283964380,"sourceType":"kernelVersion"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-03T03:57:27.114956Z","iopub.execute_input":"2025-12-03T03:57:27.115231Z","iopub.status.idle":"2025-12-03T03:57:27.394180Z","shell.execute_reply.started":"2025-12-03T03:57:27.115208Z","shell.execute_reply":"2025-12-03T03:57:27.393532Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport os\nimport time\nfrom tqdm.auto import tqdm\nfrom datetime import datetime\nimport gc\n\n# ==================== CONFIGURATION ====================\nprint(\"=\"*60)\nprint(\"MULTI-MODEL ENSEMBLE FOR PROTEIN FUNCTION PREDICTION\")\nprint(\"=\"*60)\n\n# ============ TUNING SECTION - EXPERIMENT WITH THESE ============\n\n# Strategy 1: Model weights (MUST SUM TO 1.0)\nWEIGHT_MODEL_A = 0.10  \nWEIGHT_MODEL_B = 0.05  \nWEIGHT_MODEL_C = 0.85 \n\n# Validate weights\nassert abs(WEIGHT_MODEL_A + WEIGHT_MODEL_B + WEIGHT_MODEL_C - 1.0) < 0.001, \"Weights must sum to 1.0!\"\n\n# Strategy 2: Diversity bonus \nDIVERSITY_BONUS_MODE = 'adaptive'  # 'fixed', 'adaptive', 'multiplicative', or 'none'\nDIVERSITY_BONUS_FIXED = 0.05\nDIVERSITY_BONUS_MIN = 0.02\nDIVERSITY_BONUS_MAX = 0.08  \nMIN_SCORE_FOR_BONUS = 0.01  # Match với CONFIDENCE_THRESHOLD để không bỏ sót\n\n# Strategy 3: Score calibration\nAPPLY_SCORE_CALIBRATION = False\nCALIBRATION_POWER_A = 1.0\nCALIBRATION_POWER_B = 1.0\nCALIBRATION_POWER_C = 1.0\n\n# Strategy 4: Filtering thresholds\nCONFIDENCE_THRESHOLD = 0.01  # Giữ thấp cho Fmax\nTOP_K_PER_PROTEIN = 1500\n\n# Strategy 5: Conflict resolution\nCONFLICT_RESOLUTION = 'weighted'  # 'weighted', 'max', 'min'\n\n# ================================================================\n\n# Memory optimization (KHÔNG THAY ĐỔI)\nBATCH_SIZE = 1_000_000\nEARLY_FILTER = True  # BẬT early filter để giảm RAM\nMIN_SCORE_TO_KEEP = 0.01  # Lọc sớm predictions quá thấp\nSCORE_CLIP_MIN = 0.0\nSCORE_CLIP_MAX = 1.0\n\nprint(f\"\\n⚙️ Configuration:\")\nprint(f\"  - Model A weight: {WEIGHT_MODEL_A:.2f}\")\nprint(f\"  - Model B weight: {WEIGHT_MODEL_B:.2f}\")\nprint(f\"  - Model C weight: {WEIGHT_MODEL_C:.2f}\")\nprint(f\"  - Diversity bonus: {DIVERSITY_BONUS_MODE}\")\nprint(f\"  - Min score for bonus: {MIN_SCORE_FOR_BONUS}\")\nprint(f\"  - Score calibration: {APPLY_SCORE_CALIBRATION}\")\nprint(f\"  - Confidence threshold: {CONFIDENCE_THRESHOLD}\")\nprint(f\"  - Top-K per protein: {TOP_K_PER_PROTEIN}\")\n\n# ==================== HELPER FUNCTIONS ====================\n\ndef load_model_with_validation(filepath, model_name, calibration_power=1.0):\n    \"\"\"Load và validate model predictions với memory optimization\"\"\"\n    start_time = time.time()\n    print(f\"\\n📂 Loading {model_name}...\")\n    \n    chunks = []\n    chunk_size = 5_000_000\n    \n    for chunk in pd.read_csv(filepath, sep='\\t', header=None, \n                              names=['protein', 'go_term', 'score'],\n                              chunksize=chunk_size):\n        # Clip scores\n        chunk['score'] = chunk['score'].clip(SCORE_CLIP_MIN, SCORE_CLIP_MAX)\n        \n        # Score calibration\n        if APPLY_SCORE_CALIBRATION and calibration_power != 1.0:\n            chunk['score'] = np.power(chunk['score'], calibration_power)\n        \n        # Filter early nếu bật\n        if EARLY_FILTER:\n            chunk = chunk[chunk['score'] >= MIN_SCORE_TO_KEEP]\n        \n        chunk['key'] = chunk['protein'] + '|' + chunk['go_term']\n        chunks.append(chunk)\n    \n    df = pd.concat(chunks, ignore_index=True)\n    del chunks\n    gc.collect()\n    \n    print(f\"  ✓ Loaded {len(df):,} predictions\")\n    print(f\"  ✓ Unique proteins: {df['protein'].nunique():,}\")\n    print(f\"  ✓ Unique GO terms: {df['go_term'].nunique():,}\")\n    print(f\"  ✓ Score range: [{df['score'].min():.4f}, {df['score'].max():.4f}]\")\n    print(f\"  ✓ Mean score: {df['score'].mean():.4f}\")\n    \n    # Remove duplicates\n    duplicates = df['key'].duplicated().sum()\n    if duplicates > 0:\n        print(f\"  ⚠️ Removing {duplicates:,} duplicates...\")\n        df = df.sort_values('score', ascending=False).drop_duplicates('key', keep='first')\n        gc.collect()\n    \n    elapsed = time.time() - start_time\n    print(f\"  ⏱️ Loading time: {elapsed:.2f}s\")\n    print(f\"  💾 Memory usage: ~{df.memory_usage(deep=True).sum() / 1e9:.2f} GB\")\n    \n    return df\n\ndef calculate_adaptive_bonus(scores_list):\n    \"\"\"Bonus cao khi tất cả model TỰ TIN và ĐỒNG Ý (mở rộng cho 3 models)\"\"\"\n    scores_array = np.array(scores_list).T  # shape: (n_predictions, n_models)\n    \n    # Tính agreement: 1 - std của scores\n    agreement = 1 - np.std(scores_array, axis=1) / (np.mean(scores_array, axis=1) + 1e-8)\n    agreement = np.clip(agreement, 0, 1)\n    \n    # Tính average confidence\n    avg_confidence = np.mean(scores_array, axis=1)\n    \n    # Penalty nếu có model nào quá thấp\n    min_score = np.min(scores_array, axis=1)\n    confidence_factor = min_score / MIN_SCORE_FOR_BONUS if MIN_SCORE_FOR_BONUS > 0 else 1.0\n    confidence_factor = np.clip(confidence_factor, 0, 1)\n    \n    bonus = (DIVERSITY_BONUS_MIN + \n             (DIVERSITY_BONUS_MAX - DIVERSITY_BONUS_MIN) * agreement * avg_confidence)\n    \n    return bonus * confidence_factor\n\ndef calculate_multiplicative_bonus(scores_list, weights):\n    \"\"\"Multiplicative ensemble - tốt khi tất cả model tự tin (mở rộng cho 3 models)\"\"\"\n    scores_array = np.array(scores_list).T  # shape: (n_predictions, n_models)\n    \n    # Geometric mean (nth root of product)\n    geometric_mean = np.prod(scores_array, axis=1) ** (1.0 / len(scores_list))\n    \n    # Weighted average\n    weighted_avg = np.dot(scores_array, weights)\n    \n    # Blend giữa weighted average và geometric mean\n    alpha = 0.2  # 20% geometric, 80% weighted\n    return alpha * geometric_mean + (1 - alpha) * weighted_avg\n\n# ==================== MAIN PIPELINE ====================\n\noverall_start = time.time()\n\n# Load all models\nmodel_a = load_model_with_validation(\n    '/kaggle/input/prediction-with-goa-uniprot-all/submission.tsv',\n    'Model A (GOA-UniProt)',\n    calibration_power=CALIBRATION_POWER_A\n)\n\nmodel_b = load_model_with_validation(\n    '/kaggle/input/cafa-6-protein-function-starter-eda-model/submission.tsv',\n    'Model B (Starter EDA)',\n    calibration_power=CALIBRATION_POWER_B\n)\n\nmodel_c = load_model_with_validation(\n    '/kaggle/input/cafa-quick-submission-0-358-score/submission.tsv',\n    'Model C (Custom Model)',\n    calibration_power=CALIBRATION_POWER_C\n)\n\n# Analyze overlap\nprint(f\"\\n🔨 Analyzing overlap...\")\nkeys_a = set(model_a['key'])\nkeys_b = set(model_b['key'])\nkeys_c = set(model_c['key'])\n\nall_keys = keys_a | keys_b | keys_c\noverlap_ab = keys_a & keys_b\noverlap_ac = keys_a & keys_c\noverlap_bc = keys_b & keys_c\noverlap_abc = keys_a & keys_b & keys_c\n\nonly_a = len(keys_a - keys_b - keys_c)\nonly_b = len(keys_b - keys_a - keys_c)\nonly_c = len(keys_c - keys_a - keys_b)\n\nprint(f\"  ✓ Total unique predictions: {len(all_keys):,}\")\nprint(f\"  ✓ Only in Model A: {only_a:,} ({only_a/len(all_keys)*100:.1f}%)\")\nprint(f\"  ✓ Only in Model B: {only_b:,} ({only_b/len(all_keys)*100:.1f}%)\")\nprint(f\"  ✓ Only in Model C: {only_c:,} ({only_c/len(all_keys)*100:.1f}%)\")\nprint(f\"  ✓ Overlap A∩B: {len(overlap_ab):,} ({len(overlap_ab)/len(all_keys)*100:.1f}%)\")\nprint(f\"  ✓ Overlap A∩C: {len(overlap_ac):,} ({len(overlap_ac)/len(all_keys)*100:.1f}%)\")\nprint(f\"  ✓ Overlap B∩C: {len(overlap_bc):,} ({len(overlap_bc)/len(all_keys)*100:.1f}%)\")\nprint(f\"  ✓ Overlap A∩B∩C: {len(overlap_abc):,} ({len(overlap_abc)/len(all_keys)*100:.1f}%)\")\n\n# Calculate overlap score statistics\nif len(overlap_abc) > 0:\n    overlap_a_scores = model_a[model_a['key'].isin(overlap_abc)]['score']\n    overlap_b_scores = model_b[model_b['key'].isin(overlap_abc)]['score']\n    overlap_c_scores = model_c[model_c['key'].isin(overlap_abc)]['score']\n    print(f\"  ✓ Overlap (A∩B∩C) avg score A: {overlap_a_scores.mean():.4f}\")\n    print(f\"  ✓ Overlap (A∩B∩C) avg score B: {overlap_b_scores.mean():.4f}\")\n    print(f\"  ✓ Overlap (A∩B∩C) avg score C: {overlap_c_scores.mean():.4f}\")\n\ndel keys_a, keys_b, keys_c, all_keys, overlap_ab, overlap_ac, overlap_bc, overlap_abc\ngc.collect()\n\n# Merge strategy\nprint(f\"\\n🚀 Creating ensemble (memory-efficient method)...\")\nstart_time = time.time()\n\nmodel_a_clean = model_a[['key', 'protein', 'go_term', 'score']].rename(columns={'score': 'score_a'})\nmodel_b_clean = model_b[['key', 'score']].rename(columns={'score': 'score_b'})\nmodel_c_clean = model_c[['key', 'score']].rename(columns={'score': 'score_c'})\n\ndel model_a, model_b, model_c\ngc.collect()\n\nprint(f\"  📊 Merging models...\")\n# Merge A and B first\nensemble = model_a_clean.merge(model_b_clean, on='key', how='outer')\ndel model_b_clean\ngc.collect()\n\n# Then merge with C\nensemble = ensemble.merge(model_c_clean, on='key', how='outer')\ndel model_a_clean, model_c_clean\ngc.collect()\n\n# Fill NaN\nensemble['score_a'] = ensemble['score_a'].fillna(0)\nensemble['score_b'] = ensemble['score_b'].fillna(0)\nensemble['score_c'] = ensemble['score_c'].fillna(0)\n\n# Free memory immediately\ngc.collect()\n\nprint(f\"  ✓ Merged: {len(ensemble):,} total predictions\")\nprint(f\"  💾 Memory usage: ~{ensemble.memory_usage(deep=True).sum() / 1e9:.2f} GB\")\n\n# FILTER EARLY để giảm data trước khi tính toán phức tạp\nprint(f\"  🔍 Early filtering to reduce memory...\")\ninitial_count = len(ensemble)\n\n# Chỉ giữ predictions có ít nhất 1 model score > threshold\nmask_keep = (ensemble['score_a'] >= MIN_SCORE_TO_KEEP) | \\\n            (ensemble['score_b'] >= MIN_SCORE_TO_KEEP) | \\\n            (ensemble['score_c'] >= MIN_SCORE_TO_KEEP)\n\nensemble = ensemble[mask_keep].copy()\ndel mask_keep\ngc.collect()\n\nfiltered_count = len(ensemble)\nprint(f\"    ✓ Kept {filtered_count:,}/{initial_count:,} predictions ({filtered_count/initial_count*100:.1f}%)\")\nprint(f\"    ✓ Freed ~{(initial_count - filtered_count) * 200 / 1e9:.2f} GB\")\nprint(f\"    💾 Current memory: ~{ensemble.memory_usage(deep=True).sum() / 1e9:.2f} GB\")\n\n# Calculate base ensemble score\nprint(f\"  🧮 Calculating ensemble scores (mode: {DIVERSITY_BONUS_MODE})...\")\n\nif DIVERSITY_BONUS_MODE == 'multiplicative':\n    # Multiplicative ensemble\n    mask_all = (ensemble['score_a'] > 0) & (ensemble['score_b'] > 0) & (ensemble['score_c'] > 0)\n    \n    # Base weighted average\n    ensemble['score'] = (ensemble['score_a'] * WEIGHT_MODEL_A + \n                        ensemble['score_b'] * WEIGHT_MODEL_B + \n                        ensemble['score_c'] * WEIGHT_MODEL_C)\n    \n    if mask_all.sum() > 0:\n        weights = np.array([WEIGHT_MODEL_A, WEIGHT_MODEL_B, WEIGHT_MODEL_C])\n        scores_list = [\n            ensemble.loc[mask_all, 'score_a'].values,\n            ensemble.loc[mask_all, 'score_b'].values,\n            ensemble.loc[mask_all, 'score_c'].values\n        ]\n        ensemble.loc[mask_all, 'score'] = calculate_multiplicative_bonus(scores_list, weights)\n        print(f\"    ✓ Multiplicative bonus applied to {mask_all.sum():,} predictions\")\n    \n    ensemble['bonus'] = 0.0\n    \nelse:\n    # Weighted average base\n    ensemble['weighted_score'] = (\n        ensemble['score_a'] * WEIGHT_MODEL_A + \n        ensemble['score_b'] * WEIGHT_MODEL_B +\n        ensemble['score_c'] * WEIGHT_MODEL_C\n    )\n    \n    # Apply diversity bonus\n    print(f\"  🎁 Applying diversity bonus...\")\n    if DIVERSITY_BONUS_MODE == 'adaptive':\n        # Apply bonus cho tất cả predictions có cả 3 models predict (không cần threshold cao)\n        mask_all = ((ensemble['score_a'] >= MIN_SCORE_FOR_BONUS) & \n                    (ensemble['score_b'] >= MIN_SCORE_FOR_BONUS) & \n                    (ensemble['score_c'] >= MIN_SCORE_FOR_BONUS))\n        \n        ensemble['bonus'] = 0.0\n        n_bonus = mask_all.sum()\n        \n        if n_bonus > 0:\n            print(f\"    ℹ️ Computing bonus for {n_bonus:,} predictions...\")\n            \n            # Xử lý theo BATCH để tránh tràn RAM\n            batch_size = 5_000_000\n            if n_bonus > batch_size:\n                print(f\"    ℹ️ Processing in batches of {batch_size:,}...\")\n                indices = ensemble.index[mask_all].tolist()\n                \n                for i in range(0, len(indices), batch_size):\n                    batch_idx = indices[i:i+batch_size]\n                    scores_list = [\n                        ensemble.loc[batch_idx, 'score_a'].values,\n                        ensemble.loc[batch_idx, 'score_b'].values,\n                        ensemble.loc[batch_idx, 'score_c'].values\n                    ]\n                    ensemble.loc[batch_idx, 'bonus'] = calculate_adaptive_bonus(scores_list)\n                    \n                    if i % (batch_size * 5) == 0:\n                        print(f\"      Processed {i:,}/{n_bonus:,} predictions...\")\n                    \n                    del scores_list, batch_idx\n                    gc.collect()\n            else:\n                scores_list = [\n                    ensemble.loc[mask_all, 'score_a'].values,\n                    ensemble.loc[mask_all, 'score_b'].values,\n                    ensemble.loc[mask_all, 'score_c'].values\n                ]\n                ensemble.loc[mask_all, 'bonus'] = calculate_adaptive_bonus(scores_list)\n                del scores_list\n                gc.collect()\n            \n            print(f\"    ✓ Bonus applied to {n_bonus:,} predictions ({n_bonus/len(ensemble)*100:.1f}%)\")\n            del mask_all\n            gc.collect()\n        else:\n            print(f\"    ⚠️ No predictions meet bonus criteria (MIN_SCORE_FOR_BONUS={MIN_SCORE_FOR_BONUS})\")\n    \n    elif DIVERSITY_BONUS_MODE == 'fixed':\n        mask_all = (ensemble['score_a'] > 0) & (ensemble['score_b'] > 0) & (ensemble['score_c'] > 0)\n        ensemble['bonus'] = 0.0\n        n_bonus = mask_all.sum()\n        ensemble.loc[mask_all, 'bonus'] = DIVERSITY_BONUS_FIXED\n        print(f\"    ✓ Fixed bonus applied to {n_bonus:,} predictions ({n_bonus/len(ensemble)*100:.1f}%)\")\n        del mask_all\n        gc.collect()\n    \n    else:\n        ensemble['bonus'] = 0.0\n    \n    # Final score\n    ensemble['score'] = (ensemble['weighted_score'] + ensemble['bonus']).clip(upper=SCORE_CLIP_MAX)\n\n# Conflict resolution\nif CONFLICT_RESOLUTION == 'max':\n    # Take max of individual scores or ensemble\n    ensemble['score'] = ensemble[['score_a', 'score_b', 'score_c', 'score']].max(axis=1)\nelif CONFLICT_RESOLUTION == 'min':\n    # Conservative: take min where all predict\n    mask_all = (ensemble['score_a'] > 0) & (ensemble['score_b'] > 0) & (ensemble['score_c'] > 0)\n    ensemble.loc[mask_all, 'score'] = ensemble.loc[mask_all, ['score_a', 'score_b', 'score_c']].min(axis=1)\n\n# Drop intermediate columns để giảm memory\nensemble = ensemble[['protein', 'go_term', 'score']].copy()\ngc.collect()\n\nprint(f\"  💾 Memory after cleanup: ~{ensemble.memory_usage(deep=True).sum() / 1e9:.2f} GB\")\n\nelapsed = time.time() - start_time\nprint(f\"  ✓ Ensemble created in {elapsed:.2f}s\")\n\n# Filtering\nprint(f\"\\n🔍 Filtering predictions...\")\nprint(f\"  - Before final filter: {len(ensemble):,} predictions\")\n\n# Apply confidence threshold\nif CONFIDENCE_THRESHOLD > 0:\n    before = len(ensemble)\n    ensemble = ensemble[ensemble['score'] >= CONFIDENCE_THRESHOLD].copy()\n    gc.collect()\n    after = len(ensemble)\n    print(f\"  - After confidence filter (≥{CONFIDENCE_THRESHOLD}): {after:,} predictions (removed {before-after:,})\")\nelse:\n    print(f\"  - No confidence threshold applied\")\n\n# Top-K per protein\nif TOP_K_PER_PROTEIN is not None:\n    print(f\"  - Applying top-{TOP_K_PER_PROTEIN} per protein filter...\")\n    ensemble = ensemble.sort_values('score', ascending=False)\n    ensemble = ensemble.groupby('protein').head(TOP_K_PER_PROTEIN).reset_index(drop=True)\n    gc.collect()\n    print(f\"  - After top-K filter: {len(ensemble):,} predictions\")\n\n# Sort by score\nensemble = ensemble.sort_values('score', ascending=False)\n\n# Statistics\nprint(f\"\\n📊 Final Statistics:\")\nprint(f\"  - Total predictions: {len(ensemble):,}\")\nprint(f\"  - Proteins covered: {ensemble['protein'].nunique():,}\")\nprint(f\"  - GO terms covered: {ensemble['go_term'].nunique():,}\")\nprint(f\"  - Avg predictions per protein: {len(ensemble)/ensemble['protein'].nunique():.1f}\")\nprint(f\"  - Score distribution:\")\nprint(f\"    • Mean: {ensemble['score'].mean():.4f}\")\nprint(f\"    • Median: {ensemble['score'].median():.4f}\")\nprint(f\"    • Std: {ensemble['score'].std():.4f}\")\nprint(f\"    • Min: {ensemble['score'].min():.4f}\")\nprint(f\"    • Max: {ensemble['score'].max():.4f}\")\n\n# Save submission\nprint(f\"\\n💾 Saving submission...\")\noutput_file = 'submission.tsv'\nensemble[['protein', 'go_term', 'score']].to_csv(\n    output_file, \n    sep='\\t', \n    index=False, \n    header=False\n)\nprint(f\"  ✓ Saved to: {output_file}\")\n\n# Total time\ntotal_time = time.time() - overall_start\nprint(f\"\\n⏱️ Total execution time: {total_time:.2f}s ({total_time/60:.1f} minutes)\")\nprint(\"=\"*60)\nprint(\"✅ ENSEMBLE COMPLETE!\")\nprint(f\"🎯 Experiment with different parameters to optimize score!\")\nprint(\"=\"*60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-05T01:34:39.287222Z","iopub.execute_input":"2025-12-05T01:34:39.287634Z","iopub.status.idle":"2025-12-05T01:39:51.651300Z","shell.execute_reply.started":"2025-12-05T01:34:39.287597Z","shell.execute_reply":"2025-12-05T01:39:51.649897Z"}},"outputs":[],"execution_count":null}]}