Una cadena de hoteles con sedes en Bogotá, Medellín, Cartagena, Cali y Santa Marta quiere comprender mejor el comportamiento de sus reservas, especialmente los factores asociados con las cancelaciones, las diferencias en las tarifas y la satisfacción de los huéspedes.
La base de datos corresponde a una extracción del sistema de reservas. Como ocurre habitualmente con datos provenientes de sistemas operacionales, se identificaron inconsistencias en variables categóricas, registros duplicados, valores imposibles y datos faltantes. Por esta razón, antes de realizar el análisis bivariado se efectuó un proceso de revisión y limpieza.
Objetivo general: explorar las relaciones entre las variables de las reservas hoteleras para identificar patrones que puedan ser útiles para la gestión de cancelaciones, tarifas, satisfacción y comportamiento de los clientes.
Objetivos específicos:
datos <- read.csv("reservas_hotel_laboratorio.csv",
na.strings = c("", "NA"),
stringsAsFactors = FALSE)
dim_inicial <- dim(datos)
La base contiene 668 registros y 10 variables.
Las variables utilizadas son:
kable(
data.frame(
Variable = c(
"id_reserva", "ciudad_hotel", "tipo_habitacion", "canal_reserva",
"cliente_frecuente", "anticipacion_dias", "noches",
"tarifa_por_noche_miles", "cancelada", "calificacion_satisfaccion"
),
Descripcion = c(
"Número de reserva",
"Ciudad de la sede del hotel",
"Tipo de habitación",
"Canal utilizado para realizar la reserva",
"Indica si el cliente es frecuente",
"Días entre la reserva y la llegada",
"Número de noches de la reserva",
"Tarifa por noche, en miles de pesos",
"Indica si la reserva fue cancelada",
"Calificación de satisfacción en escala de 0 a 10"
)
),
caption = "Variables de la base de reservas"
)
| Variable | Descripcion |
|---|---|
| id_reserva | Número de reserva |
| ciudad_hotel | Ciudad de la sede del hotel |
| tipo_habitacion | Tipo de habitación |
| canal_reserva | Canal utilizado para realizar la reserva |
| cliente_frecuente | Indica si el cliente es frecuente |
| anticipacion_dias | Días entre la reserva y la llegada |
| noches | Número de noches de la reserva |
| tarifa_por_noche_miles | Tarifa por noche, en miles de pesos |
| cancelada | Indica si la reserva fue cancelada |
| calificacion_satisfaccion | Calificación de satisfacción en escala de 0 a 10 |
La primera revisión permite identificar tipos de datos, valores faltantes y posibles inconsistencias. También se revisaron los identificadores de reserva para detectar registros repetidos.
duplicados_id <- sum(duplicated(datos$id_reserva))
datos <- datos %>%
distinct(id_reserva, .keep_all = TRUE)
Se encontraron diferencias de escritura que representaban la misma
categoría. Por ejemplo, una misma ciudad podía aparecer con mayúsculas,
minúsculas, espacios adicionales o con la forma abreviada “Sta Marta”.
También se encontraron las variantes N, NO,
S, SI y Sí en la variable de
cliente frecuente.
Para evitar que estas diferencias fueran interpretadas como categorías distintas, se eliminaron espacios, se normalizó el uso de minúsculas y se unificaron las categorías.
normalizar <- function(x) {
x <- str_trim(x)
x <- str_to_lower(x)
x <- str_replace_all(x, "á", "a")
x <- str_replace_all(x, "é", "e")
x <- str_replace_all(x, "í", "i")
x <- str_replace_all(x, "ó", "o")
x <- str_replace_all(x, "ú", "u")
x
}
datos <- datos %>%
mutate(
ciudad_hotel = normalizar(ciudad_hotel),
ciudad_hotel = recode(ciudad_hotel, "sta marta" = "Santa Marta",
"santa marta" = "Santa Marta", "bogota" = "Bogotá",
"cartagena" = "Cartagena", "medellin" = "Medellín",
"cali" = "Cali"),
tipo_habitacion = normalizar(tipo_habitacion),
tipo_habitacion = recode(tipo_habitacion, "sencilla" = "Sencilla",
"doble" = "Doble",
"suite" = "Suite"),
canal_reserva = normalizar(canal_reserva),
canal_reserva = recode(canal_reserva, "agencia" = "Agencia",
"telefonico" = "Telefónico",
"walk-in" = "Walk-in", "web" = "Web"),
cliente_frecuente = normalizar(cliente_frecuente),
cliente_frecuente = recode(cliente_frecuente, "n" = "No", "s" = "Si",
"no" = "No", "si" = "Si"),
cancelada = normalizar(cancelada),
cancelada = recode(cancelada, "no" = "No", "si" = "Si")
)
summary(datos)
## id_reserva ciudad_hotel tipo_habitacion canal_reserva
## Min. :1001 Length :650 Length :650 Length :650
## 1st Qu.:1163 N.unique : 5 N.unique : 3 N.unique : 4
## Median :1326 N.blank : 0 N.blank : 0 N.blank : 0
## Mean :1326 Min.nchar: 4 Min.nchar: 5 Min.nchar: 3
## 3rd Qu.:1488 Max.nchar: 11 Max.nchar: 8 Max.nchar: 10
## Max. :1650 NAs : 39
##
## cliente_frecuente anticipacion_dias noches tarifa_por_noche_miles
## Length :650 Min. :-10.00 Min. : 1.0 Min. :-208.0
## N.unique : 2 1st Qu.: 5.00 1st Qu.: 3.0 1st Qu.: 178.0
## N.blank : 0 Median : 12.00 Median : 4.0 Median : 231.0
## Min.nchar: 2 Mean : 17.51 Mean : 4.6 Mean : 263.0
## Max.nchar: 2 3rd Qu.: 25.00 3rd Qu.: 5.0 3rd Qu.: 311.8
## Max. :113.00 Max. :88.0 Max. :3942.0
##
## cancelada calificacion_satisfaccion
## Length :650 Min. : 4.000
## N.unique : 2 1st Qu.: 6.600
## N.blank : 0 Median : 7.400
## Min.nchar: 2 Mean : 7.416
## Max.nchar: 2 3rd Qu.: 8.200
## Max. :10.000
## NAs :136
Después de la estandarización, la base conserva 650 registros y presenta cinco categorías de ciudad, tres de tipo de habitación y cuatro de canal de reserva. Las variables de cliente frecuente y cancelación tienen dos categorías cada una.
Se mantienen 39 valores faltantes en canal de reserva y 136 en calificación de satisfacción, cuyo tratamiento se presenta más adelante. Los valores negativos y altos de las variables cuantitativas también se revisan en el siguiente apartado.
Se revisaron las variables cuantitativas. Los valores negativos de
anticipacion_dias y tarifa_por_noche_miles no
son compatibles con la definición de estas variables, por lo que se
transformaron en valores faltantes (NA) en lugar de
eliminarlos completamente.
datos <- datos %>%
mutate(
anticipacion_dias = if_else(anticipacion_dias < 0,
NA_real_, anticipacion_dias),
tarifa_por_noche_miles = if_else(tarifa_por_noche_miles < 0,
NA_real_, tarifa_por_noche_miles)
)
Después de reemplazar los valores negativos por NA, se
revisa la variable noches para identificar posibles valores
atípicos altos. Para lo anterior, se calculan los cuartiles de la
variable noches y se establece el límite superior mediante
la regla de 1.5 veces el rango intercuartílico. Los registros que
superan este límite se guardan en datos_noches_atipicos
para su revisión, sin eliminarlos de la base original.
cuartil_noches <- quantile(datos$noches, c(.25, .75), na.rm = TRUE)
limite_superior_noches <- cuartil_noches[2] + 1.5 * (cuartil_noches[2] - cuartil_noches[1])
datos_noches_atipicos <- datos %>%
filter(noches > limite_superior_noches, !is.na(noches))
Se calculan los cuartiles de la variable
tarifa_por_noche_miles y se establece el límite superior
mediante la regla de 1.5 veces el rango intercuartílico. Los registros
que superan este límite se guardan en datos_tarifa_atipicos
para su revisión.
cuartil_tarifa <- quantile(datos$tarifa_por_noche_miles, c(.25, .75), na.rm = TRUE)
limite_superior_tarifa <- cuartil_tarifa[2] + 1.5 * (cuartil_tarifa[2] - cuartil_tarifa[1])
datos_tarifa_atipicos <- datos %>%
filter(tarifa_por_noche_miles > limite_superior_tarifa, !is.na(tarifa_por_noche_miles))
Se presentan los diagramas de caja de tarifa por noche y número de noches para revisar su distribución antes del tratamiento.
boxplot(datos$tarifa_por_noche_miles,
main = "Tarifa por noche antes del tratamiento",
ylab = "Tarifa (miles de pesos)",
las = 1,
col = "steelblue"
)
boxplot(datos$noches,
main = "Número de noches antes del tratamiento",
ylab = "Número de noches",
las = 1,
col = "steelblue"
)
Aunque los valores altos podrían ser válidos en el contexto de las
reservas, se decidió reemplazar por NA aquellos que superan
los límites superiores calculados, por su influencia sobre medidas como
la media. Este tratamiento conserva las reservas y la información de las
demás variables, pero sustituye los valores identificados de noches y
tarifa por noche.
datos <- datos %>%
mutate(
noches = if_else(
noches > limite_superior_noches,
NA_real_,
noches
),
tarifa_por_noche_miles = if_else(
tarifa_por_noche_miles > limite_superior_tarifa,
NA_real_,
tarifa_por_noche_miles
)
)
El siguiente resumen presenta los límites superiores utilizados y el
número de valores reemplazados por NA en cada variable.
resumen_atipicos <- data.frame(
Variable = c("noches", "tarifa_por_noche_miles"),
Limite_superior = c(
unname(limite_superior_noches),
unname(limite_superior_tarifa)
),
Valores_reemplazados = c(
nrow(datos_noches_atipicos),
nrow(datos_tarifa_atipicos)
)
)
kable(
resumen_atipicos,
digits = 2,
col.names = c(
"Variable",
"Límite superior",
"Valores reemplazados por NA"
),
caption = "Resumen del tratamiento de valores atípicos altos"
)
| Variable | Límite superior | Valores reemplazados por NA |
|---|---|---|
| noches | 8.00 | 19 |
| tarifa_por_noche_miles | 514.25 | 8 |
Se presentan nuevamente los diagramas de caja de tarifa por noche y número de noches para observar su distribución después del tratamiento. Las etiquetas indican los extremos de los bigotes, las bisagras de la caja y la mediana.
bp1 <- boxplot(
datos$tarifa_por_noche_miles,
main = "Tarifa por noche después del tratamiento",
ylab = "Tarifa (miles de pesos)",
las = 1,
col = "steelblue",
xlim = c(0.5, 2.5)
)
valores_tarifa <- bp1$stats[, 1]
text(
x = 1.30,
y = valores_tarifa,
labels = paste0(
c(
"Mínimo",
"Q1",
"Q2 (Mediana)",
"Q3",
"Máximo"
),
": ",
round(valores_tarifa, 2)
),
pos = 4,
cex = 0.8
)
bp2 <- boxplot(
datos$noches,
main = "Número de noches después del tratamiento",
ylab = "Número de noches",
las = 1,
col = "steelblue",
xlim = c(0.5, 2.5)
)
valores_noches <- bp2$stats[, 1]
text(
x = 1.30,
y = valores_noches,
labels = paste0(
c(
"Mínimo",
"Q1",
"Q2 (Mediana)",
"Q3",
"Máximo"
),
": ",
round(valores_noches, 2)
),
pos = 4,
cex = 0.8
)
Se deben revisar los faltantes de canal_reserva y
calificacion_satisfaccion antes de definir su
Se compara el estado de cancelación con la ausencia de calificación de satisfacción, mediante una tabla de frecuencias y proporciones por estado de cancelación.
tabla_satisfaccion <- table(
datos$cancelada,
is.na(datos$calificacion_satisfaccion)
)
dimnames(tabla_satisfaccion) <- list(
"Reserva cancelada" = rownames(tabla_satisfaccion),
"Calificación" = c("Con calificación", "Sin calificación")
)
kable(
tabla_satisfaccion,
col.names = c("Con calificación", "Sin calificación"),
caption = "Número de reservas según cancelación y presencia de calificación"
)
| Con calificación | Sin calificación | |
|---|---|---|
| No | 514 | 15 |
| Si | 0 | 121 |
porcentajes_satisfaccion <- prop.table(
tabla_satisfaccion,
margin = 1
) * 100
kable(
porcentajes_satisfaccion,
digits = 1,
col.names = c("Con calificación (%)", "Sin calificación (%)"),
caption = "Porcentajes dentro de cada estado de cancelación"
)
| Con calificación (%) | Sin calificación (%) | |
|---|---|---|
| No | 97.2 | 2.8 |
| Si | 0.0 | 100.0 |
La revisión muestra que todas las reservas canceladas no tienen calificación, lo cual es esperable si el servicio no se utilizó. Por eso, no se imputan estos valores. Para analizar satisfacción, se crea una base con las reservas no canceladas que tienen calificación registrada.
datos_satisfaccion <- datos %>%
filter(cancelada == "No", !is.na(calificacion_satisfaccion))
Los faltantes del canal de reserva se identifican como “No informado”
en una nueva variable llamada canal_analisis. Así se
conservan los registros para las tablas descriptivas, sin modificar la
variable original.
datos <- datos %>%
mutate(canal_analisis = if_else(is.na(canal_reserva),
"No informado", canal_reserva))
Después de la limpieza quedaron 650 registros, frente a 668 registros iniciales.
Pregunta: ¿La probabilidad de que una reserva se cancele depende del canal por el que se hizo? ¿Hay algún canal que la cadena hotelera debería revisar por su alta tasa de cancelación?
Para comparar la cancelación entre canales, se calcula el número de reservas, el número de cancelaciones y la tasa observada de cancelación en cada canal. Esta tasa corresponde al porcentaje de reservas canceladas dentro de cada grupo.
tabla_p1 <- datos %>%
group_by(canal_analisis) %>%
summarise(
n = n(),
cancelaciones = sum(cancelada == "Si"),
tasa_cancelacion = mean(cancelada == "Si") * 100
)
kable(
tabla_p1,
digits = 1,
col.names = c(
"Canal de reserva",
"Reservas",
"Cancelaciones",
"Tasa de cancelación (%)"
),
caption = "Reservas y cancelaciones por canal"
)
| Canal de reserva | Reservas | Cancelaciones | Tasa de cancelación (%) |
|---|---|---|---|
| Agencia | 152 | 56 | 36.8 |
| No informado | 39 | 6 | 15.4 |
| Telefónico | 101 | 18 | 17.8 |
| Walk-in | 100 | 10 | 10.0 |
| Web | 258 | 31 | 12.0 |
Se utiliza un gráfico de barras para comparar las tasas de cancelación por canal. Las etiquetas muestran el porcentaje observado en cada grupo.
ggplot(tabla_p1, aes(x = canal_analisis, y = tasa_cancelacion)) +
geom_col(fill = "steelblue") +
geom_text(
aes(label = paste0(round(tasa_cancelacion, 1), "%")),
vjust = -0.3
) +
labs(
title = "Tasa observada de cancelación por canal",
x = "Canal de reserva",
y = "Cancelación (%)"
) +
ylim(0, max(tabla_p1$tasa_cancelacion) + 8) +
theme_minimal()
La tasa de cancelación observada en el canal Agencia es de 36.8%, mientras que los demás canales presentan tasas inferiores. Esto identifica al canal Agencia como un segmento que merece revisión operativa.
Pregunta: ¿Reservar con mucha anticipación se relaciona con pagar una tarifa distinta por noche? ¿Y con una mayor probabilidad de cancelar?
Se calculan las correlaciones de Pearson y Spearman entre los días de anticipación y la tarifa por noche. Además, se presenta un gráfico de dispersión para observar cómo se relacionan ambas variables.
cor_anticipacion_tarifa <- cor(datos$anticipacion_dias, datos$tarifa_por_noche_miles,
use = "complete.obs", method = "pearson")
cor(datos$anticipacion_dias, datos$tarifa_por_noche_miles,
use = "complete.obs", method = "spearman")
## [1] -0.03257585
ggplot(datos, aes(anticipacion_dias, tarifa_por_noche_miles)) +
geom_point(alpha = .45) +
geom_smooth(method = "loess", se = FALSE) +
labs(title = "Anticipación y tarifa por noche",
x = "Anticipación (días)", y = "Tarifa por noche (miles)") +
theme_minimal()
La correlación entre anticipación y tarifa es -0.02, por lo que no se observa una relación lineal apreciable entre ambas variables.
Para determinar si reservar con anticipación se relaciona con una mayor probabilidad de cancelar decidimos agrupar la variable días de anticipación en 3 rangos: 0 - 7 días, 8 - 30 días y más de 30 días. Lo anterior considerando que, en este sector, se pueden presentar situaciones en las que se reserve de manera inmediata (corto plazo o pocas días), mediano plazo con y largo plazo, cuando la reserva se hace con mayor planificación.
datos <- datos %>%
mutate(grupo_anticipacion = cut(
anticipacion_dias,
breaks = c(-Inf, 7, 30, Inf),
labels = c("0-7 días", "8-30 días", ">30 días")
))
q2_cancel <- datos %>%
group_by(grupo_anticipacion) %>%
summarise(
n = n(),
tasa_cancelacion = mean(cancelada == "Si") * 100,
.groups = "drop"
)
q2_cancel_filter <- q2_cancel %>%
filter(!is.na(grupo_anticipacion))
kable(
q2_cancel_filter,
digits = 1,
col.names = c(
"Grupo de anticipación",
"Reservas",
"Tasa de cancelación (%)"
),
caption = "Cancelación según anticipación de la reserva"
)
| Grupo de anticipación | Reservas | Tasa de cancelación (%) |
|---|---|---|
| 0-7 días | 224 | 13.8 |
| 8-30 días | 302 | 17.2 |
| >30 días | 118 | 29.7 |
Se utiliza un gráfico de barras para comparar las tasas de cancelación entre los grupos de anticipación. Las etiquetas muestran el porcentaje observado en cada grupo.
ggplot(q2_cancel_filter, aes(grupo_anticipacion, tasa_cancelacion)) +
geom_col() +
geom_text(aes(label = paste0(round(tasa_cancelacion, 1), "%")),
vjust = -0.3) +
labs(title = "Tasa observada de cancelación según anticipación",
x = "Grupo de anticipación", y = "Cancelación (%)") +
theme_minimal()
La tasa observada de cancelación es de: 13.8% para reservas de 0 a 7 días, 17.2% para reservas de 8 a 30 días y 29.7% para reservas con más de 30 días de anticipación.
Pregunta: ¿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?
Se presentan la media y la mediana de la tarifa, dando prioridad a esta última para la comparación. La tabla permite comparar simultáneamente ciudad y tipo de habitación.
Además, se presentan diagramas de caja para observar las diferencias en la distribución de las tarifas.
q3 <- datos %>%
group_by(ciudad_hotel, tipo_habitacion) %>%
summarise(
n = sum(!is.na(tarifa_por_noche_miles)),
media = mean(tarifa_por_noche_miles, na.rm = TRUE),
mediana = median(tarifa_por_noche_miles, na.rm = TRUE),
.groups = "drop"
)
kable(
q3,
digits = 1,
col.names = c(
"Ciudad",
"Tipo de habitación",
"Reservas con tarifa",
"Media",
"Mediana"
),
caption = "Tarifa por noche según ciudad y tipo de habitación (miles de pesos)"
)
| Ciudad | Tipo de habitación | Reservas con tarifa | Media | Mediana |
|---|---|---|---|---|
| Bogotá | Doble | 78 | 216.0 | 222.0 |
| Bogotá | Sencilla | 71 | 155.2 | 156.0 |
| Bogotá | Suite | 31 | 386.5 | 392.0 |
| Cali | Doble | 41 | 210.2 | 205.0 |
| Cali | Sencilla | 39 | 146.5 | 147.0 |
| Cali | Suite | 18 | 365.6 | 357.0 |
| Cartagena | Doble | 52 | 316.4 | 314.0 |
| Cartagena | Sencilla | 54 | 242.9 | 242.5 |
| Cartagena | Suite | 21 | 461.6 | 465.0 |
| Medellín | Doble | 67 | 222.3 | 226.0 |
| Medellín | Sencilla | 54 | 164.6 | 165.5 |
| Medellín | Suite | 23 | 391.1 | 393.0 |
| Santa Marta | Doble | 42 | 303.5 | 294.5 |
| Santa Marta | Sencilla | 37 | 218.3 | 214.0 |
| Santa Marta | Suite | 11 | 430.8 | 432.0 |
El número de reservas corresponde a los registros con tarifa disponible después de la limpieza. Los valores faltantes se excluyen del cálculo de la media y la mediana.
Se presentan diagramas de caja por tipo de habitación, separados por ciudad, para comparar el nivel y la dispersión de las tarifas.
ggplot(
datos,
aes(
x = tipo_habitacion,
y = tarifa_por_noche_miles,
fill = tipo_habitacion
)
) +
geom_boxplot(outlier.alpha = 0.35) +
facet_wrap(~ ciudad_hotel) +
labs(
title = "Tarifa por noche según habitación y ciudad",
x = "Tipo de habitación",
y = "Tarifa por noche (miles de pesos)"
) +
theme_minimal() +
theme(legend.position = "none")
En las cinco ciudades, las suites presentan las tarifas más altas, seguidas de las habitaciones dobles y las sencillas. Las tarifas más elevadas se observan en Cartagena y Santa Marta.
En Cartagena, las suites combinan tarifas altas con una menor dispersión, según lo observado en el diagrama de caja.
Pregunta: ¿La satisfacción de los huéspedes varía según el tipo de habitación o el canal de reserva?
Se utilizan únicamente las reservas no canceladas con calificación
registrada, contenidas en datos_satisfaccion. Se excluyen
las reservas canceladas y las 15 reservas no canceladas sin
calificación, sin imputar estos valores.
Para cada grupo se calcula el número de reservas, la media, la mediana, la desviación estándar y las calificaciones mínima y máxima. También se presentan diagramas de caja para comparar las distribuciones.
q4_habitacion <- datos_satisfaccion %>%
group_by(tipo_habitacion) %>%
summarise(
n = n(),
media = mean(calificacion_satisfaccion),
mediana = median(calificacion_satisfaccion),
desviacion_estandar = sd(calificacion_satisfaccion),
minimo = min(calificacion_satisfaccion),
maximo = max(calificacion_satisfaccion),
.groups = "drop"
) %>%
arrange(desc(media))
kable(
q4_habitacion,
digits = 2,
col.names = c(
"Tipo de habitación",
"Reservas",
"Media",
"Mediana",
"Desviación estándar",
"Mínimo",
"Máximo"
),
caption = "Resumen de satisfacción según tipo de habitación"
)
| Tipo de habitación | Reservas | Media | Mediana | Desviación estándar | Mínimo | Máximo |
|---|---|---|---|---|---|---|
| Suite | 85 | 8.45 | 8.50 | 1.04 | 5.6 | 10 |
| Doble | 215 | 7.49 | 7.50 | 1.15 | 4.0 | 10 |
| Sencilla | 214 | 6.93 | 6.85 | 1.07 | 4.0 | 10 |
Se presenta un diagrama de caja para comparar las calificaciones de satisfacción entre los tipos de habitación.
ggplot(
datos_satisfaccion,
aes(
x = tipo_habitacion,
y = calificacion_satisfaccion,
fill = tipo_habitacion
)
) +
geom_boxplot() +
labs(
title = "Satisfacción según tipo de habitación",
x = "Tipo de habitación",
y = "Calificación (0–10)"
) +
theme_minimal() +
theme(legend.position = "none")
Los registros sin canal de reserva se mantienen en la categoría “No informado”, para conservarlos en la comparación descriptiva.
datos_satisfaccion <- datos_satisfaccion %>%
mutate(
canal_analisis = if_else(
is.na(canal_reserva),
"No informado",
canal_reserva
)
)
q4_canal <- datos_satisfaccion %>%
group_by(canal_analisis) %>%
summarise(
n = n(),
media = mean(calificacion_satisfaccion),
mediana = median(calificacion_satisfaccion),
desviacion_estandar = sd(calificacion_satisfaccion),
minimo = min(calificacion_satisfaccion),
maximo = max(calificacion_satisfaccion),
.groups = "drop"
) %>%
arrange(desc(media))
kable(
q4_canal,
digits = 2,
col.names = c(
"Canal de reserva",
"Número de reservas",
"Media",
"Mediana",
"Desviación estándar",
"Mínimo",
"Máximo"
),
caption = "Resumen de satisfacción según canal de reserva"
)
| Canal de reserva | Número de reservas | Media | Mediana | Desviación estándar | Mínimo | Máximo |
|---|---|---|---|---|---|---|
| Web | 221 | 7.64 | 7.6 | 1.17 | 4.9 | 10.0 |
| Telefónico | 80 | 7.37 | 7.3 | 1.19 | 4.0 | 10.0 |
| No informado | 33 | 7.35 | 7.4 | 1.20 | 4.8 | 9.6 |
| Walk-in | 88 | 7.27 | 7.4 | 1.26 | 4.0 | 10.0 |
| Agencia | 92 | 7.08 | 7.1 | 1.24 | 4.4 | 9.9 |
Se presenta un diagrama de caja para comparar las calificaciones de satisfacción entre los canales de reserva.
ggplot(
datos_satisfaccion,
aes(
x = canal_analisis,
y = calificacion_satisfaccion,
fill = canal_analisis
)
) +
geom_boxplot() +
labs(
title = "Satisfacción según canal de reserva",
x = "Canal de reserva",
y = "Calificación (0–10)"
) +
theme_minimal() +
theme(legend.position = "none")
En las reservas analizadas, los huéspedes que se alojaron en suites presentan calificaciones de satisfacción más altas que los de los otros tipos de habitación.
Por canal de reserva, las diferencias observadas son pequeñas y las distribuciones se superponen. Agencia presenta una satisfacción ligeramente menor que los demás canales identificados. Este resultado, junto con la mayor tasa de cancelación observada en la pregunta 1, se señala como un aspecto para revisar, sin concluir que el canal sea la causa de ambos resultados.
La categoría “No informado” corresponde a reservas sin canal registrado y no se interpreta como un canal de reserva.
Pregunta: ¿Los clientes frecuentes se comportan distinto a los ocasionales? Se explora si cancelan con menor frecuencia y si reportan una satisfacción distinta.
Se compara la tasa observada de cancelación entre ambos grupos. Luego, se comparan la media y la mediana de satisfacción utilizando únicamente las reservas no canceladas con calificación registrada.
En las tablas, “Si” identifica a los clientes frecuentes y “No” a los ocasionales.
q5_cancel <- datos %>%
group_by(cliente_frecuente) %>%
summarise(
n = n(),
tasa_cancelacion = mean(cancelada == "Si") * 100,
.groups = "drop"
)
kable(
q5_cancel,
digits = 1,
col.names = c(
"Cliente frecuente",
"Reservas",
"Tasa de cancelación (%)"
),
caption = "Cancelación de reservas según frecuencia del cliente"
)
| Cliente frecuente | Reservas | Tasa de cancelación (%) |
|---|---|---|
| No | 470 | 18.9 |
| Si | 180 | 17.8 |
q5_sat <- datos_satisfaccion %>%
group_by(cliente_frecuente) %>%
summarise(
n = n(),
media = mean(calificacion_satisfaccion),
mediana = median(calificacion_satisfaccion),
.groups = "drop"
)
kable(
q5_sat,
digits = 2,
col.names = c(
"Cliente frecuente",
"Reservas con calificación",
"Media",
"Mediana"
),
caption = "Satisfacción según frecuencia del cliente"
)
| Cliente frecuente | Reservas con calificación | Media | Mediana |
|---|---|---|---|
| No | 369 | 7.43 | 7.4 |
| Si | 145 | 7.38 | 7.4 |
Los clientes ocasionales presentan una tasa de cancelación superior a la de los frecuentes, aunque la diferencia observada es pequeña: 1.1 puntos porcentuales.
La satisfacción reportada es similar en ambos grupos. Los clientes ocasionales tienen una media de 7.43 y los frecuentes de 7.38, una diferencia de 0.05 puntos en la escala de 0 a 10. La mediana es la misma para ambos grupos: 7.4.
Pregunta: ¿Las reservas de mayor duración presentan una menor tarifa por noche?
Se examina la relación entre el número de noches y la tarifa por noche mediante las correlaciones de Pearson y Spearman, un gráfico de dispersión y una tabla de medianas por duración de la reserva. El análisis utiliza los valores disponibles después de la limpieza.
cor_noches_tarifa_pearson <- cor(
datos$noches,
datos$tarifa_por_noche_miles,
use = "complete.obs",
method = "pearson"
)
cor_noches_tarifa_spearman <- cor(
datos$noches,
datos$tarifa_por_noche_miles,
use = "complete.obs",
method = "spearman"
)
kable(
data.frame(
Metodo = c("Pearson", "Spearman"),
Correlacion = c(
cor_noches_tarifa_pearson,
cor_noches_tarifa_spearman
)
),
digits = 3,
col.names = c("Método", "Correlación"),
caption = "Correlación entre número de noches y tarifa por noche"
)
| Método | Correlación |
|---|---|
| Pearson | 0.001 |
| Spearman | -0.011 |
Se presenta un gráfico de dispersión para observar si la tarifa por noche tiende a disminuir a medida que aumenta la duración de la reserva.
ggplot(
datos,
aes(
x = noches,
y = tarifa_por_noche_miles
)
) +
geom_point(alpha = 0.45) +
geom_smooth(method = "loess", se = FALSE) +
labs(
title = "Número de noches y tarifa por noche",
x = "Número de noches",
y = "Tarifa por noche (miles de pesos)"
) +
theme_minimal()
Se calcula la mediana de la tarifa para cada número de noches. La tabla incluye únicamente reservas con ambos valores registrados.
q6_noches <- datos %>%
filter(
!is.na(noches),
!is.na(tarifa_por_noche_miles)
) %>%
group_by(noches) %>%
summarise(
n = n(),
mediana_tarifa = median(tarifa_por_noche_miles),
.groups = "drop"
)
kable(
q6_noches,
digits = 1,
col.names = c(
"Número de noches",
"Reservas con tarifa",
"Mediana de tarifa (miles de pesos)"
),
caption = "Tarifa por noche según duración de la reserva"
)
| Número de noches | Reservas con tarifa | Mediana de tarifa (miles de pesos) |
|---|---|---|
| 1 | 23 | 186.0 |
| 2 | 90 | 243.5 |
| 3 | 149 | 225.0 |
| 4 | 153 | 234.0 |
| 5 | 99 | 221.0 |
| 6 | 51 | 233.0 |
| 7 | 42 | 241.5 |
| 8 | 14 | 191.5 |
La correlación de Pearson es de 0.001 y la de Spearman de -0.011. Ambos coeficientes son cercanos a cero.
En conjunto, el gráfico y las medianas por número de noches no muestran una disminución clara de la tarifa a medida que aumenta la duración de la reserva. Por tanto, con este análisis no se identifica un patrón general de tarifas más bajas para las estadías largas, sin que esto permita descartar descuentos en reservas específicas.
Pregunta: ¿La tasa observada de cancelación cambia según la ciudad del hotel?
Se calcula el número de reservas, el número de cancelaciones y la tasa observada de cancelación para cada ciudad. Esta tasa corresponde al porcentaje de reservas canceladas dentro de cada ciudad.
q7 <- datos %>%
group_by(ciudad_hotel) %>%
summarise(
n = n(),
cancelaciones = sum(cancelada == "Si"),
tasa_cancelacion = mean(cancelada == "Si") * 100,
.groups = "drop"
)
kable(
q7,
digits = 1,
col.names = c(
"Ciudad",
"Reservas",
"Cancelaciones",
"Tasa de cancelación (%)"
),
caption = "Reservas y cancelaciones por ciudad"
)
| Ciudad | Reservas | Cancelaciones | Tasa de cancelación (%) |
|---|---|---|---|
| Bogotá | 181 | 43 | 23.8 |
| Cali | 100 | 13 | 13.0 |
| Cartagena | 132 | 16 | 12.1 |
| Medellín | 147 | 27 | 18.4 |
| Santa Marta | 90 | 22 | 24.4 |
Se presenta un gráfico de barras para comparar las tasas observadas de cancelación entre ciudades. Las etiquetas indican el porcentaje de cancelación de cada una.
ggplot(
q7,
aes(
x = ciudad_hotel,
y = tasa_cancelacion
)
) +
geom_col(fill = "steelblue") +
geom_text(
aes(
label = paste0(round(tasa_cancelacion, 1), "%")
),
vjust = -0.3
) +
labs(
title = "Tasa observada de cancelación por ciudad",
x = "Ciudad",
y = "Cancelación (%)"
) +
ylim(0, max(q7$tasa_cancelacion) + 6) +
theme_minimal()
Santa Marta presenta la mayor tasa observada de cancelación, con un 24.4% de sus reservas canceladas, seguida de Bogotá, con un 23.8%.
Pregunta: ¿Los clientes frecuentes prefieren algún tipo de habitación en particular?
Para estudiar esta relación se utiliza una tabla de contingencia con frecuencias relativas por fila. Esta forma de presentar los datos permite comparar la distribución de habitaciones dentro de cada grupo de clientes, sin que el tamaño diferente de los grupos distorsione la comparación.
tabla_frecuente_habitacion <- table(
datos$cliente_frecuente,
datos$tipo_habitacion
)
tabla_frecuente_habitacion_prop <- prop.table(
tabla_frecuente_habitacion,
margin = 1
) * 100
kable(
as.data.frame.matrix(tabla_frecuente_habitacion),
caption = "Frecuencia de tipo de habitación según cliente frecuente"
)
| Doble | Sencilla | Suite | |
|---|---|---|---|
| No | 205 | 193 | 72 |
| Si | 78 | 67 | 35 |
kable(
round(as.data.frame.matrix(tabla_frecuente_habitacion_prop), 1),
caption = "Distribución porcentual de habitaciones dentro de cada grupo"
)
| Doble | Sencilla | Suite | |
|---|---|---|---|
| No | 43.6 | 41.1 | 15.3 |
| Si | 43.3 | 37.2 | 19.4 |
datos %>%
count(cliente_frecuente, tipo_habitacion) %>%
group_by(cliente_frecuente) %>%
mutate(proporcion = n / sum(n)) %>%
ggplot(aes(x = cliente_frecuente, y = proporcion, fill = tipo_habitacion)) +
geom_col(position = "fill") +
labs(
title = "Tipo de habitación según condición de cliente",
x = "Cliente frecuente",
y = "Proporción",
fill = "Tipo de habitación"
)
La distribución es relativamente similar entre clientes frecuentes y ocasionales. Entre los clientes frecuentes, las habitaciones Doble representan aproximadamente 43.3%, las Sencilla 37.2% y las Suite 19.4%. En los clientes ocasionales, las proporciones son aproximadamente 43.6%, 41.1% y 15.3%, respectivamente.
Por lo tanto, no se observa una preferencia marcadamente diferente por tipo de habitación entre ambos grupos en esta descripción.
Pregunta: ¿Cómo se comportan en conjunto
anticipacion_dias, noches,
tarifa_por_noche_miles y
calificacion_satisfaccion?
Se construye una matriz de correlación de Pearson con las cuatro variables. Para que todas las correlaciones sean calculadas sobre el mismo conjunto de observaciones, se utilizan únicamente los registros completos en estas cuatro variables.
cuantitativas <- datos %>%
select(
anticipacion_dias,
noches,
tarifa_por_noche_miles,
calificacion_satisfaccion
) %>%
na.omit()
matriz_cor <- cor(cuantitativas, method = "pearson")
kable(
round(matriz_cor, 2),
caption = "Matriz de correlación de las variables cuantitativas"
)
| anticipacion_dias | noches | tarifa_por_noche_miles | calificacion_satisfaccion | |
|---|---|---|---|---|
| anticipacion_dias | 1.00 | 0.03 | 0.03 | 0.01 |
| noches | 0.03 | 1.00 | -0.02 | 0.10 |
| tarifa_por_noche_miles | 0.03 | -0.02 | 1.00 | 0.33 |
| calificacion_satisfaccion | 0.01 | 0.10 | 0.33 | 1.00 |
La matriz muestra correlaciones cercanas a cero entre anticipación y tarifa, anticipación y satisfacción, y noches y tarifa. La relación entre noches y satisfacción también es débil. La correlación de mayor magnitud dentro de las relaciones entre variables diferentes corresponde a tarifa y satisfacción, con un valor aproximado de 0.33, lo que indica una asociación positiva débil.
La matriz confirma, en conjunto, que las variables cuantitativas no presentan relaciones lineales fuertes en esta base. El uso de la matriz permite visualizar simultáneamente relaciones que en los análisis por pares pueden pasar desapercibidas.
Agencia presenta la mayor tasa de cancelación entre los canales informados.
La anticipación muestra una asociación prácticamente nula con la tarifa por noche, mientras que las reservas con mayor anticipación presentan mayores tasas de cancelación.
Suite presenta las tarifas más altas y Cartagena concentra los valores más altos; se debe vigilar el efecto de valores extremos.
Suite muestra mayor satisfacción que Doble y Sencilla; Web presenta la mayor media entre canales.
Clientes frecuentes y ocasionales presentan tasas de cancelación y satisfacción bastante cercanas.
No se observa una relación lineal apreciable entre noches y tarifa por noche.
Santa Marta y Bogotá presentan las tasas de cancelación más altas entre las ciudades.
Como recomendación para la cadena hotelera, consideramos es pertinente que revisen el canal de “agencias” ya que es donde se presenta una mayor tasa de cancelación. Se debe explorar si al momento de reservar se está brindando toda información que el cliente necesita para tomar decisiones o si, por el contrario, las cancelaciones se guian por encontrar elementos no contemplados de manera inicial. Este supuesto lo consideramos al encontrar también que, para los clientes que usan este canal, la satisfacción es ligeramente menor.
Otra recomendación es que deben hacer seguimiento a las reservas con mayor anticipación incluyendo incluso en este análisis el motivo de la cancelación.
Como tercera recomendación, consideramos se debe mejorar la experiencia para los clientes en las habitaciones sencillas y dobles. Sin importar el valor que pagan por cada tipo de habitación, la experiencia debe ser memorable y buscar siempre, brindar la máxima satisfacción.
Finalmente, se debería adicionar una variable para un próximo análisis sobre la categoría del hotel en términos de lujo o estrellas. Esto permitiría analizar mejor los precios por noche y tomar decisiones frente a las tarifas cruzándolo incluso con la ciudad.