#!/usr/bin/env python3
"""
Generador del notebook Oviedo_Alexander_Proyecto_S3_PNL.ipynb
Actividad 3 — NRC-8774 Procesamiento Natural del Lenguaje — Semana 3
Grupo 7 | Docente: Nathalia Orozco Morales
"""
import json
import os

# ─────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────
def md_cell(source_lines, cell_id=None):
    cell = {
        "cell_type": "markdown",
        "metadata": {},
        "source": source_lines,
    }
    if cell_id:
        cell["id"] = cell_id
    return cell

def code_cell(source_lines, cell_id=None):
    cell = {
        "cell_type": "code",
        "metadata": {},
        "execution_count": None,
        "outputs": [],
        "source": source_lines,
    }
    if cell_id:
        cell["id"] = cell_id
    return cell

# ─────────────────────────────────────────────
# CELDA 0 — Encabezado / Portada
# ─────────────────────────────────────────────
celda_0 = md_cell([
    "# 📝 Proyecto — Procesamiento Natural del Lenguaje (Semana 3)\n",
    "## Tokenización, Limpieza, NER y Análisis de Frecuencias\n",
    "\n",
    "| Campo | Detalle |\n",
    "|-------|--------|\n",
    "| **Programa** | Especialización en Inteligencia Artificial |\n",
    "| **Institución** | Corporación Universitaria Minuto de Dios (UNIMINUTO) |\n",
    "| **Curso** | NRC-8774 — Procesamiento Natural del Lenguaje |\n",
    "| **Docente** | Nathalia Orozco Morales |\n",
    "| **Grupo** | 7 |\n",
    "| **Integrantes** | Alexander Oviedo Fadul, Maria Fernanda Ruiz Paipilla, Neheman Samir Jaller Cerchiaro, William David Obando Lopez |\n",
    "| **Fecha** | Julio 2026 |\n",
    "\n",
    "---\n",
    "\n",
    "### Objetivo\n",
    "\n",
    "Aplicar un pipeline completo de Procesamiento de Lenguaje Natural (PLN) sobre texto extraído de la página de Wikipedia de *Cien años de soledad*, utilizando las librerías **NLTK** y **spaCy**. El pipeline incluye extracción web, limpieza, tokenización, eliminación de *stop words*, lematización/stemming, análisis de frecuencias, visualizaciones (word cloud, gráficas) y reconocimiento de entidades nombradas (NER).\n",
    "\n",
    "### Preguntas orientadoras\n",
    "\n",
    "1. **¿Cuáles son las librerías más utilizadas para el procesamiento de lenguaje natural?**\n",
    "\n",
    "   Las librerías más utilizadas en Python para PLN son **NLTK** (*Natural Language Toolkit*), que resulta ideal para tareas clásicas como tokenización, stemming y acceso a corpus lingüísticos; **spaCy**, que ofrece pipelines optimizados para producción con modelos preentrenados en múltiples idiomas y soporte nativo para NER, POS tagging y análisis de dependencias; y la familia de **Transformers** de Hugging Face, que proporciona acceso a modelos como BERT, RoBERTa y GPT para tareas avanzadas de clasificación, generación y comprensión de texto. Cada una tiene fortalezas distintas: NLTK destaca en investigación y enseñanza, spaCy en eficiencia y escalabilidad, y Transformers en precisión para tareas complejas (Moreira et al., 2021).\n",
    "\n",
    "2. **¿Cómo es el proceso de tokenizar un texto y cómo influyen las Stop Words?**\n",
    "\n",
    "   La tokenización consiste en dividir un texto en unidades mínimas llamadas *tokens*, que generalmente corresponden a palabras individuales, signos de puntuación o subpalabras. Es el primer paso del pipeline de PLN porque transforma el texto continuo en una estructura que los algoritmos pueden procesar. Las *stop words* (palabras vacías como \"el\", \"de\", \"en\", \"y\") son tokens de alta frecuencia que aportan poca información semántica al análisis. Su eliminación permite que las técnicas posteriores —conteo de frecuencias, vectorización, clasificación— se concentren en las palabras que realmente describen el contenido. Sin embargo, en tareas como análisis de sentimiento o traducción automática, eliminar stop words puede ser contraproducente, ya que la estructura gramatical importa (Srinivasa, 2018).\n",
], cell_id="portada")

# ─────────────────────────────────────────────
# CELDA 1 — Instalación y setup
# ─────────────────────────────────────────────
celda_1_md = md_cell([
    "## 1. Instalación y configuración del entorno\n",
    "\n",
    "Instalamos las dependencias necesarias: NLTK para tokenización clásica, spaCy para NER y POS tagging, BeautifulSoup para extracción web, y wordcloud/matplotlib para visualizaciones.\n",
], cell_id="sec1_md")

celda_1_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 1. Instalación de dependencias\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "!pip install -q spacy nltk beautifulsoup4 requests wordcloud matplotlib pandas\n",
    "!python -m spacy download es_core_news_sm -q\n",
    "\n",
    "import nltk\n",
    "nltk.download('punkt', quiet=True)\n",
    "nltk.download('punkt_tab', quiet=True)\n",
    "nltk.download('stopwords', quiet=True)\n",
    "nltk.download('wordnet', quiet=True)\n",
    "nltk.download('omw-1.4', quiet=True)\n",
    "nltk.download('averaged_perceptron_tagger', quiet=True)\n",
    "nltk.download('averaged_perceptron_tagger_eng', quiet=True)\n",
    "\n",
    "print('✅ Todas las dependencias instaladas correctamente')\n",
], cell_id="sec1_code")

