Skip to content

Joining the modalities in space and time

The point of the release is that four independently collected modalities can be put on the same spatial and temporal footing. This page says which columns do that, what their precision is, and where the traps are. A runnable example is at examples/join_camera_acoustic_by_site_hour.py (download the dataset first, as shown in Darwin Core tables).

Temporal keys

Column Tables Meaning
eventDate, eventTime all Occurrence tables Local date and time of the record. Camera-trap values are the corrected timestamps, not the camera clock. An eventDate interval (start/end) means the record is only known to lie in that span
corrected_timestamp, correction_method, correction_confidence images.csv (camera traps) Per-image corrected capture time, the method that produced it (18 methods, supplement S5), and its confidence
timestampPrecision, timeEligible arrival traces second or day; whether the record can enter inter-arrival statistics

Three temporal traps

  • Camera clocks were wrong on 93.9% of images (defect 1). Use the corrected values; the burned-in overlay in the image shows the raw clock.
  • The 2025-08-11 drone flights had a date but no time (defect 15). Their times were recovered from the video container creation times, with the clock convention settled against the field log (provenance/drone_time_recovery_20250811.json in the drone dataset); the DJI convention is inferred, so treat those times as minute-level.
  • Acoustic Events are recording windows, not instants: a 5- or 15-minute WAV. An acoustic Occurrence's time is the recording start.

Campaign windows: June 30 – July 3 (campaign 1), August 4 – 28 (campaign 2), September 8 (campaign 3). The deployment Events carry each sensor's actual eventDate interval, which is the right denominator for effort.

Spatial keys

Modality locationID Coordinates Uncertainty
Camera traps Site code TW01 … TW11 Deployment Event: camera position. Occurrences inherit the site; animal positions are not inferred in the released table 5 m
Bioacoustics Site code Deployment Event: recorder position 50 m
Drones Flight identifier (20250818_parrot_01) Deployment Event: flight centroid; MediaEvent: video position; Occurrences: ground-projected box position where georef_status = ok 100 m (flight), per-row for projections
Citizen science none Park-level point on the observation session 5,000 m

Fixed sensors are joined by site code and campaign, because several sensors moved between campaigns (the TW07 recorder, the TW09 camera after August 18; defect 6). Drones and citizen science have no site code and are joined by coordinates or by date.

Site codes are shared across modalities. In campaign 2, camera traps and recorders were co-located at TW02, TW06, and TW09; the supplemental material (S2, S3) lists every deployment with its coordinates and dates.

The Event hierarchy

All tables share one hierarchy rooted at TheWilds_2025. Walking parentEventID from an Occurrence's eventID reaches the deployment (or flight, or session), then the modality's Project, then the study root. That is how an Occurrence finds its campaign, its sensor configuration, and its deployment coordinates.

Example: camera and acoustic co-occurrence by site-hour

import pandas as pd

def load(root):
    ev = pd.read_csv(f"{root}/events.csv", dtype=str, keep_default_na=False)
    oc = pd.read_csv(f"{root}/occurrences.csv", dtype=str, keep_default_na=False)
    return ev, oc

# Download first:
#   huggingface-cli download --repo-type dataset ABC-Center/smartwilds_metadata --local-dir smartwilds_metadata
cam_ev, cam = load("smartwilds_metadata/smartwilds-camera-traps/data")
aru_ev, aru = load("smartwilds_metadata/smartwilds-bioacoustics/data")

# 1. Temporal key: corrected local timestamp, truncated to the hour.
for df in (cam, aru):
    df["t"] = pd.to_datetime(df.eventDate + " " + df.eventTime, errors="coerce")
    df["hour"] = df.t.dt.floor("h")

# 2. Spatial key: the site code on fixed sensors (TW01 ... TW11), plus the
#    campaign window from the deployment Event, because a few sensors moved
#    between campaigns.
def campaign(ev):
    dep = ev[ev.eventType == "SensorDeployment"].set_index("eventID")
    return dep.eventDate.str.slice(0, 7)          # "2025-06" or "2025-08"
cam["campaign"] = cam.eventID.map(cam_ev.set_index("eventID").parentEventID).map(campaign(cam_ev))
aru["campaign"] = aru.eventID.map(aru_ev.set_index("eventID").parentEventID).map(campaign(aru_ev))

# 3. Co-occurrence per site-hour: camera detections next to acoustic detections.
cam_h = cam.dropna(subset=["hour"]).groupby(["campaign", "locationID", "hour"]).size().rename("camera_occurrences")
aru_h = aru.dropna(subset=["hour"]).groupby(["campaign", "locationID", "hour"]).size().rename("acoustic_occurrences")
joined = pd.concat([cam_h, aru_h], axis=1).fillna(0).astype(int)
both = joined[(joined.camera_occurrences > 0) & (joined.acoustic_occurrences > 0)]
print(f"site-hours with both modalities: {len(both):,} of {len(joined):,}")
print(both.reset_index().groupby("locationID").size().rename("site_hours_with_both").to_string())

On the current tables this reports 403 site-hours (of 2,299 with any detection) in which both a camera and a recorder at the same site produced Occurrences, at TW02, TW06, and TW09.

Arrival traces

For workload or systems questions, analysis/arrival_traces_v2/ in the study-level dataset already has the campaign-2 streams at two layers with checksums and a contract describing units and inclusion rules; see Arrival traces.