Tutorial: Mapa en {ggplot2} con calles desde Open Street Map

3/9/2024

Temas: mapas chile gráficos

En este tutorial crearemos un mapa de una región de Chile, y sobre el polígono geográfico aplicaremos otros elementos geográficos como calles, avenidas y carreteras, obtenidos desde Open Street Map (proveedor de mapas online abierto y comunitario).

Esto puede servir para crear visualizaciones de datos espaciales minimalistas que de todos modos entreguen elementos urbanos de referencia para que les usuaries puedan ubicarse mejor espacialmente.

Usaremos {dplyr} para manipular los datos, el paquete {ggplot2} para visualización de datos, {sf} para tratamiento de elementos espaciales, {rnaturalearth} para obtener mapas de cualquier país o región del mundo, y {osmdata} para obtener datos estaciales desde Open Street Map (OSM) por medio de su API pública.

Cargar paquetes

library(dplyr) #manipulación de datos
library(ggplot2) #gráficos
library(sf) #elementos espaciales
library(rnaturalearth) #mapas del mundo
library(osmdata) #datos open street map

Obtener mapa de país y de región

El primer paso es obtener un mapa de Chile, a nivel de regiones, usando la función ne_states() del paquete {rnaturalearth}. Luego filtramos el dataframe resultante para enfocarnos en una sola región del país.

# obtener mapa pero a nivel de regiones
mapa_1 <- rnaturalearth::ne_states(country = "Chile") |> 
    select(name, name_es, iso_3166_2, code_hasc, geometry)

mapa_2 <- mapa_1 |> 
    filter(name == "Los Lagos")

mapa_2
Simple feature collection with 1 feature and 4 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -74.40722 ymin: -44.045 xmax: -71.65926 ymax: -40.27304
Geodetic CRS:  WGS 84
       name   name_es iso_3166_2 code_hasc                       geometry
