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