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
15978film721023\nEl cine consiste en creerse una historia sea...2El árbol de la sangreEl árbol de la sangreEspaña6.04315el cine consiste en creerse una historia sea c...cin cons cre histori gener si fall fall primer...cine consistir creer él historia genero si fal...
43790film385351\nCon momentos así, la jungla de cristal es un...8Jungla de cristalDie HardakaEstados Unidos7.2120726con momentos asi la jungla de cristal es una d...moment asi jungl cristal mejor pelicul accion ...momento asi jungla cristal mejor pelicula acci...
18312film262344\nLes propongo la siguiente suma:\n\n\nLa ira ...10Sin perdónUnforgivenEstados Unidos8.297301les propongo la siguiente suma la ira del fora...propong siguient sum ira foraster puebl enter ...proponer siguiente sumar ira forastero pueblo ...
14191film748998\nLa cuarta e innecesaria entrega de las avent...4La jungla 4.0Live Free or Die HardakaEstados Unidos6.240889la cuarta e innecesaria entrega de las aventur...cuart innecesari entreg aventur john mcclan ac...cuarto innecesario entrega aventura john mccla...
23665film235499\nPara ver esta serie se debe seguir una lógic...6Sense8Sense8Estados Unidos7.115111para ver esta serie se debe seguir una logica ...ver seri deb segu logic simpl si gust seri per...ver serie deber seguir logica simple si gustar...

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

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): ['deadvertising' 'misionimpossible' 'generalde' 'irreversiblemente'
 'valencianar' 'eurospar' 'shaobo' 'fua' 'patiro' 'casposidad']

📍 Representación TF-IDF con TfidfVectorizer

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

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): ['mile' 'grunewald' 'pegarno' 'enchanted' 'histrionica' 'tropic' 'apolo'
 'refree' 'liviano' 'isaiah']
# 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

lda = LatentDirichletAllocation(n_components=5, random_state=123)
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 si pelicula mas hacer ser 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 él pelicula marvel personaje nuevo mejor dar
Tema 4:
pelicula mas ver hacer si él ser bien historia poder

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 = tfidf_matrix
y = data_reviews["sentiment_bin"]

Dividir el dataset en entrenamiento y prueba

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

Entrenar el clasificador Naive Bayes

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

Evaluar el modelo

y_pred = nb_classifier.predict(X_test)
print("Accuracy:", accuracy_score(y_test, y_pred))
print("Reporte de clasificación:\n", classification_report(y_test, y_pred))
Accuracy: 0.7494
Reporte de clasificación:
               precision    recall  f1-score   support

           0       0.84      0.55      0.67      4563
           1       0.71      0.91      0.80      5437

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

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

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

Referencias

Jose R. Zapata