1 Los Lagos Los Lagos      CL-LL     CL.LG MULTIPOLYGON (((-71.76926 -...

Previsualizamos la región obtenida usando {ggplot2}:

mapa_2 |> 
    ggplot() +
    geom_sf(linewidth = .4,
            color = "grey80", fill = "grey95") +
    theme_classic()

Luego realizamos el primer paso para obtener los datos desde Open Street Map, que es definir las coordenadas de la caja delimitadora o bounding box del lugar del que vamos a solicitar datos. Por ejemplo, una ciudad específica. Si bien esto se puede hacer con la función osmdata::getbb(), los resultados no siempre coinciden con lo que esperamos (la región o ciudad puede salir más recortada de lo esperado), por lo que preferimos usar directamente la caja del mapa que queremos visualizar, usando sf::st_bbox().

# obtener perímetro o área abarcada por el mapa
caja <- mapa_2 |> st_bbox()

# opcionalmente, se podría hacer con el nombre del lugar
# area <- getbb("Región de Los Lagos, Chile")

caja
     xmin      ymin      xmax      ymax 
-74.40722 -44.04500 -71.65926 -40.27304 

Obtener datos geográficos desde Open Street Map

Teniendo estas coordenadas, se solicitan los datos haciendo un query a la API Overpass de OSM. En esta solicitud se especifica la característica del mapa que se va a obtener, y sus valores; en este caso, queremos obtener las calles de una región, por lo que la feature corresponde a highway. Dividimos la query en varias partes, para tener en objetos separados las calles de distintos tamaños (a grandes rasgos: autopistas y avenidas, calles secundarias, y calles interiores).

Considerar que cada query puede tomar un tiempo, dependiendo de la cantidad de datos, y que OSM es un proyecto soportado por voluntarios, por lo que se recomienda no abusar de la API (hacer la mínima cantidad de solicitudes, no hacer muchas solicitudes seguidas, etc.)

# obtener calles dentro de la caja
calles_grandes <- caja |> 
    opq(timeout = 500) |> 
    add_osm_feature(key = "highway", 
                    value = c("motorway", "primary", "motorway_link", "primary_link")) |> 
    osmdata_sf()

calles_medianas <- caja |> 
    opq(timeout = 500) |> 
    add_osm_feature(key = "highway", 
                    value = c("secondary", "tertiary", "secondary_link", "tertiary_link")) |> 
    osmdata_sf()

calles_chicas <- caja |>
    opq(timeout = 500) |>
    add_osm_feature(key = "highway",
                    value = c("residential", "living_street",
                              "unclassified", "service", "footway")) |>
    osmdata_sf()

calles_grandes
Object of class 'osmdata' with:
                 $bbox : -44.0449981164699,-74.4072159499999,-40.2730445289999,-71.6592646899999
        $overpass_call : The call submitted to the overpass API
                 $meta : metadata including timestamp and version numbers
           $osm_points : 'sf' Simple Features Collection with 34928 points
            $osm_lines : 'sf' Simple Features Collection with 3580 linestrings
         $osm_polygons : 'sf' Simple Features Collection with 0 polygons
       $osm_multilines : NULL
    $osm_multipolygons : NULL

Habiendo obtenido los datos geográficos desde OSM, recibimos objetos que contienen los elementos espaciales. Extraemos las líneas de cada uno de los objetos, para quedarnos sólo con las calles. Opcionalmente, podemos recortar los elementos espaciales obtenidos con la figura del mapa, usando el mapa como figura de recorte para que las líneas de las calles no se salgan de los márgenes del mapa.

# extraer toda la geometría
calles_grandes_a <- calles_grandes$osm_lines
calles_medianas_a <- calles_medianas$osm_lines
calles_chicas_a <- calles_chicas$osm_lines

# recortar geometría para que calce dentro del polígono del mapa
calles_grandes_b <- st_intersection(calles_grandes$osm_lines, mapa_2)
calles_medianas_b <- st_intersection(calles_medianas$osm_lines, mapa_2)
calles_chicas_b <- st_intersection(calles_chicas$osm_lines, mapa_2)

Visualizar mapa con calles

Finalmente, podemos aplicar las capas obtenidas a nuestro gráfico de {ggplot2}. Podemos ajustar la apariencia de cada tipo de calle por separado dado que las ubicamos en capas distintas.

    ggplot() +
    # capa del país entero (como fondo)
    geom_sf(data = mapa_1, 
            linewidth = .4,
            color = "grey95", fill = "grey98") +
    # capa del mapa de la región
    geom_sf(data = mapa_2,
            linewidth = .4, 
            color = "grey80", fill = "grey95") +
    # capas osm
    geom_sf(data = calles_medianas_b, 
            linewidth = .3, alpha = .3) +
    geom_sf(data = calles_grandes_b, 
            linewidth = .4, alpha = .3) +
    # recortar vista
    coord_sf(xlim = caja[c(1, 3)],
             ylim = c(-42.4, -40.3)) +
    theme_classic()

Otro ejemplo del mapa visto desde más cerca, incluyendo la capa de calles interiores:

ggplot() +
    # capa del mapa
    geom_sf(data = mapa_2,
            fill = "grey95") +
    # capas osm
    geom_sf(data = calles_grandes_b, 
            linewidth = .3, alpha = 1) +
    geom_sf(data = calles_medianas_b, 
            linewidth = .3, alpha = .4) +
    geom_sf(data = calles_chicas_b, 
            linewidth = .2, alpha = .2) +
    # recortar vista
    coord_sf(xlim = c(-73.18, -73.09),
             ylim = c(-40.6, -40.55)) +
    theme_classic()

Ejemplo

Realizamos el mismo proceso en un solo paso, para otra región del país, mostrando la diferencia entre visualizar un dato al azar con y sin calles.

# obtener mapa
mapa_rm <- rnaturalearth::ne_states(country = "Chile") |> 
    select(name, geometry) |> 
    # filter(name == "Región Metropolitana de Santiago")
    filter(name == "Maule")
    
# obtener calles
calles_grandes_rm <- st_bbox(mapa_rm) |> 
    opq(timeout = 500) |> 
    add_osm_feature(key = "highway", 
                    value = c("motorway", "primary", "motorway_link", "primary_link")) |> 
    osmdata_sf()

calles_medianas_rm <- st_bbox(mapa_rm) |> 
    opq(timeout = 500) |> 
    add_osm_feature(key = "highway", 
                    value = c("secondary", "tertiary", "secondary_link", "tertiary_link")) |> 
    osmdata_sf()

# puntos para graficar
puntos <- calles_grandes_rm$osm_points |> 
    filter(highway == "stop") |> 
    st_intersection(mapa_rm) |> 
    rowwise() |> 
    mutate(azar = sample(1:10, 1),
           azar = ifelse(azar < 7, 1, azar)) |> 
    ungroup()
# visualizar sin calles
ggplot() +
    geom_sf(data = mapa_rm) +
    geom_sf(data = puntos, 
            aes(size = azar),
            color = "purple3", alpha = .2) +
    scale_size(range = c(3, 10)) +
    theme_classic()
# visualizar con calles
ggplot() +
    geom_sf(data = mapa_rm) +
    geom_sf(data = st_intersection(calles_grandes_rm$osm_lines, mapa_rm),
            linewidth = .2, alpha = .7) +
    geom_sf(data = st_intersection(calles_medianas_rm$osm_lines, mapa_rm),
            linewidth = .1, alpha = .3) +
    geom_sf(data = puntos, 
            aes(size = azar),
            color = "purple3", alpha = .2) +
    scale_size(range = c(3, 10)) +
    theme_classic()

Bastián Olea Herrera

Analista de datos, magíster en Sociología (PUC)

https://bastianolea.github.io/shiny_apps/

https://bastian.olea.biz

Fecha de publicación:
September 3, 2024
Extensión:
6 minute read, 1136 words
Categorías:
Tutoriales
Tags:
mapas chile gráficos
Ver también:
Portafolio de trabajos previos en R
Cargar y explorar datos de la encuesta Casen en R, usando factor de expansión
¿Arte? Nubes aleatorias en {ggplot2}