Librerías y datos

library(readr)
library(dplyr)
library(ggplot2)
library(tidyr)
library(scales)
library(leaflet)
library(viridis)
library(knitr)
# ── AirBnB (scraper propio, 1 780 registros) ─────────────────────────────────
airbnb_raw <- read_csv("AirBnB_Monterrey_FINAL_ES (3) (1).csv")

# ── Hoteles Booking (90 registros con geo) ────────────────────────────────────
hoteles_raw <- read_csv("hoteles_booking_limpio (3).csv")

# ── Etiquetas de zona ─────────────────────────────────────────────────────────
zona_labs <- c("Centro (<3 km)", "Intermedia (3-7 km)",
               "Periférica (7-15 km)", "Exterior (>15 km)")

# ── AirBnB limpio ─────────────────────────────────────────────────────────────
# precio_booking  = precio total de la estancia (5 noches en promedio)
# precio_por_noche = precio_booking / num_noches  ← variable de precio correcta
# overall_raiting  = calificacion_satisfaccion_huesped (/5)
airbnb <- airbnb_raw %>%
  mutate(
    zona = factor(
      case_when(
        dist_km_downtown <  3  ~ "Centro (<3 km)",
        dist_km_downtown <  7  ~ "Intermedia (3-7 km)",
        dist_km_downtown < 15  ~ "Periférica (7-15 km)",
        TRUE                   ~ "Exterior (>15 km)"
      ),
      levels = zona_labs
    ),
    tipo             = "AirBnB",
    overall_raiting  = calificacion_satisfaccion_huesped,      # /5
    booking_price    = precio_booking / num_noches,            # precio por noche
    number_reviews   = num_resenas,
    Dist_km_Downtown = dist_km_downtown,
    lat              = latitud,
    lon              = longitud,
    rating_10        = overall_raiting * 2                     # normalizar a /10
  )

# ── Hoteles limpio ────────────────────────────────────────────────────────────
# Precio = tarifa por 1 noche (scraper 27-abr-2026, verificado)
# Calificacion_1 en /10 (Booking.com)
hoteles <- hoteles_raw %>%
  mutate(
    zona = factor(zona, levels = zona_labs),
    tipo             = "Hotel",
    overall_raiting  = Calificacion_1,     # /10
    booking_price    = Precio,             # precio por noche
    number_reviews   = No_Comentarios,
    Dist_km_Downtown = Dist_km_Centro,
    lat              = Lat,
    lon              = Lon,
    rating_10        = overall_raiting     # ya en /10
  )

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

Pregunta A – Distancia al centro vs Precio

