1. Introducción

Una cadena de hoteles con sedes en Bogotá, Medellín, Cartagena, Cali y Santa Marta quiere entender mejor el comportamiento de sus reservas, cuales son los factores que se asocian con las cancelaciones, cómo varía la tarifa según el tipo de habitación y la ciudad, y qué tan satisfechos quedan los huéspedes según las características de su reserva.

Este reporte retoma el análisis exploratorio bivariado del Laboratorio 2 (limpieza de datos y siete preguntas de negocio) y agrega dos preguntas nuevas: si los clientes frecuentes prefieren algún tipo de habitación, y cómo se comportan en conjunto las cuatro variables cuantitativas.

2. Objetivos

Objetivo general: explorar la relación entre las características de las reservas (canal, anticipación, tipo de habitación, ciudad, noches, tipo de cliente) y tres resultados de interés para la cadena: la cancelación, la tarifa por noche y la satisfacción del huésped.

Objetivos específicos:

  • Limpiar la base de datos y documentar las decisiones tomadas en cada paso.
  • Identificar qué variables se asocian con una mayor tasa de cancelación.
  • Describir cómo cambia la tarifa según el tipo de habitación, la ciudad, la anticipación y el número de noches.
  • Comparar la satisfacción de los huéspedes según el tipo de habitación, el canal de reserva y el tipo de cliente.

3. Carga y limpieza de datos

3.1 Datos

La base es una extracción directa del sistema de reservas.

datos <- read.csv("/Users/lauranbuitrago/Downloads/LAB 3/reservas_hotel_laboratorio.csv",
                  na.strings = c("", "NA"),
                  stringsAsFactors = FALSE)
n_inicial <- nrow(datos)

Se usó na.strings para que los campos vacíos de las columnas de texto (como canal_reserva) también se lean como NA. La base inicial tiene 668 registros y 10 variables:

Variables de la base de reservas
Variable Descripcion
id_reserva Número de reserva
ciudad_hotel Ciudad de la sede del hotel
tipo_habitacion Sencilla, Doble o Suite
canal_reserva Web, Agencia, Telefónico o Walk-in
cliente_frecuente Si / No
anticipacion_dias Días entre la reserva y la fecha de llegada
noches Número de noches de la reserva
tarifa_por_noche_miles Tarifa por noche, en miles de pesos
cancelada Si / No
calificacion_satisfaccion Calificación de satisfacción (escala 0 a 10)

3.2 Inspección inicial

Antes de modificar cualquier dato, se revisó el resumen de las variables numéricas y la cantidad de faltantes por columna.

Resumen de las variables numéricas antes de limpiar
Min. 1st Qu. Median Mean 3rd Qu. Max.
anticipacion_dias -10 5.0 12.0 17.4 25.0 113
noches 1 3.0 4.0 4.6 5.0 88
tarifa_por_noche_miles -208 177.0 230.0 262.2 311.2 3942
calificacion_satisfaccion 4 6.6 7.4 7.4 8.2 10
Valores faltantes por variable (base original)
NA_por_columna
id_reserva 0
ciudad_hotel 0
tipo_habitacion 0
canal_reserva 39
cliente_frecuente 0
anticipacion_dias 0
noches 0
tarifa_por_noche_miles 0
cancelada 0
calificacion_satisfaccion 139

Desde aquí ya se ven varios problemas: la anticipación tiene un mínimo de -10 días, hay estadías de hasta 88 noches, la tarifa tiene valores negativos y un máximo de 3942 mil pesos, y faltan datos en canal_reserva y calificacion_satisfaccion.

3.3 Registros duplicados

Igual que en las prácticas, los duplicados se buscaron sin tener en cuenta el ID. En este caso el ID también estaba repetido, es decir, eran la misma reserva extraída dos veces del sistema.

n_duplicados <- sum(duplicated(datos[, -1]))
n_duplicados
## [1] 18
datos <- datos %>% distinct(across(-id_reserva), .keep_all = TRUE)
nrow(datos)
## [1] 650

Se eliminaron 18 registros duplicados y la base quedó con 650 reservas.

3.4 Inconsistencias en las variables categóricas

Las variables de texto tenían la misma categoría escrita de varias formas: mayúsculas, minúsculas, espacios al inicio o al final, tildes y abreviaturas como “Sta Marta” o “S”/“N”.

datos <- datos %>%
  mutate(
    ciudad_hotel = str_trim(ciudad_hotel),
    ciudad_hotel = str_to_lower(ciudad_hotel),
    ciudad_hotel = recode(ciudad_hotel,
                          "bogota" = "Bogota", "bogotá" = "Bogota",
                          "medellin" = "Medellin", "medellín" = "Medellin",
                          "cartagena" = "Cartagena",
                          "cali" = "Cali",
                          "santa marta" = "Santa Marta", "sta marta" = "Santa Marta"),
    tipo_habitacion = str_trim(tipo_habitacion),
    tipo_habitacion = str_to_title(str_to_lower(tipo_habitacion)),
    tipo_habitacion = factor(tipo_habitacion,
                             levels = c("Sencilla", "Doble", "Suite"),
                             ordered = TRUE),
    cliente_frecuente = str_trim(cliente_frecuente),
    cliente_frecuente = str_to_lower(cliente_frecuente),
    cliente_frecuente = recode(cliente_frecuente,
                               "si" = "Si", "sí" = "Si", "s" = "Si",
                               "no" = "No", "n" = "No"),
    cancelada = str_trim(cancelada),
    cancelada = str_to_lower(cancelada),
    cancelada = recode(cancelada, "si" = "Si", "no" = "No")
  )
