1. Introducción

Una cadena de hoteles con sedes en Cali, Bogotá, Medellín, Cartagena y Santa Marta quiere entender mejor el comportamiento de sus huéspedes: qué canales de reserva generan más cancelaciones, cómo cambian las tarifas según la ciudad y el tipo de habitación, y qué factores se relacionan con la satisfacción. Este reporte retoma el análisis del Laboratorio 2 (limpieza de datos y preguntas de negocio) y lo presenta de forma informe, para compartirlo con el equipo mediante un enlace.

2. Objetivo

Objetivo general: explorar la relación entre las variables de las reservas del hotel, con el fin de identificar patrones relevantes para la gestión de la cadena hotelera.

Objetivos específicos:

  • Depurar la base de datos de reservas (duplicados, categorías inconsistentes, valores imposibles y atípicos).
  • Responder nueve preguntas de negocio sobre cancelaciones, tarifas, satisfacción y comportamiento de los clientes.
  • Formular recomendaciones para la cadena hotelera a partir de los resultados.

3. Métodos

3.1 Datos

Se utilizó una base de datos de reservas de la cadena hotelera, con las siguientes variables:

Variables utilizadas en el análisis
Variable Descripcion
id_reserva Identificador de la reserva
ciudad_hotel Ciudad donde está el hotel
tipo_habitacion Tipo de habitación (Sencilla/Doble/Suite)
canal_reserva Canal por el que se hizo la reserva
cliente_frecuente Si el cliente es frecuente (Si/No)
cancelada Si la reserva fue cancelada (Si/No)
anticipacion_dias Días de anticipación con que se hizo la reserva
noches Número de noches reservadas
tarifa_por_noche_miles Tarifa por noche, en miles de pesos
calificacion_satisfaccion Calificación de satisfacción del huésped

3.2 Técnicas empleadas

Para cada par de variables se utilizó la técnica adecuada según su tipo:

  • Dos variables cualitativas: tabla de contingencia con frecuencias condicionales y diagramas de barras.
  • Dos variables cuantitativas: diagrama de dispersión y coeficientes de correlación (Pearson y Spearman).
  • Una variable cuantitativa y una cualitativa: resumen descriptivo por grupo y gráficos comparativos (violín y boxplot).

4. Carga y limpieza de datos

4.1 Carga del dataset

Se importó el archivo CSV indicando que los campos vacíos se reconozcan como NA.

library(dplyr)
library(stringr)
library(janitor)
library(readr)
library(stringi)
library(ggplot2)
library(corrplot)
basedatos2 <- read.csv("reservas_hotel_laboratorio.csv",
                                              na.strings = c("", "NA"),
                                              stringsAsFactors = FALSE)

La base contiene 668 reservas y 10 variables en total.

4.2 Estructura del dataset

Se exploró la estructura del dataset para identificar si alguna columna no quedó con el tipo de dato que debería tener.

str(basedatos2)
'data.frame':   668 obs. of  10 variables:
 $ id_reserva               : int  1468 1524 1410 1215 1149 1402 1049 1316 1171 1092 ...
 $ ciudad_hotel             : chr  "Bogota" "Medellin" "Bogota" "Medellin" ...
 $ tipo_habitacion          : chr  "Doble" "Doble" "Sencilla" "Sencilla" ...
 $ canal_reserva            : chr  "Web" "Telefonico" "Telefonico" "Agencia" ...
 $ cliente_frecuente        : chr  "No" "No" "no" "No" ...
 $ anticipacion_dias        : num  4 9 8 1 17 4 34 4 9 19 ...
 $ noches                   : int  5 4 3 1 1 5 3 4 4 1 ...
 $ tarifa_por_noche_miles   : num  227 277 140 176 162 159 218 156 161 173 ...
 $ cancelada                : chr  "No" "No" "No" "SI" ...
 $ calificacion_satisfaccion: num  7.2 8.1 6.1 NA NA 7 6.3 7.6 NA 6.4 ...
kable(summary(basedatos2), caption = "Resumen de los datos originales")
Resumen de los datos originales
id_reserva ciudad_hotel tipo_habitacion canal_reserva cliente_frecuente anticipacion_dias noches tarifa_por_noche_miles cancelada calificacion_satisfaccion
Min. :1001 Length :668 Length :668 Length :668 Length :668 Min. :-10.00 Min. : 1.000 Min. :-208.0 Length :668 Min. : 4.00
1st Qu.:1163 N.unique : 21 N.unique : 12 N.unique : 4 N.unique : 9 1st Qu.: 5.00 1st Qu.: 3.000 1st Qu.: 177.0 N.unique : 6 1st Qu.: 6.60
Median :1326 N.blank : 0 N.blank : 0 N.blank : 0 N.blank : 0 Median : 12.00 Median : 4.000 Median : 230.0 N.blank : 0 Median : 7.40
Mean :1326 Min.nchar: 4 Min.nchar: 5 Min.nchar: 3 Min.nchar: 1 Mean : 17.38 Mean : 4.555 Mean : 262.2 Min.nchar: 2 Mean : 7.42
3rd Qu.:1488 Max.nchar: 11 Max.nchar: 9 Max.nchar: 10 Max.nchar: 2 3rd Qu.: 25.00 3rd Qu.: 5.000 3rd Qu.: 311.2 Max.nchar: 2 3rd Qu.: 8.20
Max. :1650 NAs : 39 Max. :113.00 Max. :88.000 Max. :3942.0 Max. :10.00
NAs :139

4.3 Registros duplicados

Se buscaron filas idénticas ignorando la columna id_reserva, porque un mismo registro repetido puede tener un identificador distinto.

sum(duplicated(basedatos2[, -1]))     # duplicados ignorando la columna id
[1] 18
kable(basedatos2[duplicated(basedatos2[, -1]) | duplicated(basedatos2[, -1], fromLast = TRUE), ],
      caption = "Registros duplicados encontrados")
