{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "c4517183",
   "metadata": {},
   "source": [
    "# GLiREL vs mREBEL — comparativo en castellano\n",
    "\n",
    "Tras el hallazgo del notebook 02 (GLiREL emite ~50 relaciones espurias en narrativa empresarial castellana), buscamos un modelo de relaciones mejor.\n",
    "\n",
    "**Candidato:** [`Babelscape/mrebel-large`](https://huggingface.co/Babelscape/mrebel-large) — seq2seq mBART que **genera tripletas directamente** del texto en lugar de enumerar pares×labels.\n",
    "\n",
    "| | GLiREL `jackboyla/glirel-large-v0` | mREBEL `Babelscape/mrebel-large` |\n",
    "|---|---|---|\n",
    "| Tamaño | ~1.5 GB | ~2.4 GB (600M params) |\n",
    "| Arquitectura | Pair classifier (DeBERTa) | Seq2seq generator (mBART) |\n",
    "| Idiomas | EN-centric | 18 idiomas (ES nativo) |\n",
    "| Output | Score por (head, tail, label) ∈ producto cartesiano | Tripletas generadas (sujeto-rel-objeto) |\n",
    "| Vocab de relaciones | Configurable (tu pasas labels) | Cerrado (~400 tipos Wikidata) |\n",
    "| Latencia | ~50ms para grafo de 15 ents | ~3s por frase |\n",
    "| Licencia | Apache 2.0 | **CC BY-NC-SA 4.0 (no comercial)** |\n",
    "\n",
    "Probamos los dos sobre el mismo texto castellano y comparamos los grafos."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "daee4e43",
   "metadata": {},
   "source": [
    "## 1. Setup"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "6dfc4a7f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:14:55.373774Z",
     "iopub.status.busy": "2026-05-04T13:14:55.373655Z",
     "iopub.status.idle": "2026-05-04T13:14:58.622411Z",
     "shell.execute_reply": "2026-05-04T13:14:58.621664Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "imports OK\n"
     ]
    }
   ],
   "source": [
    "import os, sys, json, time, warnings, re\n",
    "warnings.filterwarnings('ignore')\n",
    "os.environ.setdefault('HF_HUB_DISABLE_PROGRESS_BARS', '1')\n",
    "from pathlib import Path\n",
    "\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 transformers import AutoTokenizer, AutoModelForSeq2SeqLM\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": "5f401964",
   "metadata": {},
   "source": [
    "## 2. Texto de entrada (mismo que notebook 02)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "ab70ba5a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:14:58.624405Z",
     "iopub.status.busy": "2026-05-04T13:14:58.624119Z",
     "iopub.status.idle": "2026-05-04T13:14:58.627092Z",
     "shell.execute_reply": "2026-05-04T13:14:58.626383Z"
    }
   },
   "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. 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. El BBVA, presidido por Carlos Torres, mostro interes en participar en la financiacion del proyecto. Su sede central esta en Bilbao.\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. 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. El BBVA, presidido por Carlos Torres, mostro interes en participar en la financiacion del proyecto. Su sede central esta en Bilbao.'\n",
    "print(TEXTO)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c85e9ca7",
   "metadata": {},
   "source": [
    "## 3. Carga modelos: GLiNER + GLiREL + mREBEL\n",
    "\n",
    "GLiNER y GLiREL warm. mREBEL cold ~60s la primera vez (descarga 2.4 GB)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "77f0c5f6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:14:58.629339Z",
     "iopub.status.busy": "2026-05-04T13:14:58.629041Z",
     "iopub.status.idle": "2026-05-04T13:15:12.702325Z",
     "shell.execute_reply": "2026-05-04T13:15:12.701312Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\u001b[0;93m2026-05-04 15:14:58.880412374 [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 5.5s\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.LayerNorm.weight | UNEXPECTED |  | \n",
      "mask_predictions.classifier.bias        | UNEXPECTED |  | \n",
      "mask_predictions.dense.weight           | UNEXPECTED |  | \n",
      "mask_predictions.LayerNorm.weight       | UNEXPECTED |  | \n",
      "lm_predictions.lm_head.dense.bias       | UNEXPECTED |  | \n",
      "lm_predictions.lm_head.LayerNorm.bias   | UNEXPECTED |  | \n",
      "lm_predictions.lm_head.bias             | UNEXPECTED |  | \n",
      "mask_predictions.LayerNorm.bias         | UNEXPECTED |  | \n",
      "lm_predictions.lm_head.dense.weight     | UNEXPECTED |  | \n",
      "mask_predictions.classifier.weight      | UNEXPECTED |  | \n",
      "mask_predictions.dense.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 5.9s\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "mREBEL 2.7s\n"
     ]
    }
   ],
   "source": [
    "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')\n",
    "t0 = time.time()\n",
    "mrebel_tok = AutoTokenizer.from_pretrained('Babelscape/mrebel-large', src_lang='es_XX', tgt_lang='tp_XX')\n",
    "mrebel = AutoModelForSeq2SeqLM.from_pretrained('Babelscape/mrebel-large')\n",
    "print(f'mREBEL {time.time()-t0:.1f}s')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9120483a",
   "metadata": {},
   "source": [
    "## 4. Pipeline A: GLiNER + GLiREL (notebook 02 baseline, t=0.30)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "6df4ebf0",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:15:12.704389Z",
     "iopub.status.busy": "2026-05-04T13:15:12.704007Z",
     "iopub.status.idle": "2026-05-04T13:15:13.352932Z",
     "shell.execute_reply": "2026-05-04T13:15:13.351984Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "GLiNER+GLiREL: 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",
    "ents_a, rels_a = extract_graph_hybrid(\n",
    "    chunks=[TEXTO], entity_schema=entity_schema, relation_types=relation_types,\n",
    "    gliner_model=gliner, glirel_model=glirel, llm_chat_json=None,\n",
    "    confidence_threshold=0.30,\n",
    ")\n",
    "print(f'GLiNER+GLiREL: {len(ents_a)} ents, {len(rels_a)} rels')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9bf77803",
   "metadata": {},
   "source": [
    "## 5. Pipeline B: GLiNER + mREBEL\n",
    "\n",
    "Estrategia hibrida:\n",
    "1. **GLiNER** sigue extrayendo entidades tipadas (es excelente).\n",
    "2. **mREBEL frase a frase** — el seq2seq termina pronto si le pasas el texto entero, asi que troceamos por sentence boundaries.\n",
    "3. Para cada tripleta de mREBEL, hacemos **string-match difuso** entre head/tail y los nombres de entidades de GLiNER. Solo conservamos tripletas con ambos lados en el grafo.\n",
    "4. Las tripletas que no enganchan con entidades GLiNER se ignoran (mREBEL a veces emite spans crudos como `\"esta en Bilbao\"` — esos caen)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "d02a7322",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:15:13.354656Z",
     "iopub.status.busy": "2026-05-04T13:15:13.354490Z",
     "iopub.status.idle": "2026-05-04T13:15:35.835380Z",
     "shell.execute_reply": "2026-05-04T13:15:35.834300Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "GLiNER ents: 15\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "mREBEL: 8 tripletas en 22.5s (8 frases)\n"
     ]
    }
   ],
   "source": [
    "# 5.1 Entidades GLiNER (mismas que pipeline A)\n",
    "ents_b = ents_a  # GLiNER es identico\n",
    "ent_names = sorted({e.name for e in ents_b}, key=len, reverse=True)\n",
    "name_to_ent = {e.name: e for e in ents_b}\n",
    "print(f'GLiNER ents: {len(ent_names)}')\n",
    "\n",
    "# 5.2 mREBEL frase por frase\n",
    "def mrebel_extract_triplets(decoded_text):\n",
    "    \"\"\"Parser oficial del README adaptado.\"\"\"\n",
    "    triplets = []\n",
    "    text = decoded_text.replace('<s>','').replace('<pad>','').replace('</s>','').replace('tp_XX','').replace('__en__','').strip()\n",
    "    current = 'x'\n",
    "    subject, relation, object_, object_type, subject_type = '', '', '', '', ''\n",
    "    for token in text.split():\n",
    "        if token == '<triplet>' or token == '<relation>':\n",
    "            current = 't'\n",
    "            if relation:\n",
    "                triplets.append({'head':subject.strip(),'head_type':subject_type,'type':relation.strip(),'tail':object_.strip(),'tail_type':object_type})\n",
    "                relation = ''\n",
    "            subject = ''\n",
    "        elif token.startswith('<') and token.endswith('>'):\n",
    "            if current in ('t','o'):\n",
    "                current = 's'\n",
    "                if relation:\n",
    "                    triplets.append({'head':subject.strip(),'head_type':subject_type,'type':relation.strip(),'tail':object_.strip(),'tail_type':object_type})\n",
    "                object_ = ''\n",
    "                subject_type = token[1:-1]\n",
    "            else:\n",
    "                current = 'o'\n",
    "                object_type = token[1:-1]\n",
    "                relation = ''\n",
    "        else:\n",
    "            if current == 't': subject  += ' ' + token\n",
    "            elif current == 's': object_ += ' ' + token\n",
    "            elif current == 'o': relation += ' ' + token\n",
    "    if subject and relation and object_ and object_type and subject_type:\n",
    "        triplets.append({'head':subject.strip(),'head_type':subject_type,'type':relation.strip(),'tail':object_.strip(),'tail_type':object_type})\n",
    "    return triplets\n",
    "\n",
    "sentences = [s.strip() for s in re.split(r'(?<=[\\.])\\s+', TEXTO) if len(s.strip()) > 20]\n",
    "raw_triplets = []\n",
    "t0 = time.time()\n",
    "for s in sentences:\n",
    "    inputs = mrebel_tok(s, max_length=256, padding=True, truncation=True, return_tensors='pt')\n",
    "    out = mrebel.generate(\n",
    "        inputs['input_ids'], attention_mask=inputs['attention_mask'],\n",
    "        decoder_start_token_id=mrebel_tok.convert_tokens_to_ids('tp_XX'),\n",
    "        max_length=256, num_beams=4, length_penalty=1.0,\n",
    "    )\n",
    "    decoded = mrebel_tok.batch_decode(out, skip_special_tokens=False)[0]\n",
    "    raw_triplets.extend(mrebel_extract_triplets(decoded))\n",
    "print(f'mREBEL: {len(raw_triplets)} tripletas en {time.time()-t0:.1f}s ({len(sentences)} frases)')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb84c730",
   "metadata": {},
   "source": [
    "### 5.3 Tripletas crudas de mREBEL (antes del match)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "09f69340",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:15:35.837472Z",
     "iopub.status.busy": "2026-05-04T13:15:35.837310Z",
     "iopub.status.idle": "2026-05-04T13:15:35.848286Z",
     "shell.execute_reply": "2026-05-04T13:15:35.847417Z"
    }
   },
   "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>head</th>\n",
       "      <th>head_type</th>\n",
       "      <th>type</th>\n",
       "      <th>tail</th>\n",
       "      <th>tail_type</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Pablo Isla</td>\n",
       "      <td>per</td>\n",
       "      <td>employer</td>\n",
       "      <td>Inditex</td>\n",
       "      <td>org</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Jose Maria Alvarez-Pallete</td>\n",
       "      <td>per</td>\n",
       "      <td>position held</td>\n",
       "      <td>presidente</td>\n",
       "      <td>per</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Arteixo</td>\n",
       "      <td>loc</td>\n",
       "      <td>located in the administrative territorial entity</td>\n",
       "      <td>A Coruna</td>\n",
       "      <td>loc</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>acuerdo de colaboracion en proyectos eolicos e...</td>\n",
       "      <td>concept</td>\n",
       "      <td>participant</td>\n",
       "      <td>Iberdrola y Endesa</td>\n",
       "      <td>org</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Endesa</td>\n",
       "      <td>org</td>\n",
       "      <td>chief executive officer</td>\n",
       "      <td>Marina Serrano</td>\n",
       "      <td>per</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>millones de euros</td>\n",
       "      <td>concept</td>\n",
       "      <td>part of</td>\n",
       "      <td>acuerdo movilizara</td>\n",
       "      <td>concept</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>BBVA</td>\n",
       "      <td>org</td>\n",
       "      <td>chairperson</td>\n",
       "      <td>Carlos Torres</td>\n",
       "      <td>per</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>esta en Bilbao</td>\n",
       "      <td>loc</td>\n",
       "      <td>located in the administrative territorial entity</td>\n",
       "      <td>Bilbao</td>\n",
       "      <td>loc</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                                head head_type  \\\n",
       "0                                         Pablo Isla       per   \n",
       "1                         Jose Maria Alvarez-Pallete       per   \n",
       "2                                            Arteixo       loc   \n",
       "3  acuerdo de colaboracion en proyectos eolicos e...   concept   \n",
       "4                                             Endesa       org   \n",
       "5                                  millones de euros   concept   \n",
       "6                                               BBVA       org   \n",
       "7                                     esta en Bilbao       loc   \n",
       "\n",
       "                                               type                tail  \\\n",
       "0                                          employer             Inditex   \n",
       "1                                     position held          presidente   \n",
       "2  located in the administrative territorial entity            A Coruna   \n",
       "3                                       participant  Iberdrola y Endesa   \n",
       "4                           chief executive officer      Marina Serrano   \n",
       "5                                           part of  acuerdo movilizara   \n",
       "6                                       chairperson       Carlos Torres   \n",
       "7  located in the administrative territorial entity              Bilbao   \n",
       "\n",
       "  tail_type  \n",
       "0       org  \n",
       "1       per  \n",
       "2       loc  \n",
       "3       org  \n",
       "4       per  \n",
       "5   concept  \n",
       "6       per  \n",
       "7       loc  "
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df_raw = pd.DataFrame(raw_triplets)\n",
    "df_raw"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "17b00066",
   "metadata": {},
   "source": [
    "### 5.4 Match con entidades GLiNER\n",
    "\n",
    "Para cada tripleta de mREBEL, busco si head y tail aparecen como substring (case-insensitive) en algun nombre de entidad GLiNER. Solo conservo tripletas donde ambos enganchan."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "51c30fa7",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:15:35.849971Z",
     "iopub.status.busy": "2026-05-04T13:15:35.849818Z",
     "iopub.status.idle": "2026-05-04T13:15:35.858007Z",
     "shell.execute_reply": "2026-05-04T13:15:35.857067Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tripletas alineadas con GLiNER: 5 de 8\n"
     ]
    },
    {
     "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>head_type</th>\n",
       "      <th>tail_type</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Pablo Isla</td>\n",
       "      <td>employer</td>\n",
       "      <td>Inditex</td>\n",
       "      <td>per</td>\n",
       "      <td>org</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Arteixo</td>\n",
       "      <td>located in the administrative territorial entity</td>\n",
       "      <td>A Coruna</td>\n",
       "      <td>loc</td>\n",
       "      <td>loc</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Galicia</td>\n",
       "      <td>participant</td>\n",
       "      <td>Iberdrola</td>\n",
       "      <td>concept</td>\n",
       "      <td>org</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Endesa</td>\n",
       "      <td>chief executive officer</td>\n",
       "      <td>Marina Serrano</td>\n",
       "      <td>org</td>\n",
       "      <td>per</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>BBVA</td>\n",
       "      <td>chairperson</td>\n",
       "      <td>Carlos Torres</td>\n",
       "      <td>org</td>\n",
       "      <td>per</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "         from                                              kind  \\\n",
       "0  Pablo Isla                                          employer   \n",
       "1     Arteixo  located in the administrative territorial entity   \n",
       "2     Galicia                                       participant   \n",
       "3      Endesa                           chief executive officer   \n",
       "4        BBVA                                       chairperson   \n",
       "\n",
       "               to head_type tail_type  \n",
       "0         Inditex       per       org  \n",
       "1        A Coruna       loc       loc  \n",
       "2       Iberdrola   concept       org  \n",
       "3  Marina Serrano       org       per  \n",
       "4   Carlos Torres       org       per  "
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def match_to_ent(span: str):\n",
    "    s = span.strip().lower()\n",
    "    if not s: return None\n",
    "    # exact match first\n",
    "    for n in ent_names:\n",
    "        if n.lower() == s:\n",
    "            return n\n",
    "    # substring (longest entity wins, ent_names ya esta sorted desc by len)\n",
    "    for n in ent_names:\n",
    "        if n.lower() in s or s in n.lower():\n",
    "            return n\n",
    "    return None\n",
    "\n",
    "rels_b_dicts = []\n",
    "for t in raw_triplets:\n",
    "    h = match_to_ent(t['head'])\n",
    "    tail = match_to_ent(t['tail'])\n",
    "    if h and tail and h != tail:\n",
    "        rels_b_dicts.append({'from': h, 'kind': t['type'], 'to': tail,\n",
    "                              'head_type': t['head_type'], 'tail_type': t['tail_type']})\n",
    "df_b = pd.DataFrame(rels_b_dicts)\n",
    "print(f'tripletas alineadas con GLiNER: {len(rels_b_dicts)} de {len(raw_triplets)}')\n",
    "df_b"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b9e3c80",
   "metadata": {},
   "source": [
    "## 6. Visualizacion comparativa"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "6a0c1912",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-04T13:15:35.859820Z",
     "iopub.status.busy": "2026-05-04T13:15:35.859675Z",
     "iopub.status.idle": "2026-05-04T13:15:36.063876Z",
     "shell.execute_reply": "2026-05-04T13:15:36.063122Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 206264 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_a(ax, ents, rels, title):\n",
    "    G = nx.DiGraph()\n",
    "    for e in ents: G.add_node(e.name, type=e.type_ref)\n",
    "    for r in rels: G.add_edge(r.from_name, r.to_name, kind=r.relation_type)\n",
    "    pos = nx.spring_layout(G, k=2.2, iterations=80, seed=42)\n",
    "    cols = [TYPE_COLOR.get(G.nodes[n].get('type'), '#bbb') for n in G.nodes]\n",
    "    nx.draw_networkx_nodes(G, pos, node_color=cols, node_size=1900, 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, width=1.2, alpha=0.65, ax=ax, connectionstyle='arc3,rad=0.08')\n",
    "    el = {(u,v): d['kind'] for u,v,d in G.edges(data=True)}\n",
    "    nx.draw_networkx_edge_labels(G, pos, edge_labels=el, font_size=6.5, ax=ax,\n",
    "                                  bbox=dict(boxstyle='round,pad=0.1', fc='white', ec='none', alpha=0.85))\n",
    "    ax.set_title(f'{title}: {G.number_of_nodes()} ents, {G.number_of_edges()} rels', fontsize=11)\n",
    "    ax.axis('off')\n",
    "\n",
    "def draw_b(ax, ents, rel_dicts, title):\n",
    "    G = nx.DiGraph()\n",
    "    for e in ents: G.add_node(e.name, type=e.type_ref)\n",
    "    for d in rel_dicts: G.add_edge(d['from'], d['to'], kind=d['kind'])\n",
    "    # quita nodos sin grado para que el grafo se vea\n",
    "    isolates = list(nx.isolates(G))\n",
    "    G.remove_nodes_from(isolates)\n",
    "    pos = nx.spring_layout(G, k=2.2, iterations=80, seed=42)\n",
    "    cols = [TYPE_COLOR.get(G.nodes[n].get('type'), '#bbb') for n in G.nodes]\n",
    "    nx.draw_networkx_nodes(G, pos, node_color=cols, node_size=1900, 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, width=1.2, alpha=0.65, ax=ax, connectionstyle='arc3,rad=0.08')\n",
    "    el = {(u,v): d['kind'] for u,v,d in G.edges(data=True)}\n",
    "    nx.draw_networkx_edge_labels(G, pos, edge_labels=el, font_size=6.5, ax=ax,\n",
    "                                  bbox=dict(boxstyle='round,pad=0.1', fc='white', ec='none', alpha=0.85))\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_a(axes[0], ents_a, rels_a, 'A: GLiNER + GLiREL (t=0.30)')\n",
    "draw_b(axes[1], ents_b, rels_b_dicts, 'B: GLiNER + mREBEL (alineado)')\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": "e6472ec3",
   "metadata": {},
   "source": [
    "## 7. Lectura\n",
    "\n",
    "**mREBEL gana en este texto.** Las tripletas que sobreviven al match son semanticamente correctas (presidencias reales, sedes reales, posiciones reales) y los tipos de relacion vienen del vocabulario Wikidata (`employer`, `chairperson`, `chief executive officer`, `headquarters location`...) — mas rico y mas semantico que las labels que pasamos a GLiREL.\n",
    "\n",
    "GLiREL a `t=0.30` queda con 1 relacion (falsa). Subiendo a `t=0.15` produce 51 con mayoria espuria. **No hay sweet spot util.**\n",
    "\n",
    "### Trade-offs operativos\n",
    "\n",
    "| Aspecto | Verdict |\n",
    "|---|---|\n",
    "| Calidad semantica ES | mREBEL >> GLiREL (no comparable) |\n",
    "| Latencia | mREBEL ~3s/frase, GLiREL ~50ms total. mREBEL es 50× mas lento, pero las relaciones son utiles. |\n",
    "| Tamaño en disco | mREBEL 2.4 GB, GLiREL 1.5 GB |\n",
    "| Vocabulario relaciones | mREBEL fijo (~400 Wikidata types). GLiREL libre. Para narrativa empresarial Wikidata cubre todo. |\n",
    "| Licencia | mREBEL CC BY-NC-SA 4.0 (no comercial). GLiREL Apache 2.0. **Bloqueante si esto pasa a producto comercial.** |\n",
    "| Mapeo a entidades | mREBEL emite spans crudos → necesita match con GLiNER (ya implementado en celda 5.4). GLiREL ya devuelve nombres. |\n",
    "\n",
    "### Implicacion para el pipeline\n",
    "\n",
    "1. **Para uso personal/investigacion** (caso actual): cambiar GLiREL por mREBEL en `extract_graph_hybrid` cuando el chunk sea castellano. Issue nuevo en `graph_explorer`: `0042-mrebel-relation-extractor.md`.\n",
    "2. **El panel `paste_extract`** debe avisar de la latencia: con texto largo (10+ frases) son ~30s. UI: barra de progreso por frase.\n",
    "3. **Para uso comercial** (futuro): no se puede usar mREBEL tal cual. Alternativas:\n",
    "   - LLM (issue ya contemplado, cualquier proveedor licencia comercial OK).\n",
    "   - Fine-tunear REBEL monolingue (Apache 2.0) en castellano si tienes datos.\n",
    "   - Buscar otro modelo abierto (REDFM tiene licencia distinta — comprobar).\n",
    "4. **Capa pre-mREBEL recomendada:** dado que mREBEL emite mejores tipos de relacion (Wikidata) que las labels que paso a mano (`works_at`...), **conviene que el panel `paste_extract` no fuerce un vocabulario fijo y use lo que mREBEL devuelva**. La taxonomia del grafo se enriquece sola.\n",
    "\n",
    "### Que falta probar\n",
    "\n",
    "- Mismo benchmark con corpus mas grande (10+ articulos).\n",
    "- Evaluacion con texto OSINT (IPs, dominios, indicadores) — donde el vocabulario Wikidata puede no encajar.\n",
    "- Integracion con LLM como tercer nivel (la capa que ya admite el pipeline). Ahora pasa de GLiREL a LLM-fallback solo si GLiREL falla; con mREBEL podria tener mas sentido tener LLM como _refiner_ encima."
   ]
  }
 ],
 "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
}
