{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "13f36fc3",
   "metadata": {},
   "source": [
    "# E2E — texto castellano → grafo en el notebook\n",
    "\n",
    "Validacion end-to-end del flujo del panel _Paste & Extract_ usando los thresholds calibrados en `01_gliner_glirel_tuning.ipynb`:\n",
    "\n",
    "- `entity_threshold = 0.50`\n",
    "- `relation_threshold = 0.15`\n",
    "- `relation_labels` en snake_case corto\n",
    "\n",
    "Pegamos un texto en castellano sobre el sector empresarial espanol, corremos el pipeline `extract_graph_hybrid`, y dibujamos el grafo resultante con `networkx + matplotlib`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e9efed4",
   "metadata": {},
   "source": [
    "## 1. Setup"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "06a2640a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:08:17.689631Z",
     "iopub.status.busy": "2026-05-04T13:08:17.689324Z",
     "iopub.status.idle": "2026-05-04T13:08:18.191052Z",
     "shell.execute_reply": "2026-05-04T13:08:18.190135Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "imports OK\n"
     ]
    }
   ],
   "source": [
    "import os, sys, json, warnings\n",
    "warnings.filterwarnings('ignore')\n",
    "os.environ.setdefault('HF_HUB_DISABLE_PROGRESS_BARS', '1')\n",
    "from pathlib import Path\n",
    "\n",
    "# Limpiar sys.path: el startup del kernel anade cada subdir de\n",
    "# python/functions/, y bigquery/datasets.py sombrea al paquete\n",
    "# `datasets` de HuggingFace. Dejamos solo el padre para imports\n",
    "# 'from datascience...' / 'from pipelines...' al estilo paquete.\n",
    "_pf = '/home/lucas/fn_registry/python/functions'\n",
    "sys.path = [p for p in sys.path if not p.startswith(_pf + '/')]\n",
    "if _pf not in sys.path:\n",
    "    sys.path.insert(0, _pf)\n",
    "\n",
    "import pandas as pd\n",
    "import networkx as nx\n",
    "import matplotlib.pyplot as plt\n",
    "from datascience.gliner_load_model import gliner_load_model\n",
    "from datascience.glirel_load_model import glirel_load_model\n",
    "from pipelines.extract_graph_hybrid import extract_graph_hybrid\n",
    "print('imports OK')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18a2d8e3",
   "metadata": {},
   "source": [
    "## 2. Texto de entrada (castellano)\n",
    "\n",
    "Tres parrafos sobre el sector empresarial espanol — directivos, sedes, acuerdos — con entidades de tres tipos (Person, Organization, Location) y relaciones evidentes (presidencias, sedes, acuerdos)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "6bd111ba",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:08:18.193022Z",
     "iopub.status.busy": "2026-05-04T13:08:18.192752Z",
     "iopub.status.idle": "2026-05-04T13:08:18.195991Z",
     "shell.execute_reply": "2026-05-04T13:08:18.195212Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Pablo Isla, expresidente de Inditex, ha sido nombrado consejero de Telefonica. La operacion fue anunciada por el presidente Jose Maria Alvarez-Pallete en Madrid el pasado lunes. Inditex factura mas de 30.000 millones anuales y tiene su sede en Arteixo, A Coruna.\n",
      "\n",
      "En paralelo, Iberdrola y Endesa firmaron un acuerdo de colaboracion en proyectos eolicos en Galicia. El presidente de Iberdrola, Ignacio Galan, se reunio con la CEO de Endesa, Marina Serrano, en Bilbao. El acuerdo movilizara 2.000 millones de euros en cinco anos.\n",
      "\n",
      "El BBVA, presidido por Carlos Torres, mostro interes en participar en la financiacion del proyecto. Su sede central esta en Bilbao.\n",
      "\n",
      "longitud: 660 chars  ~104 tokens\n"
     ]
    }
   ],
   "source": [
    "TEXTO = 'Pablo Isla, expresidente de Inditex, ha sido nombrado consejero de Telefonica. La operacion fue anunciada por el presidente Jose Maria Alvarez-Pallete en Madrid el pasado lunes. Inditex factura mas de 30.000 millones anuales y tiene su sede en Arteixo, A Coruna.\\n\\nEn paralelo, Iberdrola y Endesa firmaron un acuerdo de colaboracion en proyectos eolicos en Galicia. El presidente de Iberdrola, Ignacio Galan, se reunio con la CEO de Endesa, Marina Serrano, en Bilbao. El acuerdo movilizara 2.000 millones de euros en cinco anos.\\n\\nEl BBVA, presidido por Carlos Torres, mostro interes en participar en la financiacion del proyecto. Su sede central esta en Bilbao.'\n",
    "print(TEXTO)\n",
    "print()\n",
    "print(f'longitud: {len(TEXTO)} chars  ~{len(TEXTO.split())} tokens')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d722244",
   "metadata": {},
   "source": [
    "## 3. Carga de modelos\n",
    "\n",
    "Warm load (~8s cada uno) — modelos cacheados en `~/.cache/huggingface/`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "079b3779",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:08:18.197387Z",
     "iopub.status.busy": "2026-05-04T13:08:18.197264Z",
     "iopub.status.idle": "2026-05-04T13:08:33.688972Z",
     "shell.execute_reply": "2026-05-04T13:08:33.687750Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[0;93m2026-05-04 15:08:19.251860362 [W:onnxruntime:Default, device_discovery.cc:283 GetGpuDevices] Failed to detect devices under \"/sys/class/drm/card0\": device_discovery.cc:93 ReadFileContents Failed to open file: \"/sys/class/drm/card0/device/vendor\"\u001b[m\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "GLiNER 7.9s\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[1mDebertaV2Model LOAD REPORT\u001b[0m from: microsoft/deberta-v3-large\n",
      "Key                                     | Status     |  | \n",
      "----------------------------------------+------------+--+-\n",
      "lm_predictions.lm_head.dense.weight     | UNEXPECTED |  | \n",
      "mask_predictions.dense.bias             | UNEXPECTED |  | \n",
      "lm_predictions.lm_head.LayerNorm.weight | UNEXPECTED |  | \n",
      "mask_predictions.LayerNorm.weight       | UNEXPECTED |  | \n",
      "mask_predictions.dense.weight           | UNEXPECTED |  | \n",
      "lm_predictions.lm_head.LayerNorm.bias   | UNEXPECTED |  | \n",
      "lm_predictions.lm_head.dense.bias       | UNEXPECTED |  | \n",
      "mask_predictions.classifier.weight      | UNEXPECTED |  | \n",
      "lm_predictions.lm_head.bias             | UNEXPECTED |  | \n",
      "mask_predictions.classifier.bias        | UNEXPECTED |  | \n",
      "mask_predictions.LayerNorm.bias         | UNEXPECTED |  | \n",
      "\n",
      "\u001b[3mNotes:\n",
      "- UNEXPECTED\u001b[3m\t:can be ignored when loading from different task/architecture; not ok if you expect identical arch.\u001b[0m\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "GLiREL 7.6s\n"
     ]
    }
   ],
   "source": [
    "import time\n",
    "t0 = time.time(); gliner = gliner_load_model(); print(f'GLiNER {time.time()-t0:.1f}s')\n",
    "t0 = time.time(); glirel = glirel_load_model(); print(f'GLiREL {time.time()-t0:.1f}s')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b2729f88",
   "metadata": {},
   "source": [
    "## 4. Pipeline `extract_graph_hybrid` — dos pasadas\n",
    "\n",
    "El threshold del notebook 01 (`0.15`) se calibro mirando la _distribucion_ de scores (max ~0.21 en EN, ~0.17 en ES). Pero **GLiREL evalua TODOS los pares ordenados de entidades para CADA label**: con 15 entidades y 8 labels son 15×14×8 = 1680 candidatos. Aunque pocos pasan, los que pasan son una mezcla de aciertos y plausibles-pero-falsos.\n",
    "\n",
    "Vamos a hacer **dos pasadas** sobre el mismo texto: `0.15` (recall, ruidoso) y `0.30` (precision, limpio). El notebook 01 solo midio scores agregados — esta celda completa la calibracion mirando _calidad semantica_ del output."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "f673c21e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:08:33.691463Z",
     "iopub.status.busy": "2026-05-04T13:08:33.690930Z",
     "iopub.status.idle": "2026-05-04T13:08:34.233862Z",
     "shell.execute_reply": "2026-05-04T13:08:34.232871Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "recall    (t=0.15): 15 ents  51 rels\n",
      "precision (t=0.30): 15 ents   1 rels\n"
     ]
    }
   ],
   "source": [
    "entity_schema = [\n",
    "    {'type_ref': 'Person',       'label': 'person'},\n",
    "    {'type_ref': 'Organization', 'label': 'organization'},\n",
    "    {'type_ref': 'Location',     'label': 'location'},\n",
    "]\n",
    "relation_types = [\n",
    "    'works_at', 'located_in', 'appointed_as', 'headquartered_in',\n",
    "    'ceo_of', 'president_of', 'agreement_with', 'met_with',\n",
    "]\n",
    "\n",
    "def run(threshold):\n",
    "    return extract_graph_hybrid(\n",
    "        chunks=[TEXTO],\n",
    "        entity_schema=entity_schema,\n",
    "        relation_types=relation_types,\n",
    "        gliner_model=gliner,\n",
    "        glirel_model=glirel,\n",
    "        llm_chat_json=None,\n",
    "        confidence_threshold=threshold,\n",
    "    )\n",
    "\n",
    "ents_recall,    rels_recall    = run(0.15)\n",
    "ents_precision, rels_precision = run(0.30)\n",
    "print(f'recall    (t=0.15): {len(ents_recall):2d} ents  {len(rels_recall):2d} rels')\n",
    "print(f'precision (t=0.30): {len(ents_precision):2d} ents  {len(rels_precision):2d} rels')\n",
    "\n",
    "# Trabajamos a partir de aqui con 'precision' como base\n",
    "ents, rels = ents_precision, rels_precision"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8d7906d2",
   "metadata": {},
   "source": [
    "### 4.1 Tabla de entidades"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "886ef097",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:08:34.235957Z",
     "iopub.status.busy": "2026-05-04T13:08:34.235690Z",
     "iopub.status.idle": "2026-05-04T13:08:34.253381Z",
     "shell.execute_reply": "2026-05-04T13:08:34.252557Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>name</th>\n",
       "      <th>type</th>\n",
       "      <th>confidence</th>\n",
       "      <th>chunks</th>\n",
       "      <th>merged_from</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>Bilbao</td>\n",
       "      <td>Location</td>\n",
       "      <td>0.981</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Arteixo</td>\n",
       "      <td>Location</td>\n",
       "      <td>0.963</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Madrid</td>\n",
       "      <td>Location</td>\n",
       "      <td>0.950</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>A Coruna</td>\n",
       "      <td>Location</td>\n",
       "      <td>0.918</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>Galicia</td>\n",
       "      <td>Location</td>\n",
       "      <td>0.726</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Inditex</td>\n",
       "      <td>Organization</td>\n",
       "      <td>0.979</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>Iberdrola</td>\n",
       "      <td>Organization</td>\n",
       "      <td>0.979</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Telefonica</td>\n",
       "      <td>Organization</td>\n",
       "      <td>0.969</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>BBVA</td>\n",
       "      <td>Organization</td>\n",
       "      <td>0.969</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>Endesa</td>\n",
       "      <td>Organization</td>\n",
       "      <td>0.968</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Pablo Isla</td>\n",
       "      <td>Person</td>\n",
       "      <td>0.979</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>Ignacio Galan</td>\n",
       "      <td>Person</td>\n",
       "      <td>0.972</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>Marina Serrano</td>\n",
       "      <td>Person</td>\n",
       "      <td>0.957</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>Carlos Torres</td>\n",
       "      <td>Person</td>\n",
       "      <td>0.956</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Jose Maria Alvarez-Pallete</td>\n",
       "      <td>Person</td>\n",
       "      <td>0.925</td>\n",
       "      <td>[0]</td>\n",
       "      <td>[]</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                          name          type  confidence chunks merged_from\n",
       "12                      Bilbao      Location       0.981    [0]          []\n",
       "5                      Arteixo      Location       0.963    [0]          []\n",
       "4                       Madrid      Location       0.950    [0]          []\n",
       "6                     A Coruna      Location       0.918    [0]          []\n",
       "9                      Galicia      Location       0.726    [0]          []\n",
       "1                      Inditex  Organization       0.979    [0]          []\n",
       "7                    Iberdrola  Organization       0.979    [0]          []\n",
       "2                   Telefonica  Organization       0.969    [0]          []\n",
       "13                        BBVA  Organization       0.969    [0]          []\n",
       "8                       Endesa  Organization       0.968    [0]          []\n",
       "0                   Pablo Isla        Person       0.979    [0]          []\n",
       "10               Ignacio Galan        Person       0.972    [0]          []\n",
       "11              Marina Serrano        Person       0.957    [0]          []\n",
       "14               Carlos Torres        Person       0.956    [0]          []\n",
       "3   Jose Maria Alvarez-Pallete        Person       0.925    [0]          []"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df_ents = pd.DataFrame([\n",
    "    {'name': e.name, 'type': e.type_ref, 'confidence': round(e.confidence, 3),\n",
    "     'chunks': e.source_chunk_indices, 'merged_from': e.merged_from}\n",
    "    for e in ents\n",
    "]).sort_values(['type','confidence'], ascending=[True, False])\n",
    "df_ents"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "859e2a5d",
   "metadata": {},
   "source": [
    "### 4.2 Tabla de relaciones"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "95787f04",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:08:34.255364Z",
     "iopub.status.busy": "2026-05-04T13:08:34.255151Z",
     "iopub.status.idle": "2026-05-04T13:08:34.263590Z",
     "shell.execute_reply": "2026-05-04T13:08:34.262614Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>from</th>\n",
       "      <th>kind</th>\n",
       "      <th>to</th>\n",
       "      <th>confidence</th>\n",
       "      <th>chunk</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Ignacio Galan</td>\n",
       "      <td>president_of</td>\n",
       "      <td>Jose Maria Alvarez-Pallete</td>\n",
       "      <td>0.339</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "            from          kind                          to  confidence  chunk\n",
       "0  Ignacio Galan  president_of  Jose Maria Alvarez-Pallete       0.339      0"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df_rels = pd.DataFrame([\n",
    "    {'from': r.from_name, 'kind': r.relation_type, 'to': r.to_name,\n",
    "     'confidence': round(r.confidence, 3), 'chunk': r.source_chunk_index}\n",
    "    for r in rels\n",
    "]).sort_values('confidence', ascending=False)\n",
    "df_rels"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "45b13cdb",
   "metadata": {},
   "source": [
    "## 5. Visualizacion comparativa — recall vs precision\n",
    "\n",
    "Dos grafos, mismo texto, distinto threshold. Nodos coloreados por tipo, aristas etiquetadas con `relation_type`, layout fuerza-dirigido. Es el mismo render que el panel del `graph_explorer` haria tras un _Apply selected_, pero aqui en linea para validar visualmente la calibracion."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "e19a59bd",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:08:34.265474Z",
     "iopub.status.busy": "2026-05-04T13:08:34.265312Z",
     "iopub.status.idle": "2026-05-04T13:08:34.854478Z",
     "shell.execute_reply": "2026-05-04T13:08:34.853517Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 345836 characters]",
      "text/plain": [
       "<Figure size 2000x900 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "TYPE_COLOR = {'Person': '#5DA5DA', 'Organization': '#F17CB0', 'Location': '#60BD68'}\n",
    "\n",
    "def draw(ax, ents, rels, title):\n",
    "    G = nx.DiGraph()\n",
    "    for e in ents:\n",
    "        G.add_node(e.name, type=e.type_ref, confidence=e.confidence)\n",
    "    for r in rels:\n",
    "        G.add_edge(r.from_name, r.to_name, kind=r.relation_type, confidence=r.confidence)\n",
    "    pos = nx.spring_layout(G, k=2.2, iterations=80, seed=42)\n",
    "    node_colors = [TYPE_COLOR.get(G.nodes[n].get('type'), '#bbb') for n in G.nodes]\n",
    "    nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_size=1900,\n",
    "                           edgecolors='#333', linewidths=1.4, ax=ax)\n",
    "    nx.draw_networkx_labels(G, pos, font_size=8, font_weight='bold', ax=ax)\n",
    "    nx.draw_networkx_edges(G, pos, edge_color='#888', arrows=True, arrowsize=14,\n",
    "                           width=1.2, alpha=0.65, ax=ax,\n",
    "                           connectionstyle='arc3,rad=0.08')\n",
    "    edge_labels = {(u, v): d['kind'] for u, v, d in G.edges(data=True)}\n",
    "    nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_size=6.5,\n",
    "                                 font_color='#333', label_pos=0.5,\n",
    "                                 bbox=dict(boxstyle='round,pad=0.1', fc='white', ec='none', alpha=0.8),\n",
    "                                 ax=ax)\n",
    "    ax.set_title(f'{title}: {G.number_of_nodes()} ents, {G.number_of_edges()} rels', fontsize=11)\n",
    "    ax.axis('off')\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(20, 9))\n",
    "draw(axes[0], ents_recall,    rels_recall,    't=0.15 (recall)')\n",
    "draw(axes[1], ents_precision, rels_precision, 't=0.30 (precision)')\n",
    "from matplotlib.patches import Patch\n",
    "legend = [Patch(facecolor=c, edgecolor='#333', label=t) for t, c in TYPE_COLOR.items()]\n",
    "axes[0].legend(handles=legend, loc='upper left', frameon=True, fontsize=10)\n",
    "plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6da687bb",
   "metadata": {},
   "source": [
    "### 5.1 Solo el grafo de precision (vista limpia)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "fcc36f47",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:08:34.856358Z",
     "iopub.status.busy": "2026-05-04T13:08:34.856197Z",
     "iopub.status.idle": "2026-05-04T13:08:34.970293Z",
     "shell.execute_reply": "2026-05-04T13:08:34.969413Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 105488 characters]",
      "text/plain": [
       "<Figure size 1300x900 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, ax = plt.subplots(figsize=(13, 9))\n",
    "draw(ax, ents_precision, rels_precision, 'Grafo final (t=0.30)')\n",
    "from matplotlib.patches import Patch\n",
    "legend = [Patch(facecolor=c, edgecolor='#333', label=t) for t, c in TYPE_COLOR.items()]\n",
    "ax.legend(handles=legend, loc='upper left', frameon=True, fontsize=10)\n",
    "plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7d6e9738",
   "metadata": {},
   "source": [
    "## 6. Lectura empirica — el hallazgo incomodo\n",
    "\n",
    "**GLiNER funciona muy bien:** 15 entidades nucleo con confianza 0.92-0.98, en castellano, con labels en ingles. Sin asteriscos.\n",
    "\n",
    "**GLiREL no funciona bien en este dominio.** No es un problema de threshold — es de fondo:\n",
    "\n",
    "### Falsos positivos con score alto a `t=0.15`\n",
    "\n",
    "Con 51 relaciones emitidas, la mayoria son espurias. Ejemplos reales del output:\n",
    "\n",
    "| Score | from | kind | to | Realidad |\n",
    "|---|---|---|---|---|\n",
    "| 0.339 | Ignacio Galan | president_of | Jose Maria Alvarez-Pallete | **Falso.** Galan preside Iberdrola; Alvarez-Pallete preside Telefonica. No tienen relacion entre si. |\n",
    "| 0.292 | Carlos Torres | president_of | Jose Maria Alvarez-Pallete | **Falso.** Torres preside BBVA. |\n",
    "| 0.253 | Madrid | president_of | Jose Maria Alvarez-Pallete | **Sin sentido.** Una `Location` no preside a una `Person`. |\n",
    "| 0.218 | Madrid | located_in | Inditex | **Invertido.** Inditex esta en Arteixo, no Madrid esta en Inditex. |\n",
    "\n",
    "### Y al subir el threshold no mejora\n",
    "\n",
    "A `t=0.30` (precision mode), solo sobrevive **1 relacion**: la primera de la tabla — que **tambien es falsa**. GLiREL ha aprendido que dos `Person` cerca de la palabra _presidente_ disparan `president_of` con confianza alta, sin importar la sintaxis ni la direccion.\n",
    "\n",
    "### Por que pasa esto\n",
    "\n",
    "1. **GLiREL evalua todos los pares ordenados × cada label.** Con 15 ents y 8 labels son 15×14×8 = **1680 candidatos**. Incluso con error <1% por candidato, el output a threshold permisivo es ruidoso.\n",
    "2. **El modelo es atencional, no logico.** Aprende patrones de coocurrencia, no semantica. Por eso `Madrid president_of Persona` recibe score positivo cuando ambos aparecen cerca del verbo.\n",
    "3. **`jackboyla/glirel-large-v0` esta entrenado mayoritariamente en ingles.** El gap EN/ES del notebook 01 (max 0.23 vs 0.17) es la punta del iceberg — la calidad semantica tambien cae.\n",
    "\n",
    "### Que toca cambiar en el pipeline\n",
    "\n",
    "1. **No usar GLiREL como decisor final** en castellano. Usarlo como _candidate generator_ y validar con LLM. El pipeline `extract_graph_hybrid` ya admite `llm_chat_json` para fallback de entidades — habria que extender el flujo a las relaciones (issue nuevo).\n",
    "2. **Si no hay LLM disponible**, mejor emitir solo top-N por score (ej: top-3 relaciones globales) que filtrar por threshold global. El panel deja al humano elegir.\n",
    "3. **El issue `0041-split-confidence-thresholds`** sigue siendo valido (separar entity y relation thresholds), pero ahora sabemos que el problema mas grave **NO es el threshold sino la calidad del modelo en este dominio**.\n",
    "4. **Para OSINT/narrativa en EN**, GLiREL podria funcionar mejor (notebook 01 mostro scores ~25% mas altos en EN). No probado aqui.\n",
    "\n",
    "### Decision provisional para el panel `paste_extract`\n",
    "\n",
    "- **GLiNER (entidades): habilitado por defecto.** Funciona muy bien.\n",
    "- **GLiREL (relaciones): deshabilitado por defecto en castellano** o, alternativamente, mostrar siempre con un banner explicando que las relaciones son sugerencias y deben validarse antes de _Apply_.\n",
    "- **Issue nuevo:** integrar LLM como validator semantico de candidatos GLiREL antes de mostrar al usuario.\n",
    "\n",
    "**Para iterar sobre tu propio texto:** edita la celda 5 (`TEXTO = ...`) y re-ejecuta desde la celda 7. Los modelos quedan cacheados en RAM."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.13.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