Registros duplicados encontrados
id_reserva ciudad_hotel tipo_habitacion canal_reserva cliente_frecuente anticipacion_dias noches tarifa_por_noche_miles cancelada calificacion_satisfaccion
4 1215 Medellin Sencilla Agencia No 1 1 176 SI
5 1149 Sta Marta Sencilla Telefonico Si 17 1 162 Si
36 1601 Bogota Doble Agencia No 9 2 225 no 8.8
67 1024 Santa Marta Doble Web Sí 4 2 313 No 8.6
96 1476 Cali Doble Agencia No 1 2 223 No 5.8
101 1102 bogota Suite Web S 7 4 414 No 7.0
123 1590 Medellin Doble Agencia No 12 4 276 No 8.9
141 1117 medellin Doble Web No 26 3 256 No 7.5
170 1067 bogota Doble Web Si 5 2 172 No 7.8
237 1359 Cali Sencilla Web NO 35 2 191 NO 8.6
246 1437 Medellin Sencilla Web No 15 4 167 No 7.4
265 1243 Bogota Sencilla Agencia no 2 4 209 No 6.9
266 1067 bogota Doble Web Si 5 2 172 No 7.8
270 1117 medellin Doble Web No 26 3 256 No 7.5
275 1386 Bogota Suite Agencia Si 19 1 353 No 6.8
314 1024 Santa Marta Doble Web Sí 4 2 313 No 8.6
327 1237 Cartagena Doble Web N 0 4 314 No 6.2
330 1466 Bogota Doble Agencia No 26 4 188 No 7.4
333 1590 Medellin Doble Agencia No 12 4 276 No 8.9
335 1215 Medellin Sencilla Agencia No 1 1 176 SI
349 1648 CALI Sencilla Agencia NO 37 5 124 No 7.0
398 1466 Bogota Doble Agencia No 26 4 188 No 7.4
409 1237 Cartagena Doble Web N 0 4 314 No 6.2
411 1601 Bogota Doble Agencia No 9 2 225 no 8.8
428 1648 CALI Sencilla Agencia NO 37 5 124 No 7.0
453 1437 Medellin Sencilla Web No 15 4 167 No 7.4
463 1635 Santa Marta Doble Agencia No 2 5 272 Si
499 1359 Cali Sencilla Web NO 35 2 191 NO 8.6
513 1580 Bogota Sencilla Web N 7 3 128 No 8.8
538 1476 Cali Doble Agencia No 1 2 223 No 5.8
554 1580 Bogota Sencilla Web N 7 3 128 No 8.8
573 1149 Sta Marta Sencilla Telefonico Si 17 1 162 Si
578 1243 Bogota Sencilla Agencia no 2 4 209 No 6.9
663 1386 Bogota Suite Agencia Si 19 1 353 No 6.8
664 1635 Santa Marta Doble Agencia No 2 5 272 Si
668 1102 bogota Suite Web S 7 4 414 No 7.0

Se elimina el duplicado, conservando la primera aparición:

# Se elimina el duplicado, conservando la primera aparición
basedatos3 <- basedatos2 %>% distinct(across(-id_reserva), .keep_all = TRUE)
dim(basedatos3)   # ya debe tener una fila menos
[1] 650  10

4.4 Estandarización de variables

Las variables de texto tenían la misma categoría escrita de varias formas (mayúsculas, espacios sobrantes, tildes y abreviaturas). En cada una se quitaron los espacios, se pasó a minúsculas, se eliminaron las tildes y se recodificó a una etiqueta única.

Ciudad del hotel

#ciudad_hotel

kable(table(basedatos3$ciudad_hotel, useNA = "ifany"),
      caption = "Ciudad del hotel antes de limpiar")   # ver todas las formas en que aparece
Ciudad del hotel antes de limpiar
Var1 Freq
Bogota 7
Cali 10
bogota 8
Bogota 154
BOGOTA 8
Bogotá 4
cali 5
Cali 79
CALI 6
Cartagena 108
CARTAGENA 9
cartagena 9
Cartagena 6
medellin 13
Medellin 120
MEDELLIN 8
Medellín 6
santa marta 7
Santa Marta 72
SANTA MARTA 7
Sta Marta 4
basedatos3 <- basedatos3 %>%
  mutate(
    ciudad_hotel = str_trim(ciudad_hotel),                  # quita espacios sobrantes
    ciudad_hotel = str_to_lower(ciudad_hotel),               # todo a minúscula
    ciudad_hotel = stri_trans_general(ciudad_hotel, "Latin-ASCII"), #quitar las tildes
    ciudad_hotel = recode(ciudad_hotel,
                    "cali" = "Cali",
                    "bogota" = "Bogota", "bogota" = "Bogotá", 
                    "cartagena" = "Cartagena",
                    "medellin" = "Medellin", "medellin" = "Medellín", "medellin" = "medellín",
                    "santa marta" = "Santa Marta",  "sta marta" = "Santa Marta")
  )

kable(table(basedatos3$ciudad_hotel, useNA = "ifany"),
      caption = "Ciudad del hotel después de limpiar")
Ciudad del hotel después de limpiar
Var1 Freq
Bogota 181
Cali 100
Cartagena 132
Medellin 147
Santa Marta 90

Tipo de habitación

Tipo de habitación antes de limpiar
Var1 Freq
Doble 21
doble 20
Doble 234
DOBLE 8
sencilla 9
Sencilla 223
SENCILLA 13
Sencilla 15
suite 3
Suite 96
SUITE 5
Suite 3
Tipo de habitación después de limpiar
Var1 Freq
Doble 283
Sencilla 260
Suite 107

Canal de reserva

Esta variable ya venía con categorías consistentes, por lo que no requirió transformaciones.

#canal_reserva 

kable(table(basedatos3$canal_reserva, useNA = "ifany"),
      caption = "Canal de reserva")
Canal de reserva
Var1 Freq
Agencia 152
Telefonico 101
Walk-in 100
Web 258
39

Cliente frecuente

Venía escrita de varias formas (s, si, n, no, con distintas mayúsculas y tildes). Se unificó en “Si” y “No”.

