Resumen de observaciones de diciembre 2024

Python
pyinaturalist
NSW
VIC
plotly
Autor/a

José R. Ferrer-Paris

Fecha de publicación

29 de abril de 2025

Fecha de última modificación

23 de febrero de 2026

El objetivo de esta contribución es resumir la información de mis observaciones en iNaturalist realizadas durante el mes de Diciembre de 2024. En esta oportunidad quiero intentar algo diferente para visualizar la información espacial, temporal y taxonómica de las observaciones.

Cargar módulos en Python

Primero voy a importar los modules de Python necesarios para descargar la información de iNaturalist (pyinaturalist), algunas herramientas útiles para manipulación de los datos (pandas, numpy, itertools) y otras para visualización (ipyplot, plotly).

import pandas as pd
from pyinaturalist import (
    Observation,
    get_observations,
    get_taxa_by_id,
    get_places_by_id,
    pprint,
)
import ipyplot
from itertools import compress, islice
import plotly.express as px
import numpy as np

Descargar observaciones de iNaturalist

Uso la función get_observations con mi user_id y un intervalo de fechas definido con d1 and d2 para cubrir todo el mes de Diciembre de 2024.

observations = get_observations(user_id='NeoMapas', 
                                d1="2024-12-01",
                                d2="2024-12-31",
                                per_page=1000)

Hasta la fecha he subido una pequeña cantidad de observaciones de este mes, pero este número aumentará a medida que reviso y clasifico mis archivos de fotos.

len(observations['results'])

155

En este loop extraigo información específica de cada observación descaragada de iNat. En el registro principal incluyo los datos de la fecha, localidad y especie, pero además extraigo una lista de identificadores que reflejan la jerarquía taxonómica (ident_taxon_ids) y la información geográfica (place_id).

records=list()
places=list()
taxa=list()
for obs in observations['results']:
    main_record = {
        'uuid': obs['uuid'],
        'week': obs['observed_on_details']['week'],
        'day': obs['observed_on_details']['day'],
        'hour': obs['observed_on_details']['hour'],
        'location': obs['place_guess'],
        'species guess': obs['species_guess'],
    }
    records.append(main_record)
    for pid in obs['place_ids']:
        place_record = {
            'uuid': obs['uuid'],
            'place id': pid
        }
        places.append(place_record)
    for tid in obs['ident_taxon_ids']:
        taxon_record = {
            'uuid': obs['uuid'],
            'taxon id': tid
        }
        taxa.append(taxon_record)

Uso la función de DataFrame de pandas para transformar estas listas en marcos de datos que se me hacen más fáciles para filtrar y manipular más adelante.

inat_obs=pd.DataFrame(records)
places = pd.DataFrame(places)
taxa = pd.DataFrame(taxa)

Consultar información taxonómica y geográfica

Voy a utilizar los ids de taxones y lugares para consultar la información más detallada disponible en el API de iNat.

El número de id que quiero consultar excede el número permitido por la función de get_taxa_by_id, así que tengo que dividir la lista en partes:

all_taxa=list(set(taxa['taxon id']))
def chunk(it, size):
    it = iter(it)
    return iter(lambda: tuple(islice(it, size)), ())

Por tanto tengo que hacer un loop dentro de un loop para consultar todos los ids relevantes y extraer la información de los diferentes niveles jerárquicos (rank).

for slc in chunk(all_taxa,30):
    taxa_query = get_taxa_by_id(slc, rank_level=[70,60,50,40,30,20,10])
    for res in taxa_query['results']:
        qry = taxa.loc[taxa['taxon id'] == res['id'],'uuid']
        inat_obs.loc[inat_obs.uuid.isin(qry), res['rank']] = res['name']

Según el API reference:

Some example values for rank_level are 70 (kingdom), 60 (phylum), 50 (class), 40 (order), 30 (family), 20 (genus), 10 (species), 5 (subspecies)

En el caso de los ids de localidades, podemos incluirlos todos en una misma consulta, pero tenemos que hacer la asignación del nivel más explícita:

all_places=list(set(places['place id']))
response = get_places_by_id(all_places,
                            admin_level=[10,20,30])
for res in response['results']:
    qry = places.loc[places['place id'] == res['id'],'uuid']
    if res['admin_level'] == 10:
        level='state'
    elif res['admin_level'] == 20:
        level='county'
    elif res['admin_level'] == 30:
        level='town'
    inat_obs.loc[inat_obs.uuid.isin(qry), level] = res['name']

Según el API reference: > Admin level of a place, or an array of admin levels in comma-delimited format. Supported admin levels are: -10 (continent), 0 (country), 10 (state), 20 (county), 30 (town), 100 (park)