# ─────────────────────────────────────────────
# CELDA 2 — Importaciones
# ─────────────────────────────────────────────
celda_2_md = md_cell([
    "## 2. Importación de librerías\n",
    "\n",
    "Organizamos las importaciones por categoría: procesamiento web, NLTK, spaCy y visualización.\n",
], cell_id="sec2_md")

celda_2_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 2. Importación de librerías\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "import re\n",
    "import warnings\n",
    "warnings.filterwarnings('ignore')\n",
    "\n",
    "# --- Web scraping ---\n",
    "import requests\n",
    "from bs4 import BeautifulSoup\n",
    "\n",
    "# --- NLTK ---\n",
    "from nltk.tokenize import word_tokenize, sent_tokenize\n",
    "from nltk.corpus import stopwords, wordnet\n",
    "from nltk.stem import SnowballStemmer\n",
    "from nltk.probability import FreqDist\n",
    "\n",
    "# --- spaCy ---\n",
    "import spacy\n",
    "from spacy import displacy\n",
    "from collections import Counter\n",
    "\n",
    "# --- Visualización ---\n",
    "import matplotlib.pyplot as plt\n",
    "from wordcloud import WordCloud\n",
    "import pandas as pd\n",
    "\n",
    "# Cargar modelos\n",
    "nlp = spacy.load('es_core_news_sm')\n",
    "stemmer = SnowballStemmer('spanish')\n",
    "stop_words_es = set(stopwords.words('spanish'))\n",
    "\n",
    "print(f'spaCy modelo: {nlp.meta[\"name\"]} v{nlp.meta[\"version\"]}')\n",
    "print(f'Pipeline spaCy: {nlp.pipe_names}')\n",
    "print(f'Stop words en español cargadas: {len(stop_words_es)}')\n",
    "print('✅ Librerías importadas correctamente')\n",
], cell_id="sec2_code")

# ─────────────────────────────────────────────
# CELDA 3 — Extracción de texto desde Wikipedia
# ─────────────────────────────────────────────
celda_3_md = md_cell([
    "## 3. Extracción de texto desde Wikipedia\n",
    "\n",
    "Utilizamos **BeautifulSoup** para extraer el contenido del artículo de Wikipedia sobre *Cien años de soledad* de Gabriel García Márquez. Como se indicó en clase (Orozco, 2026), elegimos esta página porque no requiere autenticación, está en español y tiene suficiente contenido para un análisis significativo. El proceso de extracción implica eliminar etiquetas HTML no relevantes (scripts, tablas, figuras, botones) y conservar únicamente los párrafos con contenido sustantivo.\n",
], cell_id="sec3_md")

celda_3_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 3. Extracción de texto desde Wikipedia\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "tema = 'Cien_años_de_soledad'\n",
    "url = f'https://es.wikipedia.org/wiki/{tema}'\n",
    "\n",
    "print(f'📥 Descargando: {url}')\n",
    "response = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'})\n",
    "print(f'   Estado HTTP: {response.status_code}')\n",
    "\n",
    "soup = BeautifulSoup(response.text, 'html.parser')\n",
    "\n",
    "# Eliminar etiquetas innecesarias\n",
    "for tag in soup(['script', 'style', 'noscript', 'header', 'footer',\n",
    "                 'nav', 'aside', 'form', 'iframe', 'figure',\n",
    "                 'button', 'table', 'sup', 'img']):\n",
    "    tag.decompose()\n",
    "\n",
    "# Extraer texto principal del contenido\n",
    "contenido = soup.find('div', {'id': 'mw-content-text'})\n",
    "if contenido:\n",
    "    parrafos = contenido.find_all('p')\n",
    "else:\n",
    "    parrafos = soup.find_all('p')\n",
    "\n",
    "# Unir párrafos con contenido significativo (> 60 caracteres)\n",
    "texto_crudo = ' '.join(\n",
    "    p.get_text(separator=' ', strip=True)\n",
    "    for p in parrafos\n",
    "    if len(p.get_text(strip=True)) > 60\n",
    ")\n",
    "\n",
    "print(f'\\n📄 Texto extraído: {len(texto_crudo)} caracteres')\n",
    "print(f'   Párrafos procesados: {len([p for p in parrafos if len(p.get_text(strip=True)) > 60])}')\n",
    "print(f'\\n--- Vista previa (primeros 600 caracteres) ---')\n",
    "print(texto_crudo[:600])\n",
], cell_id="sec3_code")

# ─────────────────────────────────────────────
# CELDA 4 — Limpieza del texto
# ─────────────────────────────────────────────
celda_4_md = md_cell([
    "## 4. Limpieza del texto\n",
    "\n",
    "La limpieza es, según lo discutido en clase, el paso **más crítico** del pipeline. Un texto con ruido (URLs, emojis, paréntesis con notas aclaratorias, referencias numéricas) produce errores significativos en las etapas posteriores, especialmente en NER (Orozco, 2026). Aplicamos una serie de transformaciones con expresiones regulares para normalizar el texto.\n",
], cell_id="sec4_md")

