1.Presentación del tema

Identificación de núcleos gastronómicos en el distrito de Miraflores de la ciudad de Lima mediante análisis de densidad y clustering espacial con datos de OpenStreetMap.


2.Objetivo general

Analizar la distribución espacial de restaurantes en Miraflores para identificar zonas de alta concentración (hotspots) utilizando datos de OpenStreetMap y técnicas de visualización y análisis espacial.


3. Análisis


Activar las librerías

#install.packages("tidyverse")
library(tidyverse)
#install.packages("sf")
library(sf)
#install.packages("ggmap")
library(ggmap)
#install.packages("osmdata")
library(osmdata)


¿Dónde está el distrito de Miraflores?

Miraflores se encuentra situado en el corazón de Lima moderna, este distrito limita al norte con el centro financiero del distrito de San Isidro y al sur con el bohemio distrito de Barranco. Su frontera más espectacular es la del oeste, con los acantilados verdes que dan paso a las playas del Océano Pacífico.

Creación del cuadro delimitador del distrito

#Activación de API key
ggmap::register_stadiamaps(key = "0bf2d96e-58f7-444b-911a-3d02b8d0b013")
bbox_miraflores <- getbb("Miraflores, Lima, Peru")
#Revisión de los resultados obtenidos
bbox_miraflores
##         min       max
## x -77.05602 -77.00129
## y -12.14015 -12.10285
#Revisión de los resultados obtenidos
mapa_miraflores <- get_stadiamap(bbox=bbox_miraflores,
maptype="alidade_smooth",
zoom=14)
#Ver resultados generados
ggmap(mapa_miraflores)

#Cargar la extensión del territorio de Miraflores
polygon_miraflores <- getbb("Miraflores, Lima, Peru",
format_out = "sf_polygon")

polygon_miraflores <- polygon_miraflores[1, ]
#Ver resultado del polígono
polygon_miraflores
#Ver resultado del polígono
ggplot()+
geom_sf(data=polygon_miraflores)

#Ver polígono en OSM
ggmap(mapa_miraflores)+
geom_sf(data=polygon_miraflores, inherit.aes = FALSE)

Ajustes para mejorar la visualización: - Quitemos el relleno del polígono y demos algún color al borde. - Pongamos título, subtítulo y fuente. - Ajustemos el theme .

ggmap(mapa_miraflores)+
geom_sf(data=polygon_miraflores, fill=NA, color="black", lwd=0.75, inherit.aes = FALSE)+
labs(title="Miraflores, Lima, Perú",
caption="Fuente: OpenStreetMap")+
theme_void()


### Analizar la información existente

