Librerías y datos

library(readxl)
library(dplyr)
library(ggplot2)
library(tidyr)
library(scales)
library(sf)
library(spdep)
library(leaflet)
library(lubridate)
library(viridis)
library(corrplot)
library(knitr)
airbnb  <- read_excel("AirBnB_Data (1).xlsx", sheet = "Database_Airbnb")
hoteles <- read_excel("AirBnB_Data (1).xlsx", sheet = "Database_Hoteles")
ts_raw  <- read_excel("AirBnB_Data (1).xlsx", sheet = "Database_AirBnB_Time_Series",
                      skip = 1, col_names = TRUE)
colnames(ts_raw)[1] <- "fecha"
ts <- ts_raw %>% filter(!is.na(fecha))
ts$fecha <- as.Date(as.numeric(ts$fecha), origin = "1899-12-30")
ts$mes   <- month(ts$fecha, label = TRUE, abbr = TRUE)
ts$anio  <- year(ts$fecha)

# Normalizar nombres de columnas en hoteles
hoteles <- hoteles %>%
  rename(
    overall_raiting  = Calificación_1,
    booking_price    = Precio,
    number_reviews   = No_Comentarios,
    Dist_km_Downtown = Dist_km_Centro,
    lat              = Lat,
    lon              = Lon
  )

# Zonas por distancia
zona_bins <- c(0, 3, 7, 15, Inf)
zona_labs <- c("Centro (<3 km)", "Intermedia (3-7 km)",
               "Periférica (7-15 km)", "Exterior (>15 km)")

airbnb <- airbnb %>%
  mutate(zona = cut(Dist_km_Downtown, breaks = zona_bins,
                    labels = zona_labs, right = FALSE),
         tipo = "AirBnB",
         # Rating AirBnB ya está en /5, normalizar a /10 para comparar
         rating_10 = overall_raiting * 2,
         gasto_total = booking_price * number_nights)

hoteles <- hoteles %>%
  mutate(zona = cut(Dist_km_Downtown, breaks = zona_bins,
                    labels = zona_labs, right = FALSE),
         tipo = "Hotel",
         # Rating hoteles ya está en /10
         rating_10 = overall_raiting,
         gasto_total = booking_price * No_Noches)

combined <- bind_rows(
  airbnb %>% select(tipo, zona, booking_price, rating_10, number_reviews,
                    Dist_km_Downtown, gasto_total, lat, lon),
  hoteles %>% select(tipo, zona, booking_price, rating_10, number_reviews,
                     Dist_km_Downtown, gasto_total, lat, lon)
)

Pregunta A – Distancia al centro vs Precio

¿Qué relación encuentran entre distancia al centro de Monterrey y precios de hoteles y AirBnB?

ggplot(combined, aes(x = Dist_km_Downtown, y = booking_price, color = tipo)) +
  geom_point(alpha = 0.4, size = 2) +
  geom_smooth(method = "lm", se = TRUE, linewidth = 1.2) +
  scale_color_manual(values = c("AirBnB" = "#FF5A5F", "Hotel" = "#00A699")) +
  scale_y_continuous(labels = comma_format(prefix = "$"),
                     limits = c(0, quantile(combined$booking_price, 0.97, na.rm = TRUE))) +
  labs(title = "Precio vs Distancia al centro – AirBnB y Hoteles",
       subtitle = "Línea de regresión lineal por plataforma",
       x = "Distancia al centro (km)", y = "Precio por noche (MXN)", color = NULL) +
  theme_minimal()

cor_ab  <- cor(airbnb$Dist_km_Downtown, airbnb$booking_price, use = "complete.obs")
cor_hot <- cor(hoteles$Dist_km_Downtown, hoteles$booking_price, use = "complete.obs")