Cliente frecuente antes de limpiar
Var1 Freq
N 40
no 25
No 371
NO 34
S 5
si 5
Si 149
SI 6
Sí 15
Cliente frecuente después de limpiar
Var1 Freq
No 470
Si 180

Reserva cancelada

Se aplicó el mismo procedimiento a la variable cancelada.

Reserva cancelada antes de limpiar
Var1 Freq
no 35
No 449
NO 45
si 6
Si 104
SI 11
Reserva cancelada después de limpiar
Var1 Freq
No 529
Si 121

4.5 Datos imposibles y atípicos

Con las variables ya estandarizadas, se revisó de nuevo la estructura y el resumen del dataset para buscar valores imposibles o atípicos.

#1.1.5 Revisar datos para buscar imposibles o atípicos 

str(basedatos3)
'data.frame':   650 obs. of  10 variables:
 $ id_reserva               : int  1468 1524 1410 1215 1149 1402 1049 1316 1171 1092 ...
 $ ciudad_hotel             : chr  "Bogota" "Medellin" "Bogota" "Medellin" ...
 $ tipo_habitacion          : chr  "Doble" "Doble" "Sencilla" "Sencilla" ...
 $ canal_reserva            : chr  "Web" "Telefonico" "Telefonico" "Agencia" ...
 $ cliente_frecuente        : chr  "No" "No" "No" "No" ...
 $ anticipacion_dias        : num  4 9 8 1 17 4 34 4 9 19 ...
 $ noches                   : int  5 4 3 1 1 5 3 4 4 1 ...
 $ tarifa_por_noche_miles   : num  227 277 140 176 162 159 218 156 161 173 ...
 $ cancelada                : chr  "No" "No" "No" "Si" ...
 $ calificacion_satisfaccion: num  7.2 8.1 6.1 NA NA 7 6.3 7.6 NA 6.4 ...
kable(summary(basedatos3), caption = "Resumen del dataset después de estandarizar")
Resumen del dataset después de estandarizar
id_reserva ciudad_hotel tipo_habitacion canal_reserva cliente_frecuente anticipacion_dias noches tarifa_por_noche_miles cancelada calificacion_satisfaccion
Min. :1001 Length :650 Length :650 Length :650 Length :650 Min. :-10.00 Min. : 1.0 Min. :-208.0 Length :650 Min. : 4.000
1st Qu.:1163 N.unique : 5 N.unique : 3 N.unique : 4 N.unique : 2 1st Qu.: 5.00 1st Qu.: 3.0 1st Qu.: 178.0 N.unique : 2 1st Qu.: 6.600
Median :1326 N.blank : 0 N.blank : 0 N.blank : 0 N.blank : 0 Median : 12.00 Median : 4.0 Median : 231.0 N.blank : 0 Median : 7.400
Mean :1326 Min.nchar: 4 Min.nchar: 5 Min.nchar: 3 Min.nchar: 2 Mean : 17.51 Mean : 4.6 Mean : 263.0 Min.nchar: 2 Mean : 7.416
3rd Qu.:1488 Max.nchar: 11 Max.nchar: 8 Max.nchar: 10 Max.nchar: 2 3rd Qu.: 25.00 3rd Qu.: 5.0 3rd Qu.: 311.8 Max.nchar: 2 3rd Qu.: 8.200
Max. :1650 NAs : 39 Max. :113.00 Max. :88.0 Max. :3942.0 Max. :10.000
NAs :136

Valores imposibles

Algunas variables numéricas tenían valores negativos, que no tienen sentido en este contexto: no se puede reservar con anticipación negativa ni cobrar una tarifa negativa. Se convirtieron en NA para no distorsionar los análisis.

#anticipacion_dias

kable(basedatos3 %>% filter(anticipacion_dias < 0 | is.na(anticipacion_dias)),
      caption = "Registros con anticipación negativa o faltante")
Registros con anticipación negativa o faltante
id_reserva ciudad_hotel tipo_habitacion canal_reserva cliente_frecuente anticipacion_dias noches tarifa_por_noche_miles cancelada calificacion_satisfaccion
1510 Cali Doble Web No -4 4 260 No 7.4
1439 Santa Marta Sencilla Web No -3 5 224 Si
1239 Cartagena Sencilla Web No -7 6 276 No 8.4
1014 Bogota Doble Agencia Si -7 2 292 Si
1532 Bogota Doble Agencia No -10 6 248 Si
1628 Cartagena Sencilla Agencia No -9 5 237 No 6.7
basedatos3 <- basedatos3 %>%
  mutate(anticipacion_dias = if_else(anticipacion_dias < 0, NA_real_, anticipacion_dias))

kable(t(unclass(summary(basedatos3$anticipacion_dias))), digits = 2,
      caption = "Resumen de anticipacion_dias después de corregir")
Resumen de anticipacion_dias después de corregir
Min. 1st Qu. Median Mean 3rd Qu. Max. NAs
0 5 12 17.74 25 113 6
#tarifa_por_noche_miles 

kable(basedatos3 %>% filter(tarifa_por_noche_miles < 0 | is.na(tarifa_por_noche_miles)),
      caption = "Registros con tarifa negativa o faltante")
Registros con tarifa negativa o faltante
id_reserva ciudad_hotel tipo_habitacion canal_reserva cliente_frecuente anticipacion_dias noches tarifa_por_noche_miles cancelada calificacion_satisfaccion
1346 Medellin Sencilla Walk-in No 2 5 -125 No 6.6
1556 Bogota Sencilla Agencia Si 6 63 -65 No 8.0
1623 Medellin Doble Telefonico No 3 3 -208 No 6.9
basedatos3 <- basedatos3 %>%
  mutate(tarifa_por_noche_miles = if_else(tarifa_por_noche_miles < 0, NA_real_, tarifa_por_noche_miles))

kable(t(unclass(summary(basedatos3$tarifa_por_noche_miles))), digits = 2,
      caption = "Resumen de tarifa_por_noche_miles después de corregir")
