{"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\nfrom tqdm.auto import tqdm\nimport gc\nfrom multiprocessing import Pool, cpu_count\n\n############# Stacking Ensemble #############\n\ndef process_single_chunk(args):\n    start, end, all_keys, file_paths, weights, method, num_models = args\n    key_chunk = all_keys[start:end]\n    result = pd.DataFrame({'key': key_chunk})\n\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=1_000_000):\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            model_df = model_df.groupby('key', as_index=False).mean()\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    if method == 'median':\n        result['final_score'] = result[score_cols].median(axis=1)\n    elif method == 'weighted_average':\n        result['final_score'] = sum(result[f'score_{i}'] * weights[i] for i in range(num_models))\n    elif method == 'rank_average':\n        for i in range(num_models):\n            result[f'rank_{i}'] = result[f'score_{i}'].rank(pct=True)\n        result['final_score'] = sum(result[f'rank_{i}'] * weights[i] for i in range(num_models))\n    elif method == 'all':\n        result['median_score'] = result[score_cols].median(axis=1)\n        result['weighted_avg'] = sum(result[f'score_{i}'] * weights[i] for i in range(num_models))\n        for i in range(num_models):\n            result[f'rank_{i}'] = result[f'score_{i}'].rank(pct=True)\n        result['rank_avg'] = sum(result[f'rank_{i}'] * weights[i] for i in range(num_models))\n        result['final_score'] = (result['median_score'] * 0.25 + \n                                result['weighted_avg'] * 0.40 + \n                                result['rank_avg'] * 0.35)\n    result['protein'], result['go_term'] = zip(*result['key'].str.rsplit('_', n=1))\n    return result[['protein', 'go_term', 'final_score']]\n\ndef stacking_ensemble_fast(file_paths, weights=None, method='all', output_path='submission.tsv',\n                           chunksize=5_000_000, n_jobs=-1):\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    if n_jobs == -1:\n        n_jobs = max(1, cpu_count() - 1)\n    print(f\"Models: {len(file_paths)} | Weights: {weights} | Method: {method}\")\n    print(f\"Parallel jobs: {n_jobs} | Chunk size: {chunksize:,}\")\n\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},\n                                 usecols=[0, 1],\n                                 chunksize=5_000_000):\n            chunk = chunk.dropna()\n            keys = chunk['protein'] + '_' + chunk['go_term']\n            all_keys.update(keys.values)\n            del chunk, keys\n            gc.collect()\n\n    all_keys = sorted(all_keys)\n    print(f\"Total predictions: {len(all_keys):,}\")\n\n    print(\"\\nProcessing chunks in parallel...\")\n    chunk_args = []\n    for start in range(0, len(all_keys), chunksize):\n        end = min(start + chunksize, len(all_keys))\n        chunk_args.append((start, end, all_keys, file_paths, weights, method, len(file_paths)))\n    if n_jobs > 1:\n        with Pool(n_jobs) as pool:\n            results = list(tqdm(pool.imap(process_single_chunk, chunk_args),\n                                total=len(chunk_args), desc=\"Chunks\"))\n    else:\n        results = [process_single_chunk(args) for args in tqdm(chunk_args, desc=\"Chunks\")]\n    print(\"\\nCombining results...\")\n    final_df = pd.concat(results, ignore_index=True)\n    del results\n    gc.collect()\n    print(f\"\\nSaving to {output_path}...\")\n    final_df.to_csv(output_path, sep='\\t', index=False, header=False)\n    print(f\"✓ Done! {len(final_df):,} predictions saved\\n\")\n    return final_df\n\n############# Per-GO-Term Threshold Optimization #############\n\ndef find_go_term_thresholds(df, label_df, go_terms, thresholds=np.arange(0.01, 1.0, 0.01)):\n    merged = df.merge(label_df, on=['protein', 'go_term'], how='left').fillna({'label': 0})\n    go_thresholds = {}\n    for term in tqdm(go_terms, desc=\"GO terms\"):\n        y_true = merged.loc[merged['go_term'] == term, 'label'].astype(int).values\n        y_score = merged.loc[merged['go_term'] == term, 'final_score'].values\n        best_f1, best_t = 0, 0\n        for t in thresholds:\n            pred = (y_score >= t).astype(int)\n            tp = ((pred == 1) & (y_true == 1)).sum()\n            fp = ((pred == 1) & (y_true == 0)).sum()\n            fn = ((pred == 0) & (y_true == 1)).sum()\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            if f1 > best_f1:\n                best_f1, best_t = f1, t\n        go_thresholds[term] = best_t\n    return go_thresholds\n\ndef binarize_with_go_thresholds(df, go_thresholds):\n    df['binarized'] = df.apply(lambda row: int(row['final_score'] >= go_thresholds.get(row['go_term'], 0.5)), axis=1)\n    return df\n\n############# GO Hierarchy Enforcement #############\n\ndef enforce_go_hierarchy(df, go_parents):\n    term_list = df['go_term'].unique()\n    grouped = df.groupby('protein')\n    records = []\n    for prot, group in tqdm(grouped, desc=\"GO hierarchy\"):\n        go_positive = set(group.loc[group['binarized'] == 1, 'go_term'])\n        updated = set(go_positive)\n        queue = list(go_positive)\n        while queue:\n            term = queue.pop()\n            for parent in go_parents.get(term, []):\n                if parent not in updated:\n                    updated.add(parent)\n                    queue.append(parent)\n        for go_term in group['go_term']:\n            out_val = 1 if go_term in updated else 0\n            records.append({'protein': prot, 'go_term': go_term, 'binarized': out_val})\n    return pd.DataFrame(records, columns=['protein','go_term','binarized'])\n","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","execution":{"iopub.status.busy":"2025-11-10T06:54:40.980989Z","iopub.execute_input":"2025-11-10T06:54:40.981279Z","iopub.status.idle":"2025-11-10T06:54:43.634705Z","shell.execute_reply.started":"2025-11-10T06:54:40.981249Z","shell.execute_reply":"2025-11-10T06:54:43.633424Z"},"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":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n############# USAGE EXAMPLE #############\n\n# Ensemble step (adjust as needed)\nfile_paths = [\n    '/kaggle/input/cafa-6-t5-embeddings-with-ensemble/submission.tsv',\n    '/kaggle/input/cafa-6-predictions/submission.tsv'\n]\nweights = [0.35, 0.30]\nresult = stacking_ensemble_fast(\n    file_paths=file_paths,\n    weights=weights,\n    method='all',\n    output_path='submission.tsv',\n    chunksize=5_000_000,\n    n_jobs=-1\n)\n\n# Per-GO-term thresholding (update oof_labels, go_terms accordingly)\n# oof_labels: DataFrame ['protein','go_term','label'] from validation\n# go_terms: List/array of observed go terms\ngo_terms = result['go_term'].unique()\noof_labels = pd.read_csv('path_to_oof_labels.tsv', sep='\\t') # should have columns 'protein','go_term','label'\ngo_thresholds = find_go_term_thresholds(result, oof_labels, go_terms)\nresult = binarize_with_go_thresholds(result, go_thresholds)\n\n# Enforce GO hierarchy (update go_parents accordingly)\n# go_parents: dictionary {child: [parent1, parent2, ...]}\nimport json\nwith open('path_to_go_parents.json', 'r') as f:\n    go_parents = json.load(f)\nresult = enforce_go_hierarchy(result, go_parents)\n\n# Save final submission\nresult[['protein','go_term','binarized']].to_csv('submission.tsv', sep='\\t', index=False, header=False)\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":[],"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T06:54:43.63692Z","iopub.execute_input":"2025-11-10T06:54:43.637414Z"}},"outputs":[],"execution_count":null}]}