from pyinaturalist import get_observations
import pandas as pd
import numpy as np
import geopandas as gpd
import folium
from datetime import datetime
from IPython.display import display, HTMLFrailejones de los Páramos: mis observaciones en iNaturalist
Subtribu Espeletiinae (Asteraceae)
Frailejones de los Páramos: mis observaciones en iNaturalist
Siento una conección especial con los frailejones (Espeletiinae), un grupo de plantas endémicas de los Páramos de Venezuela, Colombia y Ecuador. Durante mis estudios de biología en Venezuela tuve la oportunidad de trabajar en el páramo y observar de cerca las adaptaciones de estas plantas para sobrevivir en el clima frío y húmedo de la alta montaña tropical. En iNaturalist tengo varias observaciones de frailejones, y en este post quiero compartir un resumen de las especies observadas y los lugares en las que las he encontrado.
Introducción
A continuación, enumero los pasos que seguí para crear un resumen de mis observaciones de frailejones en iNaturalist, incluyendo la descarga de datos, la combinación de información taxonómica y geográfica, y la visualización de los resultados.
- Descargar información de observaciones e identificaciones de iNaturalist para una consulta específica.
- Descargar datos espaciales que representan regiones de interés y
- Intersectar las observaciones con estas fronteras.
- Agrupar y resumir la información basada en datos taxonómicos y geográficos.
Herramientas y Bibliotecas
Voy a utilizar un entorno de Python con una selección de mis bibliotecas favoritas, como se explica aquí.
Declaramos una función útil para leer los datos temporales de la respuesta del API de iNat:
def as_date(x):
if type(x) == str:
y = datetime.strptime(x, "%Y-%m-%d").date()
else:
y = datetime.date(x)
return(y)Guía paso a paso
Paso 1: Descargar observaciones de iNaturalist
Empezamos por descargar las observaciones de iNaturalist utilizando la biblioteca pyinaturalist. Para obtener una selección de observaciones globales, seleccionamos el usuario neomapas y vemos dónde en el mundo han sido realizadas.
username = 'neomapas'
taxonid=794906
observations = get_observations(user_id=username, taxon_id=taxonid, per_page=0)
n_obs = observations['total_results']Imprimimos un mensaje para mostrar el número total de observaciones que tenemos para este taxón:
print("User {} has {} observations of Espeletiinae (taxon id {}) in iNaturalist".format(username,n_obs,taxonid))User neomapas has 18 observations of Espeletiinae (taxon id 794906) in iNaturalist
Me faltan incorporar mis observaciones de Colombia…
En este caso, el número total de observaciones es relativamente pequeño, pero coloco un ejemplo de código con paginación para mostrar cómo se puede descargar un número mayor de observaciones.
records=list()
idrecords=list()
msg="Requesting observations from user _{}_: page {}, total of {} observations downloaded"
j=1
while len(records) < n_obs:
print(msg.format(username,j,min(j*200,n_obs)))
observations = get_observations(
user_id='neomapas',
taxon_id=taxonid,
per_page=1000,
page=j)
for obs in observations['results']:
record = {
'uuid': obs['uuid'],
'quality': obs['quality_grade'],
'description': obs['description'],
'location': obs['place_guess'],
'longitude': obs['location'][1],
'latitude': obs['location'][0],
'species guess': obs['species_guess'],
'observed on': as_date(obs['observed_on']),
'points': obs['faves_count'] * 10 + obs['comments_count'] + obs['identifications_count'] * 3,
}
for id in obs['identifications']:
ca = id['category']
fch = id['created_at']
idrecord = {
'uuid': obs['uuid'],
'quality_grade': obs['quality_grade'],
'id_category': ca,
'created': fch}
for anc in id['taxon']['ancestors']:
idrecord[anc['rank']] = anc['name']
idrecord[id['taxon']['rank']] = id['taxon']['name']
idrecords.append(idrecord)
if len(obs['observation_photos'])>0:
record['url'] = obs['observation_photos'][0]['photo']['url']
record['attribution'] = obs['observation_photos'][0]['photo']['attribution']
records.append(record)
j=j+1Requesting observations from user _neomapas_: page 1, total of 18 observations downloaded
Este ejemplo requiere extraer información adicional que está anidada dentro de la estructura json de la respuesta de la API. Explico algunos de los detalles en este post.
Transformamos estas listas de registros en dos marcos de datos con pandas:
inat_obs=pd.DataFrame(records)
inat_ids=pd.DataFrame(idrecords)Paso 2: Combinar información de observaciones e identificaciones
Un paso complicado es transformar la información de las sugerencias de identificación en información taxonómica completa. Aquí estoy utilizando la información de identificación incluida en la respuesta de la función get_observation para reconstruir la información taxonómica. El problema es que hay múltiples sugerencias de identificación por observación, y tenemos que filtrar primero las identificaciones no validadas.
En este caso, la mitad de mis observaciones de Espeletiinae son de calidad de investigación:
inat_obs.groupby(['quality']).agg({"uuid": pd.Series.nunique})| uuid | |
|---|---|
| quality | |
| needs_id | 9 |
| research | 9 |
En estos casos, las observaciones de calidad de investigación siempre tienen sugerencias de identificación de tipo improving o supporting:
inat_ids.groupby(['quality_grade','id_category']).agg({"uuid": pd.Series.nunique})| uuid | ||
|---|---|---|
| quality_grade | id_category | |
| needs_id | improving | 5 |
| leading | 7 | |
| supporting | 2 | |
| research | improving | 9 |
| maverick | 1 | |
| supporting | 9 |
Este es un truco que se puede usar para seleccionar la información taxonómica de la mejor identificación de cada observación, y luego combinar esta información con el marco de datos de las observaciones para consolidar los datos.
ss=inat_ids.id_category.isin(['improving','supporting'])
cols=['uuid','subfamily','tribe','subtribe','genus']
best_ids = inat_ids.loc[ss,cols].drop_duplicates().dropna()
inat_obs_ids = inat_obs.join(best_ids.set_index('uuid'), on='uuid')En este punto tenemos un marco de datos con la información de las observaciones, incluyendo la información taxonómica de familia y subfamilia, que es lo que necesitamos para hacer los resúmenes por familia y país.
Me gusta tener la información de las observaciones con una figura que muestre la imagen de la observación, el nombre común y el nombre científico. Así que vamos a crear una nueva columna con esta información formateada como un string de html.
inat_obs_ids['figure'] = [
"<figure class='mini'><a href='https://www.inaturalist.org/observations/%s' target=_blank><img src='%s' height=50><figcaption class='mini'>%s: <i>%s</i></figcaption></a></figure>" % (
record['uuid'],
record['url'],
record['subtribe'],
record['species guess'])
for idx,record in inat_obs_ids.iterrows()
]Paso 3: Número de observaciones únicas y especies por familia
Ahora que tenemos toda la información combinada en un solo marco de datos, podemos usar las funciones de agrupamiento y agregación de pandas para obtener resúmenes por familia y subfamilia.
aggfuns = {
"uuid": pd.Series.nunique,
"species guess": pd.Series.nunique
}
inat_obs_ids.groupby(['subtribe','genus']).agg(aggfuns)| uuid | species guess | ||
|---|---|---|---|
| subtribe | genus | ||
| Espeletiinae | Espeletia | 14 | 9 |
gs = gpd.points_from_xy(inat_obs_ids.longitude, inat_obs_ids.latitude, crs="EPSG:4326")
inat_obs_xy=gpd.GeoDataFrame(inat_obs_ids, geometry=gs)Paso 4: Mapa de las observaciones
aggfuns = {
'observed on': ["min", "max"],
'species guess': ['count',pd.Series.nunique],
}
inat_obs_ids.groupby('location').agg(aggfuns).sort_values(('observed on','min'))| observed on | species guess | |||
|---|---|---|---|---|
| min | max | count | nunique | |
| location | ||||
| Campo Elías, Mérida, Venezuela | 2004-09-11 | 2004-09-12 | 2 | 2 |
| Uribante, 5058, Táchira, Venezuela | 2005-07-24 | 2005-07-24 | 4 | 4 |
| Rangel, Mérida, VE | 2006-07-13 | 2006-07-13 | 1 | 1 |
| Sucre, Mérida, VE | 2006-07-14 | 2006-07-14 | 3 | 2 |
| Urdaneta, Trujillo, VE | 2009-02-01 | 2009-02-01 | 1 | 1 |
| Rangel, Mérida, Venezuela | 2009-05-16 | 2009-05-16 | 1 | 1 |
| Venezuela | 2015-10-28 | 2015-10-28 | 2 | 2 |
| Jauregui, Táchira, Venezuela | 2015-12-19 | 2015-12-19 | 1 | 1 |
| Jauregui, 5022, Táchira, Venezuela | 2016-06-22 | 2016-06-22 | 1 | 1 |
| Villapinzón, Cundinamarca, Colombia | 2024-10-05 | 2024-10-05 | 1 | 1 |
map = folium.Map(tiles="Esri NatGeoWorldMap")fg = folium.FeatureGroup(name="iNaturalist observations", control=True, attribution="observers @ iNaturalist").add_to(map)
popup_text = """<img src='{url}'>
<caption><i>{species}</i> observed on {observed_on} / {attribution}</caption> {desc}
"""
for obs in observations['results']:
if obs['quality_grade'] == 'research':
if obs['description'] is None:
desc = ""
else:
desc = obs['description']
pincolor = 'green'
else:
desc = "Observation is not research quality grade."
pincolor = 'gray'
fg.add_child(
folium.Marker(
location=obs['location'],
popup=popup_text.format(
species=obs['species_guess'],
observed_on=obs['observed_on'],
desc=desc,
url = obs['observation_photos'][0]['photo']['url'],
attribution = obs['observation_photos'][0]['photo']['attribution']),
icon=folium.Icon(color=pincolor),
)
)
folium.LayerControl().add_to(map)<folium.map.LayerControl object at 0x12cde0b60>
map.fit_bounds(map.get_bounds())
mapPaso 5: Mostrar una muestra de observaciones
Ahora vamos a mostrar las mejores fotos de cada especie identificada.
En estas líneas de código …
Uso las funciones display y HTML para leer las cadenas de texto formateadas como elementos html1 para organizar las figuras y leyendas en esta página web.
selection = (
inat_obs_ids
.sort_values('points')
.groupby(['species guess'])
.agg({'figure':'unique'})
)
sections = list()
for idx,row in selection.iterrows():
sectionfigures=" ".join(row['figure'])
sectionname="<figure class='mini'><p class='figsection'>%s </p></figure>" % idx
sections.append(sectionname + sectionfigures)
allsections="<div class='container'>%s</div>" % ("".join(sections))
display(HTML(allsections))Espeletia



Espeletia argentea

Espeletia jahnii
Espeletia lindenii

Espeletia marcescens

Espeletia neriifolia


Espeletia occulta

Espeletia schultzii




Espeletia steyermarkii

Espeletiinae

Tabaquero

Conclusión
En resumen, este proceso me permitió obtener un resumen de mis observaciones de frailejones en iNaturalist, incluyendo el número de observaciones por especie y su distribución geográfica. Además, pude mostrar una selección de las mejores fotos de cada especie identificada, lo que hace que esta información sea más atractiva y fácil de entender para los lectores.
Notas
El formato de salida de los elementos html depende de las definiciones de estilo css del sitio. Mira este archivo si quieres reutilizar o adaptar mi estilo.↩︎
