1. Introducción

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.

2. Objetivo

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:

3. Carga y limpieza de datos

3.1 Carga de la información

datos <- read.csv("C:/Users/jvnse/OneDrive/Documentos/Personal/Especialización/Académico/Primer Semestre/Análisis de datos I/Laboratorio2/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"
)
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

3.2 Inspección inicial y duplicados

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)

3.3 Estandarización de variables categóricas

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.

3.4 Valores imposibles y atípicos

3.4.1. Identificación de valores negativos

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)
  )

3.4.2. Identificación de valores atípicos altos en la variable noches

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))

3.4.3. Identificación de valores atípicos altos en la variable tarifa_por_noche_miles

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))

3.4.4. Revisión gráfica y tratamiento de los valores atípicos

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"
)
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

3.4.5. Revisión gráfica posterior al tratamiento

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
)

3.5 Tratamiento de valores faltantes

Se deben revisar los faltantes de canal_reserva y calificacion_satisfaccion antes de definir su

3.5.1. Relación entre satisfacción y cancelación

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"
)
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"
)
Porcentajes dentro de cada estado de cancelación
Con calificación (%) Sin calificación (%)
No 97.2 2.8
Si 0.0 100.0

3.5.2. Base para el análisis de satisfacción

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))

3.5.3. Tratamiento de faltantes en el canal de reserva

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.

4. Análisis por pregunta

4.1 Pregunta 1 — Cancelación según canal de reserva

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"
)
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

Comparación gráfica

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()

Interpretación

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.

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

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

4.2.1. Relación entre anticipación y tarifa por noche

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.

4.2.2. Relación entre anticipación y cancelación

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"
)
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.

4.3 Pregunta 3 — Tarifa según habitación y ciudad

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.

4.3.1. Resumen de tarifas por ciudad y tipo de habitación

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)"
)
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.

4.3.2. Comparación gráfica

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")

4.3.3. Interpretación

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.

4.4 Pregunta 4 — Satisfacción según habitación y canal

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.

4.4.1. Satisfacción según tipo de habitación

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"
)
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")

4.4.2. Satisfacción según canal de reserva

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"
)
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")

4.4.3. Interpretación

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.

4.5 Pregunta 5 — Clientes frecuentes y ocasionales

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.

4.5.1. Cancelación según frecuencia del cliente

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"
)
Cancelación de reservas según frecuencia del cliente
Cliente frecuente Reservas Tasa de cancelación (%)
No 470 18.9
Si 180 17.8

4.5.2. Satisfacción según frecuencia del cliente

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"
)
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

4.5.3. Interpretación

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.

4.6. Pregunta 6 — Número de noches y tarifa por noche

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.

4.6.1. Correlación entre noches y tarifa por noche

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"
)
Correlación entre número de noches y tarifa por noche
Método Correlación
Pearson 0.001
Spearman -0.011

4.6.2. Comparación gráfica

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()

4.6.3. Resumen de tarifas por número de noches

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"
)
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

4.6.4. Interpretación

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.

4.7. Pregunta 7 — Cancelación según ciudad

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.

4.7.1. Resumen de cancelaciones por 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"
)
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

4.7.2. Comparación gráfica

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()

4.7.3. Interpretación

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%.

5. Conclusiones

  1. Agencia presenta la mayor tasa de cancelación entre los canales informados.

  2. 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.

  3. Suite presenta las tarifas más altas y Cartagena concentra los valores más altos; se debe vigilar el efecto de valores extremos.

  4. Suite muestra mayor satisfacción que Doble y Sencilla; Web presenta la mayor media entre canales.

  5. Clientes frecuentes y ocasionales presentan tasas de cancelación y satisfacción bastante cercanas.

  6. No se observa una relación lineal apreciable entre noches y tarifa por noche.

  7. Santa Marta y Bogotá presentan las tasas de cancelación más altas entre las ciudades.

  8. 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.