Número de formas distintas de escribir cada categoría
Variable Formas_antes Formas_despues
ciudad_hotel 21 5
tipo_habitacion 12 3
cliente_frecuente 9 2
cancelada 6 2

El tipo de habitación se dejó como factor ordenado (Sencilla < Doble < Suite) porque tiene un orden natural. La variable canal_reserva ya tenía sus categorías bien escritas; su único problema son los faltantes.

3.5 Valores imposibles o atípicos

Los valores que no tienen sentido se pasaron a NA en lugar de borrar la reserva completa. Esto con el fin de que no se pierda la información buena de las demás columnas (por ejemplo, una reserva con tarifa errada sigue diciendo si se canceló y por qué canal). Tampoco se intentó corregirlos, porque no se sabe cuál era el valor real.

datos <- datos %>%
  mutate(
    anticipacion_dias = if_else(anticipacion_dias < 0, NA_real_, anticipacion_dias),
    noches = if_else(noches > 30, NA_real_, noches),
    tarifa_por_noche_miles = if_else(tarifa_por_noche_miles <= 0 |
                                       tarifa_por_noche_miles >= 800,
                                     NA_real_, tarifa_por_noche_miles)
  )
Valores pasados a NA
Variable Criterio Registros Justificacion
anticipacion_dias Menor a 0 6 No se puede reservar después de llegar al hotel
noches Mayor a 30 5 Salto de 11 a 63-88 noches: error de digitación
tarifa_por_noche_miles Menor o igual a 0 3 Una tarifa no puede ser negativa
tarifa_por_noche_miles Mayor o igual a 800 6 Hasta 10 veces la tarifa normal de su tipo de habitación
  • Anticipación: los valores altos (hasta 113 días) se conservaron, porque reservar con tres o cuatro meses de anticipación es normal.
  • Noches: el máximo normal es 11 noches.Se observó un valor de 63 que aparece de golpe, sin valores intermedios. Por esta razón, se tomó 30 noches como límite razonable.
  • Tarifa: las tarifas de 837 a 3942 mil se compararon con las de su mismo tipo de habitación. Una sencilla de 1368 mil costaría cuatro veces la sencilla más cara, y una doble de 2538 mil sería más cara que cualquier suite; parecen errores con un dígito de más. Las suites de 500 a 539 mil sí se conservaron porque son coherentes con su tipo.
  • Satisfacción: todos los valores están entre 4 y 10, dentro de la escala, así que no se hizo ningunaa modificación.

3.6 Valores faltantes

Antes de eliminar faltantes se revisó si tenían relación con otra variable, sobre todo en la calificación de satisfacción.

tabla_na_sat <- table(Cancelada = datos$cancelada,
                      Falta_calificacion = is.na(datos$calificacion_satisfaccion))
kable(addmargins(tabla_na_sat),
      caption = "Reservas canceladas y calificación faltante")
Reservas canceladas y calificación faltante
FALSE TRUE Sum
No 514 15 529
Si 0 121 121
Sum 514 136 650

El 100% de las reservas canceladas no tiene calificación, mientras que en las no canceladas solo faltan 15 de 529 (2.8%). Tiene lógica: si la reserva se canceló el huésped nunca se alojó y no podía calificar. Es decir, el faltante no es al azar. Si se eliminaran todas las filas con NA se perderían casi todas las cancelaciones y no se podrían responder las preguntas sobre cancelación. Por eso la satisfacción se analiza solo con las reservas no canceladas.

Porcentaje de cancelación según si falta el canal
No Si
FALSE 81.2 18.8
TRUE 84.6 15.4

En canal_reserva las reservas sin canal cancelan un 15.4%, parecido a la tasa general (18.6%), así que no se ve una relación clara. Esos 39 registros se conservan y solo se excluyen en las preguntas que se refieren al canal.

3.7 Base final

datos$cancelada <- factor(datos$cancelada, levels = c("No", "Si"))
datos$cliente_frecuente <- factor(datos$cliente_frecuente, levels = c("No", "Si"))

datos_aloj <- datos %>% filter(cancelada == "No")   # para analizar satisfacción
Resumen de la limpieza
Paso Registros
Registros iniciales 668
Duplicados eliminados 18
Registros finales 650
Reservas no canceladas (análisis de satisfacción) 529

La base final tiene 650 reservas (de 668 al inicio). Los NA no se borraron: se excluyen en cada análisis según la variable que se use.

