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 npResumen de observaciones de diciembre 2024
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).
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_levelare 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.