Representaciones de texto para NLP

Por Jose R. Zapata

Ultima actualización: 15/Oct/2025

Representación de texto (Bag of Words, TF-IDF)

En el procesamiento del lenguaje natural (NLP), la representación de texto es crucial para convertir el texto en una forma que los algoritmos de aprendizaje automático puedan entender. A continuación, se describen algunas técnicas comunes de representación de texto:

1. Bag of Words (BoW)

La representación Bag of Words es una técnica simple y efectiva que convierte un documento en un vector de frecuencias de palabras. En esta representación, se ignoran el orden y la gramática de las palabras, centrándose únicamente en la cantidad de veces que esta una palabra en el texto.

TF-IDF (Term Frequency-Inverse Document Frequency)

TF-IDF es una técnica que pondera la frecuencia de las palabras en un documento en relación con su frecuencia en todo el corpus. La idea es que las palabras que aparecen con frecuencia en un documento pero rara vez en otros documentos son más importantes para ese documento. Osea más peso a las palabras que son informativas y menos a las que son comunes.

Análisis de Reseñas de filmaffinity

1) Carga y Exploración del Dataset 🤓

Objetivo: Exploración del Dataset.

import numpy as np
import pandas as pd

Carga del dataset

data_reviews = pd.read_parquet("filmaffinity_reviews_cleaned.parquet")
data_reviews.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 50000 entries, 0 to 49999
Data columns (total 11 columns):
 #   Column                      Non-Null Count  Dtype
---  ------                      --------------  -----
 0   film_id                     50000 non-null  object
 1   author_review_desc          50000 non-null  string
 2   author_rating               50000 non-null  int64
 3   film_title                  50000 non-null  object
 4   film_original_title         50000 non-null  object
 5   film_country                50000 non-null  object
 6   film_average_rating         50000 non-null  float64
 7   film_number_of_ratings      50000 non-null  int64
 8   clean_review                50000 non-null  object
 9   clean_review_stemming       50000 non-null  object
 10  clean_review_lemmatization  50000 non-null  object
dtypes: float64(1), int64(2), object(7), string(1)
memory usage: 4.2+ MB

Ejemplo de algunas filas del dataset

data_reviews.sample(5)

film_idauthor_review_descauthor_ratingfilm_titlefilm_original_titlefilm_countryfilm_average_ratingfilm_number_of_ratingsclean_reviewclean_review_stemmingclean_review_lemmatization
44147film787296\nEl páramo tiene una buena idea detrás, pero ...6El páramoEl páramoEspaña4.33415el paramo tiene una buena idea detras pero el ...param buen ide detr desarroll esper confus cla...paramo buen idea detra desarrollo esperar conf...
37977film671403\nMe encanta como esta rodada y lo que me hace...8Adiós, muchachosAu revoir les enfantsakaFrancia7.815937me encanta como esta rodada y lo que me hace s...encant rod hac sent histori magnif form cont t...encantar rodado hacer sentir historia magnific...
17483film325218\nToda la culpa la tiene la Lata de tomate, po...5FlashThe FlashakaEstados Unidos6.013664toda la culpa la tiene la lata de tomate por e...tod culp lat tomat razon pelicul guion confus ...todo culpa lata tomate razon pelicula guion co...
2372film512859\nEntro a valorar la película y veo críticas b...4Una boda explosivaShotgun WeddingakaEstados Unidos4.22711entro a valorar la pelicula y veo criticas bas...entro valor pelicul veo critic bastant decent ...entro valorar pelicula ver critica bastante de...
6801film489970\n¿Quién lo iba a decir? En fin, otra serie am...10Breaking BadBreaking BadEstados Unidos8.8106146quien lo iba a decir en fin otra serie america...iba dec fin seri american trat trafic drog uni...ir decir fin serie americano tratar trafico dr...

Evaluar los valores nulos