4. Análisis por pregunta ———————————————-

4.1 Pregunta 1 - Canal de reserva y cancelación

¿La probabilidad de que una reserva se cancele depende del canal por el que se hizo? ¿Hay algún canal que debería revisar la cadena hotelera por su alta tasa de cancelación?

Herramienta: al ser dos variables cualitativas, se usó una tabla de contingencia con proporciones por fila y un gráfico de barras apiladas al 100% (position = "fill"). Lo que interesa es el porcentaje de cancelación dentro de cada canal, y el “fill” normaliza cada barra para poder comparar canales de distinto tamaño.

datos_canal <- datos %>% filter(!is.na(canal_reserva))
tabla_cc <- table(datos_canal$canal_reserva, datos_canal$cancelada)
kable(addmargins(tabla_cc), caption = "Canal de reserva y cancelación")
Canal de reserva y cancelación
No Si Sum
Agencia 96 56 152
Telefonico 83 18 101
Walk-in 90 10 100
Web 227 31 258
Sum 496 115 611
kable(round(prop.table(tabla_cc, margin = 1) * 100, 1),
      caption = "Porcentaje de cancelación dentro de cada canal")
Porcentaje de cancelación dentro de cada canal
No Si
Agencia 63.2 36.8
Telefonico 82.2 17.8
Walk-in 90.0 10.0
Web 88.0 12.0
ggplot(datos_canal, aes(x = canal_reserva, fill = cancelada)) +
  geom_bar(position = "fill") +
  geom_text(
    aes(label = after_stat(
      scales::percent(..count.. / tapply(..count.., ..x.., sum)[..x..], accuracy = 0.1)
    )),
    stat = "count",
    position = position_fill(vjust = 0.5)
  ) +
  scale_fill_manual(values = c("No" = "aliceblue", "Si" = "mistyrose")) +
  scale_y_continuous(labels = scales::percent, limits = c(0, 1)) +
  labs(title = "Cancelación según canal de reserva",
       x = "Canal de reserva", y = "Porcentaje", fill = "Cancelada") +
  theme_minimal()

La cancelación sí cambia según el canal. Agencia cancela el 36.8% de sus reservas, más del doble que Telefónico (17.8%), Web (12.0%) y Walk-in (10.0%); en total, la cadena tiene una tasa de cancelación del 18.6%. El canal la cadena debería revisar es Agencia, específicamente en cuanto a sus condiciones de cancelación, depósitos o penalidades.

Limitación: es una asociación, no se puede afirmar que el canal cause la cancelación. Además, se excluyeron 39 reservas sin canal.

4.2 Pregunta 2 - Anticipación, tarifa y cancelación

¿Reservar con mucha anticipación se relaciona con pagar una tarifa distinta por noche? ¿Y con una mayor probabilidad de cancelar?

a) Anticipación y tarifa

Herramienta: al ser dos cuantitativas, se usó un diagrama de dispersión con recta de ajuste lineal y curva loess, junto con la covarianza y los coeficientes de Pearson y Spearman.

grafico_ant <- ggplot(datos, aes(x = anticipacion_dias, y = tarifa_por_noche_miles)) +
  geom_point(alpha = 0.5, color = "#028090") +
  geom_smooth(method = "lm", se = FALSE, color = "firebrick") +
  geom_smooth(method = "loess", se = FALSE, color = "darkorange", linetype = "dashed") +
  labs(title = "Anticipación vs. tarifa: ajuste lineal (rojo) vs. loess (naranja)",
       x = "Anticipación (días)", y = "Tarifa (miles)") +
  theme_minimal()

ggplotly(grafico_ant)
cov_ant <- cov(datos$anticipacion_dias, datos$tarifa_por_noche_miles, use = "complete.obs")
pearson_ant <- cor(datos$anticipacion_dias, datos$tarifa_por_noche_miles,
                   method = "pearson", use = "complete.obs")
spearman_ant <- cor(datos$anticipacion_dias, datos$tarifa_por_noche_miles,
                    method = "spearman", use = "complete.obs")

kable(data.frame(Medida = c("Covarianza", "Pearson", "Spearman"),
                 Valor = round(c(cov_ant, pearson_ant, spearman_ant), 3)),
      caption = "Anticipación y tarifa por noche")
Anticipación y tarifa por noche
Medida Valor
Covarianza -34.985
Pearson -0.021
Spearman -0.032

La correlación de Pearson es -0.02 y la de Spearman -0.03, prácticamente cero. La nube de puntos está dispersa, sin ninguna tendencia, y las dos líneas son casi planas (apenas una ligera inclinación negativa). Reservar con más anticipación no se relaciona con pagar una tarifa distinta, lo que indica que la cadena no está ofreciendo descuentos por reserva temprana.

b) Anticipación y cancelación

Herramienta: al ser una cuantitativa y una cualitativa, se usó un boxplot con los puntos superpuestos y el resumen por grupo. Además, para hablar de probabilidad de cancelar, la anticipación se agrupó en tramos y se calculó el porcentaje de cancelación en cada uno.