available_features()
##   [1] "4wd_only"                    "abandoned"                  
##   [3] "abutters"                    "access"                     
##   [5] "addr"                        "addr:*"                     
##   [7] "addr:city"                   "addr:conscriptionnumber"    
##   [9] "addr:country"                "addr:county"                
##  [11] "addr:district"               "addr:flats"                 
##  [13] "addr:full"                   "addr:hamlet"                
##  [15] "addr:housename"              "addr:housenumber"           
##  [17] "addr:inclusion"              "addr:interpolation"         
##  [19] "addr:place"                  "addr:postbox"               
##  [21] "addr:postcode"               "addr:province"              
##  [23] "addr:state"                  "addr:street"                
##  [25] "addr:subdistrict"            "addr:suburb"                
##  [27] "addr:unit"                   "admin_level"                
##  [29] "aeroway"                     "agricultural"               
##  [31] "alcohol"                     "alt_name"                   
##  [33] "amenity"                     "area"                       
##  [35] "atv"                         "backward"                   
##  [37] "barrier"                     "basin"                      
##  [39] "bdouble"                     "bicycle"                    
##  [41] "bicycle_road"                "biergarten"                 
##  [43] "boat"                        "border_type"                
##  [45] "boundary"                    "brand"                      
##  [47] "bridge"                      "bridge:name"                
##  [49] "building"                    "building:architecture"      
##  [51] "building:colour"             "building:fireproof"         
##  [53] "building:flats"              "building:levels"            
##  [55] "building:material"           "building:min_level"         
##  [57] "building:part"               "building:prefabricated"     
##  [59] "building:soft_storey"        "bus"                        
##  [61] "bus:lanes"                   "bus_bay"                    
##  [63] "busway"                      "capacity"                   
##  [65] "carriage"                    "castle_type"                
##  [67] "change"                      "charge"                     
##  [69] "clothes"                     "construction"               
##  [71] "construction#Railways"       "construction_date"          
##  [73] "covered"                     "craft"                      
##  [75] "crossing"                    "crossing:island"            
##  [77] "cuisine"                     "cutting"                    
##  [79] "cycle_rickshaw"              "cycleway"                   
##  [81] "cycleway:left"               "cycleway:left:oneway"       
##  [83] "cycleway:right"              "cycleway:right:oneway"      
##  [85] "denomination"                "destination"                
##  [87] "diet:*"                      "direction"                  
##  [89] "dispensing"                  "disused"                    
##  [91] "dog"                         "drinking_water"             
##  [93] "drinking_water:legal"        "drive_in"                   
##  [95] "drive_through"               "ele"                        
##  [97] "electric_bicycle"            "electrified"                
##  [99] "embankment"                  "embedded_rails"             
## [101] "emergency"                   "end_date"                   
## [103] "energy_class"                "entrance"                   
## [105] "est_width"                   "fee"                        
## [107] "female"                      "fire_hydrant"               
## [109] "fire_object:type"            "fire_operator"              
## [111] "fire_rank"                   "food"                       
## [113] "foot"                        "footway"                    
## [115] "ford"                        "forestry"                   
## [117] "forward"                     "frequency"                  
## [119] "frontage_road"               "fuel"                       
## [121] "full_name"                   "gauge"                      
## [123] "gender_segregated"           "golf_cart"                  
## [125] "goods"                       "gutter"                     
## [127] "hand_cart"                   "hazard"                     
## [129] "hazmat"                      "healthcare"                 
## [131] "healthcare:counselling"      "healthcare:speciality"      
## [133] "height"                      "hgv"                        
## [135] "highway"                     "historic"                   
## [137] "horse"                       "hot_water"                  
## [139] "hov"                         "ice_road"                   
## [141] "incline"                     "industrial"                 
## [143] "inline_skates"               "inscription"                
## [145] "int_name"                    "internet_access"            
## [147] "junction"                    "kerb"                       
## [149] "landuse"                     "lane_markings"              
## [151] "lanes"                       "lanes:bus"                  
## [153] "lanes:psv"                   "layer"                      
## [155] "leaf_cycle"                  "leaf_type"                  
## [157] "leisure"                     "lhv"                        
## [159] "lit"                         "loc_name"                   
## [161] "location"                    "male"                       
## [163] "man_made"                    "max_age"                    
## [165] "max_level"                   "maxaxleload"                
## [167] "maxheight"                   "maxlength"                  
## [169] "maxspeed"                    "maxstay"                    
## [171] "maxweight"                   "maxwidth"                   
## [173] "military"                    "min_age"                    
## [175] "min_level"                   "minspeed"                   
## [177] "mofa"                        "moped"                      
## [179] "motor_vehicle"               "motorboat"                  
## [181] "motorcar"                    "motorcycle"                 
## [183] "motorroad"                   "mountain_pass"              
## [185] "mtb:description"             "mtb:scale"                  
## [187] "name"                        "name:left"                  
## [189] "name:right"                  "name_1"                     
## [191] "name_2"                      "narrow"                     
## [193] "nat_name"                    "natural"                    
## [195] "nickname"                    "noexit"                     
## [197] "non_existent_levels"         "nudism"                     
## [199] "office"                      "official_name"              
## [201] "old_name"                    "oneway"                     
## [203] "oneway:bicycle"              "oneway:bus"                 
## [205] "openfire"                    "opening_hours"              
## [207] "opening_hours:drive_through" "operator"                   
## [209] "orientation"                 "oven"                       
## [211] "overtaking"                  "parking"                    
## [213] "parking:condition"           "parking:lane"               
## [215] "passenger_lines"             "passing_places"             
## [217] "place"                       "power"                      
## [219] "power_supply"                "priority"                   
## [221] "priority_road"               "produce"                    
## [223] "proposed"                    "proposed:name"              
## [225] "protected_area"              "psv"                        
## [227] "psv:lanes"                   "public_transport"           
## [229] "railway"                     "railway:preserved"          
## [231] "railway:track_ref"           "recycling_type"             
## [233] "ref"                         "ref_name"                   
## [235] "reg_name"                    "religion"                   
## [237] "religious_level"             "rental"                     
## [239] "residential"                 "roadtrain"                  
## [241] "route"                       "sac_scale"                  
## [243] "sauna"                       "service"                    
## [245] "service_times"               "shelter_type"               
## [247] "shop"                        "short_name"                 
## [249] "shoulder"                    "shower"                     
## [251] "side_road"                   "sidewalk"                   
## [253] "site"                        "ski"                        
## [255] "smoking"                     "smoothness"                 
## [257] "social_facility"             "sorting_name"               
## [259] "speed_pedelec"               "sport"                      
## [261] "start_date"                  "step_count"                 
## [263] "substation"                  "surface"                    
## [265] "tactile_paving"              "tank"                       
## [267] "taxi"                        "tidal"                      
## [269] "toilets"                     "toilets:wheelchair"         
## [271] "toll"                        "topless"                    
## [273] "tourism"                     "tourist_bus"                
## [275] "tower:type"                  "tracks"                     
## [277] "tracktype"                   "traffic_calming"            
## [279] "traffic_sign"                "trail_visibility"           
## [281] "trailblazed"                 "trailblazed:visibility"     
## [283] "trailer"                     "tunnel"                     
## [285] "tunnel:name"                 "turn"                       
## [287] "type"                        "unisex"                     
## [289] "usage"                       "vehicle"                    
## [291] "vending"                     "voltage"                    
## [293] "water"                       "wheelchair"                 
## [295] "wholesale"                   "width"                      
## [297] "winter_road"                 "wood"
# Configurar un servidor alternativo
set_overpass_url("https://overpass.kumi.systems/api/interpreter")

