Saltar al contenido

lección 3

Día 2 (mañana) — Antes de calcular, limpiar (y decidir tú)

Fechas en tres formatos, precios con euro y coma, zonas escritas de mil maneras, tipos de anuncio sin unificar, duplicados, precios imposibles y un vacío que no es un hueco. Limpia las exportaciones con criterio propio y decide tú qué es cada cosa: la decisión de hoy sobre la fecha de respuesta es la que abre el jueves.

55 min

### La parte del trabajo de la que nadie presume

Hay una cifra que se repite en cualquier corrillo de analistas: se dice que dedicamos entre el 60% y el 80% del tiempo a limpiar y preparar datos, y solo el resto a analizarlos de verdad. Al principio suena a queja; con el tiempo entiendes que es el trabajo. Un análisis brillante sobre datos sucios es un castillo sobre arena: la conclusión será tan fiable como el peor de tus campos. Ayer recorriste el terreno y anotaste dónde están las minas —el precio como texto, la zona en 183 grafías, el tipo de anuncio en ocho, los duplicados, y ese 40% de contactos sin fecha de respuesta—. Hoy toca desactivarlas, una a una. Y esta vez Irene te suelta la mano: no espera que le preguntes cómo se hace un to_datetime, espera que decidas tú qué hacer con cada suciedad y que dejes escrito por qué.

Limpiar no es «dejar los datos bonitos». Es tomar decisiones que cambian los números, y por eso cada una hay que poder defenderla. ¿Un precio de 5 € es un piso baratísimo o un error de tecleo? ¿Un contacto sin respuesta se tira o se queda? ¿El mismo piso republicado cuenta una vez o dos? Ninguna de estas preguntas tiene una respuesta técnica: tienen una respuesta que depende de qué vas a medir. Vamos por orden, empezando por lo mecánico —fechas y precios— y subiendo hasta lo que exige criterio.

### Lo que cuesta no limpiar: errores que no dan error

Vale la pena ser concreto sobre qué pasa si te saltas cada limpieza, porque el peligro de los datos sucios no es que revienten el código —ojalá—, sino que lo dejen correr y devuelvan un número plausible y equivocado. Si no normalizas la zona, el cruce con la tabla zonas se cae en silencio para las 143 grafías que no casan, y tu análisis por barrio suma menos de la cuenta sin avisar de nada: parecerá completo y le faltará un tercio de los anuncios. Si no unificas el tipo de anuncio, tu comparación destacado contra normal se parte en ocho grupos y no cuentas nada; o peor, agrupas «premium» aparte y concluyes que «premium convierte distinto», cuando premium es destacado con otro nombre. Ninguno de estos errores da un mensaje rojo: dan una tabla con aspecto normal y cifras falsas.

Si no deduplicas, inflas los recuentos de visitas y contactos, y todas tus tasas del embudo salen desviadas hacia abajo (más denominador del real). Si no validas los precios, un solo alquiler de 99.999 € te sube la media y describes pisos que no existen. Y si tratas mal la fecha de respuesta vacía —la limpieza estrella—, o borras a la mayoría no atendida y desaparece el hallazgo, o la rellenas e inventas respuestas que falsean todo el análisis de tiempos. Cada una de estas suciedades, sin limpiar, produce un número que parece bien y está mal, y ese número viaja a una reunión y sostiene una decisión. Por eso la limpieza no es el trabajo aburrido previo al análisis: es parte del análisis, y a menudo la parte que decide si tu conclusión es verdad o basura con formato bonito.

El error más peligroso de los datos no es el que rompe el programa, es el que lo deja correr. Un error de sintaxis lo ves al instante y lo arreglas; un JOIN que pierde un tercio de las filas, una media contaminada por un outlier o un nulo con significado borrado no dan ningún aviso: producen una tabla impecable con números incorrectos. Por eso la desconfianza es una virtud profesional aquí: cada vez que un resultado te salga «bien», pregúntate qué suciedad podría estar haciéndolo parecer bien sin serlo. El código que corre no es código que acierta, y una tabla bonita no es una tabla correcta.

### Fechas en tres formatos (y en inglés)

