Abrir en Colab

Introducción a los datos de GLOBE#

Examinaremos dos conjuntos de datos de GLOBE: Mosquito Habitat Mapper y Land Cover.

Puedes consultar un panel interactivo con estos datos.

Puedes ejecutar el siguiente código en Google Colab, que funciona en tu navegador y no requiere instalaciones.

Primero, cargaremos los paquetes de Python, que nos ofrecen más opciones para trabajar con el código.

import pandas as pd                           # Para trabajar con datos
pd.set_option("display.max_columns", None)    # Permite ver todas las columnas de los datos, en lugar de solo una vista previa
import geopandas as gpd                       # Para trabajar con datos espaciales
import numpy as np                            # Para trabajar con números
import matplotlib.pyplot as plt               # Para crear gráficos
from datetime import date                     # Para dar formato a las fechas
from PIL import Image                         # Para obtener y mostrar imágenes desde enlaces
import requests                               # Para obtener información desde enlaces
from io import BytesIO                        # Para trabajar con distintos tipos de entrada y salida
end_date = "2024-12-31"

Mosquito Habitat Mapper#

Utilicemos la API de GLOBE, que nos permite obtener los datos directamente sin necesidad de descargar nada.

data = gpd.read_file(f"https://api.globe.gov/search/v1/measurement/?protocols=mosquito_habitat_mapper&datefield=measuredDate&startdate=2018-01-01&enddate={end_date}&geojson=TRUE&sample=FALSE")

Si aparece el error NameError: name 'gpd' is not defined, ve a la parte superior de este cuaderno y haz clic en la flecha junto al primer bloque de código, que comienza con import geopandas as gpd. Esto cargará los paquetes necesarios para ejecutar el resto del código.

Observa las primeras 10 filas de los datos. Estas corresponden a las contribuciones recopiladas más recientemente y enviadas mediante la aplicación GLOBE Observer.

data.head(10)

Utilicemos la función info() para obtener más información sobre el contenido del conjunto de datos.

data.info()

Aquí vemos información sobre cada columna, cuántas filas contienen valores no nulos —es decir, valores que no faltan— y el tipo de datos. Un tipo object generalmente indica que el contenido es texto. También observamos una columna datetime, que resulta útil para analizar los datos a lo largo del tiempo. La columna geometry proporciona información sobre el lugar donde la persona participante recopiló los datos sobre mosquitos.

Sin embargo, algunas columnas están almacenadas como object cuando necesitamos que se almacenen como float —números decimales— o int —números enteros—. Además, cuando falta un valor en algunas filas, aparece la palabra null, que podría interpretarse como un valor real. La reemplazaremos con NaN, que Python interpreta como un valor vacío en lugar de texto.

Nota: La columna mosquitohabitatmapperLarvaeCount contiene números y también intervalos, como 1-25. Para almacenar estos datos de forma más sencilla y coherente, necesitamos convertir cada intervalo en un solo número. Cuando aparezca un intervalo, lo reemplazaremos por su valor central.

# Eliminar "mosquitohabitatmapper" del inicio de los nombres de las columnas para que sean más fáciles de leer
new_column_names = data.columns.str.replace("mosquitohabitatmapper", "")

# Escribir con mayúscula la primera letra de cada nombre de columna, excepto geometry, para mantener la coherencia
new_column_names = [name[0].upper() + name[1:] if name != 'geometry' else name for name in new_column_names]

data.columns = new_column_names
data.info()
# Agregar una columna nueva con la fecha, sin incluir la hora
data['MeasuredDate'] = data['MeasuredAt'].dt.date
# La columna LarvaeCount contiene algunos intervalos, como '1-25', que reemplazaremos por su valor central
data['LarvaeCountProcessed'] = data['LarvaeCount'].replace({
    '1-25': 13,
    '26-50': 38,
    '51-100': 76,
    'more than 100': 100,
    'null': np.nan
})

# Si LarvaeCount es muy largo —más de 10 caracteres—, probablemente se trate de un error; lo reemplazaremos con NaN
data.loc[data['LarvaeCountProcessed'].str.len() > 10, 'LarvaeCountProcessed'] = np.nan

numeric_cols = ['LarvaeCountProcessed', 'MeasurementLatitude', 'MeasurementLongitude']
data[numeric_cols] = data[numeric_cols].apply(pd.to_numeric)

# Eliminar la columna MosquitoEggCount porque todos sus valores son nulos
data = data.drop(columns=['MosquitoEggCount'])

# Reemplazar la palabra 'null' con NaN para que se almacene como un valor vacío en lugar de texto
data = data.replace('null', np.nan)

data.info()

El último paso de limpieza de datos que realizaremos por ahora consiste en eliminar los puntos con coordenadas no válidas. En ocasiones, la latitud y la longitud se registran incorrectamente y colocan un punto en medio del océano. Queremos eliminar cualquier punto que no se encuentre sobre tierra. Para hacerlo, cargaremos un archivo público con los límites de los países. Los límites nacionales —sin incluir la Antártida— se descargaron de ArcGIS Data and Maps.

countries = gpd.read_file('https://github.com/geo-di-lab/emerge-lessons/raw/refs/heads/main/docs/data/world_countries.zip')[['COUNTRY', 'geometry']].to_crs(4326)
countries.plot()

Usaremos una unión espacial, o sjoin, para obtener todos los datos que se “intersectan” con la capa de países. Esto significa que cada punto se encuentra dentro de un país o sobre uno de sus límites.