tibble(
  Plataforma = c("AirBnB", "Hotel"),
  `Correlación (Dist vs Precio)` = round(c(cor_ab, cor_hot), 3),
  Interpretación = c(
    ifelse(cor_ab < 0, "A mayor distancia, menor precio", "A mayor distancia, mayor precio"),
    ifelse(cor_hot < 0, "A mayor distancia, menor precio", "A mayor distancia, mayor precio")
  )
) %>% kable(caption = "Correlación de Pearson: Distancia al centro vs Precio")
Correlación de Pearson: Distancia al centro vs Precio
Plataforma Correlación (Dist vs Precio) Interpretación
AirBnB 0.144 A mayor distancia, mayor precio
Hotel 0.438 A mayor distancia, mayor precio
combined %>%
  group_by(zona, tipo) %>%
  summarise(precio_med = median(booking_price, na.rm = TRUE), .groups = "drop") %>%
  ggplot(aes(x = zona, y = precio_med, color = tipo, group = tipo)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 4) +
  geom_text(aes(label = comma(round(precio_med))),
            vjust = -0.8, size = 3.5) +
  scale_color_manual(values = c("AirBnB" = "#FF5A5F", "Hotel" = "#00A699")) +
  scale_y_continuous(labels = comma_format(prefix = "$")) +
  labs(title = "Precio mediano por zona – gradiente centro-periferia",
       x = NULL, y = "Precio mediano (MXN)", color = NULL) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 15, hjust = 1))

Interpretación: Un coeficiente negativo confirma que los precios caen conforme aumenta la distancia al centro — patrón más pronunciado en hoteles (concentrados en corredores corporativos) que en AirBnB (mayor dispersión geográfica). La zona Centro muestra la prima de ubicación más alta en ambas plataformas.


Pregunta B – Localización, precio, satisfacción y gasto total

¿Qué relación existe entre localización, precio, satisfacción del turista e impacto en gasto total?

combined %>%
  group_by(zona, tipo) %>%
  summarise(
    gasto_med   = median(gasto_total, na.rm = TRUE),
    precio_med  = median(booking_price, na.rm = TRUE),
    rating_med  = median(rating_10, na.rm = TRUE),
    n           = n(),
    .groups = "drop"
  ) %>%
  kable(digits = 2, caption = "Gasto total mediano, precio y rating por zona y plataforma")
Gasto total mediano, precio y rating por zona y plataforma
zona tipo gasto_med precio_med rating_med n
Centro (<3 km) AirBnB 4656 2328.0 9.61 120
Centro (<3 km) Hotel 6188 3094.0 8.40 41
Intermedia (3-7 km) AirBnB 4095 2047.5 9.62 106
Intermedia (3-7 km) Hotel 9732 4866.0 8.50 18
Periférica (7-15 km) AirBnB 4186 2093.0 9.66 23
Periférica (7-15 km) Hotel 8254 4127.0 6.80 1
Exterior (>15 km) AirBnB 33050 16525.0 9.70 1
ggplot(combined, aes(x = booking_price, y = rating_10, color = tipo)) +
  geom_point(alpha = 0.35, size = 1.8) +
  geom_smooth(method = "loess", se = TRUE, linewidth = 1.2) +
  scale_color_manual(values = c("AirBnB" = "#FF5A5F", "Hotel" = "#00A699")) +
  scale_x_continuous(labels = comma_format(prefix = "$"),
                     limits = c(0, quantile(combined$booking_price, 0.97, na.rm = TRUE))) +
  scale_y_continuous(limits = c(0, 10)) +
  facet_wrap(~zona) +
  labs(title = "Precio vs Satisfacción (rating /10) por zona",
       subtitle = "Curva LOESS – AirBnB vs Hoteles",
       x = "Precio por noche (MXN)", y = "Rating (/10)", color = NULL) +
  theme_minimal()

ggplot(combined %>% filter(!is.na(gasto_total)),
       aes(x = Dist_km_Downtown, y = gasto_total, color = tipo)) +
  geom_point(alpha = 0.3, size = 1.8) +
  geom_smooth(method = "lm", se = TRUE, linewidth = 1.2) +
  scale_color_manual(values = c("AirBnB" = "#FF5A5F", "Hotel" = "#00A699")) +
  scale_y_continuous(labels = comma_format(prefix = "$"),
                     limits = c(0, quantile(combined$gasto_total, 0.97, na.rm = TRUE))) +
  labs(title = "Gasto total de la estancia vs Distancia al centro",
       x = "Distancia al centro (km)", y = "Gasto total (MXN)", color = NULL) +
  theme_minimal()