Las fechas del portal vienen en tres formatos mezclados en la misma columna: 2024-03-12 (formato internacional, año-mes-día), 12/03/2024 (formato de aquí, día/mes/año) y 12 Mar 2024 (con el mes escrito, y ojo, en inglés: Mar, no mar). Pandas no adivina solo cómo leer una columna así de caótica, pero tiene una herramienta para intentarlo fila a fila: pd.to_datetime con format="mixed", que le dice «cada fila puede tener un formato distinto, prueba a interpretar cada una». Le añades dayfirst=True para que 12/03/2024 lo lea como 12 de marzo y no como 3 de diciembre, y errors="coerce" para que, si alguna fila no hay forma de entenderla, la deje como NaT (Not a Time, el nulo de las fechas) en vez de reventar.

Tres formatos en la misma columna, uno con el mes en inglés, convertidos a una fecha real con to_datetime format=mixed y dayfirst=True
1import pandas as pd
2
3anuncios = pd.read_csv('datos/anuncios.csv')
4fp = pd.to_datetime(anuncios['fecha_publicacion'],
5 format='mixed', dayfirst=True, errors='coerce')
6print(fp.min(), fp.max())

Convertir una columna de texto con tres formatos —uno con el mes en inglés— a fechas de verdad.

Consejo de senior: cuando conviertas fechas, cuenta siempre cuántas se quedan en NaT después. Si son cero, perfecto. Si son unas pocas, mira esas filas: suelen ser un cuarto formato que no habías visto. Y si de repente la mitad son NaT, casi seguro que te has equivocado en dayfirst y estás mandando al limbo todas las fechas con día mayor que 12 (porque «13/03» no existe si lo lees como mes 13). El recuento de NaT es tu red de seguridad al parsear fechas.

### Precios como texto con el euro pegado

El precio llega escrito como lo escribiría una persona: "1.150,00 €". Para Pandas eso es texto, no un número, y por eso ayer salía como object. Convertirlo es quitarle todo lo que no sea el número y darle la vuelta a la notación europea. El punto de los miles sobra (1.150 son mil ciento cincuenta, no uno coma quince), la coma decimal hay que cambiarla por punto (porque Python usa el punto decimal), y el símbolo del euro y los espacios fuera. El orden importa: primero quitas el euro y los espacios, luego el punto de los miles, y por último cambias la coma por punto. Si te saltas el punto de los miles, 1.150 se convertiría en 1,150 y de ahí a 1.15 €, un piso regalado.

Cuidado con el orden de los reemplazos al limpiar un número europeo. Si cambias la coma por punto ANTES de quitar el punto de los miles, "1.150,00" pasa por un momento a tener dos puntos ("1.150.00") y el resultado es impredecible. La secuencia segura es: fuera el euro y los espacios, fuera el punto de los miles, y solo entonces coma por punto. Un céntimo mal puesto en un análisis de precios te desplaza toda la distribución.

### La zona: de 183 grafías a 40 barrios

Aquí está la limpieza que más decide, porque de ella depende que puedas cruzar con zonas y agrupar por barrio. Ayer viste 183 grafías para 40 zonas. El primer golpe es fácil: .str.strip() quita los espacios de los lados y .str.lower() lo pasa todo a minúsculas, con lo que "Ruzafa ", "RUZAFA" y "ruzafa" se funden en "ruzafa". Eso solo ya baja de 183 a 62 grafías. Pero quedan las variantes de verdad, las que no se arreglan bajando a minúsculas: "russafa" y "ruzafa" son el mismo barrio escrito de dos formas, y ninguna función de texto lo sabe. Para eso hace falta un mapeo: una tabla que diga «russafa y ruzafa son Ruzafa; chamberi y chamberí son Chamberí», y así con las 40 zonas. Es trabajo manual y no hay atajo: alguien tiene que decidir qué grafías son el mismo sitio.

Normalizar la zona: strip y lower funden las mayúsculas y espacios; el mapeo cierra las variantes de escritura en la zona canónica

Consejo de senior: después de mapear categorías a su forma canónica, comprueba siempre que no queda ningún valor sin mapear (un nulo tras el map, o una categoría «otro»). Es el fallo más silencioso de la limpieza: si te olvidas de una grafía, esas filas se caen del análisis por barrio sin que nadie lo note, y tu «total por zona» suma menos que el total real. Un mapeo sin comprobar es un mapeo a medias.

