Camera Trap Repository: Amsterdamse Waterleidingduinen (Netherlands)¶
Category: Terrestrial Wildlife · Size: 21.6 GB · Format: ZIP License: CC-BY-4.0 · Zenodo record · Data sheet on the CSDH
47,597 annotated camera-trap images from three pilot studies in the Amsterdam dunes, with 2,779 observations of 20 species, in the standard Camtrap DP format.
The data is mounted read-only at /srv/data/camera-trap-amsterdam/.
Save anything you produce in your personal folder (~/).
⚠️ Large dataset (21.6 GB). Your session has 4 GB RAM and your home folder is shared — don't extract the whole archive. Read the entries you need straight from inside the ZIP (see below); if you must extract, take only specific files, not everything.
What's in the dataset¶
Each pilot (pilot1.zip, pilot2.zip, pilot3.zip) is a Camtrap DP data
package — the community standard for camera-trap data. The heavy .jpg files
live in media/, but every scientific fact we need is in four small CSVs:
| File | What it holds |
|---|---|
deployments.csv |
one row per camera placement: coordinates, dates, habitat |
media.csv |
one row per image |
observations.csv |
one row per detection event: species, timestamp, count |
events.csv |
groups media into events |
We only touch the CSVs — never the images — so this stays well inside the 4 GB budget. Let's confirm the archive layout first.
from pathlib import Path
import zipfile
DATA = Path('/srv/data/camera-trap-amsterdam')
for z in sorted(DATA.glob('*.zip')):
print(f"{z.name} ({z.stat().st_size/1e9:,.2f} GB)")
with zipfile.ZipFile(z) as zf:
for n in zf.namelist():
if n.lower().endswith('.csv'):
print(' ', n)
pilot1.zip (11.79 GB)
pilot1/deployments.csv
pilot1/events.csv
pilot1/media.csv
pilot1/observations.csv
pilot2.zip (0.35 GB)
pilot2/deployments.csv
pilot2/events.csv
pilot2/media.csv
pilot2/observations.csv
pilot3.zip (9.44 GB)
pilot3/deployments.csv
pilot3/events.csv
pilot3/media.csv
pilot3/observations.csv
Load the observations table¶
We read observations.csv straight from inside each ZIP (no extraction) and
stack the three pilots together. The observationType column tells us whether a
detection is an animal, a blank (false trigger), a human, or a vehicle —
we keep only the animals.
import pandas as pd
import numpy as np
frames = []
for name in ['pilot1', 'pilot2', 'pilot3']:
with zipfile.ZipFile(DATA / f'{name}.zip') as zf:
with zf.open(f'{name}/observations.csv') as f:
df = pd.read_csv(f, low_memory=False)
df['pilot'] = name
frames.append(df)
obs = pd.concat(frames, ignore_index=True)
print('All observations:', obs.shape)
print(obs['observationType'].value_counts())
animals = obs[obs['observationType'] == 'animal'].copy()
print(f'\nAnimal detections: {len(animals):,} across {animals["scientificName"].nunique()} species')
All observations: (8903, 29) observationType blank 6064 animal 2427 vehicle 199 unknown 154 human 56 unclassified 3 Name: count, dtype: int64 Animal detections: 2,427 across 21 species
Which species show up most?¶
The dunes are dominated by a handful of species. We attach common names to the
Latin scientificName so the charts read easily, then count detections.
common = {
'Oryctolagus cuniculus': 'European rabbit',
'Vulpes vulpes': 'Red fox',
'Dama dama': 'Fallow deer',
'Corvus corone': 'Carrion crow',
'Erinaceus europaeus': 'European hedgehog',
'Pica pica': 'Eurasian magpie',
'Mustela putorius': 'European polecat',
'Apodemus sylvaticus': 'Wood mouse',
'Lullula arborea': 'Woodlark',
'Sturnus vulgaris': 'Common starling',
'Martes martes': 'Pine marten',
'Scolopax rusticola': 'Eurasian woodcock',
}
animals['common'] = animals['scientificName'].map(common).fillna(animals['scientificName'])
counts = animals['scientificName'].value_counts()
labels = [f"{common.get(s, s)}" for s in counts.index]
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style='whitegrid')
top = counts.head(10)[::-1]
top_labels = [common.get(s, s) for s in top.index]
fig, ax = plt.subplots(figsize=(8, 5))
ax.barh(top_labels, top.values, color=sns.color_palette('viridis', len(top)))
for y, v in enumerate(top.values):
ax.text(v + max(top.values)*0.01, y, f'{v:,}', va='center', fontsize=9)
ax.set_xlabel('Number of detection events')
ax.set_title('Most-detected animals in the Amsterdam dunes (3 pilots)')
plt.tight_layout()
plt.show()
A few species carry almost all the signal — the European rabbit, red fox and fallow deer. Those three have enough detections to ask a real ecological question: when during the day is each one active?
Activity by hour — do the species split the day?¶
Camera timestamps in eventStart are ISO strings like
2022-03-09T19:30:18+0100. The two digits before the timezone offset are the
local wall-clock hour, which is exactly what we want for a diel (24-hour)
activity pattern. We bin detections by hour and normalise each species to its own
total, so we compare shapes rather than raw abundance.
animals['hour'] = animals['eventStart'].str[11:13].astype(int)
focus = ['Oryctolagus cuniculus', 'Vulpes vulpes', 'Dama dama']
colors = {'Oryctolagus cuniculus': '#1b9e77',
'Vulpes vulpes': '#d95f02',
'Dama dama': '#7570b3'}
fig, ax = plt.subplots(figsize=(10, 5))
for sp in focus:
h = animals.loc[animals['scientificName'] == sp, 'hour']
frac = h.value_counts().reindex(range(24), fill_value=0).sort_index()
frac = frac / frac.sum()
ax.plot(frac.index, frac.values, marker='o', ms=4,
label=f'{common[sp]} (n={len(h):,})', color=colors[sp])
# shade the night (approx. sunset-to-sunrise band for the dunes)
ax.axvspan(0, 6, color='0.85', alpha=0.5, zorder=0)
ax.axvspan(21, 23, color='0.85', alpha=0.5, zorder=0)
ax.set_xticks(range(0, 24, 2))
ax.set_xlabel('Hour of day (local time)')
ax.set_ylabel('Fraction of that species\' detections')
ax.set_title('Diel activity: rabbit vs. fox vs. fallow deer')
ax.legend()
plt.tight_layout()
plt.show()
The same pattern on a 24-hour clock¶
A polar plot makes the day/night split intuitive: midnight is at the top, noon at the bottom, and the radius is how active the species is at that hour.
fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={'projection': 'polar'})
ax.set_theta_zero_location('N')
ax.set_theta_direction(-1)
theta_edges = np.linspace(0, 2*np.pi, 25)
for sp in focus:
h = animals.loc[animals['scientificName'] == sp, 'hour']
frac = h.value_counts().reindex(range(24), fill_value=0).sort_index().values
frac = frac / frac.sum()
vals = np.append(frac, frac[0])
ax.plot(theta_edges, vals, color=colors[sp], lw=2, label=common[sp])
ax.fill(theta_edges, vals, color=colors[sp], alpha=0.10)
ax.set_xticks(np.linspace(0, 2*np.pi, 24, endpoint=False))
ax.set_xticklabels([f'{h:02d}' for h in range(24)], fontsize=8)
ax.set_yticklabels([])
ax.set_title('24-hour activity clock (midnight at top)', pad=20)
ax.legend(loc='upper right', bbox_to_anchor=(1.25, 1.1))
plt.tight_layout()
plt.show()
What the plots suggest. All three species avoid the middle of the day. The
red fox is strongly nocturnal (a night-time hump), the fallow deer peaks
around dusk and dawn (crepuscular), and the abundant rabbit is active across
the night with dawn/dusk peaks. Their busiest hours don't fully line up — a hint
of temporal niche partitioning, where species active in the same place stagger
when they move to reduce encounters. Try adding a fourth species, splitting by
season (eventStart month), or comparing cameras (deploymentID) to see whether
the pattern holds.
Your turn¶
This is just the starting point. Some ideas:
- Check the dataset challenge on its CSDH data sheet.
- Work on a copy: right-click the file → Duplicate (or Save Notebook As…). Your changes only live in your Hub space — they're never pushed to GitHub.
- Edited this notebook and want the original back? Use the Restore cell
below (or the
restore.ipynbnotebook). - Questions and results: on the platform forum.
Attribution: data from Camera Trap Repository: Amsterdamse Waterleidingduinen (Netherlands), license CC-BY-4.0. Notebook from the Citizen Science Data Hub (CSDH) — Fundación Ibercivis.
# ⚠️ RESTORE: this DISCARDS YOUR CHANGES to this notebook and resets it to the original.
# 1. Uncomment the line below (remove the #) 2. Run this cell
# 3. Then: menu File → Reload Notebook from Disk
# !git -C ~/citizen-science-data fetch -q origin && git -C ~/citizen-science-data checkout origin/main -- camera-trap-amsterdam.ipynb && echo "Restored. Now: File → Reload Notebook from Disk"