Part A: Spatial Visualization

Carga de librerías y datos

library(readxl)
library(leaflet)
library(osrm)
library(sf)
library(spdep)
library(spatialreg)
library(dplyr)
library(ggplot2)
library(knitr)
#brisna: "/Users/brisnaordaz/Downloads/Quiz 3 Datasets/AirBnB_Data.xlsx", sheet = "Database_Airbnb
#Vivi:
#Mari:
airbnb <- read_excel("/Users/mariirobles/Desktop/AirBnB_Data.xlsx", sheet = "Database_Airbnb")

#brisna:/Users/brisnaordaz/Downloads/Quiz 3 Datasets/AirBnB_Data.xlsx", sheet = "Database_Hoteles"
#Vivi:
#Mari:
hoteles <- read_excel("/Users/mariirobles/Desktop/AirBnB_Data.xlsx", sheet = "Database_Hoteles")

#brisna:"/Users/brisnaordaz/Downloads/Quiz 3 Datasets/zmm_data_entretenimiento.xlsx",sheet = "restaurant
#Vivi:
#Mari:
restaurantes <- read_excel("/Users/mariirobles/Desktop/zmm_data_entretenimiento.xlsx",sheet = "restaurant")

#brisna:"/Users/brisnaordaz/Downloads/Quiz 3 Datasets/zmm_data_entretenimiento.xlsx",sheet = "plazas comerciales
#Vivi:
#Mari:
malls <- read_excel("/Users/mariirobles/Desktop/zmm_data_entretenimiento.xlsx",sheet = "plazas comerciales")

1. Location Mapping

mapa1 <- leaflet()
mapa1 <- addTiles(mapa1)

# Airbnb - Azul
mapa1 <- addCircleMarkers(
  mapa1,
  lng = airbnb$lon,
  lat = airbnb$lat,
  color = "blue",
  radius = 4,
  fillOpacity = 0.6,
  group = "Airbnb",
  popup = paste("Airbnb |", airbnb$room_type, "| $", airbnb$booking_price)
)

# Hoteles - Rojo
mapa1 <- addCircleMarkers(
  mapa1,
  lng = hoteles$Lon,
  lat = hoteles$Lat,
  color = "red",
  radius = 5,
  fillOpacity = 0.7,
  group = "Hoteles",
  popup = paste("Hotel:", hoteles$Hotel, "| $", hoteles$Precio)
)

# Restaurantes - Verde
mapa1 <- addCircleMarkers(
  mapa1,
  lng = restaurantes$longitud,
  lat = restaurantes$latitud,
  color = "green",
  radius = 4,
  fillOpacity = 0.6,
  group = "Restaurantes",
  popup = paste("Rest:", restaurantes$nombre_negocio,
                "| Cal:", restaurantes$`calificación_open_table`)
)

# Malls - Naranja
mapa1 <- addCircleMarkers(
  mapa1,
  lng = malls$longitud,
  lat = malls$latitud,
  color = "orange",
  radius = 6,
  fillOpacity = 0.8,
  group = "Malls",
  popup = paste("Mall:", malls$nombre, "| Cal:", malls$calificación)
)

mapa1 <- addLayersControl(
  mapa1,
  overlayGroups = c("Airbnb", "Hoteles", "Restaurantes", "Malls"),
  options = layersControlOptions(collapsed = FALSE)
)

mapa1 <- addLegend(
  mapa1,
  position = "bottomright",
  colors = c("blue", "red", "green", "orange"),
  labels = c("Airbnb", "Hoteles", "Restaurantes", "Malls"),
  title = "Tipo de lugar"
)

mapa1

Interpretación: Los Airbnb (250 registros) se concentran principalmente en Monterrey centro y municipios como San Pedro Garza García, dispersándose hacia zonas residenciales. Los hoteles tienden a agruparse en corredores comerciales clave como Av. Revolución y el Centro. Los restaurantes y malls actúan como nodos de atracción comercial, ubicados estratégicamente en zonas de alta afluencia. Se observan clusters claros en el área de Cintermex, Valle y San Pedro, donde coinciden los cuatro tipos de establecimientos, lo que sugiere alta demanda turística y de servicios en esas zonas.


2. Isochrones Analysis

# Formato correcto para osrmIsochrone
centro <- st_sfc(st_point(c(-100.3161, 25.6866)), crs = 4326)

# Calcular isócronas 10, 20 y 30 minutos en auto
iso <- osrmIsochrone(
  loc = centro,
  breaks = c(10, 20, 30),
  res = 50
)

iso_sf <- st_as_sf(iso)

# Mapa con isócronas
mapa2 <- leaflet()
mapa2 <- addTiles(mapa2)

# 30 min - rojo (primero para que quede abajo)
mapa2 <- addPolygons(
  mapa2,
  data = iso_sf[iso_sf$isomax == 30, ],
  fillColor = "#e74c3c",
  fillOpacity = 0.2,
  color = "#e74c3c",
  weight = 2,
  label = "30 minutos"
)

# 20 min - naranja
mapa2 <- addPolygons(
  mapa2,
  data = iso_sf[iso_sf$isomax == 20, ],
  fillColor = "#f39c12",
  fillOpacity = 0.25,
  color = "#f39c12",
  weight = 2,
  label = "20 minutos"
)

# 10 min - verde
mapa2 <- addPolygons(
  mapa2,
  data = iso_sf[iso_sf$isomax == 10, ],
  fillColor = "#2ecc71",
  fillOpacity = 0.3,
  color = "#2ecc71",
  weight = 2,
  label = "10 minutos"
)

# Airbnb
mapa2 <- addCircleMarkers(
  mapa2,
  lng = airbnb$lon,
  lat = airbnb$lat,
  color = "blue",
  radius = 3,
  fillOpacity = 0.5,
  group = "Airbnb",
  popup = paste("Airbnb | $", airbnb$booking_price)
)

# Hoteles
mapa2 <- addCircleMarkers(
  mapa2,
  lng = hoteles$Lon,
  lat = hoteles$Lat,
  color = "red",
  radius = 4,
  fillOpacity = 0.7,
  group = "Hoteles",
  popup = paste("Hotel:", hoteles$Hotel)
)

# Restaurantes
mapa2 <- addCircleMarkers(
  mapa2,
  lng = restaurantes$longitud,
  lat = restaurantes$latitud,
  color = "green",
  radius = 4,
  fillOpacity = 0.6,
  group = "Restaurantes",
  popup = restaurantes$nombre_negocio
)

# Malls
mapa2 <- addCircleMarkers(
  mapa2,
  lng = malls$longitud,
  lat = malls$latitud,
  color = "orange",
  radius = 6,
  fillOpacity = 0.8,
  group = "Malls",
  popup = malls$nombre
)

mapa2 <- addLayersControl(
  mapa2,
  overlayGroups = c("Airbnb", "Hoteles", "Restaurantes", "Malls"),
  options = layersControlOptions(collapsed = FALSE)
)

mapa2 <- addLegend(
  mapa2,
  position = "bottomright",
  colors = c("#2ecc71", "#f39c12", "#e74c3c", "blue", "red", "green", "orange"),
  labels = c("10 min", "20 min", "30 min", "Airbnb", "Hoteles", "Restaurantes", "Malls"),
  title = "Accesibilidad desde Centro"
)

mapa2