ggplot(datos %>% filter(!is.na(anticipacion_dias)),
       aes(x = cancelada, y = anticipacion_dias, fill = cancelada)) +
  geom_boxplot(outlier.shape = NA, alpha = 0.6, width = 0.5) +
  geom_jitter(width = 0.12, alpha = 0.35, size = 1.3, color = "peru") +
  scale_fill_manual(values = c("#028090", "#00A896")) +
  labs(title = "Anticipación según cancelación",
       x = "Cancelada", y = "Anticipación (días)") +
  theme_minimal() +
  theme(legend.position = "none")

kable(do.call(rbind, tapply(datos$anticipacion_dias, datos$cancelada,
                            function(x) round(summary(na.omit(x)), 1))),
      caption = "Resumen de la anticipación (días) según cancelación")
Resumen de la anticipación (días) según cancelación
Min. 1st Qu. Median Mean 3rd Qu. Max.
No 0 4.2 12 16.5 23.8 107
Si 0 6.2 16 23.2 33.8 113
datos$tramo_anticipacion <- cut(datos$anticipacion_dias,
                                breaks = c(-1, 7, 30, Inf),
                                labels = c("0-7 días", "8-30 días", "Más de 30 días"))

tabla_ac <- table(datos$tramo_anticipacion, datos$cancelada)
kable(addmargins(tabla_ac), caption = "Tramo de anticipación y cancelación")
Tramo de anticipación y cancelación
No Si Sum
0-7 días 193 31 224
8-30 días 250 52 302
Más de 30 días 83 35 118
Sum 526 118 644
kable(round(prop.table(tabla_ac, margin = 1) * 100, 1),
      caption = "Porcentaje de cancelación por tramo de anticipación")
Porcentaje de cancelación por tramo de anticipación
No Si
0-7 días 86.2 13.8
8-30 días 82.8 17.2
Más de 30 días 70.3 29.7

Las reservas canceladas se hicieron con más anticipación (mediana de 16 días) que las no canceladas (mediana de 12 días). Por tramos, la cancelación sube de 13.8% (0 a 7 días) a 17.2% (8 a 30 días) y llega a 29.7% cuando se reserva con más de 30 días. Quien reserva con mucha anticipación tiene más tiempo para cambiar de planes.

Limitación: los tramos (una semana y un mes) los eligió el grupo; con otros cortes los porcentajes cambian un poco, pero la tendencia se mantiene.

4.3 Pregunta 3 - Tarifa según tipo de habitación y ciudad

¿La tarifa por noche cambia según el tipo de habitación y según la ciudad del hotel? ¿En qué ciudad(es) y tipo(s) de habitación se concentran las tarifas más altas?

Herramienta: violín con boxplot para comparar la distribución de la tarifa en cada grupo, más una tabla de medidas por grupo. Para ver las dos variables a la vez se usó un mapa de calor con la tarifa media de cada combinación ciudad–tipo de habitación.

datos_tarifa <- datos %>% filter(!is.na(tarifa_por_noche_miles))

a) Por tipo de habitación

ggplot(datos_tarifa, aes(x = tipo_habitacion, y = tarifa_por_noche_miles,
                         fill = tipo_habitacion)) +
  geom_violin(alpha = 0.6, trim = FALSE) +
  geom_boxplot(width = 0.12, fill = "white", outlier.shape = NA) +
  labs(title = "Tarifa por noche según tipo de habitación",
       x = "Tipo de habitación", y = "Tarifa por noche (miles)") +
  theme_minimal() +
  theme(legend.position = "none")

Tarifa por noche (miles) según tipo de habitación
tipo_habitacion media mediana de n
Sencilla 183.6 177 52.1 255
Doble 248.4 240 58.5 280
Suite 406.3 407 50.1 106

b) Por ciudad

ggplot(datos_tarifa, aes(x = ciudad_hotel, y = tarifa_por_noche_miles,
                         fill = ciudad_hotel)) +
  geom_violin(alpha = 0.6, trim = FALSE) +
  geom_boxplot(width = 0.12, fill = "white", outlier.shape = NA) +
  labs(title = "Tarifa por noche según ciudad",
       x = "Ciudad", y = "Tarifa por noche (miles)") +
  theme_minimal() +
  theme(legend.position = "none")

Tarifa por noche (miles) según ciudad
ciudad_hotel media mediana de n
Bogota 221.4 196.5 88.7 180
Cali 213.4 193.0 85.5 98
Cartagena 312.7 297.0 88.6 129
Medellin 227.6 209.0 84.6 144
Santa Marta 284.1 278.5 76.1 90

c) Ciudad y tipo de habitación a la vez

tarifa_ct <- datos_tarifa %>%
  group_by(ciudad_hotel, tipo_habitacion) %>%
  summarise(tarifa_media = mean(tarifa_por_noche_miles), n = n())

