import pandas as pd
import geopandas as gpd
from datetime import datetime
from pyinaturalist import (
get_observations,
get_taxa_by_id,
)
from IPython.display import display, HTML, Markdown
import contextily as cx
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib_scalebar.scalebar import ScaleBarTravel journey to Bali, Indonesia
We took a family holidays in Bali, Indonesia from 19 to 19 December 2025, and I brought my cameras along to capture as many little land and sea critters as possible. I am in the process of scanning through thousands of fotos and uploading them to iNaturalist, and I wanted to share some of the highlights here.
For this trip, I spent most time photographing insects and arachnids, for there were some other animals and plants in land and sea. I did not aim to explore the most biodiverse side of Bali, because many activities with my family were focused on semi-urban and touristic areas. But still, the beautiful landscapes of this tropical island provided ample opportunities to capture stunning images of various species.
Here I am creating a travel journal with details for each day, including maps of the localities visited, quantitative summaries of observations, and some of the standout photos from my growing iNat collection.
Load Python modules
First I will load the necessary Python modules to help with data analysis and visualization.
These include pyinaturalist for all things iNatty, pandas and geopandas for data manipulation, contextily for the basemaps, matplotlib for plotting.
I will also need to set up relative paths to the data folders in this project using pyprojroot.
import pyprojroot
repodir = pyprojroot.find_root(pyprojroot.has_dir(".git"))Download basemaps
I am going to download some basemaps for the areas I visited in Bali, so that I can overlay my observation points on them later. I am using a folder in the data directory to store these basemaps.
Since I use git for version control, and I don’t want to commit large files to it, this data folder needs to be in my .gitignore file.
basemap_dir = repodir / 'data' / 'basemapcache'
basemap_dir.mkdir(parents=True, exist_ok=True)I select a few tiles from Open Street Map and CartoDB that cover the areas I visited, and download them using contextily. I am using to different providers to have some variety in the basemaps that I can use later.
slc_provider=cx.providers.OpenStreetMap.Mapnik
w, s, e, n = (114.425, -8.844, 115.718, -8.059)
balimap = cx.bounds2raster(w, s, e, n,
ll=True,
path=basemap_dir / "bali-map.tif",
source=slc_provider
)slc_provider=cx.providers.CartoDB.Positron
w, s, e, n = (115.050, -8.862, 115.645, -8.382)
tripmap = cx.bounds2raster(w, s, e, n,
ll=True,
path=basemap_dir / "bali-trip.tif",
source=slc_provider
)Download iNaturalist observations
Now this is one of those steps we are already familiar from other posts. First I check how many observations I have in iNaturalist from Bali during the trip dates.
observations = get_observations(user_id='NeoMapas',
d1="2025-12-18",
d2="2025-12-30",
per_page=0)
n_obs = observations['total_results']
# First we need to figure out how many observations to expect:
print("User NeoMapas has {} observations in this time frame".format(n_obs))User NeoMapas has 410 observations in this time frame
Then I download all the observations, including links to photos and metadata, using pyinaturalist.
Additionally, I download information about iconic taxonomic groups to help with later summaries and plots.
taxon_dict = get_taxa_by_id([3, 20978, 26036, 40151, 47115, 47158, 47126, 47157, 47170, 47178, 47224, 47792, 144128])I will go through these observations and extract the basic information I need for my summaries and plots.
records=list()
msg="Requesting observations from user NeoMapas: page {}, total of {} observations downloaded"
j=1
while len(records) < n_obs:
print(msg.format(j,min(j*200,n_obs)))
observations = get_observations(
user_id='NeoMapas',
d1="2025-12-18",
d2="2025-12-30",
per_page=1000,
page=j)
for obs in observations['results']:
record = {'quality': obs['quality_grade'],
'description': obs['description'],
'location': obs['place_guess'],
'geoprivacy': obs['taxon_geoprivacy'],
'longitude': obs['location'][1],
'latitude': obs['location'][0],
'species guess': obs['species_guess'],
'Fecha': obs['observed_on'],
'points': obs['faves_count'] * 10 + obs['comments_count'] + obs['identifications_count'] * 3,
}
if len(obs['observation_photos'])>0:
record['url'] = obs['observation_photos'][0]['photo']['url'].replace('square', 'medium')
record['attribution'] = obs['observation_photos'][0]['photo']['attribution']
record['groups'] = [x['name'] for x in taxon_dict['results'] if x['id'] in obs['taxon']['ancestor_ids']]
records.append(record)
j=j+1Requesting observations from user NeoMapas: page 1, total of 200 observations downloaded
Requesting observations from user NeoMapas: page 2, total of 400 observations downloaded
Requesting observations from user NeoMapas: page 3, total of 410 observations downloaded
This goes now into a pandas DataFrame for easier manipulation.
df=pd.DataFrame(records)And a GeoDataFrame for mapping.
inat_obs=gpd.GeoDataFrame(
df,
geometry=gpd.points_from_xy(df.longitude, df.latitude),
crs=4326
).to_crs(epsg=3857)And before I forget, lets reformat the observation dates into simple strings for easier filtering.
inat_obs["dia"]=inat_obs.Fecha.apply(datetime.date).apply(lambda x: str(x))Map of observations
I can plot these observations on a map to highliht the locations I visited (gray circles on the map). I am using here a static basemap instead of the interactive one used in the previous posts.
Notice that all my observations were made in the southern part of Bali, where we spent our time during this trip (red rectangle).
fig, ax = plt.subplots(1, 1)
fig.set_size_inches(10,10)
inat_obs.plot(
ax=ax,
facecolor='grey',
alpha=0.7,
edgecolor='black',)
cx.add_basemap(
ax,
crs=inat_obs.crs,
source = basemap_dir / "bali-map.tif",
reset_extent=False
)
# Create a Rectangle patch
rect = patches.Rectangle((1.28e7, -9.94e5), 7e4, 6e4, linewidth=1, edgecolor='r', facecolor='none')
# Add the patch to the Axes
ax.add_patch(rect)
plt.xlim((1.273e7, 1.289e7))
plt.ylim((-9.95e5, -8.9e5))
ax.add_artist(ScaleBar(1))
plt.show()
One advantage of using a static basemap is that we can easily save the map as a high-resolution image for later use. In this case, I will be zooming in to that red rectangle and using a different base layer to give a different look. I will highlight the observation points for each day to make them stand out against the basemap and the observations from the rest of the trip. Since I need to do this for each day, it is best to define a function that can be reused multiple times:
def mapday(day):
fig, ax = plt.subplots(1, 1)
inat_obs.plot(
ax=ax,
facecolor='grey')
ss = (inat_obs.dia == day) & (inat_obs.geoprivacy != 'obscured')
inat_obs.loc[ss].plot(
ax=ax,
facecolor='magenta',
edgecolor='cyan')
cx.add_basemap(
ax,
crs=inat_obs.crs,
source = basemap_dir / "bali-trip.tif",
reset_extent=False
)
plt.xlim((1.28e7, 1.287e7))
plt.ylim((-9.95e5, -9.35e5))
plt.show()I am also defining a function to summarise the number of observations per day. This function uses Mardown and HTML to format the output nicely and provide a text description of the observations made on each day, including the total number, the numbers per iconic group and a preview of the more interesting observations of the day (selected by the numbers of favorites, ids and comments).
def summarise_obs(day):
df = inat_obs.loc[inat_obs.dia==day]
best = df.sort_values('points', ascending=False).head(3)
bestguess=', '.join(best['species guess'].to_list())
urls = best['url'].to_list()
total = df.shape[0]
group_counts = df['groups'].explode().value_counts()
groupstr= ', '.join([f"{count} {item}" for item, count in group_counts.items()])
display(Markdown(f"So far I have uploaded a total of %s observations. With %s tentatively identified at different taxonomic levels. The three more interesting observations so far are %s." % (total, groupstr, bestguess )))
display(HTML("".join([f"<img src='{url}' height=150> " for url in urls])))
display(Markdown("Check all the observations [here](https://www.inaturalist.org/observations?place_id=any&user_id=NeoMapas&d1=%s&d2=%s){target='inat'}" % (day,day)))Travel log
Now here is where my travel log begins. For each day of the trip, I will provide a summary of the observations made, including the map, the quantitative summary and some of the standout photos.
December 19: Arrival in Bali
Drove from the airport to Ubud with our driver and guide for the whole trip, checked into our Private Villa Wayan in Ubud, had a nice lunch and explored the local area.
day='2025-12-19'
mapday(day)
summarise_obs(day)
So far I have uploaded a total of 8 observations. With 3 Reptilia, 2 Insecta, 1 Plantae, 1 Mollusca, 1 Amphibia tentatively identified at different taxonomic levels. The three more interesting observations so far are Flat-tailed House Gecko, Tokay Gecko, Termites.
Check all the observations here
December 20: First day in Bali
First day! we are all excited: Abejandra wants to go through the mud in a bike before shopping some jewelry, Ada wants to taste some coffee and tea, I am looking for some Batik garments for this trip… can’t decide, let’s do it all!
Easy to do all this around Ubud, and best thing is that we can stop at any time to take fotos of the local critters.
day='2025-12-20'
mapday(day)
summarise_obs(day)
So far I have uploaded a total of 40 observations. With 22 Insecta, 6 Aves, 5 Lepidoptera, 5 Odonata, 3 Plantae, 3 Papilionoidea, 2 Reptilia, 2 Myriapoda tentatively identified at different taxonomic levels. The three more interesting observations so far are Sea-green Northern Jumper, Toxeus maxillosus, Eastern Cattle-Egret.
Check all the observations here
December 21: Busy day with dances, jewelry and monkeys
We attended a dance performance in the morning, then headed to a silver jewelry workshop, and finally visited the Monkey Forest in the afternoon.
The day started in Barong Seraya Budaya with a beautiful life performance f the Barong and Kris Dance.
Apparently there is not so much fun in buying silver rings as in making them yourself. So we signed up for a jewelry making class at Artika Silver, Gael and Abejandra made some nice rings, while Ada and I took a walk to a nearby rice field. I took the opportunity to photograph some of the insects around the area.
Then we visited the Pura Desa Lan Puseh Sukawati temple, to learn more about the Balinese culture and traditions.
Then we did some grocery shopping, had an Italo-colombian lunch at Casa Curandera, near Ubud, and headed to the Sacred Monkey Forest Sanctuary. This was a fun experience for the whole family. The monkeys were quite active and curious, and there were so many different insects and birds around, I found plenty of opportunities for photography.
day='2025-12-21'
mapday(day)
summarise_obs(day)
So far I have uploaded a total of 36 observations. With 19 Insecta, 10 Aves, 5 Lepidoptera, 5 Papilionoidea, 4 Odonata, 2 Mammalia, 1 Plantae, 1 Reptilia tentatively identified at different taxonomic levels. The three more interesting observations so far are Pacific Swallow, Javan Munia, Spotted Dove.
Check all the observations here
December 22: walking around Ubud
Ubud is a charming town with a lot to offer. Ada and I spent the day walking the ridge walk, visiting local markets, art galleries, and temples. I found a variety of insects and plants to photograph. Our beloved hijos were a bit more relaxed, but still joined us in some activities. Abejandra came with me to the post office and we send some nice postcards to our friends and family.
day='2025-12-22'
mapday(day)
summarise_obs(day)
So far I have uploaded a total of 42 observations. With 32 Insecta, 14 Lepidoptera, 13 Papilionoidea, 5 Aves, 4 Odonata, 2 Plantae, 2 Reptilia tentatively identified at different taxonomic levels. The three more interesting observations so far are Cethosia tambora, Phytomia chrysopyga, Green Junglefowl.
Check all the observations here
December 23: Up to Tegallalang
Our time at the Villa in Ubud was coming to an end, so we decided to make the most of our last full day there. I had an early morning walk to the nearby rice fields, when I got back we packed our bags and checked out, then headed out to some activites south of Ubud and then north to Tegallalang. This was a day to experience the cultural landscape of Bali and the manifestations of the Tri Hita Karana Philosophy that draws together the realms of the spirit, the human world and nature.
Our first stop was the Tegenungan Waterfall where Ada and Abejandra had a lot of fun in the water. I had a refreshing dip, but then took my cameras to continue my quest to find new observations for iNat.
We then visited the Pura Tirta Empul. It is a fascinating place, one of the island’s most revered Hindu temples. The temple is dedicated to Vishnu, the god of water, and is celebrated for its sacred spring, which has drawn worshippers for centuries seeking spiritual cleansing and renewal. It is a beautiful place to admire the insects, fish and plants surrounding the sacred springs.
We continued our journey with a nice lunch and a visit to the Tegallalang rice terrace.
We headed to our new accommodation in the late afternoon. We checked in at Mirah Guesthose and relaxed for the evening.
day='2025-12-23'
mapday(day)
summarise_obs(day)
So far I have uploaded a total of 71 observations. With 42 Insecta, 21 Odonata, 13 Lepidoptera, 11 Papilionoidea, 10 Aves, 6 Plantae, 3 Mammalia, 2 Fungi, 1 Actinopterygii, 1 Amphibia, 1 Mollusca, 1 Reptilia tentatively identified at different taxonomic levels. The three more interesting observations so far are Tenodera, Spotted Dove, Christmas Swallowtail.
Check all the observations here
December 24: Snorkel in Padang Bai and more silver
The plan was snorkeling in Padang Bai, and we had a nice trip and beatiful time below water. Abejandra and Gael were very excited to see all the colourful fish and remembering Finding Nemo. Only bad thing was that I my old underwater camera gave up the ghost after many years of service, and the new one I bought recently was not really the same quality, still I managed to get some fotos of the underwater life.
The swim and the lunch afterwards made our day, but when discussing plans for the afternoon, the conversation headed towards silver jewelry again. After consulting options with our driver, we decided to go back to the same shop. Abejandra is now the lady of the rings and Gael loves doing manual work and musing about metals and stones, so they were happy to go back.
Back to the guesthouse to relax and find a nice place for our memorable Christmas dinner in the exotic settings of Bali.
day='2025-12-24'
mapday(day)
summarise_obs(day)
So far I have uploaded a total of 59 observations. With 52 Actinopterygii, 1 Aves, 1 Insecta tentatively identified at different taxonomic levels. The three more interesting observations so far are Pinktail Triggerfish, Raccoon Butterflyfish, Eurasian Tree Sparrow.
Check all the observations here
December 25: laid back Christmas day
Not much planned for today, just a laid back Christmas day at the guesthouse. We had a nice breakfast, relaxed by the pool, some walks around the local area, exploring the nearby temple. I had a scare because my other camera was acting up, but after some troubleshooting I managed to get it working again. I managed to take some nice photos of the local insects, frogs and birds around the guesthouse.
day='2025-12-25'
mapday(day)
summarise_obs(day)
So far I have uploaded a total of 50 observations. With 33 Insecta, 12 Lepidoptera, 11 Aves, 9 Papilionoidea, 5 Odonata, 4 Amphibia, 1 Reptilia tentatively identified at different taxonomic levels. The three more interesting observations so far are Feral Pigeon, Birds, Giant Golden Orbweaver.
Check all the observations here
December 26: Nusa dua water activities
We checked out from the guesthouse and headed south to Nusa Dua for some water activities.
We had a great time paragliding, jet skiing, and going underwater. We had a big lunch at a beachside restaurant, and then headed back to Balangan Seaview Bungalow for the final leg of our trip.
day='2025-12-26'
mapday(day)
summarise_obs(day)
So far I have uploaded a total of 25 observations. With 10 Insecta, 7 Aves, 6 Actinopterygii, 6 Lepidoptera, 5 Papilionoidea, 1 Odonata, 1 Mammalia, 1 Reptilia tentatively identified at different taxonomic levels. The three more interesting observations so far are Horsfield’s Treeshrew, Pacific Reef Heron, Yellow-vented Bulbul.
Check all the observations here
December 27: Day trip to Nusa Penida
Trip to Nusa Penida. Rained most of the day, we manage to do do a short snorkeling trip to three localities and have lunch on the island, but then cancelled the rest of our journey and ferried back to Sanur. Spend the afternoon at our accomodation in Balangan Sea Bungalow.
day='2025-12-27'
mapday(day)
summarise_obs(day)
So far I have uploaded a total of 10 observations. With 2 Aves, 2 Actinopterygii, 2 Reptilia, 1 Plantae, 1 Insecta, 1 Lepidoptera, 1 Papilionoidea, 1 Mollusca tentatively identified at different taxonomic levels. The three more interesting observations so far are Sooty-headed Bulbul, Blacklip Butterflyfish, Blue Angelfish.
Check all the observations here
December 28: Exploring Balangan
Another laid back day at Balangan. Relaxed by the pool, explored the local area, took some fotos of the local critters. Had a nice lunch in front of the beach, an afternoon of swimming and boardgames with the family, a dinner with sunset views and started packing our bags for the trip back home.
day='2025-12-28'
mapday(day)
summarise_obs(day)
So far I have uploaded a total of 54 observations. With 40 Insecta, 27 Lepidoptera, 27 Papilionoidea, 5 Aves, 4 Plantae, 1 Reptilia, 1 Mollusca, 1 Amphibia tentatively identified at different taxonomic levels. The three more interesting observations so far are Pycnonotus aurigaster aurigaster, Peacock Flower Subfamily, Eurasian Tree Sparrow.
Check all the observations here
December 29: Tanah Lot and Departure
Last day in Bali. Checked out from the bungalow, headed to Tanah Lot temple for some sightseeing and photography. Had a nice lunch overlooking the temple, and decided to spend our rupiahs in some last minute shopping for souvenirs. On the way to the airport, we stopped to visit our drivers family and meet his kids, then headed to the airport for our flight back home. It took longer than expected due to traffic, but we made it in time for our flight.
day='2025-12-29'
mapday(day)
summarise_obs(day)
So far I have uploaded a total of 15 observations. With 6 Aves, 3 Insecta, 2 Myriapoda, 1 Plantae, 1 Lepidoptera, 1 Papilionoidea, 1 Actinopterygii, 1 Reptilia tentatively identified at different taxonomic levels. The three more interesting observations so far are Common Water Monitor, White-breasted Woodswallow, Yellow-vented Bulbul.
Check all the observations here
And this is the end of our Bali trip!
So nice to revisit these memories and share some of the fotos I took during the trip.
I found that this code helped me to find a a nice and reproducible way to document my observations and combine these with notes of my experiences during the trip. I hope you enjoyed reading about it, that you find the photos interesting and that the code is useful for other.
I still have many more fotos to upload to iNaturalist, so stay tuned for more updates in the future!