data_reviews.isnull().sum()
film_id                       0
author_review_desc            0
author_rating                 0
film_title                    0
film_original_title           0
film_country                  0
film_average_rating           0
film_number_of_ratings        0
clean_review                  0
clean_review_stemming         0
clean_review_lemmatization    0
dtype: int64

En este caso no hay nulos, pero para asegurarnos, vamos a eliminar cualquier fila que tenga nulos.

Nota: Si el dataset tuviera nulos, podríamos optar por imputar valores según el contexto, esto necesitaría un análisis más profundo.

data_reviews = data_reviews.dropna()

2) Representación Vectorial: Bag-of-Words y TF-IDF 📁

Objetivo: Convertir las reseñas en representaciones numéricas mediante Bag-of-Words y TF-IDF para su posterior análisis.

Bag-of-Words (BoW): Es una técnica que convierte texto en una representación numérica al contar la frecuencia de cada palabra en un documento, ignorando el orden y la gramática.

Cada documento se representa como un vector donde cada dimensión corresponde a una palabra del vocabulario y el valor es la frecuencia de esa palabra en el documento.

TF-IDF (Term Frequency-Inverse Document Frequency): Esta técnica mejora la representación BoW al ponderar la frecuencia de las palabras por su importancia en el corpus.

Calcula la frecuencia de una palabra en un documento (TF) y la multiplica por la inversa de la frecuencia de documentos que contienen esa palabra (IDF), reduciendo la influencia de palabras comunes y destacando términos más informativos.

from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer

Crear el corpus a partir de las reseñas limpias

corpus = data_reviews["clean_review_lemmatization"].tolist()

📍 Representación Bag-of-Words con CountVectorizer

Ejemplo de como usar CountVectorizer:

cv = CountVectorizer()
bow_matrix = cv.fit_transform(corpus)
print("Dimensiones de la matriz Bag-of-Words:", bow_matrix.shape)
print(
    "Ejemplo de términos (BoW):",
    np.random.choice(cv.get_feature_names_out(), size=10, replace=False),
)
Dimensiones de la matriz Bag-of-Words: (50000, 134453)
Ejemplo de términos (BoW): ['duterte' 'mantis' 'escayola' 'logicosino' 'suk' 'neumatica' 'map'
 'exacto' 'nochero' 'utrilla']

📍 Representación TF-IDF con TfidfVectorizer

Examinar las palabras con mayor peso TF-IDF para identificar términos relevantes en el texto.

Ejemplo de como usarlo:

tfidf = TfidfVectorizer()
tfidf_matrix = tfidf.fit_transform(corpus)
print("Dimensiones de la matriz TF-IDF:", tfidf_matrix.shape)
print(
    "Ejemplo de términos (TF-IDF):",
    np.random.choice(tfidf.get_feature_names_out(), size=10, replace=False),
)
Dimensiones de la matriz TF-IDF: (50000, 134453)
Ejemplo de términos (TF-IDF): ['spaceopera' 'ether' 'incopetencia' 'horario' 'temblequ' 'elucubre'
 'aupar' 'masterpiece' 'detrozar' 'cheever']
# Obtener nombres de términos y TF-IDF promedio por término en todo el corpus
feature_names = tfidf.get_feature_names_out()
mean_tfidf = np.asarray(tfidf_matrix.mean(axis=0)).ravel()

# Normalizar para interpretar como "probabilidades" (suma 1)
total = mean_tfidf.sum()
probs = mean_tfidf / total if total > 0 else np.zeros_like(mean_tfidf)

# DataFrame con término, tfidf medio y probabilidad normalizada
df_terms = pd.DataFrame({
    "term": feature_names,
    "mean_tfidf": mean_tfidf,
    "probability": probs,
})

print(
    "\nEjemplo aleatorio de 10 términos con su probabilidad (TF-IDF medio normalizado):"
)
print(
    df_terms.sample(10, random_state=2025)
    .sort_values("probability", ascending=False)
    .reset_index(drop=True)
)
Ejemplo aleatorio de 10 términos con su probabilidad (TF-IDF medio normalizado):
           term  mean_tfidf   probability
