Mapping recreational dive logs onto Darwin Core (OBIS-ENV-DATA), and three things I am unsure about

Recreational divers are in the water constantly, and almost none of what they see is written down in a form anyone can use. That gap is old and well known. Diveboard published recreational dive observations to GBIF roughly a decade ago and their dataset documentation was refreshingly blunt about the catch: divers rarely inventory a dive, they record the interesting animal, and the resulting data skews hard toward large and charismatic fish.

I have been building a dive-logging app, DIVR, that exports to Darwin Core. What follows is the mapping rather than a pitch for the app. Some of these decisions were obvious. Several were not, and two of them we got wrong first. The three questions I would actually like answers to are at the bottom.

A dive is an event, not a bag of occurrences

The first decision was to publish as a sampling event rather than as occurrence-only records. A recreational dive is unusually well suited to this: it has a start, a duration, a location, a depth profile and one observer. That is a sampling event with effort attached, even if the observer was not trying to sample anything.

So the archive is the OBIS-ENV-DATA profile: an Event core, an Occurrence extension and an ExtendedMeasurementOrFact extension.

Darwin Core term Where it comes from
eventID DIVR:event:<dive id>
eventDate dive date, date only, no time
decimalLatitude / decimalLongitude dive site coordinates, WGS84
maximumDepthInMeters max depth recorded by the diver or their computer
samplingProtocol the literal string recreational scuba diving
sampleSizeValue / sampleSizeUnit dive duration / minutes
locality / waterBody / countryCode dive site record

sampleSizeValue is the term that earns the whole structure. Without it, a sighting is an anecdote with coordinates. With it, you can at least ask how much looking produced a given record, which is the beginning of being able to say anything about effort.

Species identity: WoRMS or nothing

Every occurrence carries a scientificNameID in the form urn:lsid:marinespecies.org:taxname:<AphiaID>, and the export validator rejects anything that does not match that pattern.

This is enforced at capture, not at cleanup. A diver picks a species from a catalogue that is already bound to WoRMS; there is no free-text species field that later gets fuzzy-matched. The cost is that divers cannot record something outside the catalogue. We decided that a missing record is cheaper than an unresolvable one.

Two consequences worth stating:

  • We re-sync against WoRMS and refuse to export a sighting whose species record has not been verified in the last 30 days. Taxonomy moves, and an archive built on a stale mirror is quietly wrong.
  • When a name has changed since the dive, the name in use at the time goes into verbatimIdentification rather than originalNameUsage. That was corrected for us during review by EMODnet Biology at VLIZ.

Absence, and the coral anchor

Presence-only data is much less useful than it looks, so divers can mark a species as looked-for-and-not-seen. Those export as occurrenceStatus=absent with individualCount=0. The validator errors if an absent record ever carries a non-zero count.

The stranger problem was coral condition. We ask divers to rate coral health and coral coverage, and the natural home for those is the eMoF extension. But the relevant BODC term for coverage, PCOV7736, is defined as coverage “of biological entity specified elsewhere”. It needs an occurrence to hang from, and “the coral on this reef” is not a species.

So a coral-bearing dive synthesises a phylum-level Cnidaria occurrence (urn:lsid:marinespecies.org:taxname:1267) purely as an anchor, and the coverage and health measurements attach to that. I am genuinely unsure this is the right call, and it is the thing I would most like to be argued out of. It creates an occurrence record for something nobody observed as a taxon.

Where the vocabularies run out

Three measurements have no usable identifier, and rather than force a bad match we emit them with a null measurementTypeID:

  • Coral health rating. A subjective 1 to 5 scale invented by us. There is no BODC term for it and there should not be. It ships as a bare number with a label saying exactly what it is.
  • Horizontal visibility. Checking the NERC vocabulary turned up a vertical Secchi-disk term, which is a different measurement from how far a diver can see sideways. We left the ID null rather than reuse it.
  • Coverage rating, our categorical version of the same, for the same reason.

Abundance does map: DIVR’s three-bucket abundance maps onto SACFOR as R, O and F. Note that this means our data can never express S, A or C, because the app does not offer that resolution. A consumer treating our SACFOR column as a full SACFOR scale would be misreading it.

I would rather publish an honest null than a plausible wrong URI, but I would like to know if that is the community’s preference too.

What never reaches the archive

Filtering happens before validation, not during it:

  • Consent. Publication is opt-in and defaults to off. A dive is exported only if its owner turned citizen-science sharing on, has not set do-not-share, and is not in the deletion queue. A withdrawal removes them from every future export.
  • Private dives are excluded at the query level and never fetched.
  • Non-marine sites are dropped via a PostGIS marine-polygon check plus a water-type check.
  • Impossible coordinates are dropped, which mostly means catching NaN rather than genuinely bad positions.

Coordinate uncertainty is an editorial choice, not a measurement

This is the one I would push back on hardest if I were reviewing someone else’s archive. coordinateUncertaintyInMeters in our archive is not derived from a GPS accuracy figure. It is a constant chosen per import format, 100 m for a dive computer with an embedded fix and 150 m for a log matched to a known dive site, with a 100 m default for everything else.

That is defensible for a shore dive on a named site and much shakier for a drift dive. It is an assertion about our data model, not an observation. Publishing it as though it were measured would be worse than publishing it coarse, but I am aware those are not the only two options.

The bias we cannot engineer away

None of the above touches the real problem, and it is the same one Diveboard described.

Divers log the dive where something happened. A dive where nothing much was seen is disproportionately not logged at all. So an occurrence rate computed from this data is not P(species present) and it is not even P(species seen | dive). It is P(species recorded | dive logged), and logging propensity correlates with the outcome. That is missing-not-at-random, and it inflates levels.

Trends survive this better than levels do, provided the selection mechanism is stable. Levels do not survive it at all. Effort recorded via sampleSizeValue helps bound the problem but does not solve it, because the selection happens at the moment of logging, not during the dive.

The fix that actually works is eBird’s: a complete-checklist flag, where the observer states that they recorded everything they identified. That is on our roadmap and not in the current archive, which means the honest reading of our data today is “trends, cautiously” rather than “rates”.

Open questions

Three things I would take advice on:

  1. Is a synthetic phylum-level anchor occurrence an acceptable way to attach habitat-level measurements, or is there an established pattern we missed?
  2. Is a null measurementTypeID with a clear label preferable to a loose vocabulary match? Our assumption is yes, and consumers may disagree.
  3. Where does a caveat about self-selection belong so that it actually travels with the data, rather than sitting in a post nobody reads next to the download button?

The archive is CC-BY 4.0. Records flow out of this towards OBIS and GBIF rather than being pulled from them. If you want to pull it apart, I would rather hear about a mistake now than after it propagates.

1 Like