data = gpd.sjoin(data, countries, how="inner", predicate='intersects') \
          .drop(columns=['index_right', 'COUNTRY']) \
          .reset_index(drop=True)
data

Hay una copia de este conjunto de datos en GitHub, y la utilizaremos en capítulos posteriores. No necesitas descargar el conjunto de datos en tu computadora, ya que lo cargaremos directamente mediante el enlace.

Explorar los datos#

Creemos un gráfico sencillo que muestre la cantidad de contribuciones enviadas por participantes a lo largo del tiempo.

data_by_day = data[['SiteId', 'MeasuredDate']].groupby(['MeasuredDate'], as_index=False).count()

plt.figure(figsize=(8,4))
plt.plot(data_by_day['MeasuredDate'], data_by_day['SiteId'])
plt.xlabel("Fecha")
plt.ylabel("Contribuciones diarias")
plt.title("Contribuciones diarias a Mosquito Habitat Mapper a lo largo del tiempo (2018-2025)")
plt.show()

Ahora representemos la cantidad de contribuciones por país:

data_by_county = data[['SiteId', 'CountryName']].groupby(['CountryName'], as_index=False).count().sort_values(by='SiteId').tail(10)

plt.barh(data_by_county['CountryName'], data_by_county['SiteId'])
plt.xlabel("Contribuciones totales")
plt.ylabel("País")
plt.title("Contribuciones a Mosquito Habitat Mapper por país (2018-2025)")
plt.show()

Land Cover#

Ahora revisaremos el conjunto de datos Land Cover. Al igual que antes, obtendremos los datos mediante la API de GLOBE y utilizaremos el mismo intervalo de fechas que empleamos para el conjunto de datos de mosquitos.

data = gpd.read_file(f"https://api.globe.gov/search/v1/measurement/?protocols=land_covers&datefield=measuredDate&startdate=2018-01-01&enddate={end_date}&geojson=TRUE&sample=FALSE")

Consulta la lista de columnas del conjunto de datos:

data.info()
# Eliminar "landcovers" del inicio de los nombres de las columnas para que sean más fáciles de leer
new_column_names = data.columns.str.replace("landcovers", "")

# Escribir con mayúscula la primera letra de cada nombre de columna, excepto geometry, para mantener la coherencia
new_column_names = [name[0].upper() + name[1:] if name != 'geometry' else name for name in new_column_names]

data.columns = new_column_names
data.info()
# Agregar una columna nueva con la fecha
data['MeasuredDate'] = data['MeasuredAt'].dt.date

data.head(10)

Reemplaza los valores nulos:

data['FieldNotes'] = data['FieldNotes'].replace('(none)', np.nan)
data = data.replace('null', np.nan)
data.info()

Al igual que con el conjunto de datos de mosquitos, filtraremos los puntos que no se encuentren dentro de los límites de un país. Cuando un punto aparezca sobre el agua, asumiremos que sus coordenadas se registraron incorrectamente y lo eliminaremos del conjunto de datos final.

data = gpd.sjoin(data, countries, how="inner", predicate='intersects') \
          .drop(columns=['index_right', 'COUNTRY']) \
          .reset_index(drop=True)
data

Al igual que con los datos de mosquitos, hay una copia de este conjunto de datos en GitHub, y la utilizaremos en capítulos posteriores. No necesitas descargar el conjunto de datos en tu computadora, ya que lo cargaremos directamente mediante el enlace.

Explorar los datos#

data_by_day = data[['SiteId', 'MeasuredDate']].groupby(['MeasuredDate'], as_index=False).count()

plt.figure(figsize=(8,4))
plt.plot(data_by_day['MeasuredDate'], data_by_day['SiteId'])
plt.xlabel("Fecha")
plt.ylabel("Contribuciones diarias")
plt.title("Contribuciones diarias a Land Cover a lo largo del tiempo (2018-2025)")
plt.show()
data_by_county = data[['SiteId', 'CountryName']].groupby(['CountryName'], as_index=False).count().sort_values(by='SiteId').tail(10)

plt.barh(data_by_county['CountryName'], data_by_county['SiteId'])
plt.xlabel("Contribuciones totales")
plt.ylabel("País")
plt.title("Contribuciones a Land Cover por país (2018-2025)")
plt.show()

Una característica muy útil del conjunto de datos Land Cover es que las personas participantes envían fotografías del área observada. Veamos algunas de estas imágenes.

# Obtener la primera observación en la que se enviaron todas las fotografías
entry = data.dropna(subset=['DownwardPhotoUrl', 'EastPhotoUrl', 'NorthPhotoUrl', 'SouthPhotoUrl', 'WestPhotoUrl', 'UpwardPhotoUrl',
                            'Feature1PhotoUrl', 'Feature2PhotoUrl', 'Feature3PhotoUrl', 'Feature4PhotoUrl']).head(1)

url_list = []
col_list = []

for col in entry.columns:
  if 'Url' in col:
    print(f'{col}: {entry[col].values[0]}')
    url_list.append(entry[col].values[0])
    col_list.append(col)

display(entry)
# Mostrar todas las imágenes
plt.figure(figsize=(20, 6))

for i, (url, title) in enumerate(zip(url_list, col_list)):
    response = requests.get(url)
    img = Image.open(BytesIO(response.content))

    # Crear un gráfico con 2 filas y 5 columnas
    plt.subplot(2, 5, i + 1)
    plt.imshow(img)
    plt.title(title)
    plt.axis('off')

plt.tight_layout()
plt.show()