### La idea que ordena la limpieza de categorías: el valor canónico

La zona, la ciudad, el tipo de anuncio, el canal, el dispositivo, el tipo de anunciante: todas son columnas categóricas —columnas cuyos valores son etiquetas de un conjunto pequeño, no números— y todas llegan sucias de la misma forma, escritas de varias maneras. La idea que ordena su limpieza es la del valor canónico: para cada categoría real decides una única forma oficial de escribirla —«Ruzafa», «destacado», «Valencia», «movil»— y mapeas todas las variantes a ella. No importa cuál elijas como canónica mientras seas consistente; lo que importa es que a partir de la limpieza solo exista esa. Es como acordar cómo se escribe el nombre de un cliente en toda la empresa para que no haya tres fichas del mismo: eliges una grafía y todo lo demás apunta a ella.

El patrón técnico es siempre el mismo y merece la pena fijarlo porque lo repetirás mil veces: primero normalizas lo fácil con funciones de texto —quitar espacios con strip, igualar mayúsculas con lower— y luego, para las variantes que quedan, un mapeo explícito que las lleva al canónico. En el caso del tipo de anuncio, «destacado», «dest» y «premium» se mapean a «destacado»; «normal», «estandar» y «std» a «normal». Este mapeo lo escribe una persona, porque solo una persona sabe que «premium» y «destacado» son lo mismo en esta empresa: ninguna función de texto lo adivina. Es trabajo manual, sí, pero es trabajo que se hace una vez y queda escrito, y es lo que convierte seis u ocho grafías caóticas en las dos o cuatro categorías reales sobre las que se puede analizar.

Consejo de senior: cuando definas los valores canónicos de una categoría, cuenta cuántos esperas que haya y compruébalo después de limpiar. NidoHogar opera en cuatro ciudades: si tras normalizar te salen cinco, hay una variante sin mapear escondida. El tipo de anuncio son dos: si sale un tercer valor, o un «otro», te falta una grafía. Este control —sé cuántas categorías reales hay, y compruebo que salen exactamente esas— es lo que caza la variante que se te escapó. Una categórica limpia tiene el número de valores que el negocio dice que tiene, ni uno más.

### Precios y metros imposibles

Una vez el precio es número, hay que mirar sus extremos, porque las exportaciones traen valores imposibles: un alquiler de 0 €, otro de 5 €, otro de 99.999 €. No son alquileres reales: son errores de tecleo o pruebas del sistema. Aquí es donde entra un poco de estadística descriptiva, que no es más que preguntarle a los datos por su forma: el mínimo, el máximo, la media. Si calculas el precio medio con esos outliers dentro, el 99.999 € tira de la media hacia arriba y el 5 € hacia abajo, y el resultado no describe a ningún piso real. La decisión sensata es fijar un rango razonable —pongamos entre 200 € y 8.000 € al mes— y excluir del análisis de precios lo que quede fuera, dejándolo marcado como error en vez de borrarlo del todo.

Lo mismo con los metros cuadrados: hay valores de 0, de 3 y de 5.000, que no son pisos. La técnica es idéntica: mirar la distribución, decidir un rango plausible y validar contra él. Fíjate en que esto no es «limpiar», es decidir qué entra en el análisis, y la diferencia entre la media con outliers (1.073,98 €) y sin ellos (1.032,74 €) te enseña cuánto puede mover un puñado de valores absurdos una cifra que luego enseñarías en una reunión como si fuera la verdad.

Aquí conviene una idea de estadística descriptiva que te va a servir toda la carrera: la media es frágil ante los extremos, la mediana no. La media suma todo y divide entre el número de valores, así que un solo 99.999 € tira de ella hacia arriba con fuerza. La mediana, en cambio, es el valor que queda justo en el medio si ordenas todos de menor a mayor: le da igual que el más alto sea 8.000 € o un millón, porque solo mira quién está en el centro. Por eso, cuando hay valores extremos o una distribución torcida —y los precios y los ingresos casi siempre lo están—, la mediana describe mejor «lo normal» que la media. El ejemplo que lo fija: el sueldo medio de una oficina donde está sentado el dueño millonario es altísimo y no describe a nadie; el sueldo mediano, el del que está en el medio, sí te dice lo que cobra la gente de esa oficina.