celda_4_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 4. Limpieza del texto\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "def limpiar_texto(texto):\n",
    "    \"\"\"Pipeline de limpieza para texto extraído de páginas web.\"\"\"\n",
    "    # 1. Eliminar contenido entre paréntesis (notas, refs de Wikipedia)\n",
    "    texto = re.sub(r'\\([^)]*\\)', '', texto)\n",
    "    # 2. Eliminar URLs\n",
    "    texto = re.sub(r'https?://\\S+', '', texto)\n",
    "    texto = re.sub(r'www\\.\\S+', '', texto)\n",
    "    # 3. Eliminar emojis y caracteres Unicode especiales\n",
    "    texto = re.sub(r'[\\U00010000-\\U0010ffff]', '', texto)\n",
    "    # 4. Eliminar números sueltos (años y refs)\n",
    "    texto = re.sub(r'\\b\\d+\\b', '', texto)\n",
    "    # 5. Conservar solo letras, puntuación básica y tildes\n",
    "    texto = re.sub(r'[^\\w\\s\\.,;:¿?¡!áéíóúÁÉÍÓÚüÜñÑ]', ' ', texto)\n",
    "    # 6. Colapsar espacios múltiples\n",
    "    texto = re.sub(r'\\s+', ' ', texto)\n",
    "    return texto.strip()\n",
    "\n",
    "texto_limpio = limpiar_texto(texto_crudo)\n",
    "\n",
    "print('🧹 Comparación antes y después de la limpieza:')\n",
    "print(f'   Caracteres originales: {len(texto_crudo):,}')\n",
    "print(f'   Caracteres limpios:    {len(texto_limpio):,}')\n",
    "print(f'   Reducción:             {(1 - len(texto_limpio)/len(texto_crudo))*100:.1f}%')\n",
    "print(f'\\n--- Texto limpio (primeros 500 caracteres) ---')\n",
    "print(texto_limpio[:500])\n",
], cell_id="sec4_code")

# ─────────────────────────────────────────────
# CELDA 5 — Tokenización con NLTK
# ─────────────────────────────────────────────
celda_5_md = md_cell([
    "## 5. Tokenización con NLTK\n",
    "\n",
    "La tokenización descompone el texto en unidades mínimas (tokens). Usamos `word_tokenize` de NLTK, que maneja correctamente la puntuación y las contracciones del español. Este paso transforma el texto continuo en una lista de elementos procesables (Campesato, 2021).\n",
], cell_id="sec5_md")

celda_5_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 5. Tokenización con NLTK\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "tokens = word_tokenize(texto_limpio, language='spanish')\n",
    "\n",
    "print(f'🔤 Tokenización completada')\n",
    "print(f'   Total de tokens: {len(tokens):,}')\n",
    "print(f'   Tokens únicos:   {len(set(tokens)):,}')\n",
    "print(f'\\n--- Primeros 30 tokens ---')\n",
    "print(tokens[:30])\n",
    "\n",
    "# Distribución de frecuencias (antes de filtrar)\n",
    "freq_dist_original = FreqDist(tokens)\n",
    "print(f'\\n--- 15 tokens más frecuentes (sin filtrar) ---')\n",
    "for palabra, freq in freq_dist_original.most_common(15):\n",
    "    print(f'   {palabra:20} → {freq}')\n",
], cell_id="sec5_code")

# ─────────────────────────────────────────────
# CELDA 6 — Eliminación de signos de puntuación
# ─────────────────────────────────────────────
celda_6_md = md_cell([
    "## 6. Eliminación de signos de puntuación\n",
    "\n",
    "Filtramos los tokens que son exclusivamente signos de puntuación o caracteres no alfabéticos. Esto reduce el ruido y permite que el análisis de frecuencias refleje el contenido real del texto.\n",
], cell_id="sec6_md")

celda_6_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 6. Eliminación de signos de puntuación\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "tokens_sin_punt = [t for t in tokens if t.isalpha()]\n",
    "\n",
    "print(f'✂️  Tokens antes de filtrar puntuación: {len(tokens):,}')\n",
    "print(f'   Tokens después:                     {len(tokens_sin_punt):,}')\n",
    "print(f'   Tokens eliminados:                  {len(tokens) - len(tokens_sin_punt):,}')\n",
    "\n",
    "# Ejemplo de tokens eliminados\n",
    "tokens_punct = [t for t in tokens if not t.isalpha()]\n",
    "print(f'\\n--- Ejemplos de tokens eliminados (puntuación) ---')\n",
    "print(list(set(tokens_punct))[:20])\n",
], cell_id="sec6_code")

# ─────────────────────────────────────────────
# CELDA 7 — Eliminación de Stop Words
# ─────────────────────────────────────────────
celda_7_md = md_cell([
    "## 7. Eliminación de Stop Words\n",
    "\n",
    "Las *stop words* son palabras de alta frecuencia que no aportan significado semántico relevante (artículos, preposiciones, conjunciones). NLTK proporciona una lista predefinida para el español. Como señala Srinivasa (2018), eliminar estas palabras permite que los algoritmos de análisis se concentren en los términos que realmente caracterizan el contenido.\n",
], cell_id="sec7_md")

