Open Data Day 2026

Autores

Noé Osorio

Uriel Mendoza

Introducción

OpenDataDay, Como descargar información desde el portal del INEGI.

Parte 1: Datos Espaciales

En esta parte usaremos las siguientes librerías: tidyverse manipulación de datos, sf para lectura/manipulación de datos espaciales (vectoriales) mapview para una visualización sencilla. en caso de que no las tengas puedes usar el comando “install.packages(”acá el nombre de la librerías que vas a instalar”)” por ejemplo: install.packages(“sf”) y ejecutar la función.

Una vez instaladas, es necesario llamarlas.

Ver código
library(tidyverse)
library(sf)
library(mapview)

En el portal del INEGI, aparte de sus API’S como DENUE, banco de indicadores y el sistema de Ruteo (sakbe), podemos obtener directamente datos espaciales de México.

AlgunAs de ellAs lo haremos aplicados al Estado de Tlaxcala (por rápidez ya que es el más pequeño), puedes consultar acá las claves de la Entidad Catalogo Entidades y los datos que puedes obtener son: Área Geoestadística Estatal, Área Geoestadística Municipal, Localidad Geoestadística, Asentamientos o Vialidades

  • Areas Geoestadísticas Estatales (Recuerda que los últimosdigitos corresponden al Estado a DOS CIFRAS)
    • https://gaia.inegi.org.mx/wscatgeo/v2/geo/mgee/29
  • Áreas Geoestadísticas Municipales
    • https://gaia.inegi.org.mx/wscatgeo/v2/geo/mgem/29
  • Por ageb
    • https://gaia.inegi.org.mx/wscatgeo/v2/geo/agebu/29
  • Por manzana
    • https://gaia.inegi.org.mx/wscatgeo/v2/geo/mza/29
  • Vialidades OJO,vialidades es el único que no puede obtenerse anivel Edo, por lo que acá debes especificar el municipio (nada que soluciones un loop para cargar todo el estado)
    • https://gaia.inegi.org.mx/wscatgeo/v2/geo/vialidades/29/001

TIP

Los datos o los links de arriba regresan un archivo geojson, para que devuelva solo como formato de tabla borra la palabra geo del link

  • geojson: https://gaia.inegi.org.mx/wscatgeo/v2/geo/mgee/29
  • json : https://gaia.inegi.org.mx/wscatgeo/v2/mgee/29

Para cada consulta es un link diferente o puede ser el mismo link pero se van agregando parámetros, acá te muestro como generar algunos de forma sencilla, pero puedes ver todos los ejemplos en el portal de servicios web en Guía para desarrolladores en el Catálogo Único de Claves Geoestadísticas.

Entidades.

Le información que puedes consultar está tanto en formato tabla(json) ó “espacial” (geojson). El primer ejemplo (y único) para Tabla (json) leeremos la información de población a nivel nacional.

Ojo que algunas veces ocupo las librerías de esta forma: “nombre::función”

Lo hago así ya que no quiero llamar a toda la librería únicamente a esa función. Acá la función viene de la librería jsonlite y la función “fromJSON” es la que lee ese tipo de archivos

Ver código
pob_nal = jsonlite::fromJSON(
  "https://gaia.inegi.org.mx/wscatgeo/v2/mgee/",
  flatten = TRUE
  )

Al hacer esta consulta nos devuelve un objeto tipo lista que contiene 3 elementos: datos, metadatos, numReg

Ver código
str(pob_nal)
List of 3
 $ datos    :'data.frame':  32 obs. of  8 variables:
  ..$ cvegeo                   : chr [1:32] "01" "02" "03" "04" ...
  ..$ cve_ent                  : chr [1:32] "01" "02" "03" "04" ...
  ..$ nomgeo                   : chr [1:32] "Aguascalientes" "Baja California" "Baja California Sur" "Campeche" ...
  ..$ nom_abrev                : chr [1:32] "Ags." "BC" "BCS" "Camp." ...
  ..$ pob_total                : chr [1:32] "1425607" "3769020" "798447" "928363" ...
  ..$ pob_femenina             : chr [1:32] "728924" "1868431" "392568" "471424" ...
  ..$ pob_masculina            : chr [1:32] "696683" "1900589" "405879" "456939" ...
  ..$ total_viviendas_habitadas: chr [1:32] "386671" "1149563" "240660" "260824" ...
 $ metadatos:List of 1
  ..$ Fuente_informacion_estadistica: chr "INEGI. Censo de Población y Vivienda, 2020"
 $ numReg   : int 32
  • Datos Es la tabla de las entidades con variables de población,
  • Metadatos contiene de donde obtuvo la información (“INEGI. Censo de Población y Vivienda, 2020”)
  • numReg indica la cantidad de registros que nos devolvió (32, por que son 32 entidades)

por default (en json) suelen venir todo en tipo texto, así que pasaremos a numérico las variables y sobreeescribiremos el objeto pob_nal y ya podemos ver los Estados son sus variables (por facilidad solo muestro 10)

Ver código
pob_nal = pob_nal$datos %>% 
  mutate(across(c(pob_total:total_viviendas_habitadas),~as.numeric(.)))

pob_nal %>% head(10)
   cvegeo cve_ent               nomgeo nom_abrev pob_total pob_femenina
