My contributions to the Great Southern Bioblitz in the Great Barrier Reef

Cairns
Python
pyinaturalist
Folium
Autores/as

José R. Ferrer-Paris

Ada Sánchez-Mercado

Fecha de publicación

19 de noviembre de 2025

Fecha de última modificación

20 de noviembre de 2025

A summary of my iNaturalist observations during our visit to Cairns.

Load Python modules

import pandas as pd
import folium
from datetime import datetime, timedelta
from pyinaturalist import (
    Observation,
    get_observations,
    get_projects_by_id,
    pprint,
)
import ipyplot
from itertools import compress
import seaborn as sns
import matplotlib.pyplot as plt

Download iNaturalist observations

From the end of May to the first week of June 2025

projects = get_projects_by_id([253831, 255314])
pprint(projects)
                                                                                                                   
  ID       Title                                         Type         URL                                          
 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 
  255314   Great Southern BioBlitz 2025: Cairns          collection   https://www.inaturalist.org/projects/255314  
  253831   Great Southern Bioblitz 2025: Great Barrier   collection   https://www.inaturalist.org/projects/253831  
           Reef                                                                                                    
                                                                                                                   
gbs_gbr = projects['results'][1]
observations = get_observations(project_id=gbs_gbr['id'], 
                                per_page=0)
n_obs = observations['total_results']
print("Project _{}_ has {} observations in iNaturalist".format(gbs_gbr['title'],n_obs))
Project _Great Southern Bioblitz 2025: Great Barrier Reef_ has 1722 observations in iNaturalist
records=list()
j=1
while len(records) < n_obs:
    print("Requesting observations from project _{}_: page {}, total of {} observations downloaded".format(gbs_gbr['title'],j,min(j*200,n_obs)))
    observations = get_observations(project_id=gbs_gbr['id'],per_page=200,page=j)
    for obs in observations['results']:
        record = {
            'uuid': obs['uuid'], 
            'user_id': obs['user']['id'],
            'user': obs['user']['login'],
            'user_name': obs['user']['name'],
            'user_icon': obs['user']['icon_url'],
            'day_observed': obs['observed_on_details']['day'],
            'species_guess': obs['species_guess'],
            'quality_grade': obs['quality_grade'],
            'description': obs['description'],
            'location': obs['location']
            }
        if obs['taxon'] is not None:
            if 'iconic_taxon_name' in obs['taxon'].keys():
                record['iconic_taxon'] = obs['taxon']['iconic_taxon_name']
        if len(obs['observation_photos'])>0:
            record['url'] = obs['observation_photos'][0]['photo']['url']
            record['attribution'] = obs['observation_photos'][0]['photo']['attribution']
        if obs['oauth_application_id'] is None:
            record['app'] = 0
        else:
            record['app'] = obs['oauth_application_id']
            
        records.append(record)
    j=j+1
Requesting observations from project _Great Southern Bioblitz 2025: Great Barrier Reef_: page 1, total of 200 observations downloaded
Requesting observations from project _Great Southern Bioblitz 2025: Great Barrier Reef_: page 2, total of 400 observations downloaded
Requesting observations from project _Great Southern Bioblitz 2025: Great Barrier Reef_: page 3, total of 600 observations downloaded
Requesting observations from project _Great Southern Bioblitz 2025: Great Barrier Reef_: page 4, total of 800 observations downloaded
Requesting observations from project _Great Southern Bioblitz 2025: Great Barrier Reef_: page 5, total of 1000 observations downloaded
Requesting observations from project _Great Southern Bioblitz 2025: Great Barrier Reef_: page 6, total of 1200 observations downloaded
Requesting observations from project _Great Southern Bioblitz 2025: Great Barrier Reef_: page 7, total of 1400 observations downloaded
Requesting observations from project _Great Southern Bioblitz 2025: Great Barrier Reef_: page 8, total of 1600 observations downloaded
Requesting observations from project _Great Southern Bioblitz 2025: Great Barrier Reef_: page 9, total of 1722 observations downloaded
project_obs = pd.DataFrame(records)

Map of observations

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 day {observed_on} / {attribution}</caption> {desc}
   """
popup_text_no_foto = """
<caption><i>{species}</i> observed on day {observed_on} / No foto</caption> {desc}
   """
for obs in records:
    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'
    if 'url' not in obs.keys():
        fg.add_child(
            folium.Marker(
                location=obs['location'],
                popup=popup_text_no_foto.format(
                   species=obs['species_guess'],
                    observed_on=obs['day_observed'],
                    desc=desc),
                icon=folium.Icon(color=pincolor),
            )
          )
    else:
        fg.add_child(
            folium.Marker(
                location=obs['location'],
                popup=popup_text.format(
                   species=obs['species_guess'],
                    observed_on=obs['day_observed'],
                    desc=desc,
                   url = obs['url'],
                   attribution = obs['attribution']),
                icon=folium.Icon(color=pincolor),
            )
          )

folium.LayerControl().add_to(map)

<folium.map.LayerControl object at 0x12f688800>
map.fit_bounds(map.get_bounds())
map

Make this Notebook Trusted to load map: File -> Trust Notebook

Observations per iconic taxon

project_obs.groupby(['iconic_taxon']).agg({
    'uuid': 'count',
    'species_guess': ['count',pd.Series.nunique],
    'user_id': pd.Series.nunique})

uuid species_guess user_id
count count nunique nunique
iconic_taxon
Actinopterygii 484 456 256 25
Animalia 268 252 129 33
Arachnida 12 11 8 4
Aves 123 120 52 20
Chromista 12 12 6 2
Fungi 3 1 1 3
Insecta 23 20 16 13
Mammalia 5 5 5 5
Mollusca 228 211 70 28
Plantae 97 82 65 17
Reptilia 26 24 9 15
data_crosstab = pd.crosstab(project_obs['iconic_taxon'],
                            project_obs['day_observed'], 
                               margins = False)
plt.figure(figsize=(8, 6))
sns.heatmap(data_crosstab, annot=True, fmt='d', cmap='YlGnBu', linewidths=.5)
plt.title('Heatmap of observations per day of Bioblitzing')
plt.ylabel('Iconic taxon')
plt.xlabel('Day of observation')
plt.tight_layout()
plt.show()

Top observers

top_obs = project_obs.groupby(['user_id','user_name']).agg({
    'uuid': 'count',
    'species_guess': ['count',pd.Series.nunique]})
top_obs.loc[top_obs['uuid']['count']>50]

uuid species_guess
count count nunique
user_id user_name
74355 JR Ferrer-Paris 87 77 57
4677927 Adam Smith 316 316 227
8277420 Matt Boyle 125 125 113
9125842 JimsWildLife 53 47 33
9717763 Rachel Bowater 53 49 45
9806282 Camille Workman 58 52 47
9948261 Luke Farrell 457 0 0