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 compressMy contributions to the Great Southern Bioblitz in the Great Barrier Reef
Cairns
Python
pyinaturalist
Folium
A summary of my iNaturalist observations during our visit to Cairns.
Load Python modules
import seaborn as sns
import matplotlib.pyplot as pltDownload 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+1Requesting 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())
mapMake 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 |