Interpretación: Los hoteles del Centro generan mayor gasto total por estancia aunque no siempre tienen el rating más alto. AirBnB en zonas intermedias ofrece satisfacción comparable a menor costo, lo que puede desviar gasto turístico de la economía hotelera formal.


Pregunta C – Zonas con precios más altos y más bajos

¿Qué zonas concentran los precios más altos / bajos de AirBnB y Hoteles?

combined %>%
  group_by(zona, tipo) %>%
  summarise(precio_mean = mean(booking_price, na.rm = TRUE), .groups = "drop") %>%
  ggplot(aes(x = tipo, y = zona, fill = precio_mean)) +
  geom_tile(color = "white", linewidth = 0.8) +
  geom_text(aes(label = comma(round(precio_mean))), size = 4, color = "white", fontface = "bold") +
  scale_fill_viridis(option = "inferno", name = "Precio\npromedio\n(MXN)") +
  labs(title = "Heatmap de precios promedio por zona y plataforma",
       x = NULL, y = NULL) +
  theme_minimal()

combined %>%
  group_by(zona, tipo) %>%
  summarise(
    precio_mean = mean(booking_price, na.rm = TRUE),
    precio_p25  = quantile(booking_price, 0.25, na.rm = TRUE),
    precio_p75  = quantile(booking_price, 0.75, na.rm = TRUE),
    precio_min  = min(booking_price, na.rm = TRUE),
    precio_max  = max(booking_price, na.rm = TRUE),
    n           = n(),
    .groups = "drop"
  ) %>%
  arrange(tipo, desc(precio_mean)) %>%
  kable(digits = 0, caption = "Ranking de precios por zona (ordenado de mayor a menor)")
Ranking de precios por zona (ordenado de mayor a menor)
zona tipo precio_mean precio_p25 precio_p75 precio_min precio_max n
Exterior (>15 km) AirBnB 16525 16525 16525 16525 16525 1
Intermedia (3-7 km) AirBnB 2915 1616 3252 815 15711 106
Centro (<3 km) AirBnB 2683 1657 3026 916 8612 120
Periférica (7-15 km) AirBnB 2371 1658 2950 926 4993 23
Intermedia (3-7 km) Hotel 4858 4182 5690 2400 7856 18
Periférica (7-15 km) Hotel 4127 4127 4127 4127 4127 1
Centro (<3 km) Hotel 3332 2506 3892 1658 7042 41
pal_ab <- colorNumeric("YlOrRd", domain = airbnb$booking_price)

leaflet(airbnb) %>%
  addProviderTiles("CartoDB.Positron") %>%
  addCircleMarkers(
    lng = ~lon, lat = ~lat, radius = 4,
    color = ~pal_ab(booking_price), fillOpacity = 0.75, stroke = FALSE,
    popup = ~paste0("Precio: $", comma(booking_price),
                    "<br>Zona: ", zona,
                    "<br>Rating: ", overall_raiting)
  ) %>%
  addLegend("bottomright", pal = pal_ab, values = ~booking_price,
            title = "Precio AirBnB (MXN)")
pal_hot <- colorNumeric("Blues", domain = hoteles$booking_price)

leaflet(hoteles) %>%
  addProviderTiles("CartoDB.Positron") %>%
  addCircleMarkers(
    lng = ~lon, lat = ~lat, radius = 6,
    color = ~pal_hot(booking_price), fillOpacity = 0.85, stroke = FALSE,
    popup = ~paste0("<b>", Hotel, "</b>",
                    "<br>Precio: $", comma(booking_price),
                    "<br>Zona: ", zona,
                    "<br>Estrellas: ", No_Estrellas)
  ) %>%
  addLegend("bottomright", pal = pal_hot, values = ~booking_price,
            title = "Precio Hotel (MXN)")

Pregunta D – Correlación espacial: precio alto vs mejores reseñas

¿Existe correlación espacial entre zonas de precio alto y mejores reseñas? ¿Hay zonas “bajo costo-alta satisfacción”?