0     retuercir    0.000077  9.182959e-06
1  infantilizar    0.000057  6.856340e-06
2   apareceriar    0.000018  2.141356e-06
3        cuendo    0.000012  1.436164e-06
4   jugandoselo    0.000008  9.283262e-07
5        atilio    0.000007  8.796658e-07
6       hillard    0.000007  7.987017e-07
7    provocarir    0.000006  6.851564e-07
8     sasquatch    0.000003  3.642664e-07
9      geppetto    0.000003  3.042243e-07

3) Extracción de Términos Clave y Modelado de Temas 🔍

Objetivo: Utilizar LDA para extraer temas y términos clave de las reseñas.

Modelado de temas con LDA (Latent Dirichlet Allocation): LDA es una técnica de modelado generativo que asume que cada documento es una mezcla de temas y que cada tema es una mezcla de palabras. Ayuda a descubrir temas ocultos en una colección de documentos.

Extracción de palabras clave: Métodos como la frecuencia de términos, TF-IDF y algoritmos como RAKE (Rapid Automatic Keyword Extraction) se utilizan para identificar palabras o frases que capturan la esencia de un documento.

from sklearn.decomposition import LatentDirichletAllocation

Aplicar LDA sobre la matriz Bag-of-Words para extraer 5 temas

NOTA: tener en cuenta que no estamos haciendo una evaluación usando Train/Test, solo queremos ver los temas extraídos. Por eso se hace fit directamente sobre la matriz BoW completa.

Como se ve más adelante, si se hace una evaluación se debe primero hacer un split de los datos en Train/Test y luego hacer transformaciones solo con los datos de Train para evitar data leakage.

# usar 4 núcleos para paralelizar en n_jobs
lda = LatentDirichletAllocation(n_components=5, random_state=123, n_jobs=4)
lda.fit(bow_matrix)
def display_topics(model, feature_names: np.ndarray, no_top_words: int):
    """Muestra los temas extraídos por el modelo LDA.

    Args:
        model: El modelo LDA entrenado.
        feature_names (np.ndarray): Los nombres de las características (términos).
        no_top_words (int): Número de palabras principales a mostrar por tema.
    Returns:
        None
    """

    for topic_idx, topic in enumerate(model.components_):
        print(f"Tema {topic_idx}:")
        print(
            " ".join([
                feature_names[i] for i in topic.argsort()[: -no_top_words - 1 : -1]
            ])
        )
display_topics(lda, cv.get_feature_names_out(), 10)
Tema 0:
ver él mas si pelicula ser hacer poder ir decir
Tema 1:
pelicula mas historia él personaje film gran hacer cine ser
Tema 2:
él mas pelicula vida ser poder hacer si historia tanto
Tema 3:
mas accion hacer pelicula él marvel personaje nuevo mejor batman
Tema 4:
pelicula mas ver hacer si él ser bien poder historia

4) Clasificación Tradicional para Análisis de Sentimientos y Categorías 👍 👎

Objetivo: Entrenar y evaluar un clasificador (Naive Bayes) para determinar el sentimiento (positivo/negativo) de las reseñas.

Naive Bayes: Es un clasificador probabilístico basado en el teorema de Bayes, que asume la independencia entre las características.

Calcula la probabilidad de que un documento pertenezca a una clase (por ejemplo, positivo o negativo) asumiendo que las características (palabras) son independientes entre sí.

Esta suposición “ingenua” simplifica mucho los cálculos y permite entrenar y predecir rápidamente, algo muy útil en entornos donde el tiempo y los recursos pueden ser limitados.

Definición: Se considera reseña positiva cuando la puntuación (“author_rating”) es mayor que 6; negativa en caso contrario.

from sklearn.metrics import accuracy_score, classification_report
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB

