# Lectura de los paquetes
::p_load(tidyverse, sf, spatstat, stars, wesanderson) pacman
Tutorial: Crear mapas de calor (intensidad) para México
Mapa de calor
En esta entrada aprenderemos a hacer un mapa de calor como el que sigue:
Usaremos los siguientes paquetes para generar el mapa:
Carpetas de investigación de la Ciudad de México
Para el propósito de este análisis utilizaremos las carpetas de investigación de la Fiscalía de la Ciudad de México la cual contiene información “de las víctimas de los delitos en las carpetas de investigación de la Fiscalía General de Justicia (FGJ)”.
# Link de datos de la fiscalía de CDMX
<- paste0("https://datos.cdmx.gob.mx/dataset/7593b324-6010-44f7-",
url "8132-cb8b2276c842/resource/68304227-862f-4b86-8382-e35",
"b317a7c39/download/da_victimas_19-20.csv")
<- read_csv(url, show_col_types = F) df_fiscalia
La base se ve así:
idCarpeta | Año_inicio | Mes_inicio | FechaInicio | Delito | Categoria | Sexo | Edad | TipoPersona | CalidadJuridica | competencia | Año_hecho | Mes_hecho | FechaHecho | HoraHecho | HoraInicio | alcaldia_hechos | municipio_hechos | colonia_datos | fgj_colonia_registro | latitud | longitud |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
numeric | numeric | character | Date | character | character | character | numeric | character | character | character | numeric | character | Date | hms | hms | character | character | character | character | numeric | numeric |
8,324,429 | 2,019 | Enero | 2019-01-04 | FRAUDE | DELITO DE BAJO IMPACTO | Masculino | 62 | FISICA | OFENDIDO | FUERO COMUN | 2,018 | Agosto | 2018-08-29 | 43,200.0 | 44,340.0 | ALVARO OBREGON | GUADALUPE INN | GUADALUPE INN | 19.4 | -99.2 | |
8,324,430 | 2,019 | Enero | 2019-01-04 | PRODUCCIÓN, IMPRESIÓN, ENAJENACIÓN, DISTRIBUCIÓN, ALTERACIÓN O FALSIFICACIÓN DE TÍTULOS AL PORTADOR, DOCUMENTOS DE CRÉDITO PÚBLICOS O VALES DE CANJE | DELITO DE BAJO IMPACTO | Femenino | 38 | FISICA | VICTIMA Y DENUNCIANTE | FUERO COMUN | 2,018 | Diciembre | 2018-12-15 | 54,000.0 | 44,400.0 | AZCAPOTZALCO | VICTORIA DE LAS DEMOCRACIAS | VICTORIA DE LAS DEMOCRACIAS | 19.5 | -99.2 | |
8,324,431 | 2,019 | Enero | 2019-01-04 | ROBO A TRANSEUNTE SALIENDO DEL BANCO CON VIOLENCIA | ROBO A CUENTAHABIENTE SALIENDO DEL CAJERO CON VIOLENCIA | Masculino | 42 | FISICA | VICTIMA Y DENUNCIANTE | FUERO COMUN | 2,018 | Diciembre | 2018-12-22 | 55,800.0 | 44,580.0 | COYOACAN | COPILCO EL BAJO | COPILCO UNIVERSIDAD ISSSTE | 19.3 | -99.2 | |
8,324,435 | 2,019 | Enero | 2019-01-04 | ROBO DE VEHICULO DE SERVICIO PARTICULAR SIN VIOLENCIA | ROBO DE VEHÍCULO CON Y SIN VIOLENCIA | Masculino | 35 | FISICA | VICTIMA Y DENUNCIANTE | FUERO COMUN | 2,019 | Enero | 2019-01-04 | 21,600.0 | 44,820.0 | IZTACALCO | PANTITLAN V | AGRÍCOLA PANTITLAN | 19.4 | -99.1 | |
8,324,438 | 2,019 | Enero | 2019-01-04 | ROBO DE MOTOCICLETA SIN VIOLENCIA | ROBO DE VEHÍCULO CON Y SIN VIOLENCIA | Masculino | FISICA | VICTIMA | FUERO COMUN | 2,019 | Enero | 2019-01-03 | 72,000.0 | 45,300.0 | IZTAPALAPA | LAS AMERICAS (U HAB) | PROGRESISTA | 19.4 | -99.1 | ||
8,324,442 | 2,019 | Enero | 2019-01-04 | PRODUCCIÓN, IMPRESIÓN, ENAJENACIÓN, DISTRIBUCIÓN, ALTERACIÓN O FALSIFICACIÓN DE TÍTULOS AL PORTADOR, DOCUMENTOS DE CRÉDITO PÚBLICOS O VALES DE CANJE | DELITO DE BAJO IMPACTO | Femenino | 42 | FISICA | OFENDIDO | FUERO COMUN | 2,018 | Octubre | 2018-10-12 | 64,800.0 | 45,480.0 | COYOACAN | LOS REYES (PBLO) | PUEBLO DE LOS REYES | 19.3 | -99.2 | |
n: 6 |
Las columnas importantes para este análisis son latitud
y longitud
las cuales refieren a las coordenadas de los eventos. Por ahora utilizaremos sólo las carpetas sobre ROBO A PASAJERO.
<- df_fiscalia |>
df_robos filter(str_detect(Delito, "ROBO A PASAJERO")) |>
distinct(latitud, longitud)
Lo cual nos deja una lista de coordenadas:
latitud | longitud |
---|---|
numeric | numeric |
19.4 | -99.1 |
19.3 | -99.0 |
19.4 | -99.2 |
19.4 | -99.1 |
19.4 | -99.1 |
19.5 | -99.1 |
n: 6 |
Mapa de la CDMX
El mapa de la Ciudad de México lo obtendremos del Instituto Nacional de Estadística y Geografía. En particular éste puede encontrarse en Temas > Marco Geoestadístico
y luego seleccionando el marco más reciente y la entidad elegida (o el Nacional que baja todas las entidades).
Una vez descargado el mapa (o descargándolo con el código a continuación) leemos el archivo con la función read_sf
del paquete sf
(simple features).
# Descargamos del inegi los datos
<- tempfile(fileext = ".zip")
mapa_archivo <- tempdir()
mapa_dir <- paste0("https://www.inegi.org.mx/contenidos/productos/prod_serv",
url_mapa "/contenidos/espanol/bvinegi/productos/geografia/marcogeo/",
"889463770541/09_ciudaddemexico.zip")
download.file(url_mapa, mapa_archivo, method = "curl") #Quizá curl no funcione en windows
# Leemos el mapa
unzip(mapa_archivo, exdir = mapa_dir)
# Leemos los datos
<- read_sf(file.path(mapa_dir,"09_ciudaddemexico","conjunto_de_datos","09mun.shp")) cdmx
El resultado de leer los datos en cdmx
es un tibble
que contiene los polígonos demarcando los municipios de la CDMX. Podemos ver la gráfica de esto con geom_sf
:
ggplot(cdmx) +
geom_sf(aes(geometry = geometry)) +
theme_minimal()
Podemos agregar los puntos dentro de la gráfica (ojo a veces los puntos y las gráficas utilizan diferentes proyecciones que hay que homologar)
ggplot(cdmx) +
geom_sf(aes(geometry = geometry)) +
geom_point(aes(x = longitud, y = latitud), data = df_robos) +
theme_minimal()
Notamos que algunos de los puntos caen lejos de nuestra unidad de análisis (CDMX) por lo que restringimos los puntos a caer dentro de uno de los polígonos de geom_sf
# Convertimos la latitud y longitud a un objeto `simple features` también
# el comando crs especifica el sistema de coordenadas usado (viene en el diccionario)
<- df_robos |>
sf_robos st_as_sf(coords = c("longitud", "latitud"), crs = "WGS84")
# Homologamos los sistemas de coordenadas (el del INEGI tiene otro crs)
<- sf_robos |> st_transform(st_crs(cdmx))
sf_robos
# Intersecamos los robos que ocurrieron en cdmx:
<- sf_robos |> st_intersection(cdmx) sf_robos
Ahora sí ya coinciden:
ggplot() +
geom_sf(aes(geometry = geometry), data = cdmx) +
geom_sf(aes(geometry = geometry), data = sf_robos) + #Ojo aquí cambia a geom_sf
theme_minimal()
Densidad
Se puede ajustar una densidad kernel al proceso puntual. Ésta consiste en dividir el espacio \(\mathcal{B} \subset \mathbb{R}^2\) en ventanas (pixeles) del mismo tamaño (\(V_1, V_2, \dots, V_n\)) y calcular la cantidad de observaciones (\(N(V_1), N(V_2), \dots, N(V_n)\)) en cada uno de los sitios. La densidad conlleva además un proceso de suavizamiento adicional (el mismo que las densidades kernel
de Wasserman (2006) pero bidimensionales) que se encuentra descrito en Ellis (1991).
Para construir la densidad hay que conertir el objeto sf
en un objeto ppp
(poisson point process) de la librería spatstat
:
# Convertimos a proceso puntual (spatstat)
<- as.ppp(sf_robos$geometry, W = as.owin(cdmx))
robos_point_process
# Calculamos la densidad de puntos del proceso
<- density(robos_point_process)
robos_densidad
# Convertimos de vuelta a objeto sf (a través de pointprocess -> stars -> sf)
# https://www.andrewheiss.com/blog/2023/07/28/gradient-map-fills-r-sf
<- robos_densidad |>
densidad_sf st_as_stars() |>
st_as_sf() |>
st_set_crs(st_crs(cdmx)) #Para poner la proyección específica
#Arreglamos los bordes de los pixeles:
<- densidad_sf |> st_intersection(cdmx) densidad_sf
Finalmente graficamos:
ggplot() +
geom_sf(aes(geometry = geometry, fill = v), color = NA, data = densidad_sf) + #Ojo aquí cambia a geom_sf
geom_sf(aes(geometry = geometry), data = cdmx, fill = NA, color = "white", linewidth = 0.25) +
scale_fill_gradientn(guide = "none", colours = wes_palette("Zissou1")) +
theme_void() +
ggtitle("Densidad de robos a pasajeros")
Intensidades (de Papangelou)
La densidad no es la única medida espacial que puede generar mapas de calor. Otra medida de importancia es la intensidad. Todos los procesos de puntos, \(\mathcal{X} = \left\{X_1, X_2, \dots, X_k\right\}\), en el espacio son procesos puntuales. Para nuestros propósitos, la intensidad se define como una función \(\lambda(u, \mathcal{X})\) que depende del proceso (de los puntos en sí, \(\mathcal{X}\)) y de la posición en el espacio \(u\).
En R
podemos ajustar regresiones a los procesos puntuales mediante ppm
para obtener la intensidad. En este caso, por ejemplo, ajusto un polinomio de segundo grado a las coordenadas x
y y
de mi proceso:
<- ppm(robos_point_process, trend = ~ polynom(x,y,2), Poisson())
fit <- predict(fit) intensidad
Sin embargo se pueden agregar otras covariables espaciales (por ejemplo altura ó fecha), así como cambiar la familia del proceso (ver por ejemplo Strauss()
y otras ?pairwise.family
).
Una vez estimada la intensidad se convierte de nuevo en un objeto sf
y se grafica:
# Convertimos de vuelta a objeto sf (a través de pointprocess -> stars -> sf)
# https://www.andrewheiss.com/blog/2023/07/28/gradient-map-fills-r-sf
<- intensidad |>
intensidad_sf st_as_stars() |>
st_as_sf() |>
st_set_crs(st_crs(cdmx)) #Para poner la proyección específica
#Arreglamos los bordes de los pixeles:
<- intensidad_sf |> st_intersection(cdmx)
intensidad_sf
ggplot() +
geom_sf(aes(geometry = geometry, fill = v), color = NA, data = intensidad_sf) + #Ojo aquí cambia a geom_sf
geom_sf(aes(geometry = geometry), data = cdmx, fill = NA, color = "white", linewidth = 0.25) +
scale_fill_gradientn(guide = "none", colours = wes_palette("Zissou1")) +
theme_void() +
ggtitle("Intensidad de robos a pasajeros")
Despedida
Si en algún momento deseas un artículo de más profundidad sobre procesos puntuales espaciales ¡házmelo saber!