celda_7_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 7. Eliminación de Stop Words\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# Convertir a minúsculas y filtrar stop words\n",
    "tokens_lower = [t.lower() for t in tokens_sin_punt]\n",
    "tokens_filtrados = [t for t in tokens_lower if t not in stop_words_es]\n",
    "\n",
    "print(f'🚫 Eliminación de Stop Words')\n",
    "print(f'   Tokens antes:  {len(tokens_lower):,}')\n",
    "print(f'   Tokens después: {len(tokens_filtrados):,}')\n",
    "print(f'   Stop words eliminadas: {len(tokens_lower) - len(tokens_filtrados):,}')\n",
    "\n",
    "# Mostrar algunas stop words encontradas\n",
    "sw_encontradas = [t for t in tokens_lower if t in stop_words_es]\n",
    "sw_freq = FreqDist(sw_encontradas)\n",
    "print(f'\\n--- Top 10 stop words más frecuentes en el texto ---')\n",
    "for palabra, freq in sw_freq.most_common(10):\n",
    "    print(f'   {palabra:15} → {freq}')\n",
    "\n",
    "# Nueva distribución de frecuencias\n",
    "freq_dist_filtrada = FreqDist(tokens_filtrados)\n",
    "print(f'\\n--- 15 palabras más frecuentes (sin stop words) ---')\n",
    "for palabra, freq in freq_dist_filtrada.most_common(15):\n",
    "    print(f'   {palabra:20} → {freq}')\n",
], cell_id="sec7_code")

# ─────────────────────────────────────────────
# CELDA 8 — Visualización de frecuencias + Word Cloud
# ─────────────────────────────────────────────
celda_8_md = md_cell([
    "## 8. Análisis de frecuencias y visualización\n",
    "\n",
    "Representamos visualmente los resultados del análisis de frecuencias mediante una gráfica de barras y una nube de palabras (*word cloud*). Estas visualizaciones permiten identificar rápidamente los temas principales del texto.\n",
], cell_id="sec8_md")

celda_8_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 8. Visualización de frecuencias y Word Cloud\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "fig, axes = plt.subplots(1, 2, figsize=(18, 6))\n",
    "\n",
    "# --- Gráfica de barras: Top 20 palabras ---\n",
    "top_20 = freq_dist_filtrada.most_common(20)\n",
    "palabras = [p for p, f in top_20]\n",
    "frecuencias = [f for p, f in top_20]\n",
    "\n",
    "axes[0].barh(palabras[::-1], frecuencias[::-1], color='#2196F3', edgecolor='#1565C0')\n",
    "axes[0].set_xlabel('Frecuencia', fontsize=12)\n",
    "axes[0].set_title('Top 20 palabras más frecuentes\\n(sin stop words ni puntuación)', fontsize=13, fontweight='bold')\n",
    "axes[0].tick_params(axis='y', labelsize=10)\n",
    "\n",
    "# --- Word Cloud ---\n",
    "texto_wc = ' '.join(tokens_filtrados)\n",
    "wc = WordCloud(\n",
    "    width=800, height=400,\n",
    "    background_color='white',\n",
    "    colormap='viridis',\n",
    "    max_words=100,\n",
    "    contour_width=1,\n",
    "    contour_color='steelblue'\n",
    ").generate(texto_wc)\n",
    "\n",
    "axes[1].imshow(wc, interpolation='bilinear')\n",
    "axes[1].axis('off')\n",
    "axes[1].set_title('Nube de palabras — Cien años de soledad', fontsize=13, fontweight='bold')\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.savefig('frecuencias_wordcloud.png', dpi=150, bbox_inches='tight')\n",
    "plt.show()\n",
    "print('✅ Gráfica guardada como frecuencias_wordcloud.png')\n",
], cell_id="sec8_code")

# ─────────────────────────────────────────────
# CELDA 9 — Stemming y Lematización
# ─────────────────────────────────────────────
celda_9_md = md_cell([
    "## 9. Stemming y Lematización\n",
    "\n",
    "El **stemming** reduce las palabras a su raíz eliminando sufijos de forma algorítmica (por ejemplo, \"novelista\" → \"novel\"), mientras que la **lematización** busca la forma base real de la palabra en un diccionario (\"novelista\" → \"novelista\" como sustantivo). Comparamos ambos enfoques: stemming con `SnowballStemmer` de NLTK y lematización con spaCy (Campesato, 2021).\n",
], cell_id="sec9_md")

celda_9_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 9. Stemming y Lematización\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "\n",
    "# --- Stemming con NLTK ---\n",
    "stems = [stemmer.stem(t) for t in tokens_filtrados]\n",
    "\n",
    "# --- Lematización con spaCy ---\n",
    "texto_para_lemas = ' '.join(tokens_filtrados[:500])  # Limitar para eficiencia\n",
    "doc_lemas = nlp(texto_para_lemas)\n",
    "lemas = [token.lemma_ for token in doc_lemas if token.is_alpha]\n",
    "\n",
    "# Tabla comparativa\n",
    "print('📊 Comparación Stemming vs Lematización (muestra de 15 palabras):')\n",
    "print(f'{\"Palabra\":<20} {\"Stem (NLTK)\":<20} {\"Lema (spaCy)\":<20}')\n",
    "print('-' * 60)\n",
    "\n",
    "muestra = tokens_filtrados[:15]\n",
    "doc_muestra = nlp(' '.join(muestra))\n",
    "lemas_muestra = [t.lemma_ for t in doc_muestra if t.is_alpha]\n",
    "\n",
    "for i, palabra in enumerate(muestra):\n",
    "    stem = stemmer.stem(palabra)\n",
    "    lema = lemas_muestra[i] if i < len(lemas_muestra) else '-'\n",
    "    print(f'{palabra:<20} {stem:<20} {lema:<20}')\n",
    "\n",
    "# Estadísticas\n",
    "print(f'\\n📈 Estadísticas:')\n",
    "print(f'   Tokens únicos originales:  {len(set(tokens_filtrados)):,}')\n",
    "print(f'   Stems únicos:              {len(set(stems)):,}')\n",
    "print(f'   Lemas únicos:              {len(set(lemas)):,}')\n",
    "print(f'   Reducción por stemming:    {(1 - len(set(stems))/len(set(tokens_filtrados)))*100:.1f}%')\n",
    "print(f'   Reducción por lematización: {(1 - len(set(lemas))/len(set(tokens_filtrados[:500])))*100:.1f}%')\n",
], cell_id="sec9_code")

