{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":116062,"databundleVersionId":14084779,"sourceType":"competition"},{"sourceId":268508255,"sourceType":"kernelVersion"},{"sourceId":269339911,"sourceType":"kernelVersion"},{"sourceId":270571028,"sourceType":"kernelVersion"},{"sourceId":271888783,"sourceType":"kernelVersion"}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false},"papermill":{"default_parameters":{},"duration":2172.733498,"end_time":"2025-10-30T04:20:53.462753","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-10-30T03:44:40.729255","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"### Credit goes to this author and notebook\n\nhttps://www.kaggle.com/code/analyticaobscura/cafa-6-decoding-protein-mysteries \n\n-  here trying weighted average models\n-  meta ensemble model is upcoming ","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport os\nfrom tqdm.auto import tqdm\nfrom typing import Callable, Optional, Dict, Any\n\ndef stacking_ensemble(\n    file_paths, \n    weights=None, \n    method='all', \n    output_path='submission.tsv', \n    chunksize=100_000,\n    oof_callback: Optional[Callable] = None,\n    oof_data: Optional[Dict[str, Any]] = None\n):\n    \"\"\"\n    Advanced stacking ensemble combining median, weighted average, and rank averaging\n    \n    Parameters:\n    -----------\n    file_paths : list - Paths to prediction files\n    weights : list - Model weights (normalized automatically)\n    method : str - 'median', 'weighted_average', 'rank_average', or 'all' (stacking)\n    output_path : str - Output file path\n    chunksize : int - Rows per chunk\n    oof_callback : callable - Validation callback function\n    oof_data : dict - OOF validation data {'labels': df, 'proteins': list}\n    \n    Returns: (predictions_df, oof_metrics)\n    \"\"\"\n    \n    if weights is None:\n        weights = [1.0 / len(file_paths)] * len(file_paths)\n    else:\n        weights = np.array(weights) / np.array(weights).sum()\n    \n    print(f\"Models: {len(file_paths)} | Weights: {weights} | Method: {method}\")\n\n    # Step 1: Collect unique keys\n    print(\"\\nScanning files...\")\n    all_keys = set()\n    for path in tqdm(file_paths, desc=\"Files\"):\n        for chunk in pd.read_csv(path, sep='\\t', header=None,\n                                 names=['protein', 'go_term', 'score'],\n                                 dtype={'protein': str, 'go_term': str, 'score': float},\n                                 chunksize=chunksize):\n            chunk = chunk.dropna(subset=['protein', 'go_term'])\n            chunk['key'] = chunk['protein'] + '_' + chunk['go_term']\n            all_keys.update(chunk['key'].values)\n\n    all_keys = sorted(all_keys)\n    print(f\"Total predictions: {len(all_keys):,}\")\n\n    # Step 2: Process chunks\n    print(\"\\nProcessing chunks...\")\n    temp_files = []\n    for start in tqdm(range(0, len(all_keys), chunksize), desc=\"Chunks\"):\n        end = min(start + chunksize, len(all_keys))\n        key_chunk = all_keys[start:end]\n        result = pd.DataFrame({'key': key_chunk})\n\n        # Load scores from all models\n        for idx, path in enumerate(file_paths):\n            model_scores = []\n            for chunk in pd.read_csv(path, sep='\\t', header=None,\n                                     names=['protein', 'go_term', 'score'],\n                                     dtype={'protein': str, 'go_term': str, 'score': float},\n                                     chunksize=chunksize):\n                chunk['key'] = chunk['protein'] + '_' + chunk['go_term']\n                filtered = chunk[chunk['key'].isin(key_chunk)][['key', 'score']]\n                filtered = filtered.rename(columns={'score': f'score_{idx}'})\n                model_scores.append(filtered)\n            if model_scores:\n                model_df = pd.concat(model_scores, ignore_index=True)\n                result = result.merge(model_df, on='key', how='left')\n\n        score_cols = [col for col in result.columns if col.startswith('score_')]\n        result[score_cols] = result[score_cols].fillna(0)\n\n        # Calculate ensemble\n        if method == 'median':\n            result['final_score'] = result[score_cols].median(axis=1)\n            \n        elif method == 'weighted_average':\n            result['final_score'] = sum(result[f'score_{i}'] * weights[i] \n                                       for i in range(len(file_paths)))\n            \n        elif method == 'rank_average':\n            for i in range(len(file_paths)):\n                result[f'rank_{i}'] = result[f'score_{i}'].rank(pct=True)\n            result['final_score'] = sum(result[f'rank_{i}'] * weights[i] \n                                       for i in range(len(file_paths)))\n            \n        elif method == 'all':\n            # Stacking: combine all three methods\n            result['median_score'] = result[score_cols].median(axis=1)\n            result['weighted_avg'] = sum(result[f'score_{i}'] * weights[i] \n                                        for i in range(len(file_paths)))\n            for i in range(len(file_paths)):\n                result[f'rank_{i}'] = result[f'score_{i}'].rank(pct=True)\n            result['rank_avg'] = sum(result[f'rank_{i}'] * weights[i] \n                                    for i in range(len(file_paths)))\n            \n            # Meta-ensemble weights (tune these!)\n            result['final_score'] = (\n                result['median_score'] * 0.25 +\n                result['weighted_avg'] * 0.40 +\n                result['rank_avg'] * 0.35\n            )\n\n        result['protein'], result['go_term'] = zip(*result['key'].str.rsplit('_', n=1))\n        temp_file = f'temp_chunk_{start}.csv'\n        result[['protein', 'go_term', 'final_score']].to_csv(temp_file, index=False, sep='\\t', header=False)\n        temp_files.append(temp_file)\n\n    # Step 3: Combine chunks\n    print(\"\\nCombining chunks...\")\n    df_list = [pd.read_csv(f, sep='\\t', header=None, \n                          names=['protein', 'go_term', 'final_score']) \n               for f in tqdm(temp_files, desc=\"Combining\")]\n    final_df = pd.concat(df_list, ignore_index=True)\n    \n    # Step 4: OOF validation\n    oof_metrics = None\n    if oof_callback and oof_data:\n        print(\"\\nOOF Validation...\")\n        if 'proteins' in oof_data:\n            oof_predictions = final_df[final_df['protein'].isin(oof_data['proteins'])].copy()\n            print(f\"OOF predictions: {len(oof_predictions):,}\")\n            oof_metrics = oof_callback(oof_predictions, oof_data)\n            print(f\"F-max: {oof_metrics.get('f_max', 0):.4f} | Threshold: {oof_metrics.get('best_threshold', 0):.3f}\")\n    \n    # Step 5: Save and cleanup\n    print(f\"\\nSaving to {output_path}...\")\n    final_df.to_csv(output_path, sep='\\t', index=False, header=False)\n    \n    for temp_file in temp_files:\n        try:\n            os.remove(temp_file)\n        except:\n            pass\n    \n    print(f\"✓ Done! {len(final_df):,} predictions saved\\n\")\n    return final_df, oof_metrics\n\n\ndef cafa_oof_callback(oof_predictions: pd.DataFrame, oof_data: Dict[str, Any]) -> Dict[str, float]:\n    \"\"\"Calculate F-max and CAFA metrics\"\"\"\n    \n    labels = oof_data['labels']\n    merged = oof_predictions.merge(labels, on=['protein', 'go_term'], how='outer')\n    merged['final_score'] = merged['final_score'].fillna(0)\n    merged['label'] = merged['label'].fillna(0)\n    \n    thresholds = oof_data.get('thresholds', np.arange(0.01, 1.0, 0.01))\n    best_f1, best_threshold = 0, 0\n    precision_list, recall_list = [], []\n    \n    for threshold in thresholds:\n        pred_binary = (merged['final_score'] >= threshold).astype(int)\n        true_binary = merged['label'].astype(int)\n        \n        tp = ((pred_binary == 1) & (true_binary == 1)).sum()\n        fp = ((pred_binary == 1) & (true_binary == 0)).sum()\n        fn = ((pred_binary == 0) & (true_binary == 1)).sum()\n        \n        precision = tp / (tp + fp) if (tp + fp) > 0 else 0\n        recall = tp / (tp + fn) if (tp + fn) > 0 else 0\n        f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0\n        \n        precision_list.append(precision)\n        recall_list.append(recall)\n        \n        if f1 > best_f1:\n            best_f1, best_threshold = f1, threshold\n    \n    return {\n        'f_max': best_f1,\n        'best_threshold': best_threshold,\n        'mean_precision': np.mean(precision_list),\n        'mean_recall': np.mean(recall_list),\n        'auc_pr': np.trapz(precision_list, recall_list)\n    }\n\n","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","execution":{"iopub.execute_input":"2025-10-30T03:44:46.041801Z","iopub.status.busy":"2025-10-30T03:44:46.041386Z","iopub.status.idle":"2025-10-30T04:20:51.724413Z","shell.execute_reply":"2025-10-30T04:20:51.723175Z"},"papermill":{"duration":2165.690556,"end_time":"2025-10-30T04:20:51.727945","exception":false,"start_time":"2025-10-30T03:44:46.037389","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# =============================================================================\n# USAGE\n# =============================================================================\n\nif __name__ == \"__main__\":\n    \n    file_paths = [\n        '/kaggle/input/cafa-6-t5-embeddings-with-ensemble/submission.tsv',\n        '/kaggle/input/cafa-6-predictions/submission.tsv'\n    ]\n    \n    weights = [0.35, 0.30]  # Based on validation F-max\n    \n    # Basic stacking ensemble\n    result, _ = stacking_ensemble(\n        file_paths=file_paths,\n        weights=weights,\n        method='all',\n        output_path='submission.tsv',\n        chunksize=10_000_000\n    )\n    \n    # With OOF validation (uncomment to use)\n    # oof_data = {\n    #     'labels': pd.read_csv('oof_labels.tsv', sep='\\t', \n    #                          names=['protein', 'go_term', 'label']),\n    #     'proteins': ['prot1', 'prot2'],\n    #     'thresholds': np.arange(0.01, 1.0, 0.01)\n    # }\n    # \n    # result, metrics = stacking_ensemble(\n    #     file_paths=file_paths,\n    #     weights=weights,\n    #     method='all',\n    #     output_path='submission.tsv',\n    #     chunksize=10_000_000,\n    #     oof_callback=cafa_oof_callback,\n    #     oof_data=oof_data\n    # )\n","metadata":{"papermill":{"duration":0.001724,"end_time":"2025-10-30T04:20:51.73418","exception":false,"start_time":"2025-10-30T04:20:51.732456","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}