"Los datos no son números fríos. Son historias, patrones, errores y oportunidades. Aprende a escucharlos."
Porque aquí es donde la teoría se convierte en práctica.
En la Lección 2 aprendiste el mapa. Ahora, vas a caminar el camino.
Vas a:
⚠️ Advertencia amistosa: Esta lección tiene más código que las anteriores. Pero no te asustes. Lo haremos paso a paso, con explicaciones detalladas, errores comunes y consejos de expertos. No vas a estar solo.
Al finalizar, serás capaz de:
✅ Cargar un dataset desde una URL o archivo local usando Pandas.
✅ Explorar su estructura, contenido y posibles problemas (nulos, duplicados, valores extraños).
✅ Crear visualizaciones simples para entender patrones.
✅ Preparar los datos para el modelo: codificar etiquetas, dividir en train/test, vectorizar texto.
✅ Entender por qué cada paso de preparación es necesario.
✅ Sentirte cómodo/a manipulando datos… ¡tu nueva materia prima!
💡 Si no lo has hecho aún, abre Google Colab ahora: https://colab.research.google.com
Crea un nuevo notebook y ¡empecemos!
Vamos a usar el dataset de SMS Spam Collection. Es pequeño, limpio y perfecto para empezar.
# Siempre empieza importando lo que necesitas
import pandas as pd
📌 ¿Qué es Pandas?
Es una librería de Python para manipular y analizar datos. Piensa en ella como Excel, pero más potente y programable.
# URL del dataset (alojado en GitHub)
url = "https://raw.githubusercontent.com/justmarkham/DAT8/master/data/sms.tsv"
# Cargar con pandas
# El archivo está separado por tabuladores (\t), y no tiene encabezado
data = pd.read_csv(url, sep='\t', names=['label', 'message'])
# Mostrar las primeras 5 filas
print(data.head())
📌 Salida esperada:
label message
0 ham Go until jurong point, crazy.. Available only ...
1 ham Ok lar... Joking wif u oni...
2 spam Free entry in 2 a wkly comp to win FA Cup fina...
3 ham U dun say so early hor... U c already then say...
4 ham Nah I don't think he goes to usf, he lives aro...
✅ ¡Datos cargados! Ya tienes un DataFrame de Pandas.
# ¿Cuántas filas y columnas?
print(f"Forma del dataset: {data.shape}") # (5572, 2)
# Nombres de las columnas
print(f"Columnas: {data.columns.tolist()}") # ['label', 'message']
# Tipos de datos
print(data.dtypes)
📌 Salida:
label object
message object
dtype: object
→ Ambas columnas son de tipo object (texto en Pandas).
# Resumen estadístico (solo para columnas numéricas, pero útil para ver si hay nulos)
print(data.describe(include='all'))
📌 Salida clave:
label message
count 5572 5572
unique 2 5169
top ham Sorry, I'll call later
freq 4825 30
→ unique=2 en label: solo hay dos valores: 'ham' y 'spam'.
→ top=ham: el valor más frecuente es 'ham'.
→ freq=4825: 'ham' aparece 4825 veces.
Ahora, vamos a profundizar. No asumas nada. Explora todo.
print(data['label'].value_counts())
📌 Salida:
ham 4825
spam 747
Name: label, dtype: int64
→ ¡Tenemos un dataset desbalanceado! Hay 6.5 veces más ham que spam.
→ Esto es normal en detección de spam… pero afectará cómo evaluamos el modelo. ¡Lo veremos en la Lección 6!
print(data.isnull().sum())
📌 Salida:
label 0
message 0
dtype: int64
→ ¡Perfecto! No hay valores nulos. En la vida real, esto es raro. Siempre revisa esto.
# Crear una nueva columna: longitud del mensaje
data['length'] = data['message'].apply(len)
# Estadísticas descriptivas
print(data['length'].describe())
📌 Salida:
count 5572.000000
mean 80.489052
std 59.942492
min 2.000000
25% 36.000000
50% 61.000000
75% 111.000000
max 910.000000
Name: length, dtype: float64
→ ¡Hay mensajes de hasta 910 caracteres! ¿Serán spam? ¿Serán normales?
import matplotlib.pyplot as plt
import seaborn as sns
# Configurar estilo
sns.set_style("whitegrid")
# Histograma de longitudes, coloreado por label
plt.figure(figsize=(12, 6))
sns.histplot(data=data, x='length', hue='label', bins=50, kde=False)
plt.title("Distribución de longitud de mensajes por tipo (Spam vs Ham)", fontsize=16)
plt.xlabel("Longitud del mensaje (caracteres)", fontsize=12)
plt.ylabel("Frecuencia", fontsize=12)
plt.legend(title='Tipo', labels=['Spam', 'Ham'])
plt.show()
📌 ¿Qué ves?
Vamos a hacer un análisis de texto muy básico.
# Filtrar solo spam
spam_messages = data[data['label'] == 'spam']['message']
# Convertir a minúsculas y dividir en palabras
words = ' '.join(spam_messages).lower().split()
# Contar frecuencia de palabras
from collections import Counter
word_freq = Counter(words)
# Mostrar las 20 palabras más comunes en spam
print("Palabras más frecuentes en SPAM:")
for word, freq in word_freq.most_common(20):
print(f"{word}: {freq}")
📌 Salida típica:
free: 167
to: 137
you: 117
call: 90
txt: 89
now: 87
...
→ ¡Claro! Palabras como "free", "call", "now" son muy comunes en spam.
→ Esto confirma que el modelo podrá aprender de estas pistas.
Ahora, prepararemos los datos para el modelo. Recuerda: los modelos entienden números, no texto.
Convertiremos 'ham' y 'spam' en 0 y 1.
# Crear un mapeo
label_map = {'ham': 0, 'spam': 1}
# Aplicar el mapeo
data['label_encoded'] = data['label'].map(label_map)
# Verificar
print(data[['label', 'label_encoded']].head())
📌 Salida:
label label_encoded
0 ham 0
1 ham 0
2 spam 1
3 ham 0
4 ham 0
→ ¡Listo! Ahora la etiqueta es numérica.
¡Nunca entrenes y evalúes con los mismos datos!
from sklearn.model_selection import train_test_split
# Características (X) = mensajes
# Etiqueta (y) = label_encoded
X = data['message']
y = data['label_encoded']
# Dividir: 80% train, 20% test
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42, # Para reproducibilidad
stratify=y # ¡Mantiene la proporción de spam/ham en train y test!
)
print(f"Tamaño de train: {len(X_train)} mensajes")
print(f"Tamaño de test: {len(X_test)} mensajes")
print(f"Proporción de spam en train: {y_train.mean():.2%}")
print(f"Proporción de spam en test: {y_test.mean():.2%}")
📌 Salida:
Tamaño de train: 4457 mensajes
Tamaño de test: 1115 mensajes
Proporción de spam en train: 13.42%
Proporción de spam en test: 13.41%
→ ¡Perfecto! La proporción se mantuvo gracias a stratify=y.
Usaremos CountVectorizer de Scikit-learn.
from sklearn.feature_extraction.text import CountVectorizer
# Crear el vectorizador
vectorizer = CountVectorizer()
# Aprender el vocabulario y transformar X_train
X_train_vec = vectorizer.fit_transform(X_train)
# Solo transformar X_test (¡no aprender de él!)
X_test_vec = vectorizer.transform(X_test)
# Ver el tamaño
print(f"Vocabulario: {len(vectorizer.vocabulary_)} palabras únicas")
print(f"X_train_vec shape: {X_train_vec.shape}") # (4457, 7358)
print(f"X_test_vec shape: {X_test_vec.shape}") # (1115, 7358)
📌 ¿Qué significa (4457, 7358)?
→ Cada mensaje es ahora un vector de 7358 números (la mayoría son 0, porque no todas las palabras aparecen en todos los mensajes).
¿Quieres ver qué palabras aprendió el vectorizador?
# Obtener las primeras 20 palabras del vocabulario
vocab = vectorizer.get_feature_names_out()
print("Primeras 20 palabras del vocabulario:")
print(vocab[:20])
📌 Salida:
['00', '000', '0000', '00000', '000000', '00001', '0001', '00011', '00012', '00015', '0002', '0003', '0004', '0005', '0006', '0007', '00080', '0009', '001', '0010']
→ ¡Ups! Hay muchos números. ¿Por qué? Porque CountVectorizer por defecto toma todo como palabra, incluyendo números y signos de puntuación.
💡 Consejo profesional: Más adelante, podrías mejorar esto con:
stop_words='english' → eliminar palabras comunes ("the", "and", "is").lowercase=True → convertir a minúsculas (ya lo hace por defecto).token_pattern=r'\b[a-zA-Z]{2,}\b' → solo palabras de 2+ letras, sin números ni signos.Pero por ahora, ¡está bien! Estamos aprendiendo.
Ahora, es tu turno de explorar.
# Encuentra el índice del mensaje más largo
idx_max = data['length'].idxmax()
longest_message = data.loc[idx_max]
print(f"Longitud: {longest_message['length']} caracteres")
print(f"Tipo: {longest_message['label']}")
print(f"Mensaje: {longest_message['message']}")
📌 Salida típica:
Longitud: 910 caracteres
Tipo: spam
Mensaje: "I HAVE A DATE ON SUNDAY WITH WILL!!..." (¡un spam MUY largo!)
# Filtrar mensajes largos
long_messages = data[data['length'] > 200]
total_long = len(long_messages)
spam_long = long_messages[long_messages['label'] == 'spam'].shape[0]
print(f"Mensajes > 200 caracteres: {total_long}")
print(f"De ellos, spam: {spam_long} ({spam_long/total_long:.1%})")
📌 Salida típica:
Mensajes > 200 caracteres: 45
De ellos, spam: 43 (95.6%)
→ ¡Casi todos los mensajes largos son spam! Esto confirma nuestra hipótesis visual.
Mira algunos mensajes al azar. ¿Ves signos de puntuación, mayúsculas, números, errores ortográficos?
# Muestra 5 mensajes aleatorios
sample = data.sample(5, random_state=1)
for i, row in sample.iterrows():
print(f"[{row['label']}] {row['message'][:100]}...") # Solo primeros 100 caracteres
→ Verás cosas como:
💡 Reflexión: ¿Crees que esto afectará al modelo? ¿Cómo podrías mejorarlo? (Pista: limpieza de texto, lematización, etc. — lo veremos en cursos avanzados).
stratify en train_test_split → Desbalancea train/test.fit_transform en test → Fuga de datos (data leakage). ¡Solo transform!y_train y y_test como variables separadas → Luego no puedes entrenar ni evaluar.☐ Cargar un dataset desde una URL con Pandas.
☐ Explorar su estructura, valores únicos, nulos y estadísticas.
☐ Crear visualizaciones para entender patrones (longitud, palabras frecuentes).
☐ Codificar etiquetas de texto a números.
☐ Dividir datos en train/test manteniendo proporciones (stratify).
☐ Vectorizar texto con CountVectorizer.
☐ Entender la forma de las matrices resultantes.
☐ Hacer preguntas exploratorias y responderlas con código.
☐ Sentirte cómodo/a con la manipulación básica de datos.
"Antes de entrenar un modelo, entrena tus ojos. Aprende a ver lo que los datos te están diciendo."
← Anterior: Lección 2: El Mapa del Tesoro | Siguiente: Lección 4: ¡Entrena tu Primer Modelo! →
Course: AI-course0
Language: ES
Lesson: 3 data exploration