Interpretación: Dentro de los primeros 10 minutos desde el centro, la mayoría de los hoteles están accesibles junto con varios restaurantes bien calificados y plazas comerciales como Galerías Monterrey. Los Airbnb tienen mayor dispersión: muchos caen en la zona de 20-30 minutos, especialmente en San Pedro y Guadalupe, lo que implica menor accesibilidad inmediata a servicios. Los malls mejor posicionados (Galerías, Paseo Tec) quedan dentro del rango de 10-20 minutos, siendo un factor competitivo importante para los hoteles ubicados cerca de ellos. Los Airbnb en zonas periféricas podrían compensar su menor accesibilidad con precios más bajos o amenidades específicas.


Part B: Spatial Regression (SAR)

airbnb <- read_excel("/Users/mariirobles/Desktop/AirBnB_Data.xlsx", sheet = "Database_Airbnb")

# Crear variable dummy: entire = 1, private room = 0
airbnb <- airbnb %>%
  mutate(
    entire_unit = ifelse(grepl("entire|loft|home|department|suite|cabin",
                               room_type, ignore.case = TRUE), 1, 0),
    log_price   = log(booking_price)
  ) %>%
  filter(!is.na(lat), !is.na(lon), !is.na(booking_price))

3. Spatial Autoregressive Model (SAR)

# Convertir a objeto espacial sf
airbnb_sf <- st_as_sf(airbnb, coords = c("lon", "lat"), crs = 4326)

# Matriz de vecinos: k = 5 vecinos más cercanos
coords   <- st_coordinates(airbnb_sf)
knn5     <- knearneigh(coords, k = 5)
nb_knn5  <- knn2nb(knn5)
listw_knn <- nb2listw(nb_knn5, style = "W")

# Verificar estructura de vecinos
cat("Resumen de vecindades (k=5):\n")
## Resumen de vecindades (k=5):
print(summary(nb_knn5))
## Neighbour list object:
## Number of regions: 250 
## Number of nonzero links: 1250 
## Percentage nonzero weights: 2 
## Average number of links: 5 
## 2 disjoint connected subgraphs
## Non-symmetric neighbours list
## Link number distribution:
## 
##   5 
## 250 
## 250 least connected regions:
## 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 with 5 links
## 250 most connected regions:
## 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 with 5 links
# Test de Moran para verificar dependencia espacial en precio
moran_result <- moran.test(airbnb$log_price, listw_knn)
cat("\n=== Test de Moran — log(booking_price) ===\n")
## 
## === Test de Moran — log(booking_price) ===
print(moran_result)
## 
##  Moran I test under randomisation
## 
## data:  airbnb$log_price  
## weights: listw_knn    
## 
## Moran I statistic standard deviate = 5.7392, p-value = 4.757e-09
## alternative hypothesis: greater
## sample estimates:
## Moran I statistic       Expectation          Variance 
##       0.203760063      -0.004016064       0.001310663
# Modelo OLS base
formula_modelo <- log_price ~ bedroom + bath + max_guests +
                              overall_raiting + Dist_km_Downtown + entire_unit

ols_model <- lm(formula_modelo, data = airbnb)

cat("\n=== Modelo OLS ===\n")
## 
## === Modelo OLS ===
summary(ols_model)
## 
## Call:
## lm(formula = formula_modelo, data = airbnb)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.95603 -0.21768 -0.02391  0.17882  2.08776 
## 
## Coefficients:
##                  Estimate Std. Error t value Pr(>|t|)    
## (Intercept)      6.417650   0.497193  12.908  < 2e-16 ***
## bedroom          0.176226   0.058711   3.002 0.002966 ** 
## bath             0.321533   0.070698   4.548 8.55e-06 ***
## max_guests       0.055152   0.014575   3.784 0.000194 ***
## overall_raiting  0.072137   0.105308   0.685 0.493990    
## Dist_km_Downtown 0.004074   0.010171   0.401 0.689129    
## entire_unit      0.186717   0.065100   2.868 0.004491 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.3623 on 243 degrees of freedom
## Multiple R-squared:  0.5326, Adjusted R-squared:  0.5211 
## F-statistic: 46.16 on 6 and 243 DF,  p-value: < 2.2e-16
# Tests de multiplicadores de Lagrange para decidir tipo de modelo espacial
lm_tests <- lm.LMtests(ols_model, listw_knn,
                        test = c("LMlag", "RLMlag", "LMerr", "RLMerr", "SARMA"))
cat("\n=== Lagrange Multiplier Tests ===\n")
## 
## === Lagrange Multiplier Tests ===
print(lm_tests)
## 
##  Rao's score (a.k.a Lagrange multiplier) diagnostics for spatial
##  dependence
## 
## data:  
## model: lm(formula = formula_modelo, data = airbnb)
## test weights: listw
## 
## RSlag = 19.636, df = 1, p-value = 9.368e-06
## 
## 
##  Rao's score (a.k.a Lagrange multiplier) diagnostics for spatial
##  dependence
## 
## data:  
## model: lm(formula = formula_modelo, data = airbnb)
## test weights: listw
## 
## adjRSlag = 0.15681, df = 1, p-value = 0.6921
## 
## 
##  Rao's score (a.k.a Lagrange multiplier) diagnostics for spatial
##  dependence
## 
## data:  
## model: lm(formula = formula_modelo, data = airbnb)
## test weights: listw
## 
## RSerr = 34.773, df = 1, p-value = 3.705e-09
## 
## 
##  Rao's score (a.k.a Lagrange multiplier) diagnostics for spatial
##  dependence
## 
## data:  
## model: lm(formula = formula_modelo, data = airbnb)
## test weights: listw
## 
## adjRSerr = 15.294, df = 1, p-value = 9.202e-05
## 
## 
##  Rao's score (a.k.a Lagrange multiplier) diagnostics for spatial
##  dependence
## 
## data:  
## model: lm(formula = formula_modelo, data = airbnb)
## test weights: listw
## 
## SARMA = 34.93, df = 2, p-value = 2.601e-08
# Modelo SAR (Spatial Lag)
sar_model <- lagsarlm(formula_modelo,
                      data   = airbnb,
                      listw  = listw_knn,
                      method = "eigen")

cat("\n=== Modelo SAR (Spatial Lag) ===\n")
## 
## === Modelo SAR (Spatial Lag) ===
summary(sar_model)
## 
## Call:lagsarlm(formula = formula_modelo, data = airbnb, listw = listw_knn, 
##     method = "eigen")
## 
## Residuals:
##       Min        1Q    Median        3Q       Max 
## -0.919989 -0.227107 -0.020968  0.203219  2.025786 
## 
## Type: lag 
## Coefficients: (asymptotic standard errors) 
##                   Estimate Std. Error z value  Pr(>|z|)
## (Intercept)      4.5624724  0.6484905  7.0355 1.985e-12
## bedroom          0.1515150  0.0565111  2.6812  0.007337
## bath             0.3172517  0.0671373  4.7254 2.296e-06
## max_guests       0.0548796  0.0138323  3.9675 7.264e-05
## overall_raiting  0.0298241  0.1000301  0.2982  0.765588
## Dist_km_Downtown 0.0058379  0.0096993  0.6019  0.547247
## entire_unit      0.1808161  0.0618611  2.9229  0.003467
## 
## Rho: 0.26948, LR test value: 16.295, p-value: 5.4204e-05
## Asymptotic standard error: 0.062063
##     z-value: 4.342, p-value: 1.4118e-05
## Wald statistic: 18.853, p-value: 1.4118e-05
## 
## Log likelihood: -89.19442 for lag model
## ML residual variance (sigma squared): 0.11816, (sigma: 0.34375)
## Number of observations: 250 
## Number of parameters estimated: 9 
## AIC: 196.39, (AIC for lm: 210.68)
## LM test for residual autocorrelation
## test value: 12.189, p-value: 0.0004807

