Cuando se trabaja con datos abiertos de biodiversidad, una de las preguntas más interesantes no es solo cuántos registros pueden descargarse, sino qué cambia cuando se comparan dos infraestructuras distintas sobre el mismo espacio. En este ejercicio tomé observaciones de coleópteros del entorno del Volcán Tacaná desde iNaturalist y GBIF, las normalicé en un mismo formato y las transformé en salidas cartográficas para comparar visualmente su cobertura, densidad y superposición.
El objetivo no fue hacer scraping por sí mismo, sino construir un flujo reproducible para contrastar resultados y representarlos gráficamente. Para eso usé dos scripts de extracción, una etapa de limpieza tabular y un script final que convirtió los datos en mapas estáticos e interactivos listos para exploración.
Por qué este caso importa
Chiapas es una de las regiones más relevantes de México para el estudio de Coleoptera por su diversidad ambiental, sus gradientes altitudinales y la riqueza documentada en distintos grupos de escarabajos. La literatura regional ha mostrado que el estado concentra una fracción importante de la diversidad nacional en linajes como Scarabaeoidea y Passalidae, y al mismo tiempo conserva vacíos de muestreo que siguen siendo metodológicamente interesantes.
Dentro de ese contexto, el Volcán Tacaná es un buen laboratorio natural. Tiene complejidad topográfica, interés biogeográfico y antecedentes de estudio que lo vuelven ideal para ensayar un flujo de análisis geoespacial con datos reales. Además, comparar iNaturalist con GBIF en una misma ventana espacial permite ver algo que muchas veces se pierde en los inventarios: cada plataforma observa el territorio de forma distinta.
Qué quería resolver
La meta era sencilla de formular, pero útil desde el punto de vista analítico:
- Descargar registros de coleópteros para el área del Volcán Tacaná.
- Obtener esos datos desde dos plataformas diferentes.
- Estandarizar sus campos para que fueran comparables.
- Limpiarlos lo suficiente para evitar redundancias evidentes.
- Materializarlos en mapas que permitieran ver coincidencias y diferencias.
En otras palabras, el interés principal era pasar de una pregunta biológica a un flujo de datos geoespaciales reproducible.
Arquitectura del flujo
El pipeline quedó dividido en tres piezas muy concretas. inaturalistscrapper.py recupera observaciones desde iNaturalist, gbifscrapper.py hace lo mismo con GBIF y graficar_mapa.py integra ambos resultados para construir los productos finales.
| Componente | Rol técnico | Resultado |
|---|---|---|
inaturalistscrapper.py |
Consulta la API de iNaturalist y normaliza observaciones | coleopteros_tacana_inaturalist.csv |
gbifscrapper.py |
Consulta la API de GBIF y normaliza ocurrencias | coleopteros_tacana_gbif.csv |
graficar_mapa.py |
Integra ambos CSV, filtra, compara y grafica | PNGs y mapa_interactivo_tacana.html
|
La ventaja de esta estructura es que separa adquisición, transformación y visualización. Si una fuente cambia su API, la modificación afecta solo a su scraper y no al resto del flujo.
Delimitación espacial
Los tres scripts comparten la misma ventana geográfica. El análisis trabaja con un bounding box definido aproximadamente entre 14.9 y 15.2 de latitud y entre -92.3 y -92.0 de longitud, además de registrar la cima del Volcán Tacaná como punto de referencia cartográfica.
Eso es importante porque evita comparar cosas espacialmente distintas. En vez de usar nombres de localidad, límites administrativos o descripciones ambiguas, ambas plataformas se consultan exactamente sobre el mismo recorte espacial.
lat_min, lat_max = 14.9, 15.2
lon_min, lon_max = -92.3, -92.0
tacana_lat = 15.1325
tacana_lon = -92.1086
Extracción desde iNaturalist
Para iNaturalist usé el endpoint de observaciones y filtré por Coleoptera mediante taxon_id = 47208. La búsqueda se restringió con las coordenadas suroeste y noreste del bounding box, y la paginación se resolvió solicitando 200 registros por página.
url = "https://api.inaturalist.org/v1/observations"
params = {
"taxon_id": 47208,
"swlat": 14.9,
"swlng": -92.3,
"nelat": 15.2,
"nelng": -92.0,
"per_page": 200,
"page": 1
}
Cada respuesta JSON contiene bastante información, pero para este flujo extraje solo lo necesario para comparar fuentes: identificador, nombre de la especie, coordenadas, fecha, usuario y calidad del registro. Un detalle importante es que las coordenadas vienen en formato GeoJSON como longitud y latitud, así que hubo que invertir el orden al guardarlas.
all_observations.append({
"id": obs.get("id"),
"especie": obs.get("taxon", {}).get("name"),
"latitud": obs.get("geojson", {}).get("coordinates", [None, None])[1],
"longitud": obs.get("geojson", {}).get("coordinates", [None, None])[0],
"fecha": obs.get("observed_on_string"),
"usuario": obs.get("user", {}).get("login"),
"calidad": obs.get("quality_grade")
})
Ese paso deja a iNaturalist reducido a una tabla simple, lista para integrarse con otra fuente sin arrastrar toda la complejidad de su JSON original.
Extracción desde GBIF
El segundo script trabaja contra el endpoint de ocurrencias de GBIF. Aquí el filtro taxonómico se hace con taxonKey = 1470, y la paginación usa limit y offset, lo que cambia ligeramente la lógica respecto a iNaturalist.
url = "https://api.gbif.org/v1/occurrence/search"
params = {
"taxonKey": 1470,
"decimalLatitude": "14.9,15.2",
"decimalLongitude": "-92.3,-92.0",
"limit": 300,
"offset": 0
}
El script recorre bloques sucesivos de resultados y se detiene cuando GBIF indica que ya no hay más registros o cuando se alcanza el límite operativo de la búsqueda paginada. Después, transforma la respuesta en un esquema tabular análogo al de iNaturalist.
all_observations.append({
"id": obs.get("gbifID"),
"especie": obs.get("species") or obs.get("scientificName"),
"latitud": obs.get("decimalLatitude"),
"longitud": obs.get("decimalLongitude"),
"fecha": obs.get("eventDate"),
"usuario": obs.get("recordedBy"),
"calidad": obs.get("basisOfRecord")
})
Aquí aparece una diferencia metodológica importante: en GBIF, el campo de calidad no equivale al quality_grade de iNaturalist, sino que describe la base del registro. Eso significa que la comparación entre plataformas no es solo espacial, sino también semántica: ambas hablan de biodiversidad, pero no organizan la evidencia de la misma manera.
Limpieza y estandarización
Una vez descargados los datos, ambos scripts convierten las observaciones a un DataFrame de pandas y eliminan duplicados usando una llave compuesta por especie, coordenadas, fecha y usuario. No es una depuración taxonómica completa, pero sí una buena primera barrera contra redundancias obvias.
df = pd.DataFrame(all_observations)
df = df.drop_duplicates(
subset=["especie", "latitud", "longitud", "fecha", "usuario"],
keep="first"
)
Ese detalle hace que la salida de ambos scrapers ya sea comparable desde el punto de vista estructural. El resultado son dos CSV independientes, pero construidos con la misma lógica de campos.
Integración en graficar_mapa.py
El tercer script empieza leyendo ambos CSV y aplicando una segunda capa de control. Si algún archivo no existe, devuelve un DataFrame vacío; si existe, elimina registros sin coordenadas y vuelve a eliminar duplicados.
def cargar_datos(filepath):
if not os.path.exists(filepath):
return pd.DataFrame()
df = pd.read_csv(filepath)
df = df.dropna(subset=["latitud", "longitud"])
df = df.drop_duplicates(
subset=["especie", "latitud", "longitud", "fecha", "usuario"],
keep="first"
)
return df
Me gusta ese patrón porque hace al flujo un poco más tolerante a errores. Aunque la limpieza ya se hizo en los scrapers, el análisis final no depende ciegamente de que todo haya salido perfecto antes.
Mapas estáticos para comparar plataformas
La primera parte de graficar_mapa.py usa Matplotlib para generar salidas PNG. La función principal construye un scatter plot georreferenciado, añade la cima del volcán, dibuja el bounding box y ajusta el rango espacial para que la lectura sea consistente entre mapas.
plt.scatter(df["longitud"], df["latitud"], color=color, alpha=0.6, s=15)
plt.scatter(tacana_lon, tacana_lat, color='red', marker='*', s=200)
plt.plot(rect_x, rect_y, 'g--', alpha=0.5)
A partir de esa función se generan tres productos distintos:
- Un mapa solo para iNaturalist.
- Un mapa solo para GBIF.
- Un mapa comparativo con ambas capas superpuestas.
El comparativo es probablemente la pieza más útil del flujo, porque deja ver de inmediato si las dos plataformas se concentran en las mismas zonas o si cada una ilumina porciones distintas del territorio.
Mapa interactivo con Folium
La segunda parte del script materializa un mapa interactivo en HTML usando Folium. El mapa se centra en el Tacaná, arranca con OpenStreetMap y además incorpora una capa satelital de Esri y una capa topográfica.
mapa = folium.Map(location=[tacana_lat, tacana_lon], zoom_start=11, tiles="OpenStreetMap")
folium.TileLayer(
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
attr='Esri World Imagery',
name='Satélite Esri',
overlay=False
).add_to(mapa)
También se dibuja explícitamente el rectángulo del área de estudio y se añade un marcador para la cima del volcán. Eso ayuda mucho a que la lectura del mapa tenga contexto geográfico y no sea solo una nube de puntos.
folium.Rectangle(
bounds=[[lat_min, lon_min], [lat_max, lon_max]],
color="green",
fill=False,
weight=2,
dash_array="5, 5"
).add_to(mapa)
Capas separadas y exploración de atributos
Para distinguir visualmente las dos fuentes, el script crea un FeatureGroup para iNaturalist y otro para GBIF. Cada observación se convierte en un CircleMarker con atributos visuales propios de color, tamaño y opacidad.
grupo_inat = folium.FeatureGroup(name=f"iNaturalist ({len(df_inat)} registros)")
for _, r in df_inat.iterrows():
folium.CircleMarker(
location=[r["latitud"], r["longitud"]],
radius=4,
color="#E65100",
fill=True,
fill_color="#FF5722",
fill_opacity=0.8,
tooltip=r["especie"]
).add_to(grupo_inat)
Además, cada punto puede desplegar información contextual como especie, usuario, fecha o tipo de registro. Con LayerControl() el usuario puede activar o desactivar cada plataforma, lo que convierte al entregable en una interfaz ligera de inspección espacial, no solo en una visualización estática.
Qué entregó el pipeline
Al final, el flujo genera una pequeña colección de productos listos para análisis y comunicación:
coleopteros_tacana_inaturalist.csvcoleopteros_tacana_gbif.csvmapa_observaciones_inaturalist.pngmapa_observaciones_gbif.pngmapa_comparativa_tacana.pngmapa_interactivo_tacana.html
Eso cubre dos necesidades muy comunes en análisis de biodiversidad: producir evidencia visual reproducible para reporte y, al mismo tiempo, conservar una salida interactiva para exploración manual.
Qué deja ver este ejercicio
Lo más interesante de este caso no es la complejidad algorítmica, sino la claridad del flujo. Con decisiones relativamente simples —consultar APIs, estandarizar campos, depurar duplicados y cartografiar resultados— se obtiene una comparación espacial útil entre dos infraestructuras de biodiversidad.
También deja claro algo metodológicamente importante: dos plataformas pueden hablar del mismo grupo biológico y del mismo territorio, pero no necesariamente lo representan igual. Y esa diferencia, lejos de ser un problema, es precisamente lo que hace valioso el ejercicio.
Stack utilizado
El flujo se implementó en Python como lenguaje principal. La comunicación con APIs se resolvió con requests, la manipulación tabular con pandas, la visualización estática con matplotlib y el mapa interactivo con folium.
Software y herramientas computacionales
- McKinney, W. (2010). Data Structures for Statistical Computing in Python. Proceedings of the 9th Python in Science Conference, 56-61. https://doi.org/10.25080/Majora-92bf1922-00a
- The pandas development team. (2020). pandas-dev/pandas. Zenodo. https://doi.org/10.5281/zenodo.3509134
- Hunter, J. D. (2007). Matplotlib: A 2D graphics environment. Computing in Science & Engineering, 9(3), 90-95. https://doi.org/10.1109/MCSE.2007.55




Top comments (0)