Tras las imágenes del proyecto 2, otro dato no tabular: el texto. En este proyecto construirás de punta a punta un clasificador de sentimiento —positivo o negativo— para opiniones en redes sociales sobre un servicio de entrega a domicilio, con el NLP clásico: limpieza de texto, representación bag-of-words y TF-IDF, y los modelos del módulo 4, con el Naive Bayes multinomial de 04-06 como protagonista (allí te lo anticipamos "para texto"; hoy cumple su promesa). Para que el proyecto sea autocontenido, el dataset es ficticio pero realista: unas 40 opiniones en español sobre "RepartoYa", un servicio de entrega del estilo de MercaFresh, construidas en el propio código. En un caso real serían miles de filas de un CSV público — al final del proyecto te decimos dónde conseguirlas.
Contenido
- Definición del problema y métrica objetivo
- Construcción del dataset
- Exploración del texto
- Limpieza y normalización
- Representación: de palabras a números
- Modelado: Naive Bayes, logística y SVM lineal
- Evaluación con validación cruzada estratificada
- Interpretación: qué palabras pesan
- Límites del enfoque clásico
- Conclusiones
Definición del problema y métrica objetivo
- Problema: clasificación binaria de textos cortos — ¿la opinión es positiva (1) o negativa (0)?
- Uso de negocio: priorizar la atención al cliente (responder antes a los enfados) y medir la salud de la marca en redes.
- Métrica: con clases equilibradas como aquí, accuracy más la matriz de confusión; en un despliegue real interesaría especialmente el recall de la clase negativa (que no se escape ningún cliente furioso), el mismo razonamiento por costes de 06-04 "Curva ROC y AUC".
- Criterio de éxito: batir al azar (50 %) con claridad y, sobre todo, que el modelo sea interpretable: debemos poder ver qué palabras lo convencen.
Construcción del dataset
Con 40 ejemplos no se entrena un producto: se aprende un flujo. Los construimos equilibrados y con los vicios reales del texto en redes (mayúsculas, URLs, menciones, emojis, exceso de signos):
import pandas as pd
positivos = [
"Me ha llegado el pedido en 20 minutos, increíble servicio!!",
"La fruta fresquísima y el repartidor amabilísimo 😍",
"@RepartoYa sois los mejores, todo perfecto como siempre",
"Muy contenta con la entrega, puntual y todo bien embalado",
"Repetiré seguro, precios bien y entrega rapidísima",
"El pescado congelado llegó impecable, gran trabajo",
"Atención al cliente de 10, me resolvieron el problema al momento",
"Qué maravilla poder hacer la compra sin salir de casa 👏",
"Pedido completo y sin errores, así da gusto",
"La app es comodísima y el reparto muy rápido",
"Excelente como siempre, ni una queja en seis meses",
"El repartidor subió las bolsas hasta el quinto, un crack",
"Todo fresco, todo puntual, recomendadísimo https://repartoya.example",
"Me avisaron del retraso y me regalaron el envío, bien gestionado",
"Los sustitutos que eligieron fueron mejores que mi pedido original",
"Entrega en franja de una hora y clavada, perfecto",
"Calidad excelente en la carne, volveré a pedir",
"Muy buen servicio, la web funciona genial",
"Cinco estrellas, rápido y sin roturas",
"El mejor súper online que he probado, de verdad",
]
negativos = [
"Dos horas de retraso y nadie contesta al teléfono 😡",
"@RepartoYa me habéis traído los yogures CADUCADOS, vergonzoso",
"Pedido incompleto otra vez, faltaban 5 productos",
"La leche llegó caliente y el helado hecho sopa, fatal",
"Imposible contactar con atención al cliente, pésimo servicio",
"Me cobraron dos veces y aún espero la devolución",
"Las bolsas rotas y los huevos aplastados, un desastre",
"Cancelaron mi pedido sin avisar, no repito ni loca",
"El repartidor dejó la compra en otro portal, increíble pero mal",
"Fruta pasada y pan duro, qué decepción https://queja.example",
"45 minutos al teléfono para nada, horrible experiencia",
"Tercera vez que llega tarde, no aprenden",
"La app se cuelga al pagar, imposible terminar la compra",
"Me sustituyeron el salmón por palitos de cangrejo, en serio??",
"Todo carísimo y encima el envío tarde, no vale la pena",
"Pedí en oferta y me cobraron precio normal, engañosos",
"Nadie responde en redes, pésima atención",
"El pedido llegó congelado cuando era fresco, muy mal",
"Una hora esperando en el portal porque no encontraban la calle",
"Decepcionante de principio a fin, no lo recomiendo 👎",
]
df = pd.DataFrame({
"texto": positivos + negativos,
"sentimiento": [1] * len(positivos) + [0] * len(negativos),
})
print(df.shape, "| balance:", df["sentimiento"].mean())Transparencia metodológica: en un proyecto real este paso sería pd.read_csv(...) sobre miles de opiniones etiquetadas (a mano o por reglas como las estrellas de una reseña). Todo lo que sigue funciona idéntico con 40 filas o con 40.000 — solo cambia la solidez estadística de los resultados, como te enseñó 02-04 "Inferencia estadística": con muestras pequeñas, intervalos anchos y humildad.
Exploración del texto
El EDA de texto empieza contando cosas:
df["n_palabras"] = df["texto"].str.split().str.len()
print(df.groupby("sentimiento")["n_palabras"].describe().round(1))
from collections import Counter
palabras_neg = Counter(" ".join(df[df.sentimiento == 0]["texto"]).lower().split())
print(palabras_neg.most_common(10))Ya se ven pistas: en los negativos abundan "tarde", "pésimo", "nadie", "otra vez"; en los positivos "rápido", "perfecto", "fresca". El clasificador que construiremos formalizará exactamente esta intuición.
Limpieza y normalización
El texto en bruto de redes trae ruido que no aporta señal de sentimiento (URLs, menciones) y variantes superficiales de lo mismo ("Increíble" vs. "increíble"). La limpieza es el equivalente textual de 03-01 "Limpieza de datos":
import re
def limpiar(texto):
t = texto.lower() # 1. minúsculas
t = re.sub(r"https?://\S+", " ", t) # 2. URLs fuera
t = re.sub(r"@\w+", " ", t) # 3. menciones fuera
t = re.sub(r"[^\wáéíóúüñ\s]", " ", t) # 4. signos y emojis fuera
t = re.sub(r"\s+", " ", t).strip() # 5. espacios múltiples
return t
df["texto_limpio"] = df["texto"].map(limpiar)
print(df[["texto", "texto_limpio"]].head(3).to_string())Cada paso es una decisión con coste: al eliminar emojis perdemos 😡 y 😍, que son señal purísima de sentimiento (una mejora obvia sería convertirlos en tokens como emoji_enfado antes de limpiar). La tokenización — trocear el texto en palabras — la hará el vectorizador por nosotros separando por no-alfanuméricos; para este enfoque simple es suficiente.
Representación: de palabras a números
Ningún modelo del módulo 4 come texto: comen matrices numéricas. La solución clásica es bag-of-words ("bolsa de palabras"): cada palabra distinta del vocabulario es una columna, y cada texto una fila con el número de veces que usa cada palabra. Se pierde el orden ("no me gusta" y "me gusta no" son iguales) pero se gana una tabla sobre la que todo lo aprendido funciona.
CountVectorizer construye esa matriz; TfidfVectorizer la mejora ponderando: TF-IDF multiplica la frecuencia de la palabra en el documento (TF) por un factor que penaliza a las palabras que aparecen en casi todos los documentos (IDF). Así "pedido", que está en todas partes, pesa poco, y "caducados", rara pero reveladora, pesa mucho.
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
# sklearn no trae stopwords en español: definimos las nuestras
stopwords_es = [
"de", "la", "el", "en", "y", "a", "que", "los", "las", "un", "una",
"me", "mi", "al", "del", "por", "con", "para", "es", "se", "lo", "le",
]
vectorizador = TfidfVectorizer(
stop_words=stopwords_es,
ngram_range=(1, 2), # unigramas y bigramas
min_df=1,
)
X_demo = vectorizador.fit_transform(df["texto_limpio"])
print(X_demo.shape) # (40, n_terminos): matriz dispersaDos parámetros merecen explicación:
ngram_range=(1, 2): además de palabras sueltas, usa pares consecutivos (bigramas). Es la forma barata de recuperar algo de orden: "no recomiendo" como término propio distingue lo que "no" y "recomiendo" por separado confunden.stop_words: palabras vacías ("de", "la", "que") que solo aportan ruido. Ojo: con sentimiento hay que podar con cuidado — "no" jamás debe ser stopword aquí.
La matriz resultante es dispersa (casi todo ceros): cada texto usa unas pocas decenas de un vocabulario de cientos. sklearn la maneja eficientemente sin convertirla a densa.
Modelado: Naive Bayes, logística y SVM lineal
Tres clásicos idóneos para matrices dispersas de alta dimensión, cada uno en su Pipeline (vectorizador incluido, para que el vocabulario se aprenda solo del train de cada partición — la disciplina anti-fugas de 06-01):
from sklearn.pipeline import Pipeline
from sklearn.naive_bayes import MultinomialNB
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
def pipe(modelo):
return Pipeline([
("vec", TfidfVectorizer(stop_words=stopwords_es, ngram_range=(1, 2))),
("clf", modelo),
])
modelos = {
"MultinomialNB": pipe(MultinomialNB()),
"Logística": pipe(LogisticRegression(max_iter=1000)),
"SVM lineal": pipe(LinearSVC()),
}MultinomialNB es el reencuentro prometido en 04-06 "Naive Bayes": modela la probabilidad de cada palabra dada la clase (¿cuál es la probabilidad de ver "caducados" en una opinión negativa?) y combina todas con el teorema de Bayes de 02-05, asumiendo independencia entre palabras — una suposición falsa que en texto funciona sorprendentemente bien. La logística de 04-02 y la SVM lineal de 04-04 aprenden en cambio un peso por término, lo que las hace muy interpretables.
Evaluación con validación cruzada estratificada
Con 40 ejemplos, una única división train/test sería una lotería. Usamos la CV estratificada de 06-03 "Validación cruzada", que además mantiene el 50/50 de clases en cada partición:
from sklearn.model_selection import StratifiedKFold, cross_val_score, cross_val_predict
from sklearn.metrics import confusion_matrix, classification_report
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for nombre, modelo in modelos.items():
acc = cross_val_score(modelo, df["texto_limpio"], df["sentimiento"], cv=cv)
print(f"{nombre}: {acc.mean():.2f} ± {acc.std():.2f}")
# Matriz de confusión agregada del mejor
y_pred = cross_val_predict(modelos["MultinomialNB"],
df["texto_limpio"], df["sentimiento"], cv=cv)
print(confusion_matrix(df["sentimiento"], y_pred))
print(classification_report(df["sentimiento"], y_pred,
target_names=["negativo", "positivo"]))| Modelo | Accuracy (CV 5) | Comentario |
|---|---|---|
| MultinomialNB | ≈ 0,85–0,95 | Excelente con poco dato: su sesgo fuerte le protege |
| Logística | ≈ 0,80–0,90 | Muy interpretable; pedirá más datos para brillar |
| SVM lineal | ≈ 0,80–0,90 | Similar; suele ganar cuando hay miles de ejemplos |
Con 8 ejemplos por partición de test, cada acierto mueve la métrica un 12,5 %: las desviaciones son enormes y ningún ranking entre estos tres es concluyente. La conclusión honesta es la contraria: los tres baten con claridad al azar, señal de que la señal léxica del sentimiento es fuerte. Con miles de ejemplos las diferencias sí serían medibles.
Interpretación: qué palabras pesan
La gran virtud del enfoque clásico: se puede auditar. Entrenamos la logística con todo y miramos sus coeficientes por término (positivo = empuja hacia sentimiento positivo):
modelo = modelos["Logística"].fit(df["texto_limpio"], df["sentimiento"])
vec = modelo.named_steps["vec"]
pesos = pd.Series(modelo.named_steps["clf"].coef_[0],
index=vec.get_feature_names_out())
print("Términos más POSITIVOS:\n", pesos.nlargest(10).round(2))
print("\nTérminos más NEGATIVOS:\n", pesos.nsmallest(10).round(2))Deberías ver "rápido", "perfecto", "fresca" arriba y "tarde", "pésimo", "nadie", "no" abajo. Esto sirve para tres cosas: validar que el modelo aprende sentimiento y no casualidades ("si 'quinto' saliera como término negativo, sospecharía"), explicar predicciones individuales a negocio, y detectar sesgos — si un nombre propio o un colectivo apareciera con peso fuerte, tendrías delante el problema de proxies de 08-04 "Consideraciones éticas y de privacidad".
Límites del enfoque clásico
Honestidad obligada: bag-of-words no entiende el lenguaje, cuenta palabras.
- Negación y contexto: "no está nada mal" es positivo; para la bolsa de palabras es "no" + "nada" + "mal" = negativísimo. Los bigramas mitigan, no curan.
- Ironía y sarcasmo: "genial, otra vez tarde 👏" derrota a cualquier contador de palabras.
- Vocabulario cerrado: una palabra no vista en entrenamiento no aporta nada, aunque sea "nefasto".
Los transformers que se mencionaron en 07-04 "Redes neuronales profundas (Deep Learning)" y entre los frameworks de 08-01 (Hugging Face) resuelven gran parte de esto: leen la frase entera, entienden contexto y llegan preentrenados con el idioma ya aprendido. A cambio: más coste de cómputo, menos interpretabilidad directa y más infraestructura. En la práctica profesional, un TF-IDF + lineal sigue siendo un baseline excelente y a menudo suficiente — y siempre es el primer escalón contra el que medir cualquier transformer.
Errores Comunes y Consejos
- Vectorizar antes de dividir. Ajustar el
TfidfVectorizercon todos los textos filtra vocabulario (¡y frecuencias IDF!) del test al train. Dentro del Pipeline, imposible equivocarse. - Eliminar "no" como stopword. Las listas genéricas de stopwords incluyen la negación; en sentimiento es la palabra más informativa. Revisa siempre la lista para tu problema.
- Limpiar de más. Emojis, mayúsculas sostenidas ("CADUCADOS") y signos repetidos ("!!!") llevan señal emocional. Antes de borrarlos, pregúntate si podrían ser features.
- Creer una accuracy con 40 ejemplos. Reporta siempre la desviación entre particiones y, con muestras pequeñas, conclusiones cualitativas ("bate al azar") mejor que rankings de décimas.
- Etiquetas perezosas. Si etiquetas reseñas por estrellas (1–2 = negativo, 4–5 = positivo), documenta qué haces con las de 3: incluirlas mal etiquetadas ensucia más que descartarlas.
Retos para ampliar
- Escala a un dataset real. Sustituye las 40 frases por miles: los datasets de TASS (talleres de análisis de sentimiento en español de la SEPLN), el corpus de reseñas Multilingual Amazon Reviews, o reseñas públicas de películas en español disponibles en Hugging Face Datasets. Pista: con miles de ejemplos, repite la comparación de los tres modelos y comprueba si la SVM adelanta a Naive Bayes; añade
min_df=5para podar rarezas. - Lematización con spaCy. Instala
spacyy el modeloes_core_news_sm, y sustituye cada palabra por su lema ("llegaron" → "llegar", "caducados" → "caducado") antes de vectorizar. Pista: reduce el vocabulario agrupando variantes; mide si mejora la CV o solo lo hace más lento — no siempre compensa. - Un transformer en tres líneas. Prueba
pipeline("sentiment-analysis", model=...)de Hugging Face con un modelo en español (busca "sentiment spanish" en su hub) sobre tus 40 frases y compara sus aciertos con los de tu TF-IDF, mirando especialmente las frases con negación o sarcasmo. Pista: no necesita entrenamiento — llega preentrenado; fíjate también en cuánto tarda por frase frente a tu modelo lineal.
Conclusión
Tercer proyecto completo: has convertido texto libre en una matriz que los modelos del módulo 4 saben digerir — limpieza con expresiones regulares, bolsa de palabras, TF-IDF con n-gramas —, has reencontrado al MultinomialNB en el terreno para el que te lo anunciamos en 04-06, has evaluado con la CV estratificada que la muestra pequeña exigía y has auditado el modelo palabra a palabra, que es el superpoder del enfoque clásico frente a cajas más negras. Y has aprendido a declarar límites: la negación y el sarcasmo marcan la frontera donde los transformers toman el relevo, con su coste. Fíjate en el patrón que se repite en estos tres proyectos: la representación de los datos importa tanto como el algoritmo. En el próximo proyecto el reto no estará en la representación sino en la distribución: detectar fraudes cuando el 99 % de las transacciones son legítimas y la accuracy se convierte en una mentirosa profesional.
Curso de Machine Learning
Módulo 1: Introducción al Machine Learning
- ¿Qué es el Machine Learning?
- Historia y evolución del Machine Learning
- Tipos de Machine Learning
- Aplicaciones del Machine Learning
- El flujo de trabajo de un proyecto de Machine Learning
Módulo 2: Fundamentos de Estadística y Probabilidad
- Conceptos básicos de estadística
- Distribuciones de probabilidad
- Correlación y covarianza
- Inferencia estadística
- Teorema de Bayes
Módulo 3: Preprocesamiento de Datos
- Limpieza de datos
- Manejo de datos faltantes
- Transformación de datos
- Codificación de variables categóricas
- Normalización y estandarización
- Ingeniería de características
Módulo 4: Algoritmos de Machine Learning Supervisado
- Regresión lineal
- Regresión logística
- Árboles de decisión
- Máquinas de soporte vectorial (SVM)
- K-Vecinos más cercanos (K-NN)
- Naive Bayes
- Redes neuronales
Módulo 5: Algoritmos de Machine Learning No Supervisado
- Clustering: K-means
- Clustering jerárquico
- Análisis de componentes principales (PCA)
- Análisis de agrupamiento DBSCAN
- Visualización de datos con t-SNE y UMAP
Módulo 6: Evaluación y Validación de Modelos
- División de datos: entrenamiento, validación y prueba
- Métricas de evaluación
- Validación cruzada
- Curva ROC y AUC
- Overfitting y underfitting
Módulo 7: Técnicas Avanzadas y Optimización
- Regularización: Ridge, Lasso y Elastic Net
- Ensemble Learning
- Gradient Boosting
- Redes neuronales profundas (Deep Learning)
- Optimización de hiperparámetros
Módulo 8: Implementación y Despliegue de Modelos
- Frameworks y bibliotecas populares
- Implementación de modelos en producción
- Mantenimiento y monitoreo de modelos
- Consideraciones éticas y de privacidad
Módulo 9: Proyectos Prácticos
- Proyecto 1: Predicción de precios de viviendas
- Proyecto 2: Clasificación de imágenes
- Proyecto 3: Análisis de sentimientos en redes sociales
- Proyecto 4: Detección de fraudes
- Proyecto 5: Segmentación de clientes