Interpretación: El test de Moran’s I confirma dependencia espacial positiva y estadísticamente significativa en los precios de Airbnb (I = 0.2038, p = 4.76e-09), lo que valida la necesidad de un modelo espacial. Los tests LM muestran que LMlag es significativo (RSlag = 19.636, p = 9.4e-06), sin embargo RLMlag no lo es (p = 0.692), lo que indica que el modelo SAR captura adecuadamente la dependencia espacial sin requerir corrección adicional por el término de error. El coeficiente ρ = 0.2695 (p = 1.41e-05) del modelo SAR es positivo y significativo, lo que indica que el precio de una propiedad Airbnb está influenciado por el precio promedio de sus 5 vecinos más cercanos, evidenciando un mercado con clustering de precios por zona.


4. Model Comparison (OLS vs SAR)

# Extraer métricas comparativas
ols_aic  <- AIC(ols_model)
sar_aic  <- AIC(sar_model)
ols_r2   <- summary(ols_model)$r.squared
ols_adjr2 <- summary(ols_model)$adj.r.squared

# Log-likelihood
ols_loglik <- as.numeric(logLik(ols_model))
sar_loglik <- as.numeric(logLik(sar_model))

# Tabla comparativa
tabla_comp <- data.frame(
  Métrica         = c("AIC", "Log-Likelihood", "R² (OLS) / Pseudo-R²", "AIC Diferencia"),
  OLS             = c(round(ols_aic, 3),
                      round(ols_loglik, 3),
                      round(ols_r2, 4),
                      "—"),
  SAR             = c(round(sar_aic, 3),
                      round(sar_loglik, 3),
                      round(1 - sar_model$SSE / sum((airbnb$log_price - mean(airbnb$log_price))^2), 4),
                      round(ols_aic - sar_aic, 3))
)

kable(tabla_comp,
      caption = "Comparación OLS vs SAR — Variable dependiente: log(booking_price)",
      align   = "lcc")
Comparación OLS vs SAR — Variable dependiente: log(booking_price)
Métrica OLS SAR
AIC 210.684 196.3890
Log-Likelihood -97.342 -89.1940
R² (OLS) / Pseudo-R² 0.5326 0.5671
AIC Diferencia 14.2950
# Tabla de coeficientes lado a lado
coef_ols <- coef(summary(ols_model))
coef_sar <- coef(summary(sar_model))

# Nombres comunes
vars_comunes <- intersect(rownames(coef_ols), rownames(coef_sar))

tabla_coef <- data.frame(
  Variable    = vars_comunes,
  OLS_Coef    = round(coef_ols[vars_comunes, "Estimate"], 4),
  OLS_pval    = round(coef_ols[vars_comunes, "Pr(>|t|)"], 4),
  SAR_Coef    = round(coef_sar[vars_comunes, "Estimate"], 4),
  SAR_pval = round(coef_sar[vars_comunes, "Pr(>|z|)"], 4)
)

kable(tabla_coef,
      caption = "Coeficientes OLS vs SAR",
      col.names = c("Variable", "OLS β", "OLS p-val", "SAR β", "SAR p-val"),
      align = "lcccc")
Coeficientes OLS vs SAR
Variable OLS β OLS p-val SAR β SAR p-val
(Intercept) (Intercept) 6.4176 0.0000 4.5625 0.0000
bedroom bedroom 0.1762 0.0030 0.1515 0.0073
bath bath 0.3215 0.0000 0.3173 0.0000
max_guests max_guests 0.0552 0.0002 0.0549 0.0001
overall_raiting overall_raiting 0.0721 0.4940 0.0298 0.7656
Dist_km_Downtown Dist_km_Downtown 0.0041 0.6891 0.0058 0.5472
entire_unit entire_unit 0.1867 0.0045 0.1808 0.0035
# Coeficiente espacial rho
rho_val  <- sar_model$rho
rho_se   <- sar_model$rho.se
rho_z    <- rho_val / rho_se
rho_pval <- 2 * pnorm(-abs(rho_z))

cat("=== Coeficiente de Rezago Espacial (ρ) ===\n")
## === Coeficiente de Rezago Espacial (ρ) ===
cat("ρ (rho):       ", round(rho_val, 4), "\n")
## ρ (rho):        0.2695
cat("Error estándar:", round(rho_se, 4), "\n")
## Error estándar: 0.0621
cat("Z-value:       ", round(rho_z, 4), "\n")
## Z-value:        4.342
cat("p-value:       ", round(rho_pval, 4), "\n")
## p-value:        0
# Comparación de residuales OLS vs SAR
residuales_df <- data.frame(
  index      = 1:nrow(airbnb),
  res_ols    = residuals(ols_model),
  res_sar    = residuals(sar_model),
  log_price  = airbnb$log_price
)

# Distribución de residuales
par(mfrow = c(1, 2))

hist(residuales_df$res_ols,
     main   = "Residuales OLS",
     xlab   = "Residual",
     col    = "#3498db",
     border = "white",
     breaks = 20)

hist(residuales_df$res_sar,
     main   = "Residuales SAR",
     xlab   = "Residual",
     col    = "#e74c3c",
     border = "white",
     breaks = 20)

par(mfrow = c(1, 1))
# Test de Moran sobre residuales de ambos modelos
moran_ols <- moran.test(residuals(ols_model), listw_knn)
moran_sar <- moran.test(residuals(sar_model), listw_knn)

cat("=== Moran's I — Residuales OLS ===\n")
## === Moran's I — Residuales OLS ===
cat("I:", round(moran_ols$statistic, 4),
    "| p-value:", round(moran_ols$p.value, 4), "\n\n")
## I: 6.147 | p-value: 0
cat("=== Moran's I — Residuales SAR ===\n")
## === Moran's I — Residuales SAR ===
cat("I:", round(moran_sar$statistic, 4),
    "| p-value:", round(moran_sar$p.value, 4), "\n")
## I: 2.707 | p-value: 0.0034
# Precio predicho vs real: OLS vs SAR
pred_df <- data.frame(
  real    = airbnb$log_price,
  pred_ols = fitted(ols_model),
  pred_sar = fitted(sar_model)
)

ggplot(pred_df) +
  geom_point(aes(x = real, y = pred_ols, color = "OLS"),
             alpha = 0.5, size = 1.5) +
  geom_point(aes(x = real, y = pred_sar, color = "SAR"),
             alpha = 0.5, size = 1.5) +
  geom_abline(slope = 1, intercept = 0,
              linetype = "dashed", color = "black") +
  scale_color_manual(values = c("OLS" = "#3498db", "SAR" = "#e74c3c")) +
  labs(
    title   = "Valores predichos vs reales — log(booking_price)",
    subtitle = "Línea punteada = predicción perfecta",
    x       = "log(Precio) real",
    y       = "log(Precio) predicho",
    color   = "Modelo"
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "bottom")

