Saltar al contenido

lección 3

Día 2 — Limpiar, antes de calcular nada

Con el mapa en la cabeza, hoy tocas los datos: fechas en tres formatos (una con el mes en inglés), tarifas y costes con «€», descuentos y comisiones con «%», canal, tipo de hotel y estado en muchas grafías, y los outliers imposibles (una tarifa de 99.999 €). Normalizas cada columna con criterio propio y decides qué es una reserva consumida: la definición que reproduce las cifras exactas del resto de la semana. Sin plantilla: tú eliges el orden.

60 min

### Martes, 10:00. De mirar a arreglar

Ayer contaste los problemas; hoy los resuelves. Ya sabes que la tarifa viene con «€», que las fechas son texto en tres formatos, que el canal tiene veinticinco grafías y que hay tres vacíos con significado. La limpieza no es un trámite ni un paso previo del que hay que graduarse: es donde se decide si tu análisis es verdad. Una tarifa mal convertida, una fecha que no parsea o un canal sin normalizar no te dan un error rojo que te avise; te dan un número plausible pero falso, que es mucho peor. Por eso hoy vas despacio, columna a columna, y compruebas cada conversión antes de pasar a la siguiente.

Y hoy tomas la decisión más importante del encargo, la que Íñigo no te va a dar hecha: qué es una reserva consumida, es decir, qué filas cuentan de verdad como una estancia que generó noche e ingreso. No todas las filas de reservas valen: hay canceladas que no generaron nada, no-shows (clientes que reservaron y no aparecieron), tarifas imposibles de 99.999 € que son errores de carga, noches a cero. La definición de consumida es el filtro que aplicarás en TODA la semana, y de él dependen las cifras. Si la afinas bien, el ingreso del grupo saldrá 23.040.000 € clavados; si dejas colar una cancelada o un outlier, todo se desviará. Es tu decisión y tu responsabilidad.

### Las fechas: tres formatos y un mes en inglés

Las fechas son la trampa más fina del fichero. Vienen en tres formatos distintos, y uno de ellos trae el mes abreviado en inglés («12 Mar 2024») porque una de las fuentes exporta con la configuración regional en inglés. Un parseo ingenuo que asuma un solo formato fallaría en las otras dos formas, y peor aún: si le dices a Pandas que el mes está en español, «Mar» no lo reconocería. La forma robusta es pd.to_datetime con format='mixed', que deja que Pandas detecte el formato fila a fila, y dayfirst=True para que interprete «12/03/2024» como 12 de marzo y no como 3 de diciembre. El mes en inglés lo entiende porque Pandas trabaja por defecto con nombres de mes en inglés.

1import pandas as pd
2
3reservas = pd.read_csv('datos/reservas.csv').drop_duplicates('reserva_id')
4reservas['fecha_entrada'] = pd.to_datetime(reservas['fecha_entrada'], format='mixed', dayfirst=True)
5print(reservas['fecha_entrada'].min().date(), '->', reservas['fecha_entrada'].max().date())

Parsear fechas en tres formatos, uno con el mes en inglés, con format="mixed"

Después de parsear cualquier fecha, comprueba el rango con .min() y .max(). Si una fecha no parsea, Pandas la deja como NaT (nulo de fecha) sin avisarte, y tus cálculos temporales saldrían con huecos silenciosos. Y si ves una fecha de entrada en 2027 cuando el ejercicio es de 2024, no es un error de parseo: son reservas anticipadas para años futuros, que sí existen en un hotel, pero que tendrás que acotar con un filtro de fecha máxima cuando definas qué cuenta como consumida en el periodo que analizas.

### Los importes: quitar el euro y arreglar la coma

La tarifa por noche, los costes y la tarifa base de las habitaciones vienen como texto con formato español de moneda: «90,00 €», «1.250,00 €». Para convertirlos a número hay que hacer tres cosas en orden: quitar el «€» y los espacios, quitar el punto de los miles (en «1.250,00» el punto separa miles, no decimales) y cambiar la coma decimal por punto (Python usa el punto como separador decimal). El orden importa: si cambiaras la coma por punto antes de quitar el punto de los miles, «1.250,00» se convertiría en «1.250.00», que Python no sabe leer. Se hace con .str.replace encadenados y luego pd.to_numeric.

1tarifa = (reservas['tarifa_noche'].astype(str)
2 .str.replace('€', '', regex=False)
3 .str.replace('.', '', regex=False) # quita el punto de los miles
4 .str.replace(',', '.', regex=False) # coma decimal -> punto
5 .str.strip())
6reservas['tarifa_noche'] = pd.to_numeric(tarifa, errors='coerce')

Convertir «90,00 €» y «1.250,00 €» a número, en el orden correcto

Con la tarifa ya numérica aparecen los outliers: valores imposibles que son errores de carga, no estancias reales. Hay tarifas de 99.999 € (un valor centinela que algún sistema mete cuando falta el dato), tarifas a cero o negativas, y noches o habitaciones a cero. Ninguno es una estancia que generó ingreso, así que hay que excluirlos del análisis. La forma limpia no es borrar filas a lo bruto, sino filtrar por un rango razonable cuando calcules: tarifa mayor que cero y menor que 2.000 € (ningún hotel del grupo cobra más de eso por noche), noches mayores que cero, habitaciones mayores que cero. Ese filtro forma parte de la definición de consumida.

### Los porcentajes: la comisión que lo decide todo