Resumen de tarifa_por_noche_miles después de corregir
Min. 1st Qu. Median Mean 3rd Qu. Max. NAs
60 178 231 264.85 312.5 3942 3
kable(summary(basedatos3), caption = "Resumen general después de corregir valores imposibles")
Resumen general después de corregir valores imposibles
id_reserva ciudad_hotel tipo_habitacion canal_reserva cliente_frecuente anticipacion_dias noches tarifa_por_noche_miles cancelada calificacion_satisfaccion
Min. :1001 Length :650 Length :650 Length :650 Length :650 Min. : 0.00 Min. : 1.0 Min. : 60.0 Length :650 Min. : 4.000
1st Qu.:1163 N.unique : 5 N.unique : 3 N.unique : 4 N.unique : 2 1st Qu.: 5.00 1st Qu.: 3.0 1st Qu.: 178.0 N.unique : 2 1st Qu.: 6.600
Median :1326 N.blank : 0 N.blank : 0 N.blank : 0 N.blank : 0 Median : 12.00 Median : 4.0 Median : 231.0 N.blank : 0 Median : 7.400
Mean :1326 Min.nchar: 4 Min.nchar: 5 Min.nchar: 3 Min.nchar: 2 Mean : 17.74 Mean : 4.6 Mean : 264.9 Min.nchar: 2 Mean : 7.416
3rd Qu.:1488 Max.nchar: 11 Max.nchar: 8 Max.nchar: 10 Max.nchar: 2 3rd Qu.: 25.00 3rd Qu.: 5.0 3rd Qu.: 312.5 Max.nchar: 2 3rd Qu.: 8.200
Max. :1650 NAs : 39 Max. :113.00 Max. :88.0 Max. :3942.0 Max. :10.000
NAs :6 NAs :3 NAs :136

Valores atípicos: anticipación (días)

#revisar los atípicos 

#anticipacion dias 

kable(psych::describe(basedatos3$anticipacion_dias), digits = 2,
      caption = "Estadísticos descriptivos de anticipacion_dias")
Estadísticos descriptivos de anticipacion_dias
vars n mean sd median trimmed mad min max range skew kurtosis se
X1 1 644 17.74 17.43 12 14.89 13.34 0 113 113 1.78 4.39 0.69
kable(t(quantile(basedatos3$anticipacion_dias, probs = c(0.25, 0.5, 0.75), na.rm = TRUE)),
      caption = "Cuartiles de anticipacion_dias")
Cuartiles de anticipacion_dias
25% 50% 75%
5 12 25
hist(basedatos3$anticipacion_dias, breaks = 8, col = "lightblue",
     main = "Distribución de Anticipación Días", xlab = "Anticipación dias")

boxplot(
  basedatos3$anticipacion_dias,
  main = "Boxplot anticipación días",
  ylab = "Días"
)

Sí hay datos atípicos en esta variable, pero no son extraños en el contexto, pues es viable que una persona reserve con muchos días de anticipación una habitación de hotel. Por eso se conservan.

Valores atípicos: noches

#noches

kable(psych::describe(basedatos3$noches), digits = 2,
      caption = "Estadísticos descriptivos de noches")
Estadísticos descriptivos de noches
vars n mean sd median trimmed mad min max range skew kurtosis se
X1 1 650 4.6 6.05 4 3.98 1.48 1 88 87 10.17 114.06 0.24
kable(t(quantile(basedatos3$noches, probs = c(0.25, 0.5, 0.75), na.rm = TRUE)),
      caption = "Cuartiles de noches")
Cuartiles de noches
25% 50% 75%
3 4 5
hist(basedatos3$noches, breaks = 8, col = "lightblue",
     main = "Distribución de Noches por cliente", xlab = "Noches")

boxplot(
  basedatos3$noches,
  main = "Boxplot Noches por cliente",
  ylab = "Noches"
)

Se decidió eliminar los datos mayores a 60 noches porque se consideran datos imposibles y pueden entorpecer el análisis posterior de la variable. Por tal razón, se convierten en NA.

basedatos3 <- basedatos3 %>%
  mutate(noches = if_else(noches > 60, NA_real_, noches))

kable(t(unclass(summary(basedatos3$noches))), digits = 2,
      caption = "Resumen de noches después de corregir")
Resumen de noches después de corregir
Min. 1st Qu. Median Mean 3rd Qu. Max. NAs
1 3 4 4.1 5 11 5
boxplot(
  basedatos3$noches,
  main = "Boxplot Noches por cliente",
  ylab = "Noches"
)

Valores atípicos: tarifa por noche

#tarifa_por_noche_miles 

kable(psych::describe(basedatos3$tarifa_por_noche_miles), digits = 2,
      caption = "Estadísticos descriptivos de tarifa_por_noche_miles")
Estadísticos descriptivos de tarifa_por_noche_miles
vars n mean sd median trimmed mad min max range skew kurtosis se
X1 1 647 264.85 216.16 231 242.58 91.92 60 3942 3882 10.72 155.22 8.5
kable(t(quantile(basedatos3$tarifa_por_noche_miles, probs = c(0.25, 0.5, 0.75), na.rm = TRUE)),
      caption = "Cuartiles de tarifa_por_noche_miles")
Cuartiles de tarifa_por_noche_miles
25% 50% 75%
178 231 312.5
hist(basedatos3$tarifa_por_noche_miles, breaks = 8, col = "lightblue",
     main = "Distribución de Tarifa por noche", xlab = "Precio en miles")

boxplot(
  basedatos3$tarifa_por_noche_miles,
  main = "Boxplot Tarifa por noche",
  ylab = "Precio en miles"
)

Se considera posible un valor de hasta 1.000 (miles) por noche en casos de suites de lujo. Sin embargo, los valores superiores a ese rango resultan extraños en el contexto de la variable y pueden entorpecer los análisis posteriores. Por tal razón, los valores mayores a 1.000 se convierten en NA.

basedatos3 <- basedatos3 %>%
  mutate(tarifa_por_noche_miles = if_else(tarifa_por_noche_miles > 1000, NA_real_, tarifa_por_noche_miles))

kable(t(unclass(summary(basedatos3$tarifa_por_noche_miles))), digits = 2,
      caption = "Resumen de tarifa_por_noche_miles después de corregir")
