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.

In [1]:
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.

In [2]:
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.

In [3]:
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()
No description has been provided for this image

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.

In [4]:
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()
No description has been provided for this image

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.

In [5]:
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()
No description has been provided for this image

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.ipynb notebook).
  • 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.

In [6]:
# ⚠️ 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"