¿Qué relación existe 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 por noche 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/noche)` = 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 por noche")
Correlación de Pearson: Distancia al centro vs Precio por noche
Plataforma Correlación (Dist vs Precio/noche) Interpretación
AirBnB 0.134 A mayor distancia, mayor precio
Hotel 0.300 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 noche y zona – gradiente centro-periferia",
       x = NULL, y = "Precio mediano por noche (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 de la ZMM) 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(
    precio_med  = median(booking_price,   na.rm = TRUE),
    rating_med  = median(rating_10,       na.rm = TRUE),
    resenas_med = median(number_reviews,  na.rm = TRUE),
    n           = n(),
    .groups = "drop"
  ) %>%
  kable(digits = 2,
        col.names = c("Zona", "Tipo", "Precio/noche med (MXN)",
                      "Rating med (/10)", "Reseñas medianas", "N"),
        caption = "Precio por noche, satisfacción y volumen por zona y plataforma")
Precio por noche, satisfacción y volumen por zona y plataforma
Zona Tipo Precio/noche med (MXN) Rating med (/10) Reseñas medianas N
Centro (<3 km) AirBnB 794.85 9.90 14 405
Centro (<3 km) Hotel 1622.11 8.50 111 85
Intermedia (3-7 km) AirBnB 987.19 9.86 9 480
Intermedia (3-7 km) Hotel 2758.54 8.80 368 5
Periférica (7-15 km) AirBnB 1089.62 9.91 1 376
Exterior (>15 km) AirBnB 1198.58 9.86 6 519
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 por noche 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()

# Gasto total = precio por noche × num_noches (promedio 5 noches)
# Solo disponible en AirBnB
ggplot(airbnb %>% filter(!is.na(gasto_total)),
       aes(x = Dist_km_Downtown, y = gasto_total)) +
  geom_point(alpha = 0.35, size = 1.8, color = "#FF5A5F") +
  geom_smooth(method = "lm", se = TRUE, linewidth = 1.2, color = "#FF5A5F") +
  scale_y_continuous(
    labels = comma_format(prefix = "$"),
    limits = c(0, quantile(airbnb$gasto_total, 0.97, na.rm = TRUE))
  ) +
  labs(title = "Gasto total de la estancia AirBnB vs Distancia al centro",
       subtitle = "Gasto total = precio por noche × número de noches (~5 noches)",
       x = "Distancia al centro (km)", y = "Gasto total estancia (MXN)") +
  theme_minimal()

Interpretación: AirBnB en zonas intermedias y periféricas ofrece satisfacción comparable a menor costo por noche que los hoteles del Centro. La brecha es relevante para el turista sensible al presupuesto, que puede preferir AirBnB para estancias de varios días, desviando gasto 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 noche – 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 noche y zona (ordenado de mayor a menor)")
Ranking de precios por noche y zona (ordenado de mayor a menor)
zona tipo precio_mean precio_p25 precio_p75 precio_min precio_max n
Exterior (>15 km) AirBnB 1514 792 1831 85 8814 519
Periférica (7-15 km) AirBnB 1512 755 1770 89 9006 376
Intermedia (3-7 km) AirBnB 1229 644 1453 90 6756 480
Centro (<3 km) AirBnB 1046 337 1201 85 8968 405
Intermedia (3-7 km) Hotel 2764 2534 3451 1492 3586 5
Centro (<3 km) Hotel 1782 1104 2205 413 7047 85
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("<b>", titulo, "</b>",
                    "<br>Precio/noche: $", comma(round(booking_price)),
                    "<br>Zona: ", zona,
                    "<br>Rating: ", round(overall_raiting, 2), "/5",
                    "<br>Tipo: ", room_type)
  ) %>%
  addLegend("bottomright", pal = pal_ab, values = ~booking_price,
            title = "Precio/noche 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/noche: $", comma(round(booking_price)),
                    "<br>Zona: ", zona,
                    "<br>Tipo: ", type,
                    "<br>Rating: ", overall_raiting, "/10")
  ) %>%
  addLegend("bottomright", pal = pal_hot, values = ~booking_price,
            title = "Precio/noche Hotel (MXN)")

Interpretación: El heatmap y el ranking permiten identificar las zonas con mayor y menor presión de precios por noche. Los hoteles concentrados en el Centro enfrentan mayor competencia de AirBnB en precio, mientras que en zonas periféricas AirBnB domina con tarifas significativamente más bajas.


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

# Cuadrantes: precio por noche vs rating de satisfacción
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/noche-rating por zona – AirBnB")
Cuadrantes precio/noche-rating por zona – AirBnB
zona cuadrante n pct
Centro (<3 km) Bajo precio / Alto rating ⭐ 122 36%
Centro (<3 km) Alto precio / Bajo rating 87 25%
Centro (<3 km) Bajo precio / Bajo rating 82 24%
Centro (<3 km) Alto precio / Alto rating 52 15%
Intermedia (3-7 km) Bajo precio / Bajo rating 106 29%
Intermedia (3-7 km) Bajo precio / Alto rating ⭐ 89 25%
Intermedia (3-7 km) Alto precio / Bajo rating 88 24%
Intermedia (3-7 km) Alto precio / Alto rating 80 22%
Periférica (7-15 km) Alto precio / Alto rating 59 27%
Periférica (7-15 km) Bajo precio / Bajo rating 57 26%
Periférica (7-15 km) Bajo precio / Alto rating ⭐ 54 25%
Periférica (7-15 km) Alto precio / Bajo rating 48 22%
Exterior (>15 km) Alto precio / Bajo rating 128 32%
Exterior (>15 km) Alto precio / Alto rating 116 29%
Exterior (>15 km) Bajo precio / Bajo rating 91 23%
Exterior (>15 km) Bajo precio / Alto rating ⭐ 59 15%
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/noche vs Rating – AirBnB por zona",
       subtitle = "⭐ Bajo precio / Alto rating = amenaza competitiva para hoteles",
       x = "Precio por noche (MXN)",
       y = "Rating satisfacción huésped (/5)", color = NULL) +
  theme_minimal() +
  theme(legend.position = "bottom")

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/noche: $", comma(round(booking_price)),
                    "<br>Rating: ", round(overall_raiting, 2), "/5",
                    "<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, capturando turistas sensibles al precio sin sacrificar satisfacción.


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?

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
Exterior (>15 km) AirBnB 20772 6 519 40.0
Intermedia (3-7 km) AirBnB 18397 9 480 38.3
Centro (<3 km) AirBnB 15905 14 405 39.3
Periférica (7-15 km) AirBnB 8490 1 376 22.6
Centro (<3 km) Hotel 52110 111 85 613.1
Intermedia (3-7 km) Hotel 1696 368 5 339.2
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))

# AirBnB /5 × 2 → /10 para comparar con hoteles
ab_cats <- airbnb %>%
  summarise(
    Limpieza     = mean(calificacion_limpieza        * 2, na.rm = TRUE),
    Ubicacion    = mean(calificacion_ubicacion       * 2, na.rm = TRUE),
    Valor        = mean(calificacion_valor           * 2, na.rm = TRUE),
    Comunicacion = mean(calificacion_comunicacion    * 2, na.rm = TRUE),
    Check_in     = mean(calificacion_check_in        * 2, na.rm = TRUE),
    Exactitud    = mean(calificacion_exactitud       * 2, na.rm = TRUE)
  ) %>%
  mutate(tipo = "AirBnB")

hot_cats <- hoteles %>%
  summarise(
    Limpieza     = mean(cat_Limpieza,      na.rm = TRUE),
    Ubicacion    = mean(cat_Ubicacion,     na.rm = TRUE),
    Valor        = mean(cat_CalidadPrecio, na.rm = TRUE),
    Comunicacion = NA_real_,
    Check_in     = NA_real_,
    Exactitud    = NA_real_
  ) %>%
  mutate(tipo = "Hotel")

bind_rows(ab_cats, hot_cats) %>%
  pivot_longer(-tipo, names_to = "Categoria", values_to = "Score") %>%
  filter(!is.na(Score)) %>%
  ggplot(aes(x = Categoria, y = Score, fill = tipo)) +
  geom_col(position = "dodge") +
  geom_text(aes(label = round(Score, 1)),
            position = position_dodge(width = 0.9), vjust = -0.4, size = 3.5) +
  scale_fill_manual(values = c("AirBnB" = "#FF5A5F", "Hotel" = "#00A699")) +
  scale_y_continuous(limits = c(0, 10)) +
  labs(title = "Comparación de subcategorías de rating (escala /10)",
       subtitle = "AirBnB (/5 × 2) vs Hoteles Booking (/10)",
       x = NULL, y = "Score promedio (/10)", fill = NULL) +
  theme_minimal()

# Sentimiento léxico sobre descripciones
# Palabras sin acentos para evitar errores de encoding multibyte
palabras_pos <- c("excelente", "perfecto", "increible", "limpio", "comodo",
                  "amable", "recomiendo", "maravilloso", "bueno", "agradable",
                  "espectacular", "tranquilo", "moderno", "espacioso", "bonito")
palabras_neg <- c("malo", "sucio", "ruido", "ruidoso", "problema", "falla",
                  "decepcionante", "pequeno", "olor", "humedad", "lento",
                  "descuidado", "caro", "pesimo", "mejorar")

contar_palabras <- function(texto, palabras) {
  if (is.na(texto)) return(0L)
  texto <- tolower(iconv(texto, from = "UTF-8", to = "ASCII//TRANSLIT"))
  sum(sapply(palabras,
             function(p) lengths(regmatches(texto, gregexpr(p, texto, fixed = TRUE)))),
      na.rm = TRUE)
}

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

hoteles_sent <- hoteles %>%
  filter(!is.na(description)) %>%
  mutate(
    score_pos   = sapply(description, contar_palabras, palabras_pos),
    score_neg   = sapply(description, 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 descripciones por zona",
       subtitle = "Análisis léxico sobre campo 'descripcion' / 'description'",
       x = NULL, y = "% de propiedades", fill = "Sentimiento") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 15, hjust = 1))

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(`% Negatividad` = pct) %>%
  kable(caption = "Zonas con mayor proporción de descripciones negativas – prioridad de intervención")
Zonas con mayor proporción de descripciones negativas – prioridad de intervención
zona tipo sentimiento n % Negatividad
Centro (<3 km) Hotel Negativo 1 1.2
Centro (<3 km) AirBnB Negativo 1 0.4
Intermedia (3-7 km) AirBnB Negativo 1 0.3
Exterior (>15 km) AirBnB Negativo 1 0.2

Interpretación: Las zonas con mayor % de negatividad son candidatas a inversión en calidad de servicio. El análisis corre sobre descripciones del alojamiento — para sentimiento de reseñas individuales de huéspedes se requerirá scraping adicional de comentarios.


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?

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),  # /5
    .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),  # /10
    .groups = "drop"
  )

estrategia <- left_join(precio_zona_ab, precio_zona_hot, by = "zona") %>%
  mutate(
    brecha_precio = hot_med - ab_med,
    # ab_rating en /5 → ×2 para comparar con hotel /10
    brecha_rating = hot_rating_med - (ab_rating_med * 2),
    precio_optimo = round(ab_med * 1.05, 0),
    recomendacion = case_when(
      brecha_precio > 300 & brecha_rating < 0 ~
        "Reducir precio + mejorar servicio urgente",
      brecha_precio > 300 & brecha_rating >= 0 ~
        "Reducir precio – el servicio ya es competitivo",
      brecha_precio <= 0 ~
        "Precio ya 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 Med/noche", "Hotel Med/noche", "Brecha",
                "Rating AB (/5)", "Rating Hot (/10)",
                "Precio Óptimo Hotel", "Recomendación"),
  caption = "Estrategia de precios por noche: hoteles vs AirBnB"
)
Estrategia de precios por noche: hoteles vs AirBnB
Zona AirBnB Med/noche Hotel Med/noche Brecha Rating AB (/5) Rating Hot (/10) Precio Óptimo Hotel Recomendación
Centro (<3 km) 795 1622 827 5 8 835 Reducir precio + mejorar servicio urgente
Intermedia (3-7 km) 987 2759 1771 5 9 1037 Reducir precio + mejorar servicio urgente
Periférica (7-15 km) 1090 NA NA 5 NA 1144 Ajuste moderado de precio + reforzar amenidades
Exterior (>15 km) 1199 NA NA 5 NA 1259 Ajuste moderado de precio + reforzar amenidades
precio_base_mxn <- median(hoteles$booking_price, na.rm = TRUE)

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),
  Precio_Base_MXN = round(precio_base_mxn, 0)
) %>%
  mutate(
    Precio_Recomendado_MXN = round(Precio_Base_MXN * Factor_Demanda, 0),
    Precio_Recomendado_USD = round(Precio_Recomendado_MXN / 17.5, 2)
  )

kable(eventos_2026,
      format.args = list(big.mark = ","),
      caption = "Simulación de precios óptimos por noche y evento – 2026 (TC: $17.5 MXN/USD)")
Simulación de precios óptimos por noche y evento – 2026 (TC: $17.5 MXN/USD)
Evento Temporada Factor_Demanda Precio_Base_MXN Precio_Recomendado_MXN Precio_Recomendado_USD
PalNorte 2026 (abril) Alta 1.35 1,648 2,225 127.14
Verano 2026 (jul-ago) Media-Alta 1.15 1,648 1,895 108.29
FIFA World Cup 2026 (jun-jul) Muy Alta 1.80 1,648 2,966 169.49
Invierno 2026 (dic-ene) Alta 1.25 1,648 2,060 117.71
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) 1,127 960 1,503 1,044
Intermedia (3-7 km) 1,400 1,193 1,867 1,296
Periférica (7-15 km) 1,544 1,316 2,059 1,430
Exterior (>15 km) 1,700 1,448 2,266 1,574
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 noche, 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.80×), 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í.