20

Crea impactantes visualizaciones en tus notebooks con python y plotly

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.

newplot (1).png

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.


Sección 1: Configuracion inicial Colab

  1. Crea en root de tu Google Drive el directorio “geodatos”
# Import para tener acceso a nuestro google drivefrom google.colab import drive
drive.mount('/content/drive')

  1. Accedemos a nuestro directorio usando el magic command %cd
%cd 'drive/My Drive/geodatos'

Sección 2: API - Obtener Coordenadas Geograficas de las regiones

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.

  1. Importamos las dependencias necesarias
import requests # Peticiones httpimport json # Manejar archivos json
  1. Definimos un diccionario con los Estados (regiones) que conforman México, para posterior hacer un for loop y obtener los datos de forma ordenada.
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'
}

  1. Crearemos las funciones segun su orden de necesidad y puesdas probar tus resultados de forma inmediata y posterior veremos un bloque que contenga toda la parte 2 del tutorial, la primer funcion es _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")

  1. Una vez definida la forma de acceder a los datos de la API, creamos la función 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 geodata
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

  1. Definimos una funcion para guardar nuestro diccionario en formato json, en este caso con extension .geojson
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)

  1. Finalmente al segundo bloque de nuestro notebook le agregamos el Entry point 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.


Sección 3: Optimización del archivo geojson

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 😉).


Sección 4: Crea un Dataframe de ejemplo

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()

pd_1.png

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()

pd_2.png

Sección 5: Configuramos la visualizacion choropleth_mapbox de plotly

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.
newplot (1).png

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.

newplot (2).png

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!

Escribe tu comentario
+ 2
1
23333Puntos
5 años

Excelente Aporte

Muchas Gracias!!!