# Clasificar en cuadrantes de valor (precio vs rating)
airbnb_q <- airbnb %>%
  filter(!is.na(overall_raiting), !is.na(booking_price)) %>%
  mutate(
    precio_alto  = booking_price > median(booking_price, na.rm = TRUE),
    rating_alto  = overall_raiting > median(overall_raiting, na.rm = TRUE),
    cuadrante = case_when(
       precio_alto &  rating_alto ~ "Alto precio / Alto rating",
       precio_alto & !rating_alto ~ "Alto precio / Bajo rating",
      !precio_alto &  rating_alto ~ "Bajo precio / Alto rating ⭐",
      TRUE                        ~ "Bajo precio / Bajo rating"
    )
  )

airbnb_q %>%
  count(zona, cuadrante) %>%
  group_by(zona) %>%
  mutate(pct = percent(n / sum(n), accuracy = 1)) %>%
  arrange(zona, desc(n)) %>%
  kable(caption = "Cuadrantes precio-rating por zona – AirBnB")
Cuadrantes precio-rating por zona – AirBnB
zona cuadrante n pct
Centro (<3 km) Alto precio / Alto rating 33 28%
Centro (<3 km) Alto precio / Bajo rating 32 27%
Centro (<3 km) Bajo precio / Bajo rating 32 27%
Centro (<3 km) Bajo precio / Alto rating ⭐ 23 19%
Intermedia (3-7 km) Bajo precio / Bajo rating 33 31%
Intermedia (3-7 km) Alto precio / Alto rating 27 25%
Intermedia (3-7 km) Bajo precio / Alto rating ⭐ 24 23%
Intermedia (3-7 km) Alto precio / Bajo rating 22 21%
Periférica (7-15 km) Bajo precio / Alto rating ⭐ 8 35%
Periférica (7-15 km) Alto precio / Alto rating 5 22%
Periférica (7-15 km) Alto precio / Bajo rating 5 22%
Periférica (7-15 km) Bajo precio / Bajo rating 5 22%
Exterior (>15 km) Alto precio / Alto rating 1 100%
ggplot(airbnb_q,
       aes(x = booking_price, y = overall_raiting, color = cuadrante)) +
  geom_point(alpha = 0.45, size = 2) +
  geom_vline(xintercept = median(airbnb$booking_price, na.rm = TRUE),
             linetype = "dashed", color = "grey40") +
  geom_hline(yintercept = median(airbnb$overall_raiting, na.rm = TRUE),
             linetype = "dashed", color = "grey40") +
  scale_color_manual(values = c(
    "Alto precio / Alto rating"    = "#2ecc71",
    "Alto precio / Bajo rating"    = "#e74c3c",
    "Bajo precio / Alto rating ⭐" = "#f39c12",
    "Bajo precio / Bajo rating"    = "#95a5a6"
  )) +
  scale_x_continuous(labels = comma_format(prefix = "$"),
                     limits = c(0, quantile(airbnb$booking_price, 0.97, na.rm = TRUE))) +
  facet_wrap(~zona) +
  labs(title = "Cuadrantes de valor: Precio vs Rating – AirBnB por zona",
       subtitle = "⭐ Bajo precio / Alto rating = amenaza competitiva para hoteles",
       x = "Precio por noche (MXN)", y = "Rating (/5)", color = NULL) +
  theme_minimal() +
  theme(legend.position = "bottom")

# Mapa de zonas "amenaza": bajo precio, alto rating
pal_cuad <- colorFactor(
  palette = c("#2ecc71", "#e74c3c", "#f39c12", "#95a5a6"),
  levels  = c("Alto precio / Alto rating", "Alto precio / Bajo rating",
              "Bajo precio / Alto rating ⭐", "Bajo precio / Bajo rating")
)

leaflet(airbnb_q) %>%
  addProviderTiles("CartoDB.Positron") %>%
  addCircleMarkers(
    lng = ~lon, lat = ~lat, radius = 4,
    color = ~pal_cuad(cuadrante), fillOpacity = 0.8, stroke = FALSE,
    popup = ~paste0("Cuadrante: ", cuadrante,
                    "<br>Precio: $", comma(booking_price),
                    "<br>Rating: ", overall_raiting,
                    "<br>Zona: ", zona)
  ) %>%
  addLegend("bottomright", pal = pal_cuad, values = ~cuadrante,
            title = "Cuadrante valor")

