# machine-learning-clasico-con-python
Machine Learning con scikit-learn y XGBoost
Volver a la portada del cursoManejo de datos sucios
Sección 2 · Preparación de datos y pipelines
Manejo de datos sucios
Hasta ahora las canciones de PyRitmo llegaban perfectas: sin huecos, con cada género bien escrito y cada duración en segundos. Los datos reales no llegan así. El equipo de catálogo exporta un CSV desde tres sistemas distintos y el resultado trae filas repetidas, géneros escritos de mil formas, duraciones en otra unidad y valores faltantes. La función ensuciar() del notebook datos-sucios-y-seleccion simula ese CSV a partir de las 1500 canciones limpias. Un modelo entrenado sobre basura aprende basura, así que esta lección va en orden: diagnosticar, limpiar e imputar.
1. Diagnóstico en cuatro comandos
Antes de tocar nada, mira. Cuatro comandos de pandas revelan casi todos los problemas:
sucio = ensuciar(df, semilla=7)
print(sucio.shape) # (1525, 15)
print(sucio.duplicated().sum()) # 25
print(sucio.isna().sum()[lambda s: s > 0])
print(sucio["genero"].nunique()) # 17
print(sucio["duracion_seg"].describe())
- Tamaño: 1525 filas, 25 más de las 1500 canciones que existen.
duplicated(): exactamente 25 filas idénticas a otra anterior.isna(): faltan 86 valores detempo_bpmy 60 deseguidores_miles.nunique(): 17 géneros distintos cuando solo hay 5.describe(): la duración máxima es 419000 y la media 3173.6 segundos, casi una hora, aunque la mediana sigue en 213. Algo no cuadra.
2. Duplicados
Una canción repetida cuenta doble en el entrenamiento. Peor aún: si una copia cae en train y la otra en test, el modelo «acierta» una canción que ya vio, y la cifra de test sale inflada. drop_duplicates() conserva la primera aparición (keep="first" por omisión) y deja las 1500 filas.
3. Texto mal escrito
Con value_counts() sobre genero aparecen variantes como POP, Pop, "pop " con un espacio al final, reggaeton, regueton, electronica sin acento o regional mexicano. Para el modelo, cada una es una categoría distinta: el one-hot del CSV sucio crea 17 columnas de género en lugar de 5. La limpieza tiene dos pasos: normalizar (quitar espacios y pasar a minúsculas) y traducir con un diccionario:
mapa = {"pop": "pop", "rock": "rock", "regional": "regional",
"regional mexicano": "regional",
"reguetón": "reguetón", "reggaeton": "reguetón",
"regueton": "reguetón",
"electrónica": "electrónica", "electronica": "electrónica"}
limpio = sucio.drop_duplicates().copy()
g = limpio["genero"].str.strip().str.lower()
limpio["genero"] = g.map(mapa)
print(limpio["genero"].isna().sum()) # 0
print(limpio["genero"].value_counts())
La línea del isna() es la importante: map convierte en NaN cualquier valor que no esté en el diccionario. Si da 0, ninguna variante se escapó. El conteo final recupera los 5 géneros: pop 400, electrónica 312, rock 305, reguetón 271 y regional 212.
4. Unidades mezcladas
La media de 3173.6 segundos delata el problema: 19 canciones traen la duración en milisegundos (la máxima, 419000, son 419 segundos). Ninguna canción del catálogo dura más de 1000 segundos, así que ese umbral separa las dos unidades sin ambigüedad:
en_ms = limpio["duracion_seg"] > 1000
print(en_ms.sum()) # 19
limpio.loc[en_ms, "duracion_seg"] = limpio.loc[en_ms, "duracion_seg"] / 1000
print(limpio["duracion_seg"].max()) # 446.0
Un valor así no se borra: se corrige. La canción es válida; solo cambió la unidad en la que alguien la registró.
5. Faltantes: borrar o imputar
La salida fácil es dropna(). Con el CSV ya limpio de duplicados, géneros y unidades, deja 1358 de las 1500 canciones: pierdes 142 filas completas porque les falta un solo dato. La alternativa es imputar: rellenar el hueco con un valor razonable. Para seguidores_miles conviene la mediana y no la media, porque la columna es muy sesgada (en las 1500 canciones originales, mediana 88.5 contra media 220.75: unos pocos artistas gigantes suben la media).
El detalle que más importa es dónde se imputa. Si calculas la mediana sobre todo el CSV y luego partes, el test ya influyó en los datos de entrenamiento. La forma correcta es poner SimpleImputer dentro del pipeline, antes del escalado:
num = make_pipeline(SimpleImputer(strategy="median"), StandardScaler())
cat = make_pipeline(SimpleImputer(strategy="most_frequent"),
OneHotEncoder(handle_unknown="ignore"))
pre_imp = ColumnTransformer([("num", num, NUMERICAS),
("cat", cat, CATEGORICAS)])
modelo = make_pipeline(pre_imp, LogisticRegression(max_iter=2000))
Así, en cada pliegue de la validación cruzada la mediana se calcula solo con las canciones de entrenamiento de ese pliegue, y una canción nueva sin tempo_bpm recibe la mediana aprendida en train. En los datos limpios, esas medianas son 103.0 bpm y 89.2 mil seguidores.
6. ¿Valió la pena limpiar?
El notebook compara dos versiones con la misma regresión logística y la validación cruzada estratificada del curso:
- CSV sucio, solo imputado dentro del pipeline: AUC 0.816.
- CSV limpio (sin duplicados, géneros unificados, duraciones en segundos) e imputado: AUC 0.836.
Dos centésimas de AUC solo por limpiar, sin cambiar de modelo ni ajustar nada. La segunda parte del notebook decide qué columnas conservar, con el tema de selección de características del video anterior.
Trampas comunes
- Imputar antes de partir. La mediana debe salir solo de train; por eso el imputador va dentro del pipeline.
- Confiar en
mapsin revisar. Una variante que no está en el diccionario se vuelveNaNen silencio. Revisa siempre elisna()después. - Borrar los valores raros. Las 19 duraciones en milisegundos no son errores de la canción: se corrigen con la unidad correcta.
- Usar la media en columnas sesgadas. En
seguidores_milesla media duplica de sobra a la mediana. - Olvidar los duplicados. Inflan el test con canciones que el modelo ya memorizó.
Guarda la limpieza en una función (limpiar(df)) que reciba el CSV sucio y devuelva el limpio: la vas a reutilizar tal cual en el proyecto final.