1      01      01       Aguascalientes      Ags.   1425607       728924
2      02      02      Baja California        BC   3769020      1868431
3      03      03  Baja California Sur       BCS    798447       392568
4      04      04             Campeche     Camp.    928363       471424
5      05      05 Coahuila de Zaragoza     Coah.   3146771      1583102
6      06      06               Colima      Col.    731391       370769
7      07      07              Chiapas     Chis.   5543828      2837881
8      08      08            Chihuahua     Chih.   3741869      1888047
9      09      09     Ciudad de México      CDMX   9209944      4805017
10     10      10              Durango      Dgo.   1832650       927784
   pob_masculina total_viviendas_habitadas
1         696683                    386671
2        1900589                   1149563
3         405879                    240660
4         456939                    260824
5        1563669                    901249
6         360622                    227050
7        2705947                   1351630
8        1853822                   1146915
9        4404927                   2757433
10        904866                    493989

Usango gt podemos hacer una tabla sencilla y rápida con los primeros 10 registros.

Ver código
pob_nal %>% 
  arrange(desc(total_viviendas_habitadas)) %>% 
  head(10) %>% 
  gt() %>% 
    fmt_number(
    columns = c("pob_total":"total_viviendas_habitadas"),
    decimals = 0,
    use_seps = TRUE
  ) %>% 
    data_color(
    columns = total_viviendas_habitadas,
    method = "numeric",
    palette = "viridis"
  ) %>% 
    cols_label_with(
      fn = ~ html(str_to_title(gsub("_", "<br>", .x)))
    )
Cvegeo Cve
Ent
Nomgeo Nom
Abrev
Pob
Total
Pob
Femenina
Pob
Masculina
Total
Viviendas
Habitadas
15 15 México Mex. 16,992,418 8,741,123 8,251,295 4,569,533
09 09 Ciudad de México CDMX 9,209,944 4,805,017 4,404,927 2,757,433
30 30 Veracruz de Ignacio de la Llave Ver. 8,062,579 4,190,805 3,871,774 2,391,262
14 14 Jalisco Jal. 8,348,151 4,249,696 4,098,455 2,332,218
21 21 Puebla Pue. 6,583,278 3,423,163 3,160,115 1,713,865
19 19 Nuevo León NL 5,784,442 2,893,492 2,890,950 1,655,690
11 11 Guanajuato Gto. 6,166,934 3,170,480 2,996,454 1,587,234
07 07 Chiapas Chis. 5,543,828 2,837,881 2,705,947 1,351,630
16 16 Michoacán de Ocampo Mich. 4,748,846 2,442,505 2,306,341 1,285,469
02 02 Baja California BC 3,769,020 1,868,431 1,900,589 1,149,563

Datos espaciales

Como puedes ver solo leímos un link y fue sencillo de obtener, no necesitamos ningún token para poder leer la información, pero bueno ahora sí a leer datos espaciales (al fin!) ¿Recuerdas la Guía para desarrolladores? en el Catálogo Único de Claves Geoestadísticas cada información que indica, menciona que puedes obtener la información vectorial en formato geojson, puede ser ya sea a nivel nacional o información por Estado municipio, Ageb ,acá presentaremos algunos ejemplos

Estados

Para leer Una entidad en específico debemos armar un link. de la siguiente forma:

https://gaia.inegi.org.mx/wscatgeo/v2/geo/mgee/{cve_ent}*

Donde dice cve_ent es donde se debe reemplazar por un número del 1 al 32 para asignar los Estados, Algunos que me sé de memoria, 1 aguascalientes, el 15 Edo Mex, 09 CDMX, 32 Zacatecas etc.. Ojo que la clave del Estado debe ir a dos cifras, o sea si ponemos CDMX tendría que ser “09”, Puedes consultar las entidades acá CATÁLOGO DE ENTIDADES FEDERATIVAS y para hacernos la consulta sencilla lo dividimos en 2, el link_base y el estado que solo se lo pegamos. Haré el ejercicio con la clave 11 (Guanajuato)

Este es el link al que se hará la consulta:

Ver código
link_base = "https://gaia.inegi.org.mx/wscatgeo/v2/geo/mgee/"

paste0(link_base,"29")
[1] "https://gaia.inegi.org.mx/wscatgeo/v2/geo/mgee/29"

y con la función st_read() (de la librería sf) leemos el link

Ver código
# st_read viene de la librería sf

estado = st_read(
  paste0(link_base,"11"),
  quiet = TRUE
)

¿Qué devuelve esta consulta?

Nos regresa un objeto espacial que contiene las variables: de Nombre del Estado, Población total, femenina, masculina y el total de viviendas habitadas (todo esto del último censo de población del 2020)

Ver código
estado 
Simple feature collection with 1 feature and 8 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -102.097 ymin: 19.91275 xmax: -99.6713 ymax: 21.83942
Geodetic CRS:  WGS 84
  cvegeo cve_ent     nomgeo nom_abrev pob_total pob_femenina pob_masculina
1     11      11 Guanajuato      Gto.   6166934      3170480       2996454
  total_viviendas_habitadas                       geometry