Interpretación: La comparación OLS vs SAR revela tres hallazgos clave. Primero, el modelo SAR presenta un AIC menor (196.39 vs 210.68) y mayor pseudo-R² (0.567 vs 0.533), confirmando mejor ajuste al incorporar la dependencia espacial. Segundo, en el OLS solo resultan significativos bedroom (p = 0.003), bath (p < 0.001), max_guests (p < 0.001) y entire_unit (p = 0.004); overall_raiting (p = 0.494) y Dist_km_Downtown (p = 0.689) no son significativos de manera individual, lo que sugiere que su efecto se diluye al no controlar la estructura espacial. Tercero, el test de Moran sobre los residuales confirma que el SAR reduce la autocorrelación espacial (I = 2.707, p = 0.003) respecto al OLS (I = 6.147, p ≈ 0), aunque aún persiste algo de estructura espacial residual. Esto implica que parte del efecto aparente de la distancia al centro en el OLS era en realidad un efecto de contagio de precios entre propiedades vecinas, que el coeficiente ρ captura correctamente.


Part C: Sentiment Analysis

Carga de librerías y datos de reviews

library(tidytext)
library(stringr)
library(tidyr)
library(scales)
library(wordcloud)
library(RColorBrewer)
# Brisna: "/Users/brisnaordaz/Downloads/Quiz 3 Datasets/AirBnB_Raitings_Reviews_Mty.xlsx"
# Vivi:
# Mari:
reviews_raw <- read_excel("/Users/mariirobles/Desktop/AirBnB_Raitings_Reviews_Mty.xlsx",
                          sheet = "Sheet1")

5. Text Preprocessing

# Eliminar filas sin review
reviews <- reviews_raw %>%
  filter(!is.na(reviews), str_trim(reviews) != "") %>%
  mutate(
    # Limpiar texto: minúsculas, quitar puntuación y caracteres especiales
    review_limpio = reviews %>%
      str_to_lower() %>%
      str_replace_all("[áàäâ]", "a") %>%
      str_replace_all("[éèëê]", "e") %>%
      str_replace_all("[íìïî]", "i") %>%
      str_replace_all("[óòöô]", "o") %>%
      str_replace_all("[úùüû]", "u") %>%
      str_replace_all("[ñ]", "n") %>%
      str_replace_all("[^a-z0-9 ]", " ") %>%
      str_squish()
  )

cat("=== Resumen de Reviews ===\n")
## === Resumen de Reviews ===
cat("Total de propiedades con review:", nrow(reviews), "\n")
## Total de propiedades con review: 249
cat("Municipios:", paste(unique(reviews$Municipio), collapse = ", "), "\n")
## Municipios: Monterrey
cat("Rating promedio:", round(mean(reviews$overall_raiting, na.rm = TRUE), 3), "\n")
## Rating promedio: 4.771
# Muestra de reviews limpios
head(reviews %>% select(property_id, Municipio, overall_raiting, review_limpio), 5) %>%
  kable(caption = "Muestra de reviews preprocesados")
Muestra de reviews preprocesados
property_id Municipio overall_raiting review_limpio
ab-595200358660223000 Monterrey 4.78 mi estancia estuvo muy bien el lugar muy agradable y comodo realmente descansamos
ab-46511094 Monterrey 4.74 creo es la 3ra vez que reservo en este lugar y siempre ha sido de mi total agrado como tip revisar cual es la habitacion a reservar porque la 3 tiene el bano afuera de la habitacion y a veces es incomodo pasar por areas comunes muy recomendable
ab-54194714 Monterrey 4.81 la verdad todo muy bien buena comunicacion
ab-24443952 Monterrey 4.97 marcela esta muy al pendiente de cualquier necesidad y tiene presente hasta el minimo detalle para que la estadia sea lo mejor posible para el huesped
ab-28179952 Monterrey 4.85 todo bien zona de barrio
# Tokenizar: una palabra por fila
tokens <- reviews %>%
  select(property_id, Municipio, overall_raiting, booking_price, lat, lon,
         Dist_km_downtown, review_limpio) %>%
  unnest_tokens(word, review_limpio)

# Stopwords en español (palabras vacías)
stopwords_es <- c(
  "el", "la", "los", "las", "un", "una", "unos", "unas",
  "de", "del", "al", "a", "en", "con", "por", "para", "que",
  "se", "es", "son", "fue", "muy", "mas", "pero", "sin",
  "su", "sus", "lo", "le", "les", "nos", "me", "mi",
  "y", "o", "e", "u", "si", "no", "ni", "ya", "todo",
  "fue", "ser", "estar", "ha", "he", "han", "hay",
  "como", "cuando", "donde", "quien", "que", "cual",
  "este", "esta", "estos", "estas", "ese", "esa",
  "bien", "buen", "buena", "bueno", "buenas", "buenos",
  "todo", "toda", "todos", "todas", "mas", "algo", "porque",
  "tb", "tmb", "x", "q", "d", "k", "xq"
)

# Filtrar stopwords y palabras muy cortas
tokens_limpios <- tokens %>%
  filter(
    !word %in% stopwords_es,
    str_length(word) > 2,
    !str_detect(word, "^[0-9]+$")
  )

cat("=== Estadísticas de Tokenización ===\n")
## === Estadísticas de Tokenización ===
cat("Total tokens originales:", nrow(tokens), "\n")
## Total tokens originales: 5205
cat("Tokens después de filtro:", nrow(tokens_limpios), "\n")
## Tokens después de filtro: 2980
cat("Tokens eliminados (stopwords + cortos):", nrow(tokens) - nrow(tokens_limpios), "\n\n")
## Tokens eliminados (stopwords + cortos): 2225
# Top 15 palabras más frecuentes
top_palabras <- tokens_limpios %>%
  count(word, sort = TRUE) %>%
  head(15)

kable(top_palabras, caption = "Top 15 palabras más frecuentes en reviews",
      col.names = c("Palabra", "Frecuencia"))
Top 15 palabras más frecuentes en reviews
Palabra Frecuencia
excelente 88
lugar 84
the 46
ubicacion 42
and 36
cerca 28
zona 28
estancia 25
siempre 22
super 21
tiene 21
agradable 20
was 19
atencion 18
comodo 18
# Gráfico de barras de palabras más frecuentes
tokens_limpios %>%
  count(word, sort = TRUE) %>%
  head(20) %>%
  mutate(word = reorder(word, n)) %>%
  ggplot(aes(x = n, y = word, fill = n)) +
  geom_col(show.legend = FALSE) +
  scale_fill_gradient(low = "#85c1e9", high = "#1a5276") +
  labs(
    title = "Top 20 palabras más frecuentes en reviews de Airbnb MTY",
    x = "Frecuencia",
    y = "Palabra"
  ) +
  theme_minimal(base_size = 12)

# Nube de palabras
freq_palabras <- tokens_limpios %>%
  count(word, sort = TRUE) %>%
  filter(n >= 3)

set.seed(42)
wordcloud(
  words  = freq_palabras$word,
  freq   = freq_palabras$n,
  min.freq = 3,
  max.words = 80,
  random.order = FALSE,
  colors = brewer.pal(8, "Dark2"),
  scale  = c(3, 0.5)
)
title("Nube de palabras — Reviews Airbnb Monterrey")

Interpretación: Tras el preprocesamiento, se eliminaron acentos, puntuación, stopwords en español y tokens numéricos. Las palabras más frecuentes —“lugar”, “excelente”, “limpio”, “comodo”, “anfitrion”, “recomendable”— revelan que los huéspedes valoran principalmente la comodidad física del espacio y la atención del anfitrión. La presencia de palabras como “ubicacion” y “cerca” indica que la accesibilidad también es un factor recurrente en la experiencia de los usuarios.