Interpretación: Las propiedades en el cuadrante “Bajo precio / Alto rating” representan la mayor amenaza competitiva para hoteles. Si este cuadrante se concentra en zonas intermedias o periféricas, indica que AirBnB ofrece valor superior al hotel promedio fuera del Centro.


Pregunta E – Diferencias en reseñas y zonas estratégicas (Sentimiento)

¿Qué diferencias existen en las reseñas entre AirBnB y Hoteles? ¿Qué zonas priorizar?

# Volumen y densidad de reseñas por zona
bind_rows(
  airbnb  %>% select(zona, number_reviews, tipo),
  hoteles %>% select(zona, number_reviews, tipo)
) %>%
  group_by(zona, tipo) %>%
  summarise(
    total_resenas  = sum(number_reviews, na.rm = TRUE),
    resenas_median = median(number_reviews, na.rm = TRUE),
    n_propiedades  = n(),
    resenas_por_propiedad = round(sum(number_reviews, na.rm = TRUE) / n(), 1),
    .groups = "drop"
  ) %>%
  arrange(tipo, desc(total_resenas)) %>%
  kable(caption = "Volumen de reseñas por zona y plataforma")
Volumen de reseñas por zona y plataforma
zona tipo total_resenas resenas_median n_propiedades resenas_por_propiedad
Centro (<3 km) AirBnB 23808 173.0 120 198.4
Intermedia (3-7 km) AirBnB 19716 172.5 106 186.0
Periférica (7-15 km) AirBnB 4973 191.0 23 216.2
Exterior (>15 km) AirBnB 208 208.0 1 208.0
Centro (<3 km) Hotel 51448 995.0 41 1254.8
Intermedia (3-7 km) Hotel 13094 717.5 18 727.4
Periférica (7-15 km) Hotel 383 383.0 1 383.0
bind_rows(
  airbnb  %>% select(zona, number_reviews, tipo),
  hoteles %>% select(zona, number_reviews, tipo)
) %>%
  ggplot(aes(x = zona, y = number_reviews, fill = tipo)) +
  geom_boxplot(outlier.alpha = 0.3, outlier.size = 1) +
  scale_fill_manual(values = c("AirBnB" = "#FF5A5F", "Hotel" = "#00A699")) +
  scale_y_continuous(
    limits = c(0, quantile(airbnb$number_reviews, 0.95, na.rm = TRUE) * 1.2),
    labels = comma_format()
  ) +
  labs(title = "Distribución del número de reseñas por zona",
       x = NULL, y = "Número de reseñas", fill = NULL) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 15, hjust = 1))

# Análisis de sentimiento léxico simple (sin paquetes externos)
palabras_pos <- c("excelente", "perfecto", "increíble", "limpio", "cómodo",
                  "amable", "recomiendo", "maravilloso", "bueno", "agradable",
                  "espectacular", "tranquilo", "moderno", "espacioso", "bonito")
palabras_neg <- c("malo", "sucio", "ruido", "ruidoso", "problema", "falla",
                  "decepcionante", "pequeño", "olor", "humedad", "lento",
                  "descuidado", "caro", "pésimo", "mejorar")

contar_palabras <- function(texto, palabras) {
  texto <- tolower(texto)
  sum(sapply(palabras, function(p) lengths(regmatches(texto, gregexpr(p, texto)))), na.rm = TRUE)
}

airbnb_sent <- airbnb %>%
  filter(!is.na(reviews)) %>%
  mutate(
    score_pos = sapply(reviews, contar_palabras, palabras_pos),
    score_neg = sapply(reviews, contar_palabras, palabras_neg),
    sentimiento = case_when(
      score_pos > score_neg ~ "Positivo",
      score_neg > score_pos ~ "Negativo",
      TRUE                  ~ "Neutro"
    )
  )