# ─────────────────────────────────────────────
# CELDA 10 — Sinónimos con WordNet
# ─────────────────────────────────────────────
celda_10_md = md_cell([
    "## 10. Sinónimos con WordNet\n",
    "\n",
    "**WordNet** es una base de datos léxica que organiza las palabras en conjuntos de sinónimos (*synsets*). Utilizamos NLTK para explorar los sinónimos de palabras clave encontradas en el texto de *Cien años de soledad*. Dado que WordNet tiene mayor cobertura en inglés, buscamos equivalentes cuando sea posible y mostramos las definiciones disponibles.\n",
], cell_id="sec10_md")

celda_10_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 10. Sinónimos con WordNet\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "\n",
    "# Seleccionar palabras clave del texto (las más frecuentes y significativas)\n",
    "palabras_clave = ['novela', 'familia', 'pueblo', 'soledad', 'historia',\n",
    "                   'realismo', 'autor', 'personaje', 'mundo', 'obra']\n",
    "\n",
    "print('📖 Sinónimos desde WordNet para palabras clave del texto:\\n')\n",
    "for palabra in palabras_clave:\n",
    "    synsets_spa = wordnet.synsets(palabra, lang='spa')\n",
    "    synsets_eng = wordnet.synsets(palabra)  # Buscar también en inglés\n",
    "\n",
    "    print(f'🔹 \"{palabra}\":')\n",
    "\n",
    "    if synsets_spa:\n",
    "        for ss in synsets_spa[:2]:  # Máximo 2 synsets\n",
    "            lemas = ss.lemma_names('spa')\n",
    "            definicion = ss.definition()\n",
    "            print(f'   Synset: {ss.name()}')\n",
    "            print(f'   Sinónimos (ES): {lemas}')\n",
    "            print(f'   Definición: {definicion}')\n",
    "    elif synsets_eng:\n",
    "        ss = synsets_eng[0]\n",
    "        lemas_eng = ss.lemma_names()\n",
    "        definicion = ss.definition()\n",
    "        print(f'   Synset: {ss.name()}')\n",
    "        print(f'   Sinónimos (EN): {lemas_eng[:5]}')\n",
    "        print(f'   Definición: {definicion}')\n",
    "    else:\n",
    "        print('   No se encontraron synsets.')\n",
    "    print()\n",
], cell_id="sec10_code")

# ─────────────────────────────────────────────
# CELDA 11 — POS Tagging con spaCy
# ─────────────────────────────────────────────
celda_11_md = md_cell([
    "## 11. Etiquetado POS (Part-of-Speech) con spaCy\n",
    "\n",
    "El etiquetado POS (*Part-of-Speech tagging*) asigna a cada token su categoría gramatical: sustantivo, verbo, adjetivo, adverbio, etc. spaCy realiza este proceso automáticamente mediante su pipeline con el modelo `es_core_news_sm`, que fue entrenado con el corpus AnCora del español (Campesato, 2021).\n",
], cell_id="sec11_md")

celda_11_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 11. POS Tagging con spaCy\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# Procesar un fragmento representativo\n",
    "doc_pos = nlp(texto_limpio[:3000])\n",
    "\n",
    "# Mostrar tokens con sus etiquetas POS\n",
    "print('🏷️  Muestra de tokens con etiquetas POS (primeros 25):\\n')\n",
    "print(f'{\"Token\":<20} {\"POS\":<8} {\"POS detallado\":<12} {\"Lema\":<20}')\n",
    "print('-' * 60)\n",
    "for token in list(doc_pos)[:25]:\n",
    "    if token.is_alpha:\n",
    "        print(f'{token.text:<20} {token.pos_:<8} {token.tag_:<12} {token.lemma_:<20}')\n",
    "\n",
    "# Distribución de categorías POS\n",
    "pos_counts = Counter([token.pos_ for token in doc_pos if token.is_alpha])\n",
    "pos_df = pd.DataFrame(pos_counts.most_common(), columns=['Categoría POS', 'Frecuencia'])\n",
    "\n",
    "print(f'\\n📊 Distribución de categorías gramaticales:')\n",
    "print(pos_df.to_string(index=False))\n",
    "\n",
    "# Gráfica de distribución POS\n",
    "fig, ax = plt.subplots(figsize=(10, 5))\n",
    "colors = plt.cm.Set3(range(len(pos_df)))\n",
    "ax.bar(pos_df['Categoría POS'], pos_df['Frecuencia'], color=colors, edgecolor='gray')\n",
    "ax.set_xlabel('Categoría Gramatical', fontsize=12)\n",
    "ax.set_ylabel('Frecuencia', fontsize=12)\n",
    "ax.set_title('Distribución de categorías POS en el texto', fontsize=13, fontweight='bold')\n",
    "plt.xticks(rotation=45, ha='right')\n",
    "plt.tight_layout()\n",
    "plt.savefig('distribucion_pos.png', dpi=150, bbox_inches='tight')\n",
    "plt.show()\n",
    "print('✅ Gráfica POS guardada como distribucion_pos.png')\n",
], cell_id="sec11_code")

