{"cells": [{"cell_type": "markdown", "metadata": {}, "source": "# CAFA-6: GOA Negative Propagation\n\n**Strategy:**\n1. Use GOA UniProt \"NOT\" annotations as negative examples\n2. Propagate negatives through GO hierarchy descendants\n3. Remove false positives from predictions\n4. Add ground-truth GOA annotations with score=1.0\n5. Blend multiple high-scoring submissions"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import os, gc\nfrom collections import defaultdict\nimport pandas as pd\nfrom tqdm.auto import tqdm\nimport numpy as np\n\nprint('CAFA-6 GOA Negative Propagation v1')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Paths\nTRAIN_TERMS = '/kaggle/input/cafa-6-protein-function-prediction/Train/train_terms.tsv'\nGO_OBO = '/kaggle/input/cafa-6-protein-function-prediction/Train/go-basic.obo'\nGOA_DATA = '/kaggle/input/protein-go-annotations/goa_uniprot_all.csv'\n\n# Submission sources\nQUICK_SUB = '/kaggle/input/cafa-quick-submission-0-358-score/submission.tsv'\nPROVEN_SUB = '/kaggle/input/cafa-6-proven-ensemble-submission/submission.tsv'\n\nfor f in [TRAIN_TERMS, GO_OBO, GOA_DATA, QUICK_SUB, PROVEN_SUB]:\n    status = 'OK' if os.path.exists(f) else 'MISSING'\n    print(f'{status}: {os.path.basename(f)}')"}, {"cell_type": "markdown", "metadata": {}, "source": "## Utility Functions"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "def parse_obo(go_obo_path):\n    \"\"\"Parse GO ontology to get parent/child relationships\"\"\"\n    parents = defaultdict(set)\n    children = defaultdict(set)\n    \n    if not os.path.exists(go_obo_path): \n        return parents, children\n        \n    with open(go_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: '):\n                pid = line.split()[1].strip()\n                if cur_id: \n                    parents[cur_id].add(pid)\n                    children[pid].add(cur_id)\n            elif line.startswith('relationship: part_of '):\n                parts = line.split()\n                if len(parts) >= 3:\n                    pid = parts[2].strip()\n                    if cur_id: \n                        parents[cur_id].add(pid)\n                        children[pid].add(cur_id)\n    print(f'Parsed OBO: {len(parents)} nodes with parents, {len(children)} with children')\n    return parents, children\n\ndef get_descendants(go_id, children_map):\n    \"\"\"Get all descendant terms (children, grandchildren, etc.)\"\"\"\n    desc = set()\n    stack = [go_id]\n    while stack:\n        cur = stack.pop()\n        for child in children_map.get(cur, []):\n            if child not in desc:\n                desc.add(child)\n                stack.append(child)\n    return desc\n\ndef get_ancestors(go_id, parents_map):\n    \"\"\"Get all ancestor terms (parents, grandparents, etc.)\"\"\"\n    ans = set()\n    stack = [go_id]\n    while stack:\n        cur = stack.pop()\n        for p in parents_map.get(cur, []): \n            if p not in ans:\n                ans.add(p)\n                stack.append(p)\n    return ans"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Parse GO ontology\nprint('[1/6] Parsing GO ontology...')\nparents_map, children_map = parse_obo(GO_OBO)"}, {"cell_type": "markdown", "metadata": {}, "source": "## Load and Process GOA Data"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "print('[2/6] Loading GOA UniProt data...')\ngoa = pd.read_csv(GOA_DATA)\ngoa = goa.drop_duplicates()\nprint(f'GOA dataset shape: {goa.shape}')\nprint(f'Qualifiers: {goa.qualifier.value_counts().to_dict()}')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "print('[3/6] Processing negative annotations...')\n\n# Extract \"NOT\" annotations - these are explicit negatives\nnegative_annots = goa[goa['qualifier'].str.contains('NOT', na=False)]\nnegative_annots = negative_annots.drop(columns=['qualifier']).drop_duplicates()\nprint(f'Direct negative annotations: {len(negative_annots):,}')\n\n# Group by protein\nnegative_by_protein = negative_annots.groupby('protein_id')['go_term'].apply(list).to_dict()\n\n# Propagate negatives to all descendants\n# If protein is NOT associated with GO:X, it's also NOT associated with any child of GO:X\npropagated_negatives = {}\nfor protein in tqdm(negative_by_protein.keys(), desc='Propagating negatives'):\n    terms = set(negative_by_protein[protein])\n    extra = set()\n    for t in list(terms): \n        extra |= get_descendants(t, children_map)\n    propagated_negatives[protein] = terms | extra\n\n# Create set of negative keys for fast lookup\nnegative_keys = set()\nfor protein, terms in propagated_negatives.items():\n    for term in terms:\n        negative_keys.add(f'{protein}_{term}')\n\nprint(f'Total negative protein-GO pairs after propagation: {len(negative_keys):,}')\n\ndel negative_annots, negative_by_protein, propagated_negatives\ngc.collect()"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "print('[4/6] Processing positive GOA annotations...')\n\n# Positive annotations (no NOT qualifier) - these are ground truth\npositive_goa = goa[~goa['qualifier'].str.contains('NOT', na=False)].copy()\npositive_goa.drop(columns=['qualifier'], inplace=True)\npositive_goa = positive_goa.drop_duplicates()\n\n# Set ground-truth score\npositive_goa['score'] = 1.0\n\n# Create key and remove any that conflict with negatives\npositive_goa['pred_key'] = positive_goa['protein_id'].astype(str) + '_' + positive_goa['go_term'].astype(str)\npositive_goa = positive_goa[~positive_goa['pred_key'].isin(negative_keys)]\ngoa_positive_keys = set(positive_goa['pred_key'])\n\nprint(f'Positive GOA annotations: {len(positive_goa):,}')\n\ndel goa\ngc.collect()"}, {"cell_type": "markdown", "metadata": {}, "source": "## Load and Blend Submissions"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "def load_submission(path, chunksize=50000, min_score=0.01):\n    \"\"\"Load submission in chunks for memory efficiency\"\"\"\n    chunks = []\n    for chunk in tqdm(pd.read_csv(path, sep='\\t', header=None, chunksize=chunksize), \n                     desc=f'Loading {os.path.basename(path)}'):\n        chunk.columns = ['protein_id', 'go_term', 'score']\n        chunk = chunk[chunk['score'] >= min_score]  # Filter low scores early\n        chunk['pred_key'] = chunk['protein_id'].astype(str) + '_' + chunk['go_term'].astype(str)\n        chunks.append(chunk)\n    df = pd.concat(chunks, ignore_index=True)\n    print(f'Loaded {len(df):,} predictions from {os.path.basename(path)}')\n    return df"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "print('[5/6] Loading and processing submissions...')\n\n# Load submissions\nA = load_submission(QUICK_SUB, min_score=0.02)\nB = load_submission(PROVEN_SUB, min_score=0.02)\n\n# Clip scores\nA['score'] = A['score'].clip(upper=1.0)\nB['score'] = B['score'].clip(upper=1.0)"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Remove ground-truth and negatives from both submissions\nprint('Removing GOA positives and negatives from predictions...')\n\nA_before = len(A)\nA = A[~A.pred_key.isin(goa_positive_keys)]\nA = A[~A.pred_key.isin(negative_keys)]\nprint(f'A: {A_before:,} -> {len(A):,} (removed {A_before - len(A):,})')\n\nB_before = len(B)\nB = B[~B.pred_key.isin(goa_positive_keys)]\nB = B[~B.pred_key.isin(negative_keys)]\nprint(f'B: {B_before:,} -> {len(B):,} (removed {B_before - len(B):,})')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Blend submissions with weighted average\n# Weights based on approximate LB scores\nW_A = 0.358  # Quick submission LB score\nW_B = 0.373  # Proven ensemble approximate\n\nprint('Blending submissions...')\n\nA_keys = set(A.pred_key)\nB_keys = set(B.pred_key)\nintersect_keys = A_keys & B_keys\n\nprint(f'A only: {len(A_keys - B_keys):,}')\nprint(f'B only: {len(B_keys - A_keys):,}')\nprint(f'Intersection: {len(intersect_keys):,}')\n\n# Merge intersection with weighted average\nA_inter = A[A.pred_key.isin(intersect_keys)].copy()\nB_inter = B[B.pred_key.isin(intersect_keys)].copy()\n\nmerged = A_inter.merge(B_inter[['pred_key', 'score']], on='pred_key', suffixes=('_a', '_b'))\nmerged['score'] = (merged['score_a'] * W_A + merged['score_b'] * W_B) / (W_A + W_B)\nmerged.drop(columns=['score_a', 'score_b'], inplace=True)\n\n# Get unique predictions from each\nA_only = A[~A.pred_key.isin(B_keys)]\nB_only = B[~B.pred_key.isin(A_keys)]\n\nprint(f'Merged intersection: {len(merged):,}')\nprint(f'A only: {len(A_only):,}')\nprint(f'B only: {len(B_only):,}')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Combine everything: GOA positives + blended predictions\nprint('[6/6] Creating final submission...')\n\n# Prepare positive GOA\ngoa_final = positive_goa[['protein_id', 'go_term', 'score', 'pred_key']].copy()\n\n# Combine all parts\nsubmission = pd.concat([goa_final, merged, A_only, B_only], axis=0, ignore_index=True)\n\n# Remove duplicates, keeping highest score\nsubmission = submission.sort_values('score', ascending=False).drop_duplicates(subset=['pred_key'])\n\n# Clean up\nsubmission.drop(columns=['pred_key'], inplace=True)\nsubmission = submission.sort_values(['protein_id', 'score'], ascending=[True, False])\n\nprint(f'Final submission: {len(submission):,} predictions')\nprint(f'Unique proteins: {submission.protein_id.nunique():,}')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Save submission\nprint('Saving submission.tsv...')\nsubmission.to_csv('submission.tsv', sep='\\t', index=False, header=False)\n\nprint(f'\\n{\"=\"*60}')\nprint('DONE - CAFA6 GOA Negative Propagation v1')\nprint(f'Total predictions: {len(submission):,}')\nprint(f'Unique proteins: {submission.protein_id.nunique():,}')\nprint(f'{\"=\"*60}')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Show sample\nprint('\\nSample predictions:')\nprint(submission.head(10))\n\nprint('\\nScore distribution:')\nprint(f'min={submission.score.min():.3f}, max={submission.score.max():.3f}, mean={submission.score.mean():.3f}')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "!head submission.tsv"}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"name": "python", "version": "3.10.0"}}, "nbformat": 4, "nbformat_minor": 4}