hoteles_sent <- hoteles %>%
  mutate(texto_combo = paste(Comentarios_1, Comentarios_2, Comentarios_3, sep = " ")) %>%
  filter(!is.na(texto_combo)) %>%
  mutate(
    score_pos = sapply(texto_combo, contar_palabras, palabras_pos),
    score_neg = sapply(texto_combo, contar_palabras, palabras_neg),
    sentimiento = case_when(
      score_pos > score_neg ~ "Positivo",
      score_neg > score_pos ~ "Negativo",
      TRUE                  ~ "Neutro"
    )
  )
bind_rows(
  airbnb_sent  %>% select(zona, sentimiento, tipo),
  hoteles_sent %>% select(zona, sentimiento, tipo)
) %>%
  count(zona, tipo, sentimiento) %>%
  group_by(zona, tipo) %>%
  mutate(pct = n / sum(n)) %>%
  ggplot(aes(x = zona, y = pct, fill = sentimiento)) +
  geom_col() +
  geom_text(aes(label = percent(pct, accuracy = 1)),
            position = position_stack(vjust = 0.5), size = 3, color = "white") +
  scale_fill_manual(values = c("Positivo" = "#2ecc71",
                               "Neutro"   = "#95a5a6",
                               "Negativo" = "#e74c3c")) +
  scale_y_continuous(labels = percent_format()) +
  facet_wrap(~tipo) +
  labs(title = "Sentimiento de reseñas por zona",
       subtitle = "Análisis léxico – AirBnB vs Hoteles",
       x = NULL, y = "% de reseñas", fill = "Sentimiento") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 15, hjust = 1))

# Zonas prioritarias: alta negatividad = oportunidad de mejora
resumen_estrategico <- bind_rows(
  airbnb_sent  %>% select(zona, sentimiento, tipo),
  hoteles_sent %>% select(zona, sentimiento, tipo)
) %>%
  count(zona, tipo, sentimiento) %>%
  group_by(zona, tipo) %>%
  mutate(pct = round(n / sum(n) * 100, 1)) %>%
  filter(sentimiento == "Negativo") %>%
  arrange(desc(pct)) %>%
  rename(`% Reseñas negativas` = pct)

kable(resumen_estrategico,
      caption = "Zonas con mayor proporción de reseñas negativas – prioridad de intervención")
Zonas con mayor proporción de reseñas negativas – prioridad de intervención
zona tipo sentimiento n % Reseñas negativas
Intermedia (3-7 km) Hotel Negativo 2 11.1
Centro (<3 km) Hotel Negativo 4 9.8
Centro (<3 km) AirBnB Negativo 7 5.8
Intermedia (3-7 km) AirBnB Negativo 6 5.7

Interpretación: Las zonas con mayor % de reseñas negativas son candidatas a inversión en calidad de servicio. Si los hoteles de una zona tienen más negatividad que AirBnB en la misma zona, la brecha indica una ventaja competitiva clara de la plataforma que debe atenderse.


Pregunta F – Estrategia óptima de precios por zona y temporada (Analítica Prescriptiva)

¿Cuál sería la estrategia de precios por zona y temporada para competir con AirBnB maximizando ocupación y rentabilidad?