# Consultar restaurantes en Miraflores
restaurantes_query <- opq(bbox = bbox_miraflores) %>%
  add_osm_feature(key = "amenity", value = "restaurant") %>%
  osmdata_sf()

# Extraer solo los puntos (restaurantes como puntos)
restaurantes <- restaurantes_query$osm_points

# Ver cuántos restaurantes encontramos
nrow(restaurantes)
## [1] 1036
# Ver las primeras filas
head(restaurantes)
# Consultar restaurantes en Miraflores
gastronomia_query <- opq(bbox = bbox_miraflores) %>%
  add_osm_feature(key = "amenity", 
                  value = c("restaurant", "bar", "cafe", "pub")) %>%
  osmdata_sf()

# Extraer los puntos (restaurantes)
gastronomia <- gastronomia_query$osm_points

# Ver cuántos encontramos
nrow(gastronomia)
## [1] 1338

En base a la información del open street map se puede observar que se identifican 1036 restaurantes.A continuación, los visualizamos en el mapa para explorar su distribución en el territorio.

ggmap(mapa_miraflores)+
  geom_sf(data=polygon_miraflores, fill=NA, color="blue", lwd=0.75, inherit.aes = FALSE)+ geom_sf(data = gastronomia, inherit.aes = FALSE)+
labs(title="Locales gastronómicos",
subtitle="Miraflores, Lima, Perú",
caption="Fuente: OpenStreetMap")+
theme_void()+
theme(title=element_text(size=10, face = "bold"), #tamaño de titulo del mapa
plot.caption=element_text(face = "italic", colour = "gray35",size=7)) #tamaño de nota al pie

