Recuadros para mapas en Geopandas
Cómo hacer distintos tipos de recuadros para mapas hechos con Geopandas
- Introducción
- Datos
- Recuadro de acercamiento
- Recuadro de contexto
- Escalas
- Recuadro de áreas no contiguas
- Revisa otras entradas de este blog:
Introducción
Con la reciente publicación de los resultados del censo 2020 de INEGI varios usuarios han estado presentando algunas gráficas en redes sociales con los principales resultados. Una de las visualizaciones más populares son los mapas coropléticos, especialmente a nivel municipal. Una ventaja de estos mapas es que nos permiten tener el panorama general de todo el país e incluso detectar a grandes rasgos algunos patrones regionales. Por ejemplo, acá @claudiodanielpc nos muestra el hacinamiento en los hogares:
Siguiendo con los microdatos del #Censo2020Mx, hice este mapa en #rstats sobre el hacinamiento en cada municipio.
— Claudio Daniel Pacheco-Castro (@claudiodanielpc) March 22, 2021
Gracias al Dr. @SantaellaJulio y al gran equipo del @INEGI_INFORMA por brindarnos información para el análisis de los problemas de nuestro país. pic.twitter.com/K9zhJkSmEt
Sin embargo, un inconveniente es que hay casos de regiones donde hay un gran número de municipios en una superficie pequeña lo que dificulta la lectura de la información. Un caso común en México es el área compuesta por los estados de Oaxaca, Puebla, Chiapas, Veracruz y Estado de México que contienen 1248 municipios, poco más de la mitad del total de 2469 municipios. Muchos mapas se vuelven difícil de ver en esta zona. Por ejemplo:
Para tratar de hacer más claras este tipo de visualizaciones podemos agregar uno o varios recuadros con acercamiento (zoom) a los resultados de estas zonas, tal como lo hace el mapa del ejemplo. Si bien eso no garantiza que toda la información se entenderá mejor, al menos puede ayudar en ciertas áreas. En esta entrada veremos cómo hacer varios tipos de recuadros para mapas en Python usando Geopandas y Matplotlib.
Empecemos importando las librerías necesarias.
import os
import sys
import geopandas as gpd
import matplotlib.pyplot as plt
import pandas as pd
import requests
import matplotlib_scalebar
from matplotlib_scalebar.scalebar import ScaleBar
print('Python', sys.version)
print(pd.__name__, pd.__version__)
print(gpd.__name__, gpd.__version__)
print(requests.__name__, requests.__version__)
print(plt.matplotlib.__name__, plt.matplotlib.__version__)
print(matplotlib_scalebar.__name__, matplotlib_scalebar.__version__)
Datos
Voy a usar una versión del mapa de México algo simplificada que pesa relativamente poco. Este shapefile puede descargarse aquí.
Leemos el archivo, asignamos la clave municipal de INEGI como el índice del GeoDataFrame y además reproyectamos las coordenadas usando el sistema de coordenadas Mexico ITRF92 / UTM zone 12N
mx = gpd.read_file('mapa_mexico/')\
.set_index('CLAVE')\
.to_crs(epsg=4485)
mx.head()
Al GeoDataFrame anterior vamos a añadirle datos para que podamos graficar un mapa coroplético. Usaremos la API de DataMéxico para obtener los datos del Índice de Complejidad Económica (ECI). La propia definición del ECI de DataMexico es:
El Índice de Complejidad Económica (ECI) mide las capacidades productivas de una localidad (e.g. estado o municipalidad) a partir de la presencia de actividades (e.g. empleo, industrias o exportaciones) en esa y otras localidades. La complejidad económica de una localidad predice su nivel de ingreso, crecimiento económico, desigualdad, y emisiones de gases de efecto invernadero.
En general, un ECI más alto indica un mayor nivel de sofisticación en los productos que se producen en un municipio. Así se ven los datos:
url = 'https://api.datamexico.org/tesseract/cubes/complexity_eci/aggregate.jsonrecords?cuts%5B%5D=Latest.Latest.Latest.1&drilldowns%5B%5D=Geography+Municipality.Geography.Municipality&drilldowns%5B%5D=Date+Day.Date.Year&measures%5B%5D=ECI&parents=false&sparse=false'
data_eci = requests.get(url).json()
eci = pd.DataFrame(data_eci['data'])\
.assign(CLAVE=lambda x: x['Municipality ID'].astype(str).str.zfill(5))\
.set_index('CLAVE')
eci.head()
El índice está estandarizado con una media de 0 y varianza de 1 y tiene un rango entre -1.4 y 4.9.
eci['ECI'].describe()
Revisamos su distribución:
eci['ECI'].hist(bins=100)
Ahora anexamos los datos del ECI al GeoDataFrame:
mx = mx.join(eci, how='left')
mx.head()
Creamos un par de GeoDataFrames que nos servirán más adelante:
-
oax
contiene los datos solo para el estado de Oaxaca -
edos
es un GeoDataFrame con los polígonos de los estados del país.
oax = mx.query('CVE_EDO=="20"')
edos = mx.dissolve(by='CVE_EDO')
Y ahora visualicemos el ECI con un esquema de cortes por quintiles:
fig, ax = plt.subplots()
mx.plot(column='ECI', legend=True, scheme='quantiles', k=5, cmap='viridis_r', ax=ax)
fig.set_size_inches(12, 8)
Como se puede ver, algunas zonas del centro y sur del país son difíciles de distinguir por el gran número de municipios que hay.
Recuadro de acercamiento
Vamos a añadir un recuadro de acercamiento o detalle a la zona sur, señalando principalmente los municipios del estado de Oaxaca. Para eso usaremos la función inset_axes
de Matplotlib. Esta función nos permite añadir un nuevo objeto axes
a nuestra gráfica en cualquier parte del gráfico. La función recibe una lista con 4 valores que son: $[x, y, w, h]$:
- $x$: posición en el eje x de la esquina inferior izquierda del recuadro
- $y$: posición en el eje y de la esquina inferior izquierda del recuadro
- $w$: ancho del recuadro
- $h$: altura del recuadro
Todos estos valores vienen expresados como proporción del tamaño de la gráfica (proporción del ancho para el eje x, proporción del alto para el eje y). Veamos un ejemplo:
fig, ax = plt.subplots()
axins = ax.inset_axes([1.1, 0.5, 0.4, 0.4])
axins.set(xlim=(0.5, 0.7), ylim=(0.4, 0.6))
ax.indicate_inset_zoom(axins)
Lo que hicimos fue añadir una figura (fig
) y un objeto axes
principal (ax
). Luego añadimos el recuadro axins
y limitamos el rango del eje $x$ e $y$ en los que se enfoca el recuadro. Por último, le indicamos al recuadro cuál es la gráfica principal sobre la que esta haciendo el zoom. Lo que falta es dibujar sobre los objetos ax
y axins
.
x = [0, 0.5, 0.6, 0.8]
y = [0, 0.4, 0.5, 0.3]
ax.plot(x, y, color='C0')
axins.plot(x, y, color='C0')
fig
Aunque es relativamente fácil añadir un recuadro a una gráfica, es un poco más difícil añadir el recuadro a un mapa. La dificultad adicional está en que en el resultado final los parámetros $x$, $y$, $w$ y $h$ no se comportan exactamente como sería esperado, sino que por algún motivo el recuadro queda en una posición un poco diferente. No podría asegurarlo, pero me parece que esto pasa porque al graficar un mapa Matplotlib modifica el tamaño de la gráfica y su relación de aspecto para ajustarlo a la información geográfica. La solución (muy poco satisfactoria) es jugar con varios valores de $x$, $y$, $w$ y $h$ hasta que la posición sea la adecuada.
En el siguiente ejemplo vamos a acercarnos al estado de Oaxaca con un recuadro usando los parámetros $x=0.82, y=0.05, w=0.9, h=0.9$ y poniendo como límite al eje x el rango (1820000, 2350000) y al eje y el rango (1780000, 2150000). Los valores (y su unidad de medida) de los ejes X e Y vienen dadas por el sistema de coordenadas geográficas que usamos.
fig, ax = plt.subplots()
# Añade recuadro
axins = ax.inset_axes([0.82, 0.05, 0.9, 0.9])
# Gráfica principal
mx.plot(column='ECI', legend=True, ax=ax, scheme='quantiles', k=5, cmap='viridis_r', legend_kwds={'loc': 'lower left'})
mx.boundary.plot(lw=0.05, color='k', ax=ax)
# Gráfica recuadro
mx.plot(column='ECI', legend=False, ax=axins, scheme='quantiles', k=5, cmap='viridis_r')
mx.boundary.plot(lw=0.25, color='k', ax=axins)
edos.boundary.plot(lw=1, color='red', ax=axins)
# limita área a mostrar
axins.set(ylabel='', xlabel='', xlim=(1820000, 2350000), ylim=(1780000, 2150000), xticks=[], yticks=[])
# Establece líneas del recuadro a la gráfica principal
ax.indicate_inset_zoom(axins)
# Tamaño de la gráfica final
fig.set_size_inches(12, 8)
El acercamiento resulta bastante bueno, ahora es posible identificar mejor los valores del ECI para el estado de Oaxaca y otros vecinos. Podemos añadir un segundo recuadro para ver los municipios del industrial estado de Guanajuato. El proceso es el mismo que el anterior, aunque ahora demorará más porque debe graficar más elementos.
fig, ax = plt.subplots()
# Añade recuadro 1
axins = ax.inset_axes([0.86, 0.05, 0.9, 0.9])
# Añade recuadro 2
axins2 = ax.inset_axes([0.2, -0.5, 0.5, 0.5])
# Gráfica principal
mx.plot(column='ECI', legend=True, ax=ax, scheme='quantiles', k=5, cmap='viridis_r', legend_kwds={'loc': 'lower left'})
mx.boundary.plot(lw=0.05, color='k', ax=ax)
# Gráfica recuadro 1
mx.plot(column='ECI', legend=False, ax=axins, scheme='quantiles', k=5, cmap='viridis_r')
mx.boundary.plot(lw=0.25, color='k', ax=axins)
edos.boundary.plot(lw=1, color='red', ax=axins)
# Gráfica recuadro 2
mx.plot(column='ECI', legend=False, ax=axins2, scheme='quantiles', k=5, cmap='viridis_r')
mx.boundary.plot(lw=0.25, color='k', ax=axins2)
edos.boundary.plot(lw=1, color='red', ax=axins2)
# limita área a mostrar recuadro 1 y 2
axins.set(ylabel='', xlabel='', xlim=(1820000, 2350000), ylim=(1780000, 2150000), xticks=[], yticks=[], title='Oaxaca')
axins2.set(ylabel='', xlabel='', xlim=(1425000, 1680000), ylim=(2229203, 2453767), xticks=[], yticks=[], title='Guanajuato')
# Elimina marco de la gráfica principal
ax.set_axis_off()
# Establece líneas de los recuadros a la gráfica principal
ax.indicate_inset_zoom(axins)
ax.indicate_inset_zoom(axins2)
# Tamaño de la gráfica final
fig.set_size_inches(12, 8)