Resumen de tarifa_por_noche_miles después de corregir
Min. 1st Qu. Median Mean 3rd Qu. Max. NAs
60 178 230.5 249.65 309 837 8
boxplot(
  basedatos3$tarifa_por_noche_miles,
  main = "Boxplot Tarifa por noche",
  ylab = "Precio en miles"
)

Después de la limpieza, el dataset quedó con 650 reservas y 10 variables.

5. Resultados: análisis por pregunta

5.1 Pregunta 1: ¿La cancelación depende del canal de reserva?

¿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?

Como cancelada y canal_reserva son cualitativas, se realiza un análisis con barras apiladas.

#Relacionar: cancelada con canal_reserva, como son cualitativas realizaremos un análisis con barras apiladas

tabla_gc <- table(basedatos3$cancelada, basedatos3$canal_reserva)
kable(tabla_gc, caption = "Reservas según cancelación (filas) y canal de reserva (columnas)")
Reservas según cancelación (filas) y canal de reserva (columnas)
Agencia Telefonico Walk-in Web
No 96 83 90 227
Si 56 18 10 31
kable(round(prop.table(tabla_gc, margin = 1), 3),
      caption = "Frecuencias condicionales por fila")
Frecuencias condicionales por fila
Agencia Telefonico Walk-in Web
No 0.194 0.167 0.181 0.458
Si 0.487 0.157 0.087 0.270
ggplot(dplyr::filter(basedatos3, !is.na(canal_reserva)), aes(x = canal_reserva, fill = cancelada)) +
  geom_bar(position = "fill", na.rm = TRUE) +
  geom_text(
    aes(label = after_stat(
      scales::percent(..count.. / tapply(..count.., ..x.., sum)[..x..])
    )),
    stat = "count",
    position = position_fill(vjust = 0.5)
  ) +
  scale_y_continuous(
    labels = scales::percent,
    limits = c(0, 1)
  ) +
  labs(
    title = "Cancelaciones según Canal de Reserva",
    x = "Canal de reserva",
    y = "Porcentaje",
    fill = "Cancelaciones"
  ) +
  scale_fill_manual(     
    values = c(       
      "No" = "#4C78A8",
      "Si" = "#F58518")    
  ) +
  theme_minimal()

No tenemos suficiente información para asegurar que un canal de reserva influye del todo en que una reserva se cancele o no. Sin embargo, es notorio que las reservas que más se cancelan son las que se hicieron por el canal “Agencia”, mientras que las que menos se cancelan son las que se hacen por el canal “Web”.

La modalidad “Walk-in” es la que tiene menos porcentaje de cancelaciones porque tiene sentido que si un cliente se acerca directamente a tomar el servicio, no lo cancele.

En conclusión, se recomienda fortalecer el canal “Web” y revisar el canal “Agencia” para descubrir si hay alguna razón por la cual el 37% de las reservas realizadas se han cancelado. Por ejemplo, un tema de servicio al cliente.

5.2 Pregunta 2: ¿Reservar con anticipación se relaciona con la tarifa o con la cancelación?

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

Primero se relaciona la anticipación con la tarifa; luego, la anticipación con la cancelación. Se empieza con la matriz de correlación entre las variables cuantitativas.

vars_cuant <- c("anticipacion_dias", "noches",
                "tarifa_por_noche_miles", "calificacion_satisfaccion")

matriz_cor <- cor(basedatos3[, vars_cuant], use = "complete.obs")
kable(round(matriz_cor, 3), caption = "Matriz de correlación entre variables cuantitativas")
Matriz de correlación entre variables cuantitativas
anticipacion_dias noches tarifa_por_noche_miles calificacion_satisfaccion
anticipacion_dias 1.000 0.011 0.014 0.005
noches 0.011 1.000 0.012 0.073
tarifa_por_noche_miles 0.014 0.012 1.000 0.299
calificacion_satisfaccion 0.005 0.073 0.299 1.000
corrplot(matriz_cor,
         method = "color", type = "upper", order = "hclust",
         addCoef.col = "black", tl.col = "black", tl.srt = 45,
         title = "Matriz de correlación (orden por clustering)",
         mar = c(0, 0, 2, 0))

Este gráfico nos mostró que la correlación entre las variables cuantitativas no es muy fuerte. Ahora se revisa la relación entre anticipación y tarifa.

ggplot(basedatos3, 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 = "Anticipacion de Reserva vs. Tarifa por noche: ajuste lineal (rojo) vs. loess (naranja)",
       x = "Días de anticipación", y = "Tarifa por noche (miles)") +
  theme_minimal()

cor(basedatos3$anticipacion_dias, basedatos3$tarifa_por_noche_miles,
    method = "pearson", use = "complete.obs")
[1] -0.02546293
cor(basedatos3$anticipacion_dias, basedatos3$tarifa_por_noche_miles,
    method = "spearman", use = "complete.obs")
[1] -0.03321859

De acuerdo con el gráfico, se puede concluir que no hay correlación entre las variables días de anticipación para la reserva y tarifa por noche.

A continuación se revisa la relación entre los días de anticipación y la cancelación. Al ser una cuantitativa y una cualitativa, se propone el siguiente gráfico.

ggplot(basedatos3, aes(x = cancelada, y = anticipacion_dias, fill = cancelada)) +
  geom_violin(alpha = 0.6, trim = FALSE) +
  geom_boxplot(width = 0.12, fill = "white", outlier.shape = NA) +
  labs(title = "Cancelaciones según anticipación de la reserva",
       x = "Cancelaciones", y = "Días de anticipación de la reserva") +
  theme_minimal() +
  theme(legend.position = "none")

kable(basedatos3 %>%
  group_by(cancelada) %>%
  summarise(
    media   = mean(anticipacion_dias, na.rm = TRUE),
    mediana = median(anticipacion_dias, na.rm = TRUE),
    de      = sd(anticipacion_dias, na.rm = TRUE),
    n       = n(),
    n_validos = sum(!is.na(anticipacion_dias))
  ), digits = 2, caption = "Anticipación (días) según cancelación")
