Si eres estudiante de la Escuela de Data Science , sabes que esta es una disciplina muy completa donde cada herramienta que sumes a tu stack cuenta, también sabes que parte del trabajo del Data Scientist es contar una historia, estas historias son las que permiten comunicar de forma más efectiva nuestros insights y análisis a las personas que integran nuestra organización o nuestra comunidad, y que mejor forma de comunicar datos de nuestro entorno o nuestra población que referenciarlos en su representación cartográfica, es aquí donde la librería Plotly entra en escena.
Plotly es una poderosa librería que nos permite realizar gráficos interactivos directamente en nuestros notebooks de jupyter o directamente en Colab, en nuestro caso utilizaremos el módulo Choropleth Map, este crea un mapa compuesto de polígonos con colores representando la variación de los datos numéricos de una columna perteneciente a un DataFrame en una escala y paleta de colores que vemos en nuestra imagen.
Ya hablamos de que esta visualiacion utiliza poligonos para la mascara de colores, por lo que toca hablar del formato GeoJson, este es un formato estándar abierto diseñado para representar elementos geográficos sencillos, junto con sus atributos no espaciales, basado en JavaScript Object Notation.
Aquí tu primer ejemplo de datos GeoJson, pero por favor usa tus lentes de Pythonista y míralo como un simple diccionario con una llave “features” cuyo valor es una lista que contiene otros diccionarios, cada diccionario dentro de esa lista poseerá tres llaves a su vez “geometry” para las coordenadas y el tipo de dato de las coordenadas (Point, LineString, Polygon, Multipoligon, etc.), “id” identificador único para referenciar esa zona geográfica y “properties” para atributos no espaciales, como el nombre de la zona, la superficie u otros datos de interes.
{"features":
[{"geometry": {"coordinates": [[[-86.577799, 33.765316],
[-86.759144, 33.840617],
[-86.953664, 33.815297],
[-86.954305, 33.844862],
[-86.96296, 33.844865],
[-86.963358, 33.858221],
[-86.924387, 33.909222],
[-86.793914, 33.952059],
[-86.685365, 34.05914],
[-86.692061, 34.092654],
[-86.599632, 34.119914],
[-86.514881, 34.25437],
[-86.45302, 34.259317],
[-86.303516, 34.099073],
[-86.332723, 33.986109],
[-86.370152, 33.93977],
[-86.325622, 33.940147],
[-86.377532, 33.861706],
[-86.577528, 33.801977],
[-86.577799, 33.765316]]],
"type": "Polygon"},
"id": "01009",
"properties": {"CENSUSAREA": 644.776,
"COUNTY": "009",
"GEO_ID": "0500000US01009",
"LSAD": "County",
"NAME": "Blount",
"STATE": "01"},
"type": "Feature"},
# Poligonos de zonas adicionales
{{"geometry": ...},
{{"geometry": ...},
{{"geometry": ...},]
}
Ahora que sabes un poco más sobre datos Georreferenciados vamos al código, como todos en Platzi, te animo a usar Google Colab, pero si quieres hacer el ejercicio localmente está bien, solo omite la primer sección, de las cinco que conforman el tutorial.
# Import para tener acceso a nuestro google drivefrom google.colab import drive
drive.mount('/content/drive')
%cd 'drive/My Drive/geodatos'
Nuestro ejemplo utilizara la API **Nominatim** la cual es el motor de busqueda para OpenStreetMap en este punto debo acalarar que no estamos haciendo web scrapping a los datos, solo usamos su API, a continuarcion detallo los pasos para que los agregues en tu notebook, obviando que usas Colab.
import requests # Peticiones httpimport json # Manejar archivos json
regions = {
'1': 'AGUASCALIENTES', '2': 'BAJA CALIFORNIA', '3': 'BAJA CALIFORNIA SUR',
'4': 'CAMPECHE', '5': 'COAHUILA', '6': 'COLIMA', '7': 'CHIAPAS', '8': 'CHIHUAHUA',
'9': 'CIUDAD DE MEXICO', '10': 'DURANGO', '11': 'GUANAJUATO', '12': 'GUERRERO',
'13': 'HIDALGO', '14': 'JALISCO', '15': 'ESTADO DE MEXICO',
'16': 'MICHOACAN DE OCAMPO', '17': 'MORELOS', '18': 'NAYARIT', '19': 'NUEVO LEON',
'20': 'OAXACA', '21': 'PUEBLA', '22': 'QUERETARO', '23': 'QUINTANA ROO',
'24': 'SAN LUIS POTOSI', '25': 'SINALOA', '26': 'SONORA', '27': 'TABASCO',
'28': 'TAMAULIPAS', '29': 'TLAXCALA', '30': 'VERACRUZ DE IGNACIO DE LA LLAVE',
'31': 'YUCATAN', '32': 'ZACATECAS'
}
_get_region_data(state, country)
.def_get_region_data(state, country):
'''Get GeoJson Data from OpenStreetMap API'''
response = requests.get(f'https://nominatim.openstreetmap.org/search.php?q={state}%2C{country}&polygon_geojson=1&format=jsonv2')
region_data = response.json()[0]
#Obtenemos el nombre de la region
region_name = region_data['display_name']
# obtenemos las coordenadas
coordinates = region_data['geojson']
return region_name, coordinates
Esta se encarga de hacer la peticion http a la API, hace un parse de la data tipo Json y obtenemos el primer elemento , ya que la API nos ofrece mas coincidencias relacionadas a la consulta, este elemento es un diccionario, del que extraemos 2 atributos region_name y coordinates.
Pruébalo así
region_name, coordinates = _get_region_data("VERACRUZ", "MEXICO")
build_geographical_data(regions, country)
, esta función utiliza internamente a _get_region_data(state, country)
, y agrupa los datos como un diccionario con la estructura del formato GeoJson, para después hacer append a este diccionario a la lista de “features” del diccionario principal y al final retorna el diccionario como geodatadefbuild_geographical_data(regions, country):
'''Builds a dictionary using GeoJson format'''
geodata = {"features": []}
for key, region in regions.items():
region_name, coordinates = _get_region_data(region, country)
data = {
"geometry": coordinates,
"id": f"{key}",
"properties": {
"GEO_ID": f"{key}{country}",
"FULLNAME": f"{region_name.upper()}",
"SHORTNAME": f"{region}",
"STATE": f"{key}"},
"type": "Feature"
}
geodata["features"].append(data)
return geodata
def_save_geodata(geodata_mx):
'''Saves geodata as geojson file'''with open('geodata_mx.geojson', 'w') as fp:
json.dump(geodata_mx, fp, indent=4)
if name == "main":
y definimos nuestra funcion main()
para orquestar las funciones anteriores, tu codigo debe verse mas o menos asi.defmain(regions, country):
geodata_mx = build_geographical_data(regions, country)
_save_geodata(geodata_mx)
defbuild_geographical_data(regions, country):
'''Builds a dictionary using GeoJson format'''
geodata = {"features": []}
for key, region in regions.items():
region_name, coordinates = _get_region_data(region, country)
data = {
"geometry": coordinates,
"id": f"{key}",
"properties": {
"GEO_ID": f"{key}{country}",
"FULLNAME": f"{region_name.upper()}",
"SHORTNAME": f"{region}",
"STATE": f"{key}"},
"type": "Feature"
}
geodata["features"].append(data)
return geodata
def_get_region_data(state, country):
'''Get GeoJson Data from OpenStreetMap API'''
response = requests.get(f'https://nominatim.openstreetmap.org/search.php?q={state}%2C{country}&polygon_geojson=1&format=jsonv2')
region_data = response.json()[0]
#Obtenemos el nombre de la region
region_name = region_data['display_name']
# obtenemos las coordenadas
coordinates = region_data['geojson']
return region_name, coordinates
def_save_geodata(geodata_mx):
'''Saves geodata as geojson file'''with open('geodata_mx.geojson', 'w') as fp:
json.dump(geodata_mx, fp, indent=4)
if __name__ == "__main__":
# main orquesta obtener, procesar y guardar la data en disco# la data no se guarda en memoria, posteior minifica poligonos
main(regions, country='MEXICO')
Como observas en nuestro Entry Point no guardamos los datos en una variable, nos limitamos a guardarlos en disco, para hacer un proceso de mitificación de los polígonos en la siguiente sección, esto es necesario para reducir el uso de memoria y CPU durante nuestro computo al tener un mapa tan detallado y quedarnos solo con los las coordenadas necesarias para mantener la forma de los polígonos, algo así como PlayStation 5 vs Nintendo 64.
La data obtenida pesa aproximadamente 85mb algo poco práctico, ya que como mencionaba obtuvimos todas las coordenadas disponibles para crear los polígonos en OpenstreetMap, el siguiente paso es minificarlo utilizando una herramienta disponible para node.js llamada mapshaper. (Sí aún no sabes nada de Javascript te aconsejo tomar el curso de Fundamentos de JavaScript y el Curso de Fundamentos de Node.js)
Si no estas utilizando Colab, es buen momento para que descargues e instales Node.JS , ya que la instancia de nuestra maquina virtual en Google Colab ya lo tiene instalado por defecto, ahora la instalación de mapshaper en el mismo notebook usando el signo de explanación !
para acceder a los comandos del sistema operativo como si fuese la terminal.
!npm install -g mapshaper
Con el siguiente comando mapshaper reduce el 90% de las coordenadas de cada poligono, pero manteniendo la forma de los mismos.
# Archivo .geojson
!mapshaper geodata_mx.geojson -simplify dp 10% keep-shapes -o format=geojson geodata_mx_minified.geojson
Obtenemos el contenido minificado ahrora en formato Json
# Archivo .json
!mapshaper geodata_mx.geojson -simplify dp 10% keep-shapes -o format=geojson geodata_mx_minified.json
En este punto te invito a subir ambos archivos a un repositorio en tu GitHub y dentro de la página observar la diferencia entre ambos archivos a la hora de visualizarlos (trustu me 😉).
En este punto usaremos las regiones que teníamos definidas al inicio, recuerda que nuestro GeoJson tiene la llave id y esta nos va a permitir enlazar nuestras coordenadas con el Estado (región) de México.
import pandas as pdimport numpy as np
df_example = pd.DataFrame.from_dict(regions, orient='index', columns=['estado']).reset_index()
df_example.columns = ['id','estado']
df_example.head()
Ahora creamos una columna adicional al DataFrame, a la cual para este ejemplo agregaremos números aleatorios en un rango del 1 al 10 con la funcion np.random.randint(min, max)
, estos serán los datos numéricos que nuestra visualización va a interpretar.
# Creamos la columna nivel_felicidad_habitantes con valores random
df_example['nivel_felicidad_habitantes'] = [np.random.randint(1, 10) for data in df_example['id']]
df_example.tail()
Instalamos la primer dependencia, ya que Colab no la tiene por defecto.
!pip install plotly
Agregamos las dependencias a la seccion 5 de nuestro proyecto
import jsonimport plotly.express as px
Ahora, procedemos a cargar los datos de nuestro archivo con la data geográfica, en este caso tenemos dos opciones, consumir la data desde nuestro repositorio remoto en GitHub en modo raw
# recuerda previamente importamos la libreria requestsurl = 'https://raw.githubusercontent.com/rb-one/geodata_mx/main/geodata_mx_minified.json'geodata_mx = requests.get(url).json()
O consumir el archivo geodata_mx_minified.json que tenemos en disco local.
geodata_mx_local = json.loads(open('geodata_mx_minified.json').read())
# Dataframenuestro y geodata_mx tienen comparten "id" como dato en comun para referenciar las regiones fig = px.choropleth_mapbox(df_example, # DataFrame a referenciargeojson=geodata_mx,# Origen Datos Geograficos para aplicar mascara de color desde Githublocations='id',# Dato comun entre Dataframe y geodata_mx.json color='nivel_felicidad_habitantes',# Columna datos numericos para referenciar escalacolor_continuous_scale="Viridis",# En adalante Parametros para customizar visualizacion range_color=(0,10),
mapbox_style="carto-positron",zoom=3,center = {"lat": 19.408351, "lon": -99.155119},
opacity=0.5,labels={'nivel_felicidad_habitantes':'felicidad x1000 habitantes'}
)
fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0})
fig.show()
# En caso de no usar un repositorio remoto puedes cambiar # el parametro geojson de geodata_mx a geodata_mx_local# Prueba las opciones para color_contiuous_scale# Greys, YlGnBu, Greens, YlOrRd, Bluered, RdBu, Reds, Blues, Picnic, Rainbow, # Portland, Jet, Hot, Blackbody, Earth, Electric, Viridis, Cividis.
Obtenemos el resultado final de nuestro tutorial, donde no solamente tenemos una visualización estática, sino que podemos interactuar con el mapa, darle zoom, seleccionar áreas de interés o descargar las imágenes.
Con esto termina nuestro tutorial, ahora tienes los conocimientos de una nueva herramienta para crear impactantes visualizaciones con tus datos y los conocimientos que de la Escuela de Data Science, ahora salta a hacer un proyecto personal, integra lo aprendido con el curso de Curso de Fundamentos de Estadística y Análisis de Datos con Python para continuar practicando, y por último, pero no menos importante ¡Nunca Pares de Aprender!
Excelente Aporte
Muchas Gracias!!!