ggplot(tarifa_ct, aes(x = tipo_habitacion, y = ciudad_hotel, fill = tarifa_media)) +
  geom_tile(color = "white") +
  geom_text(aes(label = round(tarifa_media, 0)), size = 4) +
  scale_fill_gradient(low = "#F0F9F4", high = "#028090") +
  labs(title = "Tarifa media por noche (miles) por ciudad y tipo de habitación",
       x = "Tipo de habitación", y = "Ciudad", fill = "Tarifa media") +
  theme_minimal()

La tarifa cambia claramente según el tipo de habitación: los tres violines están separados y en orden. La Suite tiene una mediana de 407 mil, muy por encima de la Doble (240 mil) y la Sencilla (177 mil).

Por ciudad, Cartagena tiene la mediana más alta (297 mil) y una mayor concentración de tarifas entre la mediana y el tercer cuartil; le sigue Santa Marta (278.5 mil). Bogotá, Medellín y Cali tienen medianas más bajas y parecidas entre sí (entre 193 y 209 mil).

Combinando ambas variables, las tarifas más altas se concentran en las suites de Cartagena (468 mil en promedio) y Santa Marta (431 mil), seguidas por las habitaciones dobles de esas mismas ciudades de playa.

Limitación: algunas combinaciones tienen pocas reservas (por ejemplo, las suites de Santa Marta), así que esos promedios son menos estables.

4.4 Pregunta 4 - Satisfacción según tipo de habitación y canal

¿La satisfacción de los huéspedes varía según el tipo de habitación o el canal de reserva?

Como se explicó en la limpieza, aquí solo se usan las reservas no canceladas (datos_aloj), porque las canceladas no tienen calificación.

Herramienta: violín con boxplot por grupo, una tabla con media, desviación estándar y coeficiente de variación (CV), y un gráfico por paneles que cruza las dos variables.

datos_sat <- datos_aloj %>% filter(!is.na(calificacion_satisfaccion))

a) Por tipo de habitación

ggplot(datos_sat, aes(x = tipo_habitacion, y = calificacion_satisfaccion,
                      fill = tipo_habitacion)) +
  geom_violin(alpha = 0.6, trim = FALSE) +
  geom_boxplot(width = 0.12, fill = "white", outlier.shape = NA) +
  labs(title = "Satisfacción según tipo de habitación",
       x = "Tipo de habitación", y = "Satisfacción (calificación)") +
  theme_minimal() +
  theme(legend.position = "none")

Satisfacción según tipo de habitación
tipo_habitacion media mediana de coef_variacion n
Sencilla 6.93 6.85 1.07 15.40 214
Doble 7.49 7.50 1.15 15.35 215
Suite 8.45 8.50 1.04 12.32 85

b) Por canal de reserva

ggplot(datos_sat %>% filter(!is.na(canal_reserva)),
       aes(x = canal_reserva, y = calificacion_satisfaccion, fill = canal_reserva)) +
  geom_violin(alpha = 0.6, trim = FALSE) +
  geom_boxplot(width = 0.12, fill = "white", outlier.shape = NA) +
  labs(title = "Satisfacción según canal de reserva",
       x = "Canal de reserva", y = "Satisfacción (calificación)") +
  theme_minimal() +
  theme(legend.position = "none")

Satisfacción según canal de reserva
canal_reserva media mediana varianza de coef_variacion n
Agencia 7.08 7.1 1.54 1.24 17.53 92
Telefonico 7.37 7.3 1.42 1.19 16.18 80
Walk-in 7.27 7.4 1.58 1.26 17.29 88
Web 7.64 7.6 1.36 1.17 15.28 221

c) Tipo de habitación y canal a la vez

ggplot(datos_sat %>% filter(!is.na(canal_reserva)),
       aes(x = tipo_habitacion, y = calificacion_satisfaccion)) +
  geom_boxplot(fill = "mediumorchid3", alpha = 0.6) +
  facet_wrap(~ canal_reserva, ncol = 2) +
  labs(title = "Satisfacción por tipo de habitación y canal de reserva",
       x = "Tipo de habitación", y = "Satisfacción (calificación)") +
  theme_minimal()

La satisfacción sí varía según el tipo de habitación: la Suite es la mejor calificada (media de 8.45), luego la Doble (7.49) y la Sencilla es la peor (6.93). Las cajas de Suite y Sencilla casi no se superponen.

Según el canal, en cambio, la diferencia es pequeña: las medias van de 7.08 (Agencia) a 7.64 (Web) y los violines se superponen casi por completo. En el gráfico por paneles se ve que en todos los canales se mantiene el mismo orden Suite > Doble > Sencilla, es decir, lo que marca la diferencia es el tipo de habitación y no el canal.

El coeficiente de variación muestra que la dispersión dentro de cada grupo es parecida: entre 15% y 18% por canal (ligeramente heterogéneo) y entre 12.3% y 15.4% por tipo de habitación (homogéneo). Esto confirma que las calificaciones dentro de cada grupo son consistentes, y que la diferencia relevante está entre los tipos de habitación.

