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_id | author_review_desc | author_rating | film_title | film_original_title | film_country | film_average_rating | film_number_of_ratings | clean_review | clean_review_stemming | clean_review_lemmatization | |
---|---|---|---|---|---|---|---|---|---|---|---|
15978 | film721023 | \nEl cine consiste en creerse una historia sea... | 2 | El árbol de la sangre | El árbol de la sangre | España | 6.0 | 4315 | el 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... |
43790 | film385351 | \nCon momentos así, la jungla de cristal es un... | 8 | Jungla de cristal | Die Hardaka | Estados Unidos | 7.2 | 120726 | con momentos asi la jungla de cristal es una d... | moment asi jungl cristal mejor pelicul accion ... | momento asi jungla cristal mejor pelicula acci... |
18312 | film262344 | \nLes propongo la siguiente suma:\n\n\nLa ira ... | 10 | Sin perdón | Unforgiven | Estados Unidos | 8.2 | 97301 | les propongo la siguiente suma la ira del fora... | propong siguient sum ira foraster puebl enter ... | proponer siguiente sumar ira forastero pueblo ... |
14191 | film748998 | \nLa cuarta e innecesaria entrega de las avent... | 4 | La jungla 4.0 | Live Free or Die Hardaka | Estados Unidos | 6.2 | 40889 | la cuarta e innecesaria entrega de las aventur... | cuart innecesari entreg aventur john mcclan ac... | cuarto innecesario entrega aventura john mccla... |
23665 | film235499 | \nPara ver esta serie se debe seguir una lógic... | 6 | Sense8 | Sense8 | Estados Unidos | 7.1 | 15111 | para 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