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 | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 44147 | film787296 | \nEl páramo tiene una buena idea detrás, pero ... | 6 | El páramo | El páramo | España | 4.3 | 3415 | el paramo tiene una buena idea detras pero el ... | param buen ide detr desarroll esper confus cla... | paramo buen idea detra desarrollo esperar conf... |
| 37977 | film671403 | \nMe encanta como esta rodada y lo que me hace... | 8 | Adiós, muchachos | Au revoir les enfantsaka | Francia | 7.8 | 15937 | me 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... |
| 17483 | film325218 | \nToda la culpa la tiene la Lata de tomate, po... | 5 | Flash | The Flashaka | Estados Unidos | 6.0 | 13664 | toda 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... |
| 2372 | film512859 | \nEntro a valorar la película y veo críticas b... | 4 | Una boda explosiva | Shotgun Weddingaka | Estados Unidos | 4.2 | 2711 | entro a valorar la pelicula y veo criticas bas... | entro valor pelicul veo critic bastant decent ... | entro valorar pelicula ver critica bastante de... |
| 6801 | film489970 | \n¿Quién lo iba a decir? En fin, otra serie am... | 10 | Breaking Bad | Breaking Bad | Estados Unidos | 8.8 | 106146 | quien 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