La media se va con un solo extremo; la mediana no se mueve. Por eso con precios e ingresos (torcidos) la mediana describe mejor «lo normal»

No confundas quitar outliers con maquillar los datos. Excluir un alquiler de 0 € o de 99.999 € es legítimo, porque no son pisos reales y lo puedes justificar. Pero recortar los valores altos «porque me estropean el gráfico», cuando son datos de verdad (pisos de lujo caros pero reales), es hacer trampa: estarías escondiendo parte de la realidad para que la historia salga más limpia. La regla es que un outlier se excluye si puedes explicar por qué no es un dato válido, no porque te incomode. Y siempre se deja escrito cuántos excluiste y con qué criterio, para que cualquiera pueda comprobar que no barriste bajo la alfombra lo que no te gustaba.

### Nulos con significado: el vacío que abre el caso

Y llegamos a la decisión del día, la que Irene quiere que tomes tú. Hay tres columnas con vacíos, y no significan lo mismo. fecha_baja vacía en anuncios significa que el anuncio sigue activo (no se ha dado de baja): es un vacío con significado, y rellenarlo sería inventar una fecha de baja que no existe. plan_suscripcion vacío en agencias significa que es un particular sin suscripción: otro vacío con significado. Y fecha_respuesta vacía en contactos —la grande— significa que la agencia no respondió al inquilino. Ninguna de las tres se rellena ni se tira: cada una es información.

La reacción automática de un junior con fecha_respuesta es una de dos: dropna() para quitar esas filas «incompletas», o rellenarlas con alguna fecha. Las dos serían un error grave. Si tiras los 7.364 contactos sin respuesta, borras justo a la mayoría que no fue atendida, que es —te lo adelanto— el corazón del hallazgo del jueves. Lo correcto es crear una columna booleana respondido (verdadero si hay fecha, falso si está vacía) y, solo para los que sí tienen respuesta, calcular horas_respuesta como la diferencia entre la fecha de respuesta y la de contacto. Esa columna respondido convierte un vacío molesto en una variable con la que se puede analizar. La decisión de hoy es lo que hace posible la pregunta del jueves.

Nunca uses dropna() a lo bruto sobre una tabla entera para «quitar lo incompleto». Borra filas por columnas que no te importan, y sobre todo borra los nulos con significado, que son a menudo el dato más valioso. Aquí, un dropna() sobre contactos se llevaría por delante los 7.364 contactos sin respuesta y con ellos el hallazgo del caso. Antes de eliminar un nulo, pregúntate siempre: ¿este hueco es que falta el dato, o es que el dato es «no pasó nada»? Son cosas distintas y se tratan distinto.

Dos tipos de nulo muy distintos: «falta el dato» (se puede imputar) y «no pasó nada» (es información; se marca, no se borra)

Merece la pena detenerse en por qué este error es tan común y tan grave, porque no es un despiste tonto: es una trampa de diseño. Las herramientas te empujan a él. dropna() existe, es de una línea, y su nombre suena a «limpiar». fillna() también. La ruta cómoda, la que el editor te sugiere, es tratar todo nulo como suciedad a eliminar. Y en muchos datasets funciona, porque muchos nulos sí son datos que faltan. El problema es que en cuanto un nulo significa «no pasó nada» —no contestó, sigue activo, nunca compró, no devolvió—, borrarlo destruye justo la señal que buscabas. Y no lo notas: el código corre, la tabla queda más pequeña y más limpia, y el hallazgo desaparece en silencio. La única defensa es la disciplina de preguntarte, columna a columna, qué significa el vacío antes de tocarlo.

Consejo de senior: cuando heredes una tabla, dedica cinco minutos a escribir, al lado de cada columna que tenga nulos, una frase con qué significa un vacío ahí. «fecha_respuesta vacía = no contestó», «fecha_baja vacía = activo», «comision vacía = contrato sin comisión registrada». Ese pequeño diccionario de nulos, que casi nadie hace, es lo que te separa de borrar por accidente el dato más importante. Y si la tabla no tiene ese diccionario en ninguna parte —como aquí, donde se pregunta por Slack—, escribirlo tú es un regalo para el siguiente que la toque, que a menudo eres tú mismo dentro de tres meses.

