Datos no estructurados (II)
Simplificación (stop words)
Siguiendo dentro de la lógica de tratamiento del texto como BoW (bolsa de palabras), buscando ahora la simplificación y significación de los elementos (las palabras), el paso lógico que sigue es la eliminación de las llamadas palabras irrelevantes (stop words).
Para el enfoque bolsa de palabras (BoW), un texto se reduce a las frecuencias de aparición de sus componentes (las palabras), sin prestar atención a la estructura ni al orden de las palabras. Es por ello que, bajo esta premisa, determinadas palabras ("el", "un", "en", "de", "la" o "los") van a aparecer en la mayoría de los textos, si no en todos, por lo que no resultan significativas de esos textos e incrementan innecesariamente el corpus con el que el algoritmo deberá trabajar. Eliminarlas es una opción posible y deseable. Posible porque no aportan nada al posterior análisis; deseable porque interfieren en éste: los algoritmos de clasificación o de agrupamiento podrían interpretar que dos textos son similares porque comparten palabras como las anteriores, siendo que éstas no son decisorias para determinar el contenido del texto. Si las eliminamos, por el contrario, se incrementa el valor semántico de las palabras que no son eliminadas y que sí tienen una fuerte carga conceptual, definitoria del contenido del texto ("comportamiento", "atipico", "ansiedad", "urgente", "evaluacion").
No obstante, también el uso de esta estrategias genera controversia, incluso sin salirse del marco del enfoque BoW (o casi): determinadas palabras, como las que implican negación, normalmente son consideradas palabras vacías, pero resultan altamenten definitorias del significado del mensaje: no es lo mismo decir "es disruptivo" que negarlo ("no es disruptivo"), y eso es precisamente lo que resulta de eliminar la negación si aplicamos un procedimiento como el que aquí se propone. Pero por otro lado mantener las negaciones rompe la lógica del planteamiento del modelo BoW, que se basa en el análisis estadístico-vectorial de las palabras "llenas" sin concesión alguna a la dismensión estructural-secuencial de las palabras en el texto. Puede que el enfoque BoW presente importantes limitaciones (actualmente se puede considerar que pertenece al pasado) pero dentro de sus límites y en el marco de sus modelos de análisis es coherente y funcional. Pensando en superarlo, habrá que hacerlo dando los pasos necesarios, no introduciendo parches ajenos a la lógica del modelo que, además, no terminan de resolver los problemas que pretenden resolver.
Aun así, desde una perspectiva fundamentalmente didáctica como la que preside esta entrada, voy a plantear varias opciones de desarrollo en términos de script python de tratamiento del procedimiento para que te queden disponibles y puedas realizar los análisis y las comparaciones que desees. Para empezar, recueda que nuestro texto de partida es el mismo que fue de llegada en la entrada anterior:
Expongo a continuación el script más simple y más próximo al contenido del texto a tratar.
from collections import Counter
import re
import pandas as pd
# 1. Texto de partida. En este caso el resultante de la limpieza y normalización anterior
texto_normalizado = (
"el alumno presenta un comportamiento atipico en el aula se observa un incremento "
"de la ansiedad durante las clases de matematicas el orientador indica que el caso "
"es urgente y requiere atencion inmediata ver informe en los padres no co operan "
"con las pautas el proximo miercoles se realizara otra evaluacion psicologica "
"nota importante no se debe clasificar al menor sin mas evidencias cientificas"
)
# Lista de stop words ajustada a texto_normalizado y excluyendo "no"
stop_words_estandar = {
'el', 'un', 'en', 'se', 'de', 'la', 'durante', 'las', 'que', 'es', 'y',
'al', 'con', 'para', 'por', 'una', 'los', 'otra', 'sin', 'mas', 'ver'
}
# --- 2. Función de reducción de stop words --------------------------------------
def analizar_frecuencias(texto):
palabras = texto.split()
total_palabras = len(palabras)
# Contar ocurrencias
contador = Counter(palabras)
# Construimos un diccionario con los datos
datos_frecuencia = []
for palabra, count in contador.most_common():
porcentaje = (count / total_palabras) * 100
datos_frecuencia.append({
'Palabra': palabra,
'Frecuencia Absoluta': count,
'Porcentaje (%)': round(porcentaje, 2)
})
return pd.DataFrame(datos_frecuencia), total_palabras
# (A) Análisis PRE-limpieza-------------------------
df_pre, total_pre = analizar_frecuencias(texto_normalizado)
# (B) NUCLEO DEL PROCESO: eliminar stop words----------------------------------------------
palabras_limpias = [p for p in texto_normalizado.split() if p not in stop_words_estandar]
texto_limpio = " ".join(palabras_limpias)
#------------------------------------------------------------------------------------------
# (C) Análisis POST-limpieza--------------------
df_post, total_post = analizar_frecuencias(texto_limpio)
# --- Script de visualización ---------------------------------------------------
print(f"--- ANÁLISIS PRE-LIMPIEZA (Total palabras: {total_pre}) ---")
print(df_pre.head(8)) # Mostramos las 8 más frecuentes
print(f"\n--- ANÁLISIS POST-LIMPIEZA (Total palabras: {total_post}) ---")
print(df_post.head(8))
print (f"\n--- TEXTO LIMPIO \n {texto_limpio}")
Observa que junto con el texto sobre el que trabajamos, también he incluído expresamente un conjunto de palabras vacías (stop word) que se ajusta al contenido de dicho texto, excluyendo las negaciones, aunque tu puedes incluirlas, si lo prefieres.
Este script, además de proceder a la eliminación de esas palabras vacías también nos obrece el recuento del total de palabras del texto original y del resultante, junto con el análisis de las ocho palabras más frecuentes en cada fase del procedo. Estos últimos datos son únicamente a nivel informativo, por lo que puedes eliminarlos del script si así lo deseas. Personalmente no lo recomiendo, ya que precisamente esta función prioriza y expresa el recuento antes y después de la eliminación de las stop word.
Pero lo verdaderamente interesante de la función es precisamente el procedimiento de eliminación de ese grupo de palabras vacías del texto original. Para ello estas dos instrucciones...
palabras_limpias = [p for p in texto_normalizado.split() if p not in stop_words_estandar]
texto_limpio = " ".join(palabras_limpias)
... desarrollan el segmentación del texto texto_normalizado extrayéndo palabra a palabra mediante el bucle for p in texto_normalizado y comprobando que dicha palabra no está en el grupo de palabras vacías stop_word_estandar mediante un condicional if p not in.... El resultado es una lista de palabras llenas que posteriormente se convierten en un texto sobre la variable texto_limpio que resulta de aplicar la función join() (en realidad "".join(palabras_limpias).
En realidad esta forma es una forma sintética y eficiente de plantear lo que en un script básico de python se expresaría como sigue:
# Primera expresión----------------------
palabras_limpias = []
for p in texto_normalizado.split():
if p not in stop_words_estandar:
palabras_limpias.append(p)
# Segunda expresión ---------------------
texto_limpio = ""
for palabra in palabras_limpias:
if texto_limpio != "": # Si la variable ya tiene texto, le sumamos un espacio antes de la siguiente palabra
texto_limpio = texto_limpio + " "
texto_limpio = texto_limpio + palabra # Sumamos la palabra al texto
Vamos a ver ahora una segunda alternativa, empleando la biblioteca NLTK
from collections import Counter
import pandas as pd
import nltk
from nltk.corpus import stopwords
# Aseguramos la descarga del corpus oficial de stop words de NLTK
nltk.download('stopwords', quiet=True)
# 1. Texto normalizado de base
texto_normalizado = (
"el alumno presenta un comportamiento atipico en el aula se observa un incremento "
"de la ansiedad durante las clases de matematicas el orientador indica que el caso "
"es urgente y requiere atencion inmediata ver informe en los padres no co operan "
"con las pautas el proximo miercoles se realizara otra evaluacion psicologica "
"nota importante no se debe clasificar al menor sin mas evidencias cientificas"
)
# 2. Carga del conjunto stopwords de NLTK en español (~300 palabras)
stop_words_completo = set(stopwords.words('spanish'))
# Especificamos SÓLO las palabras que SÍ se desean mantener (blindaje de negaciones/matices)
palabras_a_mantener = {'no', 'ni', 'sin', 'tampoco', 'jamas', 'nunca'}
# Creamos el filtro final: el conjunto completo menos nuestras excepciones
stop_words_eficiente = stop_words_completo.difference(palabras_a_mantener)
# 3. Función de recuento y cálculo porcentual
def analizar_y_formatear(texto):
palabras = texto.split()
total_palabras = len(palabras)
contador = Counter(palabras)
datos = []
for palabra, count in contador.most_common():
porcentaje = (count / total_palabras) * 100
datos.append({
'Palabra': palabra,
'Frec. Absoluta': count,
'Porcentaje': f"{porcentaje:.2f}%"
})
return pd.DataFrame(datos), total_palabras
# --- Script de ejecución: llamada a función
# Fase A: Recuento Pre-Limpieza
df_pre, total_pre = analizar_y_formatear(texto_normalizado)
# Fase B: NÚCLEO DEL PROCEDIMIENTO: eliminación de stop words
palabras_filtradas = [p for p in texto_normalizado.split() if p not in stop_words_eficiente]
texto_limpio = " ".join(palabras_filtradas)
# Fase C: Recuento Post-Limpieza
df_post, total_post = analizar_y_formatear(texto_limpio)
# --- SALIDA POR CMD ---
# Configuración de visualización para la consola de Windows / CMD
pd.set_option('display.max_rows', 12)
pd.set_option('display.width', 1000)
print("\n" + "="*60)
print(f" AUDITORÍA DE FRECUENCIAS (MÉTODO NLTK) - TOTAL: {total_pre} ".center(60))
print("="*60)
print(f"\n[TOP 10] PALABRAS ANTES DE LA LIMPIEZA:")
print("-" * 45)
print(df_pre.head(10).to_string(index=False))
print("\n" + "-"*60)
print(f"\n[TOP 10] PALABRAS DESPUÉS DE LA LIMPIEZA (Total: {total_post}):")
print("-" * 45)
print(df_post.head(10).to_string(index=False))
print("\n" + "="*60)
print(" TEXTO RESULTANTE PARA BOLSA DE PALABRAS (BoW) ".center(60))
print("="*60)
print(texto_limpio)
print("="*60 + "\n")
La biblioteca NLTK dispone de varias funciones para el PLN, aunque ahora nos centraremos en la función de limpieza (eliminación) de las palabras vacías (stop words) usando la biblioteca NLTK. Primero nos aseguramos de cargar su conjunto stop words en español nltk.download('stopwords', quiet=True); después creamos un conjunto de excepciones palabras_a_mantener = {'no', 'ni', 'sin', 'tampoco', 'jamas', 'nunca'} y finalmente creamos el subconjunto de stop words que realmente vamos a aplicar stop_words_eficiente = stop_words_completo.difference(palabras_a_mantener).
Aunque en este script desarrollamos el proceso de eliminación de palabras vacías dentro del script principal, las instrucciones que se ejecutan son las mismas que en el script anterior...
# Fase B: NÚCLEO DEL PROCEDIMIENTO: eliminación de stop words
palabras_filtradas = [p for p in texto_normalizado.split() if p not in stop_words_eficiente]
texto_limpio = " ".join(palabras_filtradas)
... aunque ahora emplemos dentro del condicional el subconjunto que creamos antes if p not in stop_words_eficiente]. Dado que el texto sobre el que trabajamos es el mismo que el usado en el script anterior, el resultado que obtenemos es el mismo que si aplicamos el primer script de esta entrada.
Sobra decir que si eliminamos el subconjunto de excepciones el texto resultante sería ligeramente diferente,más coherente con el planteamiento de trabajo del modelo BoW, pero aquí he preferido mantener estas excepciones para mostrar la instrucción que nos pertime incluir una excepción en el manejo de los contenidos de una colección de datos (función difference()).