logo Netzschleuder network catalog, repository, and centrifuge

History of globi_interactions

The changes to the source code of this entry in the repository, newest first, each with the differences it made to the entry's files. They are also listed on GitLab.

[entries] New entry: globi_interactions by Tiago de Paula Peixoto 169dd71
diff --git a/entries/globi_interactions/__init__.py b/entries/globi_interactions/__init__.py
new file mode 100644
index 0000000..c1d4b85
--- /dev/null
+++ b/entries/globi_interactions/__init__.py
@@ -0,0 +1,173 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2026 Tiago de Paula Peixoto <tiago@skewed.de>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import pyarrow
+import pyarrow.csv
+import pyarrow.compute as pc
+
+from .. import *
+
+title = "Global Biotic Interactions (GloBI, 2026)"
+description = r"""Species interactions compiled by Global Biotic Interactions (GloBI), an open infrastructure that indexes and integrates existing datasets of interactions between organisms: predator-prey (e.g. `eats`, `preysOn`), parasite-host (e.g. `parasiteOf`, `endoparasiteOf`, `hasHost`), pathogen-host, pollination and flower visits (`visitsFlowersOf`, `pollinates`), symbiosis, and many others, from hundreds of studies, museum collections, citizen-science observations (e.g. iNaturalist), and literature-mining efforts. The data are those of the "interpreted data products" release 0.11 of September 2026, in which the names of the organisms given in the datasets were linked to taxonomic name authorities (e.g. Catalogue of Life, NCBI Taxonomy, GBIF, WoRMS, ITIS, EOL, Wikidata).
+
+Nodes are taxa (mostly species and genera), identified by their interpreted name; names that could not be linked to a taxonomic authority are kept as given in the dataset (their `taxon_id` is `no:match`). A directed edge from taxon A to taxon B is an interaction record, e.g. "A eats B" or "A is a parasite of B", in the direction of the source of the record; a pair of taxa is connected by as many edges as there are records of interactions between them, each with its type, the dataset it comes from, and, if given, the location and date of the observation.
+
+- `interactions`: the interaction records.
+- `refuted`: the refuted interaction records, i.e. records stating that an interaction does *not* happen (e.g. that a predator does not eat a prey), which GloBI publishes separately.
+
+Left out are the occurrence-level details of the records (life stage, sex, body part, physiological state and basis of record of the organisms, the specimens' catalog numbers, and the locality names), the references (bibliographic citations of the studies, which are given by GloBI for each dataset), and the records whose source or target has no name at all (their number is given by the graph property `unnamed_records`). The names as given in the datasets ("verbatim" names, before their interpretation) are also not included.
+
+The networks have the following properties:
+
+- `name`: the name of the taxon.
+- `taxon_id`: the identifier of the taxon in a name authority, e.g. `COL:...`, `NCBI:9606`, `GBIF:...` (`no:match` if the name could not be linked; that of the first record of the taxon, if several were used).
+- `rank`: the taxonomic rank of the taxon, e.g. `species`, `genus` (empty if not known).
+- `path`: the taxonomic hierarchy of the taxon, from the root down, separated by ` | ` (empty if not known).
+- `kingdom`, `phylum`, `class`, `order`, `family`, `genus`: the names of the higher taxa of the taxon (empty if not known); they follow the name authority used, e.g. `Animalia` or `Metazoa` for the kingdom.
+- Edge `interaction`: the type of the interaction, as named by GloBI (OBO Relation Ontology), e.g. `eats`, `parasiteOf`, `pollinates`, `interactsWith`.
+- Edge `dataset`: the dataset (GitHub repository or other source indexed by GloBI) the record comes from, e.g. `globalbioticinteractions/inaturalist`.
+- Edge `latitude`, `longitude`: the coordinates of the observation (NaN if not given).
+- Edge `time`: the date of the observation, in Unix time (NaN if not given; some datasets give dates that are obviously wrong, e.g. in the year 5, which are kept as they are).
+- `unnamed_records` (graph): the number of records left out, since their source or target has no name."""
+tags = ['Biological', 'Food web', 'Unweighted', 'Multigraph', 'Timestamps', 'Metadata']
+url = ['https://www.globalbioticinteractions.org/',
+       'https://doi.org/10.5281/zenodo.22691479']
+citation = [('J. H. Poelen, J. D. Simons, C. J. Mungall, "Global biotic interactions: An open infrastructure to share and analyze species-interaction datasets." Ecological Informatics 24, 148-159 (2014)',
+             'https://doi.org/10.1016/j.ecoinf.2014.08.005')]
+icon_hash = None
+upstream_license = ("CC0 1.0 (the records come from datasets with their own citations)",
+                    "https://creativecommons.org/publicdomain/zero/1.0/")
+# Zenodo record of release 0.11 (2026-09-10) of the interpreted data products
+upstream_prefix = 'https://zenodo.org/records/22691479/files'
+files = [("interactions.tsv.gz", "interactions", None),
+         ("refuted-interactions.tsv.gz", "refuted", None)]
+
+def fetch_upstream(force=False):
+    return fetch_upstream_files(__name__.split(".")[-1], upstream_prefix, files,
+                                force)
+
+# For checks only: if set, only (about) this many records are read
+_check_rows = None
+
+TAXON = ["TaxonId", "TaxonName", "TaxonRank", "TaxonPathNames",
+         "TaxonKingdomName", "TaxonPhylumName", "TaxonClassName",
+         "TaxonOrderName", "TaxonFamilyName", "TaxonGenusName"]
+VPROPS = ["taxon_id", "name", "rank", "path", "kingdom", "phylum", "class",
+          "order", "family", "genus"]
+EDGE = ["interactionTypeName", "sourceNamespace", "decimalLatitude",
+        "decimalLongitude", "eventDate"]
+
+class Codes(dict):
+    "Integer codes of strings."
+    def __call__(self, s):
+        c = self.get(s)
+        if c is None:
+            c = self[s] = len(self)
+        return c
+    def labels(self):
+        return [s for s, c in sorted(self.items(), key=lambda x: x[1])]
+
+def categorical_ep(g, codes, labels, name):
+    "Store the integer codes `codes` of `labels` as the string property `name`."
+    c = g.new_ep("int", vals=codes)
+    p = g.new_ep("string")
+    map_property_values(c, p, lambda i: labels[i])
+    g.ep[name] = p
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+    name = __name__.split(".")[-1]
+    cols = ([f"{s}{x}" for s in ("source", "target") for x in TAXON] + EDGE)
+    for fname, alt, fmt in files:
+        if alts is not None and alt not in alts:
+            continue
+        taxa = {}          # name -> vertex
+        vprops = []        # vertex -> tuple of properties
+        src, tgt, inter, data, lat, lon, time = ([] for i in range(7))
+        icodes, dcodes = Codes(), Codes()
+        n = unnamed = 0
+        with open_upstream_file(name, fname, "rb") as f:
+            reader = pyarrow.csv.open_csv(
+                f,
+                read_options=pyarrow.csv.ReadOptions(block_size=1 << 26),
+                parse_options=pyarrow.csv.ParseOptions(delimiter="\t",
+                                                       quote_char=False,
+                                                       double_quote=False,
+                                                       newlines_in_values=False),
+                convert_options=pyarrow.csv.ConvertOptions(
+                    include_columns=cols,
+                    column_types=dict({c: pyarrow.string() for c in cols},
+                                      decimalLatitude=pyarrow.float64(),
+                                      decimalLongitude=pyarrow.float64()),
+                    strings_can_be_null=False))
+            for batch in reader:
+                n += batch.num_rows
+                named = pc.and_(pc.not_equal(batch.column("sourceTaxonName"), ""),
+                                pc.not_equal(batch.column("targetTaxonName"), ""))
+                unnamed += batch.num_rows - pc.sum(named).as_py()
+                batch = batch.filter(named)
+                ends = []
+                for s in ("source", "target"):
+                    names = batch.column(f"{s}TaxonName")
+                    u = pc.unique(names)
+                    first = pc.index_in(u, value_set=names).to_numpy()
+                    props = [batch.column(f"{s}{x}").take(first).to_pylist()
+                             for x in TAXON]
+                    vs = numpy.empty(len(u), dtype="int64")
+                    for i, row in enumerate(zip(*props)):
+                        v = taxa.get(row[1])
+                        if v is None:
+                            v = taxa[row[1]] = len(vprops)
+                            vprops.append(row)
+                        vs[i] = v
+                    ends.append(vs[pc.index_in(names, value_set=u).to_numpy()])
+                src.append(ends[0])
+                tgt.append(ends[1])
+                for col, codes, out in (("interactionTypeName", icodes, inter),
+                                        ("sourceNamespace", dcodes, data)):
+                    d = batch.column(col).dictionary_encode()
+                    m = numpy.array([codes(x) for x in d.dictionary.to_pylist()],
+                                    dtype="int32")
+                    out.append(m[d.indices.to_numpy()])
+                lat.append(batch.column("decimalLatitude").to_numpy(zero_copy_only=False))
+                lon.append(batch.column("decimalLongitude").to_numpy(zero_copy_only=False))
+                # e.g. 2012-06-05T00:00:00Z
+                t = pc.strptime(batch.column("eventDate"), format="%Y-%m-%dT%H:%M:%SZ",
+                                unit="s", error_is_null=True)
+                time.append(pc.cast(t, pyarrow.int64()).to_numpy(zero_copy_only=False))
+                if _check_rows is not None and n >= _check_rows:
+                    break
+
+        g = Graph(directed=True)
+        g.add_vertex(len(vprops))
+        g.add_edge_list(numpy.column_stack([numpy.concatenate(src),
+                                            numpy.concatenate(tgt)]))
+        del src, tgt
+        categorical_ep(g, numpy.concatenate(inter), icodes.labels(), "interaction")
+        categorical_ep(g, numpy.concatenate(data), dcodes.labels(), "dataset")
+        del inter, data
+        g.ep.latitude = g.new_ep("double", vals=numpy.concatenate(lat))
+        g.ep.longitude = g.new_ep("double", vals=numpy.concatenate(lon))
+        g.ep.time = g.new_ep("double", vals=numpy.concatenate(time))
+        del lat, lon, time
+        for i, k in enumerate(VPROPS):
+            g.vp[k] = g.new_vp("string", vals=[p[i] for p in vprops])
+        g.gp.unnamed_records = g.new_gp("int64_t", val=unnamed)
+        yield alt, g