Hay una historia que ilustra esto mejor que cualquier definición, y es de las que se cuentan en los equipos de datos. Durante la Segunda Guerra Mundial, los ingenieros aliados estudiaban los aviones que volvían de las misiones para reforzar el blindaje donde tenían más impactos de bala. Un estadístico, Abraham Wald, les paró: estáis mirando solo los aviones que volvieron. Los que recibieron impactos donde estos no los tienen —el motor, la cabina— no volvieron para que los midierais. Hay que blindar justo donde los supervivientes no tienen agujeros. A esto se le llama el sesgo del superviviente: sacar conclusiones solo de lo que sobrevivió y olvidar lo que no está en los datos. Tirar los contactos sin respuesta es exactamente eso: quedarte con los aviones que volvieron. Los ignorados son los que no volvieron, y son justo donde está la lección.

### Las banderas booleanas: convertir un hecho en algo que se cuenta

La columna respondido que has creado es una bandera booleana: una columna que solo vale verdadero o falso. Parece un detalle menor y es una de las herramientas más útiles del analista, porque convierte un hecho difuso en algo que se cuenta y se promedia. «La agencia respondió» es un hecho; respondido=True lo hace calculable. Y aquí viene lo elegante: la media de una columna booleana es directamente una proporción. Si respondido vale True (que cuenta como 1) o False (que cuenta como 0), su media es la fracción de verdaderos, es decir, la tasa de respuesta. Por eso, en cuanto tienes la bandera, «¿qué porcentaje se responde?» es simplemente la media de respondido. Crear una buena bandera booleana es la mitad del camino de muchas preguntas.

Este patrón —convertir una condición en una bandera 0/1 y luego promediarla o sumarla— lo vas a usar constantemente. «¿Qué porcentaje de anuncios está activo?»: bandera activo (fecha_baja vacía), media. «¿Qué porcentaje de contratos tiene comisión?»: bandera, media. «¿Qué porcentaje de contactos convirtió?»: bandera convertido, media. Es el mismo gesto una y otra vez: defines la condición que te importa como verdadero/falso, y entonces contarla, promediarla o cruzarla con otra dimensión es trivial. Aprender a ver las preguntas de negocio como «una bandera y una media» te simplifica la mitad del trabajo, porque muchísimas preguntas de porcentaje son exactamente eso disfrazado.

### Los pisos republicados: cuándo uno es dos

La última trampa es la que avisó Diego: las agencias retiran un piso y lo vuelven a subir con otro anuncio_id, así que el mismo piso aparece dos veces. Cuando cuentas anuncios de dos formas —con y sin colapsar los republicados— te salen dos cifras distintas, y no sabes cuál usar. Se lo llevas a Irene y su respuesta es la lección de fondo del día: «Depende de la pregunta, y por eso hay que escribirlo. Si mides cuántos pisos distintos hay, el republicado es uno. Si mides cuántos contactos generó cada publicación, cada anuncio_id va por su lado. Para la tasa de conversión, colapsa el piso en uno: si no, partes sus visitas y sus contactos en dos y la tasa te sale mal. Y déjalo escrito, que dentro de un mes no te vas a acordar de qué contaste.»

Consejo de senior: un recuento sin su definición pegada no es un número, es una trampa. «Hay 4.500 anuncios» no significa nada hasta que dices si los republicados cuentan una o dos veces. Y para una tasa, elegir mal parte el numerador y el denominador de sitios distintos: si las visitas de un piso van a un anuncio_id y sus contratos a otro, la tasa de cada uno sale rota. Escribe siempre, al lado de cada cifra, qué has contado exactamente. Tu yo de dentro de un mes te lo agradecerá, y tu credibilidad en la demo depende de poder responder «¿y eso cómo lo has contado?».

### Cuándo sí se rellena un hueco (y cuándo es un pecado)

