Al cerrar el módulo anterior prometimos poner la estadística a trabajar sobre datos reales e imperfectos, y aquí cumplimos esa promesa. Los datasets de MercaFresh —como los de cualquier empresa real— llegan con duplicados, errores de captura, valores imposibles y categorías escritas de tres formas distintas. Ningún algoritmo del módulo 4 puede compensar un dato de entrada corrupto: si entrenamos el modelo de churn con clientes duplicados o edades negativas, aprenderá patrones que no existen. En esta lección aprenderás a auditar un dataset, detectar sus problemas y corregirlos de forma sistemática con pandas. Es, según la mayoría de profesionales, la fase donde se invierte más tiempo de un proyecto de ML: hacerla bien marca la diferencia entre un modelo útil y uno engañoso.
Contenido
- Por qué los datos reales están sucios
- El dataset de clientes de MercaFresh
- Auditoría inicial: conocer el terreno
- Duplicados: exactos y lógicos
- Errores de captura y valores imposibles
- Inconsistencias de texto y categorías
- Tipos de datos incorrectos
- Outliers: detectarlos y decidir qué hacer
Por qué los datos reales están sucios
Los datasets de los tutoriales vienen limpios; los de las empresas, no. Las causas son siempre parecidas:
- Entrada manual: un teleoperador de MercaFresh teclea "BCN" un día y "Barcelona" al siguiente.
- Integración de sistemas: la web, la app móvil y el call center guardan los datos con formatos distintos (fechas
2025-03-14vs14/03/2025). - Errores de software: un bug registró durante dos semanas importes de pedido a 0 €.
- Migraciones históricas: al cambiar de CRM en 2023, algunos clientes se importaron dos veces.
- Valores centinela: alguien usó
-1o999para decir "no lo sé", y ahora parecen datos reales.
La consecuencia práctica se resume en la frase clásica garbage in, garbage out: un modelo entrenado con basura produce predicciones basura, pero con apariencia de precisión matemática. Por eso la limpieza no es un trámite, sino parte del análisis.
flowchart LR
A[Datos crudos] --> B[Auditoría]
B --> C[Duplicados]
C --> D[Valores imposibles]
D --> E[Texto y categorías]
E --> F[Tipos de datos]
F --> G[Outliers]
G --> H[Dataset limpio]
Este es el orden que seguiremos: primero entender, luego corregir de lo más obvio a lo más sutil.
El dataset de clientes de MercaFresh
Trabajaremos con un extracto ficticio pero realista de la tabla de clientes, exportada del CRM para el proyecto de churn. Vamos a construirlo en código para que puedas reproducir todo lo que sigue:
import pandas as pd
import numpy as np
datos = {
"id_cliente": [101, 102, 102, 104, 105, 106, 107, 108, 109, 110],
"nombre": ["Ana Ruiz", "Luis Gil", "Luis Gil", "Marta Vega", "Joan Pons",
"Sara Mora", "Pau Serra", "Eva Lima", "Leo Cano", "Iris Bou"],
"edad": [34, 29, 29, -5, 41, 38, 127, 45, 31, 27],
"ciudad": ["Barcelona", "BCN", "BCN", " barcelona ", "Valencia",
"VALENCIA", "Madrid", "madrid", "Sevilla", "Barcelona"],
"gasto_total": ["1250.50", "890.00", "890.00", "0", "2100.75",
"310.20", "15400.00", "670.40", "0", "980.10"],
"fecha_alta": ["2023-05-10", "2024-01-15", "2024-01-15", "2023-11-02",
"2022-07-30", "2027-03-01", "2021-02-14", "2023-09-19",
"2024-06-05", "2023-12-28"],
"num_pedidos": [18, 12, 12, 0, 25, 4, 210, 9, 0, 14],
}
df = pd.DataFrame(datos)
print(df)A simple vista ya se intuyen problemas: el cliente 102 aparece dos veces, hay una edad de -5 y otra de 127, la ciudad "Barcelona" está escrita de cuatro maneras, el gasto es texto y hay una fecha de alta en 2027 (¡en el futuro!). En un dataset de 10 filas los ves de un vistazo; en uno de 200.000 necesitas método.
Auditoría inicial: conocer el terreno
Antes de tocar nada, hay que radiografiar el dataset. Tres funciones de pandas cubren el 80 % de la auditoría inicial:
info() responde tres preguntas: ¿cuántas filas hay?, ¿de qué tipo es cada columna?, ¿cuántos valores no nulos tiene cada una? Aquí descubrimos el primer problema silencioso: gasto_total es de tipo object (texto), no numérico. Un object donde esperabas números es una alarma roja.
describe() nos da media, desviación, mínimo, máximo y cuartiles — exactamente los estadísticos del módulo 2 (lección 02-01). Su gran utilidad en limpieza es mirar min y max: una edad mínima de -5 y máxima de 127 no necesitan más análisis para saber que algo va mal.
# 3) Cardinalidad: cuántos valores distintos tiene cada columna
print(df.nunique())
print(df["ciudad"].unique())nunique() cuenta valores distintos por columna. Si sabemos que MercaFresh opera en 4 ciudades pero ciudad tiene 7 valores únicos, hay inconsistencias de escritura. unique() nos las enseña: ['Barcelona' 'BCN' ' barcelona ' 'Valencia' 'VALENCIA' 'Madrid' 'madrid' 'Sevilla'].
| Función | Pregunta que responde | Problema que destapa |
|---|---|---|
info() |
¿Qué tipos y cuántos nulos? | Tipos incorrectos, columnas incompletas |
describe() |
¿Qué rangos toman los números? | Valores imposibles, escalas extrañas |
nunique() / unique() |
¿Cuántas categorías distintas? | Categorías duplicadas por escritura |
Duplicados: exactos y lógicos
Duplicados exactos
Un duplicado exacto es una fila idéntica a otra en todas sus columnas. En MercaFresh, el cliente 102 (Luis Gil) se importó dos veces durante la migración del CRM:
# ¿Cuántas filas duplicadas exactas hay?
print(df.duplicated().sum()) # 1
# Verlas (keep=False muestra TODAS las copias, no solo la segunda)
print(df[df.duplicated(keep=False)])
# Eliminarlas conservando la primera aparición
df = df.drop_duplicates()
print(len(df)) # 9 filasPuntos clave de drop_duplicates:
keep="first"(por defecto) conserva la primera copia;keep="last", la última;keep=Falseelimina todas.- Devuelve un DataFrame nuevo: hay que reasignarlo (
df = df.drop_duplicates()).
¿Por qué importa para el modelo de churn? Un cliente duplicado pesa el doble en el entrenamiento: el modelo "verá" dos veces sus patrones y sesgará sus conclusiones hacia ese perfil.
Duplicados lógicos
Más peligrosos son los duplicados lógicos: filas que representan la misma entidad pero no son idénticas byte a byte. Por ejemplo, el mismo cliente dado de alta dos veces con el gasto repartido entre ambas fichas, o el mismo pedido registrado por la web y por la app con ids distintos. Para cazarlos, se busca duplicidad solo en las columnas que definen la identidad:
# ¿Hay ids de cliente repetidos aunque el resto de columnas difiera?
print(df.duplicated(subset=["id_cliente"]).sum())
# O identidad por nombre + fecha de alta
sospechosos = df[df.duplicated(subset=["nombre", "fecha_alta"], keep=False)]Con duplicados lógicos, borrar sin mirar es arriesgado: a veces lo correcto es fusionar (sumar los pedidos de ambas fichas) en lugar de eliminar. Es una decisión de negocio, no solo técnica: pregunta siempre qué representa cada fila.
Errores de captura y valores imposibles
Un valor imposible es aquel que viola las reglas del mundo real o del negocio. La técnica consiste en definir reglas de validez explícitas y comprobar cuántas filas las incumplen:
# Regla 1: la edad debe estar entre 18 y 100 (MercaFresh exige mayoría de edad)
edades_invalidas = df[(df["edad"] < 18) | (df["edad"] > 100)]
print(edades_invalidas[["id_cliente", "edad"]])
# 104 -> -5 (error de signo o de tecleo)
# 107 -> 127 (¿teclearon 12 y 7 juntos? ¿es 27?)
# Regla 2: un cliente con pedidos debe tener gasto > 0
# (primero convertiremos gasto_total a número; lo vemos en el apartado de tipos)
# Regla 3: la fecha de alta no puede ser futura
fechas = pd.to_datetime(df["fecha_alta"])
futuras = df[fechas > pd.Timestamp("2026-08-24")]
print(futuras[["id_cliente", "fecha_alta"]]) # 106 -> 2027-03-01¿Y qué hacemos con ellos? Hay tres opciones, en orden de preferencia:
- Corregir, si conocemos el error: una edad de -5 con signo cambiado puede ser 5 (imposible, menor de edad) o un teclazo; si el CRM original tiene el dato bueno, se recupera de allí.
- Marcar como faltante (
np.nan), si no podemos saber el valor real: es honesto reconocer que no lo sabemos, y en la próxima lección veremos cómo tratar esos nulos. - Eliminar la fila, solo si el registro entero es irrecuperable y son pocos casos.
# Opción 2 aplicada: convertir imposibles en NaN
df.loc[(df["edad"] < 18) | (df["edad"] > 100), "edad"] = np.nan
df.loc[pd.to_datetime(df["fecha_alta"]) > pd.Timestamp("2026-08-24"),
"fecha_alta"] = np.nanFíjate en df.loc[condición, columna] = valor: es la forma segura en pandas de modificar un subconjunto (evita el famoso SettingWithCopyWarning).
Los importes a 0 merecen reflexión aparte: un gasto_total de 0 con num_pedidos de 0 puede ser legítimo (cliente registrado que nunca compró — ¡muy relevante para churn!), mientras que gasto 0 con 12 pedidos es un error. La misma cifra puede ser dato válido o error según el contexto: valida combinaciones de columnas, no columnas aisladas.
Inconsistencias de texto y categorías
La columna ciudad es el ejemplo perfecto: "Barcelona", "BCN" y " barcelona " son la misma ciudad para un humano, pero tres categorías distintas para pandas y para cualquier modelo. Si no lo arreglamos, el futuro one-hot encoding (lección 03-04) creará tres columnas donde debería haber una.
La normalización sigue una receta en tres pasos con los métodos de cadena str.*:
# Paso 1: quitar espacios sobrantes al principio y al final
df["ciudad"] = df["ciudad"].str.strip()
# Paso 2: unificar mayúsculas/minúsculas
df["ciudad"] = df["ciudad"].str.lower()
print(df["ciudad"].unique())
# ['barcelona' 'bcn' 'valencia' 'madrid' 'sevilla']
# Paso 3: mapear alias y abreviaturas a un valor canónico
alias = {"bcn": "barcelona", "vlc": "valencia", "mad": "madrid"}
df["ciudad"] = df["ciudad"].replace(alias)
# Toque final estético: capitalizar
df["ciudad"] = df["ciudad"].str.title()
print(df["ciudad"].value_counts())
# Barcelona 3
# Madrid 2
# Valencia 2
# Sevilla 1Detalle importante: str.strip() y str.lower() transforman el texto carácter a carácter, mientras que replace() sustituye valores completos usando un diccionario. Para sustituciones dentro del texto (por ejemplo, quitar puntos de "S.L.") existe str.replace("...", "..."), que es otro método distinto — confundirlos es un clásico.
Para descubrir alias que no conocías, value_counts() es tu aliado: ordena las categorías por frecuencia y las variantes raras aparecen al final de la lista con conteos de 1 o 2.
Tipos de datos incorrectos
Que un número esté almacenado como texto es un problema invisible hasta que explota: no puedes calcular medias, los ordenamientos son alfabéticos ("15400" < "890" porque "1" < "8") y describe() lo ignora.
# gasto_total llegó como texto: convertirlo a float
df["gasto_total"] = df["gasto_total"].astype(float)
# Si hubiera valores no convertibles ("N/D", "error"), astype fallaría.
# to_numeric con errors="coerce" los convierte en NaN en vez de romper:
df["gasto_total"] = pd.to_numeric(df["gasto_total"], errors="coerce")
# Las fechas en texto se convierten con to_datetime
df["fecha_alta"] = pd.to_datetime(df["fecha_alta"], errors="coerce")
print(df.dtypes)| Situación | Herramienta | Comportamiento ante errores |
|---|---|---|
| Texto numérico limpio | astype(float) / astype(int) |
Lanza excepción |
| Texto numérico con basura | pd.to_numeric(..., errors="coerce") |
Convierte basura en NaN |
| Fechas en texto | pd.to_datetime(..., errors="coerce") |
Convierte basura en NaT |
| Formato de fecha ambiguo | pd.to_datetime(..., format="%d/%m/%Y") |
Exige el formato indicado |
El parámetro format merece atención: "14/03/2025" puede ser 14 de marzo o (en convención americana) un error. Especificar format="%d/%m/%Y" elimina la ambigüedad. Convertir las fechas a datetime real además desbloquea operaciones que usaremos en la lección 03-03: extraer el mes, calcular antigüedad, restar fechas.
Outliers: detectarlos y decidir qué hacer
Los outliers ya los conoces del módulo 2: en 02-01 los viste asomar en los boxplots de importes de pedido, y en 02-02 aprendiste que un z-score alto señala una rareza. Ahora los aplicamos como herramientas de limpieza sobre gasto_total.
Detección con z-score
gasto = df["gasto_total"].dropna()
z = (gasto - gasto.mean()) / gasto.std()
print(df.loc[z.abs() > 3, ["id_cliente", "gasto_total"]])Recordatorio de 02-02: si los datos fueran normales, solo un 0,3 % de los valores caería más allá de |z| > 3. El cliente 107, con 15.400 € de gasto y 210 pedidos, se dispara. El z-score tiene una debilidad: la media y la desviación que usa se contaminan por los propios outliers, así que en muestras pequeñas o muy sesgadas puede fallar.
Detección con IQR
El método del rango intercuartílico (02-01) es más robusto porque los cuartiles apenas se inmutan ante valores extremos:
q1 = gasto.quantile(0.25)
q3 = gasto.quantile(0.75)
iqr = q3 - q1
limite_inf = q1 - 1.5 * iqr
limite_sup = q3 + 1.5 * iqr
outliers = df[(df["gasto_total"] < limite_inf) | (df["gasto_total"] > limite_sup)]
print(outliers[["id_cliente", "gasto_total"]])Es exactamente la regla que dibuja los "bigotes" del boxplot que ya conoces.
La decisión: ¿error o realidad?
Aquí está el matiz que separa la limpieza mecánica de la buena limpieza: un outlier no es necesariamente un error. El cliente 107 podría ser un restaurante que compra a diario en MercaFresh — un cliente valiosísimo, no un dato corrupto. Las opciones:
| Situación | Acción recomendada |
|---|---|
| Outlier claramente erróneo (edad 127) | Corregir o convertir en NaN |
| Outlier real pero atípico (cliente-restaurante) | Conservar; quizá marcar con una columna es_empresa |
| Outlier real que distorsiona el modelo | Acotar (capping/winsorizing: recortar al percentil 99) o transformar la variable (lección 03-03) |
| Muchos outliers estructurales | Detectarlos es en sí un objetivo de negocio (anomalías, módulo 5) |
# Ejemplo de capping al percentil 99
p99 = df["gasto_total"].quantile(0.99)
df["gasto_total"] = df["gasto_total"].clip(upper=p99)La regla de oro: nunca elimines un outlier sin poder explicar por qué. Documenta cada decisión — tu yo del futuro (y el modelo de churn) te lo agradecerán.
Errores Comunes y Consejos
- Limpiar sin auditar primero. Ejecutar
drop_duplicates"por si acaso" antes de entender los datos puede borrar registros legítimos. Primeroinfo(),describe(),nunique(); después, actuar. - Olvidar reasignar.
df.drop_duplicates()sindf =no cambia nada: la mayoría de operaciones de pandas devuelven una copia. - Confundir
replace()constr.replace(). El primero sustituye valores completos de la Serie; el segundo, subcadenas dentro de cada texto. - Eliminar outliers automáticamente con |z| > 3. Es un detector, no un juez: cada outlier merece un diagnóstico (¿error o cliente excepcional?).
- Validar columnas de forma aislada. Gasto 0 puede ser válido o no según
num_pedidos: las reglas de validez potentes cruzan columnas. - No documentar los cambios. Guarda el número de filas eliminadas y las reglas aplicadas; en un proyecto real te pedirán cuentas de cada registro descartado.
- Consejo: escribe la limpieza como una función reproducible (
def limpiar_clientes(df): ...) en lugar de celdas sueltas. Cuando llegue la exportación del mes siguiente, la aplicarás en segundos.
Ejercicios
Ejercicio 1
Con el DataFrame df original de la lección (antes de limpiar), escribe el código que: (a) cuente los duplicados exactos, (b) los elimine conservando la primera aparición y (c) compruebe si queda algún id_cliente repetido.
Ejercicio 2
El equipo de MercaFresh te pasa esta columna de una nueva exportación: ["Madrid", "MAD", " madrid", "Màdrid", "madriz"]. Normalízala para que todos los valores acaben siendo "Madrid". Pista: necesitarás strip, lower y un diccionario de alias que incluya los errores tipográficos.
Ejercicio 3
La tabla de pedidos tiene una columna importe con estos valores: [22.5, 19.0, 24.3, 21.7, 480.0, 23.1, 20.9, 18.4]. Detecta outliers con el método IQR y razona (en un comentario) si el valor detectado debería eliminarse, conservarse o investigarse, sabiendo que MercaFresh también sirve a pequeños restaurantes.
Soluciones
Ejercicio 1
# (a) Contar duplicados exactos
print(df.duplicated().sum()) # 1
# (b) Eliminar conservando la primera aparición
df = df.drop_duplicates(keep="first")
# (c) ¿Quedan ids repetidos? (duplicados lógicos)
print(df.duplicated(subset=["id_cliente"]).sum()) # 0 en este casoSi (c) devolviera más de 0, habría que inspeccionar esas filas con keep=False y decidir si fusionar o eliminar.
Ejercicio 2
s = pd.Series(["Madrid", "MAD", " madrid", "Màdrid", "madriz"])
s = s.str.strip().str.lower() # 'madrid', 'mad', 'madrid', 'màdrid', 'madriz'
alias = {"mad": "madrid", "màdrid": "madrid", "madriz": "madrid"}
s = s.replace(alias)
s = s.str.title()
print(s.unique()) # ['Madrid']Los errores tipográficos (madriz) no se arreglan con reglas generales: hay que descubrirlos con value_counts() y añadirlos al diccionario de alias uno a uno.
Ejercicio 3
importes = pd.Series([22.5, 19.0, 24.3, 21.7, 480.0, 23.1, 20.9, 18.4])
q1, q3 = importes.quantile(0.25), importes.quantile(0.75)
iqr = q3 - q1
lim_inf, lim_sup = q1 - 1.5 * iqr, q3 + 1.5 * iqr
print(importes[(importes < lim_inf) | (importes > lim_sup)]) # 480.0
# Razonamiento: 480 € es un outlier estadístico claro, pero MercaFresh sirve
# a restaurantes: podría ser un pedido mayorista legítimo. Antes de eliminar,
# investigar: ¿el cliente tiene historial de pedidos grandes? ¿Coincide con
# un cliente de tipo empresa? Si es legítimo, se conserva (y quizá se marca
# con una columna 'pedido_mayorista'); solo se elimina si se confirma error.Conclusión
En esta lección has aprendido a convertir un dataset caótico en uno confiable: auditar con info(), describe() y nunique(); eliminar duplicados exactos y diagnosticar los lógicos con drop_duplicates; definir reglas de validez para cazar valores imposibles; unificar categorías con str.strip/lower y replace; corregir tipos con astype, to_numeric y to_datetime; y detectar outliers con z-score e IQR sabiendo que detectarlos no es sentenciarlos. La idea central: la limpieza es análisis, no trámite — cada corrección es una decisión que debe poder explicarse.
Habrás notado que varias veces la solución honesta fue convertir un valor sospechoso en NaN: hemos ido acumulando huecos deliberadamente. En la próxima lección abordamos precisamente eso: los datos faltantes de MercaFresh, por qué faltan (que no siempre es azar) y las estrategias para eliminarlos o imputarlos sin engañar al modelo.
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