4.5 Pregunta 5 - Clientes frecuentes vs. ocasionales

¿Los clientes frecuentes se comportan distinto a los ocasionales? ¿Cancelan con menor frecuencia y reportan una satisfacción distinta?

a) Cancelación

Herramienta: tabla de contingencia con proporciones por fila y barras apiladas al 100%, por ser dos variables cualitativas.

tabla_fc <- table(datos$cliente_frecuente, datos$cancelada)
kable(addmargins(tabla_fc), caption = "Tipo de cliente y cancelación")
Tipo de cliente y cancelación
No Si Sum
No 381 89 470
Si 148 32 180
Sum 529 121 650
kable(round(prop.table(tabla_fc, margin = 1) * 100, 1),
      caption = "Porcentaje de cancelación según tipo de cliente")
Porcentaje de cancelación según tipo de cliente
No Si
No 81.1 18.9
Si 82.2 17.8
ggplot(datos, aes(x = cliente_frecuente, fill = cancelada)) +
  geom_bar(position = "fill") +
  geom_text(
    aes(label = after_stat(
      scales::percent(..count.. / tapply(..count.., ..x.., sum)[..x..], accuracy = 0.1)
    )),
    stat = "count",
    position = position_fill(vjust = 0.5)
  ) +
  scale_fill_manual(values = c("No" = "aliceblue", "Si" = "mistyrose")) +
  scale_y_continuous(labels = scales::percent) +
  labs(title = "Proporción de cancelaciones según tipo de cliente",
       x = "Cliente frecuente", y = "Proporción", fill = "¿Canceló?") +
  theme_minimal()

La diferencia es de apenas 1.1 puntos: cancela el 18.9% de los clientes ocasionales y el 17.8% de los frecuentes. Dicho de otra forma, cancela 1 de cada 5.3 clientes ocasionales y 1 de cada 5.6 frecuentes; la razón es casi la misma en los dos grupos.

b) Satisfacción

Herramienta: violín con boxplot y tabla de medidas por grupo, usando solo reservas no canceladas.

ggplot(datos_sat, aes(x = cliente_frecuente, y = calificacion_satisfaccion,
                      fill = cliente_frecuente)) +
  geom_violin(alpha = 0.6, trim = FALSE) +
  geom_boxplot(width = 0.12, fill = "white", outlier.shape = NA) +
  labs(title = "Satisfacción según tipo de cliente",
       x = "Cliente frecuente", y = "Satisfacción (calificación)") +
  theme_minimal() +
  theme(legend.position = "none")

Satisfacción según tipo de cliente
cliente_frecuente media mediana varianza de n
No 7.43 7.4 1.42 1.19 369
Si 7.38 7.4 1.64 1.28 145

Los dos grupos se comportan de manera muy similar: la media es 7.43 en ocasionales y 7.38 en frecuentes, con la misma mediana (7.4) y una dispersión parecida. En el violín de los clientes frecuentes se alcanza a ver una forma con dos picos (bimodal), pero las medidas de centro y dispersión no muestran diferencias representativas.

Limitación: el grupo de clientes frecuentes es mucho más pequeño (180 reservas frente a 470 de ocasionales). Para la cadena, ser cliente frecuente no está asociado con cancelar menos ni con quedar más satisfecho, así que el programa de fidelización no parece estar marcando una diferencia.

4.6 Pregunta 6 - Noches de estadía y tarifa por noche

¿El número de noches se relaciona con la tarifa pagada por noche? ¿Hay algo parecido a un descuento por estadías largas?

Herramienta: diagrama de dispersión con recta lineal y loess, y los coeficientes de Pearson y Spearman. Como el número de noches es discreto, se usó geom_jitter para que los puntos no queden encima unos de otros, y se complementó con la tarifa mediana por número de noches.

ggplot(datos, aes(x = noches, y = tarifa_por_noche_miles)) +
  geom_jitter(width = 0.2, alpha = 0.5, color = "#028090") +
  geom_smooth(method = "lm", se = FALSE, color = "firebrick") +
  geom_smooth(method = "loess", se = FALSE, color = "darkorange", linetype = "dashed") +
  scale_x_continuous(breaks = 1:11) +
  labs(title = "Noches vs. tarifa: ajuste lineal (rojo) vs. loess (naranja)",
       x = "Número de noches", y = "Tarifa por noche (miles)") +
  theme_minimal()

pearson_noc <- cor(datos$noches, datos$tarifa_por_noche_miles,
                   method = "pearson", use = "complete.obs")
spearman_noc <- cor(datos$noches, datos$tarifa_por_noche_miles,
                    method = "spearman", use = "complete.obs")

mediana_noches <- tapply(datos$tarifa_por_noche_miles, datos$noches, median, na.rm = TRUE)
kable(data.frame(Noches = names(mediana_noches),
                 Tarifa_mediana = as.vector(mediana_noches),
                 Reservas = as.vector(table(datos$noches))),
      caption = "Tarifa mediana (miles) según número de noches")
