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.
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:
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:
| 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) |
Antes de modificar cualquier dato, se revisó el resumen de las variables numéricas y la cantidad de faltantes por columna.
| 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 |
| 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.
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.
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")
)
| 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.
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)
)
| 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 |
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")
| 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.
| 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.
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
| 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.
¿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")
| 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")
| 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.
¿Reservar con mucha anticipación se relaciona con pagar una tarifa distinta por noche? ¿Y con una mayor probabilidad de cancelar?
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")
| 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.
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")
| 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")
| 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")
| 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.
¿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))
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")
| 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 |
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")
| 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 |
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.
¿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))
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")
| 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 |
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")
| 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 |
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.
¿Los clientes frecuentes se comportan distinto a los ocasionales? ¿Cancelan con menor frecuencia y reportan una satisfacción distinta?
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")
| 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")
| 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.
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")
| 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.
¿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")
| 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.
¿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")
| 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.
¿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")
| 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")
| 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.
¿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")
| 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.