ggmap(mapa_miraflores)+
geom_sf(data=polygon_miraflores, fill=NA, color="black", lwd=0.75, inherit.aes = FALSE)+
geom_sf(data = gastronomia, aes(color=amenity), inherit.aes = FALSE)+
labs(title="Locales gastronómicos",
subtitle="Miraflores, Lima, Perú",
caption="Fuente: OpenStreetMap",
color="")+
scale_color_manual(values=c("deeppink", "tomato", "skyblue", "yellow2"))+
theme_void()+
theme(title=element_text(size=10, face = "bold"), #tamaño de titulo del mapa
plot.caption=element_text(face = "italic", colour = "gray35",size=7)) #tamaño de nota al pie

Se realiza el cálculo para identificar la cantidad de NA que existen y que no se están visibilizando en la categorías mostradas previamente,

gastronomia %>%
group_by(amenity) %>%
summarise(cantidad=n())

Como se puede ver en los resultados, existen 721 NA que superan en cantidad a los restaurantes.Sin embargo, para fines del presente trabajo, se optará por eliminar los NA.

gastronomia <- gastronomia %>%
  filter(!is.na(amenity))

# Ver resumen
gastronomia %>%
  group_by(amenity) %>%
  summarise(cantidad = n())

Creación de mapa interactivo

#install.packages("leaflet")
library(leaflet)
leaflet(gastronomia) %>%
  addTiles() %>%
  addCircleMarkers()
#Creación de paletas de colores según categoría gastronómica
factpal <- colorFactor(palette = c("pink","tomato","skyblue","yellow2"), 
                       levels = gastronomia$amenity)
leaflet(gastronomia) %>%
  addTiles() %>%
  addCircleMarkers(popup = paste("Tipo:", gastronomia$amenity, "<br>",
"Nombre:", gastronomia$name),
color = ~factpal(amenity))%>%
  addLegend("bottomright", pal = factpal, values = ~amenity,
title = "Tipo",
opacity = 1) %>%
addMiniMap()

Creación de mapa de calor

#install.packages("leaflet.extras")
library(leaflet.extras)
leaflet(gastronomia) %>%
  addTiles() %>%
  addHeatmap(radius = 15,
             max = 0.05) %>%
  addMiniMap()
library(dbscan)
gastronomia <- gastronomia %>%
  st_transform(32718)
modelo <- dbscan(gastronomia %>% st_coordinates(.), eps = 250, minPts = 8)

modelo
## DBSCAN clustering for 617 objects.
## Parameters: eps = 250, minPts = 8
## Using euclidean distances and borderpoints = TRUE
## The clustering contains 6 cluster(s) and 96 noise points.
## 
##   0   1   2   3   4   5   6 
##  96 441  23  22  11  11  13 
## 
## Available fields: cluster, eps, minPts, metric, borderPoints
gastronomia<- gastronomia %>%
  mutate(cluster=modelo$cluster)
gastronomia <- gastronomia %>%
  st_transform(4326)

Identificar clusters

ggmap(mapa_miraflores)+
  geom_sf(data=gastronomia %>%
            filter(cluster!=0), aes(color=as.factor(cluster)), inherit.aes = FALSE)+
  theme_void()

### Identificar polígonos clusters

ggmap(mapa_miraflores)+
geom_sf(data=gastronomia %>%
filter(cluster!=0), aes(color=as.factor(cluster)), inherit.aes = FALSE)+
theme_void()

gastronomia_clusters <- gastronomia %>%
  filter(cluster != 0) %>%
  group_by(cluster) %>%
  summarise(geometry = st_union(geometry)) %>%
  st_convex_hull()