# ─────────────────────────────────────────────
# CELDA 12 — NER con spaCy
# ─────────────────────────────────────────────
celda_12_md = md_cell([
    "## 12. Reconocimiento de Entidades Nombradas (NER)\n",
    "\n",
    "El NER identifica y clasifica entidades como personas (PER), organizaciones (ORG), lugares (LOC) y misceláneas (MISC) dentro del texto. Utilizamos el modelo `es_core_news_sm` de spaCy y visualizamos los resultados con `displacy`. Como se discutió en clase, el preprocesamiento previo mejora significativamente la precisión del NER al eliminar artefactos que confunden al modelo (Orozco, 2026).\n",
], cell_id="sec12_md")

celda_12_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 12. NER con spaCy\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "doc_ner = nlp(texto_limpio[:5000])\n",
    "\n",
    "print(f'🔍 Entidades encontradas: {len(doc_ner.ents)}\\n')\n",
    "\n",
    "# Tabla de entidades\n",
    "print(f'{\"Entidad\":<40} {\"Tipo\":<8} {\"Descripción\"}')\n",
    "print('-' * 80)\n",
    "for ent in doc_ner.ents:\n",
    "    descripcion = spacy.explain(ent.label_) or ''\n",
    "    print(f'{ent.text:<40} {ent.label_:<8} {descripcion}')\n",
    "\n",
    "# Resumen por tipo de entidad\n",
    "ent_types = Counter([ent.label_ for ent in doc_ner.ents])\n",
    "print(f'\\n📊 Resumen por tipo de entidad:')\n",
    "ent_df = pd.DataFrame(ent_types.most_common(), columns=['Tipo', 'Cantidad'])\n",
    "print(ent_df.to_string(index=False))\n",
], cell_id="sec12_code")

# ─────────────────────────────────────────────
# CELDA 12b — Visualización NER con displacy
# ─────────────────────────────────────────────
celda_12b_md = md_cell([
    "### 12.1 Visualización NER con displacy\n",
    "\n",
    "displacy es la herramienta de visualización integrada en spaCy. Resalta las entidades directamente en el texto con colores según su categoría, lo que permite una revisión visual rápida de la calidad del NER.\n",
], cell_id="sec12b_md")

celda_12b_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 12.1 Visualización NER con displacy\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# Visualizar las primeras oraciones con entidades resaltadas\n",
    "doc_viz = nlp(texto_limpio[:2000])\n",
    "displacy.render(doc_viz, style='ent', jupyter=True)\n",
], cell_id="sec12b_code")

# ─────────────────────────────────────────────
# CELDA 12c — Análisis de entidades tipo PER
# ─────────────────────────────────────────────
celda_12c_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 12.2 Análisis detallado de entidades por tipo\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# Clasificar entidades por tipo\n",
    "entidades_por_tipo = {}\n",
    "for ent in doc_ner.ents:\n",
    "    if ent.label_ not in entidades_por_tipo:\n",
    "        entidades_por_tipo[ent.label_] = []\n",
    "    entidades_por_tipo[ent.label_].append(ent.text)\n",
    "\n",
    "for tipo, entidades in entidades_por_tipo.items():\n",
    "    desc = spacy.explain(tipo) or tipo\n",
    "    print(f'\\n🏷️  {tipo} ({desc}):')\n",
    "    # Mostrar entidades únicas con frecuencia\n",
    "    freq = Counter(entidades)\n",
    "    for ent_text, count in freq.most_common(10):\n",
    "        print(f'   {ent_text:<35} ({count} apariciones)')\n",
    "\n",
    "# Gráfica de entidades por tipo\n",
    "fig, ax = plt.subplots(figsize=(8, 5))\n",
    "colores_ner = {'PER': '#e74c3c', 'LOC': '#3498db', 'ORG': '#2ecc71', 'MISC': '#9b59b6'}\n",
    "bars = ax.bar(ent_df['Tipo'], ent_df['Cantidad'],\n",
    "              color=[colores_ner.get(t, '#95a5a6') for t in ent_df['Tipo']],\n",
    "              edgecolor='gray')\n",
    "ax.set_xlabel('Tipo de entidad', fontsize=12)\n",
    "ax.set_ylabel('Cantidad', fontsize=12)\n",
    "ax.set_title('Distribución de entidades nombradas (NER)', fontsize=13, fontweight='bold')\n",
    "\n",
    "# Añadir etiquetas de valor\n",
    "for bar in bars:\n",
    "    height = bar.get_height()\n",
    "    ax.text(bar.get_x() + bar.get_width()/2., height,\n",
    "            f'{int(height)}', ha='center', va='bottom', fontweight='bold')\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.savefig('distribucion_ner.png', dpi=150, bbox_inches='tight')\n",
    "plt.show()\n",
    "print('✅ Gráfica NER guardada como distribucion_ner.png')\n",
], cell_id="sec12c_code")