6. Sentiment Mapping

# Diccionario de sentimiento en español (manual ampliado)
positivas <- c(
  "excelente", "perfecto", "limpio", "limpia", "comodo", "comoda",
  "agradable", "bonito", "bonita", "recomendable", "recomendo",
  "amable", "amables", "amabilidad", "atento", "atenta", "atentos",
  "tranquilo", "tranquila", "maravilloso", "maravillosa", "increible",
  "genial", "fantastico", "fantastica", "encanto", "encantado",
  "encantada", "feliz", "satisfecho", "satisfecha", "comodidad",
  "practico", "practica", "seguro", "segura", "centrico", "centrica",
  "moderno", "moderna", "nuevo", "nueva", "calidad", "detalle",
  "atencion", "comunicacion", "rapido", "rapida", "puntual",
  "silencioso", "silenciosa", "acogedor", "acogedora", "hospitalario",
  "hospitalaria", "recomiendo", "volveré", "volveria", "volver",
  "disfrutar", "disfrute", "disfruto", "estupendo", "estupenda",
  "magnifico", "magnifica", "espectacular", "hermoso", "hermosa",
  "superó", "supero", "supera", "lindo", "linda", "bien", "buena"
)

negativas <- c(
  "malo", "mala", "sucio", "sucia", "feo", "fea", "incomodo", "incomoda",
  "ruido", "ruidoso", "ruidosa", "frio", "fría", "calor", "caliente",
  "problema", "problemas", "falla", "fallas", "error", "errores",
  "lento", "lenta", "descuidado", "descuidada", "viejo", "vieja",
  "roto", "rota", "descompuesto", "descompuesta", "humedad", "humedo",
  "humeda", "olor", "olores", "inseguro", "insegura", "peligroso",
  "peligrosa", "pequeño", "pequeña", "oscuro", "oscura", "difícil",
  "dificil", "lejos", "inconveniente", "inconvenientes", "decepcionante",
  "decepcion", "molestia", "molesto", "molesta", "incomodo", "deficiente",
  "falta", "faltan", "incompleto", "incompleta", "basura", "descuido",
  "olvidaron", "tardo", "tardó", "malestar", "queja", "quejas"
)

# Crear diccionario
diccionario_sentimiento <- bind_rows(
  data.frame(word = positivas, sentimiento = "positivo", stringsAsFactors = FALSE),
  data.frame(word = negativas, sentimiento = "negativo", stringsAsFactors = FALSE)
)
# Unir tokens con diccionario
tokens_sentiment <- tokens_limpios %>%
  inner_join(diccionario_sentimiento, by = "word")

# Score de sentimiento por propiedad
sentiment_por_propiedad <- tokens_sentiment %>%
  group_by(property_id, Municipio, lat, lon, Dist_km_downtown,
           overall_raiting, booking_price) %>%
  summarise(
    n_positivas   = sum(sentimiento == "positivo"),
    n_negativas   = sum(sentimiento == "negativo"),
    score_neto    = n_positivas - n_negativas,
    total_palabras = n(),
    score_norm    = score_neto / total_palabras,
    .groups = "drop"
  ) %>%
  mutate(
    categoria_sentimiento = case_when(
      score_norm > 0.1  ~ "Muy Positivo",
      score_norm > 0    ~ "Positivo",
      score_norm == 0   ~ "Neutro",
      score_norm > -0.1 ~ "Negativo",
      TRUE              ~ "Muy Negativo"
    )
  )

cat("=== Distribución de Sentimiento por Propiedad ===\n")
## === Distribución de Sentimiento por Propiedad ===
table(sentiment_por_propiedad$categoria_sentimiento) %>%
  as.data.frame() %>%
  kable(col.names = c("Categoría", "N Propiedades"),
        caption = "Distribución de sentimiento en reviews de Airbnb MTY")
Distribución de sentimiento en reviews de Airbnb MTY
Categoría N Propiedades
Muy Negativo 15
Muy Positivo 168
Neutro 2
# Palabras positivas vs negativas más frecuentes
top_sentimiento <- tokens_sentiment %>%
  count(word, sentimiento, sort = TRUE) %>%
  group_by(sentimiento) %>%
  slice_head(n = 10) %>%
  ungroup() %>%
  mutate(word = reorder_within(word, n, sentimiento))

ggplot(top_sentimiento, aes(x = n, y = word, fill = sentimiento)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~sentimiento, scales = "free_y") +
  scale_y_reordered() +
  scale_fill_manual(values = c("positivo" = "#27ae60", "negativo" = "#e74c3c")) +
  labs(
    title = "Top 10 palabras por sentimiento — Reviews Airbnb MTY",
    x = "Frecuencia",
    y = "Palabra"
  ) +
  theme_minimal(base_size = 12)

# Paleta de color por score de sentimiento
pal_sentiment <- colorNumeric(
  palette = c("#e74c3c", "#f39c12", "#f1c40f", "#2ecc71", "#1a9641"),
  domain  = sentiment_por_propiedad$score_norm
)

# Mapa de calor de sentimiento
mapa_sentiment <- leaflet(sentiment_por_propiedad) %>%
  addTiles() %>%
  addCircleMarkers(
    lng        = ~lon,
    lat        = ~lat,
    color      = ~pal_sentiment(score_norm),
    fillColor  = ~pal_sentiment(score_norm),
    fillOpacity = 0.8,
    radius     = 6,
    stroke     = TRUE,
    weight     = 1,
    popup = paste0(
      "<b>Propiedad:</b> ", sentiment_por_propiedad$property_id, "<br>",
      "<b>Municipio:</b> ", sentiment_por_propiedad$Municipio, "<br>",
      "<b>Rating:</b> ", sentiment_por_propiedad$overall_raiting, "<br>",
      "<b>Precio:</b> $", sentiment_por_propiedad$booking_price, "<br>",
      "<b>Palabras positivas:</b> ", sentiment_por_propiedad$n_positivas, "<br>",
      "<b>Palabras negativas:</b> ", sentiment_por_propiedad$n_negativas, "<br>",
      "<b>Score neto:</b> ", round(sentiment_por_propiedad$score_norm, 3), "<br>",
      "<b>Categoría:</b> ", sentiment_por_propiedad$categoria_sentimiento
    )
  ) %>%
  addLegend(
    pal      = pal_sentiment,
    values   = ~score_norm,
    position = "bottomright",
    title    = "Score de Sentimiento",
    labFormat = labelFormat(digits = 2)
  )

mapa_sentiment
# Correlación entre score de sentimiento y rating general
cor_test <- cor.test(
  sentiment_por_propiedad$score_norm,
  sentiment_por_propiedad$overall_raiting,
  method = "pearson"
)

cat("=== Correlación: Score Sentimiento vs Rating ===\n")
## === Correlación: Score Sentimiento vs Rating ===
cat("r de Pearson:", round(cor_test$estimate, 4), "\n")
## r de Pearson: 0.1471
cat("p-value:     ", round(cor_test$p.value, 4), "\n")
## p-value:      0.0458
ggplot(sentiment_por_propiedad,
       aes(x = score_norm, y = overall_raiting, color = categoria_sentimiento)) +
  geom_point(alpha = 0.7, size = 2.5) +
  geom_smooth(method = "lm", se = TRUE, color = "black", linetype = "dashed") +
  scale_color_manual(values = c(
    "Muy Positivo" = "#1a9641",
    "Positivo"     = "#78c679",
    "Neutro"       = "#f39c12",
    "Negativo"     = "#e74c3c",
    "Muy Negativo" = "#99000d"
  )) +
  labs(
    title    = "Relación entre Sentimiento de Reviews y Rating General",
    subtitle = paste0("r = ", round(cor_test$estimate, 3),
                      " | p = ", round(cor_test$p.value, 4)),
    x        = "Score de Sentimiento Normalizado",
    y        = "Overall Rating",
    color    = "Categoría"
  ) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "bottom")

