Monday, August 31, 2026

Using GBIF RDF to find applicable taxonomic literature

Recently, after the wonderful work of Andra Waagmeester, Jerven Bolleman, and Hannah Bast, an RDF version of the data on the Global Biodiversity Information Facility (GBIF) was made available through a SPARQL endpoint on QLever.

Roderic Page writes about it in iPhylo:

What I like most about this work is that it may help catalyse further exploration of linked data for biodiversity. There have been lots of discussions of the years, and lots of small-scale (typically short-lived) demos, but nothing on the scale of having billions of records available to query. Given how central GBIF is to biodiversity informatics, there’s now an incentive to explore links to other datasets […].

Now, it just so happens that the Library of Identification Resources (LoIR) is available as an RDF dataset. In fact, the project has a great use case for querying GBIF occurrences. The search engine ranks the taxonomic literature in LoIR based on its applicability on an observation. For example, if I have seen a shield bug in the Netherlands (Fig. 1) and want to identify which species it is, it will generate a checklist of shield bugs in the Netherlands from GBIF occurrences, and find the identification keys with the highest coverage of the Dutch checklist.


Figure 1: Pentatomidae, 2.ix.2021, Eindhoven, the Netherlands (link)

This is currently implemented in JavaScript, but with the SPARQL endpoint for GBIF data, it can also be implemented in SPARQL. The taxonomic scope of shields bugs (Pentatomidae) is represented by gbifsp:9650, the geographic scope of Netherlands by "NL". First, the checklist is generated:

SELECT ?taxon ?scientificName (COUNT(?occurrence) AS ?amount) WHERE {
  # Species in gbifsp:9650 and their scientific name
  ?taxon skos:broader* gbifsp:9650 ;
         gbifterms:rank gbifrank:species ;
         dwc:scientificName ?scientificName .
  # Occurrences of those species in NL
  ?occurrence dwc:countryCode "NL" ;
              dwciri:toTaxon ?taxon .
} GROUP BY ?taxon ?scientificName

This part of the query has to then be repeated (as far as I can tell) to get the number of species and the total amount of occurrences. We can combine this query with LoIR using a SERVICE <https://qlever.dev/api/gbif> statement. Following, resources in the LoIR with those species are selected, and the number of species (and their occurrences) are calculated as a proportion of the respective totals.

SELECT
  ?resource
  (COUNT(?taxon)/?totalCount AS ?pTax)
  (SUM(?amount)/?totalAmount AS ?pObs)
WHERE {
  SERVICE <https://qlever.dev/api/gbif> {
    # ...
  }

  # Match with resource checklists
  ?resource dcterms:subject/dwc:taxonID ?taxon .
} GROUP BY ?resource ?totalCount ?totalAmount

Finally, some additional metadata gets queried, resulting in the full query:

PREFIX dcterms: <http://purl.org/dc/terms/>
PREFIX dwc: <http://rs.tdwg.org/dwc/terms/>
PREFIX dwciri: <http://rs.tdwg.org/dwc/iri/>
PREFIX gbifrank: <https://rs.gbif.org/terms/rank/>
PREFIX gbifsp: <https://www.gbif.org/species/>
PREFIX gbifterms: <https://rs.gbif.org/terms/>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

SELECT ?resource ?title (YEAR(?date) AS ?year) ?pTax ?pObs WHERE {
  {
    SELECT
      ?resource
      (COUNT(?taxon)/?totalCount AS ?pTax)
      (SUM(?amount)/?totalAmount AS ?pObs)
    WHERE {
      SERVICE <https://qlever.dev/api/gbif> {
        # Get species of Pentatomidae (gbifsp:9650) occuring in the Netherlands ("NL") and their count
        {
          SELECT ?taxon ?scientificName (COUNT(?occurrence) AS ?amount) WHERE {
            ?taxon skos:broader* gbifsp:9650 ;
                  gbifterms:rank gbifrank:species ;
                  dwc:scientificName ?scientificName .
            ?occurrence dwc:countryCode "NL" ;
                        dwciri:toTaxon ?taxon .
          } GROUP BY ?taxon ?scientificName
        }
        # Repeat to get the count of species and total amount of occurrences
        {
          SELECT (COUNT(?taxon) AS ?totalCount) (SUM(?amount) AS ?totalAmount) WHERE {
            {
              SELECT ?taxon (COUNT(?occurrence) AS ?amount) WHERE {
                ?taxon skos:broader* gbifsp:9650 ;
                       gbifterms:rank gbifrank:species ;
                       dwc:scientificName ?scientificName .
                ?occurrence dwc:countryCode "NL" ;
                            dwciri:toTaxon ?taxon .
              } GROUP BY ?taxon
            }
          }
        }
      }

      # Match with resource checklists
      ?resource dcterms:subject/dwc:taxonID ?taxon .
    } GROUP BY ?resource ?totalCount ?totalAmount
  }

  # Get additional metadata
  ?work dcterms:hasPart ?resource ;
        dcterms:title ?title ;
        dcterms:issued ?date .
} ORDER BY DESC(?pTax)

This returns the following table, which is pretty similar to the results obtained with the JavaScript-based algorithm.

Resource Title Year Prop. species Prop. occurrences
B1288:1 Les Punaises Pentatomoidea de France 2015 0.87 0.96
B2473:1 Veldgids Wantsen. Pentatomorpha 2026 0.83 0.96
B2473:2 Veldgids Wantsen. Pentatomorpha 2026 0.83 0.96
B887:1 Veldgids wantsen. Deel 1 2016 0.70 0.83
B937:1 Soortzoeker Schildwantsen van Nederland 2018 0.70 0.83
B2569:1 Щитники Средней Азии (Hemiptera, Pentatomoidea) 1965 0.68 0.83
B3094:1 Tabelle per la determinazione dei piu comuni eterotteri italinai (Heteroptera) 1989 0.62 0.79
B1376:1 Щитники 1961 0.60 0.80
B2890:1 Revisión de los pentatómidos ibéricos (Hemiptera): Parte II. Tribus Aeliini Stål, 1872, Stagonomini nov. nom. (= Eysarcorini auct…) y Carpocorini Distant, 1902 1974 0.60 0.99

It doesn’t account for synonyms yet, nor for the additional factors affecting the ranking of the results in the original implementation. It also cannot find resources without checklists by comparing taxonomic and geographic scopes yet. But this is all possible in SPARQL, and without enormously complicated queries! Overall, it seems like a very viable use case for the GBIF RDF.

References