Obteniendo rutas, tiempo y distancia de viajes con Open Source Routing Machine

Agradecemos a Angie Scetta, arquitecta (UBA) y maestranda en Economía Urbana (UTDT), que nos muestra cómo usar OSRM: el paquete de R que permite estimar trayectos, tiempos y distancias de viaje a través de la grilla de calles de cualquier parte del mundo.

Angie se especializa en análisis de datos urbanos. Trabaja en el Gobierno de la Ciudad de Buenos Aires analizando el impacto de proyectos de diversas áreas de gobierno, y como profesora asistente en la materia Ciencia de Datos para Ciudades en la Universidad Torcuato Di Tella

Analizando recorridos y distancias en la Ciudad

A continuación vamos a ver un ejemplo de aplicación del servicio de ruteo de “OSRM” (http://project-osrm.org/), un paquete de uso libre, basado en datos de OpenStreetMap, que es muy útil a la hora de calcular distancias (km) y tiempos de viaje (minutos) entre 2 o más puntos georreferenciados.

El paquete se compone de 4 funciones que son:

  • osrmTable(): matriz de tiempos de viaje entre puntos.
  • osrmRoute(): camino más corto entre 2 puntos.
  • osrmTrip(): viaje entre múltiples puntos.
  • osrmIsochrone(): polígonos de isocronas.

En este caso vamos a calcular el camino más corto entre 2 puntos con la función osrmRoute().

Para comenzar, vamos a cargar las librerías requeridas: “tidyverse”, “sf”, “leaflet” y obviamente, “osrm”.

library(tidyverse)
library(sf)
library(leaflet)
library(osrm)

Lo siguiente que debemos hacer es elegir cuáles serán nuestros puntos de origen y de destino. Para esto vamos a aprovechar los dataset disponibles en el Portal de Datos Abiertos de la Ciudad http://https://data.buenosaires.gob.ar/ utilizando la ubicación de hospitales públicos (formato CSV) y los polígonos de los barrios de la Ciudad (formato geoJSON). Como ambos archivos tienen información geográfica vamos a poder ubicarlos en el espacio y calcular las distancias entre ellos.

barrios <- st_read("http://cdn.buenosaires.gob.ar/datosabiertos/datasets/barrios/barrios.geojson")
hospitales <- read.csv("http://cdn.buenosaires.gob.ar/datosabiertos/datasets/hospitales/hospitales.csv",
                       encoding="UTF-8") %>%
                       mutate(nombre=as.character(nombre))

¿Cómo podemos calcular la distancia y tiempo de viaje que hay entre los barrios de la Ciudad y los hospitales públicos? Para simplificar la tarea, tomemos 2 barrios como ejemplo: uno ubicado al Norte como Palermo y otro al Sur como Villa Lugano y calculemos la distancia y tiempo de viaje (en auto) que existe entre estos y cada hospital. Entonces, nuestros puntos de origen serán los centroides de ambos barrios y los de destino la ubicación exacta de los 36 hospitales. Cabe destacar que, el Centro Asistencial Cecilia Grierson, incluido como un hospital en el dataset descargado, no brinda los mismos servicios (de complejidad) que el resto.

Los centroides podemos calcularlos fácilmente con la función st_centroid() del paquete sf y la geometría podemos transformarla en coordenadas X e Y a partir de st_coordinates(). Esta última operación es necesaria para el posterior ruteo.

barrios_centroides <- barrios %>%
  st_centroid() %>%
  filter(barrio=="PALERMO" | barrio=="VILLA LUGANO") %>%
  mutate(ubicacion=if_else(barrio=="PALERMO", "NORTE", "SUR"))

barrios_centroides <- cbind(barrios_centroides, st_coordinates(barrios_centroides)) %>%
                      st_set_geometry(NULL) %>% 
                      rename(LON_ORIGEN=X,
                             LAT_ORIGEN=Y)

Mapa 1.

hospitales %>% 
  group_by(barrio) %>% 
  summarise(cantidad=n()) %>%
  mutate(barrio=toupper(barrio)) %>% 
  left_join(barrios, by="barrio") %>%
  ungroup() %>% 
ggplot()+
  geom_sf(data=barrios, fill="gray90", color="white")+
  geom_sf(aes(fill=cantidad, geometry = geometry), color="white")+
  geom_point(data=barrios_centroides, aes(x=LON_ORIGEN, y=LAT_ORIGEN, color=ubicacion), shape=4, stroke=2, size=2)+
  geom_point(data=hospitales, aes(x=long, y=lat), size=1.5)+
  scale_fill_gradient(low="gold", high= "deeppink4")+
  scale_color_manual(values = c("turquoise4", "magenta4"))+
  labs(title = "Hospitales públicos por barrio",
         fill="",
         color="",
          x="",
          y="",
         caption= "Fuente: Elaboración propia en base a datos de BAData")+
  theme(panel.background = element_rect(fill = "gray100", colour = "gray100", size = 2, linetype = "solid"),
        panel.grid.major = element_line(size = 0.5, linetype = "dashed", colour = "gray80"),
        title=element_text(size=10, face = "bold"), plot.title = element_text(hjust = 0.5),
        legend.key.size = unit(0.6, "cm"), legend.key.width = unit(0.5,"cm"),
        legend.text=element_text(size=7),
        plot.caption=element_text(face = "italic", colour = "gray35",size=6),
        axis.text = element_blank(), axis.ticks = element_blank())

En el Mapa 1 podemos ver como se distribuyen los hospitales de la Ciudad y donde se localizan los 2 centroides a analizar. Vale aclarar que la ausencia de hospitales en algunos barrios no quiere decir que estos no tengan establecimientos de salud pública ya que puede haber otros tipos de establecimientos que no son tenidos en cuenta en este análisis como los CeSAC, CEMAR o los Centros Medicinales Barriales.

Ahora sí, para los ruteos, vamos a necesitar crear una función con osrmRoute() que haga los cálculos entre nuestros puntos de origen (centroides de los 2 barrios) y los de destino (hospitales).

ATENCIÓN: Como estamos usando 2 orígenes pero múltiples destinos, es necesario que creemos una función que haga los múltiples ruteos a la vez. Sino deberíamos hacer uno por uno a mano y sería muy engorroso.

Generemos la función de ruteo

En la función vamos a calcular las 2 nuevas variables que nos interesan: distancias y duración de viajes. Para esto ajustamos algunos argumentos como “returnclass” que hace referencia al tipo de objeto que queremos que nos devuelva la función, en este caso, un objeto espacial “sf”; y “overview” que hace referencia a la calidad con la que se genera la nueva geometría, en este caso utilizaremos la de mayor precisión que es “full”.

ruteo_hospitales <- function(o_nombre, o_x, o_y, d_nombre, d_x, d_y) {
  ruta <- osrmRoute(src = c(o_nombre, o_x, o_y),
                    dst = c(d_nombre, d_x, d_y), 
                    returnclass = "sf",
                    overview = "full")
  
  cbind(ORIGEN = o_nombre, DESTINO = d_nombre, ruta)
}

Ahora si, solo nos falta un paso para que la función creada se ejecute bien: Generar un dataframe para cada barrio que contenga las variables NOMBRE_ORIGEN, LON_ORIGEN, LAT_ORIGEN, NOMBRE_DESTINO, LON_DESTINO y LAT_DESTINO. Como verán, ya tenemos todos estos datos, así que es solo cuestión de organizarlos.

Barrio del Norte: PALERMO

h_palermo <- hospitales %>%
  mutate(NOMBRE_ORIGEN="PALERMO") %>%
  left_join(barrios_centroides, by=c("NOMBRE_ORIGEN"="barrio")) %>%
  rename(NOMBRE_DESTINO=nombre,
         LON_DESTINO=long,
         LAT_DESTINO=lat) %>%
  select(NOMBRE_ORIGEN, LON_ORIGEN, LAT_ORIGEN, NOMBRE_DESTINO, LON_DESTINO, LAT_DESTINO)

head(h_palermo)
##   NOMBRE_ORIGEN LON_ORIGEN LAT_ORIGEN
## 1       PALERMO  -58.42234  -34.57386
## 2       PALERMO  -58.42234  -34.57386
## 3       PALERMO  -58.42234  -34.57386
## 4       PALERMO  -58.42234  -34.57386
## 5       PALERMO  -58.42234  -34.57386
## 6       PALERMO  -58.42234  -34.57386
##                                             NOMBRE_DESTINO LON_DESTINO
## 1              HOSPITAL GENERAL DE NIÑOS PEDRO DE ELIZALDE   -58.37755
## 2              HOSPITAL GENERAL DE NIÑOS RICARDO GUTIERREZ   -58.41207
## 3 HOSPITAL DE ODONTOLOGIA DR. RAMON CARRILLO (EX NACIONAL)   -58.40273
## 4                  HOSPITAL DE SALUD MENTAL BRAULIO MOYANO   -58.38516
## 5                 HOSPITAL DE GASTROENTEROLOGIA B. UDAONDO   -58.39131
## 6                         INSTITUTO DE ZOONOSIS L. PASTEUR   -58.43494
##   LAT_DESTINO
## 1   -34.62885
## 2   -34.59419
## 3   -34.58453
## 4   -34.63940
## 5   -34.63415
## 6   -34.60847

A RUTEAR!

ruteo_palermo <- list(h_palermo$NOMBRE_ORIGEN, h_palermo$LON_ORIGEN,h_palermo$LAT_ORIGEN,
                   h_palermo$NOMBRE_DESTINO, h_palermo$LON_DESTINO,h_palermo$LAT_DESTINO)

ruteo_norte <- pmap(ruteo_palermo, ruteo_hospitales) %>% 
  reduce(rbind)

Ya tenemos los primeros datos de Palermo. Revisemos los resultados.

ruteo_norte %>% 
summary()
##      ORIGEN  
##  PALERMO:36  
##              
##              
##              
##              
##              
##              
##                                                      DESTINO  
##  HOSPITAL GENERAL DE NIÑOS PEDRO DE ELIZALDE             : 1  
##  HOSPITAL GENERAL DE NIÑOS RICARDO GUTIERREZ             : 1  
##  HOSPITAL DE ODONTOLOGIA DR. RAMON CARRILLO (EX NACIONAL): 1  
##  HOSPITAL DE SALUD MENTAL BRAULIO MOYANO                 : 1  
##  HOSPITAL DE GASTROENTEROLOGIA B. UDAONDO                : 1  
##  INSTITUTO DE ZOONOSIS L. PASTEUR                        : 1  
##  (Other)                                                 :30  
##       src    
##  PALERMO:36  
##              
##              
##              
##              
##              
##              
##                                                        dst    
##  HOSPITAL GENERAL DE NIÑOS PEDRO DE ELIZALDE             : 1  
##  HOSPITAL GENERAL DE NIÑOS RICARDO GUTIERREZ             : 1  
##  HOSPITAL DE ODONTOLOGIA DR. RAMON CARRILLO (EX NACIONAL): 1  
##  HOSPITAL DE SALUD MENTAL BRAULIO MOYANO                 : 1  
##  HOSPITAL DE GASTROENTEROLOGIA B. UDAONDO                : 1  
##  INSTITUTO DE ZOONOSIS L. PASTEUR                        : 1  
##  (Other)                                                 :30  
##     duration        distance               geometry 
##  Min.   :10.48   Min.   : 2.816   LINESTRING   :36  
##  1st Qu.:19.32   1st Qu.: 5.869   epsg:4326    : 0  
##  Median :24.68   Median :10.982   +proj=long...: 0  
##  Mean   :24.24   Mean   :10.648                     
##  3rd Qu.:27.66   3rd Qu.:13.477                     
##  Max.   :40.96   Max.   :30.527                     
## 

Efectivamente, se agregaron 2 nuevos campos llamados duration y distance. Podemos ver que:

  • La duración promedio de los viajes es de 24 minutos, mientras que la distancia promedio es de 10,6 km.
  • El hospital más cercano está a 2,8 km y el más lejano a 30,5 km.
  • El Hospital al que se accede en menor tiempo de viaje es en 10,5 minutos y el de mayor es 41 min.

¿Cuál es el Hospital más cercano al centro de Palermo?

filter(ruteo_norte, distance == min(distance))$DESTINO
## [1] HOSPITAL GENERAL DE AGUDOS DR. J. A. FERNANDEZ
## 36 Levels: HOSPITAL GENERAL DE NIÑOS PEDRO DE ELIZALDE ...

¿Cuáles son los 10 hospitales que tienen los recorridos de menor distancia?

ruteo_norte <- ruteo_norte %>% 
  arrange(distance) %>% 
  head(10) %>% 
  left_join(h_palermo, by=c("DESTINO"="NOMBRE_DESTINO")) %>% 
  mutate(RUTA = paste("Desde", ORIGEN,"hasta", DESTINO))

ruteo_norte$DESTINO
##  [1] "HOSPITAL GENERAL DE AGUDOS DR. J. A. FERNANDEZ"          
##  [2] "HOSPITAL GENERAL DE NIÑOS RICARDO GUTIERREZ"             
##  [3] "HOSPITAL DE ODONTOLOGIA DR. RAMON CARRILLO (EX NACIONAL)"
##  [4] "HOSPITAL GENERAL DE AGUDOS B. RIVADAVIA"                 
##  [5] "INSTITUTO DE REHABILITACION PSICOFISICA (I.R.E.P.)"      
##  [6] "HOSPITAL MUNICIPAL DE ONCOLOGIA MARIE CURIE"             
##  [7] "INSTITUTO DE ZOONOSIS L. PASTEUR"                        
##  [8] "HOSPITAL OFTALMOLOGICO DR. PEDRO LAGLEYZE"               
##  [9] "HOSPITAL GENERAL DE AGUDOS DR. C. DURAND"                
## [10] "HOSPITAL GENERAL DE AGUDOS J. M. RAMOS MEJIA"

LISTO! Ya logramos lo que queríamos, y podemos decir a que distancia y tiempo de viaje está cada hospital. Pero, no nos conformemos con esto. Aprovechemos la magia de leaflet() para ver toda está información en un lindo mapa interactivo.

Mapa 2. Rutas calculadas desde Palermo hasta los 10 hospitales públicos de la Ciudad más cercanos

paleta <- c(low="gold", high= "deeppink4")

icons_d <- awesomeIcons(icon = "hospital-o",
                      iconColor = "black",
                      library = "fa",
                      markerColor = "red")

icons_o <- awesomeIcons(icon = "whatever",
                      iconColor = "black",
                      library = "fa",
                      markerColor = "gray")

labels <- sprintf(
  "<strong>%s</strong><br/>%g km <br/>%g min",
  ruteo_norte$RUTA, round(ruteo_norte$distance, 2), round(ruteo_norte$duration, 0)
) %>% lapply(htmltools::HTML)

mapa1 <- leaflet(ruteo_norte) %>%
  addTiles() %>%
  addProviderTiles(providers$CartoDB) %>%
  addPolylines(color = ~colorNumeric(paleta, ruteo_norte$distance)(distance),
               weight = 6,
               label = labels,
    labelOptions = labelOptions(
      style = list("font-weight" = "normal", padding = "2px 5px"),
      textsize = "10px",
      direction = "top"),
              highlight = highlightOptions(weight = 8,
                                           bringToFront = TRUE)) %>% 
  addLegend("bottomright", pal = colorNumeric(paleta, ruteo_norte$distance), values = ~distance,
            title = "Distancia",
            labFormat = labelFormat(suffix = "km"),
            opacity = 0.75) %>%
  addAwesomeMarkers(~LON_DESTINO, ~LAT_DESTINO, popup = ~DESTINO, icon=icons_d)%>%
  addAwesomeMarkers(~LON_ORIGEN, ~LAT_ORIGEN, popup = ~ORIGEN, icon=icons_o)
mapa1

Ahora repitamos el proceso con Villa Lugano a ver que ocurre

Barrio del Sur: VILLA LUGANO

h_lugano <- hospitales %>%
  mutate(NOMBRE_ORIGEN="VILLA LUGANO") %>%
  left_join(barrios_centroides, by=c("NOMBRE_ORIGEN"="barrio")) %>%
  rename(NOMBRE_DESTINO=nombre,
         LON_DESTINO=long,
         LAT_DESTINO=lat) %>%
  select(NOMBRE_ORIGEN, LON_ORIGEN, LAT_ORIGEN, NOMBRE_DESTINO, LON_DESTINO, LAT_DESTINO)

head(h_lugano)
##   NOMBRE_ORIGEN LON_ORIGEN LAT_ORIGEN
## 1  VILLA LUGANO  -58.47617  -34.67499
## 2  VILLA LUGANO  -58.47617  -34.67499
## 3  VILLA LUGANO  -58.47617  -34.67499
## 4  VILLA LUGANO  -58.47617  -34.67499
## 5  VILLA LUGANO  -58.47617  -34.67499
## 6  VILLA LUGANO  -58.47617  -34.67499
##                                             NOMBRE_DESTINO LON_DESTINO
## 1              HOSPITAL GENERAL DE NIÑOS PEDRO DE ELIZALDE   -58.37755
## 2              HOSPITAL GENERAL DE NIÑOS RICARDO GUTIERREZ   -58.41207
## 3 HOSPITAL DE ODONTOLOGIA DR. RAMON CARRILLO (EX NACIONAL)   -58.40273
## 4                  HOSPITAL DE SALUD MENTAL BRAULIO MOYANO   -58.38516
## 5                 HOSPITAL DE GASTROENTEROLOGIA B. UDAONDO   -58.39131
## 6                         INSTITUTO DE ZOONOSIS L. PASTEUR   -58.43494
##   LAT_DESTINO
## 1   -34.62885
## 2   -34.59419
## 3   -34.58453
## 4   -34.63940
## 5   -34.63415
## 6   -34.60847
ruteo_lugano <- list(h_lugano$NOMBRE_ORIGEN, h_lugano$LON_ORIGEN,h_lugano$LAT_ORIGEN,
                   h_lugano$NOMBRE_DESTINO, h_lugano$LON_DESTINO,h_lugano$LAT_DESTINO)

ruteo_sur <- pmap(ruteo_lugano, ruteo_hospitales) %>% 
  reduce(rbind)
ruteo_sur %>% 
  summary()
##           ORIGEN  
##  VILLA LUGANO:36  
##                   
##                   
##                   
##                   
##                   
##                   
##                                                      DESTINO  
##  HOSPITAL GENERAL DE NIÑOS PEDRO DE ELIZALDE             : 1  
##  HOSPITAL GENERAL DE NIÑOS RICARDO GUTIERREZ             : 1  
##  HOSPITAL DE ODONTOLOGIA DR. RAMON CARRILLO (EX NACIONAL): 1  
##  HOSPITAL DE SALUD MENTAL BRAULIO MOYANO                 : 1  
##  HOSPITAL DE GASTROENTEROLOGIA B. UDAONDO                : 1  
##  INSTITUTO DE ZOONOSIS L. PASTEUR                        : 1  
##  (Other)                                                 :30  
##            src    
##  VILLA LUGANO:36  
##                   
##                   
##                   
##                   
##                   
##                   
##                                                        dst    
##  HOSPITAL GENERAL DE NIÑOS PEDRO DE ELIZALDE             : 1  
##  HOSPITAL GENERAL DE NIÑOS RICARDO GUTIERREZ             : 1  
##  HOSPITAL DE ODONTOLOGIA DR. RAMON CARRILLO (EX NACIONAL): 1  
##  HOSPITAL DE SALUD MENTAL BRAULIO MOYANO                 : 1  
##  HOSPITAL DE GASTROENTEROLOGIA B. UDAONDO                : 1  
##  INSTITUTO DE ZOONOSIS L. PASTEUR                        : 1  
##  (Other)                                                 :30  
##     duration        distance               geometry 
##  Min.   : 7.61   Min.   : 2.672   LINESTRING   :36  
##  1st Qu.:17.63   1st Qu.:10.515   epsg:4326    : 0  
##  Median :19.93   Median :12.027   +proj=long...: 0  
##  Mean   :22.01   Mean   :12.891                     
##  3rd Qu.:25.70   3rd Qu.:14.732                     
##  Max.   :36.96   Max.   :28.180                     
## 

En este caso podemos ver que: La duración promedio de los viajes es de 22 minutos, mientras que la distancia promedio es de 12,9 km. El hospital más cercano está a 2,67 km y el más lejano a 28,18 km. * El Hospital al que se accede en menor tiempo de viaje es en 7,6 minutos y el de mayor es 37 minutos.

¿Cuál es el Hospital más cercano al centro de Villa Lugano?

filter(ruteo_sur, distance == min(distance))$DESTINO
## [1] HOSPITAL CECILIA GRIERSON
## 36 Levels: HOSPITAL GENERAL DE NIÑOS PEDRO DE ELIZALDE ...

¿Cuáles son los 10 hospitales que tienen los recorridos de menor distancia?

ruteo_sur <- ruteo_sur %>% 
  arrange(distance) %>% 
  head(10) %>% 
  left_join(h_lugano, by=c("DESTINO"="NOMBRE_DESTINO")) %>% 
  mutate(RUTA = paste("Desde", ORIGEN,"hasta", DESTINO))

ruteo_sur$DESTINO
##  [1] "HOSPITAL CECILIA GRIERSON"                        
##  [2] "HOSPITAL GENERAL DE AGUDOS P. PIÑERO"             
##  [3] "HOSPITAL GENERAL DE AGUDOS DR. T. ALVAREZ"        
##  [4] "HOSPITAL DE QUEMADOS DR. ARTURO UMBERTO ILLIA"    
##  [5] "HOSPITAL GENERAL DE AGUDOS DONACION F. SANTOJANNI"
##  [6] "HOSPITAL GENERAL DE AGUDOS D. VELEZ SARSFIELD"    
##  [7] "HOSPITAL DE ODONTOLOGIA  JOSE DUEÑAS"             
##  [8] "HOSPITAL OFTALMOLOGICO DR. PEDRO LAGLEYZE"        
##  [9] "HOSPITAL DE REHABILITACION M. ROCCA"              
## [10] "HOSPITAL GENERAL DE AGUDOS DR. C. DURAND"

Mapa 3. Rutas calculadas desde Villa Lugano hasta los 10 hospitales públicos de la Ciudad más cercanos

labels <- sprintf(
  "<strong>%s</strong><br/>%g km <br/>%g min",
  ruteo_sur$RUTA, round(ruteo_sur$distance, 2), round(ruteo_sur$duration, 0)
) %>% lapply(htmltools::HTML)

mapa2 <- leaflet(ruteo_sur) %>%
  addTiles() %>%
  addProviderTiles(providers$CartoDB) %>%
  addPolylines(color = ~colorNumeric(paleta, ruteo_sur$distance)(distance),
              label = labels,
    labelOptions = labelOptions(
      style = list("font-weight" = "normal", padding = "2px 5px"),
      textsize = "10px",
      direction = "top")) %>% 
  addLegend("bottomright", pal = colorNumeric(paleta, ruteo_sur$distance), values = ~distance,
            title = "Distancia",
            labFormat = labelFormat(suffix = "km"),
            opacity = 0.75)%>%
  addAwesomeMarkers(~LON_DESTINO, ~LAT_DESTINO, popup = ~DESTINO, icon=icons_d)%>%
  addAwesomeMarkers(~LON_ORIGEN, ~LAT_ORIGEN, popup = ~ORIGEN, icon=icons_o)
mapa2

LISTO! Objetivo cumplido. Hicimos la función de ruteo y la ejecutamos entre 2 barrios de la Ciudad y los 36 hospitales públicos.