ggmap(mapa_miraflores)+
  geom_sf(data=gastronomia %>%
            filter(cluster!=0), aes(color=as.factor(cluster)), inherit.aes = FALSE)+
  geom_sf(data=gastronomia_clusters, aes(fill=as.factor(cluster)), color=NA, 
          alpha=0.5, inherit.aes = FALSE)+
  labs(title="Clústers de establecimientos gastronómicos",
       subtitle="Miraflores, Lima, Perú",
       color="",
       fill="")+
  theme_void()

leaflet(gastronomia) %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  addPolygons(data=gastronomia_clusters,
              color = ~case_when(
                cluster == 0 ~ "gray",
                cluster == 1 ~ "#005f73",
                cluster == 2 ~ "#0a9396",
                cluster == 3 ~ "#06d6a0",
                cluster == 4 ~ "#ee9b00",
                cluster == 5 ~ "#ca6702",
                cluster == 6 ~ "#ae2012")) %>%
  addCircleMarkers(
    popup = paste("Tipo:", gastronomia$amenity, "<br>",
                  "Nombre:", gastronomia$name, "<br>",
                  "Clúster:", gastronomia$cluster),
    radius=4,
    color = ~case_when(
      cluster == 0 ~ "gray",
      cluster == 1 ~ "#005f73",
      cluster == 2 ~ "#0a9396",
      cluster == 3 ~ "#06d6a0",
      cluster == 4 ~ "#ee9b00",
      cluster == 5 ~ "#ca6702",
      cluster == 6 ~ "#ae2012")) %>%
  addMiniMap(tiles = providers$CartoDB.Positron)

Crear densidad de establecimientos gastronómicos

# Extraer coordenadas lon/lat
gastronomia_coords <- gastronomia %>%
  st_transform(4326) %>%  # en WGS84
  mutate(
    lon = st_coordinates(.)[,1],
    lat = st_coordinates(.)[,2]
  )
ggplot() +
  stat_density2d(
    data = gastronomia_coords, 
    aes(x = lon, y = lat, fill = after_stat(level)), 
    geom = "polygon", 
    alpha = 0.75, 
  ) +
  labs(
    title = "Densidad de establecimientos gastronómicos",
    subtitle = "Miraflores, Lima, Perú",
    fill = "Densidad"
  ) +
  scale_fill_distiller(palette = "Spectral") +
  theme_void()

#Agregar polígono de Miraflores encima
ggplot() +
  stat_density2d(
    data = gastronomia_coords, 
    aes(x = lon, y = lat, fill = after_stat(level)), 
    geom = "polygon", 
    alpha = 0.75
  ) +
  geom_sf(data = polygon_miraflores, fill = NA, color = "black", lwd = 1) +
  labs(
    title = "Densidad de establecimientos gastronómicos",
    subtitle = "Miraflores, Lima, Perú",
    fill = "Densidad"
  ) +
  scale_fill_distiller(palette = "Spectral") +
  theme_void()

### Densidad de establecimientos gastronómicos con maps

#Incluir mapa
ggmap(mapa_miraflores) +
  stat_density2d(
    data = gastronomia_coords, 
    aes(x = lon, y = lat, fill = after_stat(level)), 
    geom = "polygon", 
    alpha = 0.5
  ) +
  geom_sf(data = polygon_miraflores, fill = NA, color = "black", lwd = 0.5, inherit.aes = FALSE) +
  labs(
    title = "Densidad de establecimientos gastronómicos",
    subtitle = "Miraflores, Lima, Perú",
    fill = "Densidad"
  ) +
  scale_fill_distiller(palette = "Spectral") +
  theme_void()

### Densidad de establecimientos gastronómicos