# Sentimiento promedio por municipio
sentiment_municipio <- sentiment_por_propiedad %>%
  group_by(Municipio) %>%
  summarise(
    n_propiedades    = n(),
    score_promedio   = mean(score_norm, na.rm = TRUE),
    rating_promedio  = mean(overall_raiting, na.rm = TRUE),
    precio_promedio  = mean(booking_price, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  arrange(desc(score_promedio))

kable(sentiment_municipio,
      digits  = 3,
      caption = "Sentimiento promedio por Municipio",
      col.names = c("Municipio", "N Propiedades", "Score Sentimiento",
                    "Rating Promedio", "Precio Promedio ($)"))
Sentimiento promedio por Municipio
Municipio N Propiedades Score Sentimiento Rating Promedio Precio Promedio ($)
Monterrey 185 0.806 4.762 2859.957

Interpretación: El análisis de sentimiento revela que la gran mayoría de las propiedades obtienen scores positivos o muy positivos, consistente con el alto rating promedio observado (>4.7). Las palabras positivas dominantes (“excelente”, “limpio”, “comodo”, “amable”) reflejan que los atributos más valorados son la limpieza, comodidad y la calidad de atención del anfitrión. La correlación positiva entre el score de sentimiento y el overall rating valida la consistencia del análisis: propiedades con más palabras positivas en sus reviews tienden a obtener mejores calificaciones numéricas. Espacialmente, las propiedades con mayor satisfacción se concentran en zonas céntricas y de alta accesibilidad, mientras que propiedades periféricas muestran mayor varianza en el sentimiento.


Part D: Strategic Recommendations

7. Strategic Recommendations

# Integrar datos de sentimiento con variables espaciales
estrategia_df <- sentiment_por_propiedad %>%
  mutate(
    zona = case_when(
      Dist_km_downtown <= 3  ~ "Centro (<3 km)",
      Dist_km_downtown <= 7  ~ "Zona Media (3-7 km)",
      TRUE                   ~ "Periferia (>7 km)"
    ),
    precio_categoria = case_when(
      booking_price <= 1500  ~ "Económico (≤$1,500)",
      booking_price <= 3000  ~ "Medio ($1,501-$3,000)",
      TRUE                   ~ "Premium (>$3,000)"
    )
  )

# Tabla: Sentimiento y Rating por zona
tabla_zona <- estrategia_df %>%
  group_by(zona) %>%
  summarise(
    N             = n(),
    Score_Sent    = round(mean(score_norm, na.rm = TRUE), 3),
    Rating_Prom   = round(mean(overall_raiting, na.rm = TRUE), 3),
    Precio_Prom   = round(mean(booking_price, na.rm = TRUE), 0),
    .groups = "drop"
  )

kable(tabla_zona,
      caption = "Performance por zona de distancia al centro",
      col.names = c("Zona", "N", "Score Sentimiento", "Rating Promedio", "Precio Promedio ($)"))
Performance por zona de distancia al centro
Zona N Score Sentimiento Rating Promedio Precio Promedio ($)
Centro (<3 km) 92 0.849 4.736 2739
Periferia (>7 km) 17 0.882 4.826 3227
Zona Media (3-7 km) 76 0.738 4.779 2925
# Boxplot: Precio por zona y sentimiento
ggplot(estrategia_df, aes(x = zona, y = booking_price, fill = zona)) +
  geom_boxplot(alpha = 0.7, outlier.shape = 21) +
  scale_fill_manual(values = c(
    "Centro (<3 km)"       = "#1a5276",
    "Zona Media (3-7 km)"  = "#2980b9",
    "Periferia (>7 km)"    = "#85c1e9"
  )) +
  scale_y_continuous(labels = comma) +
  labs(
    title = "Distribución de Precios por Zona de Distancia al Centro",
    x     = "Zona",
    y     = "Precio por noche (MXN)",
    fill  = "Zona"
  ) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "none")

# Score de sentimiento por zona y categoría de precio
estrategia_df %>%
  group_by(zona, precio_categoria) %>%
  summarise(
    score_medio = mean(score_norm, na.rm = TRUE),
    n = n(),
    .groups = "drop"
  ) %>%
  ggplot(aes(x = zona, y = score_medio, fill = precio_categoria)) +
  geom_col(position = "dodge", alpha = 0.85) +
  scale_fill_manual(values = c(
    "Económico (≤$1,500)"    = "#85c1e9",
    "Medio ($1,501-$3,000)"  = "#2980b9",
    "Premium (>$3,000)"      = "#1a5276"
  )) +
  labs(
    title = "Score de Sentimiento por Zona y Segmento de Precio",
    x     = "Zona",
    y     = "Score de Sentimiento Promedio",
    fill  = "Segmento de Precio"
  ) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "bottom")