# ─────────────────────────────────────────────
# CELDA 13 — Frecuencias post-lematización + Word Cloud
# ─────────────────────────────────────────────
celda_13_md = md_cell([
    "## 13. Análisis de frecuencias post-lematización\n",
    "\n",
    "Recalculamos las frecuencias utilizando los **lemas** obtenidos con spaCy. La lematización agrupa formas flexionadas bajo su forma canónica (por ejemplo, \"escribió\", \"escriben\", \"escrito\" → \"escribir\"), lo que proporciona un análisis más preciso del contenido temático del texto.\n",
], cell_id="sec13_md")

celda_13_code = code_cell([
    "# ═══════════════════════════════════════════════════════════════════\n",
    "# 13. Frecuencias post-lematización\n",
    "# ═══════════════════════════════════════════════════════════════════\n",
    "doc_full = nlp(texto_limpio[:5000])\n",
    "\n",
    "# Obtener lemas filtrados (sin stop words ni puntuación)\n",
    "lemas_filtrados = [\n",
    "    token.lemma_.lower() for token in doc_full\n",
    "    if token.is_alpha\n",
    "    and token.lemma_.lower() not in stop_words_es\n",
    "    and len(token.lemma_) > 2\n",
    "]\n",
    "\n",
    "freq_lemas = FreqDist(lemas_filtrados)\n",
    "print(f'📊 Frecuencias basadas en lemas (top 20):')\n",
    "for lema, freq in freq_lemas.most_common(20):\n",
    "    print(f'   {lema:<25} → {freq}')\n",
    "\n",
    "# Word Cloud con lemas\n",
    "fig, axes = plt.subplots(1, 2, figsize=(18, 6))\n",
    "\n",
    "# Barras\n",
    "top_lemas = freq_lemas.most_common(20)\n",
    "axes[0].barh([l for l, f in top_lemas][::-1], [f for l, f in top_lemas][::-1],\n",
    "             color='#E91E63', edgecolor='#AD1457')\n",
    "axes[0].set_xlabel('Frecuencia', fontsize=12)\n",
    "axes[0].set_title('Top 20 lemas más frecuentes', fontsize=13, fontweight='bold')\n",
    "\n",
    "# Word Cloud de lemas\n",
    "wc_lemas = WordCloud(\n",
    "    width=800, height=400,\n",
    "    background_color='white',\n",
    "    colormap='magma',\n",
    "    max_words=80\n",
    ").generate(' '.join(lemas_filtrados))\n",
    "\n",
    "axes[1].imshow(wc_lemas, interpolation='bilinear')\n",
    "axes[1].axis('off')\n",
    "axes[1].set_title('Nube de palabras (lemas)', fontsize=13, fontweight='bold')\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.savefig('frecuencias_lemas.png', dpi=150, bbox_inches='tight')\n",
    "plt.show()\n",
    "print('✅ Gráfica de lemas guardada como frecuencias_lemas.png')\n",
], cell_id="sec13_code")

# ─────────────────────────────────────────────
# CELDA 14 — Conclusiones
# ─────────────────────────────────────────────
celda_14 = md_cell([
    "## 14. Conclusiones\n",
    "\n",
    "A lo largo de este proyecto aplicamos un pipeline completo de Procesamiento de Lenguaje Natural sobre el artículo de Wikipedia de *Cien años de soledad*, y la experiencia nos dejó varias lecciones que vale la pena compartir:\n",
    "\n",
    "1. **La limpieza del texto es determinante.** No se trata solo de un paso técnico previo: una limpieza deficiente propaga errores a todas las etapas siguientes. Por ejemplo, las notas entre paréntesis de Wikipedia y las referencias numéricas generaban tokens espurios que distorsionaban el análisis de frecuencias. Al aplicar un pipeline de limpieza riguroso —eliminando URLs, paréntesis, emojis y caracteres especiales—, los resultados mejoraron notablemente tanto en frecuencias como en NER.\n",
    "\n",
    "2. **NLTK y spaCy son complementarios, no excluyentes.** NLTK resultó muy útil para la tokenización inicial, el acceso a stop words y la exploración léxica con WordNet, mientras que spaCy demostró superioridad en tareas más estructuradas como POS tagging y NER gracias a su pipeline preentrenado. En un flujo de trabajo real, combinar ambas herramientas permite aprovechar las fortalezas de cada una.\n",
    "\n",
    "3. **La lematización supera al stemming en calidad.** Aunque el stemming es más rápido y no requiere modelo de lenguaje, produce raíces que a veces carecen de sentido lingüístico (por ejemplo, \"novelista\" se reduce a \"novel\"). La lematización de spaCy, al utilizar un modelo entrenado con corpus reales del español, genera formas base que sí corresponden a palabras válidas.\n",
    "\n",
    "4. **El NER requiere iteración.** El modelo `es_core_news_sm` de spaCy logró identificar correctamente la mayoría de las entidades (Gabriel García Márquez como PER, Colombia como LOC, Nobel como MISC), pero también presentó errores: en algunos casos confundió apellidos con locaciones o clasificó frases completas como entidades misceláneas. Como señaló la profesora Orozco en clase, estos errores son inherentes a los modelos ligeros y pueden mitigarse con limpieza previa, reglas de post-procesamiento o modelos de mayor tamaño.\n",
    "\n",
    "5. **Las visualizaciones potencian la comprensión.** Las nubes de palabras y las gráficas de distribución permitieron identificar de un vistazo los temas centrales del texto (familia, soledad, pueblo, realismo, Nobel) y la estructura gramatical predominante (alta proporción de sustantivos y adjetivos, coherente con un texto narrativo-enciclopédico).\n",
    "\n",
    "En síntesis, este laboratorio reforzó la idea de que el PLN no es un proceso lineal sino iterativo: cada resultado intermedio debe revisarse y ajustarse antes de avanzar al siguiente paso. La combinación de herramientas clásicas (NLTK) con soluciones modernas (spaCy) ofrece un marco robusto para abordar tareas de análisis de texto en español.\n",
    "\n",
    "---\n",
    "\n",
    "**Grupo 7** — Alexander Oviedo Fadul · Maria Fernanda Ruiz Paipilla · Neheman Samir Jaller Cerchiaro · William David Obando Lopez\n",
], cell_id="sec14")