Tarifa mediana (miles) según número de noches
Noches Tarifa_mediana Reservas
1 186.0 23
2 243.5 90
3 230.0 154
4 234.0 153
5 221.0 101
6 233.0 53
7 241.5 42
8 191.5 15
9 219.0 13
11 167.0 1

Pearson es -0.015 y Spearman -0.019: no hay relación. La recta es plana y la tarifa mediana sube y baja sin ningún patrón (entre 167 y 244 mil) según el número de noches. No se observa un descuento por estadías largas, ni el efecto contrario.

Limitación: hay muy pocas reservas de 8 noches o más (28), así que en ese extremo es difícil sacar conclusiones.

4.7 Pregunta 7 - Pregunta propia: tarifa y satisfacción

¿Los huéspedes que pagan una tarifa más alta quedan más satisfechos? ¿O eso depende del tipo de habitación?

Herramienta: dispersión de tarifa contra satisfacción, primero en general y luego coloreada por tipo de habitación, con una recta para cada grupo. Se eligió así porque en la pregunta 3 se observó que la tarifa depende mucho del tipo de habitación.

ggplot(datos_aloj, aes(x = tarifa_por_noche_miles, y = calificacion_satisfaccion)) +
  geom_point(alpha = 0.5, color = "#028090") +
  geom_smooth(method = "lm", se = FALSE, color = "firebrick") +
  labs(title = "Tarifa vs. satisfacción (reservas no canceladas)",
       x = "Tarifa por noche (miles)", y = "Calificación de satisfacción") +
  theme_minimal()

r_general <- cor(datos_aloj$tarifa_por_noche_miles, datos_aloj$calificacion_satisfaccion,
                 method = "pearson", use = "complete.obs")
ggplot(datos_aloj, aes(x = tarifa_por_noche_miles, y = calificacion_satisfaccion,
                       color = tipo_habitacion)) +
  geom_point(alpha = 0.5, size = 1.5) +
  geom_smooth(method = "lm", se = FALSE) +
  scale_color_manual(values = c("#00A896", "#028090", "#F4A261")) +
  labs(title = "Tarifa vs. satisfacción según tipo de habitación",
       x = "Tarifa por noche (miles)", y = "Calificación de satisfacción",
       color = "Tipo de habitación") +
  theme_minimal() +
  theme(legend.position = "bottom")

kable(datos_aloj %>%
        group_by(tipo_habitacion) %>%
        summarise(r = cor(tarifa_por_noche_miles, calificacion_satisfaccion,
                          use = "complete.obs"),
                  n = n()),
      digits = 2, caption = "Correlación tarifa–satisfacción dentro de cada tipo de habitación")
Correlación tarifa–satisfacción dentro de cada tipo de habitación
tipo_habitacion r n
Sencilla -0.09 219
Doble 0.07 222
Suite -0.10 88

En general, sí aparece una correlación positiva moderada (r = 0.34): a mayor tarifa, mayor satisfacción. Pero al separar por tipo de habitación la relación desaparece (r entre -0.10 y 0.07 dentro de cada tipo) y las rectas quedan casi planas.

Es decir, la satisfacción no sube por pagar más, sino porque las tarifas altas corresponden a las suites, y los huéspedes de suite son los más satisfechos. El tipo de habitación está detrás de la relación. Para la cadena, subir los precios no va a mejorar la satisfacción; lo que importa es la experiencia en la habitación.

4.8 Pregunta adicional 1 - Cliente frecuente y tipo de habitación

¿Los clientes frecuentes prefieren algún tipo de habitación en particular?

Herramienta: al ser dos variables cualitativas, se usó una tabla de contingencia con proporciones por fila (la distribución de tipos de habitación dentro de cada tipo de cliente) y barras apiladas al 100%. Se compara por fila porque los dos grupos tienen tamaños muy distintos.

tabla_ft <- table(datos$cliente_frecuente, datos$tipo_habitacion)
kable(addmargins(tabla_ft), caption = "Tipo de cliente y tipo de habitación")
Tipo de cliente y tipo de habitación
Sencilla Doble Suite Sum
No 193 205 72 470
Si 67 78 35 180
Sum 260 283 107 650
kable(round(prop.table(tabla_ft, margin = 1) * 100, 1),
      caption = "Porcentaje de cada tipo de habitación según tipo de cliente")
Porcentaje de cada tipo de habitación según tipo de cliente
Sencilla Doble Suite
No 41.1 43.6 15.3
Si 37.2 43.3 19.4
ggplot(datos, aes(x = cliente_frecuente, fill = tipo_habitacion)) +
  geom_bar(position = "fill") +
  geom_text(
    aes(label = after_stat(
      scales::percent(..count.. / tapply(..count.., ..x.., sum)[..x..], accuracy = 0.1)
    )),
    stat = "count",
    position = position_fill(vjust = 0.5)
  ) +
  scale_fill_manual(values = c("#B7E4C7", "#00A896", "#028090")) +
  scale_y_continuous(labels = scales::percent, limits = c(0, 1)) +
  labs(title = "Tipo de habitación según tipo de cliente",
       x = "Cliente frecuente", y = "Porcentaje", fill = "Tipo de habitación") +
  theme_minimal()