Crear la variable binaria de sentimiento: 1 (positivo) si author_rating > 6, 0 (negativo) de lo contrario

UMBRAL = 6
data_reviews["sentiment_bin"] = (data_reviews["author_rating"] > UMBRAL).astype(int)

Usar la representación TF-IDF para el modelo

# Dividir los datos en conjuntos de entrenamiento y prueba

X_data = data_reviews["clean_review_lemmatization"]
y_data = data_reviews["sentiment_bin"]

Dividir el dataset en entrenamiento y prueba, antes de realizar transformaciones para evitar data leakage

X_train, X_test, y_train, y_test = train_test_split(
    X_data, y_data, test_size=0.2, random_state=42
)

Entrenar el clasificador Naive Bayes

# inicializar el transformador TF-IDF
tfidf = TfidfVectorizer()

# Entrenar y procesar el transformador TF-IDF
# olo con los datos de entrenamiento
x_tfidf_matrix = tfidf.fit_transform(X_train)

nb_classifier = MultinomialNB()
nb_classifier.fit(x_tfidf_matrix, y_train)

Evaluar el modelo

# Usar el transformador TF-IDF entrenado  con lo datos de train
# para procesar los datos de prueba
X_test_processed = tfidf.transform(X_test)

y_pred = nb_classifier.predict(X_test_processed)

print("Accuracy:", accuracy_score(y_test, y_pred))
print("Reporte de clasificación:\n", classification_report(y_test, y_pred))
Accuracy: 0.751
Reporte de clasificación:
               precision    recall  f1-score   support

           0       0.84      0.56      0.67      4563
           1       0.71      0.91      0.80      5437

    accuracy                           0.75     10000
   macro avg       0.78      0.74      0.74     10000
weighted avg       0.77      0.75      0.74     10000

NOTA: Para seleccionar el mejor modelo y evitar overfitting, se debería usar validación cruzada y ajustar hiperparámetros.

Pueden ver un ejemplo en el siguiente enlace: https://joserzapata.github.io/post/ciencia-datos-proyecto-python/6-model_selection/

Guardar el modelo

import joblib

model_path = "nb_classifier_model.pkl"
joblib.dump(nb_classifier, model_path)
['nb_classifier_model.pkl']

Cargar el modelo

mi_modelo = joblib.load(model_path)

Probar con nuevos datos

import re
import string


def limpiar_texto(text: str) -> str:
    """Preprocesa el texto realizando los siguientes pasos:
    1. Convertir a minúsculas.
    2. Eliminar acentos de vocales.
    3. Eliminar textos entre corchetes (ej.: etiquetas).
    4. Eliminar URLs.
    5. Eliminar etiquetas HTML.
    6. Eliminar signos de puntuación.
    7. Eliminar saltos de línea.
    8. Eliminar palabras que contienen números.
    9. Eliminar emojis y caracteres especiales (no ASCII), excepto la ñ.
    10. Eliminar espacios extras entre palabras.
    11. Eliminar espacios extras al inicio y final.

    Args:
        text (str): El texto a procesar.
    Returns:
        str: El texto preprocesado.

    Example:
        >>> text_preprocess("¡Hola! Visita https://example.com para más info.")
        'hola visita para mas info'


    """
    # Convertir a minúsculas
    text = str(text).lower()

    # Eliminar acentos de vocales
    MAP_VOCALES = {
        "á": "a",
        "é": "e",
        "í": "i",
        "ó": "o",
        "ú": "u",
        "ü": "u",
    }
    translate = str.maketrans(MAP_VOCALES)
    text = text.translate(translate)

    # Eliminar textos entre corchetes (ej.: etiquetas)
    text = re.sub(r"\[.*?\]", "", text)

    # Eliminar URLs
    text = re.sub(r"https?://\S+|www\.\S+", "", text)

    # Eliminar etiquetas HTML
    text = re.sub(r"<.*?>+", "", text)

    # Eliminar signos de puntuación
    text = re.sub(f"[{re.escape(string.punctuation)}]", "", text)

    # Eliminar saltos de línea
    text = re.sub(r"\n", " ", text)

    # Eliminar palabras que contienen números
    text = re.sub(r"\w*\d\w*", "", text)

    # Eliminar emojis y caracteres especiales (no ASCII), excepto la ñ
    text = re.sub(r"[^\x00-\x7Fñ]+", "", text)

    # Eliminar espacios extras entre palabras
    text = re.sub(r"\s+", " ", text)

    # Eliminar espacios extras al inicio y final
    text = text.strip()

    return text