Ahora tenemos un marco de datos con todas las columnas que necesitamos:

inat_obs.head()

uuid week day hour location species guess kingdom class phylum order family genus species county town state
0 d8c8414b-93f3-49c0-8183-d914b693f6b2 49 8 9 South Gippsland - East, AU-VI, AU Plants Plantae Magnoliopsida Tracheophyta Proteales Proteaceae Banksia Banksia marginata South Gippsland - East NaN Victoria
1 0a6647a0-b4d8-4eed-bad9-a90950b72df2 49 7 16 South Gippsland - East, AU-VI, AU Brightfig Tribe Plantae Magnoliopsida Tracheophyta Caryophyllales Aizoaceae NaN NaN South Gippsland - East NaN Victoria
2 0cd647dd-15cc-4b23-80f0-c95f37511c5c 49 7 16 South Gippsland - East, AU-VI, AU Sea Spurge Plantae Magnoliopsida Tracheophyta Malpighiales Euphorbiaceae Euphorbia Euphorbia paralias South Gippsland - East NaN Victoria
3 b9930751-8fa8-4836-a140-01373e734db0 49 7 12 South Gippsland - East, AU-VI, AU Silver Banksia Plantae Magnoliopsida Tracheophyta Proteales Proteaceae Banksia Banksia marginata South Gippsland - East NaN Victoria
4 fa7b07dd-7d89-4f2b-a40e-354a3aa393dc 49 6 10 South Gippsland - East, AU-VI, AU Beetles Animalia Insecta Arthropoda Coleoptera Scarabaeidae Phyllotocus Phyllotocus australis South Gippsland - East NaN Victoria

Resumir datos con pandas

Para resumir las observaciones quiero agrupar y agregar los datos, y puedo hacer esto con las funciones de pandas, por ejemplo:

agg_funcs = {'uuid':['count']}
group_columns = ['week','day']
inat_obs.groupby(group_columns).agg(agg_funcs)

uuid
count
week day
49 4 7
5 8
6 53
7 57
8 7
50 15 4
52 26 2
27 11
28 1
29 5

Pero entre más columnas agrego, la tabla se expande y una tabla muy extensa se hace más compleja de visualizar y comprender.

Visualización interactiva

Los gráficos interactivos ofrecen una oportunidad para que sea el usuario el que explore y controle el nivel de detalle que más le interese. Vamos a intentar aquí usar la función treemap del paquete plotly.

Primero preparo los datos:

agg_funcs = {'uuid':['count']}
group_columns = ['week','day','hour']
obs_by_date=inat_obs.groupby(group_columns).agg(agg_funcs).reset_index()
obs_by_date.columns = [' '.join(col).strip() for col in obs_by_date.columns.values]

Y luego ejecuto la función para crear la figura:

fig = px.treemap(obs_by_date, 
    path=[px.Constant("Obs from Dec 2024"),] + group_columns, 
    values='uuid count',
    color='uuid count', 
    hover_data=['uuid count'],
    color_continuous_scale='RdBu')
fig.update_layout(margin = dict(t=5, l=5, r=5, b=5)) # reducir margen
fig.show()

Se pueden condensar todos esos pasos si declaramos una función propia:

def group_and_plot_data(x,aggfuncs,groupcols):
    gd=x.groupby(groupcols).agg(aggfuncs).reset_index()
    gd.columns = [' '.join(col).strip() for col in gd.columns.values]
    value_col = gd.columns.values[-1]
    fig = px.treemap(gd, 
        path=[px.Constant("Obs from Dec 2024"),] + groupcols,  
        values=value_col,
        color=value_col, 
        hover_data=[value_col],
        color_continuous_scale='RdBu')
    fig.update_layout(margin = dict(t=5, l=5, r=5, b=5))
    return(fig)

Y de esta forma podemos mostrar los resultados por grupo taxonómico:

group_columns = ['kingdom','phylum','class','order','family']
fig1 = group_and_plot_data(inat_obs, agg_funcs, group_columns)
fig1.show()

O por localidad

group_columns = ['state','county']
fig2 = group_and_plot_data(inat_obs, agg_funcs, group_columns)
fig2.show()

Conclusión

En estos tres ejemplos vemos como se visualizan las tablas de datos de una forma interactiva, balanceando la complejidad de los niveles anidados con un mecanismo intuitivo para explorar los niveles de mayor interés para el lector. Esta es una forma diferente de explorar los datos, y puede ser útil en algunos contextos. Como en todas las visualizaciones estadísticas, es importante que el mensaje sea claro y que el método esté alineado con el mensaje.