Ir al contenido principal

Detección de reseñas de hoteles veraces y engañosas con machine learning

En este tutorial usarás un algoritmo de machine learning para resolver un problema real en Python. Aprenderás a leer múltiples archivos de texto en Python, extraer etiquetas, usar dataframes y mucho más.
Actualizado 17 sept 2026  · 11 min leer

Explorar con IA

ChatGPTClaudePerplexity

En este tutorial trabajarás con un conjunto de datos de texto, el Deceptive Opinion Spam Corpus, como ejemplo.

\n

En pocas palabras, hoy veremos:

\n
    \n
  • Empezarás con una breve introducción al conjunto de datos de reseñas de hoteles e importarás todos los módulos necesarios,
  • \n
  • Después entenderás la estructura de los datos: obtener todos los archivos de texto desde la ruta, extraer las etiquetas, crear un dataframe con etiquetas y reseñas y, por último, unir ambos dataframes,
  • \n
  • Luego preprocesarás los datos: eliminarás las stopwords de las reseñas con una biblioteca de PLN y extraerás las categorías gramaticales de cada palabra del conjunto de datos.
  • \n
  • Una vez preprocesados los datos, prepararás el entrenamiento: dividirás en entrenamiento y prueba, aprenderás qué es Tfidf, vectorizarás los conjuntos de entrenamiento y prueba, implementarás el modelo de máquinas de vectores de soporte, guardarás tanto el vectorizador Tfidf como el modelo y los volverás a cargar.
  • \n
  • Por último, predecirás sobre los datos de prueba: representarás la matriz de confusión, la precisión y el informe de clasificación para comprobar la eficacia del modelo; también lo validarás variando el estado aleatorio al dividir los datos y, para terminar, probarás el modelo con dos reseñas reales de Yelp.
  • \n
\n

\n

Entender el conjunto de datos Deceptive Opinion Spam

\n

Antes de cargar los datos, conviene revisar exactamente con qué vas a trabajar. Este corpus contiene reseñas veraces y engañosas de 20 hoteles de Chicago. El conjunto se describe en dos artículos según el sentimiento de la reseña. En concreto, las reseñas de sentimiento positivo se tratan en 1 y las de sentimiento negativo en 2. Puedes consultar los artículos para profundizar.

\n

El corpus incluye:

\n
    \n
  • 400 reseñas veraces y positivas de TripAdvisor
  • \n
  • 400 reseñas positivas engañosas de Mechanical Turk
  • \n
  • 400 reseñas veraces y negativas de Expedia, Hotels.com, Orbitz, Priceline, TripAdvisor y Yelp
  • \n
  • 400 reseñas negativas engañosas de Mechanical Turk
  • \n
\n

En total son 1600 reseñas. Tu tarea será clasificar las reseñas veraces y las engañosas con un algoritmo de machine learning.

¡Vamos a ello!

\n

Importar todos los módulos necesarios

\n

Empieza importando módulos como os, pandas, nltk, regex y, muy importante, Sklearn, ya que en este tutorial usarás el algoritmo de machine learning Support Vector Machine que ofrece la biblioteca Sklearn.

\n

Necesitarás os para iterar por carpetas y subcarpetas donde residen los archivos de texto; fnmatch para filtrar solo los .txt; pandas para estructurar los datos en filas y columnas y poder manipularlos; Regex para extraer datos según patrones; Nltk para eliminar stopwords que no quieres que tu modelo aprenda. Y, por último, Sklearn te proporcionará librerías para manipular y vectorizar los datos, aprender la frontera entre puntos con un algoritmo de ML y, finalmente, calcular la precisión.

\n
import os\nimport fnmatch\nfrom textblob import TextBlob\nimport pandas as pd\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom nltk.corpus import stopwords\nfrom nltk import pos_tag,pos_tag_sents\nimport regex as re\nimport operator\nfrom sklearn.svm import SVC, LinearSVC\nfrom sklearn.metrics import classification_report, accuracy_score, confusion_matrix\nfrom sklearn_cross_validation import train_test_split  \nfrom sklearn import metrics\nfrom sklearn import svm\nfrom sklearn.grid_search import GridSearchCV\nimport pickle\nfrom nltk.corpus import stopwords\n
\n