Anticipación (días) según cancelación
cancelada media mediana de n n_validos
No 16.51 12 15.94 529 526
Si 23.19 16 22.20 121 118

El gráfico muestra mayor dispersión en los datos del “sí”; además, muestra que la mayoría de reservas se hacen con poca anticipación. Después de ver este gráfico, se puede concluir que no hay una diferencia entre los días de anticipación de la reserva con el hecho de cancelar o no una reserva.

5.3 Pregunta 3: ¿La tarifa cambia según el tipo de habitación y la 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?

media_ct <- basedatos3 %>%
  filter(!is.na(tarifa_por_noche_miles)) %>%
  group_by(ciudad_hotel, tipo_habitacion) %>%
  summarise(media = mean(tarifa_por_noche_miles),
            n = n(), .groups = "drop")

kable(media_ct, digits = 1, caption = "Tarifa promedio por noche según ciudad y tipo de habitación")
Tarifa promedio por noche según ciudad y tipo de habitación
ciudad_hotel tipo_habitacion media n
Bogota Doble 216.0 78
Bogota Sencilla 155.2 71
Bogota Suite 386.5 31
Cali Doble 210.2 41
Cali Sencilla 163.7 40
Cali Suite 365.6 18
Cartagena Doble 316.4 52
Cartagena Sencilla 242.9 54
Cartagena Suite 468.1 23
Medellin Doble 222.3 67
Medellin Sencilla 164.6 54
Medellin Suite 391.1 23
Santa Marta Doble 303.5 42
Santa Marta Sencilla 218.3 37
Santa Marta Suite 430.8 11
ggplot(media_ct, aes(x = tipo_habitacion, y = ciudad_hotel, fill = media)) +
  geom_tile(color = "white") +
  geom_text(aes(label = round(media, 1),
                color = media > mean(media)), size = 4) +
  scale_color_manual(values = c("TRUE" = "white", "FALSE" = "black"),
                     guide = "none") +
  scale_fill_gradient(low = "#F0F9F4", high = "#028090") +
  labs(title = "Tarifa promedio por noche según ciudad y tipo de habitación",
       x = "Tipo de habitación", y = "Ciudad",
       fill = "Tarifa media (miles)") +
  theme_minimal()

El gráfico permite observar que sí hay un incremento en la tarifa dependiendo del tipo de habitación en todas las ciudades. Sin embargo, Cartagena tiene una tarifa superior respecto a las otras sedes para todos los tipos de habitación.

Para responder la primera pregunta, la tarifa sí cambia de acuerdo a la habitación y a la ciudad. Sobre la segunda pregunta, las tarifas más altas se observan en Cartagena, seguida por Santa Marta. Respecto al tipo de habitación, las Suites son las más costosas en todas las ciudades.

5.4 Pregunta 4: ¿La satisfacción varía según el tipo de habitación o el canal de reserva?

En la limpieza se entendió que la variable de satisfacción tenía muchos valores faltantes, seguramente por quienes cancelan la reserva. Para esta pregunta, se eliminan los valores NA del gráfico y de las medias, mostrando solo los datos que sí existen.

media_scl <- basedatos3 %>%
  filter(!is.na(calificacion_satisfaccion), !is.na(tipo_habitacion), !is.na(canal_reserva)) %>%
  group_by(tipo_habitacion, canal_reserva) %>%
  summarise(media = mean(calificacion_satisfaccion), n = n(), .groups = "drop")

kable(media_scl, digits = 2,
      caption = "Satisfacción promedio según tipo de habitación y canal de reserva")
Satisfacción promedio según tipo de habitación y canal de reserva
tipo_habitacion canal_reserva media n
Doble Agencia 7.06 36
Doble Telefonico 7.19 35
Doble Walk-in 7.55 34
Doble Web 7.77 95
Sencilla Agencia 6.73 44
Sencilla Telefonico 7.00 30
Sencilla Walk-in 6.62 38
Sencilla Web 7.13 91
Suite Agencia 8.46 12
Suite Telefonico 8.53 15
Suite Walk-in 8.21 16
Suite Web 8.64 35
ggplot(media_scl, aes(x = tipo_habitacion, y = canal_reserva, fill = media)) +
  geom_tile(color = "white", linewidth = 0.8) +
  geom_text(aes(label = round(media, 1),
                color = media > mean(media)), size = 4) +
  scale_color_manual(values = c("TRUE" = "white", "FALSE" = "#08306B"),
                     guide = "none") +
  scale_fill_gradient(low = "#DEEBF7", high = "#08519C") +
  labs(title = "Satisfacción promedio según canal de reserva y tipo de habitación",
       x = "Tipo de habitación", y = "Canal de reserva",
       fill = "Satisfacción media") +
  theme_minimal()

El gráfico muestra una relación entre la calificación de la satisfacción y el tipo de habitación. Sin embargo, no hay diferencias entre la calificación de la satisfacción y los canales de reserva, puesto que no parecen influir en las opiniones de los clientes.

5.5 Pregunta 5: ¿Los clientes frecuentes se comportan distinto a los ocasionales?

Se explora si cancelan con menor frecuencia y si reportan una satisfacción distinta.

Primero se explora la relación entre el tipo de cliente y la cancelación.

tabla_cf <- table(Cliente_frecuente = basedatos3$cliente_frecuente,
                  Cancelada = basedatos3$cancelada)
kable(tabla_cf, caption = "Reservas según tipo de cliente y cancelación")
Reservas según tipo de cliente y cancelación
No Si
No 381 89
Si 148 32
prop_cf <- as.data.frame(prop.table(tabla_cf, margin = 1))
names(prop_cf) <- c("Cliente_frecuente", "Cancelada", "Proporcion")
kable(prop_cf, digits = 3, caption = "Proporción de cancelación según tipo de cliente")
Proporción de cancelación según tipo de cliente
Cliente_frecuente Cancelada Proporcion
No No 0.811
Si No 0.822
No Si 0.189
Si Si 0.178
ggplot(prop_cf, aes(x = Cancelada, y = Cliente_frecuente,
                    size = Proporcion, color = Proporcion)) +
  geom_point() +
  geom_text(aes(label = scales::percent(Proporcion, accuracy = 1)),
            color = "black", size = 3.5, vjust = -2.2) +
  scale_size_continuous(range = c(4, 22), name = "Proporción") +
  scale_color_gradient(low = "#B7E4C7", high = "#028090") +
  guides(color = "none") +
  labs(title = "Proporción de cancelaciones según tipo de cliente",
       x = "¿Reserva cancelada?", y = "Cliente frecuente") +
  theme_minimal()