ggmap(mapa_miraflores) +
  stat_density2d(
    data = gastronomia_coords, 
    aes(x = lon, y = lat, fill = after_stat(level)), 
    geom = "polygon", 
    alpha = 0.5
  ) +
  geom_sf(data = polygon_miraflores, fill = NA, color = "black", lwd = 0.5, inherit.aes = FALSE) +
  labs(
    title = "Densidad de establecimientos gastronómicos",
    subtitle = "Miraflores, Lima, Perú",
    fill = "Densidad",
    x = "Longitud", 
    y = "Latitud"   
  ) +
  scale_fill_distiller(palette = "Spectral") +
  theme_void() +
  theme(
    axis.text = element_text(size = 8),  # Mostrar coordenadas
    axis.title.x = element_text(size = 10, face = "bold"),
    axis.title.y = element_text(size = 10, face = "bold", angle = 90),
    axis.title = element_text(size = 10, face = "bold")  # Títulos de ejes
  )

### Cargar los tipos de vías de OSM

# Descargar vías principales de Miraflores
vias_principales <- opq(bbox = bbox_miraflores) %>%
  add_osm_feature(key = "highway", 
                  value = c("primary", "secondary", "trunk", "motorway")) %>%
  osmdata_sf()

# Extraer líneas
vias <- vias_principales$osm_lines

# Intersección con Miraflores
vias <- st_intersection(vias, polygon_miraflores)

# Ver cuántas vías
nrow(vias)
## [1] 563

Mapa de densidad de establecimientos gastronómicos con tipos de vías

ggmap(mapa_miraflores) +
  stat_density2d(
    data = gastronomia_coords, 
    aes(x = lon, y = lat, fill = after_stat(level)), 
    geom = "polygon", 
    alpha = 0.5
  ) +
  geom_sf(data = vias, color = "red", lwd = 0.1, alpha = 0.7, inherit.aes = FALSE) +  # ✅ Vías
  geom_sf(data = polygon_miraflores, fill = NA, color = "black", lwd = 0.5, inherit.aes = FALSE) +
  labs(
    title = "Densidad gastronómica y vías principales",
    subtitle = "Miraflores, Lima, Perú",
    fill = "Densidad",
    x = "Longitud",
    y = "Latitud",
    caption = "Fuente: OpenStreetMap"
  ) +
  scale_fill_distiller(palette = "Spectral") +
  theme_void() +
  theme(
    axis.text = element_text(size = 8),
    axis.title = element_text(size = 10, face = "bold"),
    plot.caption = element_text(face = "italic", colour = "gray35", size = 7)
  )

### Mapa de densidad de establecimientos gastronómicos con tipos de vías

ggmap(mapa_miraflores) +
  stat_density2d(
    data = gastronomia_coords, 
    aes(x = lon, y = lat, fill = after_stat(level)), 
    geom = "polygon", 
    alpha = 0.5
  ) +
  geom_sf(data = vias, aes(color = highway), lwd = 0.2, inherit.aes = FALSE) +  # Color por tipo
  geom_sf(data = polygon_miraflores, fill = NA, color = "black", lwd = 0.5, inherit.aes = FALSE) +
  labs(
    title = "Densidad gastronómica y vías principales",
    subtitle = "Miraflores, Lima, Perú (hora punta típica)",
    fill = "Densidad",
    color = "Tipo de vía",
    x = "Longitud",
    y = "Latitud",
    caption = "Fuente: OpenStreetMap"
  ) +
  scale_fill_distiller(palette = "Spectral") +
  scale_color_manual(values = c(
    "primary" = "red",
    "secondary" = "orange",
    "trunk" = "darkred",
    "motorway" = "purple"
  ),
    guide = guide_legend(
      override.aes = list(
        linetype = 1,      
        linewidth = 0.2,     
        shape = NA,        
        fill = NA          
      )
    )
  ) +
  theme_void() +
  theme(
    axis.text = element_text(size = 8),
    axis.title.x = element_text(size = 10, face = "bold"),
    axis.title.y = element_text(size = 10, face = "bold", angle = 90),
    plot.caption = element_text(face = "italic", colour = "gray35", size = 7)
  )