logo Netzschleuder network catalog, repository, and centrifuge

History of austria_comorbidity

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/austria_comorbidity/__init__.py b/entries/austria_comorbidity/__init__.py
index f11161d..0693271 100644
--- a/entries/austria_comorbidity/__init__.py
+++ b/entries/austria_comorbidity/__init__.py
@@ -52,7 +52,7 @@ url = ['https://doi.org/10.6084/m9.figshare.27102553',
        'https://github.com/elmadervic/Comorbidity-Networks-From-Population-Wide-Health-Data']
 citation = [('E. Dervić, K. Ledebur, S. Thurner, P. Klimek, "Comorbidity Networks From Population-Wide Health Data: Aggregated Data of 8.9M Hospital Patients (1997–2014)." Scientific Data 12, 215 (2025)',
              'https://doi.org/10.1038/s41597-025-04508-9')]
-icon_hash = None
+date_added = '2026-11-03'
 upstream_license = ('CC BY 4.0', 'https://creativecommons.org/licenses/by/4.0/')
 
 LEVELS = {"icd": "ICD", "blocks": "Blocks", "chronic": "Chronic"}
[entries] New entry: austria_comorbidity by Tiago de Paula Peixoto 3e3032b
Fixes #20
diff --git a/entries/austria_comorbidity/__init__.py b/entries/austria_comorbidity/__init__.py
new file mode 100644
index 0000000..f11161d
--- /dev/null
+++ b/entries/austria_comorbidity/__init__.py
@@ -0,0 +1,239 @@
+#! /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/>.
+
+from .. import *
+import gzip
+import struct
+import xml.etree.ElementTree as ET
+import numpy
+
+title = "Comorbidity networks of Austrian hospital patients (2003-2014)"
+description = """Networks of comorbidity between diagnoses, from the hospital claims data of the whole Austrian population: all hospital stays between 1997 and 2014 (44.6 million stays of 9 million patients), from the database of the Austrian Ministry of Health, aggregated by Dervić, Ledebur, Thurner and Klimek. Each stay has a primary diagnosis and possibly secondary ones, coded with the ICD-10 classification. The study is limited to the stays between 2003 and 2014 of the patients who had no hospital stay between 1997 and 2002 (3,378,906 patients), and to the 1,080 three-digit ICD-10 codes between A00 and N99 (leaving out pregnancy, injuries, poisoning and other external causes).
+
+Nodes are diagnoses, at three levels of resolution: the three-digit ICD-10 codes (1,080 nodes; networks starting with `icd`), the blocks of the ICD-10 classification (131 nodes; `blocks`), and 46 chronic conditions (`chronic`). Patients are stratified by sex (`female`, `male`), by ten-year age group (from 0-9 to 70-79 years), and by two-year period (from 2003-2004 to 2013-2014). An undirected edge connects two diagnoses for which a two-by-two contingency table of the patients of a stratum was computed, counting the patients with both diagnoses, with only one of them, or with neither.
+
+There are two kinds of networks:
+
+- The comorbidity networks of the authors (e.g. `icd_female_age_30-39` or `icd_female_2003-2004`), for each sex and age group (pooling the periods) or each sex and period (pooling the age groups): an edge connects two diagnoses whose odds ratio, estimated with the Cochran-Mantel-Haenszel method over the strata (periods, or age groups), is larger than 1.5, with a $p$-value smaller than 0.05 and at least 100 patients with both diagnoses. The networks of chronic conditions of the age group 0-9 have no edges and are left out.
+- The unthresholded contingency tables, for every pair of diagnoses with at least one patient with both, in each stratum (e.g. `raw_icd_female_30-39_2003-2004`) and in all strata together (e.g. `raw_icd_female`, with the values of the strata as vectors).
+
+All networks of a level of resolution have all its diagnoses as nodes, including those without edges. The attributes of the nodes are taken from the GEXF file of the network `*_male_2003-2004` of each level (in the files of the three-digit codes of the female age groups, the codes and descriptions of 164 nodes are shifted with respect to their labels, e.g. the node H43 has those of F15). The positions, sizes and colors of the nodes in the GEXF files of the authors (the colors are the chapters of the ICD-10 classification) are left out, and so is the prevalence of each diagnosis per sex, age group and year, which the source also gives.
+
+The networks have the following properties:
+
+- (Node) `name`: the ICD-10 code (`icd`), the range of codes of the block (`blocks`), or the name of the chronic condition (`chronic`), as given.
+- (Node) `icd_code` (`icd`, `chronic`): the ICD-10 code, or the range of codes of the chronic condition.
+- (Node) `description` (`icd`): the description of the ICD-10 code, in German.
+- (Node) `short_description`, `long_description` (`icd`): the short and long descriptions of the ICD-10 code, in English.
+- (Node) `patients` (`raw_*`): the number of patients with the diagnosis, as given in the contingency tables (in the networks of all strata, a vector with the value of each stratum).
+- (Edge) `weight` (comorbidity networks): the odds ratio of the two diagnoses.
+- (Edge) `a` (`raw_*`): the number of patients with both diagnoses.
+- (Edge) `b` (`raw_*`): the number of patients with the diagnosis of the target node but not that of the source node.
+- (Edge) `c` (`raw_*`): the number of patients with the diagnosis of the source node but not that of the target node.
+- (Edge) `d` (`raw_*`): the number of patients with neither diagnosis.
+- (Graph) `strata` (networks of all strata): the strata (age group and period) of the entries of the vector properties, in order."""
+tags = ['Biological', 'Public Health', 'Weighted', 'Metadata']
+url = ['https://doi.org/10.6084/m9.figshare.27102553',
+       'https://github.com/elmadervic/Comorbidity-Networks-From-Population-Wide-Health-Data']
+citation = [('E. Dervić, K. Ledebur, S. Thurner, P. Klimek, "Comorbidity Networks From Population-Wide Health Data: Aggregated Data of 8.9M Hospital Patients (1997–2014)." Scientific Data 12, 215 (2025)',
+             'https://doi.org/10.1038/s41597-025-04508-9')]
+icon_hash = None
+upstream_license = ('CC BY 4.0', 'https://creativecommons.org/licenses/by/4.0/')
+
+LEVELS = {"icd": "ICD", "blocks": "Blocks", "chronic": "Chronic"}
+SEXES = {"female": "Female", "male": "Male"}
+AGES = ["0-9", "10-19", "20-29", "30-39", "40-49", "50-59", "60-69", "70-79"]
+PERIODS = [f"{y}-{y + 1}" for y in range(2003, 2015, 2)]
+
+# (the archive Data.zip of version 2 of the figshare item, by file id)
+upstream_prefix = 'https://ndownloader.figshare.com/files'
+ARCHIVE = "52015403:zip:Data"
+
+def _gexf(level, sex, kind, i):
+    label = i + 1 if kind == "Age" else PERIODS[i][:4]
+    return f"{ARCHIVE}/4.Graphs-gexffiles/Graph_{SEXES[sex]}_{LEVELS[level]}_{kind}_{label}.gexf"
+
+def _tables(level, sex):
+    return f"{ARCHIVE}/2.ContingencyTables/{LEVELS[level]}_ContingencyTables_{SEXES[sex]}_Final.rds"
+
+# The node attributes are taken from one GEXF file of the level (in the
+# files of the ICD codes of the female age groups, those of 164 nodes are
+# shifted, e.g. the node H43 has the code and descriptions of F15)
+def _nodes_file(level):
+    return _gexf(level, "male", "Year", 0)
+
+files = []
+for level in LEVELS:
+    for sex in SEXES:
+        for i, age in enumerate(AGES):
+            if level == "chronic" and i == 0:
+                continue        # (no edges)
+            files.append(((_gexf(level, sex, "Age", i), _nodes_file(level)),
+                          f"{level}_{sex}_age_{age}", None))
+        for i, period in enumerate(PERIODS):
+            files.append(((_gexf(level, sex, "Year", i), _nodes_file(level)),
+                          f"{level}_{sex}_{period}", None))
+for level in LEVELS:
+    for sex in SEXES:
+        files.append(((_tables(level, sex), _nodes_file(level)),
+                      f"raw_{level}_{sex}", None))
+        for age in AGES:
+            for period in PERIODS:
+                files.append(((_tables(level, sex), _nodes_file(level)),
+                              f"raw_{level}_{sex}_{age}_{period}", None))
+
+def fetch_upstream(force=False):
+    return fetch_upstream_files(__name__.split(".")[-1], upstream_prefix, files,
+                                force)
+
+# Limit of edges per network, for checks (None: all)
+_check_rows = None
+
+GEXF_NS = {"g": "http://www.gexf.net/1.3"}
+NODE_ATTRS = {"icd_code": "icd_code", "descr": "description",
+              "ShortDescription": "short_description",
+              "LongDescription": "long_description"}
+
+def read_gexf(f):
+    """Return the nodes (in the order of their ids, as a list of dicts of
+    attributes, with the label as "name") and the edges (with 0-based node
+    indices) of a GEXF file of the authors."""
+    root = ET.parse(f).getroot()
+    graph = root.find("g:graph", GEXF_NS)
+    titles = {a.get("id"): a.get("title")
+              for a in graph.findall("g:attributes/g:attribute", GEXF_NS)}
+    nodes = {}
+    for v in graph.findall("g:nodes/g:node", GEXF_NS):
+        attrs = {"name": v.get("label")}
+        for av in v.findall("g:attvalues/g:attvalue", GEXF_NS):
+            attrs[titles[av.get("for")]] = av.get("value")
+        nodes[int(v.get("id"))] = attrs
+    assert sorted(nodes) == list(range(1, len(nodes) + 1))
+    edges = [(int(e.get("source")) - 1, int(e.get("target")) - 1,
+              float(e.get("weight")))
+             for e in graph.findall("g:edges/g:edge", GEXF_NS)]
+    return [nodes[i] for i in range(1, len(nodes) + 1)], edges
+
+def read_tables(f):
+    """Read the contingency tables, an R array (saved with saveRDS()) of
+    dimensions (diagnosis, diagnosis, period, age group, cell), as a numpy
+    array indexed by (cell, age group, period, diagnosis j, diagnosis i)."""
+    data = gzip.decompress(f.read())
+    assert data[:2] == b"X\n"
+    pos = 14
+    if struct.unpack_from(">i", data, 2)[0] == 3:
+        pos += 4 + struct.unpack_from(">i", data, 14)[0]
+    flags, n = struct.unpack_from(">ii", data, pos)
+    assert flags & 0xff == 14      # a vector of doubles
+    a = numpy.frombuffer(data, ">f8", n, pos + 8)
+    N = int(round((n / (4 * len(AGES) * len(PERIODS))) ** .5))
+    a = a.reshape((4, len(AGES), len(PERIODS), N, N))
+    assert not numpy.isnan(a).any()
+    return a.astype("int64")
+
+def add_nodes(g, nodes):
+    g.add_vertex(len(nodes))
+    g.vp.name = g.new_vp("string", vals=[v["name"] for v in nodes])
+    for k, p in NODE_ATTRS.items():
+        if k in nodes[0]:
+            g.vp[p] = g.new_vp("string", vals=[v.get(k, "") for v in nodes])
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+    name = __name__.split(".")[-1]
+    tables = {}
+    for fnames, alt, fmt in files:
+        if alts is not None and alt not in alts:
+            continue
+        level = alt.split("_")[1] if alt.startswith("raw_") else alt.split("_")[0]
+        if not alt.startswith("raw_"):
+            gfile, nfile = fnames
+            with open_upstream_file(name, gfile, "rb") as f:
+                gnodes, edges = read_gexf(f)
+            with open_upstream_file(name, nfile, "rb") as f:
+                nodes, _ = read_gexf(f)
+            assert [v["name"] for v in gnodes] == [v["name"] for v in nodes]
+            # (every edge is listed in both directions, with the same weight)
+            w = {}
+            for s, t, x in edges:
+                k = (min(s, t), max(s, t))
+                assert w.setdefault(k, x) == x
+            g = Graph(directed=False)
+            add_nodes(g, nodes)
+            elist = [(s, t, x) for (s, t), x in sorted(w.items())]
+            if _check_rows is not None:
+                elist = elist[:_check_rows]
+            g.ep.weight = g.new_ep("double")
+            g.add_edge_list(elist, eprops=[g.ep.weight])
+            yield alt, g
+            continue
+
+        tfile, nfile = fnames
+        if tfile not in tables:
+            tables.clear()
+            with open_upstream_file(name, tfile, "rb") as f:
+                tables[tfile] = read_tables(f)
+        T = tables[tfile]
+        with open_upstream_file(name, nfile, "rb") as f:
+            nodes, _ = read_gexf(f)
+        N = len(nodes)
+        assert T.shape[-1] == N
+        # T[cell, age, period, j, i] is the cell of the table of diagnoses
+        # i and j (as rows and columns); edges (s, t) with s < t are taken
+        # from i = s, j = t.
+        iu = numpy.triu_indices(N, 1)
+        cells = T[..., iu[1], iu[0]]                    # (4, ages, periods, pairs)
+        diag = T[0][..., numpy.arange(N), numpy.arange(N)]  # (ages, periods, N)
+        g = Graph(directed=False)
+        add_nodes(g, nodes)
+        rest = alt[len(f"raw_{level}_"):].split("_")[1:]
+        if len(rest) == 0:
+            mask = (cells[0] > 0).any(axis=(0, 1))
+            if _check_rows is not None:
+                mask &= numpy.cumsum(mask) <= _check_rows
+            S = len(AGES) * len(PERIODS)
+            g.vp.patients = g.new_vp("vector<int64_t>")
+            for v in range(N):
+                g.vp.patients[v] = diag[:, :, v].reshape(S)
+            g.add_edge_list(numpy.array([iu[0][mask], iu[1][mask]]).T)
+            vals = cells[:, :, :, mask].reshape((4, S, -1))
+            for k, x in zip("abcd", vals):
+                p = g.new_ep("vector<int64_t>")
+                for e, row in zip(g.edges(), x.T):
+                    p[e] = row
+                g.ep[k] = p
+            g.gp.strata = g.new_gp("vector<string>",
+                                   val=[f"{a}, {p}" for a in AGES for p in PERIODS])
+        else:
+            age, period = rest
+            ia, ip = AGES.index(age), PERIODS.index(period)
+            c = cells[:, ia, ip]
+            mask = c[0] > 0
+            if _check_rows is not None:
+                mask &= numpy.cumsum(mask) <= _check_rows
+            g.vp.patients = g.new_vp("int64_t", vals=diag[ia, ip])
+            g.ep.a = g.new_ep("int64_t")
+            g.ep.b = g.new_ep("int64_t")
+            g.ep.c = g.new_ep("int64_t")
+            g.ep.d = g.new_ep("int64_t")
+            g.add_edge_list(numpy.array([iu[0][mask], iu[1][mask],
+                                         *c[:, mask]]).T,
+                            eprops=[g.ep.a, g.ep.b, g.ep.c, g.ep.d])
+        yield alt, g