La comisión de cada canal viene como «18 %», «0 %», y el descuento de cada reserva como «15 %». Los dos hay que convertirlos a fracción (0,18, 0,15) para poder multiplicar: si dejas «18» como número entero y multiplicas el ingreso por 18, te sale un disparate. La conversión es quitar el «%» y los espacios, pasar a número y dividir entre cien. La comisión es la pieza más importante de todo el caso, porque es la que convierte el ingreso bruto de Costa en un ingreso neto mucho menor: cada punto de comisión mal convertido movería el GOPPAR final. Vale la pena comprobar a ojo que las seis comisiones quedan como esperas: directo y teléfono a 0, booking a 0,18, expedia a 0,20, turoperador a 0,25, corporate a 0,08.

El orden de las operaciones al limpiar un importe: primero el símbolo, luego el separador de miles, al final el decimal. Cambiarlo produce nulos silenciosos

### Las categorías: canal, tipo de hotel y estado

Normalizar una categoría es reducir muchas grafías a un conjunto pequeño de valores canónicos. El patrón es siempre el mismo: pasar todo a minúsculas y quitar espacios con .str.lower().str.strip(), y luego mapear las variantes al valor bueno con un diccionario y .replace(). En el canal, «booking.com», «bkg» y «ota» van a «booking»; «ttoo», «mayorista» y «grupo» van a «turoperador»; «empresa» y «corp» van a «corporate». Y antes de todo, el canal vacío se rellena con «directo», porque ya sabes que una reserva sin canal es una reserva directa. Ese fillna es una decisión de negocio, no de limpieza: estás diciendo «el vacío aquí significa directo», y por eso no lo tiras.

1canal = reservas['canal'].fillna('directo').astype(str).str.strip().str.lower()
2mapa = {
3 'web': 'directo', 'directa': 'directo',
4 'tel': 'telefono', 'teléfono': 'telefono',
5 'booking.com': 'booking', 'bkg': 'booking', 'ota': 'booking',
6 'exp': 'expedia',
7 'ttoo': 'turoperador', 'mayorista': 'turoperador', 'grupo': 'turoperador',
8 'empresa': 'corporate', 'corp': 'corporate',
9}
10reservas['canal'] = canal.replace(mapa)

Normalizar el canal: rellenar el vacío como «directo» y mapear las grafías a seis categorías

El tipo de hotel se reduce a dos categorías. «Vacacional», «resort» y «playa» van a «vacacional»; «urbano», «ciudad» y «business» van a «urbano». Esta normalización es la que te permitirá el jueves el control de confusión: comparar el GOPPAR dentro de cada tipo para descartar la corazonada de Nuria. Y el estado de la reserva se reduce a cuatro: «checkout» y «completada» a «completada»; «ok» a «confirmada»; «anulada» y «cancel» a «cancelada»; «noshow» y «no show» a «no_show». De esas cuatro, solo las dos primeras (confirmada y completada) cuentan como estancias que generaron noche; las canceladas y los no-shows se excluyen.

Consejo de senior: normaliza siempre a minúsculas y sin espacios ANTES de mapear, no después. Si mapeas primero, tendrás que escribir en el diccionario cada variante de mayúsculas y minúsculas («Booking», «BOOKING», «booking»), y siempre se te escapará una. Bajando todo a minúsculas primero, el diccionario solo necesita una entrada por variante real de escritura, y es mucho más difícil que se cuele una grafía sin traducir.

### La decisión del día: qué es una reserva consumida

Aquí está la decisión que gobierna toda la semana, y que nadie te da hecha. Una reserva consumida es la que de verdad generó una estancia con ingreso. Combinando todo lo anterior, la definición defendible es: estado confirmada o completada (las canceladas y los no-shows fuera), sin fecha de cancelación (por si el estado no está bien puesto, doble red), tarifa entre cero y 2.000 € (fuera los outliers y los ceros), noches mayores que cero, habitaciones mayores que cero, y fechas coherentes (la entrada dentro del periodo, la salida no anterior a la entrada). Ese es el filtro que reproduce las cifras exactas del caso, y el que aplicarás en cada consulta SQL de aquí al viernes.

La red de seguridad del estado y la fecha de cancelación juntos no es paranoia: en datos reales, el estado y la fecha no siempre concuerdan, porque los pone gente y sistemas distintos en momentos distintos. Una reserva puede figurar como «confirmada» y tener una fecha de cancelación posterior, o al revés. Exigir las dos condiciones —estado bueno Y sin fecha de cancelación— es lo que hace que tu recuento de estancias sea robusto a esa incoherencia. Fiarte de una sola de las dos te dejaría colar cancelaciones disfrazadas.

Cuando apliques ese filtro sobre las reservas limpias y deduplicadas, te quedarán 88.493 reservas consumidas en el periodo. De esas 88.493 estancias saldrán todas las cifras de la semana: las noches vendidas, el ingreso, la comisión y, al final, el GOPPAR. Nota que el número es menor que las 92.791 no canceladas que contaste ayer, porque el filtro añade condiciones (estado bueno, tarifa válida, fechas coherentes, dentro del periodo) que recortan un poco más. Con la definición cerrada, ya tienes los cimientos: mañana empiezas a construir la escalera de métricas sobre estos datos limpios, y lo harás en SQL, que es como trabaja el equipo cuando los números van a pasar al panel.

Cierras el segundo día con las seis tablas limpias y una definición de consumida que defenderías ante Íñigo si te la discutiera. La exploración se hizo en Pandas porque leer y sanear CSV crudos es terreno de Pandas; el análisis, los números que van a Elena, se produce en SQL sobre las tablas ya modeladas, que es como trabaja el equipo. Ya sabes limpiar. Ahora falta calcular, y ahí empieza la parte que a Bruno le va a encantar… hasta que deje de gustarle.

Regístrate para guardar tu progreso.