\n

Obtener todos los archivos de texto de la ruta, extraer sus etiquetas y crear un dataframe con las etiquetas

\n
\"estructura
Figura: estructura de los datos
\n


Tras importar las librerías, lo siguiente es extraer las etiquetas. Las etiquetas ayudarán al modelo a distinguir si una reseña pertenece a la clase veraz o a la engañosa.

\n

Primero iterarás por todos los archivos de texto para obtener su ruta absoluta y, a partir de ella, extraer las etiquetas correspondientes.

\n
path = 'op_spam_train/'\n\nlabel = []\n\nconfigfiles = [os.path.join(subdir,f)\nfor subdir, dirs, files in os.walk(path)\n    for f in fnmatch.filter(files, '*.txt')]\n
\n

Deberías obtener 1600 rutas, cada una para un archivo de texto. ¡Vamos a comprobarlo!

\n
len(configfiles)\n
\n
1600\n
\n

Imprimamos también una de las rutas.

\n
configfiles[1]\n
\n
'op_spam_train/negative_polarity/deceptive_from_MTurk/fold4/d_swissotel_14.txt'\n
\n

Como ves, para extraer las etiquetas necesitaremos algún filtro. Para ello usaremos Regex (expresiones regulares).

\n
for f in configfiles:\n    c = re.search('(trut|deceptiv)\\w',f)\n    label.append(c.group())\n
\n

Una vez extraídas las etiquetas en una lista llamada label, creamos un dataframe con ella.

\n
labels = pd.DataFrame(label, columns = ['Labels'])\n
\n

Mostramos las cinco primeras filas del dataframe labels.

\n
labels.head(5)\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
 Labels
0deceptive
1deceptive
2deceptive
3deceptive
4deceptive
\n
\n

Obtener todas las reseñas y guardarlas en una lista

\n

Una vez que tienes todas las etiquetas, toca extraer las reseñas de los archivos de texto.

