{"cells": [{"cell_type": "markdown", "metadata": {}, "source": "# CAFA-6 Super Ensemble - Advanced Post-Processing\n\n**Objetivo:** Combinar las mejores submissions p\u00fablicas con post-processing avanzado.\n\n## T\u00e9cnicas implementadas:\n1. **Weighted Ensemble** - Pesos calculados por LB score (exponencial)\n2. **GOA Negative Propagation** - Eliminar t\u00e9rminos NOT + todos sus descendientes\n3. **GOA Positive Injection** - Inyectar ground truth con score = 1.0\n4. **GO Hierarchy Propagation** - Propagar scores a ancestros\n5. **Score Normalization** - Escalar max score a 0.95 por prote\u00edna\n6. **Root Forcing** - GO roots siempre = 1.0\n7. **Diversity Bonus** - +0.03 si m\u00faltiples modelos acuerdan\n\n## Acknowledgments:\n- seddik turki (blend-goa-negative-propagation)\n- sasa_leaf (merge-of-2submission-lb-0-25)"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import os\nimport gc\nimport numpy as np\nimport pandas as pd\nfrom collections import defaultdict\nfrom tqdm.auto import tqdm\n\nprint('Libraries loaded!')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# ============================================================\n# CONFIGURACI\u00d3N\n# ============================================================\nclass CFG:\n    # Paths\n    OBO_PATH = \"/kaggle/input/cafa-6-protein-function-prediction/Train/go-basic.obo\"\n    \n    # Submissions a combinar (path, lb_score)\n    # Usando kernel sources disponibles\n    SUBMISSIONS = [\n        (\"/kaggle/input/cafa-6-blend-goa-negative-propagation/submission.tsv\", 0.250),\n        (\"/kaggle/input/merge-of-2submission-lb-0-25/submission.tsv\", 0.250),\n    ]\n    \n    # GOA annotations\n    GOA_PATH = \"/kaggle/input/goa-uniprot-all/selection_ids_goa_uniprot_all.csv\"\n    \n    # Hyperpar\u00e1metros de post-processing\n    DIVERSITY_BONUS = 0.03      # Bonus si ambos modelos predicen mismo par\n    MIN_AGREEMENT = 2           # M\u00ednimo modelos para diversity bonus\n    SCORE_THRESHOLD = 0.001     # Threshold m\u00ednimo\n    NORMALIZE_MAX = 0.95        # Target para normalizaci\u00f3n\n    \n    # GO Root terms (siempre = 1.0)\n    ROOTS = {'GO:0003674', 'GO:0008150', 'GO:0005575'}\n\nprint(f'Config loaded. {len(CFG.SUBMISSIONS)} submissions to ensemble.')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# ============================================================\n# FUNCIONES AUXILIARES\n# ============================================================\n\ndef parse_obo(obo_path):\n    \"\"\"Parse GO ontology para obtener relaciones parent/child\"\"\"\n    parents = defaultdict(set)\n    children = defaultdict(set)\n    \n    if not os.path.exists(obo_path):\n        print(f'[WARN] OBO not found: {obo_path}')\n        return parents, children\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: '):\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    \n    print(f'[OBO] Parsed {len(parents)} terms')\n    return parents, children\n\n\ndef get_ancestors(term, parents_map, cache=None):\n    \"\"\"Obtener todos los ancestros de un t\u00e9rmino GO\"\"\"\n    if cache is not None and term in cache:\n        return cache[term]\n    ancestors = set()\n    stack = [term]\n    while stack:\n        cur = stack.pop()\n        for p in parents_map.get(cur, []):\n            if p not in ancestors:\n                ancestors.add(p)\n                stack.append(p)\n    if cache is not None:\n        cache[term] = ancestors\n    return ancestors\n\n\ndef get_descendants(term, children_map, cache=None):\n    \"\"\"Obtener todos los descendientes de un t\u00e9rmino GO\"\"\"\n    if cache is not None and term in cache:\n        return cache[term]\n    descendants = set()\n    stack = [term]\n    while stack:\n        cur = stack.pop()\n        for c in children_map.get(cur, []):\n            if c not in descendants:\n                descendants.add(c)\n                stack.append(c)\n    if cache is not None:\n        cache[term] = descendants\n    return descendants\n\n\ndef calculate_weights(lb_scores):\n    \"\"\"Pesos exponenciales basados en LB score\"\"\"\n    scores = np.array(lb_scores)\n    exp_scores = np.exp(scores * 10)\n    return exp_scores / exp_scores.sum()\n\nprint('Helper functions defined!')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# ============================================================\n# 1. CARGAR ONTOLOG\u00cdA GO\n# ============================================================\nprint('=' * 60)\nprint('[1/7] Loading GO Ontology...')\nparents_map, children_map = parse_obo(CFG.OBO_PATH)"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# ============================================================\n# 2. PROCESAR GOA ANNOTATIONS\n# ============================================================\nprint('\\n[2/7] Loading GOA annotations...')\nnegative_keys = set()\npositive_df = None\n\nif os.path.exists(CFG.GOA_PATH):\n    goa = pd.read_csv(CFG.GOA_PATH)\n    print(f'GOA loaded: {len(goa):,} rows')\n    \n    # Detectar columnas\n    pid_col = 'db_object_id' if 'db_object_id' in goa.columns else 'protein_id'\n    term_col = 'go_id' if 'go_id' in goa.columns else 'go_term'\n    \n    # Negative annotations (NOT qualifiers)\n    if 'qualifier' in goa.columns:\n        neg_goa = goa[goa['qualifier'].str.contains('NOT', na=False)]\n        print(f'Found {len(neg_goa):,} NOT annotations')\n        \n        desc_cache = {}\n        neg_expanded = []\n        \n        for _, row in tqdm(neg_goa.iterrows(), total=len(neg_goa), desc='Propagating negatives'):\n            pid = str(row[pid_col])\n            term = str(row[term_col])\n            if pid and term and term != 'nan':\n                neg_expanded.append((pid, term))\n                # Propagar a descendientes\n                for desc in get_descendants(term, children_map, desc_cache):\n                    neg_expanded.append((pid, desc))\n        \n        negative_keys = {f'{p}_{t}' for p, t in neg_expanded}\n        print(f'[GOA-] {len(negative_keys):,} negative pairs (propagated)')\n    \n    # Positive annotations (non-NOT)\n    pos_goa = goa[~goa['qualifier'].str.contains('NOT', na=False)] if 'qualifier' in goa.columns else goa\n    \n    positive_df = pd.DataFrame({\n        'protein_id': pos_goa[pid_col].astype(str),\n        'go_term': pos_goa[term_col].astype(str),\n        'score': 1.0\n    }).drop_duplicates()\n    positive_df = positive_df[positive_df['go_term'] != 'nan']\n    print(f'[GOA+] {len(positive_df):,} positive annotations')\n    \n    del goa\n    gc.collect()\nelse:\n    print(f'[WARN] GOA not found: {CFG.GOA_PATH}')\n    print('Continuing without GOA corrections...')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# ============================================================\n# 3. CARGAR SUBMISSIONS\n# ============================================================\nprint('\\n[3/7] Loading submissions...')\n\n# Listar archivos disponibles\nprint('Available inputs:')\nfor d in os.listdir('/kaggle/input'):\n    path = f'/kaggle/input/{d}'\n    if os.path.isdir(path):\n        files = os.listdir(path)\n        print(f'  {d}/: {files[:5]}')\n\n# Filtrar submissions que existen\nvalid_subs = [(p, s) for p, s in CFG.SUBMISSIONS if os.path.exists(p)]\nprint(f'\\nFound {len(valid_subs)}/{len(CFG.SUBMISSIONS)} valid submissions')\n\nif not valid_subs:\n    raise ValueError('No valid submissions found!')\n\n# Calcular pesos exponenciales\nlb_scores = [s for _, s in valid_subs]\nweights = calculate_weights(lb_scores)\n\n# Cargar cada submission\nsubmissions = []\nfor (path, lb_score), weight in zip(valid_subs, weights):\n    df = pd.read_csv(path, sep='\\t', header=None, names=['protein_id', 'go_term', 'score'])\n    df['protein_id'] = df['protein_id'].astype(str)\n    df['go_term'] = df['go_term'].astype(str)\n    df['score'] = pd.to_numeric(df['score'], errors='coerce').fillna(0).clip(0, 1.0)\n    df['weight'] = weight\n    print(f'  {os.path.basename(os.path.dirname(path))}: {len(df):,} rows, LB={lb_score:.3f}, weight={weight:.3f}')\n    submissions.append(df)\n\nprint(f'\\nTotal: {sum(len(s) for s in submissions):,} predictions to ensemble')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# ============================================================\n# 4. WEIGHTED ENSEMBLE + DIVERSITY BONUS\n# ============================================================\nprint('\\n[4/7] Creating weighted ensemble...')\n\n# Concatenar todas las submissions\nall_preds = pd.concat(submissions, ignore_index=True)\nall_preds['key'] = all_preds['protein_id'] + '_' + all_preds['go_term']\nprint(f'Concatenated: {len(all_preds):,} rows')\n\n# Eliminar predicciones negativas (GOA NOT)\nif negative_keys:\n    before = len(all_preds)\n    all_preds = all_preds[~all_preds['key'].isin(negative_keys)]\n    print(f'Removed {before - len(all_preds):,} negative predictions')\n\n# Agrupar y calcular weighted average\nprint('Aggregating predictions...')\n\n# M\u00e9todo m\u00e1s eficiente\nall_preds['weighted_score'] = all_preds['score'] * all_preds['weight']\nagg = all_preds.groupby(['protein_id', 'go_term']).agg({\n    'weighted_score': 'sum',\n    'weight': 'sum',\n    'score': 'count'  # Cuenta de modelos que predicen este par\n}).reset_index()\nagg.columns = ['protein_id', 'go_term', 'weighted_sum', 'weight_sum', 'n_models']\nagg['score'] = agg['weighted_sum'] / agg['weight_sum']\n\n# Diversity bonus: si m\u00faltiples modelos acuerdan, boost\nagg.loc[agg['n_models'] >= CFG.MIN_AGREEMENT, 'score'] += CFG.DIVERSITY_BONUS\nagg['score'] = agg['score'].clip(0, 1.0)\n\nensemble = agg[['protein_id', 'go_term', 'score']].copy()\nprint(f'[ENSEMBLE] {len(ensemble):,} unique predictions')\nprint(f'  Pairs with agreement bonus: {(agg[\"n_models\"] >= CFG.MIN_AGREEMENT).sum():,}')\n\ndel all_preds, submissions, agg\ngc.collect()"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# ============================================================\n# 5. GO HIERARCHY PROPAGATION + NORMALIZATION\n# ============================================================\nprint('\\n[5/7] Propagating to ancestors + normalizing...')\n\nancestor_cache = {}\npropagated_rows = []\n\n# Agrupar por prote\u00edna\nprotein_preds = ensemble.groupby('protein_id').apply(\n    lambda g: dict(zip(g['go_term'], g['score']))\n).to_dict()\n\nfor pid, terms_dict in tqdm(protein_preds.items(), desc='Propagating'):\n    final_scores = terms_dict.copy()\n    \n    # A. Propagar scores a ancestros (parent >= child)\n    for term, score in list(terms_dict.items()):\n        for ancestor in get_ancestors(term, parents_map, ancestor_cache):\n            final_scores[ancestor] = max(final_scores.get(ancestor, 0), score)\n    \n    # B. Force GO roots to 1.0\n    if final_scores:\n        for root in CFG.ROOTS:\n            final_scores[root] = 1.0\n    \n    # C. Score normalization (boost max non-root to 0.95)\n    max_non_root = max((s for t, s in final_scores.items() if t not in CFG.ROOTS), default=0)\n    if 0 < max_non_root < CFG.NORMALIZE_MAX:\n        scale = CFG.NORMALIZE_MAX / max_non_root\n        for t in final_scores:\n            if t not in CFG.ROOTS:\n                final_scores[t] = min(1.0, final_scores[t] * scale)\n    \n    # Agregar filas (filtrar muy bajos)\n    for term, score in final_scores.items():\n        if score >= CFG.SCORE_THRESHOLD:\n            propagated_rows.append((pid, term, round(score, 6)))\n\nfinal_df = pd.DataFrame(propagated_rows, columns=['protein_id', 'go_term', 'score'])\nprint(f'[PROPAGATED] {len(final_df):,} predictions')\n\ndel ensemble, protein_preds\ngc.collect()"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# ============================================================\n# 6. INYECTAR GOA POSITIVOS\n# ============================================================\nprint('\\n[6/7] Injecting GOA positive annotations...')\n\nif positive_df is not None and len(positive_df) > 0:\n    # Solo para prote\u00ednas que est\u00e1n en nuestras predicciones\n    test_proteins = set(final_df['protein_id'].unique())\n    positive_relevant = positive_df[positive_df['protein_id'].isin(test_proteins)].copy()\n    \n    print(f'  Relevant positive annotations: {len(positive_relevant):,}')\n    \n    before = len(final_df)\n    final_df = pd.concat([final_df, positive_relevant], ignore_index=True)\n    final_df = final_df.groupby(['protein_id', 'go_term'], as_index=False)['score'].max()\n    \n    print(f'[GOA+] Final count: {len(final_df):,} (delta: {len(final_df) - before:,})')\nelse:\n    print('[SKIP] No positive GOA to inject')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# ============================================================\n# 7. GUARDAR SUBMISSION\n# ============================================================\nprint('\\n[7/7] Saving submission...')\n\n# Ordenar por protein_id, luego por score descendente\nfinal_df = final_df.sort_values(['protein_id', 'score'], ascending=[True, False])\n\n# Guardar sin header\nfinal_df.to_csv('submission.tsv', sep='\\t', index=False, header=False)\n\nprint('\\n' + '=' * 60)\nprint('DONE!')\nprint(f'Final submission: {len(final_df):,} predictions')\nprint(f'Unique proteins: {final_df[\"protein_id\"].nunique():,}')\nprint(f'Unique GO terms: {final_df[\"go_term\"].nunique():,}')\nprint(f'Score range: [{final_df[\"score\"].min():.4f}, {final_df[\"score\"].max():.4f}]')\nprint('=' * 60)"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Preview\nprint('\\nPreview (first 20 rows):')\nprint(final_df.head(20).to_string(index=False))\n\nprint('\\nScore distribution:')\nprint(final_df['score'].describe())"}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"name": "python", "version": "3.10.0"}}, "nbformat": 4, "nbformat_minor": 4}