📝 Proyecto — Procesamiento Natural del Lenguaje (Semana 3)¶
Tokenización, Limpieza, NER y Análisis de Frecuencias¶
| Campo | Detalle |
|---|---|
| Programa | Especialización en Inteligencia Artificial |
| Institución | Corporación Universitaria Minuto de Dios (UNIMINUTO) |
| Curso | NRC-8774 — Procesamiento Natural del Lenguaje |
| Docente | Nathalia Orozco Morales |
| Grupo | 7 |
| Integrantes | Alexander Oviedo Fadul, Maria Fernanda Ruiz Paipilla, Neheman Samir Jaller Cerchiaro, William David Obando Lopez |
| Fecha | Julio 2026 |
Objetivo¶
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).
Preguntas orientadoras¶
¿Cuáles son las librerías más utilizadas para el procesamiento de lenguaje natural?
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).
¿Cómo es el proceso de tokenizar un texto y cómo influyen las Stop Words?
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).
1. Instalación y configuración del entorno¶
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.
# ═══════════════════════════════════════════════════════════════════
# 1. Instalación de dependencias
# ═══════════════════════════════════════════════════════════════════
!pip install -q spacy nltk beautifulsoup4 requests wordcloud matplotlib pandas
!python -m spacy download es_core_news_sm -q
import nltk
nltk.download('punkt', quiet=True)
nltk.download('punkt_tab', quiet=True)
nltk.download('stopwords', quiet=True)
nltk.download('wordnet', quiet=True)
nltk.download('omw-1.4', quiet=True)
nltk.download('averaged_perceptron_tagger', quiet=True)
nltk.download('averaged_perceptron_tagger_eng', quiet=True)
print('✅ Todas las dependencias instaladas correctamente')
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12.9/12.9 MB 63.4 MB/s eta 0:00:00 ✔ Download and installation successful You can now load the package via spacy.load('es_core_news_sm') ⚠ Restart to reload dependencies If you are in a Jupyter or Colab notebook, you may need to restart Python in order to load all the package's dependencies. You can do this by selecting the 'Restart kernel' or 'Restart runtime' option. ✅ Todas las dependencias instaladas correctamente
2. Importación de librerías¶
Organizamos las importaciones por categoría: procesamiento web, NLTK, spaCy y visualización.
# ═══════════════════════════════════════════════════════════════════
# 2. Importación de librerías
# ═══════════════════════════════════════════════════════════════════
import re
import warnings
warnings.filterwarnings('ignore')
# --- Web scraping ---
import requests
from bs4 import BeautifulSoup
# --- NLTK ---
from nltk.tokenize import word_tokenize, sent_tokenize
from nltk.corpus import stopwords, wordnet
from nltk.stem import SnowballStemmer
from nltk.probability import FreqDist
# --- spaCy ---
import spacy
from spacy import displacy
from collections import Counter
# --- Visualización ---
import matplotlib.pyplot as plt
from wordcloud import WordCloud
import pandas as pd
# Cargar modelos
nlp = spacy.load('es_core_news_sm')
stemmer = SnowballStemmer('spanish')
stop_words_es = set(stopwords.words('spanish'))
print(f'spaCy modelo: {nlp.meta["name"]} v{nlp.meta["version"]}')
print(f'Pipeline spaCy: {nlp.pipe_names}')
print(f'Stop words en español cargadas: {len(stop_words_es)}')
print('✅ Librerías importadas correctamente')
spaCy modelo: core_news_sm v3.8.0 Pipeline spaCy: ['tok2vec', 'morphologizer', 'parser', 'attribute_ruler', 'lemmatizer', 'ner'] Stop words en español cargadas: 313 ✅ Librerías importadas correctamente
3. Extracción de texto desde Wikipedia¶
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.
# ═══════════════════════════════════════════════════════════════════
# 3. Extracción de texto desde Wikipedia
# ═══════════════════════════════════════════════════════════════════
tema = 'Cien_años_de_soledad'
url = f'https://es.wikipedia.org/wiki/{tema}'
print(f'📥 Descargando: {url}')
response = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'})
print(f' Estado HTTP: {response.status_code}')
soup = BeautifulSoup(response.text, 'html.parser')
# Eliminar etiquetas innecesarias
for tag in soup(['script', 'style', 'noscript', 'header', 'footer',
'nav', 'aside', 'form', 'iframe', 'figure',
'button', 'table', 'sup', 'img']):
tag.decompose()
# Extraer texto principal del contenido
contenido = soup.find('div', {'id': 'mw-content-text'})
if contenido:
parrafos = contenido.find_all('p')
else:
parrafos = soup.find_all('p')
# Unir párrafos con contenido significativo (> 60 caracteres)
texto_crudo = ' '.join(
p.get_text(separator=' ', strip=True)
for p in parrafos
if len(p.get_text(strip=True)) > 60
)
print(f'\n📄 Texto extraído: {len(texto_crudo)} caracteres')
print(f' Párrafos procesados: {len([p for p in parrafos if len(p.get_text(strip=True)) > 60])}')
print(f'\n--- Vista previa (primeros 600 caracteres) ---')
print(texto_crudo[:600])
📥 Descargando: https://es.wikipedia.org/wiki/Cien_años_de_soledad Estado HTTP: 200 📄 Texto extraído: 79996 caracteres Párrafos procesados: 126 --- Vista previa (primeros 600 caracteres) --- Cien años de soledad es una novela del escritor colombiano ganadora del Premio Nobel de Literatura Gabriel García Márquez . Es opinión general que se trata de una obra maestra de la literatura hispanoamericana y universal, y es una de las obras más traducidas y leídas en español . En marzo de 2007, el IV Congreso Internacional de la Lengua Española celebrado en Cartagena de Indias la consideró una de las obras más importantes en dicha lengua. Figura en la lista de las 100 mejores novelas en español del siglo XX del periódico español El Mundo , en la de los 100 libros del siglo XX del diario fr
4. Limpieza del texto¶
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.
# ═══════════════════════════════════════════════════════════════════
# 4. Limpieza del texto
# ═══════════════════════════════════════════════════════════════════
def limpiar_texto(texto):
"""Pipeline de limpieza para texto extraído de páginas web."""
# 1. Eliminar contenido entre paréntesis (notas, refs de Wikipedia)
texto = re.sub(r'\([^)]*\)', '', texto)
# 2. Eliminar URLs
texto = re.sub(r'https?://\S+', '', texto)
texto = re.sub(r'www\.\S+', '', texto)
# 3. Eliminar emojis y caracteres Unicode especiales
texto = re.sub(r'[\U00010000-\U0010ffff]', '', texto)
# 4. Eliminar números sueltos (años y refs)
texto = re.sub(r'\b\d+\b', '', texto)
# 5. Conservar solo letras, puntuación básica y tildes
texto = re.sub(r'[^\w\s\.,;:¿?¡!áéíóúÁÉÍÓÚüÜñÑ]', ' ', texto)
# 6. Colapsar espacios múltiples
texto = re.sub(r'\s+', ' ', texto)
return texto.strip()
texto_limpio = limpiar_texto(texto_crudo)
print('🧹 Comparación antes y después de la limpieza:')
print(f' Caracteres originales: {len(texto_crudo):,}')
print(f' Caracteres limpios: {len(texto_limpio):,}')
print(f' Reducción: {(1 - len(texto_limpio)/len(texto_crudo))*100:.1f}%')
print(f'\n--- Texto limpio (primeros 500 caracteres) ---')
print(texto_limpio[:500])
🧹 Comparación antes y después de la limpieza: Caracteres originales: 79,996 Caracteres limpios: 75,599 Reducción: 5.5% --- Texto limpio (primeros 500 caracteres) --- Cien años de soledad es una novela del escritor colombiano ganadora del Premio Nobel de Literatura Gabriel García Márquez . Es opinión general que se trata de una obra maestra de la literatura hispanoamericana y universal, y es una de las obras más traducidas y leídas en español . En marzo de , el IV Congreso Internacional de la Lengua Española celebrado en Cartagena de Indias la consideró una de las obras más importantes en dicha lengua. Figura en la lista de las mejores novelas en español del
5. Tokenización con NLTK¶
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).
# ═══════════════════════════════════════════════════════════════════
# 5. Tokenización con NLTK
# ═══════════════════════════════════════════════════════════════════
tokens = word_tokenize(texto_limpio, language='spanish')
print(f'🔤 Tokenización completada')
print(f' Total de tokens: {len(tokens):,}')
print(f' Tokens únicos: {len(set(tokens)):,}')
print(f'\n--- Primeros 30 tokens ---')
print(tokens[:30])
# Distribución de frecuencias (antes de filtrar)
freq_dist_original = FreqDist(tokens)
print(f'\n--- 15 tokens más frecuentes (sin filtrar) ---')
for palabra, freq in freq_dist_original.most_common(15):
print(f' {palabra:20} → {freq}')
🔤 Tokenización completada Total de tokens: 14,040 Tokens únicos: 3,246 --- Primeros 30 tokens --- ['Cien', 'años', 'de', 'soledad', 'es', 'una', 'novela', 'del', 'escritor', 'colombiano', 'ganadora', 'del', 'Premio', 'Nobel', 'de', 'Literatura', 'Gabriel', 'García', 'Márquez', '.', 'Es', 'opinión', 'general', 'que', 'se', 'trata', 'de', 'una', 'obra', 'maestra'] --- 15 tokens más frecuentes (sin filtrar) --- de → 860 , → 817 la → 508 . → 465 y → 386 en → 357 el → 311 que → 304 a → 296 su → 212 se → 175 los → 171 con → 166 por → 157 un → 149
6. Eliminación de signos de puntuació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.
# ═══════════════════════════════════════════════════════════════════
# 6. Eliminación de signos de puntuación
# ═══════════════════════════════════════════════════════════════════
tokens_sin_punt = [t for t in tokens if t.isalpha()]
print(f'✂️ Tokens antes de filtrar puntuación: {len(tokens):,}')
print(f' Tokens después: {len(tokens_sin_punt):,}')
print(f' Tokens eliminados: {len(tokens) - len(tokens_sin_punt):,}')
# Ejemplo de tokens eliminados
tokens_punct = [t for t in tokens if not t.isalpha()]
print(f'\n--- Ejemplos de tokens eliminados (puntuación) ---')
print(list(set(tokens_punct))[:20])
✂️ Tokens antes de filtrar puntuación: 14,040 Tokens después: 12,699 Tokens eliminados: 1,341 --- Ejemplos de tokens eliminados (puntuación) --- ['.ª', ',', ':', ';', '.', '...']
7. Eliminación de Stop Words¶
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.
# ═══════════════════════════════════════════════════════════════════
# 7. Eliminación de Stop Words
# ═══════════════════════════════════════════════════════════════════
# Convertir a minúsculas y filtrar stop words
tokens_lower = [t.lower() for t in tokens_sin_punt]
tokens_filtrados = [t for t in tokens_lower if t not in stop_words_es]
print(f'🚫 Eliminación de Stop Words')
print(f' Tokens antes: {len(tokens_lower):,}')
print(f' Tokens después: {len(tokens_filtrados):,}')
print(f' Stop words eliminadas: {len(tokens_lower) - len(tokens_filtrados):,}')
# Mostrar algunas stop words encontradas
sw_encontradas = [t for t in tokens_lower if t in stop_words_es]
sw_freq = FreqDist(sw_encontradas)
print(f'\n--- Top 10 stop words más frecuentes en el texto ---')
for palabra, freq in sw_freq.most_common(10):
print(f' {palabra:15} → {freq}')
# Nueva distribución de frecuencias
freq_dist_filtrada = FreqDist(tokens_filtrados)
print(f'\n--- 15 palabras más frecuentes (sin stop words) ---')
for palabra, freq in freq_dist_filtrada.most_common(15):
print(f' {palabra:20} → {freq}')
🚫 Eliminación de Stop Words Tokens antes: 12,699 Tokens después: 6,675 Stop words eliminadas: 6,024 --- Top 10 stop words más frecuentes en el texto --- de → 866 la → 553 en → 408 y → 388 el → 340 a → 305 que → 304 su → 223 se → 192 los → 175 --- 15 palabras más frecuentes (sin stop words) --- aureliano → 99 buendía → 75 macondo → 72 años → 66 josé → 62 arcadio → 61 úrsula → 55 casa → 44 obra → 41 familia → 41 pueblo → 39 novela → 38 márquez → 38 garcía → 37 soledad → 36
8. Análisis de frecuencias y visualizació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.
# ═══════════════════════════════════════════════════════════════════
# 8. Visualización de frecuencias y Word Cloud
# ═══════════════════════════════════════════════════════════════════
fig, axes = plt.subplots(1, 2, figsize=(18, 6))
# --- Gráfica de barras: Top 20 palabras ---
top_20 = freq_dist_filtrada.most_common(20)
palabras = [p for p, f in top_20]
frecuencias = [f for p, f in top_20]
axes[0].barh(palabras[::-1], frecuencias[::-1], color='#2196F3', edgecolor='#1565C0')
axes[0].set_xlabel('Frecuencia', fontsize=12)
axes[0].set_title('Top 20 palabras más frecuentes\n(sin stop words ni puntuación)', fontsize=13, fontweight='bold')
axes[0].tick_params(axis='y', labelsize=10)
# --- Word Cloud ---
texto_wc = ' '.join(tokens_filtrados)
wc = WordCloud(
width=800, height=400,
background_color='white',
colormap='viridis',
max_words=100,
contour_width=1,
contour_color='steelblue'
).generate(texto_wc)
axes[1].imshow(wc, interpolation='bilinear')
axes[1].axis('off')
axes[1].set_title('Nube de palabras — Cien años de soledad', fontsize=13, fontweight='bold')
plt.tight_layout()
plt.savefig('frecuencias_wordcloud.png', dpi=150, bbox_inches='tight')
plt.show()
print('✅ Gráfica guardada como frecuencias_wordcloud.png')
✅ Gráfica guardada como frecuencias_wordcloud.png
9. Stemming y Lematizació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).
# ═══════════════════════════════════════════════════════════════════
# 9. Stemming y Lematización
# ═══════════════════════════════════════════════════════════════════
# --- Stemming con NLTK ---
stems = [stemmer.stem(t) for t in tokens_filtrados]
# --- Lematización con spaCy ---
texto_para_lemas = ' '.join(tokens_filtrados[:500]) # Limitar para eficiencia
doc_lemas = nlp(texto_para_lemas)
lemas = [token.lemma_ for token in doc_lemas if token.is_alpha]
# Tabla comparativa
print('📊 Comparación Stemming vs Lematización (muestra de 15 palabras):')
print(f'{"Palabra":<20} {"Stem (NLTK)":<20} {"Lema (spaCy)":<20}')
print('-' * 60)
muestra = tokens_filtrados[:15]
doc_muestra = nlp(' '.join(muestra))
lemas_muestra = [t.lemma_ for t in doc_muestra if t.is_alpha]
for i, palabra in enumerate(muestra):
stem = stemmer.stem(palabra)
lema = lemas_muestra[i] if i < len(lemas_muestra) else '-'
print(f'{palabra:<20} {stem:<20} {lema:<20}')
# Estadísticas
print(f'\n📈 Estadísticas:')
print(f' Tokens únicos originales: {len(set(tokens_filtrados)):,}')
print(f' Stems únicos: {len(set(stems)):,}')
print(f' Lemas únicos: {len(set(lemas)):,}')
print(f' Reducción por stemming: {(1 - len(set(stems))/len(set(tokens_filtrados)))*100:.1f}%')
print(f' Reducción por lematización: {(1 - len(set(lemas))/len(set(tokens_filtrados[:500])))*100:.1f}%')
📊 Comparación Stemming vs Lematización (muestra de 15 palabras): Palabra Stem (NLTK) Lema (spaCy) ------------------------------------------------------------ cien cien cien años años año soledad soled soledad novela novel novela escritor escritor escritor colombiano colombian colombiano ganadora ganador ganadorar premio premi premio nobel nobel nobel literatura literatur literatura gabriel gabriel gabriel garcía garc garcía márquez marquez márquez opinión opinion opinión general general general 📈 Estadísticas: Tokens únicos originales: 2,973 Stems únicos: 2,153 Lemas únicos: 325 Reducción por stemming: 27.6% Reducción por lematización: 5.8%
10. Sinónimos con WordNet¶
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.
# ═══════════════════════════════════════════════════════════════════
# 10. Sinónimos con WordNet
# ═══════════════════════════════════════════════════════════════════
# Seleccionar palabras clave del texto (las más frecuentes y significativas)
palabras_clave = ['novela', 'familia', 'pueblo', 'soledad', 'historia',
'realismo', 'autor', 'personaje', 'mundo', 'obra']
print('📖 Sinónimos desde WordNet para palabras clave del texto:\n')
for palabra in palabras_clave:
synsets_spa = wordnet.synsets(palabra, lang='spa')
synsets_eng = wordnet.synsets(palabra) # Buscar también en inglés
print(f'🔹 "{palabra}":')
if synsets_spa:
for ss in synsets_spa[:2]: # Máximo 2 synsets
lemas = ss.lemma_names('spa')
definicion = ss.definition()
print(f' Synset: {ss.name()}')
print(f' Sinónimos (ES): {lemas}')
print(f' Definición: {definicion}')
elif synsets_eng:
ss = synsets_eng[0]
lemas_eng = ss.lemma_names()
definicion = ss.definition()
print(f' Synset: {ss.name()}')
print(f' Sinónimos (EN): {lemas_eng[:5]}')
print(f' Definición: {definicion}')
else:
print(' No se encontraron synsets.')
print()
📖 Sinónimos desde WordNet para palabras clave del texto: 🔹 "novela": Synset: novel.n.02 Sinónimos (ES): ['novela'] Definición: a printed and bound book that is an extended work of fiction Synset: novel.n.01 Sinónimos (ES): ['novela'] Definición: an extended fictional work in prose; usually in the form of a story 🔹 "familia": Synset: kin.n.02 Sinónimos (ES): ['clan', 'familia', 'saga', 'tribu'] Definición: group of people related by blood or marriage Synset: family.n.02 Sinónimos (ES): ['familia', 'unidad_familiar'] Definición: primary social group; parents and children 🔹 "pueblo": Synset: people.n.01 Sinónimos (ES): ['gente', 'personas', 'pueblo'] Definición: (plural) any group of human beings (men or women or children) collectively Synset: folk.n.01 Sinónimos (ES): ['pueblo'] Definición: people in general (often used in the plural) 🔹 "soledad": Synset: aloneness.n.01 Sinónimos (ES): ['soledad'] Definición: a disposition toward being alone Synset: solitude.n.01 Sinónimos (ES): ['soledad'] Definición: a state of social isolation 🔹 "historia": Synset: history.n.03 Sinónimos (ES): ['historia'] Definición: the discipline that records and interprets past events involving human beings Synset: history.n.05 Sinónimos (ES): ['historia'] Definición: all that is remembered of the past as preserved in writing; a body of knowledge 🔹 "realismo": Synset: realism.n.01 Sinónimos (ES): ['realismo'] Definición: the attribute of accepting the facts of life and favoring practicality and literal truth Synset: realism.n.03 Sinónimos (ES): ['realismo'] Definición: (philosophy) the philosophical doctrine that physical objects continue to exist when not perceived 🔹 "autor": Synset: generator.n.03 Sinónimos (ES): ['autor'] Definición: someone who originates or causes or initiates something Synset: inventor.n.01 Sinónimos (ES): ['artífice', 'autor', 'descubridor', 'inventor'] Definición: someone who is the first to think of or make something 🔹 "personaje": Synset: persona.n.02 Sinónimos (ES): ['imagen', 'personaje'] Definición: (Jungian psychology) a personal facade that one presents to the world Synset: character.n.04 Sinónimos (ES): ['figura', 'papel', 'personaje'] Definición: an actor's portrayal of someone in a play 🔹 "mundo": Synset: world.n.08 Sinónimos (ES): ['hombre', 'humanidad', 'mundo'] Definición: all of the living human inhabitants of the earth Synset: world.n.03 Sinónimos (ES): ['mundo', 'realidad'] Definición: all of your experiences that determine how things appear to you 🔹 "obra": Synset: play.n.06 Sinónimos (ES): ['obra'] Definición: utilization or exercise Synset: oeuvre.n.01 Sinónimos (ES): ['obra', 'obra_completa', 'producción'] Definición: the total output of a writer or artist (or a substantial part of it)
11. Etiquetado POS (Part-of-Speech) con spaCy¶
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).
# ═══════════════════════════════════════════════════════════════════
# 11. POS Tagging con spaCy
# ═══════════════════════════════════════════════════════════════════
# Procesar un fragmento representativo
doc_pos = nlp(texto_limpio[:3000])
# Mostrar tokens con sus etiquetas POS
print('🏷️ Muestra de tokens con etiquetas POS (primeros 25):\n')
print(f'{"Token":<20} {"POS":<8} {"POS detallado":<12} {"Lema":<20}')
print('-' * 60)
for token in list(doc_pos)[:25]:
if token.is_alpha:
print(f'{token.text:<20} {token.pos_:<8} {token.tag_:<12} {token.lemma_:<20}')
# Distribución de categorías POS
pos_counts = Counter([token.pos_ for token in doc_pos if token.is_alpha])
pos_df = pd.DataFrame(pos_counts.most_common(), columns=['Categoría POS', 'Frecuencia'])
print(f'\n📊 Distribución de categorías gramaticales:')
print(pos_df.to_string(index=False))
# Gráfica de distribución POS
fig, ax = plt.subplots(figsize=(10, 5))
colors = plt.cm.Set3(range(len(pos_df)))
ax.bar(pos_df['Categoría POS'], pos_df['Frecuencia'], color=colors, edgecolor='gray')
ax.set_xlabel('Categoría Gramatical', fontsize=12)
ax.set_ylabel('Frecuencia', fontsize=12)
ax.set_title('Distribución de categorías POS en el texto', fontsize=13, fontweight='bold')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.savefig('distribucion_pos.png', dpi=150, bbox_inches='tight')
plt.show()
print('✅ Gráfica POS guardada como distribucion_pos.png')
🏷️ Muestra de tokens con etiquetas POS (primeros 25):
Token POS POS detallado Lema
------------------------------------------------------------
Cien NUM NUM cien
años NOUN NOUN año
de ADP ADP de
soledad NOUN NOUN soledad
es AUX AUX ser
una DET DET uno
novela NOUN NOUN novela
del ADP ADP del
escritor NOUN NOUN escritor
colombiano ADJ ADJ colombiano
ganadora ADJ ADJ ganador
del ADP ADP del
Premio PROPN PROPN Premio
Nobel PROPN PROPN Nobel
de ADP ADP de
Literatura PROPN PROPN Literatura
Gabriel PROPN PROPN Gabriel
García PROPN PROPN García
Márquez PROPN PROPN Márquez
Es AUX AUX ser
opinión NOUN NOUN opinión
general ADJ ADJ general
que PRON PRON que
se PRON PRON él
📊 Distribución de categorías gramaticales:
Categoría POS Frecuencia
NOUN 107
ADP 104
DET 68
PROPN 57
ADJ 45
VERB 40
PRON 31
AUX 16
CCONJ 15
ADV 15
NUM 8
SCONJ 7
✅ Gráfica POS guardada como distribucion_pos.png
12. Reconocimiento de Entidades Nombradas (NER)¶
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).
# ═══════════════════════════════════════════════════════════════════
# 12. NER con spaCy
# ═══════════════════════════════════════════════════════════════════
doc_ner = nlp(texto_limpio[:5000])
print(f'🔍 Entidades encontradas: {len(doc_ner.ents)}\n')
# Tabla de entidades
print(f'{"Entidad":<40} {"Tipo":<8} {"Descripción"}')
print('-' * 80)
for ent in doc_ner.ents:
descripcion = spacy.explain(ent.label_) or ''
print(f'{ent.text:<40} {ent.label_:<8} {descripcion}')
# Resumen por tipo de entidad
ent_types = Counter([ent.label_ for ent in doc_ner.ents])
print(f'\n📊 Resumen por tipo de entidad:')
ent_df = pd.DataFrame(ent_types.most_common(), columns=['Tipo', 'Cantidad'])
print(ent_df.to_string(index=False))
🔍 Entidades encontradas: 63 Entidad Tipo Descripción -------------------------------------------------------------------------------- Cien años de soledad MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Premio Nobel MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Literatura Gabriel García Márquez PER Named person or family. IV Congreso Internacional de la Lengua Española MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Cartagena de Indias LOC Non-GPE locations, mountain ranges, bodies of water El Mundo ORG Companies, agencies, institutions, etc. Le Monde PER Named person or family. Club de libros de Noruega ORG Companies, agencies, institutions, etc. La primera edición de la novela MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Buenos Aires LOC Non-GPE locations, mountain ranges, bodies of water editorial Sudamericana MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Hasta la fecha MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Gabriel García Márquez PER Named person or family. Ciudad de México LOC Non-GPE locations, mountain ranges, bodies of water Buenos Aires LOC Non-GPE locations, mountain ranges, bodies of water Aracataca LOC Non-GPE locations, mountain ranges, bodies of water La hojarasca , MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Macondo LOC Non-GPE locations, mountain ranges, bodies of water Gabriel García Márquez PER Named person or family. Cien años de Soledad PER Named person or family. Carlos Barral PER Named person or family. Seix Barral de Barcelona MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Yo creo MISC Miscellaneous entities, e.g. events, nationalities, products or works of art llegó PER Named person or family. Después LOC Non-GPE locations, mountain ranges, bodies of water García Márquez PER Named person or family. Editorial Sudamericana MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Buenos Aires LOC Non-GPE locations, mountain ranges, bodies of water Francisco Porrúa PER Named person or family. La publicación ya estaba decidida MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Simplemente ORG Companies, agencies, institutions, etc. La casa MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Cien años de soledad MISC Miscellaneous entities, e.g. events, nationalities, products or works of art La casa grande MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Álvaro Cepeda Samudio PER Named person or family. Cien PER Named person or family. editorial Sudamericana MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Buenos Aires LOC Non-GPE locations, mountain ranges, bodies of water García Márquez PER Named person or family. Paco Porrúa PER Named person or family. Xavi Ayén PER Named person or family. La novela MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Jomi García Ascot PER Named person or family. María Luisa Elío PER Named person or family. García Márquez PER Named person or family. México LOC Non-GPE locations, mountain ranges, bodies of water El libro MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Buendía PER Named person or family. Macondo LOC Non-GPE locations, mountain ranges, bodies of water Una de las ediciones más relevantes MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Real Academia Española ORG Companies, agencies, institutions, etc. Asociación de Academias de la Lengua Española MISC Miscellaneous entities, e.g. events, nationalities, products or works of art En ocasión de esa edición de MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Nicolás Pernett PER Named person or family. El libro narra la historia MISC Miscellaneous entities, e.g. events, nationalities, products or works of art Buendía PER Named person or family. Macondo LOC Non-GPE locations, mountain ranges, bodies of water José Arcadio Buendía PER Named person or family. Úrsula Iguarán PER Named person or family. Prudencio Aguilar PER Named person or family. José Arcadio Buendía PER Named person or family. José Arcadio PER Named person or family. Úrsula MISC Miscellaneous entities, e.g. events, nationalities, products or works of art 📊 Resumen por tipo de entidad: Tipo Cantidad PER 25 MISC 22 LOC 12 ORG 4
12.1 Visualización NER con displacy¶
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.
# ═══════════════════════════════════════════════════════════════════
# 12.1 Visualización NER con displacy
# ═══════════════════════════════════════════════════════════════════
# Visualizar las primeras oraciones con entidades resaltadas
doc_viz = nlp(texto_limpio[:2000])
displacy.render(doc_viz, style='ent', jupyter=True)
# ═══════════════════════════════════════════════════════════════════
# 12.2 Análisis detallado de entidades por tipo
# ═══════════════════════════════════════════════════════════════════
# Clasificar entidades por tipo
entidades_por_tipo = {}
for ent in doc_ner.ents:
if ent.label_ not in entidades_por_tipo:
entidades_por_tipo[ent.label_] = []
entidades_por_tipo[ent.label_].append(ent.text)
for tipo, entidades in entidades_por_tipo.items():
desc = spacy.explain(tipo) or tipo
print(f'\n🏷️ {tipo} ({desc}):')
# Mostrar entidades únicas con frecuencia
freq = Counter(entidades)
for ent_text, count in freq.most_common(10):
print(f' {ent_text:<35} ({count} apariciones)')
# Gráfica de entidades por tipo
fig, ax = plt.subplots(figsize=(8, 5))
colores_ner = {'PER': '#e74c3c', 'LOC': '#3498db', 'ORG': '#2ecc71', 'MISC': '#9b59b6'}
bars = ax.bar(ent_df['Tipo'], ent_df['Cantidad'],
color=[colores_ner.get(t, '#95a5a6') for t in ent_df['Tipo']],
edgecolor='gray')
ax.set_xlabel('Tipo de entidad', fontsize=12)
ax.set_ylabel('Cantidad', fontsize=12)
ax.set_title('Distribución de entidades nombradas (NER)', fontsize=13, fontweight='bold')
# Añadir etiquetas de valor
for bar in bars:
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height,
f'{int(height)}', ha='center', va='bottom', fontweight='bold')
plt.tight_layout()
plt.savefig('distribucion_ner.png', dpi=150, bbox_inches='tight')
plt.show()
print('✅ Gráfica NER guardada como distribucion_ner.png')
🏷️ MISC (Miscellaneous entities, e.g. events, nationalities, products or works of art): Cien años de soledad (2 apariciones) editorial Sudamericana (2 apariciones) Premio Nobel (1 apariciones) IV Congreso Internacional de la Lengua Española (1 apariciones) La primera edición de la novela (1 apariciones) Hasta la fecha (1 apariciones) La hojarasca , (1 apariciones) Seix Barral de Barcelona (1 apariciones) Yo creo (1 apariciones) Editorial Sudamericana (1 apariciones) 🏷️ PER (Named person or family.): García Márquez (3 apariciones) Gabriel García Márquez (2 apariciones) Buendía (2 apariciones) José Arcadio Buendía (2 apariciones) Literatura Gabriel García Márquez (1 apariciones) Le Monde (1 apariciones) Cien años de Soledad (1 apariciones) Carlos Barral (1 apariciones) llegó (1 apariciones) Francisco Porrúa (1 apariciones) 🏷️ LOC (Non-GPE locations, mountain ranges, bodies of water): Buenos Aires (4 apariciones) Macondo (3 apariciones) Cartagena de Indias (1 apariciones) Ciudad de México (1 apariciones) Aracataca (1 apariciones) Después (1 apariciones) México (1 apariciones) 🏷️ ORG (Companies, agencies, institutions, etc.): El Mundo (1 apariciones) Club de libros de Noruega (1 apariciones) Simplemente (1 apariciones) Real Academia Española (1 apariciones)
✅ Gráfica NER guardada como distribucion_ner.png
13. Análisis de frecuencias post-lematizació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.
# ═══════════════════════════════════════════════════════════════════
# 13. Frecuencias post-lematización
# ═══════════════════════════════════════════════════════════════════
doc_full = nlp(texto_limpio[:5000])
# Obtener lemas filtrados (sin stop words ni puntuación)
lemas_filtrados = [
token.lemma_.lower() for token in doc_full
if token.is_alpha
and token.lemma_.lower() not in stop_words_es
and len(token.lemma_) > 2
]
freq_lemas = FreqDist(lemas_filtrados)
print(f'📊 Frecuencias basadas en lemas (top 20):')
for lema, freq in freq_lemas.most_common(20):
print(f' {lema:<25} → {freq}')
# Word Cloud con lemas
fig, axes = plt.subplots(1, 2, figsize=(18, 6))
# Barras
top_lemas = freq_lemas.most_common(20)
axes[0].barh([l for l, f in top_lemas][::-1], [f for l, f in top_lemas][::-1],
color='#E91E63', edgecolor='#AD1457')
axes[0].set_xlabel('Frecuencia', fontsize=12)
axes[0].set_title('Top 20 lemas más frecuentes', fontsize=13, fontweight='bold')
# Word Cloud de lemas
wc_lemas = WordCloud(
width=800, height=400,
background_color='white',
colormap='magma',
max_words=80
).generate(' '.join(lemas_filtrados))
axes[1].imshow(wc_lemas, interpolation='bilinear')
axes[1].axis('off')
axes[1].set_title('Nube de palabras (lemas)', fontsize=13, fontweight='bold')
plt.tight_layout()
plt.savefig('frecuencias_lemas.png', dpi=150, bbox_inches='tight')
plt.show()
print('✅ Gráfica de lemas guardada como frecuencias_lemas.png')
📊 Frecuencias basadas en lemas (top 20): novela → 12 ser → 11 libro → 9 primero → 9 año → 7 garcía → 7 edición → 7 márquez → 6 pueblo → 6 cien → 5 obra → 5 haber → 5 soledad → 4 escritor → 4 tratar → 4 lengua → 4 publicar → 4 buenos → 4 aires → 4 editorial → 4
✅ Gráfica de lemas guardada como frecuencias_lemas.png
14. Conclusiones¶
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:
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.
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.
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.
El NER requiere iteración. El modelo
es_core_news_smde 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.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).
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.
Grupo 7 — Alexander Oviedo Fadul · Maria Fernanda Ruiz Paipilla · Neheman Samir Jaller Cerchiaro · William David Obando Lopez
15. Referencias¶
Campesato, O. (2021). Working with Text: POS. En Natural language processing fundamentals for developers (pp. 69-74). Mercury Learning and Information.
Campesato, O. (2021). Chapter 4. Algorithms and Toolkits (I). En Natural language processing fundamentals for developers (pp. 123-160). Mercury Learning and Information.
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.
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.
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.
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.
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).
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.