1                   1587234 MULTIPOLYGON (((-101.3615 2...

En R hay muchas formas de visualizar, plot, leaflet, con mapview puedes hacer la visualización dinámica.

Ver código
mapview(estado)

Recuerda que cuando leemos json lo lee todo como formato de texto.

Ahora intentemos con municipios, al hacer la misma opoeración ahora con el link de municipios y visualizarla a Cada alcaldía le asignó un color distinto, esto es porque está haciendo un “fill” por nombre y no por el valor de viviendas, puedes corroborar esto pasándolo a formato tabla

Ver código
mun = st_read("https://gaia.inegi.org.mx/wscatgeo/v2/geo/mgem/09",quiet=TRUE)

mapview(mun,z="total_viviendas_habitadas")

Formato tabla

Ver código
mun %>% as_tibble()
# A tibble: 16 × 10
   cvegeo cve_ent cve_mun nomgeo    cve_cab pob_total pob_femenina pob_masculina
   <chr>  <chr>   <chr>   <chr>     <chr>   <chr>     <chr>        <chr>        
 1 09010  09      010     Álvaro O… ----    759137    398130       361007       
 2 09016  09      016     Miguel H… ----    414470    219003       195467       
 3 09005  09      005     Gustavo … ----    1173351   609477       563874       
 4 09007  09      007     Iztapala… ----    1835486   947835       887651       
 5 09015  09      015     Cuauhtém… ----    545884    284933       260951       
 6 09017  09      017     Venustia… ----    443704    233586       210118       
 7 09013  09      013     Xochimil… ----    442178    226726       215452       
 8 09003  09      003     Coyoacán  ----    614447    325337       289110       
 9 09008  09      008     La Magda… ----    247622    129335       118287       
10 09009  09      009     Milpa Al… ----    152685    78314        74371        
11 09014  09      014     Benito J… ----    434153    232032       202121       
12 09011  09      011     Tláhuac   ----    392313    202123       190190       
13 09006  09      006     Iztacalco ----    404695    212343       192352       
14 09002  09      002     Azcapotz… ----    432205    227255       204950       
15 09012  09      012     Tlalpan   ----    699928    365051       334877       
16 09004  09      004     Cuajimal… ----    217686    113537       104149       
# ℹ 2 more variables: total_viviendas_habitadas <chr>,
#   geometry <MULTIPOLYGON [°]>

pasamos a numérico y sobreescribimos

Ver código
mun = mun %>% mutate(across(c("pob_total":"total_viviendas_habitadas"),as.numeric))
Ver código
mapview(mun,z="total_viviendas_habitadas")

Nuevamente Estado

El marco geoestadístico se actualiza cada diciembre aunque en su página indica que la próxima publicación será el 31 de agosto INEGI MG . Tal vez te preguntes ¿Bueno pero qué se actualiza? ¿Se agregan más estados? ¿Cuál debo ocupar? y la respuesta es “Depende” Los datos que leemos directamente del link son del último marco Geoestadístico, peero contiene la información del Censo del 2020 así que puedes encontrarte algunos municipios que no tengan información

Ver código
#Guerrero

mun = st_read("https://gaia.inegi.org.mx/wscatgeo/v2/geo/mgem/12",quiet=TRUE)

mun = mun %>% mutate(across(c("pob_total":"total_viviendas_habitadas"),as.numeric))

mapview(mun,z="pob_total")

En el mapa se muestran unas zonas en gris, que son municipios que no tiene información, por eso y no es que no tenga población, sino el próximo censo será en el 2030, así que probablemente surjan otros municipios, agebs rurales pasen a urbanos.. etc. Los siguientes Ejercicios los haremos de forma más rápida

Ejemplo Agebs

solo por diversión, te mostraré a hacer un mapa vibariado, para esto debes tener la librería “bivariateLeaflet”

Ver código
library(bivariateLeaflet)

ageb_tlaxcala = st_read("https://gaia.inegi.org.mx/wscatgeo/v2/geo/agebu/29") %>% 
   mutate(across(c("pob_total":"total_viviendas_habitadas"),as.numeric))
Reading layer `29' from data source 
  `https://gaia.inegi.org.mx/wscatgeo/v2/geo/agebu/29' using driver `GeoJSON'
Simple feature collection with 675 features and 11 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -98.65882 ymin: 19.10507 xmax: -97.62544 ymax: 19.66969
Geodetic CRS:  WGS 84
Ver código
mapa = create_bivariate_map(
  data = ageb_tlaxcala,
  var_1 = "pob_masculina",    # Total population
  var_2 = "pob_femenina"     # Median household income
)

mapa

Ejemplo vialidades

Recuerda que vialidades es el único que no se puede a nivel Entidad así que debes integrar los dígitos del municipio

Ver código
vialidades_tlaxcala = st_read("https://gaia.inegi.org.mx/wscatgeo/v2/geo/vialidades/29/001")
Reading layer `001' from data source 
  `https://gaia.inegi.org.mx/wscatgeo/v2/geo/vialidades/29/001' 
  using driver `GeoJSON'
Simple feature collection with 768 features and 9 fields
Geometry type: MULTILINESTRING
Dimension:     XY
Bounding box:  xmin: -98.22655 ymin: 19.3385 xmax: -98.14951 ymax: 19.3799
Geodetic CRS:  WGS 84
Ver código
mapview(vialidades_tlaxcala)  

Parte 2: Herramientas para el análisis espacial

En esta sección exploraremos algunas herramientas prácticas que amplían las posibilidades de tu análisis espacial. Algunos ejemplos calcular tiempos de desplazamiento, trazar rutas, y convertir texto en coordenadas (y viceversa).

Isocronas

Las isocronas delimitan el área alcanzable desde un punto en un tiempo determinado. En lugar de medir distancia en kilómetros (que ignora la infraestructura real) miden distancia en tiempo “real” de desplazamiento a través de vialidades. Ponemos “real” entre comillas ya que la movilidad va más allá de infraestructura, debe considerar la red de transporte, la configuración del terreno, la persona etc. aprovechando que tenemos los agebs, calculamos el centroide y sobre ese hacemos las isocronas (haremos solo 20 como ejemplo)

Ver código
ageb_tlaxcala = st_read("https://gaia.inegi.org.mx/wscatgeo/v2/geo/agebu/29") %>%
  head(20) %>% 
  mutate(across(c("pob_total":"total_viviendas_habitadas"),as.numeric)) %>% 
  st_centroid() 
Reading layer `29' from data source 
  `https://gaia.inegi.org.mx/wscatgeo/v2/geo/agebu/29' using driver `GeoJSON'
Simple feature collection with 675 features and 11 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -98.65882 ymin: 19.10507 xmax: -97.62544 ymax: 19.66969
Geodetic CRS:  WGS 84
Warning: st_centroid assumes attributes are constant over geometries
Ver código
mapview(ageb_tlaxcala)

Hay varias formas de generar isocronas, de forma rápida podemos usar ya sea con OSRM o MapBox Para ambas debes de delimitar.

  • Ubicación donde empezará la isocrona
  • Modalidad (Caminando, bicicleta o vehículo)
  • Tiempo: Al ser movilidad en tiempo, puedes usar un vector o un valor, típicamente se usa 5,10,15

Ejemplo OSRM con un solo punto (No necesitas ningún token), tenemos nuestro punto de partida y el área que podrías llegar en 5 y 15 mnts

Ver código
punto_1 = ageb_tlaxcala[1,]

iso_1 = osrm::osrmIsochrone(
  loc = punto_1, # Donde inicia
  breaks = c(5,15),  #Vector de tiempo
  osrm.profile ="car" # como me voy a mover
  )

mapview(iso_1,z="isomax")+
  mapview(punto_1)

con mapbox igual puedes crearlas, desde su api, puedes crear una cuenta de mapbox gratis (si tienes tu correo de universidad es super útil) , puedes crear tu cuenta acá MapBox

usa la función mb_acces_token para integrarlo como una variable de entorno.

El token de este ejemplo no es funcional, así que dentro d ela página puedes obtenerlo de forma gratuita, para guardarla como variable de entorno puedes ya sea hacer esta operación o con la librería use_this la función edit_r_environ() agregando el token a “MAPBOX_PUBLIC_TOKEN = …”

Ver código
mapboxapi::mb_access_token(token = "pk.eyJ1Ijoi[TU_TOKEN_AQUÍ]",
                           install = TRUE)

Ponemos ambas para que puedas compararlas. Hay más servicios y algoritmos, pero estos son dos muy comunes. (OSRM y MapBox)

Ver código
library(mapboxapi)
Usage of the Mapbox APIs is governed by the Mapbox Terms of Service.
Please visit https://www.mapbox.com/legal/tos/ for more information.
Ver código
iso_2 = mb_isochrone(location = punto_1,
             profile = "driving",
             time = c(5,15))


mapview(iso_1)+
  mapview(iso_2,z="time")+
  mapview(punto_1)

Ruteo

El ruteo calcula rutas sobre la red vial entre dos puntos. Ya sea en Encuestas OD o en Distancias Cercanas es útil poder generar las rutas, acá si haremos al menos 10 rutas :) Seguimos con el ejemplo de los Agebs que tenemos como punto y lo separaré en dos objetos los 3 más poblados y veré cuáles son los agebs más cercanos a estas zonas, (hay una librería útil para esto nngeo)

install.packages(“nngeo”)

Ver código
library(nngeo)

muy_poblados = ageb_tlaxcala %>% arrange(desc(pob_total)) %>% 
  head(5)


nn = st_nn(x = muy_poblados,
           y =ageb_tlaxcala, 
           k = 5, 
           progress = FALSE)
lon-lat points
Ver código
conectar = st_connect(x = muy_poblados,
           y = ageb_tlaxcala,
           ids = nn)
Calculating nearest IDs
Calculating lines

Los rojos son lo más poblados y la linea que los conecta son con el punto más cercano

Ver código
library(leaflet)

leaflet() %>% 
  addProviderTiles(providers$CartoDB) %>% 
  addCircleMarkers(data=ageb_tlaxcala) %>% 
  addCircleMarkers(data=muy_poblados,color = "red",opacity = 1) %>% 
  addPolylines(data=conectar)

ahora con ruteo, como primer paso es obtener lo más cercanos, con la misma librería y con un spatial join podemos lograrlo. POr facilidad solo me quedé con la variable de cvegeo de ambso objetos, la idea es luego filtrar sus coordenadas y hacer el ruteo, ORIGEN se repetirá 3 veces porque es la cantidad que puse de destinos en k y destino son los 3 más cercanos.

Ver código
cercanos = st_join(muy_poblados %>% select(origen = cvegeo), 
        ageb_tlaxcala %>% select(destino=cvegeo), 
        join = st_nn, 
        k = 5, 
        progress = FALSE
        )
lon-lat points
Ver código
cercanos
Simple feature collection with 25 features and 2 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: -98.20114 ymin: 19.33003 xmax: -98.16624 ymax: 19.35481
Geodetic CRS:  WGS 84
First 10 features:
           origen       destino                  geometry
1   2900100010059 2900100010059 POINT (-98.16624 19.3465)
1.1 2900100010059 290010001003A POINT (-98.16624 19.3465)
1.2 2900100010059 2900100010063 POINT (-98.16624 19.3465)
1.3 2900100010059 2900200020075 POINT (-98.16624 19.3465)
1.4 2900100010059 2900100010044 POINT (-98.16624 19.3465)
2   2900100010063 2900100010063 POINT (-98.1713 19.35481)
2.1 2900100010063 2900100010059 POINT (-98.1713 19.35481)
2.2 2900100010063 2900100010082 POINT (-98.1713 19.35481)
2.3 2900100010063 2900200020075 POINT (-98.1713 19.35481)
2.4 2900100010063 2900100010044 POINT (-98.1713 19.35481)

para el ruteo podemos realizarlo con una observación por una con un loop sencillo, pero antes ver si con uno funciona, en el ejercicio cada renglón es un “par” origen Destino, así que vamos a excluir los que su “origen es igual a su destino” ya que ocmo le pasamos todos lso agebs, lo incluye como el más cercano y agregamos una columna llamada “viaje” que es un identificador para el loop

Ver código
base_ruta = cercanos %>% 
  filter(origen!=destino) %>% 
  mutate(viaje=1:n())

base_ruta
Simple feature collection with 20 features and 3 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: -98.20114 ymin: 19.33003 xmax: -98.16624 ymax: 19.35481
Geodetic CRS:  WGS 84
First 10 features:
           origen       destino                   geometry viaje
1.1 2900100010059 290010001003A  POINT (-98.16624 19.3465)     1
1.2 2900100010059 2900100010063  POINT (-98.16624 19.3465)     2
1.3 2900100010059 2900200020075  POINT (-98.16624 19.3465)     3
1.4 2900100010059 2900100010044  POINT (-98.16624 19.3465)     4
2.1 2900100010063 2900100010059  POINT (-98.1713 19.35481)     5
2.2 2900100010063 2900100010082  POINT (-98.1713 19.35481)     6
2.3 2900100010063 2900200020075  POINT (-98.1713 19.35481)     7
2.4 2900100010063 2900100010044  POINT (-98.1713 19.35481)     8
3.1 2900200010022 2900200050060 POINT (-98.19242 19.33003)     9
3.2 2900200010022 2900200040094 POINT (-98.19242 19.33003)    10

Para calcular las rutas, primero definimos una función que toma el identificador de cada viaje, filtra el origen y destino en un solo paso con %in%, y obtiene la ruta con mb_optimized_route. Después, usamos map para aplicarla a todos los viajes y list_rbind para combinar los resultados en un solo dataframe. Esto es preferible a un for loop con rbind porque evita recrear el dataframe en cada iteración, lo que se vuelve lento con muchos viajes.

Ver código
obtener_ruta = function(id_viaje) {
  datos = base_ruta %>% filter(viaje == id_viaje)
  
  ageb_tlaxcala %>% 
    filter(cvegeo %in% c(datos$origen, datos$destino)) %>% 
    mb_optimized_route(input_data = ., profile = "walking", output = "sf") %>% 
    pluck("route") %>% 
    mutate(id = id_viaje,
           origen=datos$origen,
           destino=datos$destino)
}

contenedor = map(base_ruta$viaje, obtener_ruta) %>% 
  list_rbind()

¿Que nos regresa? una tabla con las geometrías, así que para pasarla un objketo espacial con la función “%>% sf::st_as_sf()”

Ver código
contenedor
                         geometry distance duration id        origen
1  LINESTRING (-98.16594 19.34...   2.5408 30.49000  1 2900100010059
2  LINESTRING (-98.16594 19.34...   2.8488 34.18000  2 2900100010059
3  LINESTRING (-98.16594 19.34...   6.3346 76.01333  3 2900100010059
4  LINESTRING (-98.16594 19.34...   5.2446 62.93667  4 2900100010059
5  LINESTRING (-98.16594 19.34...   2.8488 34.18000  5 2900100010063
6  LINESTRING (-98.18093 19.35...   3.4284 41.14000  6 2900100010063
7  LINESTRING (-98.17122 19.35...   6.6402 79.68000  7 2900100010063
8  LINESTRING (-98.16437 19.36...   3.2508 39.00333  8 2900100010063
9  LINESTRING (-98.19237 19.32...   2.4080 28.89667  9 2900200010022
10 LINESTRING (-98.18931 19.33...   2.5332 30.39667 10 2900200010022
11 LINESTRING (-98.19237 19.32...   3.1114 37.33000 11 2900200010022
12 LINESTRING (-98.2043 19.333...   3.0690 36.82333 12 2900200010022
13 LINESTRING (-98.18931 19.33...   4.1118 49.34333 13 290020002008A
14 LINESTRING (-98.19757 19.33...   4.5226 54.25667 14 290020002008A
15 LINESTRING (-98.18246 19.35...   2.7976 33.57667 15 290020002008A
16 LINESTRING (-98.19237 19.32...   3.9726 47.66667 16 290020002008A
17 LINESTRING (-98.2043 19.333...   1.0868 13.04000 17 2900200050060
18 LINESTRING (-98.19757 19.33...   2.4288 29.13333 18 2900200050060
19 LINESTRING (-98.19237 19.32...   2.4080 28.89667 19 2900200050060
20 LINESTRING (-98.18931 19.33...   3.6252 43.49667 20 2900200050060
         destino
1  290010001003A
2  2900100010063
3  2900200020075
4  2900100010044
5  2900100010059
6  2900100010082
7  2900200020075
8  2900100010044
9  2900200050060
10 2900200040094
11 2900200010037
12 2900200050056
13 2900200040094
14 2900200010037
15 2900200020075
16 2900200010022
17 2900200050056
18 2900200010037
19 2900200010022
20 2900200040094

y visualizamos

Ver código
espacial  = contenedor %>% 
  sf::st_as_sf()

espacial
Simple feature collection with 20 features and 5 fields
Geometry type: LINESTRING
Dimension:     XY
Bounding box:  xmin: -98.2043 ymin: 19.329 xmax: -98.15668 ymax: 19.36419
Geodetic CRS:  WGS 84
First 10 features:
   distance duration id        origen       destino
1    2.5408 30.49000  1 2900100010059 290010001003A
2    2.8488 34.18000  2 2900100010059 2900100010063
3    6.3346 76.01333  3 2900100010059 2900200020075
4    5.2446 62.93667  4 2900100010059 2900100010044
5    2.8488 34.18000  5 2900100010063 2900100010059
6    3.4284 41.14000  6 2900100010063 2900100010082
7    6.6402 79.68000  7 2900100010063 2900200020075
8    3.2508 39.00333  8 2900100010063 2900100010044
9    2.4080 28.89667  9 2900200010022 2900200050060
10   2.5332 30.39667 10 2900200010022 2900200040094
                         geometry
1  LINESTRING (-98.16594 19.34...
2  LINESTRING (-98.16594 19.34...
3  LINESTRING (-98.16594 19.34...
4  LINESTRING (-98.16594 19.34...
5  LINESTRING (-98.16594 19.34...
6  LINESTRING (-98.18093 19.35...
7  LINESTRING (-98.17122 19.35...
8  LINESTRING (-98.16437 19.36...
9  LINESTRING (-98.19237 19.32...
10 LINESTRING (-98.18931 19.33...
Ver código
leaflet() %>% 
  addProviderTiles(providers$CartoDB) %>% 
  addCircleMarkers(data=ageb_tlaxcala) %>% 
  addCircleMarkers(data=muy_poblados,color = "red",opacity = 1) %>% 
  addPolylines(data=conectar,color="black") %>% 
  addPolylines(data=espacial,color="green",opacity = 1)

Geocoding

Convierte direcciones de texto en coordenadas geográficas. Es el primer paso para espacializar bases de datos administrativas: clínicas, escuelas, juzgados, centros de atención a violencia, etc.

Para este ejercicio vamos a usar la Plataforma Nacional de Datos Abiertos los datos de: Bibliotecas abiertas al público: Datos sobre la ubicación de las bibliotecas, nombre, dirección y servicios que se ofrecen.

Ver código
bibliotecas = read.csv("https://repodatos.atdt.gob.mx/api_update/inah/bibliotecas_abiertas_publico/INAH_bibliotecas_abiertas_ok.csv",encoding = "utf8")

glimpse(bibliotecas)
Rows: 71
Columns: 8
$ consecutivo   <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1…
$ siglas        <chr> "AGS", "BC", "BC - CDE", "BCS", "CAMP", "CHIS", "CHIS - …
$ estado        <chr> "Aguascalientes", "Baja California", "Baja California", …
$ nombre        <chr> "Biblioteca del Centro INAH Aguascalientes", "Biblioteca…
$ acceso        <chr> "Público", "Público", "Público", "Restringido", "Público…
$ direccion     <chr> "Calle Juan de Montoro No. 226, Zona Centro, C.P. 20000,…
$ perfil_acervo <chr> "Colecciones del INAH", "Biblioteca y archivos de Baja C…
$ servicios     <chr> "sin dato", "Atención a usuario en sala y asesoría perso…

El data set No contiene coordenadas pero sí contiene dirección así que con geocoding podemos obtener esas coordenadas. Dos opciones: * tidygeocoding * mapbox api

Ejemplo con tidygeocoding

Para no ver tantas columnas me quedaré con 2, consecutivo (como identificador y dirección) este es un ejemplo deuna dirección:

“Calle Juan de Montoro No. 226, Zona Centro, C.P. 20000, Aguascalientes, Aguascalientes.”

Tiene calle, me imagino que colonia, codigo postal, supongo que Municipio y Entidad. Para las funciones hay servicios gratuitos como OpenStretMap (OSM), arcgis y census (este último solo para Estados unidos) , en “method” se puede ajustar. Para este ejemplo le puse “arcgis”

Ver código
library(tidygeocoder)

geo1 = bibliotecas %>%
  select(consecutivo,nombre,direccion) %>% 
  mutate(direccion_completa = paste(direccion, "México")) %>% 
  geocode(address = "direccion_completa",
          method="arcgis",
          lat = "lat",
          long = "lng")
Passing 65 addresses to the ArcGIS single address geocoder
Query completed in: 23.6 seconds

Si harás muchos te recomiendo madar por bloques, tipo primero dame 50, esperar unos segundos otros 50 .. etc en el ejemplo hizo las primeras 654

Ver código
geo1 %>% glimpse()
Rows: 71
Columns: 6
$ consecutivo        <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, …
$ nombre             <chr> "Biblioteca del Centro INAH Aguascalientes", "Bibli…
$ direccion          <chr> "Calle Juan de Montoro No. 226, Zona Centro, C.P. 2…
$ direccion_completa <chr> "Calle Juan de Montoro No. 226, Zona Centro, C.P. 2…
$ lat                <dbl> 21.88128, 32.66284, 31.86422, 24.14290, 19.84560, 1…
$ lng                <dbl> -102.29291, -115.45858, -116.62992, -110.30807, -90…

y magiaaa… ahora ya tenemos las coordenadas.

Ver código
leaflet() %>% 
  addProviderTiles(providers$CartoDB) %>% 
  addCircleMarkers(data=geo1,label=~nombre)
Assuming "lng" and "lat" are longitude and latitude, respectively

Versión con MapBox

mapbox tiene una función llamada mb_batch_geocode que puedes pasarle un dataFrame, pero llega a “romperse” en cas ode que no reconozca una y no decirnos cuál es, así que pasar una por una (como en el ruteo) puede ser buena opción

Tip 2 si por alguna razón uno falla le agregamos un ‘posibly’, le pondremos null y saltará al otro es como una implementación del try_catch pero mucho más sencilla

Ver código
geocodificar_uno_a_uno = function(dir) {
  mb_geocode(
    search_text = dir,
    country     = "MX",
    language    = "ES",
    output      = "sf"
  )
}

geocodificar_safe = possibly(geocodificar_uno_a_uno, otherwise = NULL)

geo2 <- map(bibliotecas$direccion, geocodificar_safe) %>% 
  list_rbind()
Ver código
leaflet() %>% 
  addProviderTiles(providers$CartoDB) %>% 
  addCircleMarkers(data=geo1,label=~nombre,color="black") %>% 
  addCircleMarkers(data=geo2,color="red")
Assuming "longitude" and "latitude" are longitude and latitude, respectively
Assuming "lng" and "lat" are longitude and latitude, respectively

Parte 3: Modelos de lenguaje para análisis espacial

Uso de llm para extraer información de imágenes/fotos

Seguramente te ha pasado que queremos extraer información de mapas ya sea históricos, algun mapa que te gustó y le tomaste una foto o hechos por otras personas pero no tenemos ni el archivo, ni conocemos a la persona o no pudimos contactarla, entonces con ayuda de modelos de lenguaje podemos apoyarnos en extraer la información. POr ejemplo acá hay un mapa (Puedes tomarle foto)

Para usar los modelos de lenguaje en R existen distintas librerías, una que me gusta a mí se llama ellmer , pero antes de iniciar para ocupar modelos de lenguaje tenemos 2 Opciones:

  • Usar modelos pequeños de forma local ollama
  • Usar alguna api, ya sea gratuita o de pago (Acá lo haremos gratis) Ya sea a través de GitHub o de Gemini

Debes guardar las variables en una variable de entorno (puedes hacerlo de forma sencilla con la librería usethis con la función usethis::edit_r_environ()) , se abrirá una ventana y ahí podrás asignar tus variables, una vez que las tengas, las guardarás, cerrarás la pestañas y reiniciarás “R” Ya sea asi usaste algún llm de github o gemini usaremos la palabra chat, (para hacernos la vida un poco facil en este ejercicio ) les llamaremos m1 y m2

Ver código
library(ellmer)

ellmer::models_google_gemini()
                                        id cached_input input output
1        deep-research-pro-preview-12-2025           NA    NA     NA
2                         gemini-2.0-flash      0.02500 0.100    0.4
3                     gemini-2.0-flash-001      0.02500 0.100    0.4
4                    gemini-2.0-flash-lite      0.01875 0.075    0.3
5                gemini-2.0-flash-lite-001           NA    NA     NA
6  gemini-2.5-computer-use-preview-10-2025           NA    NA     NA
7                         gemini-2.5-flash      0.07500 0.300    2.5
8                   gemini-2.5-flash-image           NA    NA     NA
9                    gemini-2.5-flash-lite      0.02500 0.100    0.4
10   gemini-2.5-flash-lite-preview-09-2025      0.02500 0.100    0.4
11            gemini-2.5-flash-preview-tts      0.03750 0.150    0.6
12                          gemini-2.5-pro      0.31250 1.250   10.0
13              gemini-2.5-pro-preview-tts      0.31250 1.250   10.0
14                  gemini-3-flash-preview           NA    NA     NA
15              gemini-3-pro-image-preview           NA    NA     NA
16                    gemini-3-pro-preview           NA    NA     NA
17          gemini-3.1-flash-image-preview           NA    NA     NA
18           gemini-3.1-flash-lite-preview           NA    NA     NA
19                  gemini-3.1-pro-preview           NA    NA     NA
20      gemini-3.1-pro-preview-customtools           NA    NA     NA
21                     gemini-flash-latest      0.07500 0.300    2.5
22                gemini-flash-lite-latest      0.02500 0.100    0.4
23                       gemini-pro-latest           NA    NA     NA
24          gemini-robotics-er-1.5-preview           NA    NA     NA
25                          gemma-3-12b-it           NA    NA     NA
26                           gemma-3-1b-it           NA    NA     NA
27                          gemma-3-27b-it           NA    NA     NA
28                           gemma-3-4b-it           NA    NA     NA
29                         gemma-3n-e2b-it           NA    NA     NA
30                         gemma-3n-e4b-it           NA    NA     NA
31                 nano-banana-pro-preview           NA    NA     NA
Ver código
m2 = chat_google_gemini()
Using model = "gemini-2.5-flash".

Una vez que tenemos nuestro modelo podemos verificar si puede “respondernos”

Ver código
m2$chat("CUentame un chiste breve sobre datos abiertos ")
¡Aquí tienes uno!

¿Cuál es el chiste de los datos abiertos?

Que están... **¡tan abiertos que a veces necesitas un mapa del tesoro para 
encontrarlos!**
Ver código
m2$chat("se breve, ¿Sabes que esel OpenDataDay?")
Sí, es un evento anual que celebra y promueve el uso y el impacto de los datos 
abiertos a nivel mundial.

Ahora sí el vamos a extraer los valores de la imagen.

1.- Necesitamos ya se auna foto/archivo 2.- Una tabla para asignar las variabels 3.- el chat

La imagen del mapa la guardé en un objeto llamado “i2.png”

Ver código
foto = google_upload(path = "imagen/i2.png")

m2 = chat_google_gemini(system_prompt = "Tu objetivo es ayudarme a extraer los valores")
Using model = "gemini-2.5-flash".
Ver código
type_datos = type_object(
id = type_string("Genera un id para cada registro del registro que inicie con 'p_1','p_2','p_3' hasta el limite de registros"),
valor = type_integer("cantidad de valores que observas registrados"),
lat=type_number("integra la latitud del punto valor, la ubicación se encuentra en CDMX Mexico"),
lng=type_number("integra la longitud del punto valor, la ubicación se encuentra en CDMX Mexico")
)

type_final = type_array(items = type_datos)
Ver código
resultado = m2$chat_structured(
  foto,
  type = type_final,
  "Extrae todos las cantidades visibles en el mapa así como sus coordenadas"
)
Ver código
resultado
# A tibble: 18 × 4
   id    valor   lat   lng
   <chr> <int> <dbl> <dbl>
 1 p_1     850  19.5 -99.2
 2 p_2     515  19.5 -99.2
 3 p_3     143  19.5 -99.1
 4 p_4     719  19.5 -99.2
 5 p_5    3838  19.4 -99.2
 6 p_6     401  19.4 -99.1
 7 p_7     284  19.4 -99.1
 8 p_8    2153  19.4 -99.2
 9 p_9     304  19.4 -99.3
10 p_10     10  19.3 -99.3
11 p_11   1280  19.3 -99.2
12 p_12     24  19.3 -99.2
13 p_13    551  19.2 -99.2
14 p_14    950  19.2 -99.2
15 p_15      9  19.2 -99.0
16 p_16    260  19.3 -99.0
17 p_17    561  19.2 -99.1
18 p_18   2278  19.4 -99  
Ver código
leaflet() %>% 
  addProviderTiles(providers$OpenStreetMap) %>% 
  addCircleMarkers(data=resultado,label = ~scales::comma(valor))
Assuming "lng" and "lat" are longitude and latitude, respectively

“Peor es nada” aunque el prompt fue muy ambiguo fue un poco “útil” ¿Qué podemos pasarle para mejorar? el límite de la entidad, un prompt mucho mejor, más detallado etc.. pero como primer acercamiento es útil.

Uso de modelos de lenguaje para información de texto.

¿Te hapasado que quieres extraer información de un texto? estructurarlo, extraer personasjes incluso asignarles características, bueno nos apoyaremos del modelo de lenguaje, Durante el OpenDataDay Hicimos Este ejercicio

La idea es extraer el texto así como tratar de inferir el género de la persona y las coordenadas de donde viene. el documento lo puedes descargar de acá Documento

(por alguna razón le puse de nombre MapBox (1).pdf)

Ver código
archivo = google_upload(path = "imagen/token mapbox (1).pdf")

m2 = chat_google_gemini(system_prompt = "Identifica los nombres en español, asigna el género de la persona, así como sus coordenadas del Archivo")
Using model = "gemini-2.5-flash".
Ver código
type_datos = type_object(
  nombre = type_string("Nombre en español de la persona",required = FALSE),
  genero = type_string("Genero que identifiques asociado al nombre",required = FALSE),
  origen=type_string("Indica de donde proviene la persona",required = TRUE),
  lat=type_number("Obten las latitud del origen de la persona",required = TRUE),
  lng=type_number("Obten la longitud del origen de la persona",required = TRUE)
)

type_final = type_array(items = type_datos)

Ahor ya que tenemos la estructura así como las variables que nos interesan podemos llamar al modelo nuevamente, acá usamos una función llamada “requiered” esta es importante ya que si le pones TRUE el modelo se “esforzará” por devolverte algo, para los que no lo tienen puede dejarlos en NULL en caso de que no identifique la información

Ver código
resultado_final = m2$chat_structured(
  archivo,
  type = type_final,
  "Extrae nombres, genero, origen y sus coordenadas del archivo"
)

nota improtante la clase de valor debe estar asociada al tipo, por ejemplo type_string son palabras, type number números

Ver código
resultado_final
# A tibble: 6 × 5
  nombre      genero    origen                     lat    lng
  <chr>       <chr>     <chr>                    <dbl>  <dbl>
1 Noé         Masculino Ecatepec                  19.6  -99.1
2 Juan Javier Masculino Portales CDMX             19.4  -99.1
3 Yoselin     Femenino  Ciudad Juárez, Chihuahua  31.7 -106. 
4 Javier      Masculino Narvarte                  19.4  -99.2
5 Ana         Femenino  Cancún                    21.2  -86.8
6 Vale        Femenino  Yucatán                   21.0  -89.6

La ventaja de modelos de lenguaje para extraewr información es que podemos ponerles solo una palabra, un sitio y puede asignarle la coordenada.

Ver código
leaflet() %>% 
  addProviderTiles(providers$OpenStreetMap) %>% 
  addCircleMarkers(data = resultado_final,label = ~nombre)
Assuming "lng" and "lat" are longitude and latitude, respectively

Gracias

Perdón la demora en compartir las notas, espero te sea de utilidad.

Dudas,Quejas,retroalimentación, Reclamos y chistes, siempre feliz de escuchar.

Contactos:

Noé Osorio: ecostat.nog@gmail.com / X

Uriel Mendoza: urielmendozacastillo@gmail.com