Los dos grupos reservan las habitaciones dobles en la misma proporción (43.3% de los frecuentes y 43.6% de los ocasionales). La diferencia está en los extremos: los clientes frecuentes tienden a reservar un poco más las Suites (19.4% frente a 15.3%) y un poco menos las Sencillas (37.2% frente a 41.1%).

Hay una ligera inclinación de los clientes frecuentes hacia la Suite, pero la diferencia es de aproximadamente 4 puntos y el patrón general es igual en ambos grupos. Esto es coherente con la pregunta 5: los clientes frecuentes no se comportan de forma muy distinta a los ocasionales.

Limitación: solo hay 35 reservas de Suite de clientes frecuentes, así que una diferencia de 4 puntos podría cambiar con unas pocas reservas más.

4.9 Pregunta adicional 2 - Matriz de correlación de las cuantitativas

¿Cómo se comportan en conjunto las cuatro variables cuantitativas? ¿Aparece alguna relación que no se había notado al mirarlas de a pares?

Herramienta: matriz de correlación de Pearson entre las cuatro variables, presentada como tabla y como gráfico con corrplot.

cuantitativas <- datos[, c("anticipacion_dias", "noches",
                           "tarifa_por_noche_miles", "calificacion_satisfaccion")]
matriz_cor <- cor(cuantitativas, use = "complete.obs")
n_completos <- sum(complete.cases(cuantitativas))

kable(round(matriz_cor, 2), caption = "Matriz de correlación de Pearson")
Matriz de correlación de Pearson
anticipacion_dias noches tarifa_por_noche_miles calificacion_satisfaccion
anticipacion_dias 1.00 0.01 0.02 0.00
noches 0.01 1.00 -0.02 0.08
tarifa_por_noche_miles 0.02 -0.02 1.00 0.33
calificacion_satisfaccion 0.00 0.08 0.33 1.00
matriz_cor2 <- matriz_cor
colnames(matriz_cor2) <- rownames(matriz_cor2) <- c("Anticipación", "Noches",
                                                    "Tarifa", "Satisfacción")

corrplot(matriz_cor2,
         method = "color", type = "upper",
         addCoef.col = "black", tl.col = "black", tl.srt = 45,
         mar = c(0, 0, 2, 0))
title("Matriz de correlación", line = 1)

Un detalle importante con use = "complete.obs" la matriz solo usa las reservas que tienen las cuatro variables completas, que son 499. Como las canceladas no tienen calificación, la matriz queda calculada únicamente con reservas no canceladas. Por eso las correlaciones de anticipación y noches con la tarifa no son exactamente iguales a las de las preguntas 2 y 6, aunque siguen siendo prácticamente cero.

Al mirar las cuatro variables juntas, la única correlación que se destaca es la de tarifa con satisfacción (0.34); todas las demás están entre -0.02 y 0.07. Esto confirma lo visto de a pares ni la anticipación ni el número de noches se relacionan con la tarifa, y tampoco con la satisfacción.

La relación tarifa–satisfacción es la que más llama la atención en la matriz, pero, como se vio en la pregunta 7, no es una relación directa: desaparece al separar por tipo de habitación. La matriz por sí sola no permite ver eso, porque solo considera las variables cuantitativas; una correlación moderada puede estar explicada por una tercera variable cualitativa que no aparece en la matriz.

5. Conclusiones y recomendaciones

  • Cancelaciones por canal: el canal Agencia es el que más cancela (36.8%, el doble que los demás). Se recomienda revisar las condiciones con las agencias, y tomar medidas como pedir depósito o cobrar penalidad por cancelación.
  • Anticipación: las reservas hechas con más de 30 días de anticipación cancelan casi el 30%. Se podrían manejar con tarifas no reembolsables o recordatorios antes de la llegada. Además, reservar temprano no cambia la tarifa, así que no hay un incentivo de precio para hacerlo.
  • Tarifas: dependen del tipo de habitación y de la ciudad; las más altas están en las suites de Cartagena y Santa Marta. No hay descuento por estadías largas, y se podría probar uno para aumentar las noches por reserva.
  • Satisfacción: los huéspedes de Suite son los más satisfechos y los de Sencilla los menos, en todos los canales. Vale la pena revisar la experiencia en las habitaciones sencillas. El canal casi no cambia la satisfacción, y pagar más por sí solo tampoco la aumenta.
  • Clientes frecuentes: no cancelan menos, no están más satisfechos y solo muestran una leve preferencia por las suites. El programa de fidelización debería revisarse.
  • Datos: la satisfacción solo se puede medir en quienes se alojaron. Sería útil que el sistema registre también el motivo de las cancelaciones y el canal de todas las reservas. install.packages(“tinytex”) tinytex::install_tinytex()