# ─────────────────────────────────────────────
# CELDA 15 — Referencias
# ─────────────────────────────────────────────
celda_15 = md_cell([
    "## 15. Referencias\n",
    "\n",
    "Campesato, O. (2021). Working with Text: POS. En *Natural language processing fundamentals for developers* (pp. 69-74). Mercury Learning and Information.\n",
    "\n",
    "Campesato, O. (2021). Chapter 4. Algorithms and Toolkits (I). En *Natural language processing fundamentals for developers* (pp. 123-160). Mercury Learning and Information.\n",
    "\n",
    "Ganegedara, T. (2018). Chapter 2: Understanding TensorFlow. En *Natural language processing with TensorFlow: Teach language to machines using python's deep learning library* (pp. 27-65). Packt Publishing.\n",
    "\n",
    "Moreira, D., Cruz, I., Gonzalez, K., Quirumbay, A., Magallan, C., Guarda, T., Andrade, A. y Castillo, C. (2021). Análisis del estado actual de procesamiento de lenguaje natural. *Revista Ibérica de Sistemas e Tecnologias de Informação*, (E42), 126-136.\n",
    "\n",
    "Srinivasa, B. (2018). Chapter 1. What is Text Analysis. En *Natural language processing and computational linguistics: A practical guide to text analysis with python, gensim, spacy, and keras* (pp. 9-20). Packt Publishing.\n",
    "\n",
    "Arumugam, R. y Shanmugamani, R. (2018). Chapter 2. Text Classification and POS Tagging Using NLTK. En *Hands-on natural language processing with python: A practical guide to applying deep learning architectures to your NLP applications* (pp. 28-51). Packt Publishing.\n",
    "\n",
    "Guerrero, F. y Marco, M. (2014). Parte II: Gramáticas de contexto libre, Parsing, unificación de rasgos y semántica y análisis semántico. En *Procesamiento del Lenguaje Natural*. Monografías de la Revista NeoInstrumenta (pp. 30-87).\n",
    "\n",
    "Echeverri, M. y Manjarres, R. (2020). Asistente Virtual Académico Utilizando Tecnologías Cognitivas de Procesamiento de Lenguaje Natural. *Revista Politécnica*, 16(31), 85-96.\n",
], cell_id="sec15")


# ═══════════════════════════════════════════════════════════════════
# Ensamblar notebook
# ═══════════════════════════════════════════════════════════════════
cells = [
    celda_0,
    celda_1_md, celda_1_code,
    celda_2_md, celda_2_code,
    celda_3_md, celda_3_code,
    celda_4_md, celda_4_code,
    celda_5_md, celda_5_code,
    celda_6_md, celda_6_code,
    celda_7_md, celda_7_code,
    celda_8_md, celda_8_code,
    celda_9_md, celda_9_code,
    celda_10_md, celda_10_code,
    celda_11_md, celda_11_code,
    celda_12_md, celda_12_code,
    celda_12b_md, celda_12b_code,
    celda_12c_code,
    celda_13_md, celda_13_code,
    celda_14,
    celda_15,
]

notebook = {
    "nbformat": 4,
    "nbformat_minor": 5,
    "metadata": {
        "kernelspec": {
            "display_name": "Python 3",
            "language": "python",
            "name": "python3"
        },
        "language_info": {
            "name": "python",
            "version": "3.10.0"
        },
        "colab": {
            "provenance": [],
            "toc_visible": True
        }
    },
    "cells": cells,
}

# Guardar
out_dir = os.path.dirname(os.path.abspath(__file__))
out_path = os.path.join(out_dir, "Oviedo_Alexander_Proyecto_S3_PNL.ipynb")
with open(out_path, "w", encoding="utf-8") as f:
    json.dump(notebook, f, ensure_ascii=False, indent=2)

print(f"✅ Notebook generado: {out_path}")
print(f"   Celdas totales: {len(cells)}")
print(f"   Celdas markdown: {sum(1 for c in cells if c['cell_type'] == 'markdown')}")
print(f"   Celdas código: {sum(1 for c in cells if c['cell_type'] == 'code')}")