Hemos insistido en no rellenar los nulos con significado, pero hay huecos que sí piden un valor, y conviene saber distinguirlos. num_fotos vacío probablemente es cero fotos: el anuncio no subió ninguna, y ahí rellenar con cero es razonable y defendible. habitaciones vacío es más delicado: un estudio puede no tener el dato porque técnicamente no tiene habitaciones separadas, así que rellenarlo con cero o dejarlo como desconocido depende de lo que vayas a hacer con esa columna. La regla es: rellena un hueco solo cuando tengas una razón sólida de qué valor le corresponde, y déjalo escrito. Rellenar con la media «para no perder filas» es casi siempre un pecado, porque inventa un dato que no existe y contamina cualquier cálculo posterior con un valor de relleno que se hace pasar por real.

A rellenar huecos se le llama imputar, y es una de esas técnicas que parecen inofensivas y hacen mucho daño mal usadas. Imputar con la media, con la mediana o con el valor más frecuente es fácil y a veces necesario, pero cada valor imputado es una invención, y las invenciones se acumulan: si imputas el 20% de una columna con su media, has estrechado artificialmente su variabilidad y cualquier análisis de dispersión saldrá falseado. Por eso la jerarquía sensata es: primero, ¿el hueco significa algo? (entonces es una bandera, no un hueco). Segundo, ¿sé qué valor le corresponde de verdad? (num_fotos vacío = 0). Y solo en último lugar, si hay que imputar, hacerlo con criterio, marcando qué se imputó y sabiendo que has metido ruido. Imputar a la ligera es maquillar la ausencia de datos, no resolverla.

### El mapa de la limpieza de hoy, de un vistazo

Antes de pasar a calcular, para un momento y mira el conjunto de lo que has desactivado hoy, porque cada tipo de suciedad ejercita una técnica distinta y conviene tenerlas fichadas para el próximo dataset sucio, que lo habrá. No es una lista para memorizar: es el repertorio que un analista reconoce al instante y aplica sin pensar, como un mecánico que oye un ruido y ya sabe qué mirar.

El repertorio completo de hoy: cada mancha con su técnica. La resaltada no es limpieza, es el dato que abre el caso

### Deja escrito lo que hiciste: la limpieza también se defiende

Un último hábito que separa a un analista fiable de uno que «casi cuadra»: documentar las decisiones de limpieza según las tomas. No en un documento aparte que nadie leerá, sino en comentarios dentro del propio código y en una nota corta al lado de cada cifra: «precios excluidos: 6 fuera del rango 200-8.000 €», «zona: 183 grafías mapeadas a 40 canónicas», «contactos deduplicados por contacto_id: 125 fuera», «republicados colapsados en uno para las tasas». Parece burocracia, pero es lo que te salva en la demo del viernes cuando Álvaro pregunte «¿y ese número cómo lo has sacado?». Si puedes responder al instante qué incluiste, qué excluiste y por qué, tu cifra aguanta; si titubeas, toda la sala empieza a dudar de todo tu trabajo, aunque esté impecable.

Hay una razón más profunda, y es la reproducibilidad. Un análisis vale lo que vale la posibilidad de que otra persona —o tú mismo dentro de tres meses— lo repita y llegue al mismo número. Si tus decisiones de limpieza viven solo en tu cabeza, tu análisis es una caja negra: da un número que nadie puede verificar ni recalcular cuando lleguen datos nuevos. Por eso el código, con sus decisiones escritas, es tan superior a la hoja de cálculo donde limpiaste a mano celda por celda: el código es la receta, y una receta se puede volver a cocinar. Cuando dentro de un trimestre alguien quiera rehacer este análisis con datos frescos, la diferencia entre «ejecuto el mismo script» y «intento acordarme de qué toqué» es la diferencia entre un análisis profesional y una anécdota.

Con las fechas convertidas, los precios en número, la zona canónica, el tipo de anuncio unificado, los duplicados fuera y la bandera respondido creada, los datos están listos para calcular. A partir de aquí el equipo trabaja sobre las tablas ya limpias, y el análisis —los números que van a ir a la demo— se produce en SQL, que es como trabaja el resto del equipo de datos. Mañana montas el embudo entero y confirmas, o no, el titular de Gonzalo.

Regístrate para guardar tu progreso.