De acuerdo con el gráfico, no hay relación entre la cancelación de la reserva y el tipo de cliente.

Ahora se explora la relación entre “calificación” y “cancelación”.

basedatos3 %>%
  mutate(tiene_calificacion = ifelse(is.na(calificacion_satisfaccion),
                                     "Sin calificación (NA)", "Con calificación")) %>%
  count(cancelada, tiene_calificacion) %>%
  ggplot(aes(x = cancelada, y = n, fill = tiene_calificacion)) +
  geom_col(position = "dodge", alpha = 0.85) +
  geom_text(aes(label = n), position = position_dodge(width = 0.9), vjust = -0.5) +
  scale_fill_manual(values = c("Con calificación" = "#08519C",
                               "Sin calificación (NA)" = "#9ECAE1")) +
  labs(title = "Disponibilidad de la calificación de satisfacción según cancelación",
       x = "¿Reserva cancelada?", y = "N° de reservas", fill = "") +
  theme_minimal()

El gráfico muestra que la mayoría de las personas que SI cancelaron la reserva, efectivamente no tienen registro de calificación de satisfacción. Sin embargo, 15 de los clientes que NO cancelaron la reserva, decidieron NO calificar.

Teniendo en cuenta esta información, para explorar las tres variables se propone el siguiente gráfico:

media_cfc <- basedatos3 %>%
  filter(!is.na(cliente_frecuente), !is.na(cancelada)) %>%
  group_by(cliente_frecuente, cancelada) %>%
  summarise(n_total = n(),
            n_calif = sum(!is.na(calificacion_satisfaccion)),
            media   = mean(calificacion_satisfaccion, na.rm = TRUE),
            .groups = "drop") %>%
  mutate(media = ifelse(n_calif == 0, NA_real_, media),
         etiqueta = ifelse(is.na(media),
                           paste0("Sin calificaciones\n(", n_total, " reservas)"),
                           paste0(round(media, 2), "\n(n = ", n_calif, " de ", n_total, ")")),
         color_texto = case_when(is.na(media) ~ "gris",
                                 media > mean(media, na.rm = TRUE) ~ "claro",
                                 TRUE ~ "oscuro"))

kable(media_cfc, digits = 2,
      caption = "Satisfacción promedio según tipo de cliente y cancelación")
Satisfacción promedio según tipo de cliente y cancelación
cliente_frecuente cancelada n_total n_calif media etiqueta color_texto
No No 381 369 7.43 7.43
(n = 369 de 381) claro
No Si 89 0 Sin calificaciones
(89 reservas) gris
Si No 148 145 7.38 7.38
(n = 145 de 148) oscuro
Si Si 32 0 Sin calificaciones
(32 reservas) gris
ggplot(media_cfc, aes(x = cancelada, y = cliente_frecuente, fill = media)) +
  geom_tile(color = "white", linewidth = 0.8) +
  geom_text(aes(label = etiqueta, color = color_texto), size = 4.5) +
  scale_color_manual(values = c("claro" = "white",
                                "oscuro" = "#08306B",
                                "gris" = "grey30"),
                     guide = "none") +
  scale_fill_gradient(low = "#DEEBF7", high = "#08519C",
                      na.value = "grey85") +
  labs(title = "Satisfacción promedio según tipo de cliente y cancelación",
       subtitle = "Gris: combinaciones sin calificaciones registradas",
       x = "¿Reserva cancelada?", y = "Cliente frecuente",
       fill = "Satisfacción media") +
  theme_minimal() +
  theme(panel.grid = element_blank())

Este último gráfico permite observar que no solo no hay relación entre el tipo de cliente y si cancela o no su reserva, sino que además tampoco hay relación entre el tipo de cliente y la calificación de la satisfacción.

5.6 Pregunta 6: ¿El número de noches se relaciona con la tarifa por noche?

¿Se observa algo parecido a un descuento por estadías largas, el efecto contrario, o ninguna relación?

Se busca entender si hay correlación entre las noches y la tarifa. Se propone:

ggplot(basedatos3, aes(x = noches, 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 = "No. Noches vs. Tarifa por noche: ajuste lineal (rojo) vs. loess (naranja)",
       x = "No. Noches", y = "Tarifa por noche (miles)") +
  theme_minimal()

cor(basedatos3$anticipacion_dias, basedatos3$tarifa_por_noche_miles,
    method = "pearson", use = "complete.obs")
[1] -0.02546293
cor(basedatos3$anticipacion_dias, basedatos3$tarifa_por_noche_miles,
    method = "spearman", use = "complete.obs")
[1] -0.03321859

De acuerdo con las pruebas realizadas, no hay correlación entre las variables. El gráfico permite observar que la mayoría de los clientes se quedan menos de 6 noches y que los menores precios se agrupan ahí.

5.7 Pregunta 7: ¿Hay ciudades con frecuencia de cancelación más alta?

Al ser variables cualitativas, se propone el siguiente gráfico.

tabla_cc <- table(Ciudad = basedatos3$ciudad, Cancelada = basedatos3$cancelada)
kable(tabla_cc, caption = "Reservas según ciudad y cancelación")
Reservas según ciudad y cancelación
No Si
Bogota 138 43
Cali 87 13
Cartagena 116 16
Medellin 120 27
Santa Marta 68 22
prop_cc <- as.data.frame(prop.table(tabla_cc, margin = 1))
names(prop_cc) <- c("Ciudad", "Cancelada", "Proporcion")
kable(prop_cc, digits = 3, caption = "Proporción de reservas canceladas y no canceladas por ciudad")
Proporción de reservas canceladas y no canceladas por ciudad
Ciudad Cancelada Proporcion
Bogota No 0.762
Cali No 0.870
Cartagena No 0.879
Medellin No 0.816
Santa Marta No 0.756
Bogota Si 0.238
Cali Si 0.130
Cartagena Si 0.121
Medellin Si 0.184
Santa Marta Si 0.244
ggplot(prop_cc, aes(x = Ciudad, y = Proporcion, fill = Cancelada)) +
  geom_col(color = "white") +
  geom_text(aes(label = scales::percent(Proporcion, accuracy = 1)),
            position = position_stack(vjust = 0.5), color = "white", fontface = "bold") +
  scale_fill_manual(values = c("No" = "#9E9AC8", "Si" = "#54278F")) +
  scale_y_continuous(labels = scales::percent) +
  labs(title = "Proporción de reservas canceladas y no canceladas por ciudad",
       x = "Ciudad", y = "Frecuencia relativa", fill = "Cancelada") +
  theme_minimal()

De acuerdo con el gráfico, se puede observar que las reservas que SI cancelan son similares en proporción para las ciudades Bogotá y Santa Marta (con un porcentaje bajo ambas). De las reservas que NO cancelan, la mayoría se reportan en Cartagena y Cali; sin embargo, no hay diferencia significativa con las otras ciudades.

En conclusión, estos datos muestran que no hay un porcentaje de mayor cancelación en ninguna ciudad, por lo que no se deben hacer recomendaciones específicas por área geográfica a la cadena hotelera.

5.8 Pregunta 8: ¿Los clientes frecuentes prefieren algún tipo de habitación en particular?

tabla_ch <- table(basedatos3$cliente_frecuente, basedatos3$tipo_habitacion)
kable(addmargins(tabla_ch),
      caption = "Tabla de contingencia: cliente frecuente y tipo de habitación")
Tabla de contingencia: cliente frecuente y tipo de habitación
Doble Sencilla Suite Sum
No 205 193 72 470
Si 78 67 35 180
Sum 283 260 107 650
kable(round(prop.table(tabla_ch, margin = 1), 3),
      caption = "Frecuencias condicionales por fila (dado el tipo de cliente)")
Frecuencias condicionales por fila (dado el tipo de cliente)
Doble Sencilla Suite
No 0.436 0.411 0.153
Si 0.433 0.372 0.194

A continuación se propone un gráfico:

ggplot(dplyr::filter(basedatos3, !is.na(cliente_frecuente), !is.na(tipo_habitacion)),
       aes(x = cliente_frecuente, fill = tipo_habitacion)) +
  geom_bar(position = "fill") +
  scale_y_continuous(labels = scales::percent) +
  labs(title = "Tipo de habitación según cliente frecuente",
       x = "Cliente frecuente", y = "Porcentaje", fill = "Tipo de habitación") +
  theme_minimal()

De acuerdo con el gráfico, se puede observar que no hay relación entre el tipo de habitación y si es cliente frecuente o no.

5.9 Pregunta 9: ¿Cómo se comportan en conjunto las cuatro variables cuantitativas del dataset (anticipacion_dias, noches, tarifa_por_noche_miles y calificacion_satisfaccion)?

vars_cuant <- c("anticipacion_dias", "noches",
                "tarifa_por_noche_miles", "calificacion_satisfaccion")

matriz_cor <- cor(basedatos3[, vars_cuant], use = "complete.obs")
kable(round(matriz_cor, 3), caption = "Matriz de correlación entre las cuatro variables cuantitativas")
Matriz de correlación entre las cuatro variables cuantitativas
anticipacion_dias noches tarifa_por_noche_miles calificacion_satisfaccion
anticipacion_dias 1.000 0.011 0.014 0.005
noches 0.011 1.000 0.012 0.073
tarifa_por_noche_miles 0.014 0.012 1.000 0.299
calificacion_satisfaccion 0.005 0.073 0.299 1.000
corrplot(matriz_cor,
         method = "color", type = "upper", order = "hclust",
         addCoef.col = "black", tl.col = "black", tl.srt = 45,
         title = "Matriz de correlación entre variables cuantitativas",
         mar = c(0, 0, 2, 0))

De acuerdo con el gráfico, no existen correlaciones entre las variables cuantitativas de la base de datos. No hay suficiente información para realizar recomendaciones en este sentido.

6. Conclusiones y recomendaciones

Resumen de hallazgos
Tema Hallazgo
Canal de reserva “Agencia” tiene la mayor tasa de cancelación; “Web” la menor.
Anticipación No se relaciona con la tarifa ni con la cancelación.
Tarifas Cartagena es la ciudad más cara; las Suites son el tipo de habitación más costoso.
Satisfacción Varía según el tipo de habitación, no según el canal.
Cliente frecuente Se comporta igual que el ocasional en cancelación y satisfacción.
Noches No hay descuento por estadías largas ni efecto contrario.
Ciudad Ninguna ciudad destaca por mayor cancelación.

Recomendación 1: canales de reserva. Se recomienda fortalecer el canal “Web”, que combina un volumen importante de reservas con una baja cancelación, y revisar el canal “Agencia” para entender por qué sus reservas se cancelan. Algunas hipótesis a investigar son las condiciones de cancelación pactadas con las agencias, la calidad del servicio al cliente y la ausencia de penalizaciones o anticipos.

Recomendación 2: tipo de habitación. Las suites son el tipo de habitación con mejor calificación de satisfacción promedio. Se recomienda enfocar la oferta en las suites, reforzando un servicio más premium (por ejemplo, atención personalizada y servicios adicionales), para aprovechar la buena percepción de los huéspedes.Adicionalmente, se recomienda fortalecer la oferta por tipo de habitación en todos los públicos, teniendo en cuenta que no hay relaciones con el tipo de habitación y si es cliente frecuente o no.

Estos resultados son de naturaleza descriptiva y correlacional: no permiten afirmar que una variable cause el comportamiento de otra.