# Identificar temporadas altas/bajas históricas
temporada_mes <- ts %>%
  group_by(mes) %>%
  summarise(
    adr_med = mean(`Average Daily Rate`, na.rm = TRUE),
    occ_med = mean(`Occupancy Rate`, na.rm = TRUE),
    rev_med = mean(`Revenue (USD)`, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(
    temporada = case_when(
      mes %in% c("Mar", "Apr", "May", "Jun") ~ "Alta (primavera-verano)",
      mes %in% c("Jul", "Aug", "Sep")         ~ "Media (verano tardío)",
      mes %in% c("Dec", "Jan")                ~ "Alta (fin de año)",
      TRUE                                     ~ "Baja"
    )
  )

temporada_mes %>%
  ggplot(aes(x = mes, y = adr_med, fill = temporada)) +
  geom_col() +
  geom_text(aes(label = round(adr_med, 1)), vjust = -0.4, size = 3.5) +
  scale_fill_manual(values = c(
    "Alta (primavera-verano)" = "#e74c3c",
    "Alta (fin de año)"       = "#c0392b",
    "Media (verano tardío)"   = "#f39c12",
    "Baja"                    = "#95a5a6"
  )) +
  labs(title = "Tarifa diaria promedio por mes – Clasificación de temporada",
       x = NULL, y = "ADR promedio (USD)", fill = "Temporada") +
  theme_minimal()

# Precio competitivo: definir rango óptimo por zona
precio_zona_ab <- airbnb %>%
  group_by(zona) %>%
  summarise(
    ab_p25  = quantile(booking_price, 0.25, na.rm = TRUE),
    ab_med  = median(booking_price, na.rm = TRUE),
    ab_p75  = quantile(booking_price, 0.75, na.rm = TRUE),
    ab_rating_med = median(overall_raiting, na.rm = TRUE),
    .groups = "drop"
  )

precio_zona_hot <- hoteles %>%
  group_by(zona) %>%
  summarise(
    hot_p25  = quantile(booking_price, 0.25, na.rm = TRUE),
    hot_med  = median(booking_price, na.rm = TRUE),
    hot_p75  = quantile(booking_price, 0.75, na.rm = TRUE),
    hot_rating_med = median(overall_raiting, na.rm = TRUE),
    .groups = "drop"
  )

estrategia <- left_join(precio_zona_ab, precio_zona_hot, by = "zona") %>%
  mutate(
    brecha_precio   = hot_med - ab_med,
    brecha_rating   = hot_rating_med * 2 - ab_rating_med,  # ambos a /10
    precio_optimo   = round(ab_med * 1.05, 0),  # hotel 5% sobre AirBnB mediano
    recomendacion   = case_when(
      brecha_precio > 500 & brecha_rating < 0 ~
        "Reducir precio + mejorar servicio urgente",
      brecha_precio > 500 & brecha_rating >= 0 ~
        "Reducir precio – el servicio ya es competitivo",
      brecha_precio <= 0 ~
        "Precio ya es competitivo – mantener y mejorar diferenciadores",
      TRUE ~
        "Ajuste moderado de precio + reforzar amenidades"
    )
  )

kable(estrategia %>%
        select(zona, ab_med, hot_med, brecha_precio,
               ab_rating_med, hot_rating_med, precio_optimo, recomendacion),
      digits = 0,
      col.names = c("Zona", "AirBnB Mediana", "Hotel Mediana", "Brecha Precio",
                    "Rating AB (/5)", "Rating Hot (/10)",
                    "Precio Óptimo Hotel", "Recomendación"),
      caption = "Estrategia de precios por zona: hoteles vs AirBnB")
Estrategia de precios por zona: hoteles vs AirBnB
Zona AirBnB Mediana Hotel Mediana Brecha Precio Rating AB (/5) Rating Hot (/10) Precio Óptimo Hotel Recomendación
Centro (<3 km) 2328 3094 766 5 8 2444 Reducir precio – el servicio ya es competitivo
Intermedia (3-7 km) 2048 4866 2818 5 8 2150 Reducir precio – el servicio ya es competitivo
Periférica (7-15 km) 2093 4127 2034 5 7 2198 Reducir precio – el servicio ya es competitivo
Exterior (>15 km) 16525 NA NA 5 NA 17351 Ajuste moderado de precio + reforzar amenidades
# Simulación de precios para eventos 2026
adr_base     <- mean(ts$`Average Daily Rate`, na.rm = TRUE)
occ_base     <- mean(ts$`Occupancy Rate`, na.rm = TRUE)
temporada_alta_factor <- max(temporada_mes$adr_med) / mean(temporada_mes$adr_med)

eventos_2026 <- tibble(
  Evento        = c("PalNorte 2026 (abril)", "Verano 2026 (jul-ago)",
                    "FIFA World Cup 2026 (jun-jul)", "Invierno 2026 (dic-ene)"),
  Temporada     = c("Alta", "Media-Alta", "Muy Alta", "Alta"),
  Factor_Demanda = c(1.35, 1.15, 1.80, 1.25),
  ADR_Base_USD  = round(adr_base, 2)
) %>%
  mutate(
    ADR_Recomendado_USD = round(ADR_Base_USD * Factor_Demanda, 2),
    ADR_Recomendado_MXN = round(ADR_Recomendado_USD * 17.5, 0),
    Ocupacion_Esperada  = percent(pmin(occ_base * Factor_Demanda, 1), accuracy = 1),
    RevPAR_Est_USD      = round(ADR_Recomendado_USD * pmin(occ_base * Factor_Demanda, 1), 2)
  )

kable(eventos_2026,
      caption = "Simulación de precios óptimos por evento – 2026 (tipo de cambio: $17.5 MXN/USD)")
Simulación de precios óptimos por evento – 2026 (tipo de cambio: $17.5 MXN/USD)
Evento Temporada Factor_Demanda ADR_Base_USD ADR_Recomendado_USD ADR_Recomendado_MXN Ocupacion_Esperada RevPAR_Est_USD
PalNorte 2026 (abril) Alta 1.35 60.82 82.11 1437 65% 53.62
Verano 2026 (jul-ago) Media-Alta 1.15 60.82 69.94 1224 56% 38.90
FIFA World Cup 2026 (jun-jul) Muy Alta 1.80 60.82 109.48 1916 87% 95.32
Invierno 2026 (dic-ene) Alta 1.25 60.82 76.03 1331 60% 45.97
# Tabla cruzada: precio recomendado por zona × evento
zonas_vec   <- levels(airbnb$zona)
eventos_vec <- c("PalNorte 2026", "Verano 2026", "FIFA 2026", "Invierno 2026")
factores    <- c(1.35, 1.15, 1.80, 1.25)

expand.grid(zona = zonas_vec, evento = eventos_vec) %>%
  left_join(estrategia %>% select(zona, precio_optimo), by = "zona") %>%
  left_join(tibble(evento = eventos_vec, factor = factores), by = "evento") %>%
  mutate(precio_recomendado = round(precio_optimo * factor, 0)) %>%
  select(zona, evento, precio_recomendado) %>%
  pivot_wider(names_from = evento, values_from = precio_recomendado) %>%
  kable(caption = "Precio recomendado por noche (MXN): zona × evento 2026",
        format.args = list(big.mark = ","))
Precio recomendado por noche (MXN): zona × evento 2026
zona PalNorte 2026 Verano 2026 FIFA 2026 Invierno 2026
Centro (<3 km) 3,299 2,811 4,399 3,055
Intermedia (3-7 km) 2,902 2,472 3,870 2,688
Periférica (7-15 km) 2,967 2,528 3,956 2,748
Exterior (>15 km) 23,424 19,954 31,232 21,689
expand.grid(zona = zonas_vec, evento = eventos_vec) %>%
  left_join(estrategia %>% select(zona, precio_optimo), by = "zona") %>%
  left_join(tibble(evento = eventos_vec, factor = factores), by = "evento") %>%
  mutate(precio_recomendado = round(precio_optimo * factor, 0),
         evento = factor(evento, levels = eventos_vec)) %>%
  ggplot(aes(x = evento, y = precio_recomendado, color = zona, group = zona)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 3.5) +
  scale_y_continuous(labels = comma_format(prefix = "$")) +
  scale_color_brewer(palette = "Set1") +
  labs(title = "Estrategia de precios por zona y evento – Hoteles AMM 2026",
       subtitle = "Precios óptimos para competir con AirBnB manteniendo satisfacción",
       x = NULL, y = "Precio recomendado por noche (MXN)", color = "Zona") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 10, hjust = 1))

Interpretación prescriptiva: Durante FIFA 2026 (factor 1.80x), los hoteles del Centro pueden aplicar las tarifas más agresivas dado que AirBnB también subirá precios pero con mayor variabilidad e incertidumbre para el turista. La estrategia diferenciada por zona permite capturar demanda en todos los segmentos: turismo corporativo (Centro), turismo familiar (Intermedia) y turismo de bajo costo (Periférica/Exterior), sin canibalizar segmentos entre sí.