logo Netzschleuder network catalog, repository, and centrifuge

History of offsides_twosides

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] Add the date when each entry was added, and remove icon_hash by Tiago de Paula Peixoto beff7c9
The dates are those of the commits that first added the entries' files
(2020-08-11 for the entries of the initial commit); renamed entries keep
the date of their first name.

Changed: none
diff --git a/entries/offsides_twosides/__init__.py b/entries/offsides_twosides/__init__.py
index bf4a68f..11d9e1d 100644
--- a/entries/offsides_twosides/__init__.py
+++ b/entries/offsides_twosides/__init__.py
@@ -44,7 +44,7 @@ tags = ['Biological', 'Drug interactions', 'Public Health', 'Weighted', 'Multigr
 url = ['https://nsides.io/', 'https://tatonettilab-resources.s3.amazonaws.com/nsides/README.txt']
 citation = [('N. P. Tatonetti, P. P. Ye, R. Daneshjou, R. B. Altman, "Data-Driven Prediction of Drug Effects and Interactions." Science Translational Medicine 4, 125ra31 (2012)',
              'https://doi.org/10.1126/scitranslmed.3003377')]
-icon_hash = None
+date_added = '2026-10-31'
 upstream_license = None
 upstream_prefix = 'https://tatonettilab-resources.s3.amazonaws.com/nsides'
 files = [("OFFSIDES.csv.xz", "offsides", None),
[entries] Prefix the properties in the descriptions with (Node), (Edge) or (Graph) by Tiago de Paula Peixoto bc3dbbd
The lists of properties at the end of the descriptions marked edge
properties with "Edge" but left node properties unmarked, or grouped
them in "Nodes: `a`, ...; `b`, ..." items, so the kind of a property
was often unclear. Every item now starts with (Node), (Edge) or (Graph),
followed by the property names, a colon and the explanation, one item
per property or group of properties; qualifications (kinds of nodes,
networks) follow the names in parentheses. Properties that were only
named got a short explanation, and a few items that named the wrong
kind of property were corrected.
diff --git a/entries/offsides_twosides/__init__.py b/entries/offsides_twosides/__init__.py
index fd8d47d..bf4a68f 100644
--- a/entries/offsides_twosides/__init__.py
+++ b/entries/offsides_twosides/__init__.py
@@ -32,14 +32,14 @@ The drugs are RxNorm ingredients, and the adverse events are MedDRA terms (prefe
 
 The networks have the following properties:
 
-- `rxnorm_id`: the RxNorm identifier of the drug (-1 for adverse events).
-- `meddra_id` (`offsides`): the MedDRA identifier of the adverse event (-1 for drugs).
-- `name`: the name of the drug or of the adverse event.
-- `is_drug` (`offsides`): whether the node is a drug (otherwise it is an adverse event).
-- Edge `condition_meddra_id`, `condition_name` (`twosides`): the MedDRA identifier and name of the adverse event.
-- Edge `a`: the number of reports for the drug (or pair of drugs) that report the event; `b`: the number of its reports that do not report it; `c`: the number of reports of the matched control drugs that report the event; `d`: the number of reports of the control drugs that do not.
-- Edge `prr`: the proportional reporting ratio, $(a/(a+b))/(c/(c+d))$, and `prr_error` its error estimate (NaN if not given; the counts are -1 if not given).
-- Edge `mean_reporting_frequency`: the fraction of the reports of the drug (or pair) that report the event, $a/(a+b)$."""
+- (Node) `rxnorm_id`: the RxNorm identifier of the drug (-1 for adverse events).
+- (Node) `meddra_id` (`offsides`): the MedDRA identifier of the adverse event (-1 for drugs).
+- (Node) `name`: the name of the drug or of the adverse event.
+- (Node) `is_drug` (`offsides`): whether the node is a drug (otherwise it is an adverse event).
+- (Edge) `condition_meddra_id`, `condition_name` (`twosides`): the MedDRA identifier and name of the adverse event.
+- (Edge) `a`: the number of reports for the drug (or pair of drugs) that report the event; `b`: the number of its reports that do not report it; `c`: the number of reports of the matched control drugs that report the event; `d`: the number of reports of the control drugs that do not.
+- (Edge) `prr`: the proportional reporting ratio, $(a/(a+b))/(c/(c+d))$, and `prr_error` its error estimate (NaN if not given; the counts are -1 if not given).
+- (Edge) `mean_reporting_frequency`: the fraction of the reports of the drug (or pair) that report the event, $a/(a+b)$."""
 tags = ['Biological', 'Drug interactions', 'Public Health', 'Weighted', 'Multigraph', 'Metadata']
 url = ['https://nsides.io/', 'https://tatonettilab-resources.s3.amazonaws.com/nsides/README.txt']
 citation = [('N. P. Tatonetti, P. P. Ye, R. Daneshjou, R. B. Altman, "Data-Driven Prediction of Drug Effects and Interactions." Science Translational Medicine 4, 125ra31 (2012)',
[entries] New entry: offsides_twosides by Tiago de Paula Peixoto e0a2011
diff --git a/entries/offsides_twosides/__init__.py b/entries/offsides_twosides/__init__.py
new file mode 100644
index 0000000..fd8d47d
--- /dev/null
+++ b/entries/offsides_twosides/__init__.py
@@ -0,0 +1,177 @@
+#! /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 = "OFFSIDES and TWOSIDES drug side effects and interactions (FAERS)"
+description = r"""Networks of drug side effects and of side effects of drug combinations, from OFFSIDES and TWOSIDES of the Tatonetti lab (nSIDES project), mined from the reports of adverse events of the FDA Adverse Event Reporting System (FAERS). For each drug (or pair of drugs) and each adverse event, the proportional reporting ratio (PRR) compares the frequency of the event among the reports of the drug with its frequency among the reports of control drugs, which are matched by a propensity score model to correct for confounding (e.g. by indication or co-medication). OFFSIDES focuses on effects that are not listed on the drug label ("off-label" side effects). The files repeat their header line every 200,000 rows; these lines are skipped. The data are those of the files published in March 2024 (from the release of November 2019), which their authors describe as out of date.
+
+- `offsides`: bipartite network of drugs and adverse events (side effects), where an edge is a signal of the drug causing the event.
+- `twosides`: network of drugs, where an edge joins two drugs whose combination is associated with an adverse event, labeled by the event; two drugs are joined by one edge per event (so that the network is a multigraph).
+
+The drugs are RxNorm ingredients, and the adverse events are MedDRA terms (preferred terms).
+
+The networks have the following properties:
+
+- `rxnorm_id`: the RxNorm identifier of the drug (-1 for adverse events).
+- `meddra_id` (`offsides`): the MedDRA identifier of the adverse event (-1 for drugs).
+- `name`: the name of the drug or of the adverse event.
+- `is_drug` (`offsides`): whether the node is a drug (otherwise it is an adverse event).
+- Edge `condition_meddra_id`, `condition_name` (`twosides`): the MedDRA identifier and name of the adverse event.
+- Edge `a`: the number of reports for the drug (or pair of drugs) that report the event; `b`: the number of its reports that do not report it; `c`: the number of reports of the matched control drugs that report the event; `d`: the number of reports of the control drugs that do not.
+- Edge `prr`: the proportional reporting ratio, $(a/(a+b))/(c/(c+d))$, and `prr_error` its error estimate (NaN if not given; the counts are -1 if not given).
+- Edge `mean_reporting_frequency`: the fraction of the reports of the drug (or pair) that report the event, $a/(a+b)$."""
+tags = ['Biological', 'Drug interactions', 'Public Health', 'Weighted', 'Multigraph', 'Metadata']
+url = ['https://nsides.io/', 'https://tatonettilab-resources.s3.amazonaws.com/nsides/README.txt']
+citation = [('N. P. Tatonetti, P. P. Ye, R. Daneshjou, R. B. Altman, "Data-Driven Prediction of Drug Effects and Interactions." Science Translational Medicine 4, 125ra31 (2012)',
+             'https://doi.org/10.1126/scitranslmed.3003377')]
+icon_hash = None
+upstream_license = None
+upstream_prefix = 'https://tatonettilab-resources.s3.amazonaws.com/nsides'
+files = [("OFFSIDES.csv.xz", "offsides", None),
+         ("TWOSIDES.csv.xz", "twosides", 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 rows are read
+_check_rows = None
+
+COLUMNS = {
+    "offsides": ["drug_rxnorn_id", "drug_concept_name", "condition_meddra_id",
+                 "condition_concept_name", "A", "B", "C", "D", "PRR",
+                 "PRR_error", "mean_reporting_frequency"],
+    "twosides": ["drug_1_rxnorn_id", "drug_1_concept_name", "drug_2_rxnorm_id",
+                 "drug_2_concept_name", "condition_meddra_id",
+                 "condition_concept_name", "A", "B", "C", "D", "PRR",
+                 "PRR_error", "mean_reporting_frequency"]}
+NUMS = {"A": "a", "B": "b", "C": "c", "D": "d", "PRR": "prr",
+        "PRR_error": "prr_error",
+        "mean_reporting_frequency": "mean_reporting_frequency"}
+
+def convert(col, t):
+    """The string column converted to the type t, with the empty values as
+    -1 (integers) or NaN (floats)."""
+    col = pc.if_else(pc.equal(col, ""), pyarrow.scalar(None, pyarrow.string()), col)
+    col = col.cast(t)
+    return col.fill_null(-1 if t == pyarrow.int64() else numpy.nan)
+
+def read_batches(name, fname, cols):
+    """The rows of the file, in blocks. The files are concatenations of
+    pieces of 200,000 rows, each with its header line, which are skipped."""
+    types = {c: pyarrow.int64() for c in cols
+             if c.endswith("_id") or c in "ABCD"}
+    types.update({c: pyarrow.float64() for c in ("PRR", "PRR_error",
+                                                 "mean_reporting_frequency")})
+    n = 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),
+            convert_options=pyarrow.csv.ConvertOptions(
+                column_types={c: pyarrow.string() for c in cols}))
+        assert reader.schema.names == cols, reader.schema.names
+        for b in reader:
+            if _check_rows is not None and n >= _check_rows:
+                break
+            b = b.filter(pc.not_equal(b.column(cols[0]), cols[0]))
+            n += b.num_rows
+            yield pyarrow.RecordBatch.from_arrays(
+                [convert(b.column(c), types[c]) if c in types else b.column(c)
+                 for c in cols], names=cols)
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+    name = __name__.split(".")[-1]
+    for fname, alt, fmt in files:
+        if alts is not None and alt not in alts:
+            continue
+        cols = COLUMNS[alt]
+        ends = ([("drug_rxnorn_id", "drug_concept_name"),
+                 ("condition_meddra_id", "condition_concept_name")]
+                if alt == "offsides" else
+                [("drug_1_rxnorn_id", "drug_1_concept_name"),
+                 ("drug_2_rxnorm_id", "drug_2_concept_name")])
+        names = [{}, {}]
+        src, tgt = [], []
+        nums = {c: [] for c in NUMS}
+        cond, cond_names = [], {}
+        for b in read_batches(name, fname, cols):
+            for k, (ic, nc) in enumerate(ends):
+                ids = b.column(ic).to_numpy()
+                (src if k == 0 else tgt).append(ids)
+                u, first = numpy.unique(ids, return_index=True)
+                nm = b.column(nc).take(first).to_pylist()
+                for i, x in zip(u.tolist(), nm):
+                    names[k].setdefault(i, x)
+            for c in NUMS:
+                nums[c].append(b.column(c).to_numpy())
+            if alt == "twosides":
+                ids = b.column("condition_meddra_id").to_numpy()
+                cond.append(ids)
+                u, first = numpy.unique(ids, return_index=True)
+                for i, x in zip(u.tolist(),
+                                b.column("condition_concept_name").take(first).to_pylist()):
+                    cond_names.setdefault(i, x)
+        src = numpy.concatenate(src)
+        tgt = numpy.concatenate(tgt)
+        g = Graph(directed=False)
+        if alt == "offsides":
+            drugs = numpy.array(sorted(names[0]), dtype="int64")
+            conds = numpy.array(sorted(names[1]), dtype="int64")
+            g.add_vertex(len(drugs) + len(conds))
+            g.add_edge_list(numpy.column_stack(
+                [numpy.searchsorted(drugs, src),
+                 len(drugs) + numpy.searchsorted(conds, tgt)]))
+            g.vp.rxnorm_id = g.new_vp("int64_t", vals=numpy.concatenate([drugs, -numpy.ones(len(conds), dtype="int64")]))
+            g.vp.meddra_id = g.new_vp("int64_t", vals=numpy.concatenate([-numpy.ones(len(drugs), dtype="int64"), conds]))
+            g.vp.name = g.new_vp("string", vals=[names[0][i] for i in drugs.tolist()] +
+                                 [names[1][i] for i in conds.tolist()])
+            g.vp.is_drug = g.new_vp("bool", vals=[True] * len(drugs) +
+                                    [False] * len(conds))
+        else:
+            dnames = dict(names[1])
+            dnames.update(names[0])
+            drugs = numpy.array(sorted(dnames), dtype="int64")
+            g.add_vertex(len(drugs))
+            g.add_edge_list(numpy.column_stack([numpy.searchsorted(drugs, src),
+                                                numpy.searchsorted(drugs, tgt)]))
+            g.vp.rxnorm_id = g.new_vp("int64_t", vals=drugs)
+            g.vp.name = g.new_vp("string", vals=[dnames[i] for i in drugs.tolist()])
+            cond = numpy.concatenate(cond)
+            g.ep.condition_meddra_id = g.new_ep("int64_t", vals=cond)
+            codes = numpy.array(sorted(cond_names), dtype="int64")
+            labels = [cond_names[i] for i in codes.tolist()]
+            cp = g.new_ep("int", vals=numpy.searchsorted(codes, cond))
+            del cond
+            p = g.new_ep("string")
+            map_property_values(cp, p, lambda i: labels[i])
+            g.ep.condition_name = p
+            del cp
+        del src, tgt
+        for c, pn in NUMS.items():
+            x = numpy.concatenate(nums[c])
+            nums[c] = None
+            g.ep[pn] = g.new_ep("int64_t" if c in "ABCD" else "double", vals=x)
+        yield alt, g