\n
review = []\ndirectory =os.path.join(\"op_spam_train\")\nfor subdir,dirs ,files in os.walk(directory):\n   # print (subdir)\n    for file in files:\n        if fnmatch.filter(files, '*.txt'):\n            f=open(os.path.join(subdir, file),'r')\n            a = f.read()\n            review.append(a)\n
\n

Como antes, ahora creamos un dataframe a partir de la lista review.

\n
reviews = pd.DataFrame(review, columns = ['HotelReviews'])\n
\n

Mostramos las cinco primeras filas del dataframe reviews.

\n
reviews.head(5)\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
 HotelReviews
0Grant it, this hotel seems very nice, but I wa...
1I recently stayed at the Swissotel Chicago wit...
2I was sorely disappointed with the Sheraton Ch...
3My family and I stayed at the Sheraton Chicago...
4I recently stayed at the Homewood Suites by Hi...
\n
\n

¡Genial! Hasta aquí, todo bastante intuitivo, ¿verdad?

\n

Ahora unamos los dataframes de labels y reviews.

\n

Unir los dataframes de reseñas y etiquetas

\n
result = pd.merge(reviews, labels,right_index=True,left_index = True)\n\n\nresult['HotelReviews'] = result['HotelReviews'].map(lambda x: x.lower())\n
\n

Tras la unión, tienes un nuevo dataframe llamado result. Veamos algunas filas.

\n
result.head()\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
 HotelReviewsLabels
0grant it, this hotel seems very nice, but i wa...deceptive
1i recently stayed at the swissotel chicago wit...deceptive
2i was sorely disappointed with the sheraton ch...deceptive
3my family and i stayed at the sheraton chicago...deceptive
4i recently stayed at the homewood suites by hi...deceptive
\n
\n

\n

Eliminar stopwords de la columna de reseñas

\n

Cuando trabajas con texto, es clave eliminar las stopwords. No aportan significado y no ayudan al modelo a aprender patrones útiles.

\n

Crearemos una nueva columna llamada review_without_stopwords. La función lambda procesará todas las filas de HotelReviews. Usaremos una lista por comprensión para guardar solo las palabras que no estén en la variable stop.

\n
import ntlk\nnltk.download('stopwords')\n
\n

Nota: para ejecutar la celda siguiente, puede que necesites correr las líneas anteriores en tu terminal.

\n
stop = stopwords.words('english')\n\nresult['review_without_stopwords'] = result['HotelReviews'].apply(lambda x: ' '.join([word for word in x.split() if word not in (stop)]))\n
\n

¡Veamos rápidamente el dataframe sin stopwords!

\n
result.head()\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
 HotelReviewsLabelsreview_without_stopwords
0grant it, this hotel seems very nice, but i wa...deceptivegrant it, hotel seems nice, pleased stay here....
1i recently stayed at the swissotel chicago wit...deceptiverecently stayed swissotel chicago husband two ...
2i was sorely disappointed with the sheraton ch...deceptivesorely disappointed sheraton chicago. outside ...
3my family and i stayed at the sheraton chicago...deceptivefamily stayed sheraton chicago hotel towers be...
4i recently stayed at the homewood suites by hi...deceptiverecently stayed homewood suites hilton chicago...
\n
\n

Extraer categorías gramaticales de las reseñas para usarlas como características del modelo

\n

Según 1, las opiniones veraces y engañosas podrían clasificarse, respectivamente, en géneros informativos e imaginativos. Hay una diferencia distributiva marcada: el primero suele contener más sustantivos, adjetivos, preposiciones, determinantes y conjunciones coordinadas, mientras que el segundo incluye más verbos, adverbios, pronombres y predeterminantes. Además, no sorprende que las opiniones engañosas contengan más superlativos, ya que a menudo recurren a un lenguaje exagerado.

\n

Ahora, para cada palabra de cada fila del dataframe, extraeremos su categoría gramatical y la usaremos como vector de características para el modelo.

\n

Para el etiquetado pos usaremos TextBlob, una librería de Python para procesar texto. Ofrece una API sencilla para tareas comunes de PLN como etiquetado gramatical, extracción de sintagmas nominales, análisis de sentimiento, clasificación, traducción y más.

\n

Nota: para ejecutar la celda de la función pos, puede que necesites correr estas líneas en tu terminal.

\n
import nltk\nnltk.download('punkt')\nnltk.download('averaged_perceptron_tagger')\n
\n
def pos(review_without_stopwords):\n    return TextBlob(review_without_stopwords).tags\n
\n
os = result.review_without_stopwords.apply(pos)\nos1 = pd.DataFrame(os)\n
\n

Cada fila del dataframe os1 contendrá una lista de palabras con su categoría gramatical. No podrás vectorizar una lista tal cual para alimentar el modelo, así que tendrás que convertir esas listas en cadenas.

\n
os1.head()\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
 review_without_stopwords
0[(grant, NN), (it, PRP), (hotel, NN), (seems, ...
1[(recently, RB), (stayed, VBN), (swissotel, NN...
2[(sorely, RB), (disappointed, JJ), (sheraton, ...
3[(family, NN), (stayed, VBD), (sheraton, JJ), ...
4[(recently, RB), (stayed, VBN), (homewood, NN)...
\n
\n

Convirtamos cada fila en una cadena, uniendo cada palabra con su etiqueta pos mediante una barra inclinada y separando palabras con un espacio.

\n
os1['pos'] = os1['review_without_stopwords'].map(lambda x:\" \\n\".join([\"\".join(x) for x in x ]) )\n
\n

Por último, unimos la columna pos con el dataframe principal result y mostramos algunas filas.

\n
result = result = pd.merge(result, os1,right_index=True,left_index = True)\nresult.head()\n
\n
\"primeras
\n

\n

Fase de entrenamiento

\n

Dividir los datos en dos partes: 80% entrenamiento y 20% prueba

\n

Con las características ya extraídas, toca dividir los datos en entrenamiento y prueba. En este tutorial usaremos un 80% para entrenar y un 20% para probar.

\n
review_train, review_test, label_train, label_test = train_test_split(result['pos'],result['Labels'], test_size=0.2,random_state=13)\n
\n

Vectorizar los datos de entrenamiento y prueba con TfidfVectorizer

\n

Entendamos en detalle qué hace TfidfVectorizer.

\n

Tfidf tiene dos partes:

\n
    \n
      \n
    • Frecuencia de término (Tf): cuántas veces aparece una palabra en un documento. Por ejemplo, la palabra “the” es muy común y tendrá alta frecuencia en todos tus documentos, pero no aporta contexto. En cambio, “messi” sí aporta contexto: probablemente el documento trate de fútbol. Podrías pensar en dar menos peso a “the”. También existen palabras muy raras, como “floccinaucinihilipilification” (“El acto o hábito de considerar algo sin valor”). Si usas simplemente el recíproco de la frecuencia, podría pesar parecido a “messi”, pero tampoco aporta contexto. Por ejemplo: tf(“the”) = 100, tf(“messi”) = 5, tf(“floccinaucinihilipilification”) = 1.
    • \n
    \n
\n\n
    \n
  • Frecuencia inversa de documento (idf): resuelve el problema de palabras muy frecuentes o muy raras. Se calcula con el logaritmo de {número de documentos del corpus dividido entre el número de documentos en los que aparece el término}. Para “the”, la razón será cercana a 1 y su log a 0. idf(“the”) = 0; idf(“messi”) = log(10/3) = 0,52 (si el corpus es de fútbol); idf(“floccinaucinihilipilification”) = log(10/1) = 1.
  • \n
  • Último paso: queremos que el peso de “messi” sea mayor que el de las otras dos. Multiplicamos: tfidf = tf x idf. “the” = 100 x 0 = 0; “floccinaucinihilipilification” = 1 x 1 = 1; “messi” = 5 x 0,52 = 2,6.
  • \n
\n

Esta ponderación ayuda al modelo de ML en clasificación, ya que indicamos explícitamente qué palabras pesan más o menos.

\n

Ahora que lo conoces, apliquémoslo a entrenamiento y prueba.

\n
tf_vect = TfidfVectorizer(lowercase = True, use_idf=True, smooth_idf=True, sublinear_tf=False)\n\nX_train_tf = tf_vect.fit_transform(review_train)\nX_test_tf = tf_vect.transform(review_test)\n
\n

Implementación del modelo

\n

Ahora implementarás un modelo de machine learning llamado Support Vector Machines (SVM). Para entenderlo mejor, puedes seguir este enlace.

\n

Para seleccionar los mejores hiperparámetros, usarás GridSearchCV, que, en base a tus datos de entrenamiento y etiquetas, sugiere los mejores valores de entre las listas que especifiques. Elegiremos cinco valores para C y gamma y obtendremos los óptimos según los datos.

\n
def svc_param_selection(X, y, nfolds):\n    Cs = [0.001, 0.01, 0.1, 1, 10]\n    gammas = [0.001, 0.01, 0.1, 1]\n    param_grid = {'C': Cs, 'gamma' : gammas}\n    grid_search = GridSearchCV(svm.SVC(kernel='linear'), param_grid, cv=nfolds)\n    grid_search.fit(X, y)\n    return grid_search.best_params_\n
\n
svc_param_selection(X_train_tf,label_train,5)\n
\n
{'C': 10, 'gamma': 0.001}\n
\n
clf = svm.SVC(C=10,gamma=0.001,kernel='linear')\nclf.fit(X_train_tf,label_train)\npred = clf.predict(X_test_tf)\n
\n

Guardar el vectorizador Tfidf y el modelo de ML

\n

Guardemos el modelo que acabas de entrenar, junto con el vectorizador Tfidf, usando la librería pickle que importaste al principio. Así, más adelante podrás cargar los datos, vectorizarlos y predecir con el modelo sin reentrenar.

\n
with open('vectorizer.pickle', 'wb') as fin:\n    pickle.dump(tf_vect, fin)\n
\n
with open('mlmodel.pickle','wb') as f:\n    pickle.dump(clf,f)\n
\n

Cargar el vectorizador Tfidf y el modelo de ML

\n
pkl = open('mlmodel.pickle', 'rb')\nclf = pickle.load(pkl)   \nvec = open('vectorizer.pickle', 'rb')\ntf_vect = pickle.load(vec)\n
\n
/home/naveksha/.local/lib/python3.5/site-packages/sklearn/base.py:311: UserWarning: Trying to unpickle estimator SVC from version pre-0.18 when using version 0.19.1. This might lead to breaking code or invalid results. Use at your own risk.\n  UserWarning)\n/home/naveksha/.local/lib/python3.5/site-packages/sklearn/base.py:311: UserWarning: Trying to unpickle estimator TfidfTransformer from version pre-0.18 when using version 0.19.1. This might lead to breaking code or invalid results. Use at your own risk.\n  UserWarning)\n/home/naveksha/.local/lib/python3.5/site-packages/sklearn/base.py:311: UserWarning: Trying to unpickle estimator TfidfVectorizer from version pre-0.18 when using version 0.19.1. This might lead to breaking code or invalid results. Use at your own risk.\n  UserWarning)\n
\n

\n

Predecir sobre los datos de prueba

\n
X_test_tf = tf_vect.transform(review_test)\npred = clf.predict(X_test_tf)\n
\n

Representar la matriz de confusión, la precisión y el informe de clasificación para analizar el rendimiento

\n
print(metrics.accuracy_score(label_test, pred))\n
\n
0.96875\n
\n
print (confusion_matrix(label_test, pred))\n
\n
[[149   6]\n [  4 161]]\n
\n

La matriz de confusión anterior es de 2 x 2: el primer elemento de la primera fila es Verdadero Positivo (TP), el segundo es Falso Negativo (FN), el primero de la segunda fila es Falso Positivo (FP) y el segundo es Verdadero Negativo (TN).

\n

De esta matriz se concluye que solo 10 muestras de prueba de 320 se clasificaron mal.

\n
print (classification_report(label_test, pred))\n
\n
             precision    recall  f1-score   support\n\n  deceptive       0.97      0.96      0.97       155\n      truth       0.96      0.98      0.97       165\n\navg / total       0.97      0.97      0.97       320\n
\n

Probar el modelo con distintos estados aleatorios

\n
    \n
  • Random State 1
  • \n
\n
review_train, review_test, label_train, label_test = train_test_split(result['pos'],result['Labels'], test_size=0.2,random_state=1)\n
\n
X_test_tf = tf_vect.transform(review_test)\npred = clf.predict(X_test_tf)\n
\n
print(metrics.accuracy_score(label_test, pred))\n
\n
0.978125\n
\n
print (confusion_matrix(label_test, pred))\n
\n
[[146   5]\n [  2 167]]\n
\n
print (classification_report(label_test, pred))\n
\n
             precision    recall  f1-score   support\n\n  deceptive       0.99      0.97      0.98       151\n      truth       0.97      0.99      0.98       169\n\navg / total       0.98      0.98      0.98       320\n
\n
    \n
  • Random State 10
  • \n
\n
review_train, review_test, label_train, label_test = train_test_split(result['pos'],result['Labels'], test_size=0.2,random_state=10)\n
\n
X_test_tf = tf_vect.transform(review_test)\npred = clf.predict(X_test_tf)\n
\n
print(metrics.accuracy_score(label_test, pred))\n
\n
0.98125\n
\n
print (confusion_matrix(label_test, pred))\n
\n
[[155   5]\n [  1 159]]\n
\n
print (classification_report(label_test, pred))\n
\n
             precision    recall  f1-score   support\n\n  deceptive       0.99      0.97      0.98       160\n      truth       0.97      0.99      0.98       160\n\navg / total       0.98      0.98      0.98       320\n
\n\n
    \n
  • Random State 42
  • \n
\n\n
review_train, review_test, label_train, label_test = train_test_split(result['pos'],result['Labels'], test_size=0.2,random_state=42)\n
\n
X_test_tf = tf_vect.transform(review_test)\npred = clf.predict(X_test_tf)\n
\n
print(metrics.accuracy_score(label_test, pred))\n
\n
0.984375\n
\n
print (confusion_matrix(label_test, pred))\n
\n
[[165   3]\n [  2 150]]\n
\n
print (classification_report(label_test, pred))\n
\n
             precision    recall  f1-score   support\n\n  deceptive       0.99      0.98      0.99       168\n      truth       0.98      0.99      0.98       152\n\navg / total       0.98      0.98      0.98       320\n
\n

Como puedes ver, el modelo ha hecho un gran trabajo y no muestra sobreajuste, ya que lo probaste varias veces dividiendo los datos de forma diferente.

\n

Probar el modelo con dos reseñas de Yelp

\n
def test_string(s):\n    X_test_tf = tf_vect.transform([s])\n    y_predict = clf.predict(X_test_tf)\n    return y_predict\n
\n
test_string(\"The hotel was good.The room had a 27-inch Samsung led tv, a microwave.The room had a double bed\")\n
\n
array(['truth'], dtype=object)\n
\n
test_string(\"My family and I are huge fans of this place. The staff is super nice, and the food is great. The chicken is very good, and the garlic sauce is perfect. Ice cream topped with fruit is delicious too. Highly recommended!\")\n
\n
array(['deceptive'], dtype=object)\n
\n

El modelo predijo correctamente ambas reseñas: la primera es veraz y la segunda engañosa.

\n

Nota

\n

El rendimiento del modelo puede variar al implementarlo en equipos con especificaciones distintas; es un comportamiento inusual observado. Si la comunidad puede ayudar a identificar el motivo, sería de gran ayuda para muchas personas, incluida la autora o el autor de este artículo. Por comunidad nos referimos a quienes lean este tutorial.

\n

¡Feliz aprendizaje!

\n

Esperamos que este tutorial te haya resultado útil y aporte valor a tus competencias.

\n

Si quieres profundizar en Machine Learning, echa un vistazo al curso de DataCamp Building Chatbots in Python.

Temas
Python
Aprendizaje automático

Aprende más sobre Python y machine learning

Curso

Comprender el machine learning

2 h
308K
Introducción al machine learning, ¡y no hay que programar!
Ver detallesRight Arrow
Iniciar Curso
Ver másRight Arrow
Relacionado

Tutorial

Aprendizaje automático de datos categóricos con el tutorial de Python

Aprenda los trucos más comunes para manejar datos categóricos y preprocesarlos para construir modelos de aprendizaje automático.
Moez Ali's photo

Moez Ali

14 min

Tutorial

Clasificación de textos en Python

Descubra qué es la clasificación de textos, cómo funciona y casos de uso con éxito. Explore ejemplos de principio a fin sobre cómo crear un canal de preprocesamiento de texto seguido de un modelo de clasificación de texto en Python.
Moez Ali's photo

Moez Ali

12 min

Tutorial

Tutorial de análisis NLTK del sentimiento para principiantes

Tutorial de análisis del sentimiento NLTK (natural language toolkit) de Python. Aprende a crear y desarrollar análisis del sentimiento utilizando Python. Sigue pasos específicos para extraer y analizar texto para el procesamiento del lenguaje natural.
Moez Ali's photo

Moez Ali

13 min

Python

Tutorial

Comprender la regresión logística en el tutorial de Python

Aprende sobre la regresión logística, sus propiedades básicas, y construye un modelo de machine learning sobre una aplicación del mundo real en Python.
Avinash Navlani's photo

Avinash Navlani

10 min

Tutorial

Tutorial de Generación de nubes de palabras en Python

Aprende a realizar Análisis exploratorios de datos para el Procesamiento del lenguaje natural utilizando WordCloud en Python.
Duong Vu's photo

Duong Vu

11 min

Tutorial

Introducción a Q-learning: tutorial para principiantes

Conoce el algoritmo de aprendizaje por refuerzo sin modelo más popular con un tutorial en Python.
Abid Ali Awan's photo

Abid Ali Awan

11 min

Ver MásVer Más