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)
)
¿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")
| 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.
¿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")
| 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.
¿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)")
| 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)")
¿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")
| 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.
¿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")
| 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")
| 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.
¿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")
| 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)")
| 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 = ","))
| 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í.