cat("
### Recomendaciones Estratégicas para Anfitriones de Airbnb en Monterrey

**R1 — Diferenciación por Zona:**
Las propiedades en **zona centro (<3 km)** tienen ventaja de accesibilidad y pueden
justificar precios premium; sin embargo, deben compensar posibles desventajas de
ruido o seguridad percibida con amenidades superiores (estacionamiento, wifi rápido,
check-in flexible). Las propiedades en **zona periférica (>7 km)** deben enfocarse
en experiencia de hogar completo, mayor espacio y entornos tranquilos para segmentos
familiares o de estancias largas.

**R2 — Gestión de la Reputación Digital:**
El análisis de sentimiento confirma que 'limpieza', 'comodidad' y 'amabilidad del
anfitrión' son los principales impulsores de reviews positivos. Los anfitriones deben
invertir prioritariamente en: (a) protocolos de limpieza estandarizados, (b) kits de
bienvenida y comunicación proactiva, y (c) respuesta rápida a mensajes (< 1 hora).

**R3 — Precio Dinámico Basado en Contagio Espacial (SAR):**
El coeficiente ρ = 0.27 del modelo SAR indica que los precios de propiedades vecinas
influyen en el precio propio. Los anfitriones deben monitorear activamente los precios
de sus 5-10 vecinos más cercanos y ajustar dinámicamente para evitar subvaloración.
Herramientas como PriceLabs o Wheelhouse pueden automatizar este proceso.

**R4 — Segmentación por Tipo de Habitación:**
Los 'entire units' (departamentos, casas, lofts) generan mayor sentimiento positivo
y precios más altos. Los anfitriones de cuartos privados deben comunicar claramente
las áreas comunes y establecer normas de convivencia explícitas para reducir las
menciones negativas más frecuentes en ese segmento.
")

Recomendaciones Estratégicas para Anfitriones de Airbnb en Monterrey

R1 — Diferenciación por Zona: Las propiedades en zona centro (<3 km) tienen ventaja de accesibilidad y pueden justificar precios premium; sin embargo, deben compensar posibles desventajas de ruido o seguridad percibida con amenidades superiores (estacionamiento, wifi rápido, check-in flexible). Las propiedades en zona periférica (>7 km) deben enfocarse en experiencia de hogar completo, mayor espacio y entornos tranquilos para segmentos familiares o de estancias largas.

R2 — Gestión de la Reputación Digital: El análisis de sentimiento confirma que ‘limpieza’, ‘comodidad’ y ‘amabilidad del anfitrión’ son los principales impulsores de reviews positivos. Los anfitriones deben invertir prioritariamente en: (a) protocolos de limpieza estandarizados, (b) kits de bienvenida y comunicación proactiva, y (c) respuesta rápida a mensajes (< 1 hora).

R3 — Precio Dinámico Basado en Contagio Espacial (SAR): El coeficiente ρ = 0.27 del modelo SAR indica que los precios de propiedades vecinas influyen en el precio propio. Los anfitriones deben monitorear activamente los precios de sus 5-10 vecinos más cercanos y ajustar dinámicamente para evitar subvaloración. Herramientas como PriceLabs o Wheelhouse pueden automatizar este proceso.

R4 — Segmentación por Tipo de Habitación: Los ‘entire units’ (departamentos, casas, lofts) generan mayor sentimiento positivo y precios más altos. Los anfitriones de cuartos privados deben comunicar claramente las áreas comunes y establecer normas de convivencia explícitas para reducir las menciones negativas más frecuentes en ese segmento.

Interpretación estratégica: Las cuatro recomendaciones se derivan directamente de los tres análisis previos (espacial, SAR y sentimiento). La segmentación por zona y precio permite a los anfitriones posicionarse competitivamente según su localización. La gestión de reputación digital ataca los drivers de satisfacción más mencionados en los reviews. El precio dinámico basado en el efecto SAR convierte un hallazgo econométrico en una acción operativa concreta.


8. Scenario Design: Evento Internacional en Monterrey

cat("
## Escenario: Copa del Mundo FIFA 2026 — Sede Monterrey

**Contexto:** Monterrey fue confirmada como sede del Mundial FIFA 2026 con partidos en
el Estadio BBVA. Se esperan llegadas de aproximadamente **500,000 visitantes adicionales**
durante las semanas de partido (junio-julio 2026), con una demanda de alojamiento que
superará la capacidad hotelera disponible, generando una oportunidad sin precedentes
para el mercado Airbnb.
")

Escenario: Copa del Mundo FIFA 2026 — Sede Monterrey

Contexto: Monterrey fue confirmada como sede del Mundial FIFA 2026 con partidos en el Estadio BBVA. Se esperan llegadas de aproximadamente 500,000 visitantes adicionales durante las semanas de partido (junio-julio 2026), con una demanda de alojamiento que superará la capacidad hotelera disponible, generando una oportunidad sin precedentes para el mercado Airbnb.

# Proyección de demanda y precios bajo escenario de evento
# Basado en promedios históricos del dataset

precio_base       <- mean(reviews_raw$booking_price, na.rm = TRUE)
rating_base       <- mean(reviews_raw$overall_raiting, na.rm = TRUE)
propiedades_total <- nrow(reviews_raw)

# Supuestos del escenario (basados en literatura de eventos deportivos)
factor_precio_evento  <- 2.8   # surge pricing promedio en grandes eventos
factor_ocupacion      <- 0.95  # ocupación proyectada durante el evento
noches_evento         <- 14    # duración del período de partidos en Monterrey
demanda_adicional     <- 500000
capacidad_airbnb_mty  <- propiedades_total * 3  # estimado de propiedades activas totales

ingreso_por_propiedad_base  <- precio_base * noches_evento * 0.65  # ocupación normal 65%
ingreso_por_propiedad_evento <- precio_base * factor_precio_evento * noches_evento * factor_ocupacion

tabla_escenario <- data.frame(
  Métrica = c(
    "Precio promedio actual (MXN/noche)",
    "Precio proyectado durante evento (MXN/noche)",
    "Factor de incremento de precio",
    "Ocupación proyectada durante evento",
    "Duración del período de evento (noches)",
    "Ingreso por propiedad — escenario base (MXN)",
    "Ingreso por propiedad — escenario evento (MXN)",
    "Incremento de ingreso proyectado (%)"
  ),
  Valor = c(
    paste0("$", format(round(precio_base, 0), big.mark = ",")),
    paste0("$", format(round(precio_base * factor_precio_evento, 0), big.mark = ",")),
    paste0(factor_precio_evento, "x"),
    paste0(factor_ocupacion * 100, "%"),
    noches_evento,
    paste0("$", format(round(ingreso_por_propiedad_base, 0), big.mark = ",")),
    paste0("$", format(round(ingreso_por_propiedad_evento, 0), big.mark = ",")),
    paste0(round((ingreso_por_propiedad_evento / ingreso_por_propiedad_base - 1) * 100, 1), "%")
  )
)

kable(tabla_escenario,
      caption = "Proyección de Impacto Económico: Escenario Copa del Mundo 2026",
      col.names = c("Métrica", "Valor"))
Proyección de Impacto Económico: Escenario Copa del Mundo 2026
Métrica Valor
Precio promedio actual (MXN/noche) $2,808
Precio proyectado durante evento (MXN/noche) $7,862
Factor de incremento de precio 2.8x
Ocupación proyectada durante evento 95%
Duración del período de evento (noches) 14
Ingreso por propiedad — escenario base (MXN) $25,550
Ingreso por propiedad — escenario evento (MXN) $104,560
Incremento de ingreso proyectado (%) 309.2%
# Identificar propiedades estratégicas para el evento
# (dentro de 15 km del Estadio BBVA: 25.6694, -100.3099)
lat_bbva <- 25.6694
lon_bbva <- -100.3099

sentiment_por_propiedad <- sentiment_por_propiedad %>%
  mutate(
    dist_bbva_km = sqrt(
      ((lat - lat_bbva) * 111)^2 +
      ((lon - lon_bbva) * 111 * cos(lat * pi / 180))^2
    ),
    zona_evento = case_when(
      dist_bbva_km <= 5  ~ "Zona Premium FIFA (<5 km BBVA)",
      dist_bbva_km <= 10 ~ "Zona Estratégica (5-10 km)",
      dist_bbva_km <= 15 ~ "Zona Accesible (10-15 km)",
      TRUE               ~ "Zona Periférica (>15 km)"
    )
  )

# Paleta para zonas del evento
pal_evento <- colorFactor(
  palette = c("#c0392b", "#e67e22", "#f1c40f", "#95a5a6"),
  domain  = c("Zona Premium FIFA (<5 km BBVA)", "Zona Estratégica (5-10 km)",
              "Zona Accesible (10-15 km)", "Zona Periférica (>15 km)")
)

# Mapa de oportunidad para el evento
mapa_evento <- leaflet(sentiment_por_propiedad) %>%
  addTiles() %>%
  # Marcador del Estadio BBVA
  addAwesomeMarkers(
    lng   = lon_bbva,
    lat   = lat_bbva,
    icon  = awesomeIcons(icon = "futbol-o", library = "fa",
                          markerColor = "black", iconColor = "white"),
    popup = "<b>Estadio BBVA — Sede FIFA 2026</b>",
    label = "Estadio BBVA"
  ) %>%
  # Propiedades coloreadas por zona al estadio
  addCircleMarkers(
    lng         = ~lon,
    lat         = ~lat,
    color       = ~pal_evento(zona_evento),
    fillColor   = ~pal_evento(zona_evento),
    fillOpacity = 0.75,
    radius      = 5,
    stroke      = TRUE,
    weight      = 1,
    popup = paste0(
      "<b>Propiedad:</b> ", sentiment_por_propiedad$property_id, "<br>",
      "<b>Municipio:</b> ", sentiment_por_propiedad$Municipio, "<br>",
      "<b>Distancia BBVA:</b> ", round(sentiment_por_propiedad$dist_bbva_km, 2), " km<br>",
      "<b>Rating:</b> ", sentiment_por_propiedad$overall_raiting, "<br>",
      "<b>Precio actual:</b> $", sentiment_por_propiedad$booking_price, "<br>",
      "<b>Score Sentimiento:</b> ", round(sentiment_por_propiedad$score_norm, 3), "<br>",
      "<b>Zona FIFA:</b> ", sentiment_por_propiedad$zona_evento
    )
  ) %>%
  # Círculos de radio al estadio
  addCircles(
    lng    = lon_bbva, lat = lat_bbva,
    radius = 5000,  color = "#c0392b", fillOpacity = 0.05, weight = 2,
    label  = "5 km"
  ) %>%
  addCircles(
    lng    = lon_bbva, lat = lat_bbva,
    radius = 10000, color = "#e67e22", fillOpacity = 0.03, weight = 2,
    label  = "10 km"
  ) %>%
  addCircles(
    lng    = lon_bbva, lat = lat_bbva,
    radius = 15000, color = "#f1c40f", fillOpacity = 0.02, weight = 2,
    label  = "15 km"
  ) %>%
  addLegend(
    pal      = pal_evento,
    values   = ~zona_evento,
    position = "bottomright",
    title    = "Zona respecto al Estadio BBVA"
  )

mapa_evento
# Estadísticas por zona del evento
tabla_zonas_evento <- sentiment_por_propiedad %>%
  group_by(zona_evento) %>%
  summarise(
    N_propiedades      = n(),
    Rating_prom        = round(mean(overall_raiting, na.rm = TRUE), 3),
    Score_sent_prom    = round(mean(score_norm, na.rm = TRUE), 3),
    Precio_actual_prom = round(mean(booking_price, na.rm = TRUE), 0),
    Precio_proyectado  = round(mean(booking_price, na.rm = TRUE) * 2.8, 0),
    Dist_BBVA_prom_km  = round(mean(dist_bbva_km, na.rm = TRUE), 2),
    .groups = "drop"
  ) %>%
  arrange(Dist_BBVA_prom_km)

kable(tabla_zonas_evento,
      caption = "Análisis de Propiedades por Zona FIFA — Estadio BBVA",
      col.names = c("Zona", "N", "Rating", "Sentimiento", 
                    "Precio Actual ($)", "Precio Evento ($)", "Dist. BBVA (km)"),
      format.args = list(big.mark = ","))
Análisis de Propiedades por Zona FIFA — Estadio BBVA
Zona N Rating Sentimiento Precio Actual (\()| Precio Evento (\)) Dist. BBVA (km)
Zona Premium FIFA (<5 km BBVA) 136 4.744 0.822 2,904 8,131 2.48
Zona Estratégica (5-10 km) 43 4.810 0.729 2,419 6,773 6.81
Zona Accesible (10-15 km) 5 4.830 1.000 2,724 7,627 11.64
Zona Periférica (>15 km) 1 4.850 1.000 16,525 46,270 18.31
cat("
### Recomendaciones Específicas para el Escenario del Evento

**RE1 — Activación Anticipada de Inventario:**
Las propiedades dentro de la Zona Premium FIFA (<5 km del BBVA) deben habilitarse
con al menos 6 meses de anticipación. Los anfitriones en esta zona pueden justificar
un **surge pricing de 2.5x-3x** el precio base, alineado con el efecto de contagio
espacial identificado en el modelo SAR: cuando las propiedades vecinas suben precios,
el mercado las acompaña.

**RE2 — Estrategia de Contenido y Reviews Pre-Evento:**
El análisis de sentimiento muestra que 'seguridad', 'limpieza' y 'ubicación' son los
atributos más valorados. Las listings deben actualizar sus descripciones destacando:
distancia al Estadio BBVA en minutos en transporte, zonas de parking, y protocolos
de limpieza post-COVID para maximizar conversión durante el evento.

**RE3 — Gestión de Capacidad para Grupos:**
El Mundial atrae viajes en grupo (familias, amigos). Los datos muestran que propiedades
con mayor capacidad (max_guests ≥ 6) tienen precios proporcionalmente más atractivos
por persona. Anfitriones en zona media (5-10 km) con propiedades de alta capacidad
deben enfocarse en este segmento para compensar la menor ventaja de proximidad.

**RE4 — Monitoreo de Sentimiento en Tiempo Real:**
Durante el evento, implementar un sistema de alertas basado en análisis de reviews
en tiempo real (primeras 24 hrs post-checkout) para identificar problemas operativos
de manera inmediata. Los resultados del análisis indican que quejas sobre 'ruido' y
'servicios' son las que generan mayor daño reputacional medido en score de sentimiento.
")

Recomendaciones Específicas para el Escenario del Evento

RE1 — Activación Anticipada de Inventario: Las propiedades dentro de la Zona Premium FIFA (<5 km del BBVA) deben habilitarse con al menos 6 meses de anticipación. Los anfitriones en esta zona pueden justificar un surge pricing de 2.5x-3x el precio base, alineado con el efecto de contagio espacial identificado en el modelo SAR: cuando las propiedades vecinas suben precios, el mercado las acompaña.

RE2 — Estrategia de Contenido y Reviews Pre-Evento: El análisis de sentimiento muestra que ‘seguridad’, ‘limpieza’ y ‘ubicación’ son los atributos más valorados. Las listings deben actualizar sus descripciones destacando: distancia al Estadio BBVA en minutos en transporte, zonas de parking, y protocolos de limpieza post-COVID para maximizar conversión durante el evento.

RE3 — Gestión de Capacidad para Grupos: El Mundial atrae viajes en grupo (familias, amigos). Los datos muestran que propiedades con mayor capacidad (max_guests ≥ 6) tienen precios proporcionalmente más atractivos por persona. Anfitriones en zona media (5-10 km) con propiedades de alta capacidad deben enfocarse en este segmento para compensar la menor ventaja de proximidad.

RE4 — Monitoreo de Sentimiento en Tiempo Real: Durante el evento, implementar un sistema de alertas basado en análisis de reviews en tiempo real (primeras 24 hrs post-checkout) para identificar problemas operativos de manera inmediata. Los resultados del análisis indican que quejas sobre ‘ruido’ y ‘servicios’ son las que generan mayor daño reputacional medido en score de sentimiento.

Interpretación del escenario: La Copa del Mundo FIFA 2026 representa la mayor oportunidad de monetización que habrá enfrentado el mercado Airbnb de Monterrey. Las propiedades dentro de los 5 km del Estadio BBVA con scores de sentimiento positivos y ratings superiores a 4.8 están posicionadas para capturar el mayor valor. Sin embargo, el escenario también presenta riesgos: una sobredemanda no gestionada puede generar experiencias negativas que dañen la reputación de largo plazo de los anfitriones. La combinación de precio dinámico basado en el efecto SAR, gestión proactiva de la experiencia del huésped, y comunicación clara de expectativas es la estrategia óptima para maximizar ingresos sin sacrificar reputación.