#!python3 -m spacy download es_core_news_sm
import nltk
import spacy
from nltk.corpus import stopwords

nltk.download("stopwords")
stopword_es = set(stopwords.words("spanish"))

nlp_es = spacy.load("es_core_news_sm")


def clean_stopwords_and_lemmatization(text):
    # Procesar el texto usando spaCy
    doc = nlp_es(text)
    # Eliminar stopwords y aplicar lematización
    lemmatized = [
        token.lemma_ for token in doc if token.text.lower() not in stopword_es
    ]
    # Unir los tokens lematizados y eliminar espacios extra
    return " ".join(lemmatized).strip()
[nltk_data] Downloading package stopwords to /home/joser/nltk_data...
[nltk_data]   Package stopwords is already up-to-date!
def predecir_sentimiento(review: str, modelo_sentimientos) -> int:
    """Predice el sentimiento de una reseña de película.

    Args:
        review (str): La reseña de la película.
    Returns:
        int: 1 si el sentimiento es positivo, 0 si es negativo.

    Example:
        >>> predecir_sentimiento("¡Me encantó esta película, fue fantástica!")
        1
        >>> predecir_sentimiento("No me gustó para nada, fue aburrida.")
        0
    """
    # Preprocesar el texto
    review_clean = limpiar_texto(review)
    review_lemmatized = clean_stopwords_and_lemmatization(review_clean)

    # Transformar el texto usando el mismo vectorizador TF-IDF
    review_tfidf = tfidf.transform([review_lemmatized])

    # Predecir el sentimiento usando el modelo cargado
    sentiment = modelo_sentimientos.predict(review_tfidf)

    return int(sentiment[0])
# Ejemplo de nueva reseña
new_review = "Esta película es excelente y superó mis expectativas."


# Realizar la predicción con el modelo cargado
prediction = predecir_sentimiento(new_review, mi_modelo)

# Mostrar la predicción (por ejemplo, 1 para positivo, 0 para negativo)
print("Predicción de sentimiento:", prediction)
Predicción de sentimiento: 1
# Ejemplo de nueva reseña
new_review = (
    "No me gustó para nada, fue aburrida, los actores eran pésimos y fue muy larga."
)

# Realizar la predicción con el modelo cargado
prediction = predecir_sentimiento(new_review, mi_modelo)

# Mostrar la predicción (por ejemplo, 1 para positivo, 0 para negativo)
print("Predicción de sentimiento:", prediction)
Predicción de sentimiento: 0
# Ejemplo de nueva reseña
new_review = "me gustó? ni siquiera la terminé de ver."

# Realizar la predicción con el modelo cargado
prediction = predecir_sentimiento(new_review, mi_modelo)

# Mostrar la predicción (por ejemplo, 1 para positivo, 0 para negativo)
print("Predicción de sentimiento:", prediction)
Predicción de sentimiento: 0

Librerías Usadas

from watermark import watermark

print(watermark(python=True, iversions=True, globals_=globals()))
Python implementation: CPython
Python version       : 3.12.11
IPython version      : 9.5.0

joblib   : 1.5.2
re       : 2.2.1
watermark: 2.5.0
numpy    : 2.3.3
nltk     : 3.9.1
sklearn  : 1.7.2
spacy    : 3.8.7
pandas   : 2.3.2

Referencias

Jose R. Zapata