{"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":14875579,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CAFA 6 Protein Function Prediction: FULL EDA \n\n\n## Reference: \n\n- https://www.kaggle.com/code/taylorsamarel/cafa-6-protein-function-starter-eda-model\n- https://www.kaggle.com/code/leonidkulyk/eda-cafa5-pfp-interactive-dags-plotly\n- https://www.kaggle.com/code/thedrcat/cafa-5-protein-function-prediction-eda/notebook\n\n\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"code","source":"\nimport sys\nimport subprocess\nimport numpy as np\nimport pandas as pd\nimport networkx as nx\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom collections import Counter\nfrom pathlib import Path\nfrom scipy.spatial.distance import pdist, squareform\n\n# --- Install Dependencies ---\ndef install(package):\n    subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", package])\n\ntry:\n    import obonet\nexcept ImportError:\n    install('obonet')\n    import obonet\n\ntry:\n    from Bio import SeqIO\n    from Bio.SeqUtils.ProtParam import ProteinAnalysis\nexcept ImportError:\n    install('biopython')\n    from Bio import SeqIO\n    from Bio.SeqUtils.ProtParam import ProteinAnalysis\n\ntry:\n    from pyvis.network import Network\nexcept ImportError:\n    install('pyvis')\n    from pyvis.network import Network\n\nimport plotly.express as px\n\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# Set display options\npd.set_option('display.max_columns', None)\npd.set_option('display.max_rows', 100)\npd.set_option('display.width', 200)\npd.set_option('display.precision', 6)\n\n# --- Configuration ---\nclass CFG:\n    BASE_DIR = Path('/kaggle/input/cafa-6-protein-function-prediction')\n    TRAIN_DIR = BASE_DIR / 'Train'\n    FILES = {\n        'obo': TRAIN_DIR / 'go-basic.obo',\n        'train_seq': TRAIN_DIR / 'train_sequences.fasta',\n        'train_terms': TRAIN_DIR / 'train_terms.tsv',\n        'train_tax': TRAIN_DIR / 'train_taxonomy.tsv',\n        'ia': BASE_DIR / 'IA.tsv'\n    }\n    # Use 20% of data for speed in EDA, set to 1.0 for full run\n    SAMPLE_FRAC = 0.20 \n    SEED = 42\n\nplt.style.use('seaborn-v0_8-whitegrid')\nsns.set_context(\"notebook\", font_scale=1.1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T15:48:27.344399Z","iopub.execute_input":"2025-12-16T15:48:27.344638Z","iopub.status.idle":"2025-12-16T15:48:47.239756Z","shell.execute_reply.started":"2025-12-16T15:48:27.344618Z","shell.execute_reply":"2025-12-16T15:48:47.239022Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Graph Topology & Information Accretion\n\nDoes the \"depth\" of a term in the DAG predict its weight (IA)?","metadata":{}},{"cell_type":"code","source":"\nprint(\"Loading Ontology & IA Weights...\")\ngo_graph = obonet.read_obo(CFG.FILES['obo'])\ndf_ia = pd.read_csv(CFG.FILES['ia'], sep='\\t', names=['term', 'ia'])\n\n# Define Roots\nroots = {'BP': 'GO:0008150', 'MF': 'GO:0003674', 'CC': 'GO:0005575'}\n\n# Function to calculate depth (shortest path from root)\n# Note: We cache this because calculating it for 40k nodes is slow\nnode_depths = {}\nnode_namespace = {}\n\nprint(\"Calculating Graph Depths (this takes a moment)...\")\nfor node, data in go_graph.nodes(data=True):\n    ns = data.get('namespace', '')\n    node_namespace[node] = ns\n    \n    # Map namespace to root ID\n    root_id = None\n    if ns == 'biological_process': root_id = roots['BP']\n    elif ns == 'molecular_function': root_id = roots['MF']\n    elif ns == 'cellular_component': root_id = roots['CC']\n    \n    if root_id and root_id in go_graph:\n        try:\n            # Shortest path is a proxy for \"minimum depth\"\n            depth = nx.shortest_path_length(go_graph, source=node, target=root_id)\n            node_depths[node] = depth\n        except nx.NetworkXNoPath:\n            node_depths[node] = 0\n    else:\n        node_depths[node] = 0\n\n# Merge Data\ndf_ia['depth'] = df_ia['term'].map(node_depths)\ndf_ia['namespace'] = df_ia['term'].map(node_namespace)\ndf_ia = df_ia.dropna(subset=['depth'])\n\n# --- Visualization: Depth vs IA ---\nplt.figure(figsize=(14, 6))\nsns.boxplot(data=df_ia, x='depth', y='ia', hue='namespace', palette='Set2')\nplt.title(\"Relationship between GO Term Depth and Information Accretion (IA)\")\nplt.xlabel(\"Distance to Root (Depth)\")\nplt.ylabel(\"Information Accretion (Weight)\")\nplt.legend(title='Ontology Aspect', loc='lower right')\nplt.grid(True, alpha=0.3)\nplt.show()\n\nprint(\"Insight: Deeper terms generally have higher IA, but there is significant variance.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T15:48:47.241221Z","iopub.execute_input":"2025-12-16T15:48:47.241757Z","iopub.status.idle":"2025-12-16T15:48:55.627305Z","shell.execute_reply.started":"2025-12-16T15:48:47.241736Z","shell.execute_reply":"2025-12-16T15:48:55.626417Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nprint(f\"Nodes: {len(go_graph)}\")\nprint(f\"Edges: {go_graph.number_of_edges()}\")\n\n# Helper to map terms to namespaces (MF, BP, CC)\nterm_to_namespace = {\n    node: data.get('namespace', 'unknown') \n    for node, data in go_graph.nodes(data=True)\n}\n\n# Namespace mapping for cleaner labels\nns_map = {\n    'molecular_function': 'MF',\n    'biological_process': 'BP',\n    'cellular_component': 'CC'\n}\n\n# Count terms per namespace\nns_counts = Counter([ns_map.get(v, v) for v in term_to_namespace.values()])\n\nplt.figure(figsize=(8, 3.5))\nplt.bar(ns_counts.keys(), ns_counts.values(), color=['#FF9999', '#66B2FF', '#99FF99'])\nplt.title(\"Distribution of GO Terms by Aspect (Ontology)\")\nplt.ylabel(\"Count of Unique Terms\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T15:48:55.628225Z","iopub.execute_input":"2025-12-16T15:48:55.628525Z","iopub.status.idle":"2025-12-16T15:48:55.865315Z","shell.execute_reply.started":"2025-12-16T15:48:55.628505Z","shell.execute_reply":"2025-12-16T15:48:55.864535Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Training Data Analysis\n\nAnalyzing the `train_terms.tsv` and `train_taxonomy.tsv`.","metadata":{}},{"cell_type":"code","source":"print(\"Loading tabular data...\")\ndf_terms = pd.read_csv(CFG.FILES['train_terms'], sep='\\t', names=['EntryID', 'term', 'aspect'])\ndf_tax = pd.read_csv(CFG.FILES['train_tax'], sep='\\t', names=['EntryID', 'taxonomyID'])\n\n# Merge taxonomy info into terms\ndf_merged = df_terms.merge(df_tax, on='EntryID', how='left')\n\nif CFG.SAMPLE_FRAC < 1.0:\n    print(f\"Subsampling data to {CFG.SAMPLE_FRAC*100}%...\")\n    unique_proteins = df_merged['EntryID'].unique()\n    sampled_proteins = np.random.choice(unique_proteins, int(len(unique_proteins) * CFG.SAMPLE_FRAC), replace=False)\n    df_merged = df_merged[df_merged['EntryID'].isin(sampled_proteins)]\n\nprint(f\"Total Annotations: {len(df_merged)}\")\nprint(f\"Unique Proteins: {df_merged['EntryID'].nunique()}\")\nprint(f\"Unique Taxa: {df_merged['taxonomyID'].nunique()}\")\n\ndf_merged.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T15:48:55.866162Z","iopub.execute_input":"2025-12-16T15:48:55.866371Z","iopub.status.idle":"2025-12-16T15:48:56.546918Z","shell.execute_reply.started":"2025-12-16T15:48:55.866354Z","shell.execute_reply":"2025-12-16T15:48:56.546183Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Interactive Network Visualization (PyVis)\n\nVisualize the parents and children of specific terms. This helps understand the \"Is-A\" relationships.\n","metadata":{}},{"cell_type":"code","source":"\ndef plot_interactive_dag(graph, term_id, radius=1, height=\"600px\"):\n    \"\"\"\n    Plots an interactive network of the GO term and its neighbors.\n    radius: How many hops (parents/children) to include.\n    \"\"\"\n    if term_id not in graph:\n        print(f\"Term {term_id} not found in Ontology.\")\n        return\n\n    \n    subg = nx.ego_graph(graph, term_id, radius=radius)\n    \n    # Initialize PyVis Network\n    nt = Network(notebook=True, height=height, width=\"100%\", cdn_resources='in_line', directed=True)\n    \n    # Add Nodes with Custom Styling\n    for node in subg.nodes():\n        data = subg.nodes[node]\n        name = data.get('name', 'Unknown')\n        definition = data.get('def', 'No definition')[:100] + \"...\"\n        \n        # Highlight the central node\n        if node == term_id:\n            color = '#ff5733' # Orange/Red for center\n            size = 25\n            title = f\"<b>{node} (TARGET)</b><br>{name}<br><br>{definition}\"\n        else:\n            color = '#97c2fc' # Blue for neighbors\n            size = 15\n            title = f\"<b>{node}</b><br>{name}<br><br>{definition}\"\n            \n        label = f\"{node}\\n{name[:15]}...\" # Short label\n        nt.add_node(node, label=label, title=title, color=color, size=size)\n\n    # Add Edges\n    for edge in subg.edges():\n        nt.add_edge(edge[0], edge[1], color='gray')\n\n    # Physics Options for better layout\n    nt.set_options(\"\"\"\n    var options = {\n      \"physics\": {\n        \"hierarchicalRepulsion\": {\n          \"nodeDistance\": 150\n        }\n      }\n    }\n    \"\"\")\n    \n    return nt.show(\"go_network.html\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T15:48:56.547735Z","iopub.execute_input":"2025-12-16T15:48:56.548025Z","iopub.status.idle":"2025-12-16T15:48:56.555785Z","shell.execute_reply.started":"2025-12-16T15:48:56.548005Z","shell.execute_reply":"2025-12-16T15:48:56.555071Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Example 1: DNA Repair ---\nprint(\"Visualizing 'DNA Repair' (GO:0006281) neighborhood...\")\nplot_interactive_dag(go_graph, \"GO:0006281\", radius=200)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T15:48:56.556533Z","iopub.execute_input":"2025-12-16T15:48:56.556873Z","iopub.status.idle":"2025-12-16T15:48:56.698082Z","shell.execute_reply.started":"2025-12-16T15:48:56.556820Z","shell.execute_reply":"2025-12-16T15:48:56.697425Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# --- Example 2: A Specific Molecular Function ---\n# \"ATP binding\" (GO:0005524)\nprint(\"Visualizing 'ATP binding' (GO:0034655) neighborhood...\")\nplot_interactive_dag(go_graph, \"GO:0034655\", radius=100)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T15:48:56.700260Z","iopub.execute_input":"2025-12-16T15:48:56.700710Z","iopub.status.idle":"2025-12-16T15:48:56.821536Z","shell.execute_reply.started":"2025-12-16T15:48:56.700688Z","shell.execute_reply":"2025-12-16T15:48:56.820843Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Protein Physicochemical Properties\n\n Extracting features from sequences: Molecular Weight, Instability, Aromaticity.\n","metadata":{"execution":{"iopub.status.busy":"2025-12-16T15:05:40.142762Z","iopub.execute_input":"2025-12-16T15:05:40.144039Z","iopub.status.idle":"2025-12-16T15:05:40.150430Z","shell.execute_reply.started":"2025-12-16T15:05:40.143995Z","shell.execute_reply":"2025-12-16T15:05:40.149121Z"}}},{"cell_type":"code","source":"\nprint(\"Parsing Sequences & Calculating Properties...\")\n\n# Load subset of taxonomy to link species\ndf_tax = pd.read_csv(CFG.FILES['train_tax'], sep='\\t', names=['EntryID', 'taxonomyID'])\ntarget_ids = set(df_tax['EntryID'].unique())\n\n# Subsample for speed if configured\nif CFG.SAMPLE_FRAC < 1.0:\n    target_ids = set(pd.Series(list(target_ids)).sample(frac=CFG.SAMPLE_FRAC, random_state=CFG.SEED))\n\nproperties = []\n\nfor record in SeqIO.parse(CFG.FILES['train_seq'], \"fasta\"):\n    clean_id = record.id.split('|')[1] if '|' in record.id else record.id\n    \n    if clean_id in target_ids:\n        seq_str = str(record.seq)\n        \n        # Skip sequences with non-standard amino acids (B, J, O, U, X, Z) for ProtParam\n        if not set(seq_str).issubset(set(\"ACDEFGHIKLMNPQRSTVWY\")):\n            continue\n            \n        try:\n            X = ProteinAnalysis(seq_str)\n            properties.append({\n                'EntryID': clean_id,\n                'Length': len(seq_str),\n                'MolWeight': X.molecular_weight(),\n                'Aromaticity': X.aromaticity(),\n                'Instability': X.instability_index(),\n                'Isoelectric': X.isoelectric_point()\n            })\n        except:\n            continue\n\ndf_props = pd.DataFrame(properties)\nprint(f\"Calculated properties for {len(df_props)} proteins.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T15:48:56.822368Z","iopub.execute_input":"2025-12-16T15:48:56.822690Z","iopub.status.idle":"2025-12-16T15:49:02.141722Z","shell.execute_reply.started":"2025-12-16T15:48:56.822663Z","shell.execute_reply":"2025-12-16T15:49:02.140946Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"##  Visualization: Physicochemical Distributions \nfig, axes = plt.subplots(2, 2, figsize=(16, 10))\n\nsns.histplot(data=df_props, x='Length', bins=50, log_scale=True, ax=axes[0,0], color='teal')\naxes[0,0].set_title(\"Sequence Length (Log Scale)\")\n\nsns.histplot(data=df_props, x='MolWeight', bins=50, log_scale=True, ax=axes[0,1], color='orange')\naxes[0,1].set_title(\"Molecular Weight (Log Scale)\")\n\nsns.histplot(data=df_props, x='Aromaticity', bins=50, ax=axes[1,0], color='purple')\naxes[1,0].set_title(\"Aromaticity (Relative Frequency of F, W, Y)\")\n\nsns.histplot(data=df_props, x='Instability', bins=50, ax=axes[1,1], color='crimson')\naxes[1,1].set_title(\"Instability Index (>40 is unstable)\")\naxes[1,1].axvline(40, color='black', linestyle='--')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T15:49:02.142654Z","iopub.execute_input":"2025-12-16T15:49:02.142962Z","iopub.status.idle":"2025-12-16T15:49:03.660019Z","shell.execute_reply.started":"2025-12-16T15:49:02.142936Z","shell.execute_reply":"2025-12-16T15:49:03.659016Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\nprint(\"Extracting Protein Properties...\")\ndf_tax = pd.read_csv(CFG.FILES['train_tax'], sep='\\t', names=['EntryID', 'taxonomyID'])\ntarget_ids = set(df_tax['EntryID'].unique())\n\nif CFG.SAMPLE_FRAC < 1.0:\n    target_ids = set(pd.Series(list(target_ids)).sample(frac=CFG.SAMPLE_FRAC, random_state=CFG.SEED))\n\nproperties = []\nfor record in SeqIO.parse(CFG.FILES['train_seq'], \"fasta\"):\n    clean_id = record.id.split('|')[1] if '|' in record.id else record.id\n    if clean_id in target_ids:\n        seq_str = str(record.seq)\n        if not set(seq_str).issubset(set(\"ACDEFGHIKLMNPQRSTVWY\")): continue\n        try:\n            X = ProteinAnalysis(seq_str)\n            properties.append({\n                'EntryID': clean_id,\n                'Length': len(seq_str),\n                'MolWeight': X.molecular_weight(),\n                'Instability': X.instability_index(),\n                'Aromaticity': X.aromaticity()\n            })\n        except: continue\n\ndf_props = pd.DataFrame(properties)\n\n# Plotly Interactive Histograms\nfig = px.histogram(df_props, x=\"Length\", nbins=100, log_y=True, \n                   title=\"Protein Sequence Length Distribution (Log Scale)\",\n                   color_discrete_sequence=['#636EFA'])\nfig.show()\n\nfig = px.scatter(df_props, x=\"MolWeight\", y=\"Instability\", \n                 title=\"Molecular Weight vs. Instability Index\",\n                 opacity=0.3, color_discrete_sequence=['#EF553B'])\nfig.add_hline(y=40, line_dash=\"dash\", line_color=\"black\", annotation_text=\"Unstable Threshold\")\nfig.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T15:49:03.660814Z","iopub.execute_input":"2025-12-16T15:49:03.661059Z","iopub.status.idle":"2025-12-16T15:49:11.288755Z","shell.execute_reply.started":"2025-12-16T15:49:03.661041Z","shell.execute_reply":"2025-12-16T15:49:11.287794Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Label Co-occurrence Matrix\n\nIf a protein has Function A, how likely is it to have Function B?\n\n","metadata":{}},{"cell_type":"code","source":"\nprint(\"Analyzing Label Co-occurrence...\")\ndf_terms = pd.read_csv(CFG.FILES['train_terms'], sep='\\t', names=['EntryID', 'term', 'aspect'])\n\n# Filter to proteins we analyzed\ndf_terms_sub = df_terms[df_terms['EntryID'].isin(df_props['EntryID'])]\n\n# Get Top 20 terms (excluding roots)\ntop_terms = df_terms_sub['term'].value_counts().head(25).index.tolist()\n# Remove roots if present\nroots_set = set(roots.values())\ntop_terms = [t for t in top_terms if t not in roots_set][:20]\n\n# Create Binary Matrix (One-Hot)\ndf_pivot = df_terms_sub[df_terms_sub['term'].isin(top_terms)].pivot_table(\n    index='EntryID', columns='term', aggfunc='size', fill_value=0\n)\ndf_pivot = (df_pivot > 0).astype(int)\n\n# Calculate Correlation (Jaccard-ish via Pearson phi coefficient)\ncorr_matrix = df_pivot.corr(method='pearson')\n\n# Map IDs to Names for plotting\nterm_names = {t: go_graph.nodes[t].get('name', t)[:15] for t in top_terms}\ncorr_matrix.index = corr_matrix.index.map(term_names)\ncorr_matrix.columns = corr_matrix.columns.map(term_names)\n\n# --- Visualization: Co-occurrence Heatmap ---\nplt.figure(figsize=(14, 12))\nsns.heatmap(corr_matrix, cmap='coolwarm', center=0, annot=False, square=True, linewidths=.5)\nplt.title(\"Correlation of Top 20 GO Terms\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T15:50:36.609158Z","iopub.execute_input":"2025-12-16T15:50:36.609484Z","iopub.status.idle":"2025-12-16T15:50:37.512445Z","shell.execute_reply.started":"2025-12-16T15:50:36.609461Z","shell.execute_reply":"2025-12-16T15:50:37.511645Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Taxonomic Clustering by Amino Acid Composition\n\nDo different species have fundamentally different protein compositions?\n","metadata":{}},{"cell_type":"code","source":"\nprint(\"Clustering Species by Amino Acid Composition...\")\n\n# Calculate AA frequencies per protein\naa_cols = list(\"ACDEFGHIKLMNPQRSTVWY\")\nfor aa in aa_cols:\n    df_props[aa] = 0.0\n\n# (Re-parsing efficiently for AA counts on the subset)\n# Note: In a full pipeline, do this in one pass. Separated here for clarity.\nseq_map = {rec.id.split('|')[1] if '|' in rec.id else rec.id : str(rec.seq) \n           for rec in SeqIO.parse(CFG.FILES['train_seq'], \"fasta\") \n           if (rec.id.split('|')[1] if '|' in rec.id else rec.id) in set(df_props['EntryID'])}\n\naa_data = []\nfor pid, seq in seq_map.items():\n    if not set(seq).issubset(set(aa_cols)): continue\n    counts = Counter(seq)\n    length = len(seq)\n    row = {'EntryID': pid}\n    row.update({aa: counts.get(aa, 0)/length for aa in aa_cols})\n    aa_data.append(row)\n\ndf_aa = pd.DataFrame(aa_data)\n\n# Merge with Taxonomy\ndf_tax_sub = df_tax[df_tax['EntryID'].isin(df_aa['EntryID'])]\ndf_aa_tax = df_aa.merge(df_tax_sub, on='EntryID')\n\n# Group by Taxon (Average AA composition per species)\n# Filter for species with at least 50 proteins to avoid noise\ntaxon_counts = df_aa_tax['taxonomyID'].value_counts()\nvalid_taxa = taxon_counts[taxon_counts > 50].index\ndf_species_profile = df_aa_tax[df_aa_tax['taxonomyID'].isin(valid_taxa)].groupby('taxonomyID')[aa_cols].mean()\n\n# Select top 30 species for visualization\ntop_30_taxa = taxon_counts.head(30).index\ndf_vis = df_species_profile.loc[df_species_profile.index.intersection(top_30_taxa)]\n\n# --- Visualization: Clustermap ---\ng = sns.clustermap(df_vis, cmap='viridis', standard_scale=1, figsize=(12, 10),\n                   row_cluster=True, col_cluster=True)\ng.fig.suptitle(\"Species Clustering by Amino Acid Composition (Top 30 Species)\", y=1.02)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T15:50:46.142465Z","iopub.execute_input":"2025-12-16T15:50:46.142838Z","iopub.status.idle":"2025-12-16T15:53:33.532655Z","shell.execute_reply.started":"2025-12-16T15:50:46.142810Z","shell.execute_reply":"2025-12-16T15:53:33.531806Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 7. Multi-Label Cardinality\n\nHow many labels does a single protein usually have?","metadata":{}},{"cell_type":"code","source":"label_counts = df_terms_sub.groupby('EntryID').size()\n\nplt.figure(figsize=(12, 6))\nsns.histplot(label_counts, bins=range(1, 50), kde=True, color='darkblue')\nplt.title(\"Distribution of Labels per Protein (Cardinality)\")\nplt.xlabel(\"Number of GO Terms\")\nplt.xlim(0, 50)\nplt.show()\n\nstats = label_counts.describe()\nprint(f\"Average labels per protein: {stats['mean']:.2f}\")\nprint(f\"Median labels per protein: {stats['50%']:.2f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T15:53:34.077926Z","iopub.execute_input":"2025-12-16T15:53:34.078193Z","iopub.status.idle":"2025-12-16T15:53:34.515977Z","shell.execute_reply.started":"2025-12-16T15:53:34.078165Z","shell.execute_reply":"2025-12-16T15:53:34.515216Z"}},"outputs":[],"execution_count":null}]}